《算法竞赛进阶指南》0x60 斜率优化DP
0x60 斜率优化DP
任务安排
题意:
nnn 个任务排成序列,将任务分批。执行第 iii 个任务所需要时间 tit_iti。每批任务开始前,需要 sss 时间,一批任务所需要的时间为 sss 加上每个任务需要时间。同一批任务的完成时间为该批所有任务执行完成的时间。每个任务的代价为完成时刻与费用系数 cic_ici 的成绩。
询问最小总费用。
数据规模:1≤n≤5000,1≤ti,ci≤100,0≤s≤501 \\le n \\le 5000, 1 \\le t_i, c_i \\le 100, 0 \\le s \\le 501≤n≤5000,1≤ti,ci≤100,0≤s≤50
解析:
设计状态。第一维是前 iii 个任务,在状态转移时需要知道前边有多少分组,所以增加一维 jjj,表示是第 jjj 组任务。
所以 fi,jf_{i,j}fi,j 为前 iii 个任务,分成 jjj 组的最小代价。
状态转移:fi,j=min{fk,j−1+(S×j+∑u=1iti)×∑u=k+1ici}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,j−1+(S×j+u=1∑iti)×u=k+1∑ici}
用前缀和优化后时间复杂度为 O(n3)O(n^3)O(n3)。
增加一维状态 jjj 是为了计算 sss 对当前组的贡献。如果当前组为 [l,r][l,r][l,r] ,影响到的任务为 [l,n][l,n][l,n]。采用 费用提前计算 的思想,将对后续任务的代价加到当前状态上。
fi=min{fj+s×∑k=j+1nck+∑k=1itk×∑k=j+1ick}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+1∑nck+k=1∑itk×k=j+1∑ick}
前缀和优化:fi=min{fj+s×(scn−scj)+sti×(sci−scj)}f_i = \\min\\Big\\{f_j+s\\times(sc_n-sc_j)+st_i\\times(sc_i-sc_j)\\Big\\}fi=min{fj+s×(scn−scj)+sti×(sci−scj)}
通过费用提前计算的思想简化状态。时间复杂度为 O(n2)O(n^2)O(n2)
代码:
#include<bits/stdc++.h>
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×105,1≤ti,ci≤512,0≤s≤5121 \\le n \\le 3\\times 10^5, 1 \\le t_i, c_i \\le 512, 0 \\le s \\le 5121≤n≤3×105,1≤ti,ci≤512,0≤s≤512
解析:
状态转移方程:fi=min{fj+s×(scn−scj)+sti×(sci−scj)}f_i = \\min\\Big\\{f_j+s\\times(sc_n-sc_j)+st_i\\times(sc_i-sc_j)\\Big\\}fi=min{fj+s×(scn−scj)+sti×(sci−scj)}
去掉min:fi=fj+s×(scn−scj)+sti×(sci−scj)f_i = f_j+s\\times(sc_n-sc_j)+st_i\\times(sc_i-sc_j)fi=fj+s×(scn−scj)+sti×(sci−scj)
右侧的项分为三类:只与 iii 有关,只与 jjj 有关,与 i,ji,ji,j 均有关。将同一类的放在一起:fi=(sti×sci+s×scn)+fj−scj×(s+sti)f_i = (st_i\\times sc_i+s\\times sc_n)+f_j-sc_j\\times (s+st_i)fi=(sti×sci+s×scn)+fj−scj×(s+sti)。
进行移项,形如 y=kx+by = kx+by=kx+b 的形式
将func(i)×func(j)func(i)\\times func(j)func(i)×func(j) 看成 k×xk \\times xk×x ,fif_ifi 的项出现在 bbb 中,func(j)func(j)func(j) 的项在 yyy 中。
如果 xxx 的表达式单调递减,等式两边同乘-1,变为单调递增。
y=fjy = f_jy=fj,x=scjx = sc_jx=scj,k=s+stik = s+st_ik=s+sti,b=fi−(sti×sci+s×scn)b = f_i-(st_i\\times sc_i+s\\times sc_n)b=fi−(sti×sci+s×scn)。
设 j1,j2(j1≤j2)j_1,j_2(j_1 \\le j_2)j1,j2(j1≤j2) 是 iii 的两个合法决策点,且满足 j2j_2j2 优于 j1j_1j1。
即:fj1−scj1×(s+sti)≥fj2−scj2×(s+sti)f_{j_1}-sc_{j_1}\\times (s+st_i) \\ge f_{j_2}-sc_{j_2}\\times (s+st_i)fj1−scj1×(s+sti)≥fj2−scj2×(s+sti)
因为 scscsc 单调递增,所以 s+sti≥fj2−fj1scj2−scj1s+st_i \\ge \\frac{f_{j_2}-f_{j_1}}{sc_{j_2}-sc_{j_1}}s+sti≥scj2−scj1fj2−fj1,即 ki≥Yj2−Yj1Xj2−Xj1k_i \\ge \\frac{Y_{j_2}-Y_{j_1}}{X_{j_2}-X_{j_1}}ki≥Xj2−Xj1Yj2−Yj1。
不等式右侧是 P(j2)P(j_2)P(j2) 和 P(j1)P(j_1)P(j1) 两点的斜率。即决策点 j2j_2j2 优于 j1j_1j1 满足的条件。
设有 A,B,CA, B, CA,B,C 三点,满足 xa<xb<xcx_a < x_b < x_cxa<xb<xc,且 k1=k(A,B),k2=k(B,C)k_1 = k(A, B),k_2 = k(B, C)k1=k(A,B),k2=k(B,C)。
如果 k1>k2k1 > k2k1>k2,可以证明,BBB 无论如何不会成为最优决策点,所以可以从候选决策点中删除。
所以,可以维护一个斜率递增下凸壳。
因为 xxx 是随 jjj 递增,所以下凸壳可以用单调队列维护。因为 kik_iki 随 iii 递增,所以具有决策单调性,及时将队首不够优秀的决策点出队。
时间复杂度为 O(n)O(n)O(n)
代码:
#include<bits/stdc++.h>
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;elsereturn (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×105,0≤s,ci≤512,−512≤ti≤5121 \\le n \\le 3\\times 10^5, 0 \\le s, c_i \\le 512, -512 \\le t_i \\le 5121≤n≤3×105,0≤s,ci≤512,−512≤ti≤512
解析:
y=fjy = f_jy=fj,x=scjx = sc_jx=scj,k=s+stik = s+st_ik=s+sti,b=fi−(sti×sci+s×scn)b = f_i-(st_i\\times sc_i+s\\times sc_n)b=fi−(sti×sci+s×scn)
横坐标单调,斜率不单调。仍然可以用队列维护凸包,但最优决策点不知道在哪,不具有决策单调性,即不能让队首出队。寻找最优决策点时,在凸包上二分。
二分:在凸包上找到最优决策点 jjj,满足 k(j−1,j)≤ki<k(j,j+1)k(j-1, j) \\le k_i < k(j, j+1)k(j−1,j)≤ki<k(j,j+1)
代码:
#include<bits/stdc++.h>
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;elsereturn (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;
}
运输小猫
题意:
mmm 只猫,ppp 个饲养员,nnn 座山。第 i−1i-1i−1 座山与第 iii 座山的距离为 did_idi。第 iii 只猫在 hih_ihi 山上玩,在 tit_iti 时间开始等饲养员。饲养员从第 1 座山出发走到第 nnn 座山,接正在等待的猫。使猫等待时间和最小。
解析:
对每只猫,求出饲养员出发的时间 aia_iai,恰好使这只猫不用等待。ai=ti−∑k=1idka_i = t_i-\\sum\\limits_{k=1}\\limits^id_kai=ti−k=1∑idk。
则如果饲养员在时刻 ttt 出发,接上猫 iii,猫 iii 的等待时间为 t−ait-a_it−ai。
按 aaa 排序。饲养员带走的一定是连续的猫。
令 fi,jf_{i,j}fi,j 为前 iii 个饲养员,带走前 jjj 只猫 的最小等待时间和。
饲养员 iii 的出发时间一定为 aja_jaj。如果 t<ajt < a_jt<aj,接不到猫;如果 t>ajt > a_jt>aj,可以更早出发使等待时间变短。
fi,j=min{fi−1,k+∑u=k+1j(aj−au)}f_{i,j} = \\min\\Big\\{f_{i-1, k} + \\sum\\limits_{u=k+1}\\limits^j(a_j-a_u) \\Big\\}fi,j=min{fi−1,k+u=k+1∑j(aj−au)}
前缀和优化:fi,j=min{fi−1,k+aj×(j−k)−(sj−sk)}f_{i,j} = \\min\\Big\\{f_{i-1, k} + a_j\\times (j-k)-(s_j-s_k) \\Big\\}fi,j=min{fi−1,k+aj×(j−k)−(sj−sk)} (sss 是 aaa 的前缀和)
整理一下方程:fi−1,k+sk=aj×k+fi,j−aj×jf_{i-1, k}+s_k = a_j\\times k + f_{i,j} - a_j\\times jfi−1,k+sk=aj×k+fi,j−aj×j
Y=fi−1,k+skY= f_{i-1, k}+s_kY=fi−1,k+sk,X=kX = kX=k,K=ajK = a_jK=aj,B=fi,j−aj×jB = f_{i,j} - a_j\\times jB=fi,j−aj×j。
因为按 aaa 升序排序,所以斜率单调。横坐标也单调。所以单调队列维护下凸壳,具有决策单调性。
代码:
#include<bits/stdc++.h>
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;elsereturn (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;
}