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单调增(可选). 令: 在队首,如 ...
随机推荐
- Leetcode No.66 Plus One(c++实现)
1. 题目 1.1 英文题目 Given a non-empty array of decimal digits representing a non-negative integer, increm ...
- Docker部署Redis集群(主从复制 高可用)
环境 vmware12+centos7 关于环境安装可以参考我的另一篇博客 https://www.cnblogs.com/pengboke/p/13063168.html 1.清理环境 我这里用的虚 ...
- Spring学习总结(一)---谈谈对Spring IOC的理解(一:理论知识理解)
学习过Spring框架的人一定都会听过Spring的IoC(控制反转) .DI(依赖注入)这两个概念,对于初学Spring的人来说,总觉得IoC .DI这两个概念是模糊不清的,是很难理解的,今天和大家 ...
- mongodb,redis,mysql的区别和具体应用场景(转)
一.MySQL 关系型数据库. 在不同的引擎上有不同 的存储方式. 查询语句是使用传统的sql语句,拥有较为成熟的体系,成熟度很高. 开源数据库的份额在不断增加,mysql的份额页在持续增长. 缺点就 ...
- [刘阳Java]_酷炫视频播放器制作_界面篇
今天开始分享一篇酷炫播放器制作,包括界面+JS.整个案例非常类似腾讯视频,优酷视频,爱奇艺视频.我们先看一下效果图,然后这篇文章主要界面篇 是不是效果比较酷炫,那么我接着来给大家说一下这个界面设计思路 ...
- IO流 connect reset
目录 出现场景 解决思路 出现场景 通过外部OBS下载10文件,然后通过工具将这10个文件打包成一个文件A.zip上传,最后将这个A.zip下载并解压,解压A.zip后发现文件数量不是10个. 解决思 ...
- java网络编程基础——网络基础
java网络编程 网络编程基础 1.常用的网络拓扑结构: 星型网络.总线网络.环线网络.树形网络.星型环线网络 2.通信协议的组成 通信协议通常由3部分组成: 语义部分:用于决定通信双方对话类型 语法 ...
- P3643 [APIO2016]划艇
P3643 [APIO2016]划艇 题意 一个合法序列可表示为一个长度为 \(n\) 的序列,其中第 \(i\) 个数可以为 0 或 \([l_i,r_i]\) 中一个整数,且满足所有不为零的数组成 ...
- 【并查集模板】并查集模板 luogu-3367
题目描述 简单的并查集模板 输入描述 第一行包含两个整数N.M,表示共有N个元素和M个操作. 接下来M行,每行包含三个整数Zi.Xi.Yi 当Zi=1时,将Xi与Yi所在的集合合并 当Zi=2时,输出 ...
- 第三十一篇 -- 理一下.h和.cpp的关系
今天突然想到一个问题,我们平时写代码会将代码进行分类,写到不同的cpp里,然后要用到那个类里面的函数,就直接include .h文件就好了.然后今天就在想,.h里面都是一些声明,它是怎么链接到.cpp ...