【8*】Slope Trick 学习笔记
前言
从 一些快速笔记 中分离出来的,因为这一知识点写的内容太多了,可以拿出来专门开一篇学习笔记。
Slope Trick 不是斜率优化!Slope Trick 不是斜率优化!Slope Trick 不是斜率优化!
此类知识点大纲中并未涉及,所以【8】是我自己的估计,后带星号表示估计,仅供参考。
Slope Trick
Slope Trick 是一种优化 DP 的方法,核心思想是通过只存储DP转移的一些关键信息,从而利用数据结构高效维护转移。
Slope Trick 通常用于二维或高维 DP,把其中一维看作函数的自变量,其他维度都看作函数,转移就考虑两个函数之间的变化。一般使用 Slope Trick 优化的 DP 都满足如下性质:
:是连续函数。
:是分段一次函数或凸/凹函数,且斜率一般为整数。
在 Slope Trick 中我们一般只记录初始的斜率和斜率变化(一般为 )的位置(记作变化点),放到数据结构中维护,如果一个位置的斜率变化多次则记录多次。
鉴于许多极为优秀的性质,Slope Trick 中有许多函数操作可以快速维护。这些操作快速维护的核心在于 Slope Trick 函数中会有一段水平的区间,这段区间往往是最大值或最小值。我们通常用两个堆分别维护这段水平区间左(包括水平段左端点)右(包括水平段右端点)的变化点。
:相加。直接把初始斜率相加,并合并变化位置集合。
:取前缀/后缀 max/min。以前缀 min 为例,把上升的位置,即水平的区间后面的变化点直接扔掉。
答案统计的时候有两种方法,一种是还原图像,另一种是记录决策点。
例题
例题 :
CF713C Sonya and Problem Wihtout a Legend
先把 减 转化为非严格递增。考虑写出朴素的 DP 式子。记 表示 时的最小花费,转移比较显然。
记 表示 ,注意到这是一个凸函数,而 也是一个凸函数,故 也为凸函数。且由于每次横坐标变化为 ,斜率是整数,符合 Slope Trick 的优化条件。考虑把改写后的式子写出来。
于是转移就转化为了维护两个凸函数相加且取前缀最小值。我们不考虑初始斜率,加入 等价于加入两个变化点 。我们按照 和已有的最小点 的位置分类讨论。
我们用一个堆维护 的转移点。 即为堆顶的横坐标。
:,首先初始斜率加 ,加入的第一个 把 处的斜率变成了 ,加入的第二个 取前缀最小值的时候删除了,所以只需要加入一个 。
:,首先初始斜率加 ,加入了两个 , 位置的斜率变化量为 ,在求前缀最小值的时候消掉了,因此我们先加入一个 ,弹出 后再加入一个 。此时新图象水平段向上平移了 个单位长度,注意到最后到答案就是水平段的纵坐标,把 累加到答案里即可。
注意到每次的决策点就是最小值的点,直接累加转移即可。初始斜率好像不需要记录。
#include <bits/stdc++.h>
using namespace std;
long long n,a[4000],ans=0;
priority_queue<long long>q;
int main()
{
scanf("%lld",&n);
for(int i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
a[i]-=i,q.push(a[i]);
long long h=q.top();
if(a[i]<h)ans+=h-a[i],q.push(a[i]),q.pop();
}
printf("%lld\n",ans);
return 0;
}
例题 :
AT_abc217_h [ABC217H] Snuketoon
设 表示时间 结束后在位置 的最小伤害,其余状态均不可优化。考虑先写出转移式子。
把 看成关于 的函数 ,不难证明函数 是凸的。考虑归纳法, 是凸的,显然 是凸的。假设 是凸的,则取 后 还是凸的,再加上一个凸函数 ,于是 为凸函数。所以 是凸的,且不难发现斜率为整数,所以可以使用 Slope Trick。
考虑前一步的取 ,不难发现其实就是最小值想左右扩展,水平的那一段左边的往左平移一位,水平的那一段右边的往右平移一位,对于左右两个堆维护一个整体偏移量即可。
考虑 , 为一个关于 的函数,此函数初始斜率为 ,存在一个变化点 使斜率为 。考虑添加变化点,如果这个变化点在水平段以及水平段左边,那么直接插入左堆,水平段纵坐标不变化。否则由于斜率加了 ,画图发现右堆水平段右端点处变成了左堆水平段左端点,弹出来右堆堆顶后插入左堆,并把新变化点插入右堆,顺便记录水平段纵坐标变化。
也是同理。注意偏移量处理容易弄错。
注意初始状态 只有 ,其余均为正无穷。我们可以把初始纵坐标设为 ,然后插入足够多个 变化点,就可以实现这个效果。
#include <bits/stdc++.h>
using namespace std;
long long n,t,d,x,lst=0,d1=0,d2=0,ans=0;
priority_queue<long long>l,r;
int main()
{
scanf("%lld",&n);
for(int i=1;i<=n;i++)l.push(0),r.push(0);
for(int i=1;i<=n;i++)
{
scanf("%lld%lld%lld",&t,&d,&x);
d1-=(t-lst),d2+=(t-lst),lst=t;
if(d==0)
{
if(x<=-r.top()+d2)l.push(x-d1);
else ans+=(x-(-r.top()+d2)),l.push(-r.top()+d2-d1),r.pop(),r.push(-(x-d2));
}
else if(d==1)
{
if(x>=l.top()+d1)r.push(-(x-d2));
else ans+=(l.top()+d1-x),r.push(-(l.top()+d1-d2)),l.pop(),l.push(x-d1);
}
}
printf("%lld\n",ans);
return 0;
}
例题 :
记 表示使点 子树内与所有叶子节点距离均为 的最小代价。我们不难列出如下方程。
是一个关于 的凸函数,因为一定存在一个最佳长度使原 DP 值最小。无论偏小还是偏大,由于绝对值的影响,都只会增大而不会减小。于是我们定性地分析出了 的性质。
考虑令 ,我们考虑先求出 ,然后通过合并求出 。对于 ,设 的左边最小值为 ,右边最小值为 ,我们进行分类讨论。
:若 ,则 至少以 的斜率减小,而 至多以 的斜率增加,因此整个函数一定单调不增,取 一定最小。此时 。
:若 , 的零点至多在 处,同上得取 是 前一段的最小值。注意到斜率已经为 ,后面一定不优。此时 。
:若 , 的零点 一定在区间 , 取到最小值,两个函数都取到了最小值。此时 。注意到此时 一定在斜率为 的那一段上,所以也有 。
:若 ,则函数 的零点一定大于 ,且相加后在 处斜率取到 ,于是取 值最小。此时 。注意到 ,所以 。
注意到变化后还是一个连续的函数,在交界处都完美汇合,于是考虑观察 和 的变化。注意到第一种情况只是增加了初始值,没有改变斜率,不管它。第二、三、四种情况都只用到了 ,因此我们可以把 以及之后的点都扔掉,也就是以这个点开头斜率 的点都扔掉。同时第二、三、四种情况斜率分别为 ,我们加入 ,就完成了变换。
在这个题中,我们有办法偷懒不维护每个点的斜率。注意到点 每有一个, 以及其右边的点数量就会加 。因此,若儿子数量为 ,我们弹出前 大的决策点就找到了 ,也找到了以这个点开头斜率 的点。
合并两个函数集合,直接左偏树就行了。初始状态直接 即可,因为是和在下一层统计等效的,能省去一些特判。
最后统计答案可以通过 还原函数得到。
#include <bits/stdc++.h>
using namespace std;
long long n,m,x,y,rt[400000],v[4000000],lc[4000000],rc[4000000],dist[4000000],cnt=0,ans=0;
vector<long long>s[400000],w[400000];
long long merge(long long x,long long y)
{
if(!x||!y)return x+y;
if(v[x]<v[y])swap(x,y);
rc[x]=merge(rc[x],y);
if(dist[rc[x]]>dist[lc[x]])swap(lc[x],rc[x]);
dist[x]=dist[rc[x]]+1;
return x;
}
void del(long long x)
{
long long p=rt[x];
rt[x]=merge(lc[rt[x]],rc[rt[x]]);
lc[p]=rc[p]=dist[p]=0;
}
void insert(long long x,long long k)
{
v[++cnt]=k;
rt[x]=merge(rt[x],cnt);
}
void dfs(long long x)
{
if(s[x].size()==0)insert(x,0);
for(int i=0;i<(int)s[x].size();i++)
{
dfs(s[x][i]);
for(int j=0;j<(int)s[s[x][i]].size()-1;j++)del(s[x][i]);
long long l=0,r=v[rt[s[x][i]]];
del(s[x][i]),l=v[rt[s[x][i]]],del(s[x][i]),insert(s[x][i],l+w[x][i]),insert(s[x][i],r+w[x][i]);
rt[x]=merge(rt[x],rt[s[x][i]]);
}
}
int main()
{
scanf("%lld%lld",&n,&m);
dist[0]=-1,n+=m;
for(int i=2;i<=n;i++)scanf("%lld%lld",&x,&y),s[x].push_back(i),w[x].push_back(y),ans+=y;
dfs(1);
for(int j=0;j<(int)s[1].size();j++)del(1);
while(rt[1])ans-=v[rt[1]],del(1);
printf("%lld\n",ans);
return 0;
}
后记
感觉 Slope Trick 还是要多画图,瞪着想一上午不如随手画一下图。还是缺乏想象力啊。
天上数日幻作尘世百年 一瞬清风擦面
何故撩拨我心弦 凌霄宝殿怎及
阑珊处烟火点点
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!