正题
我就以这一题:玩具装箱装玩具来引入我们今天的话题。
我们先设f[i]表示前i个玩具装的最小费用是多少。
那么,很明显就有我们枚举一个j,使得j+1到i装在一起,那么就有下面的方程。
然后我们使
那么就有
上面的推导过程是很明显的。
接着,我们设
那么就有
我们现在已知,要找到一组x和y使得f[i]最小。
我们先把a[i]^2丢掉,就剩下:
现在就像一条公式了,所以要使f[i]最小,就相当于让一条斜率为-k (2*a[i]) 的直线,从无穷小往上平移,碰到的第一个点就是要求的x和y。明显这个点肯定在1到i-1的下凸包上,又因为a[i]是递增的,所以决策一定是单调上升的。
那么证明了决策单调性,可以用常规方法,也可以用维护凸包的方法,维护一下就好了,就像下图,我们每一次选中的点一定是对于这个斜率的“凸”点。如果有兴趣的同学可以用四边形不等式来证明一下。
下面是代码,这道题不知道为什么,算斜率的时候用double是错的,long long 却是对的。
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
using namespace std;
int n,m;
long long a[50010],b[50010],f[50010];
struct node{
long long x,y;
}s[50010],now;
int st=1,ed=1;
long long get_k(node x,node y){
return (x.y-y.y)/(x.x-y.x);
}
int main(){
scanf("%d %d",&n,&m);
int x,sum=0;
a[0]=0;b[0]=m+1;
s[st]=(node){b[0],b[0]*b[0]};
for(int i=1;i<=n;i++){
scanf("%d",&x);
sum+=x;
a[i]=sum+i;b[i]=sum+i+m+1;
}
for(int i=1;i<=n;i++){
while(st<ed && get_k(s[st],s[st+1])<=2*a[i]) st++;
f[i]=-2*a[i]*s[st].x+s[st].y+a[i]*a[i];
now=(node){b[i],f[i]+b[i]*b[i]};
while(st<ed && get_k(s[ed-1],s[ed])>=get_k(s[ed],now)) ed--;
s[++ed]=now;
}
printf("%lld\n",f[n]);
}
有一篇好论文:https://wenku.baidu.com/view/eeb6d3ea19e8b8f67c1cb937.html
四边形不等式:如果状态x转移到状态y的代价为w[x,y],只要满足
那么这个动态规划问题的决策就是单调的。
像上面的方程,我们可以把它转化为
明显是状态i转移到状态j的代价,自己动笔,丰衣足食。
好的,我们现在研究完了这一题,发现什么,它的是单调上升的,而且斜率也是单调上升的。所以我们可以直接用一个单调队列来维护下凸包,而不用考虑前面的值。
现在,我们要研究的问题就是(x单调,k不单调)(x不单调,k单调)和(x不单调,k不单调)三个分支问题。
我现在只会(x不单调,k不单调)的 O(n log2 n) 的解法,其他的也一样可以这样做,但是不知道有没有特殊的 O(n) 解法。
x不单调,k不单调
首先我们知道,这种东西会很烦,所以我们就想到了CDQ分治。
我们先全部按k排序,很明显k是可以根据输入直接得到的,并记录下这个元素原来是在哪里的。
每次分,我们把(l , r)区间内的,按原来的位置分成左右两堆,下去分治。
先做左边,每次递归结束后会把这些元素按照x坐标(算出来的),从左到右排序。
那么左边的序列就是关于x有序的了,我们用左边的构建一个下凸包,然后用右边没有改变过顺序的斜率,去更新每一个节点的答案,然后就完了。
最后把右边分治一下。递归回来之后,把左边排好序的和右边排好序的做一次类似于归并排序的东西就好了。
例题有[NOI2007]货币兑换。
如果多加一维限制,表示x所能取的范围在之间呢?
好的,问了一下zhf大佬,是双log的。
我们构建一棵线段树,以位置为关键字。每个节点维护一个凸包。
每次我们对一个节点有一个它的斜率,对于bound[x]~x-1这段区间,在线段树上是log个的,把它找出来,它肯定是被完全覆盖的,所以,对于log个区间分别二分就可以了。
插入一个节点的时候,会更改log个区间,当且仅当这个区间的元素被填满,我们就把它排序构造一个凸包,那么插入的时间也是两个log的。
这样子就可以完美得完成这个口胡题。
以上纯属口胡,要做更多的题,才能完善这个恶心有趣的知识体系。