前言

一些快速笔记 中分离出来的,因为这一知识点写的内容太多了,可以拿出来专门开一篇学习笔记。

Slope Trick 不是斜率优化!Slope Trick 不是斜率优化!Slope Trick 不是斜率优化!

此类知识点大纲中并未涉及,所以【8】是我自己的估计,后带星号表示估计,仅供参考。

Slope Trick

Slope Trick 是一种优化 DP 的方法,核心思想是通过只存储DP转移的一些关键信息,从而利用数据结构高效维护转移。

Slope Trick 通常用于二维或高维 DP,把其中一维看作函数的自变量,其他维度都看作函数,转移就考虑两个函数之间的变化。一般使用 Slope Trick 优化的 DP 都满足如下性质:

\(1\):是连续函数。

\(2\):是分段一次函数或凸/凹函数,且斜率一般为整数。

在 Slope Trick 中我们一般只记录初始的斜率和斜率变化(一般为 \(\pm1\))的位置(记作变化点),放到数据结构中维护,如果一个位置的斜率变化多次则记录多次。

鉴于许多极为优秀的性质,Slope Trick 中有许多函数操作可以快速维护。这些操作快速维护的核心在于 Slope Trick 函数中会有一段水平的区间,这段区间往往是最大值或最小值。我们通常用两个堆分别维护这段水平区间左(包括水平段左端点)右(包括水平段右端点)的变化点。

\(1\):相加。直接把初始斜率相加,并合并变化位置集合。

\(2\):取前缀/后缀 max/min。以前缀 min 为例,把上升的位置,即水平的区间后面的变化点直接扔掉。

答案统计的时候有两种方法,一种是还原图像,另一种是记录决策点。

例题

例题 \(1\):

CF713C Sonya and Problem Wihtout a Legend

先把 \(a_i\) 减 \(i\) 转化为非严格递增。考虑写出朴素的 DP 式子。记 \(f_{i,j}\) 表示 \(a_i=j\) 时的最小花费,转移比较显然。

\[f_{i,j}=\min_{k=1}^jf_{i-1,k}+\mid a_{i}-j\mid
\]

记 \(g_{j}\) 表示 \(\min_{k=1}^jf_{i-1,k}\),注意到这是一个凸函数,而 \(\mid a_{i}-j\mid\) 也是一个凸函数,故 \(f_{i,j}\) 也为凸函数。且由于每次横坐标变化为 \(1\),斜率是整数,符合 Slope Trick 的优化条件。考虑把改写后的式子写出来。

\[f_{i,j}=g_j+\mid a_{i}-j\mid
\]

于是转移就转化为了维护两个凸函数相加且取前缀最小值。我们不考虑初始斜率,加入 \(\mid a_{i}-j\mid\) 等价于加入两个变化点 \(\{a_{i},a_{i}\}\)。我们按照 \(a_{i}\) 和已有的最小点 \(h\) 的位置分类讨论。

我们用一个堆维护 \(g_j\) 的转移点。\(h\) 即为堆顶的横坐标。

\(1\):\(a_i\ge h\),首先初始斜率加 \(1\),加入的第一个 \(a_i\) 把 \(a_i\) 处的斜率变成了 \(0\),加入的第二个 \(a_i\) 取前缀最小值的时候删除了,所以只需要加入一个 \(a_i\)。

\(2\):\(a_i\lt h\),首先初始斜率加 \(1\),加入了两个 \(a_i\),\(h\) 位置的斜率变化量为 \(1\),在求前缀最小值的时候消掉了,因此我们先加入一个 \(a_i\),弹出 \(h\) 后再加入一个 \(a_i\)。此时新图象水平段向上平移了 \(h-a_i\) 个单位长度,注意到最后到答案就是水平段的纵坐标,把 \(h-a_i\) 累加到答案里即可。

注意到每次的决策点就是最小值的点,直接累加转移即可。初始斜率好像不需要记录。

#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;
}

例题 \(2\):

AT_abc217_h [ABC217H] Snuketoon

设 \(f_{i,j}\) 表示时间 \(i\) 结束后在位置 \(j\) 的最小伤害,其余状态均不可优化。考虑先写出转移式子。

\[f_{i,j}=\min\{f_{i-1,j-1},f_{i-1,j},f_{i-1,j+1}\}+\text{cost}(i,j)
\]

把 \(f_{i,j}\) 看成关于 \(j\) 的函数 \(f_i(j)\),不难证明函数 \(f_i(j)\) 是凸的。考虑归纳法,\(\text{cost}(i,j)\) 是凸的,显然 \(f_1(j)\) 是凸的。假设 \(f_n(j)\) 是凸的,则取 \(\min\) 后 \(f_n(j)\) 还是凸的,再加上一个凸函数 \(\text{cost}(i,j)\),于是 \(f_{n+1}(j)\) 为凸函数。所以 \(f_i(j)\) 是凸的,且不难发现斜率为整数,所以可以使用 Slope Trick。

考虑前一步的取 \(\min\),不难发现其实就是最小值想左右扩展,水平的那一段左边的往左平移一位,水平的那一段右边的往右平移一位,对于左右两个堆维护一个整体偏移量即可。

考虑 \(D_i=0\),\(\text{cost}(i,j)\) 为一个关于 \(j\) 的函数,此函数初始斜率为 \(-1\),存在一个变化点 \(\{X_i\}\) 使斜率为 \(0\)。考虑添加变化点,如果这个变化点在水平段以及水平段左边,那么直接插入左堆,水平段纵坐标不变化。否则由于斜率加了 \(1\),画图发现右堆水平段右端点处变成了左堆水平段左端点,弹出来右堆堆顶后插入左堆,并把新变化点插入右堆,顺便记录水平段纵坐标变化。

\(D_i=1\) 也是同理。注意偏移量处理容易弄错。

注意初始状态 \(f_0(j)\) 只有 \(f_0(0)=0\),其余均为正无穷。我们可以把初始纵坐标设为 \(0\),然后插入足够多个 \(\{0\}\) 变化点,就可以实现这个效果。

#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;
}

例题 \(3\):

P3642 [APIO2016] 烟火表演

记 \(f_{x,i}\) 表示使点 \(x\) 子树内与所有叶子节点距离均为 \(i\) 的最小代价。我们不难列出如下方程。

\[f_{x,i}=\sum_{v\in\text{son}(x)}\min_{j\le i}f_{v,j}+\mid e_d+j-i\mid
\]

\(f_{x,i}\) 是一个关于 \(i\) 的凸函数,因为一定存在一个最佳长度使原 DP 值最小。无论偏小还是偏大,由于绝对值的影响,都只会增大而不会减小。于是我们定性地分析出了\(f_{x,i}\) 的性质。

考虑令 \(g_{v,i}=\min_{j\le i}f_{v,j}+\mid e_d+j-i\mid\),我们考虑先求出 \(g_{v,i}\),然后通过合并求出 \(f_{x,i}\)。对于 \(g_{v,i}\),设 \(f_{v,j}\) 的左边最小值为 \(l\),右边最小值为 \(r\),我们进行分类讨论。

\(1\):若 \(i\lt l\),则 \(f_{v,j}\) 至少以 \(1\) 的斜率减小,而 \(\mid e_d+j-i\mid\) 至多以 \(1\) 的斜率增加,因此整个函数一定单调不增,取 \(j=i\) 一定最小。此时 \(g_{v,i}=f_{v,i}+e_d\)。

\(2\):若 \(l\le i\lt l+e_d\),\(\mid e_d+j-i\mid\) 的零点至多在 \(l\) 处,同上得取 \(l\) 是 \(l\) 前一段的最小值。注意到斜率已经为 \(0\),后面一定不优。此时 \(g_{v,i}=f_{v,l}+e_d+l-i\)。

\(3\):若 \(l+e_d\le i\lt r+e_d\),\(\mid e_d+j-i\mid\) 的零点 \(i-e_d\) 一定在区间 \([l,r)\),\(f_{v,j}\) 取到最小值,两个函数都取到了最小值。此时 \(g_{v,i}=f_{v,i-e_d}\)。注意到此时 \(i-e_d\) 一定在斜率为 \(0\) 的那一段上,所以也有 \(g_{v,i}=f_{v,l}\)。

\(4\):若 \(i\gt r+e_d\),则函数 \(\mid e_d+j-i\mid\) 的零点一定大于 \(r\),且相加后在 \(r\) 处斜率取到 \(0\),于是取 \(j=r\) 值最小。此时 \(g_{v,i}=f_{v,r}+i-r-e_d\)。注意到 \(f_{v,l}=f_{v,r}\),所以 \(g_{v,i}=f_{v,l}+i-r-e_d\)。

注意到变化后还是一个连续的函数,在交界处都完美汇合,于是考虑观察 \(g_{v,i}\) 和 \(f_{v,j}\) 的变化。注意到第一种情况只是增加了初始值,没有改变斜率,不管它。第二、三、四种情况都只用到了 \(f_{v,l}\),因此我们可以把 \(l\) 以及之后的点都扔掉,也就是以这个点开头斜率 \(\ge0\) 的点都扔掉。同时第二、三、四种情况斜率分别为 \(-1,0,1\),我们加入 \(\{l+e_d,r+e_d\}\),就完成了变换。

在这个题中,我们有办法偷懒不维护每个点的斜率。注意到点 \(x\) 每有一个,\(r\) 以及其右边的点数量就会加 \(1\)。因此,若儿子数量为 \(k\),我们弹出前 \(k-1\) 大的决策点就找到了 \(l,r\),也找到了以这个点开头斜率 \(\ge0\) 的点。

合并两个函数集合,直接左偏树就行了。初始状态直接 \(\{0\}\) 即可,因为是和在下一层统计等效的,能省去一些特判。

最后统计答案可以通过 \(f_{1,0}=\sum e_d\) 还原函数得到。

#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;
}

后记

学习资料1 学习资料2 学习资料3

感觉 Slope Trick 还是要多画图,瞪着想一上午不如随手画一下图。还是缺乏想象力啊。

天上数日幻作尘世百年 一瞬清风擦面

何故撩拨我心弦 凌霄宝殿怎及

阑珊处烟火点点

【8*】Slope Trick 学习笔记的更多相关文章

  1. <老友记>学习笔记

    这是六个人的故事,从不服输而又有强烈控制欲的monica,未经世事的千金大小姐rachel,正直又专情的ross,幽默风趣的chandle,古怪迷人的phoebe,花心天真的joey——六个好友之间的 ...

  2. Deep Learning(深度学习)学习笔记整理系列之(三)

    Deep Learning(深度学习)学习笔记整理系列 zouxy09@qq.com http://blog.csdn.net/zouxy09 作者:Zouxy version 1.0 2013-04 ...

  3. udacity android 学习笔记: lesson 4 part b

    udacity android 学习笔记: lesson 4 part b 作者:干货店打杂的 /titer1 /Archimedes 出处:https://code.csdn.net/titer1 ...

  4. Miller_Rabbin&&Pollard_Rho 学习笔记

    占坑,待填 I Intro 首先我们考虑这样一个问题 给定一个正整数\(p(p<=1e8)\),请判断它是不是质数 妈妈我会试除法! 于是,我们枚举$ \sqrt p$ 以内的所有数,就可以非常 ...

  5. 计算平面面积和斜面面积-ArcGIS案例学习笔记

    计算平面面积和斜面面积-ArcGIS案例学习笔记 联系方式:谢老师,135_4855_4328,xiexiaokui#139.com 数据:实验数据\Chp8\Ex5\demTif.tif 平面面积= ...

  6. Deep learning with Python 学习笔记(5)

    本节讲深度学习用于文本和序列 用于处理序列的两种基本的深度学习算法分别是循环神经网络(recurrent neural network)和一维卷积神经网络(1D convnet) 与其他所有神经网络一 ...

  7. 「学习笔记」wqs二分/dp凸优化

    [学习笔记]wqs二分/DP凸优化 从一个经典问题谈起: 有一个长度为 \(n\) 的序列 \(a\),要求找出恰好 \(k\) 个不相交的连续子序列,使得这 \(k\) 个序列的和最大 \(1 \l ...

  8. 学习笔记 - 2sat

    学习笔记 - 2sat 决定重新启用Markdown--只是因为它支持MathJax数学公式 noip考完,既轻松又无奈,回来慢慢填坑 这篇博客也是拖了好久,通过kuangbin的博客才弄懂2-sat ...

  9. 【学习笔记】动态规划—斜率优化DP(超详细)

    [学习笔记]动态规划-斜率优化DP(超详细) [前言] 第一次写这么长的文章. 写完后感觉对斜优的理解又加深了一些. 斜优通常与决策单调性同时出现.可以说决策单调性是斜率优化的前提. 斜率优化 \(D ...

  10. tensorflow学习笔记——自编码器及多层感知器

    1,自编码器简介 传统机器学习任务很大程度上依赖于好的特征工程,比如对数值型,日期时间型,种类型等特征的提取.特征工程往往是非常耗时耗力的,在图像,语音和视频中提取到有效的特征就更难了,工程师必须在这 ...

随机推荐

  1. lua三色标记的读写屏障理解

    起因是已经被标记为黑色的对象无法进行再次遍历,然而黑色对象发生了引用变化:断开了引用或者引用了别的对象,会导致多标(不再被黑色对象引用的对象未能回收),漏标(黑色对象的新引用未能遍历标记)

  2. javaWeb之路径

    一. 路径写法: 1. 路径分类 a. 相对路径:通过相对路径不可以确定唯一资源 * 如:./index.html * 不以/开头,以.开头路径 * 规则:找到当前资源和目标资源之间的相对位置关系 * ...

  3. jmeter性能测试案例:电商系统并发订单测试

    场景描述:本案例主要实现多用户同时提交订单,以检测系统对瞬时压力的响应情况.具体流程包括用户登录-添加商品-提交订单.涉及多个接口联动和参数处理,步骤如下: 第一步,登录用户 1.新建"下订 ...

  4. 一个简单的struts2配置

    目录 1 需求 2 需要导入的jar包 3 项目的目录结构 3.1  demo1.jsp 3.2 success.jsp 3.3 HelloAction.java 3.4 struts.xml 3.5 ...

  5. 工具 | Hashcat

    0x00 简介 Hashcat是一款强大的密码破解工具. 下载地址 Hashcat下载: Hashcat下载 0x01 功能说明 直接破解 组合攻击 掩码暴力破解 混合攻击 联合攻击 注:仅供安全研究 ...

  6. AI制作祝福视频,直播礼物收不停,广州塔、动态彩灯、LED表白(附下载链接)

    在追剧的时候经常能看到一些浪漫的告白桥段,男主用圣诞彩灯表白.用城市标志性建筑的LED表白,或者在五光十色的烟花绽放后刻下女主角的名字,充满了仪式感和氛围感~ 现在,这样的表白效果用AI软件就能实现了 ...

  7. C#之使用线程池

    简述 创建线程是昂贵的操作,所以为每个短暂的异步操作创建线程会产生显著的开销,线程池就是该问题的解决方案,我们事先分配一定的资源,将这些资源放入资源池,每次需要新的资源,只需从池中获取一个,而不用创建 ...

  8. Django Formsets总结

    formset是将多个表单用在同一个页面上的抽象层. 我们有: from django import forms class ArticleForm(forms.Form): title=forms. ...

  9. File与IO流之File练习

    创建文件夹,并在其中创建文件 package Java_test; import java.io.*; public class Test { public static void main(Stri ...

  10. thinkphp R方法传参

    thinkphp 框架中R方法和A方法很类似. 但是R方法在调用模块且传参数的时候,是有一些需要注意的地方的. R方法在传参数是只能传之前就有的参数,而不能传自定义的或者新增的参数. 例如:A --- ...