《算法竞赛进阶指南》0x60 斜率优化DP

0x60 斜率优化DP

任务安排

题意:

n n n 个任务排成序列,将任务分批。执行第 i i i 个任务所需要时间 t i t_i ti。每批任务开始前,需要 s s s 时间,一批任务所需要的时间为 s s s 加上每个任务需要时间。同一批任务的完成时间为该批所有任务执行完成的时间。每个任务的代价为完成时刻与费用系数 c i c_i ci 的成绩。

询问最小总费用。

数据规模: 1 ≤ n ≤ 5000 , 1 ≤ t i , c i ≤ 100 , 0 ≤ s ≤ 50 1 \le n \le 5000, 1 \le t_i, c_i \le 100, 0 \le s \le 50 1n5000,1ti,ci100,0s50

解析:

设计状态。第一维是前 i i i 个任务,在状态转移时需要知道前边有多少分组,所以增加一维 j j j,表示是第 j j j 组任务。

所以 f i , j f_{i,j} fi,j 为前 i i i 个任务,分成 j j j 组的最小代价。

状态转移: f i , j = min ⁡ { f k , j − 1 + ( S × j + ∑ u = 1 i t i ) × ∑ u = k + 1 i c i } f_{i,j} = \min\Big\{f_{k,j-1}+(S\times j+\sum\limits_{u = 1}\limits^it_i) \times \sum\limits_{u = k+1}\limits^ic_i\Big\} fi,j=min{fk,j1+(S×j+u=1iti)×u=k+1ici}

用前缀和优化后时间复杂度为 O ( n 3 ) O(n^3) O(n3)

增加一维状态 j j j 是为了计算 s s s 对当前组的贡献。如果当前组为 [ l , r ] [l,r] [l,r] ,影响到的任务为 [ l , n ] [l,n] [l,n]。采用 费用提前计算 的思想,将对后续任务的代价加到当前状态上。

f i = min ⁡ { f j + s × ∑ k = j + 1 n c k + ∑ k = 1 i t k × ∑ k = j + 1 i c k } f_i = \min\Big\{f_j+s\times \sum\limits_{k = j+1}\limits^nc_k+\sum\limits_{k=1}\limits^{i}t_k\times \sum\limits_{k = j+1}\limits^{i}c_k\Big\} fi=min{fj+s×k=j+1nck+k=1itk×k=j+1ick}

前缀和优化: f i = min ⁡ { f j + s × ( s c n − s c j ) + s t i × ( s c i − s c j ) } f_i = \min\Big\{f_j+s\times(sc_n-sc_j)+st_i\times(sc_i-sc_j)\Big\} fi=min{fj+s×(scnscj)+sti×(sciscj)}

通过费用提前计算的思想简化状态。时间复杂度为 O ( n 2 ) O(n^2) O(n2)

代码:

#include
using namespace std;
typedef long long ll;
typedef double db;
#define fi first
#define se second
#define debug(x) cerr << #x << ": " << (x) << endl
#define rep(i, a, b) for(int i = (a); i <= (b); i++)
const int maxn = 1e5+10;
const int maxm = 1e5+10;
const int INF = 0x3f3f3f3f;
typedef pair<int, int> pii;

ll sumt[maxn], sumc[maxn], f[maxn];
ll n, s;

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	
	cin >> n >> s;
	for(int i = 1; i <= n; i++){
		cin >> sumt[i] >> sumc[i];
		sumt[i] += sumt[i-1];
		sumc[i] += sumc[i-1];
	}
	
	memset(f, INF, sizeof(f));
	f[0] = 0;
	
	for(int i = 1; i <= n; i++){
		for(int j = 0; j < i; j++){
			f[i] = min(f[i], f[j]+sumt[i] * (sumc[i]-sumc[j]) + s * (sumc[n]-sumc[j]));
		}
	}
	cout << f[n] << endl;
	return 0;
}


任务安排2

题意:

完全同上题。

数据规模: 1 ≤ n ≤ 3 × 1 0 5 , 1 ≤ t i , c i ≤ 512 , 0 ≤ s ≤ 512 1 \le n \le 3\times 10^5, 1 \le t_i, c_i \le 512, 0 \le s \le 512 1n3×105,1ti,ci512,0s512

解析:

状态转移方程: f i = min ⁡ { f j + s × ( s c n − s c j ) + s t i × ( s c i − s c j ) } f_i = \min\Big\{f_j+s\times(sc_n-sc_j)+st_i\times(sc_i-sc_j)\Big\} fi=min{fj+s×(scnscj)+sti×(sciscj)}

去掉min: f i = f j + s × ( s c n − s c j ) + s t i × ( s c i − s c j ) f_i = f_j+s\times(sc_n-sc_j)+st_i\times(sc_i-sc_j) fi=fj+s×(scnscj)+sti×(sciscj)

右侧的项分为三类:只与 i i i 有关,只与 j j j 有关,与 i , j i,j i,j 均有关。将同一类的放在一起: f i = ( s t i × s c i + s × s c n ) + f j − s c j × ( s + s t i ) f_i = (st_i\times sc_i+s\times sc_n)+f_j-sc_j\times (s+st_i) fi=(sti×sci+s×scn)+fjscj×(s+sti)

进行移项,形如 y = k x + b y = kx+b y=kx+b 的形式

f u n c ( i ) × f u n c ( j ) func(i)\times func(j) func(i)×func(j) 看成 k × x k \times x k×x f i f_i fi 的项出现在 b b b 中, f u n c ( j ) func(j) func(j) 的项在 y y y 中。

如果 x x x 的表达式单调递减,等式两边同乘-1,变为单调递增。

y = f j y = f_j y=fj x = s c j x = sc_j x=scj k = s + s t i k = s+st_i k=s+sti b = f i − ( s t i × s c i + s × s c n ) b = f_i-(st_i\times sc_i+s\times sc_n) b=fi(sti×sci+s×scn)

j 1 , j 2 ( j 1 ≤ j 2 ) j_1,j_2(j_1 \le j_2) j1,j2(j1j2) i i i 的两个合法决策点,且满足 j 2 j_2 j2 优于 j 1 j_1 j1

即: f j 1 − s c j 1 × ( s + s t i ) ≥ f j 2 − s c j 2 × ( s + s t i ) f_{j_1}-sc_{j_1}\times (s+st_i) \ge f_{j_2}-sc_{j_2}\times (s+st_i) fj1scj1×(s+sti)fj2scj2×(s+sti)

因为 s c sc sc 单调递增,所以 s + s t i ≥ f j 2 − f j 1 s c j 2 − s c j 1 s+st_i \ge \frac{f_{j_2}-f_{j_1}}{sc_{j_2}-sc_{j_1}} s+stiscj2scj1fj2fj1,即 k i ≥ Y j 2 − Y j 1 X j 2 − X j 1 k_i \ge \frac{Y_{j_2}-Y_{j_1}}{X_{j_2}-X_{j_1}} kiXj2Xj1Yj2Yj1

不等式右侧是 P ( j 2 ) P(j_2) P(j2) P ( j 1 ) P(j_1) P(j1) 两点的斜率。即决策点 j 2 j_2 j2 优于 j 1 j_1 j1 满足的条件。

设有 A , B , C A, B, C A,B,C 三点,满足 x a < x b < x c x_a < x_b < x_c xa<xb<xc,且 k 1 = k ( A , B ) , k 2 = k ( B , C ) k_1 = k(A, B),k_2 = k(B, C) k1=k(A,B),k2=k(B,C)

如果 k 1 > k 2 k1 > k2 k1>k2,可以证明, B B B 无论如何不会成为最优决策点,所以可以从候选决策点中删除。

所以,可以维护一个斜率递增下凸壳。

因为 x x x 是随 j j j 递增,所以下凸壳可以用单调队列维护。因为 k i k_i ki i i i 递增,所以具有决策单调性,及时将队首不够优秀的决策点出队。

时间复杂度为 O ( n ) O(n) O(n)

代码:

#include
using namespace std;
typedef long long ll;
typedef double db;
#define fi first
#define se second
#define debug(x) cerr << #x << ": " << (x) << endl
#define rep(i, a, b) for(int i = (a); i <= (b); i++)
const int maxn = 3e5+10;
const int maxm = 1e5+10;
const int INF = 0x3f3f3f3f;
const double eps = 1e-10;
typedef pair<int, int> pii;

#define x(a) (c[a])
#define y(a) (f[a])
#define k(a) (t[a]+s)
ll t[maxn], c[maxn], f[maxn];
ll q[maxn], hh = 1, tt = 0;
long double slope(int a, int b){
	long double x1 = (long double)x(a);
	long double y1 = (long double)y(a);
	
	long double x2 = (long double)x(b);
	long double y2 = (long double)y(b);
	
	if(fabs(x2-x1) < eps)
		return y2 > y1 ? INF : -INF;
	else
		return (y2-y1)/(x2-x1);
}
int n, s;
void init(){
	cin >> n >> s;
	int x, y;
	for(int i = 1; i <= n; i++){
		cin >> x >> y;
		t[i] = t[i-1] + x;
		c[i] = c[i-1] + y;
	}
}
void dp(){
	q[++tt] = 0;
	for(int i = 1; i <= n; i++){
		while(hh < tt && slope(q[hh], q[hh+1]) <= k(i)) 
			hh++;
		int p = q[hh];
		f[i] = f[p] + (c[i]-c[p]) * t[i] + (c[n]-c[p]) * s;
		while(hh < tt && slope(q[tt-1], q[tt]) >= slope(q[tt], i)) 
			tt--;
		q[++tt] = i;
	}
	cout << f[n] << endl;
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	
	cin >> n >> s;
	int x, y;
	for(int i = 1; i <= n; i++){
		cin >> t[i] >> c[i];
		t[i] += t[i-1];
		c[i] += c[i-1];
	}
	dp();
	return 0;
}


任务安排3

题意:

完全同上题。

数据规模: 1 ≤ n ≤ 3 × 1 0 5 , 0 ≤ s , c i ≤ 512 , − 512 ≤ t i ≤ 512 1 \le n \le 3\times 10^5, 0 \le s, c_i \le 512, -512 \le t_i \le 512 1n3×105,0s,ci512,512ti512

解析:

y = f j y = f_j y=fj x = s c j x = sc_j x=scj k = s + s t i k = s+st_i k=s+sti b = f i − ( s t i × s c i + s × s c n ) b = f_i-(st_i\times sc_i+s\times sc_n) b=fi(sti×sci+s×scn)

横坐标单调,斜率不单调。仍然可以用队列维护凸包,但最优决策点不知道在哪,不具有决策单调性,即不能让队首出队。寻找最优决策点时,在凸包上二分。

二分:在凸包上找到最优决策点 j j j,满足 k ( j − 1 , j ) ≤ k i < k ( j , j + 1 ) k(j-1, j) \le k_i < k(j, j+1) k(j1,j)ki<k(j,j+1)

代码:

#include
using namespace std;
typedef long long ll;
typedef double db;
#define fi first
#define se second
#define debug(x) cerr << #x << ": " << (x) << endl
#define rep(i, a, b) for(int i = (a); i <= (b); i++)
const int maxn = 3e5+10;
const int maxm = 1e5+10;
const double eps = 1e-10;
const int INF = 0x3f3f3f3f;

#define x(a) (c[a])
#define y(a) (f[a]) 
#define k(a) (t[a] + s)
using namespace std;
typedef long long LL;
const int N = 300005;
ll t[maxn], c[maxn], f[maxn];
ll q[maxn], hh = 1, tt = 0;
int n, s;

long double slope(int a, int b){
	long double x1 = (long double)x(a);
	long double y1 = (long double)y(a);
	
	long double x2 = (long double)x(b);
	long double y2 = (long double)y(b);
	
	if(fabs(x2-x1) < eps)
		return y2 > y1 ? INF : -INF;
	else
		return (y2-y1)/(x2-x1);
}

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	
    cin >> n >> s;
	int x, y;
	for(int i = 1; i <= n; i++){
		cin >> t[i] >> c[i];
		t[i] += t[i-1];
		c[i] += c[i-1];
	}
	
    q[++tt] = 0;
    for (int i = 1; i <= n; i++) {
        int l = hh, r = tt;
        while(l < r) {
            int mid = (l+r) >> 1;
            if(slope(q[mid], q[mid+1]) >= k(i)) 
				r = mid;
            else l = mid + 1;
        }
        
        int p = q[r];
        f[i] = f[p] + (c[i] - c[p]) * t[i] + (c[n] - c[p])* s;
        while(hh < tt && slope(q[tt-1], q[tt]) >= slope(q[tt], i)) 
			tt--;
        q[++tt] = i;
    }
    cout << f[n] << endl;
    return 0;
}



运输小猫

题意:

m m m 只猫, p p p 个饲养员, n n n 座山。第 i − 1 i-1 i1 座山与第 i i i 座山的距离为 d i d_i di。第 i i i 只猫在 h i h_i hi 山上玩,在 t i t_i ti 时间开始等饲养员。饲养员从第 1 座山出发走到第 n n n 座山,接正在等待的猫。使猫等待时间和最小。

解析:

对每只猫,求出饲养员出发的时间 a i a_i ai,恰好使这只猫不用等待。 a i = t i − ∑ k = 1 i d k a_i = t_i-\sum\limits_{k=1}\limits^id_k ai=tik=1idk

则如果饲养员在时刻 t t t 出发,接上猫 i i i,猫 i i i 的等待时间为 t − a i t-a_i tai

a a a 排序。饲养员带走的一定是连续的猫。

f i , j f_{i,j} fi,j 为前 i i i 个饲养员,带走前 j j j 只猫 的最小等待时间和。

饲养员 i i i 的出发时间一定为 a j a_j aj。如果 t < a j t < a_j t<aj,接不到猫;如果 t > a j t > a_j t>aj,可以更早出发使等待时间变短。

f i , j = min ⁡ { f i − 1 , k + ∑ u = k + 1 j ( a j − a u ) } f_{i,j} = \min\Big\{f_{i-1, k} + \sum\limits_{u=k+1}\limits^j(a_j-a_u) \Big\} fi,j=min{fi1,k+u=k+1j(ajau)}

前缀和优化: f i , j = min ⁡ { f i − 1 , k + a j × ( j − k ) − ( s j − s k ) } f_{i,j} = \min\Big\{f_{i-1, k} + a_j\times (j-k)-(s_j-s_k) \Big\} fi,j=min{fi1,k+aj×(jk)(sjsk)} s s s a a a 的前缀和)

整理一下方程: f i − 1 , k + s k = a j × k + f i , j − a j × j f_{i-1, k}+s_k = a_j\times k + f_{i,j} - a_j\times j fi1,k+sk=aj×k+fi,jaj×j

Y = f i − 1 , k + s k Y= f_{i-1, k}+s_k Y=fi1,k+sk X = k X = k X=k K = a j K = a_j K=aj B = f i , j − a j × j B = f_{i,j} - a_j\times j B=fi,jaj×j

因为按 a a a 升序排序,所以斜率单调。横坐标也单调。所以单调队列维护下凸壳,具有决策单调性。

代码:

#include
using namespace std;
typedef long long ll;
typedef double db;
#define fi first
#define se second
#define debug(x) cerr << #x << ": " << (x) << endl
#define rep(i, a, b) for(int i = (a); i <= (b); i++)
const int maxn = 1e5+10;
const int maxm = 1e5+10;
const double eps = 1e-10;
const ll INF = 0x3f3f3f3f3f3f3f3f;

ll f[110][maxn], s[maxn], a[maxn], d[maxn], g[maxn];
ll q[maxn], hh, tt;
int n, m, p;
long double slope(int a, int b){
	long double x1 = (long double)a;
	long double y1 = (long double)g[a];
	
	long double x2 = (long double)b;
	long double y2 = (long double)g[b];
	
	if(fabs(x2-x1) < eps)
		return y2 > y1 ? INF : -INF;
	else
		return (y2-y1)/(x2-x1);
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	
	cin >> n >> m >> p;
	for(int i = 2, x; i <= n; i++){
		cin >> x;
		d[i] = d[i-1] + x;
	} 
	for(int i = 1, x, k; i <= m; i++){
		cin >> x >> k;
		a[i] = k-d[x];
	}
	sort(a+1, a+1+m);
	for(int i = 1; i <= m; i++)
		s[i] = s[i-1] + a[i];
	memset(f, 0x3f,sizeof(f));
	f[0][0] = 0;
	
	for(int i = 1; i <= p; i++){
		for(int j = 1; j <= m; j++)
			g[j] = f[i-1][j]+s[j];
			
		q[1] = 0; 
		hh = tt = 1;
		for(int j = 1; j <= m; j++){
			while(hh < tt && slope(q[hh], q[hh+1]) <= a[j]) 
				hh++;
			f[i][j] = min(f[i-1][j], g[q[hh]]+a[j]*(j-q[hh])-s[j]);
			if(g[j] >= INF) 
				continue;
			while(hh < tt && slope(q[tt-1], q[tt]) >= slope(q[tt], j)) 
				tt--;
			q[++tt] = j;
		}
	}
	cout << f[p][m] << endl;
	return 0;
}


你可能感兴趣的:(算法竞赛进阶指南,动态规划,算法,c++)