先枚举物品,然后枚举余数,然后再枚举转移状态
cpp12345678910111213141516171819 int ans=0;
for(int i=1;i<=n;i++){
int vi,wi,si;
cin>>vi>>wi>>si;
if(vi==0) ans+=wi*si;//特判
si=min(si,v/vi);//真正可用物品数量
for(int d=0;d<vi;d++){//枚举余数
deque<pii> q;
int k=(v-d)/vi;//可以转移的状态数量
for(int j=0;j<=k;j++){
int now=dp[d+j*vi]-j*wi;//生成一个新状态,注意要-j*wi。后面还会加回来
while(!q.empty()&&now>=q.back().first) q.pop_back();
q.push_back(pii(now,j));
while(!q.empty()&&q.front().second+si<j) q.pop_front();
dp[d+j*vi]=max(dp[d+j*vi],q.front().first+j*wi);
}
}
}
cout<<ans+dp[v];
加入状态的时候 ,取出状态的时候
(1)
若dp方程满足四边形不等式,设 为 的最优决策点,则有
(2)
若dp方程满足四边形不等式,设 为 的最优决策点,则有
我们需要证明 这个二元函数满足区间包含单调性和四边形不等式,才能说明dp满足四边形不等式
区间包含单调性
若 则
四边形不等式
若 则
此外还有如下结论帮助我们证明:
如果 和 符合四边形不等式/区间包含单调性,则对于任意 都有 也符合四边形不等式/区间包含单调性
如果存在 和 使 ,则 符合四边形恒等式(即总是等号成立的四边形不等式)。如果 单调递增,那么 还符合区间包含单调性
若 单调递增且下凸( 单调递增), 符合四边形不等式和区间包含单调性,则 也符合四边形不等式和区间包含单调性
将dp最优决策点 数组打表,如果发现每一行、每一列都具有单调性,那么可以大胆赌一把dp满足四边形不等式
如果转移方程中,dp的当前位置i和准备转移到当前位置的决策点j存在形如 的一项,那么大概率可以使用斜率优化
将转移方程进行整理,由于当前dp到的位置i是固定的,因此所有与i有关的变量(除了 )都视为常数。将含有 的变量标记出来,将其移项,使得等式的一边只有一项带有 ,其余和 有关的都移到另一边,这样我们就可以整理得到形如 的式子,并且我们要求的 会在b这个位置。按题目要求,我们最大化/最小化截距。
按照上面的式子,对于每个决策点,我们将其在二维平面上表示为点 。然后做斜率为k的直线并进行平移,使得截距取得最值。 叫做目标斜率。要想快速得到最优决策点,我们需要维护一个凸壳。
一般来说,题目要求最小值,我们维护下凸壳,求最大值,我们维护上凸壳
下凸壳的点集,相邻点连线的斜率是单调递增的。下凸壳的点集,斜率则是单调递减的
按照凸壳方向和目标斜率之间的关系,我们分为下面几类
下凸壳,目标斜率递增;上凸壳,目标斜率递减
使用单调队列维护
首先我们需要找到最优决策点。这个点和上个点的斜率需要小于目标斜率,和下一个点的斜率需要大于目标斜率。不满足的点直接移出单调队列
cpp12while(head<tail&&slope(que[head],que[head+1])<目标斜率) head++;
dp[i]=...通过que[head]进行转移
然后准备加入新的决策点i。加入之前,还需要维护凸壳的单调性
cpp12while(head<tail&&slope(que[tail],que[tail-1])>slope(que[tail],i)) tail--;
que[++tail]=i;
下凸壳,目标斜率递减;上凸壳,目标斜率递增
使用单调栈维护,因为我们移出一个点时只会从一个方向移出,就相当于一个单调栈
同上
cpp123456#define st sta[sta.size()-1]
#define nd sta[sta.size()-2]
while(sta.size()>=2&&slope(st,nd)<目标斜率) sta.pop_back();
dp[i]=...通过sta[st]进行转移
while(sta.size()>=2&&slope(st,i)>slope(st,nd)) sta.pop_back();
sta.push_back(i);
当目标斜率并没有单调性时,我们就不能随意的移出凸包上的点。我们需要维护整个凸包。通过二分来查找最优决策点
cpp1234567891011121314int get(int k){
if(tail==head) return que[head];
int l=head,r=tail,mid,ans;
while(l<=r){
mid=(l+r)>>1;
if(slope(que[mid],que[mid+1])<=k)){
ans=mid;
r=mid-1;
}else{
l=mid+1;
}
}
return ans;
}
转移并维护凸包
cpp1234int p=get(目标斜率);
dp[i]=...通过p进行转移
while(head<tail&&slope(que[tail],que[tail-1])>slope(que[tail],i)) tail--;
que[++tail]=i;
上述情况,决策点的横坐标总是单调递增的。但如果决策点横坐标没有单调性,则需要splay来动态维护凸包。其他还有CDQ分治和set维护动态凸包,未完待续...
二维状态下的斜率优化和一维状态的情况类似。对于状态 ,其由 转移过来,则可以看做一个滚动数组。
首先最外层循环k,每次循环时,将单调队列清空,注意单调队列初始化方法
cpp12head=tail=1;
que[head]=0;
其余类似于一维情况
注意单调队列维护的是 中的决策点。换句话说, 这一层dp的结果并不会作为这一层之后dp时的决策点,而是作为 层的决策点。全部的决策点已经在这一层开始dp之前求好了,就差这一层dp时进行维护
以P5504 [JSOI2011] 柠檬
为例,本题转移时,要求决策点j还要满足 的条件。我们可以将不同类的决策点放入不同的单调队列/单调栈中,dp时从满足条件的集合中选取
cpp1234567vector<int> sta[100004];
#define st sta[now][sta[now].size()-1]
#define nd sta[now][sta[now].size()-2]
for(int i=1;i<=n;i++){
int now=s[i];
....
}
值得注意的时,本题中dp到i时,i本身也可以作为一个决策点,毕竟转移方程是
所以可以先加入决策点i,再选取决策点,把两个while换一下位置
精度问题在二分凸包的类型中会凸显重要性
斜率用double类型表示难免会造成精度损失,因此在比较斜率时,可以把分数展开,用乘法形式,例如:
cpp1while(sta[now].size()>=2&&(Y(i)-Y(st))*(X(st)-X(nd))>(Y(st)-Y(nd))*(X(i)-X(st))) sta[now].pop_back();
此时,不等号取登也有了意义,可以对不等号取等
cpp1while(sta[now].size()>=2&&(Y(i)-Y(st))*(X(st)-X(nd))>=(Y(st)-Y(nd))*(X(i)-X(st))) sta[now].pop_back();
另外,需要根据单调性,确保 等是正的