HDU2829 浅谈四边形优化DP

来源:互联网 发布:单机养宠物软件 编辑:程序博客网 时间:2024/06/05 22:28

这里写图片描述
世界真的很大
在动态规划中,经常遇到形如下式的状态转移方程:
dp(i,j)=min{dp(i,k-1) + dp(k,j)} + w(i,j) (i≤k≤j)
dp(i,j)表示区间[i,j]上的最优值。w(i,j)表示在转移时需要额外付出的代价。
正如这道题:hdu2829(题目就很有意思):
劳伦斯在第一次世界大战期间是一个有争议的人物。他是一名英国军官,曾在阿拉伯战区服役,并带领一批阿拉伯国民在游击队对奥斯曼帝国进行游击队。 他的主要目标是铁路。 在大片电影“阿拉伯的劳伦斯”中提出了一个高度虚构的版本。
你要写一个程序来帮助劳伦斯找出如何最有效地利用他有限的资源。 你有一些来自英国情报的信息。 首先,铁路线是完全线性的 - 没有分支,没有马刺。 接下来,英国情报部门向每个仓库分配了一个战略重要性 - 整数从1到100.一个仓库自己没有用,只有连接到其他仓库才有价值。 整个铁路的战略价值是通过将铁路线直接或间接连接的每对车站的战略价值的产物相加来计算的。 考虑这条铁路:

这里写图片描述
其战略价值为4 * 5 + 4 * 1 + 4 * 2 + 5 * 1 + 5 * 2 + 1 * 2 = 49。
现在,假设劳伦斯只有足够的资源进行一次攻击。他不能自己攻击仓库 - 他们太捍卫了。他必须攻击在沙漠中间的车库之间的铁路线。考虑如果劳伦斯在中间直接攻击这条铁路线,会发生什么:
这里写图片描述
剩下的铁路的战略价值是4 * 5 + 1 * 2 = 22。但是,假设劳伦斯攻击4和5个仓库:
这里写图片描述
剩下的铁路的战略价值是5 * 1 + 5 * 2 + 1 * 2 = 17。这是劳伦斯最好的选择。
考虑到铁路的描述和劳伦斯可以执行的攻击次数,就可以确定他能为该铁路实现的最小的战略价值。

我很辛苦的翻译了一遍;
很显然,这道题的转移方程的时间复杂度为O(N3)。
因此,我们需要通过四边形不等式来优化方程。

首先介绍什么是“区间包含的单调性”和“四边形不等式”
区间包含的单调性:
如果对于 i≤i’<j≤j’,有 w(i’,j)≤w(i,j’),那么说明w具有区间包含的单调性。
可以形象理解为如果小区间包含于大区间中,那么小区间的w值不超过大区间的w值。
四边形不等式:
如果对于 i ≤i’< j≤j’,有 w(i,j)+w(i’,j’)≤w(i’,j)+w(i,j’),我们称函数w满足四边形不等式。
可以形象理解为两个交错区间的w的和不超过小区间与大区间的w的和。
下面给出两个定理:
1、如果上述的 w 函数同时满足区间包含单调性和四边形不等式性质,那么函数dp也满足四边形不等式性质。
我们再定义 s(i,j) 表示 dp(i,j) 取得最优值时对应的下标(即 i≤k≤j 时,k 处的 dp 值最大,则 s(i,j)=k)。此时有如下定理
2、假如dp(i,j)满足四边形不等式,那么s(i,j)单调,即s(i,j)≤s(i,j+1)≤s(i+1,j+1)。
这两个定理证明在赵爽的《动态规划加速原理之四边形不等式》中给出了相关的证明。
有了上述的两个定理后,我们发现如果w函数满足区间包含单调性和四边形不等式性质
那么有 s(i,j-1)≤s(i,j)≤s(i+1,j) 。
即原来的状态转移方程可以改写为下式:
dp(i,j)=min{dp(i,k-1) + dp(k,j)} + w(i,j) (s(i,j-1)≤k≤s(i+1,j))
区间的长度最多有n个,对于固定的长度 L,不同的状态也有 n 个
故时间复杂度为 O(N^2)。
今后只需要根据方程的形式以及 w 函数是否满足两条性质即可考虑使用四边形不等式来优化了。

在这道题里,四边形优化就可以用了,首先我们预处理出这道题的状态转移方程:
f(i,j)=min{f(i,k-1) + f(k,j)} + w(i,j) (i≤k≤j)
里的w数组(f(i,j)表示前i个点分成j段的最小值)。
w(i,j)表示i到j分成一段,里面各个数相互的乘积,这里其实有一个递推式:
对于w(i,j),w(i,j-1)已经处理好了,将第j个位置加入到w(i,j-1)里,就是w(i,j),就是加上i到j-1的总和乘以j的值,代码:

for(int i=1;i<=n;i++)            for(int j=i+1;j<=n;j++)                w[i][j]=w[i][j-1]+a[j]*(sum[j-1]-sum[i-1]);

然后由w数组对f数组的边界值进行预处理,f(i,1)就是前i个分成一段的最小值就是w(1,i),注意f(i,i)的值是需要赋为0的,因为可以把每个点都分成一段,这样都是0了。
还有就是对这个s数组预处理边界值,预处理的时候其实没那么麻烦,直接把左边界赋为0,右边界赋为n,就是说第一次还是需要从0到n跑一边,但之后就不需要了,预处理代码:

for(int i=1;i<=n;i++)            f[i][1]=w[1][i],f[i][i]=0,s[i][1]=0;

然后开始dp,注意这里和传统的四边形有所不同,最外层的j枚举的并不是区间长度而是分段段数,所以并不是处理f(i,j)时所有比i,j短的s都处理好了,而是所有s(p,q),q比j小的都处理好了,又因为:
s(i-1,j)≤s(i,j)≤s(i+1,j)里面当第二位为j(未处理)时,有一个i+1,如果从小到大s(i+1,j)就没有预先处理好,所以要从大到小枚举,这很重要!!!代码:

for(int j=1;j<=m+1;j++)        {            s[n+1][j]=n;            for(int i=n;i>j;i--)                for(int k=s[i][j-1];k<=s[i+1][j];k++)                if(f[k][j-1]+w[k+1][i]<f[i][j])                {                    f[i][j]=f[k][j-1]+w[k+1][i];                    s[i][j]=k;//处理出最优项k                }        }

完整代码:

#include<stdio.h>#include<cstring>using namespace std;typedef long long dnt; int m,n;dnt sum[1010],a[1010],w[1010][1010],s[1010][1010];dnt f[1010][1010];int main(){    while(true)    {        scanf("%d%d",&n,&m);        if(n==0) break ;        for(int i=1;i<=n;i++)        {            scanf("%I64d",&a[i]);            sum[i]=a[i]+sum[i-1];        }        memset(w,0,sizeof(w));        memset(f,127,sizeof(f));        for(int i=1;i<=n;i++)            for(int j=i+1;j<=n;j++)                w[i][j]=w[i][j-1]+a[j]*(sum[j-1]-sum[i-1]);        for(int i=1;i<=n;i++)            f[i][1]=w[1][i],f[i][i]=0,s[i][1]=0;        for(int j=1;j<=m+1;j++)        {            s[n+1][j]=n;            for(int i=n;i>j;i--)                for(int k=s[i][j-1];k<=s[i+1][j];k++)                if(f[k][j-1]+w[k+1][i]<f[i][j])                {                    f[i][j]=f[k][j-1]+w[k+1][i];                    s[i][j]=k;                }        }        printf("%I64d\n",f[n][m+1]);    }    return 0;}

嗯,就是这样。