Slope Trick:解决一类凸代价函数DP优化
【前言】
在补Codeforce的DP时遇到一个比较新颖的题,然后在知乎上刚好 hycc 桑也写了这道题的相关题解,这里是作为学习并引用博客的部分内容
这道题追根溯源发现2016年这个算法已经在APIO2016烟花表演与Codeforces 713C引入,自那之后似乎便销声匿迹了。相关题型数量也较少,因而在这里结合前辈们的工作做一些总结。---by hycc
问题引入:Codeforces 713C
题目链接:Here
题意:
给定 \(n\) 个正整数 \(a_i\) ,每次操作可以选择任意一个数将其 \(+1\) 或 \(-1\) ,问至少需要多少次操作可以使得 \(n\) 个数保持严格单增。
数据范围:\(1\le n\le 3000,1\le a_i\le 10^9\)
对我来说这道题其实和曾经写过的 POJ-3666:求不升的DP是一样的
这个题是求升序的DP,那么有什么变化呢
不升的条件是:\(a_i -a_j \ge 0\)
升序的条件是:\(a_i -a_j \ge i - j\) 对任意 \(i,j\) 均满足
有没有理解到什么?移项有:\(a_i - i \ge a_j - j\)
所以将 \(a\) 数字变形一下就和POJ3666就是一个题!
【AC Code】
const int N = 3100;
int n, m;
ll f[N][N], a[N], b[N];
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
a[i] = a[i] - i;
b[i] = a[i];
}
sort(b + 1, b + 1 + n);
m = 1;
for (int i = 2; i <= n; ++i) if (b[i] != b[i - 1]) b[++m] = b[i];
memset(f, 0, sizeof(f));
for (int i = 1; i <= n; ++i) {
ll Min = LLONG_MAX;
for (int j = 1; j <= m; j++) {
Min = min(Min, f[i - 1][j]);
f[i][j] = abs(b[j] - a[i]) + Min;
}
}
ll ans = LLONG_MAX;
for (int i = 1; i <= m; ++i) ans = min(ans, f[n][i]);
cout << ans << "\n";
}
当然上面说的思路并不是本篇博客实际想表达,以下才是正文
对于朴素的 \(\mathcal{O}(n^2)\ DP\) :
一个显然的性质:如果不是“严格单增”而是“严格非降”,那么最终形成的严格非降序列,其中每个元素一定属于 \(\{a_i\}\)
将元素离散化后可以设计 \(f_{i,j}\) 表示到第 \(i\) 个数取 \(j\) 的最少操作数
那么有转移 \(f_{i,j} = \min\limits_{k\le j}f_{i-1,k} + | a_i - j|\) ,记录 \(f_{i-1,*}\) 的前缀 \(\min\) 即可做到 \(\mathcal{O}(n^2)\)
至于如何做到“严格非降”,\(a_{i-1} < a_i,a_{i -1} \le a_i - i,a_{i-1}-(i-1)\le a_i - i\)
于是令 \(a_i = a_i - i\) 即可。
赛后的评论区中出现了一种 \(\mathcal{O}(Nlog\ N)\)的做法,也就是 Slope Trick算法的第一次现身(?)
Slope Trick:解决一类凸代价函数的DP优化问题
当序列DP的转移代价函数为
连续
分段线性函数
凸函数
时,可以通过记录分段函数的最右一段 \(f_r(x)\) 以及其分段点 \(L\) 实现快速维护代价的效果。
如:\(f(x)=\left\{\begin{array}{rr}
-x-3 & (x \leq-1) \\
x & (-1<x \leq 1) \\
2 x-1 & (x>1)
\end{array}\right.\)
可以仅记录 \(f_r(x) = 2x - 3\) 与分段点 \(L_f = \{-1,-1,1\}\) 来实现对该分段函数的存储。
注意:要求相邻分段点之间函数的斜率差为 \(1\) ,也就是说相邻两段之间斜率差 \(\ge 1\) 的话,这个分段点要在序列里出现多次。
优秀的性质:
\(F(x),G(x)\) 均为满足上述条件的分段线性函数,那么 \(H(x) =F(x)+G(x)\) 同样为满足条件的分段线性函数,且 \(H_r(x) = F_r(x) + G_r(x),L_H = L_F \bigcup L_G\) 。
该性质使得我们可以很方便得运用数据结构维护 \(L\) 序列。
回顾:Codeforces 713C
转移方程为 \(f_{i,j} = \min\limits_{k\le j}f_{i-1,k} + |a_i - j|\)
令 \(F_{i}(x)=f_{i, x}, G_{i}(x)=\min\limits _{k \leq x} f_{i-1, k}=\min \limits_{k \leq x} F_{i-1}(k)\)
那么有 \(F_i(x) = G_i(x) + |x -a_i|\) ,其中 \(F_i,G_i\) 均为分段线性函数。
\(G_i\) 求的是 \(F_{i-1}\) 的关于函数值的前缀最小值,由于 \(F_{i-1}\) 是一个凸函数,因而其最小值应该在斜率 \(=0\) 处取得,其后部分可以舍去。
而每次由 \(G_i(x)\) 加上 \(|x-a_i|\) ,等价于在 \(L\) 中添加两个分段点 \(\{a_i,a_j\}\)
因而 \(G_i\) 各段的函数斜率形如 \(\{...,-3,-2,-1,0\}\) ,加上 $|x-a_i| $后斜率变为 \(\{...,-3,-2,-1,0,1\}\) ,因而需要删除末尾的分段点。
具体实现中:使用大根堆维护分段点单调有序,每次加入两个 \(a_i\) ,再弹出堆顶元素。
总复杂度 :\(\mathcal{O}(n\ log\ n)\)
\]
Codeforces 1534G
题意:
一个无限大的二维平面上存在 \(n\) 个点 \((x_i,y_i)\) 均需要被访问一次,从 \((0,0)\) 出发,每次可以向右或向上移动一个单位。
可以在任意位置 \((X,Y)\) 访问 \((x_i,y_i)\) 并付出 \(\max\{|X-x_i|,|Y-y_i|\}\) 的代价(访问后依然留在 \((X,Y)\) )。同一位置可以访问多个点。
问:至少需要花费多少代价才能使得所有点均被访问?
数据范围: \(1\le n\le 800000,0\le x_i,y_i\le 10^9\)

结合上图可以看出,对于点 \((X,Y)\) ,一定会选择路径与直线 \(x+y=X+Y\)(红线)的交点 \((x,y)\) 处作为访问的发起点(在这条线上 \(|X-x| = |Y-y|\) )。
考虑到这条红线是倾斜的,因而将坐标系顺时针翻转 \(45^°\),即 \((x+y,x-y)\) 代替 \((x,y)\)
此时,每次移动变为 \((x+1,y-1)\) 或 \((x+1,y+1)\)
把所有点按新的 \(x\) 坐标排序,即可转为序列上的问题。
设值域为 \(M\) ,则很容易写出 \(\mathcal{O}(nM)\) 的转移方程:
\(f_{i,Y}\) 表示从左到右考虑到横坐标为 \(x_i\) 的所有点,当前路径到了 \((x_i,Y)\) 的最小代价,
那么有
$f_{i,Y}=\min\limits_{Y-\left|x_{i}-x_{i-1}\right|\leq k\leq Y+\left|x_{i}-x_{i-1}\right|}f_{i-1, k}+\sum\limits_{(x, y), x=x_{i}}|Y-y| $
同样,设 \(F_{i}(x)=f_{i, x}, G_{i}(x)=\sum\limits_{x-\left|x_{i}-x_{i-1}\right| \leq k \leq x+\left|x_{i}-x_{i-1}\right|} f_{i-1, k}\)
那么 \(F_{i}(x)=G_{i}(x)+\sum_{\left(x^{\prime}, y^{\prime}\right), x^{\prime}=x_{i}}\left|x-y^{\prime}\right|\)
主要问题在于 \(G_i(x)\) 的维护,是取一个区间范围 \([L,R]\) 内的最小值。
若斜率为 \(0\) 的两端点在 \([L,R]\) 内,那么直接取最小值即可。
若斜率为 \(0\) 的两端点在 \(L\) 左侧,需要取 \(L\) 处的值作为最小值。
若斜率为 \(0\) 的两端点在 \(R\) 右侧,需要取 \(R\) 处的值作为最小值。
因而,需要维护斜率为 \(0\) 的折线的两侧分割点 \((a,b)\) ,同时还需要支持从斜率为 \(0\) 处向两侧访问,因而使用小根堆与大根堆分别维护 \(b\) 右侧以及 \(a\) 左侧的点。
每次添加新的分割点时,根据新分割点与 \(a,b\) 的大小关系决定插入小根堆or大根堆,同时调整 \(a,b\) ,每次调整复杂度是 \(\mathcal{O}(1)\) 的(从小根堆中取出塞入大根堆或反之)
【AC Code】借用 jiangly的代码
#include <bits/stdc++.h>
using i64 = long long;
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<std::pair<int, int>> a;
for (int i = 0; i < n; i++) {
int x, y;
std::cin >> x >> y;
a.emplace_back(x + y, x);
}
std::priority_queue<i64> hl;
std::priority_queue<i64, std::vector<i64>, std::greater<>> hr;
for (int i = 0; i < n + 5; i++) {
hl.push(0);
hr.push(0);
}
i64 tag = 0, mn = 0;
int last = 0;
std::sort(a.begin(), a.end());
for (auto [s, x] : a) {
int d = s - last;
last = s;
tag += d;
if (x <= hl.top()) {
mn += hl.top() - x;
hl.push(x);
hl.push(x);
hr.push(hl.top() - tag);
hl.pop();
} else if (x >= hr.top() + tag) {
mn += x - (hr.top() + tag);
hr.push(x - tag);
hr.push(x - tag);
hl.push(hr.top() + tag);
hr.pop();
} else {
hl.push(x);
hr.push(x - tag);
}
}
std::cout << mn << "\n";
return 0;
}
Slope Trick:解决一类凸代价函数DP优化的更多相关文章
- [笔记] Slope Trick:解决一类凸代价函数的DP优化问题
原理 当序列 DP 的转移代价函数满足 连续: 凸函数: 分段线性函数. 时,可以通过记录分段函数的最右一段 \(f_r(x)\) 以及其分段点 \(L\) 实现快速维护代价的效果. 如:$ f(x) ...
- 重修 Slope Trick(看这篇绝对够!)
Slope Trick 算法存在十余载了,但是我没有找到多少拍手叫好的讲解 blog,所以凭借本人粗拙的理解来写这篇文章. 本文除标明外所有图片均为本人手绘(若丑见谅),画图真的不容易啊 qwq(无耻 ...
- [总结]一些 DP 优化方法
目录 注意本文未完结 写在前面 矩阵快速幂优化 前缀和优化 two-pointer 优化 决策单调性对一类 1D/1D DP 的优化 \(w(i,j)\) 只含 \(i\) 和 \(j\) 的项--单 ...
- DP 优化方法大杂烩 & 做题记录 I.
标 * 的是推荐阅读的部分 / 做的题目. 1. 动态 DP(DDP)算法简介 动态动态规划. 以 P4719 为例讲一讲 ddp: 1.1. 树剖解法 如果没有修改操作,那么可以设计出 DP 方案 ...
- dp优化 | 各种dp优化方式例题精选
前言 本文选题都较为基础,仅用于展示优化方式,如果是要找题单而不是看基础概念,请忽略本文. 本文包含一些常见的dp优化("√"表示下文会进行展示,没"√"表示暂 ...
- [提升性选讲] 树形DP进阶:一类非线性的树形DP问题(例题 BZOJ4403 BZOJ3167)
转载请注明原文地址:http://www.cnblogs.com/LadyLex/p/7337179.html 树形DP是一种在树上进行的DP相对比较难的DP题型.由于状态的定义多种多样,因此解法也五 ...
- loj6171/bzoj4899 记忆的轮廊(期望dp+优化)
题目: https://loj.ac/problem/6171 分析: 设dp[i][j]表示从第i个点出发(正确节点),还可以有j个存档点(在i点使用一个存档机会),走到终点n的期望步数 那么 a[ ...
- DP 优化方法合集
0. 前言 写完这篇文章后发现自己对于 DP 的优化一窍不通,所以补了补 DP 的一些优化,写篇 blog 总结一下. 1. 单调队列/单调栈优化 1.2 算法介绍 这应该算是最基础的 DP 优化方法 ...
- 常见的DP优化类型
常见的DP优化类型 1单调队列直接优化 如果a[i]单调增的话,显然可以用减单调队列直接存f[j]进行优化. 2斜率不等式 即实现转移方程中的i,j分离.b单调减,a单调增(可选). 令: 在队首,如 ...
随机推荐
- gitlab git 安装
1.配置yum源 vim /etc/yum.repos.d/gitlab-ce.repo [gitlab-ce] name=Gitlab CE Repository baseurl=https://m ...
- XCTF logmein
一.查壳 发现是64位的Linux文件(ELF可以看出是linux的文件) 二.拖入ida64,静态分析 注意这里两个坑: 1.strcpy是复制字符串的意思,前面定义的v8数组只有8个,但是后面的字 ...
- [Kong] key-auth实现对API请求的密钥认证
目录 1. 配置密钥验证插件 2. 确认插件配置正确 3. 创建cunsumer 4. 给cunsumer提供关键凭证 5. 验证 6. 小结 [前言]: 下面我们将配置key-auth插件以向服务添 ...
- nacos集群部署
对于nacos的集群部署,建议大家参考nacos官网进行,地址:https://nacos.io/zh-cn/docs/cluster-mode-quick-start.html 下面是我自己部署na ...
- Spring解决Attribute tx bound to namespace httpwww.w3.org2000xmlns was already specified
Spring|解决Attribute "tx" bound to namespace "http://www.w3.org/2000/xmlns/" was a ...
- Halcon——图像增强算子以及分类
摘要 图像增强就是指通过某种图像处理方法对退化的某些图像特征,如边缘.轮廓.对比度等进行处理,以改善图像的视觉效果,提高图像的清晰度,或是突出图像中的某些"有用",压缩其他&quo ...
- C++之vector容器
一.STL的基本概念 STL(Standard Template Library)标准模板库大体上分为六大组件,分别为容器,算法,迭代器,仿函数,适配器和空间配置器,其中最重要的是容器,算法和迭代器, ...
- JavaScript实现拖放效果
JavaScript实现拖放效果 笔者实现该效果也是套用别人的轮子的.传送门 然后厚颜无耻的贴别人的readme~,笔者为了方便查阅就直接贴了,有不想移步的可以看这篇.不过还是最好请到原作者的GitH ...
- P1447能量采集
P1447能量采集 定义:(i,j)表示处于(i,j)的植物的贡献 我们发现,点(i,j)与(0,0)的连线所过整点的数目为\(\gcd(i,j)\) 发现要是想记录每个点的答案并不好算.那么怎么好算 ...
- zookeeper与eureka比较
一个分布式系统不可能同时满足C(一致性).A(可用性)和P(分区容错性) zookeeper确保cp 当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接受服务直接d ...