斜率优化DP(POJ1180 && POJ3709)
斜率优化其实就是把每个状态看上直角坐标系上离散的点抽象出x,y 表示斜率 (y2 - y1) / (x2 - x1) 于一个关系状态i个函数的关系,然后维护点见斜率的上凸性或者下凸性。具体的情况要看于i有关的函数的单调性。 斜率优化dp基本都是和单调队列结合使用的。
POJ 1180这题,状态转移方程开始只想到正向推O(n^2)的复杂度,但是正向推有两个不确定量,不能优化。后来看解题报告才知道可以从后往前推。sumT[i]表示从i到n的任务所需要的时间总和,sumF[i]表示从i到n的费用系数总和,dp[i]表示对于从i到n的任务安排的最优解:
dp[i]=min(dp[j]+(sunT[i]-sumT[j]+s)*sumF[i]) (1<=i<=n+1;i<j<=n+1)
我们考虑在计算dp[i]时,对于i < j < k来说, 如果保证决策k比决策j大的条件是:
dp[j] + (S + sumT[i] - sumT[j]) * sumF[i] < dp[k] + (S + sumT[i] -sumT[k]) * sumF[i]
通过移项整理,可以化简为:
(dp[j] - dp[k]) / (sumT[j] - sumT[k]) < sumF[i]
可知当我们计算dp[i]时,若(dp[j] - dp[k]) / (sumT[j] - sumT[k]) >=sumF[i]时我们可以舍弃j(决策K优于决策J);
因此我们可以用一个单调队列,对于元素i需要入对时,(i<j<k),我们如何维护呢,不妨设函数Q(j,k)=(dp[j] - dp[k]) / (sumT[j] - sumT[k]);
因为i需要入对,我们需要讨论的即是对于决策j,我们是否需要保留,(下面我们来讨论J需要舍弃的条件);
如果j需要舍弃,即对于决策i,j,i优于j;对于决策j,k,k优于j;故此我们有Q(i,j)<sumF[i],sumF[i]<=Q(j,k);
即推出 Qi,j)<Q(j,k);
综上:可以考虑维护一个斜率的队列来优化整个DP过程:
(1)假设i(马上要入队的元素)<j< k依次是队列尾部的元素,那么我们就要考虑Q(i,j)是否大于Q(j,k),
如果Q(i,j) < Q(j,k),那么可以肯定j一定不会是决策点,可以从队列中将j去掉,依次向前推,
直到找到一个队列元素少于2个或者Q(i,j)>= Q(j,k)的点才停止。
(2)假设k>j(k是头元素)是依次是队列头部的元素,如果g(j,k) < sumF[i]的话,
那么对于i来说决策点j肯定优于决策点k,又由于sumF[i]是随着i减少而递增的,
所以当Q(j,k) < sumF[i]时,就一定有Q(j,k) < sumF[i-1],因此当前的决策点k不仅仅在考虑dp[i]时不会是最佳决策点,
而且在后面的DP中也一定不会是最佳决策点,所以我们可以把k从队列 的头部删除,依次往后如此操作,
直到队列元素小于2或者Q(j,k)>= sumF[i]。

//#pragma comment(linker,"/STACK:327680000,327680000") #include <iostream> #include <cstdio> #include <cmath> #include <vector> #include <cstring> #include <algorithm> #include <string> #include <set> #include <functional> #include <numeric> #include <sstream> #include <stack> #include <map> #include <queue> #define CL(arr, val) memset(arr, val, sizeof(arr)) #define REP(i, n) for((i) = 0; (i) < (n); ++(i)) #define FOR(i, l, h) for((i) = (l); (i) <= (h); ++(i)) #define FORD(i, h, l) for((i) = (h); (i) >= (l); --(i)) #define L(x) (x) << 1 #define R(x) (x) << 1 | 1 #define MID(l, r) (l + r) >> 1 #define Min(x, y) (x) < (y) ? (x) : (y) #define Max(x, y) (x) < (y) ? (y) : (x) #define E(x) (1 << (x)) #define iabs(x) (x) < 0 ? -(x) : (x) #define OUT(x) printf("%I64d\n", x) #define Read() freopen("data.in", "r", stdin) #define Write() freopen("data.out", "w", stdout); typedef long long LL; const double eps = 1e-8; const double PI = acos(-1.0); const int inf = ~0u>>2; using namespace std; const int N = 10010; int q[N], t[N], f[N], dp[N], st[N], sf[N]; bool cmp_head(int i, int k, int j) { return (dp[j] - dp[k]) - sf[i]*(st[j] - st[k]) < 0; } bool cmp_tail(int j, int k, int l) { return (dp[j] - dp[k])*(st[k] - st[l]) < (dp[k] - dp[l])*(st[j] - st[k]); } int main() { //Read(); int n, s, i; while(~scanf("%d", &n)) { scanf("%d", &s); for(i = 1; i <= n; ++i) { scanf("%d%d", t + i, f + i); } st[n+1] = st[n+1] = 0; for(i = n; i >= 1; --i) { st[i] = st[i+1] + t[i]; sf[i] = sf[i+1] + f[i]; dp[i] = inf; } int head = 0, tail = -1; q[++tail] = n + 1; dp[n+1] = 0; for(i = n; i >= 1; --i) { while(tail > head && cmp_head(i, q[head], q[head+1])) head++; dp[i] = min(dp[i], dp[q[head]] + (s + st[i] - st[q[head]])*sf[i]); while(tail > head && cmp_tail(i, q[tail], q[tail-1])) tail--; q[++tail] = i; } printf("%d\n", dp[1]); } return 0; }
POJ 3709
O(n^2)的DP方程:f[i]=Min{f[j]+sum[i]-sum[j]-a[j+1]*(i-j)}。
斜率优化:
假设决策j1<j2并且j2优于(或者不差于)j1,那么
f[j1]+sum[i]-sum[j1]+a[j1+1]*(i-j1) >= f[j2]+sum[i]-sum[j2]-a[j2+1]*(i-j2) --------------- 不等式(1)
化简得:[(f[j1]-sum[j1]+a[j1+1]*j1) - (f[j2]-sum[j2]+a[j2+1])] >= i*(a[j1+1]-a[j2+1])。
因为有序序列嘛,a[j2+1]>=a[j1+1], 所以a[j1+1]-a[j2+1] <= 0。
所以也可以写成:[(f[j1]-sum[j1]+a[j1+1]*j1) - (f[j2]-sum[j2]+a[j2+1])] / (a[j1+1]-a[j2+1]) <= i。
对于a[j1+1]==a[j2+1]的情况,不能用除法了,只能用乘法那个表达式,
所以,如果对于决策j1,j2满足上述表达式,则j2 优于 j1。
但是如果不满足上述表达式呢,如果不满足那么j2就一定比j1差吗??NO!!因为如果j1,j2不变,不等式(1)左边是个常数,不会变;而a[j1+1]-a[j2+1]<= 0,所以随着i的增加不等式右边是会变小的(或者不变),如果变小的话,那么有可能在某一个i位置不等式会成立,也就是说在以后较大的某个i,j2会优于j1。
OK,然后我们看看怎样用单调队列结合这两个性质来解这个问题。
令dy(j1, j2) = (f[j1]-sum[j1]+a[j1+1]*j1) - (f[j2]-sum[j2]+a[j2+1]), dx(j1,j2) = a[j1+1]-a[j2+1]。
首先刚开始队首元素为0——很明显~~
然后假设队列首尾指针head < tail 并且dy(queue[head],queue[head+1]) >=i*dx(queue[head],queue[head+1]),那么队首元素直接丢掉就可以了。因为i是递增的,如果当前queue[head]没有queue[head+1]好,那么今后也不会。
队尾的操作要稍微难理解一点,不是那么直观:因为对于队尾的2个原素x, y来说,如果对于当前i,y比x要烂,那么由前面的证明:对于比较大的i,y不一定就比x烂,有可能比x好呢。那么对这种情况看来不好处理,但是我们来看看队尾3个元素的情况:x,y,z,如果dy(x,y)/dx(x,y)>=dy(y,z)/dx(y,z),那么可以直接把y给删了。因为dy(x,y)/dx(x,y)和dy(y,z)/dx(y,z)是个常数,对于某个i,如果dy(x,y)/dx(x,y)<=i的话,那么dy(y,z)/dx(y,z)一定也小于等于i,也就是说:如果y优于x,那么z一定优于y,这个时候留着y就没用了。。。

/#pragma comment(linker,"/STACK:327680000,327680000") #include <iostream> #include <cstdio> #include <cmath> #include <vector> #include <cstring> #include <algorithm> #include <string> #include <set> #include <functional> #include <numeric> #include <sstream> #include <stack> #include <map> #include <queue> #define CL(arr, val) memset(arr, val, sizeof(arr)) #define REP(i, n) for((i) = 0; (i) < (n); ++(i)) #define FOR(i, l, h) for((i) = (l); (i) <= (h); ++(i)) #define FORD(i, h, l) for((i) = (h); (i) >= (l); --(i)) #define L(x) (x) << 1 #define R(x) (x) << 1 | 1 #define MID(l, r) (l + r) >> 1 #define Min(x, y) (x) < (y) ? (x) : (y) #define Max(x, y) (x) < (y) ? (y) : (x) #define E(x) (1 << (x)) #define iabs(x) (x) < 0 ? -(x) : (x) #define OUT(x) printf("%I64d\n", x) #define Read() freopen("data.in", "r", stdin) #define Write() freopen("data.out", "w", stdout); typedef long long LL; const double eps = 1e-8; const double PI = acos(-1.0); const int inf = ~0u>>2; using namespace std; const int N = 500010; LL a[N], q[N], dp[N], sum[N]; LL dy(int j, int k) { return dp[j] - sum[j] + a[j+1]*j - (dp[k] - sum[k] + a[k+1]*k); } LL dx(int j, int k) { return a[j+1] - a[k+1]; } int main() { //Read(); int T, n, k, i, j, x, y, z; scanf("%d", &T); while(T--) { scanf("%d%d", &n, &k); sum[0] = 0; for(i = 1; i <= n; ++i) { scanf("%lld", a + i); sum[i] = sum[i-1] + a[i]; dp[i] = inf; } dp[0] = 0; int head = 0, tail = -1; q[++tail] = 0; for(i = 1; i <= n; ++i) { while(head < tail && dy(q[head], q[head+1]) >= i*dx(q[head], q[head+1])) head++; j = q[head]; dp[i] = dp[j] + sum[i] - sum[j] - a[j+1]*(i - j); z = i - k + 1; if(z >= k) { while(head < tail) { x = q[tail-1]; y = q[tail]; if(dy(x, y)*dx(y, z) >= dy(y, z)*dx(x, y)) tail--; else break; } q[++tail] = z; } } printf("%lld\n", dp[n]); } return 0; }
做完这两道题,说一下感受。斜率优化,既然是斜率,就是要表示成dy/dx的形式,设g(x, y) < F表示x优于y。对于单调队列的队尾来说,设要插入的元素为z,队尾元素为y,队尾前一个元素为x,如果g(y, z) < g(x, y) z则y可定不是最优的,删掉,知道出现g(y, z) >= g(x, y)则把z插到队尾。对于对头,放的肯定是当前状态转移时的最优策略。假设队头元素为k,队头的下一个元素为j,如果g(j, k) < F则j优于k,这时候k就要出队列。。。。