斜率优化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需要舍弃,即对于决策iji优于j;对于决策jkk优于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]

View Code
//#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就没用了。。。

  

View Code
/#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就要出队列。。。。

   

 

  

 

 

 

 

 

 

 

 

 

posted @ 2013-01-21 18:45  AC_Von  阅读(1235)  评论(0编辑  收藏  举报