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单调增(可选). 令: 在队首,如 ...
随机推荐
- 微信小程序支付 后台处理逻辑 (转)
<?phpnamespace app\parent\controller; use think\Request; class Wxpay{ function wechat(){ //微信配 ...
- PYTHON UNRAR
下载安装VINRAR 安装上面的下载文件 安装位置:32位具体位置 C:\Program Files (x86)\UnrarDLL 64位具体位置:C:\Program Files (x86 ...
- C语言变量类型
#include <stdio.h> // C 语言中,任何数据类型都不可以直接存储一个字符串.那么字符串如何存储? //在 C 语言中,字符串有两种存储方式,一种是通过字符数组存储,另一 ...
- React 组件间通信 总结
组件间通信 5.1.1. 方式一: 通过props传递 1) 共同的数据放在父组件上, 特有的数据放在自己组件内部(state) 2) 通过props可以传递一般数据和 ...
- 【动画消消乐】HTML+CSS 自定义加载动画 065
前言 Hello!小伙伴! 非常感谢您阅读海轰的文章,倘若文中有错误的地方,欢迎您指出- 自我介绍 ଘ(੭ˊᵕˋ)੭ 昵称:海轰 标签:程序猿|C++选手|学生 简介:因C语言结识编程,随后转入计 ...
- mysql实现主从复制、读写分离的配置方法(二)
由于接触主从复制,读写分离的时间比较短,应用还不够熟练,目的是能通过MyCat实现基础的读写分离操作. 其核心功能是分库分表,配合数据库的主从模式还可实现读写分离. 1. 测试环境 一台win10主机 ...
- 【动态规划】合唱队形 luogu-
分析 做两遍最长上升子序列,在遍历一下,取最大值. AC代码 #include <bits/stdc++.h> using namespace std; #define ms(a,b) m ...
- Django的ORM如何执行group by 语句
问题描述: 使用Django的ORM建立了如下Model: class Book(models.Model): name = models.CharField(max_length=300) page ...
- xmind8-update9 安装破解激活教程
xmind8是一款原型图设计流行的软件,相比于xmind2020功能更为丰富,比如画甘特图等.本教程来教大家如何对xmind8 update9进行安装激活,使用全部功能,无限期使用! 只看本文一篇即可 ...
- 深入刨析tomcat 之---第21篇 tomcat 对jsp页面支持的实现原理
writedby 张艳涛 web技术,以前的动态网页技术多是jsp页面,比如点击一个菜单目录,直接访问了一个LRJSDetailInput.jsp页面,这个页面 有<html><bo ...