【8*】Slope Trick 学习笔记

news/2025/2/11 14:48:57/文章来源:https://www.cnblogs.com/w9095/p/18709727

前言

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

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 还是要多画图,瞪着想一上午不如随手画一下图。还是缺乏想象力啊。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/882097.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis基础-跳表

一、跳表原理1.1、什么是跳表跳跃表是一种随机化的数据结构,在查找、插入和删除这些字典操作上,其效率可比拟于平衡二叉树(如红黑树),大多数操作只需要O(log n)平均时间,但它的代码以及原理更简单。跳跃表的定义如下:“Skip lists are data structures that use probab…

Linux Android 下的内存注入手段

ptrace_inject_mem long ptrace(enum __ptrace_request request, pid_t pid,void *addr, void *data);请求 (Request) 值 说明PTRACE_TRACEME 0 使调用进程变为被跟踪进程。在子进程调用 ptrace(PTRACE_TRACEME, ...) 后,父进程可以使用 PTRACE_ATTACH 进行跟踪。PTRACE_PEEKT…

解决 idea 无法创建java8 模版

解决 idea 无法创建java8 模版由于 Spring 官方不再维护 旧的版本了导致的 https://start.spring.io 解决方式很简单 切换到 阿里源就可以了 https://start.aliyun.com/ 项目生成 成功总结 :IDEA 内置的 Spring Initializr 创建 Spring Boot 项目实际上是依赖官方链接功能,官…

高性价比云服务新选择

在数字化转型加速的当下,寻找稳定可靠的云服务成为许多用户的刚需。深度测试,3丰云平台提供的免费云服务器**展现出超出预期的实用性,现将其核心优势整理如下: 一、零门槛云资源获取 通过官网([https://www.sanfengyun.com)的清晰引导,用户可快速完成注册并开通免费云服…

MySQL索引、MVCC、锁问题

一、MySQL索引 1.1.索引简介 索引是一种数据库中的数据对象,它能够提高数据库中的数据检索速度.MySQL支持多种类型的索引,每种类型的索引有其特定的用途和性能特点. MySQL中的索引种类如下:B-Tree索引 数据结构B-Tree 根据叶子结点的存储数据的种类不同分为:聚簇索引(主键索引)…

OpenEuler部署DM8主备复制集群

DM8、主备复制案例说明: 在OpenEuler系统上部署DM8的主备复制架构。 系统环境: [root@node209 dm]# cat /etc/os-release NAME="openEuler" VERSION="20.03 (LTS-SP4)" ID="openEuler" VERSION_ID="20.03" PRETTY_NAME="openEu…

OpenEuler部署DM8主备复制

DM8、主备复制案例说明: 在OpenEuler系统上部署DM8的主备复制架构。 系统环境: [root@node209 dm]# cat /etc/os-release NAME="openEuler" VERSION="20.03 (LTS-SP4)" ID="openEuler" VERSION_ID="20.03" PRETTY_NAME="openEu…

对称密码算法

1. 定义 对称密码算法加密过程与解密过程使用相同的或容易相互推导得出的密钥,即加密和解密两方的密钥是“对称”的 2. 加解密流程用户通过加密算法将明文变换为密文。密文的具体值由密钥和加密算法共同决定。只有掌握了同一个密钥和对应解密算法的用户才可以将密文逆变换为有…

SaaS+AI应用架构:业务场景、智能体、大模型、知识库、传统工具系统

大家好,我是汤师爷~ 在SaaS与AI应用的演进过程中,合理的架构设计至关重要。本节将详细介绍其五个核心层次:业务场景层:发现和确定业务场景 智能体层:构建可复用的智能应用 大模型层:采用最合适的大模型,作为思考推理的核心 知识库:管理企业的核心知识资产 传统工具:传…

50N06-ASEMI吹风筒专用MOS管50N06

50N06-ASEMI吹风筒专用MOS管50N06编辑:ll 50N06-ASEMI吹风筒专用MOS管50N06 型号:50N06 品牌:ASEMI 封装:TO-252 最大漏源电流:50A 漏源击穿电压:60V 批号:最新 RDS(ON)Max:15mΩ 引脚数量:3 沟道类型:N沟道MOS管 芯片尺寸:MIL 漏电流: 恢复时间:ns 芯片材质: …

瑞芯微开发板/主板Android调试串口配置为普通串口方法

本文介绍瑞芯微开发板/主板Android调试串口配置为普通串口方法,不同板型找到对应文件修改,修改的方法相通。触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。…

Ubuntu22.04 搭建离线APT源(apt-mirror)

1、应用场景 目前需要在Linux环境下做UE开发,Ubuntu版本使用的是22.04。项目现场是没法连到外网的,所以安装库十分不方便,所以需要搭建一套内网的离线APT源。 2、搭建步骤 2.1 资源拉取 首先需要在联网机器上将资源拉取到本地。 本教程使用使用apt-mirror拉取镜像源,使用ng…