其实关于数形结合的这种思想我一直不太明白
最近学了一下关于斜率优化方面的知识,才慢慢地理解了这种思想方法
使用条件
关于动态规划
如果方程形如:
$ F[i]=min(a[i]f[j]+b[i]c[j]+d[i]) $
就是在方程中有关于i的常数和关于j的常数的乘积。这时候使用斜率优化最恰当。
主要思想
数形结合!!
我们观察式子:
我们要使得决策点j优于决策点k
那就是
$ a[i]f[j]+b[i]c[j]+d[i]<a[i]f[k]+b[i]c[j]+d[i] $
$ a[i]f[j]+b[i]c[j]<a[i]f[k]+b[i]c[k] $
$ a[i]f[j]-a[i]f[k]<b[i]c[k]-b[i]c[j] $
$ a[i](f[j]-f[k])<b[i](c[k]-c[j]) $
$ \frac{f[j]-f[k]}{c[k]-c[j]}<\frac{b[i]}{a[i]} $
我们把关于j,k的放在不等式左边,然后把常数(已知的)都放右边
这样左边的式子就是一个斜率一样的东西
即,j点的坐标为($ f[j] $,$ -c[j] $)
然后我们可以把整个式子化成一个一次函数的形式:
$ f[i]=a[i]f[j]+b[i]c[j]+d[i] $
其中我们知道了j的坐标,所以:
$ a[i]f[j]=b[i](-c[j])+f[i]-d[i] $
$ f[j]=\frac{b[i]}{a[i]}*(-c[j])+\frac{f[i]-d[i]}{a[i]} $
所以我们发现了这是一个$ y=kx+b $的形式
可以看成一条一次函数。
找决策点的任务是使得我们得出来的$f[i]$最小,所以,应使得上述解析式在j为一定值时的截距最小,只是我们可以想到维护一个下凸壳。
为什么要维护凸壳?
我们可以把每次查询的过程理解为一个一次函数,已经确定了斜率,现在我们在坐标系中有一堆散点,然后我们要把这个直线从下往上推上去,直到经过第一个决策点j为止,则在这时,直线的截距必定最小,这个j就是我们所说的最优决策点。
我们来看一下图:
如上图,我们在一堆散点中维护出一个下凸壳,在每次查询i时会有一个斜率,则这个斜率就可以在我们维护的下凸壳中找到一个最优决策点j,使得截距最小。
如何维护下凸壳?
维护一个下凸壳,我们可以使用一个队列(其实严格意义不能说是一个队列,因为它队首可以出,队尾可以进可以出)。
上面看起来很懵逼,我们来看一下每次要什么操作:
int h=1,t; d[t=1]=0; for (i=1;i<=n;++i){ while(h<t&&本次询问i的斜率>队首和队首后一个构成的直线的斜率) ++h; f[i]=a[i]*f[d[h]]+b[i]*c[j]+d[i]; while(t>h&&即将放入队列中的i点和队尾的斜率<=队尾和其前一个点构成的斜率) --t; d[++t]=i; }
其中,第二个while的斜率比较我用了<=,我也不知道为什么,在做HDU3507的时候,交到oj上时,不加这个=就错了...
上面代码仅限维护下凸壳,如果是上凸壳的话,注意一下不等号的方向。
斜率不单调?
有两种奇怪的情况:
1.询问点i所对应的斜率不单调
这种情况比较普通,我们就不能像上面那样,直接比较队首了,因为...显然啊。
那么我们可以直接在这个单调的凸壳上面二分,这样也可以保证有一个n log的时间。
2.决策点j的横坐标不单调
这个的话,因为作者能力有限,并不会,只知道可以用平衡树或者线段树之类的维护。
有兴趣的可以了解一下李超树。
注意
比较斜率的时候可能会有严重的精度问题,所以最好把等式两边斜率的分子分母交叉相乘来判断。更加重要的是,因为不等式两边同时乘或除以一个负数,不等号的方向会改变,所以我们要保证分子分母都是非负数再乘过去,不然会出很大的问题。
例题
上面提到的HDU3507就是道好题,大家有兴趣可以去看看。
#include<cstdio> #include<iostream> #include<cstring> using namespace std; const int maxn=5e5+10; int n,m; int c[maxn]; int f[maxn]; int sum[maxn]; int d[maxn*4]; int X(int x){ return sum[x]; } int Y(int x){ return f[x]+sum[x]*sum[x]; } int main(){ int i,j; while(scanf("%d%d",&n,&m)!=EOF){ for (i=1;i<=n;++i) scanf("%d",&c[i]),sum[i]=sum[i-1]+c[i]; memset(f,127,sizeof(f)),f[0]=0; int h=1,t; d[t=1]=0; for (i=1;i<=n;++i){ while(h<t&&2*sum[i]*(X(d[h+1])-X(d[h]))>Y(d[h+1])-Y(d[h])) ++h; f[i]=f[d[h]]+(sum[i]-sum[d[h]])*(sum[i]-sum[d[h]])+m; while(t>h&&(Y(d[t])-Y(i))*(X(d[t-1])-X(d[t]))<=(X(d[t])-X(i))*(Y(d[t-1])-Y(d[t]))) --t; d[++t]=i; } printf("%d\n",f[n]); } }
总结
我们来总结一下做这种题的一般步骤,加深理解:
1.首先列出dp方程。
2.然后像上面第一点一样先用j和k之间的决策优秀性来解出不等式,确定每个决策点的横纵坐标。
3.按照刚刚确定的横纵坐标来把原方程移项,拆成y=kx+b的形式(小技巧:观察,有关于i的常量和关于j的变量相乘的项,一般关于i的常量就是斜率)。
4.可以开始确定维护凸壳的形状,以及维护的方式和查询的方式。
5.切掉这道题。
6.AK这套比赛!
本人太菜,如有问题还请大佬多多海涵!