现在的位置: 首页 > 算法 > 正文

dp 斜率优化 poj1180(转) 反过来dp 经典

2019年04月14日 算法 ⁄ 共 2751字 ⁄ 字号 评论关闭

N个任务排成一个序列在一台机器上等待完成(顺序不得改变),这N个任务被分成若干批,每批包含相邻的若干任务。 从时刻0开始,这些任务被分批加工,第i个任务单独完成所需的时间是Ti。在每批任务开始前,机器需要启动时间S,而完成这批任务所需的时间是各个任务需 要时间的总和(同一批任务将在同一时刻完成)。每个任务的费用是它的完成时刻乘以一个费用系数Fi。请确定一个分组方案,使得总费用最小。(1 <= N <= 10000)

题目分析:
        此道题目有个提示,即任务的顺序不得改变,那么很显然,任务的调度安排是具有阶段性的,我们可以用DP来解决此问题,这个DP方程不难写出,由于本题的DP方向有正推和倒推两种,我们选择倒推(至于为什么后面会讲解)
首先设:sumT[i]表示从i到n的任务所需要的时间总和, sumF[i]表示从i到n的费用系数总和,dp[i]表示对于从i到n的任务安排的最优解。
那么很容易可以得出这样一个简单的DP状态转移方程:     (注: 数组存储从1到n)
                    
                dp[i] = min{dp[j] + (S + sumT[i] - sumT[j]) * sumF[i]        { i < j <= n + 1 }  
                                                                                                      边界条件 dp[n+1] = 0
                                                                                                                      sumT[n+1]=sumF[n+1]=0
         
         可以清楚的看到,此种DP方法的时间复杂度是O(n^2)的,那么如何降低复杂度呢?

经典的优化分析:
         我们考虑在计算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]

这个类似于斜率的东西又怎么来优化?
为了证明和更好的说明接下来的优化,我们定义这么几个变量:
设:   
                  d[i, x] = dp[x] + (S + sumT[i] - sumT[x]) * sumF[i] ;                 
                  g[j, k] = (dp[j] - dp[k]) / (sumT[j] - sumT[k])

那么我们可以总结一下上面推出来的式子:
根据上面有:
                               i<j<k
同理可证:             d[i, j] < d[i, k]    <=>   g[j, k] < sumF[i]         决策j比决策k小

进而我们发现这么一个问题,当i < j < k < l时,如果有g[j, k] < g[k, l],那么k永远都不会成为计算dp[i]时的决策点。

证明:
如果g[j, k] < g[k, l],那么我们可以分两个方面考虑g[j, k]与sumF[i]的关系:
(1)如果g[k, l] >= sumF[i],那么决策k大于等于决策l,也就说决策k不可能是决策点
(2)如果g[j, k] < sumF[i],那么决策k要大于决策j,所以k也不可能是决策点

根据上面的结论和一些特性,我们可以考虑维护一个斜率的双向队列来优化整个DP过程:

(1)假设i(马上要入队的元素) <j< k依次是队列尾部的元素,那么我们就要考虑g[i, j]是否大于g[j, k],如果g[i, j] < g[j, k],那么可以肯定j一定不会是决策点,所以我们可以从队列中将j去掉,然后依次向前推,直到找到一个队列元素少于3个或者g[i j] >= g[j, k]的点才停止。
(2)假设a>b(a是头元素)是依次是队列头部的元素,那么我们知道,如果g[b, a] < sumF[i]的话,那么对于i来说决策点b肯定优于决策点a,又由于sumF[i]是随着i减少而递增的(这个就是为什么倒推的原因),所以当g[a, b] < sumF[i]时,就一定有g[a, b] < sumF[i-1],因此当前的决策点a不仅仅在考虑dp[i]时不会是最佳决策点,而且在后面的DP中也一定不会是最佳决策点,所以我们可以把a从队列 的头部删除,依次往后如此操作,直到队列元素小于2或者g[b, a] >=
sumF[i]。
(3)对于i的更新,一定是队列头部的决策点最好,所以O(1)即可转移。

复杂度分析: 由于任何一个点只能进出队列一次,所有均摊的复杂度为O(n)。

//coded by huicpc0207
#include<iostream>
#include<stdio.h>
#include<math.h>
#include<string.h>
#include<string>
#include<vector>
#include<stack>
#include<queue>
#include<algorithm>
#include<set>
#include<map>
using namespace std;
typedef long long LL;
const int N=10050;
int n,s,t[N],f[N],id[N];
LL opt[N],sf[N],st[N];
inline LL dx(int k1,int k2){ return st[k1]-st[k2]; }
inline LL dy(int k1,int k2){ return opt[k1]-opt[k2]; }
inline bool G(int k1,int k2,int i)
{
    return dy(k1,k2)>sf[i]*dx(k1,k2);
}
inline bool inc(int k1,int k2,int k3)
{
    return dy(k1,k2)*dx(k2,k3)<dy(k2,k3)*dx(k1,k2);
}
int main()
{
    while(scanf("%d%d",&n,&s)!=-1)
    {
        for(int i=n;i>=1;i--)
            scanf("%d%d",&t[i],&f[i]);
        st[0]=sf[0]=0;
        for(int i=1;i<=n;i++)
        {
            st[i]=st[i-1]+t[i];
            sf[i]=sf[i-1]+f[i];
        }
        int l=0,r=-1;
        id[++r]=0; opt[0]=0;
        for(int i=1;i<=n;i++)
        {
            while(l<r&&G(id[l],id[l+1],i)) l++;
            int k=id[l];
            opt[i]=opt[k]+(s+st[i]-st[k])*sf[i];
            while(l<r&&!inc(id[r-1],id[r],i)) r--;
            id[++r]=i;
        }
        printf("%I64d\n",opt[n]);
    }
}

抱歉!评论已关闭.