DP 学习笔记
Lucky_Xiang · · 算法·理论
决策单调性 DP
对于某些 dp 会存在以下形式:
这种 dp 经常会存在决策单调性(可以证明或打表验证)。也就是说,对于
那么就可以分治处理:设
斜率优化 DP
link
对于两个决策点
若满足决策单调性(或
二分队列
二分队列内每一个元素都代表一个决策,其中存储了决策位置与最优决策有效范围。其中最优决策有效范围满足单调队列的性质。可以通过二分找出两个决策的有效范围的临界点。
int get(int i,int j)
{
int L=i+1,R=n;
int k=n;
while(L<=R)
{
int mid=(L+R)>>1;
if(f[i]+W(i+1,mid)<f[j]+W(j+1,mid))
{
L=mid+1;
k=mid;
}
else R=mid-1;
}
return k;
}
bool check(LL mid)
{
head=1; tail=0;
q[++tail]={0,n};
for(int i=1;i<=n;i++)
{
while(head<tail && q[head].mr<i)head++;
f[i]=f[q[head].p]+W(q[head].p+1,i)+mid;
s[i]=s[q[head].p]+1;
while(head<tail && get(q[tail].p,i)<=q[tail-1].mr)tail--;
q[tail].mr=get(q[tail].p,i);
q[++tail]={i,n};
}
return s[n]>=k;
}
wqs 二分
wqs 二分常用于求解恰好选择
不妨假设
可以试图给函数加权,即每选择一个物品必须将价值
三点共线
但是,可能出现三点共线的情况。面对这种情况,可以在 dp 时强迫选择最左边(或最右边)的点,便于更新答案。
L=-inf; R=inf;
while(L<=R)
{
mid=(L+R)>>1;
check(mid);
//这里的 check 会更新 ans 和 cnt,
//使得 ans 最小的情况下 cnt 最小。
if(cnt<=k)
{
L=mid+1;
res=ans+mid*k;
//找到斜率最接近的位置更新
}
else R=mid-1;
}
值得注意的是,wqs 二分的斜率一定是整数,因为定义域连续、值域为整数。
选择至多 k 个
那么,怎么处理至多(或至少)选择
不妨先不加权求一遍答案,这时最优解为
P2619
直接对白边加权,用 wqs 二分去跑 Kruskal 算法即可。
CF739E
一个质朴的 dp 是
但是,这样做是
于是可以根据第二种精灵球的选择个数,来调节
这题甚至有更优做法。用 wqs 二分套 wqs 二分,设
Slope Trick
有时候,dp 的某个维度会特别大,但是 dp 数组可以画成一条凸的折线,那么就可以使用 slope trick 维护。通常,可以使用一些数据结构来维护斜率的变化点。
P4597
不妨设:
那么,就可以写出转移方程:
固定
先考虑
用一个大根堆维护所有斜率变化点。如果大根堆中有一个
- 叠加绝对值:向大根堆中加入两个
a_u 。 - 推平斜率
>0 部分:删除大根堆中的最大元素。(不妨设它是s )
但是,只维护斜率变化点还不够,我们还需要关心折线的最低点。容易发现只有叠加
动态维护这个过程,最终折线最低点的纵坐标就是答案。
但是,如果要求出具体解怎么办?
不妨设
-
如果
s_{n-1}\le a_n ,则局部最优解和全局最优解不会产生冲突,一定存在取a_{n-1}=s_{n-1} 的最优解; -
如果
s_{n-1}>a_n ,局部最优解和全局最优解产生了冲突。但是,注意到折线是下凸的,且s_{n-1} 处f_{n-1,x} 的斜率会从0 变成+1 。因此,s_{n-1} 前面的部分一定是单调不上升的,取a_{n-1}=s_n 一定存在最优解。
从
P3642
设
还是使用大根堆维护斜率变化点。先不考虑
再考虑连向父亲节点的引线。不妨设它的长度为
然后考虑改变这条引线的长度。分两种情况讨论:
- 缩短引线长度:将这条折线沿左上方移动
t 格(这里指沿x 方向的投影),会扫过一片区域。 - 延长引线长度:将这条折线沿右上方无限平移,会扫过一片区域。
容易发现扫过区域并集的轮廓就是新的折线。
这会对大根堆产生什么影响呢?对于第一种操作,
转折点就可以维护了,那斜率最低点怎么维护?可以维护每条折线的最低点,再进行合并,但是比较麻烦。有一种更好的方法:注意到
P13954
维护凸包不一定要维护转折点坐标,有时候可以维护每个线段的斜率。这样的好处是非常好维护不同凸包的合并。
定义
另外,如果
注意到最终答案是