史上最详尽的斜率优化!

最近被钦定要写教材,负责斜率优化那一块,就把写的内容搬了些上来。

3.6.1 斜率优化DP的基本思想

考虑这样一个问题:现在要给n个数a[1],a[2]…a[n]分组,每分出一组,你的代价为该组所有数的和的平方+一个常数M(即设你分出的一组数字是2,3,4,M=5,则你分出该组的代价为(2+3+4)^2+5),求出一种分组方式使得代价总和最小,输出这个最小的代价和。

 

如果是最基础的dp,能够很显然地列出来:设dp[i]为将1~i分组的最小代价和,s[i]为a[1]+a[2]..+a[i]的和。dp[i]=Min(f[j]+(s[i]-s[j])^2+M),(0<=j<i)。但是这样dp的时间复杂度是O(n^2)的,如果n很大(比如说n=100000),这样的时间复杂度我们是接受不了的,我们需要寻求优化。

 

我们想,在转移时我们要花费大量时间枚举j,这很不划算,我们能否用O(1)或者O(logn)的时间寻找到所有转移中最优的那个j呢?

我们假设在求解dp[i]的时候,有j,k(j>k)使得从j转移比从k转移更优,那么需要满足条件:

dp[j]+(s[i]s[j])^2+M<dp[k]+(s[i]s[k])^2+M

展开上式,移项并消去同类项可得:

(dp[j]dp[k]+s[j]^2s[k]^2)/(S[j]S[k])<2*s[i]

我们设f[j]=dp[j]+s[j]^2,f[k]同理,则可得:(f[j]f[k])/(s[j]s[k])<2*s[i]

也就是说当j>k时,若有上式,则用j更新dp[i]比用k更新dp[i]优。通过这样我们已经能够比较两个点在转移i的时候的优劣性了。

 

现在我们令g(j,k)=(f[j]-f[k])/(s[j]-s[k]),上式即转换为若g(j,k)<2*s[i],则j比k优,反之k比j优。有一个结论是:

设x为大于i的一个点,如果g(j,k)>g(i,j)(k<j<i),则无论如何j都不可能再成为一个决策点了(也就是说对于以后每个dp[x],都不可能由dp[j]转移而来)。

我们来尝试证明它:

g(j,k)和g(i,j)和2*s[x]有三种关系,即g(j,k)>g(i,j)>2*s[x],g(j,k)> 2*s[x]>g(i,j),2*s[x]>g(j,k)>g(i,j),我们分情况讨论:对于第一种关系,j比i优,但k比j优,所以j不可能成为最优决策点。对于第二种关系,i比j优,k比j优,所以j不可能成为最优决策点。对于第三种关系,i比j优,j比k优,所以j不可能成为最优决策点。 综上所述,我们可以断言:只要存在g(j,k)>g(i,j),(k<j<i),那么j无论如何都不可能再成为最优决策点。

我们来想一想怎么运用这个性质来优化dp。我们建立一个队列d表示可能的决策点集合,每次我们算出了dp[i]的值,便可以知道f[i]的值了。我们将i加入这个集合,根据上述的性质,我们可以用i删去这个集合中的某些点。具体来说,就是删去所有g(j,k)>g(i,j)的j。那我们难道要枚举每个j,k把它们尝试删除吗?这样太慢了,我们再来想办法。

我们每次这样维护,可以知道将队列d中的点映射到平面上(以s为横座标,以f为纵座标),它们依次的连线是下凸的(因为g(j,k)的本质就是斜率),如图所示:

 

有着这样一个性质,每次我们在队列末尾插入一个点i,就只需要检验在队列中i,与i前面一个位置j,与j前面一个位置k(这里所说的位置都是指在队列中的位置),是否是呈下凸的,如果不呈,则将j删去后继续检验,直到呈下凸为止(这个用while循环可以实现)。

这样,我们就得到一个由所有可能决策点所组成的队列d,如果我们现在要求dp[i],我们应该由队列d中的哪个来转移过来呢?

我们先回想一下开始得到的结论:对于k<j<i,如果g(j,k)<2*s[i],则j比k优,反之k比j优。

         我们现在知道集合d是下凸的,也就是g的值是逐渐递增的,我们需要寻找一个最大的g(j,k),使得g(j,k)<2*s[i],那么这个j就是最优的决策点。证明:因为g(j,k)<2*s[i],所以对于i来说j比k优,因为g递增,所以对于k后一个位置l,g(k,l)<g(j,k)<2*s[i],所以k比l优,以此类推对于i来说,j应为最优的决策点。因为g的递增性,寻找最大的g(j,k)<2*s[i],可以用二分法,总的时间复杂度就可以优化成O(nlogn),这是一般的斜率优化的解法。但注意到这题有个性质,就是s[i]也是递增的,这样我们就可以像单调队列那样来找到最优决策点,而不用二分,这时的时间复杂度就是O(n)的了。

         那么我们关于斜率优化DP的思想的学习到这里基本就结束了,我们现在来帮助大家总结一下整个过程,希望以此来帮助大家彻底掌握这个知识点。

         1.先列出一个最基础的dp;如果这个dp不涉及最小最大值转移,或对转移有较多复杂的限制条件,则无法用斜率优化。

2.令k<j<i,考虑对于i来说j比k优的情况,列出式子,并转换为一个形如(f[j]-f[k])/(s[j]-s[k])<(或>)s[i]的式子(这个大于小于号是根据题目是求最小值还是最大值确定的)。如果转化不了说明这个dp不能用斜率优化。

3.得到结论当g(j,k)<(或>)g(i,j)时,j不再可能是一个最优决策点。

4.对于可能的决策点队列d,根据结论维护上凸或下凸,即每次加入一个点就删去一些点。

5.在队列d中,二分查找一个斜率小于s[i]且斜率最大的点(或斜率大于s[i]且斜率最小的点,看不等式符号决定),作为dp[i]的转移点。特殊地,如果s[i]单调递增递减,还可以用单调队列维护。

 

 

3.6.2 斜率优化DP的应用

例3.6-1 hdu 3507 Print Article:

【参考程序】

#include<cstdio>

#define N 100010

int a[N],d[N],s[N],dp[N]; //字母的定义与上文相同

int y(int x,int y)   //将相邻点映射到平面上的纵座标之差

{

         return dp[y]-dp[x]+s[y]*s[y]-s[x]*s[x];

}

int x(int x,int y)  //将相邻点映射到平面上的横座标之差

{

         return s[y]-s[x];

}

int main()

{

         int n,M;

         scanf("%d%d",&n,&M);

       for (int i=1;i<=n;i++)

         {

                scanf("%d",&a[i]);

                s[i]=s[i-1]+a[i];

         }

         int l=0; int r=0;

         for (int i=1;i<=n;i++)

         {

                   int k=2*s[i];

                   while (l<r && y(d[l],d[l+1])<=k*x(d[l],d[l+1])) l++; //单调队列,找出斜率小于k的最大值(为了保证精度这里用乘法代替除法)

                   dp[i]=dp[d[l]]+x(d[l],i)*x(d[l],i)+M;

                   while (l<r && y(d[r-1],d[r])*x(d[r],i)>=y(d[r],i)*x(d[r-1],d[r])) r--; //每加入一个点,为维护上凸或下凸的性质要删去一些点

                   r++; d[r]=i;

         }

         printf("%d\n",dp[n]);

         return 0;

}

 

例3.6-2:[APIO2010] 特别行动队

【题目描述】

  你有一支由n名预备役士兵组成的部队,士兵从1到n编号,要将他们拆分成若干特别行动队调入战场。出于默契考虑,同一支特别行动队中队员的编号应该连续,即为形如(i,i+1,…,i+k)的序列。

  编号为i的士兵的初始战斗力为xi,一支特别运动队的初始战斗力x为队内士兵初始战斗力之和,即x=(xi)+(xi+1)+…+(xi+k)。

  通过长期的观察,你总结出一支特别行动队的初始战斗力x将按如下经验公式修正为x’:x’=ax^2+bx+c,其中a,b,c是已知的系数(a<0)。

  作为部队统帅,现在你要为这支部队进行编队,使得所有特别行动队修正后战斗力之和最大。试求出这个最大和。

  例如,你有4名士兵,x1=2,x2=2,x3=3,x4=4。经验公式中的参数为a=-1,b=10,c=-20。此时,最佳方案是将士兵组成3个特别行动队:第一队包含士兵1和士兵2,第二队包含士兵3,第三队包含士兵4。特别行动队的初始战斗力分别为4,3,4,修正后的战斗力分别为4,1,4。修正后的战斗力和为9,没有其它方案能使修正后的战斗力和更大。

【输入格式】

输入由三行组成。第一行包含一个整数n,表示士兵的总数。第二行包含三个整数a,b,c,经验公式中各项的系数。第三行包含n个用空格分隔的整数x1,x2,…,xn,分别表示编号为1,2,…,n的士兵的初始战斗力。

【输出格式】

输出一个整数,表示所有特别行动队修正战斗力之和的最大值。

【样例输入】

4

-1 10 -20

2 2 3 4

【样例输出】

9

【数据范围】

20%的数据中,n<=1000;

50%的数据中,n<=10000;

100%的数据中,1<=n<=1000000,-5<=a<=-1,b<=10000000,|c|<=10000000,1<=xi<=100。

 

【问题分析】

容易写出dp方程:dp[i]=max(dp[j]+a*(s[i]−s[j])^2+b*(s[i]−s[j])+c)。考虑斜率优化。我们回忆一下上文的步骤,令k<j<i,考虑对于i来说j比k优的情况,列出式子dp[j]+a*(s[i]−s[j])^2+b*(s[i]−s[j])+c> dp[k]+a*(s[i]−s[k])^2+b*(s[i]−s[k])+c,令f[x]=dp[x]+a*s[x]^2,则可得(f[j]-f[k])/(s[j]-s[k])>2*a*s[i]+b。令g(j,k)=(f[j]-f[k])/(s[j]-s[k]),则若g(j,k)>2*a*s[i]+b,则对于i来说j比k优,反之相反。可得结论若有g(j,k)<g(i,j),(其中k<j<i),则j不再可能成为一个决策点。我们维护可能的决策点队列d,根据结论可知将s作为横座标,f作为纵座标,映射到平面上,是要维护一个上凸的图形(斜率要递减)。每次加入一个点到队列d中,为了维护上凸的性质就会删去一些点。然后我们在队列d中,用单调队列维护一个斜率大于2*a*s[i]+b且斜率最小的点,作为dp[i]的转移点。

 

【参考程序】

#include<cstdio>

long long s[1000010],p[1000010];

long long f[1000010];

int d[1000010];

int main()

{

         int n;

         scanf("%d",&n);

         long long a,b,c;

         scanf("%lld%lld%lld",&a,&b,&c);

         int x;

         for (int i=1;i<=n;i++) scanf("%d",&x),s[i]=s[i-1]+x;//s是前缀和

         f[0]=0;

         int head=1; int tail=1; d[1]=0;

         for (int i=1;i<=n;i++)

         {

                   while (head<tail && p[d[head+1]]-p[d[head]]>(2*a*s[i]+b)*(s[d[head+1]]-s[d[head]])) head++;//单调队列找出转移点

                   int j=d[head];

                   f[i]=f[j]+a*(s[i]-s[j])*(s[i]-s[j])+b*(s[i]-s[j])+c;//这里的f相当于分析中的dp数组

                   p[i]=f[i]+a*s[i]*s[i];//这里的p相当于分析中的f数组

                   while (tail>head && (p[d[tail]]-p[d[tail-1]])*(s[i]-s[d[tail]])<(p[i]-p[d[tail]])*(s[d[tail]]-s[d[tail-1]])) tail--;//为维护上凸,将一些点删去

                   d[++tail]=i;

         }

         printf("%lld\n",f[n]);

         return 0;

}

 

3.6.3 斜率优化习题推荐

1.[HNOI2008] 玩具装箱

【题目描述】

  P教授要去看奥运,但是他舍不下他的玩具,于是他决定把所有的玩具运到北京。他使用自己的压缩器进行压缩,其可以将任意物品变成一堆,再放到一种特殊的一维容器中。P教授有编号为1…N的N件玩具,第i件玩具经过压缩后变成一维长度为C[i].为了方便整理,P教授要求在一个一维容器中的玩具编号是连续的。同时如果一个一维容器中有多个玩具,那么两件玩具之间要加入一个单位长度的填充物,形式地说如果将第i件玩具到第j个玩具放到一个容器中,那么该容器的长度将为 x=j-i+C[k],(i<=K<=j)

  制作容器的费用与容器的长度有关,根据教授研究,如果容器长度为x,其制作费用为(x-L)^2.其中L是一个常量。P教授不关心容器的数目,他可以制作出任意长度的容器,甚至超过L。但他希望费用最小.

【输入格式】

第一行输入两个整数N,L,第二行输入所有C[i],1<=N<=50000,1<=L,C[i]<=10^7

【输出格式】

一行,输出最小费用

【样例输入】

5 4

3 4 2 1 4

【样例输出】

1

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章