原理

当序列 DP 的转移代价函数满足

  • 连续;
  • 凸函数;
  • 分段线性函数.

时,可以通过记录分段函数的最右一段 \(f_r(x)\) 以及其分段点 \(L\) 实现快速维护代价的效果。

如:$ f(x)= \begin{cases} -x-3 & (x \le -1) \ x &( -1 < x\le1)\ 2x-1 &(x > 1)\end{cases} $

可以仅记录 \(f_r(x)=2x-1\) 与分段点 \(L_f=\{-1,-1,1\}\) 来实现对该分段函数的存储。

注意:要求相邻分段点之间函数的斜率差为 \(1\) ,也就是说相邻两段之间斜率差 \(>1\) 的话,这个分段点要在序列里出现多次。

优秀的性质:

\(F(x),G(x)\) 均为满足上述条件的分段线性函数,那么 \(H(x)=F(x)+G(x)\) 同样为满足条件的分段线性函数,且 \(H_r(x)=F_r(x)+G_r(x),L_H=L_F\cup L_G\)。

该性质使得我们可以很方便得运用数据结构维护 \(L\) 序列。

例题

CF713C Sonya and Problem Wihtout a Legend

给定一个有 \(n\) 个正整数的数组,一次操作中,可以把任意一个元素加一或减一。(元素可被减至负数或 \(0\)),求使得原序列严格递增的求最小操作次数。

\(n\le 3000,a_i\le10^9\)

首先 \(a_i=a_i-i\) 转成不降.

设 \(f_{i,j}\) 表示第 \(i\) 个数为 \(j\) 的最小操作次数,则有

\[f_{i,j}=\min_{k\le j}\{f_{i-1, k}\}+|a_i-j|
\]

绝对值函数显然是满足条件的凸函数,所以考虑直接维护这个函数。

因为是求前缀最小值,同时绝对值函数最右直线的斜率为正,故不妨只留下斜率为 \(0\) 的左侧函数。

那么每次转移就是向 \(L_f\) 中插入 \(\{a_i,a_i\}\),同时,这时最右直线的斜率到达 \(1\),故删去最右分段点。

于是最后 \(f_r\) 的截距 \(b\) 就是答案。

点击查看代码
signed main(){
read(n);
lfor(i, 1, n) read(a[i]), a[i] -= i;
lfor(i, 1, n){
Q.push(a[i]), Q.push(a[i]);
b += -a[i]; int R = Q.top(); Q.pop();
b += R;
}
cout << b << endl;
return 0;
}
CF1534G A New Beginning

你在一个二维平面直角坐标系的原点 \((0,0)\) 上,可以向上或向右移动 \(1\) 单位长度任意次。

现在给 \(n\) 个点,第 \(i\) 个点为 \((x_i,y_i)\),求对这 \(n\) 个点都打上标记的最少花费。

具体的,在任意时刻,在 \((X,Y)\) 对 \((x,y)\) 打标记,需要花费 \(\max(|X-x|,|Y-y|)\) 。

\(1\leq n\leq8\times10^5;0\leq x_i,y_i\leq10^9;\)

首先,将切比雪夫距离转换成曼哈顿距离,\((x,y)\rightarrow(\frac{x+y}{2},\frac{x-y}{2})\)。

容易发现一个点一定是在走到 \(X=x_i\) 的时候标记最优,故设 \(f_{i,j}\) 表示 \(X=x_i,Y=j\),且所以 \(x_i\le X\) 的点已经全部标记的最小代价,则有

\[f_{i,j}=\min_{j-|x_{i+1}-x_i|\le k\le j+|x_{i+1}-x_i|}\{f_{i-1, k}\}+\sum_{(x,y),x=x_i}|j-y|
\]

那么考虑一次转移时,凸函数的变化是怎么样的。

即最低点往左右各延伸 \(|x_{i+1}-x_i|\) 的单位长度,其他位置平移,然后加上 \(\sum_{(x,y),x=x_i}|j-y|\)。

直接用大根堆和小根堆分别维护斜率为 \(0\) 的左右侧的分段点,平移就给整个堆打标记;与其他凸函数合并的时候,先直接按照大小加,然后讨论一下两个堆的分开位置是不是仍然斜率为 \(0\),不是的话,讨论一下是要把哪个堆顶的一些分段点放到另一个堆中。

点击查看代码
signed main(){
read(n);
lfor(i, 1, n){
int x, y; read(x), read(y);
p[i] = {x + y, x - y}, uni[++tot] = p[i].x;
} sort(p + 1, p + n + 1, [&](pal a, pal b){ return a.x < b.x; });
sort(uni + 1, uni + tot + 1), tot = unique(uni + 1, uni + tot + 1) - uni - 1;
lfor(i, 1, n) v[lsh(p[i].x)].pb(p[i].y); lfor(i, 1, tot){
if(i > 1) More(uni[i] - uni[i - 1]);
for(int it : v[i]){
K += 1, B += -it;
if(!Q2.empty() && it <= Q2.top() + det2){
Q1.push(it - det1), Q1.push(it - det1);
}else Q2.push(it - det2), Q2.push(it - det2);
} while(Q2.size() < K) Q2.push(Q1.top() + det1 - det2), Q1.pop();
while(Q2.size() > K) Q1.push(Q2.top() + det2 - det1), Q2.pop();
} while(Q2.size()) stk[++top] = Q2.top() + det2, Q2.pop();
while(top) K--, B += stk[top--];
cout << B / 2 << endl;
return 0;
}
CF1210G Mateusz and Escape Room

给出一个 \(n\) 个点的环,每个点上都有一堆石子,其中第 \(i\) 个点上的石子个数为 \(a_i\) 。

每次操作可以选择一堆石子,将其中的一颗石子向左或是向右移到相邻的石子堆中。

要求在进行一系列操作后,位置 \(i\) 的石子个数要在 \([L_i,R_i]\) 之间,问最少操作次数?

\(n,a_i,L_i,R_i\le 35000\).

我们可以设 \(x_i\) 表示第 \(i\) 堆石子向第 \(i+1\) 堆石子移动了 个石子 ( 可以为负 ),则最终状态下每个位置的石子数就是 \(b_i=a_i-x_i+x_{i-1}\),而答案就是 \(\sum_{i=1}^n|x_i|\)。

那么如果枚举 \(x_1\),就可以用 dp 解决这个问题了,设 \(f_{i,j}\) 已经确定了\(x_{1\sim i}\) ,且 \(2\sim i\) 已经合法的最小操作次数,则有:

\[f_{i,j}=\min_{j-(L_i-a_i)\le k\le j-(R_i-a_i)}\{f_{i-1, k}\}+|j|
\]

用上一题类似的方式维护即可。

其次因为答案关于 \(x_1\) 是单峰的,所以可以三分 \(x_1\),最终 \(O(n\log n\log \sum a_i)\) 解决问题。

点击查看代码
LL solve(int x1){
K = B = det1 = det2 = 0; K = n + 10, B = abs(x1) - K * x1;
lfor(i, 1, K) Q1.push(x1), Q2.push(x1); lfor(i, 2, n){
det1 -= (r[i] - a[i]), det2 -= (l[i] - a[i]), B += K * (l[i] - a[i]); K += 1;
if(!Q2.empty() && 0 <= Q2.top() + det2) Q1.push(-det1), Q1.push(-det1);
else Q2.push(-det2), Q2.push(-det2); while(Q2.size() < K) Q2.push(Q1.top() + det1 - det2), Q1.pop();
while(Q2.size() > K) Q1.push(Q2.top() + det2 - det1), Q2.pop();
} int top = 0, L = l[1] - a[1] + x1, R = r[1] - a[1] + x1; LL sum = INF;
while(!Q2.empty()) stk[++top] = Q2.top() + det2, Q2.pop();
while(!Q1.empty()) stk[++top] = Q1.top() + det1, Q1.pop();
sort(stk + 1, stk + top + 1); stk[top + 1] = INF, stk[0] = -INF;
if(stk[top] < L) sum = min(sum, K * L + B);
for(int i = top; i >= 1;){
LL x = stk[i];
LL y = K * x + B; if(L <= x && x <= R) sum = min(sum, y); while(stk[i] == x && i >= 1) --K, B += x, --i;
if(stk[i] < L && L < x) sum = min(sum, K * L + B);
if(stk[i] < R && R < x) sum = min(sum, K * R + B);
}
if(R < stk[0]) sum = min(sum, K * R + B); return Ans = min(Ans, sum), sum;
} int main(){
read(n);
lfor(i, 1, n) read(a[i]), read(l[i]), read(r[i]), A += a[i];
LL l = -A, r = A, mid;
while(l <= r){
mid = l + r >> 1;
if(solve(mid - 1) > solve(mid)) l = mid + 1;
else r = mid - 1;
}
cout << Ans << endl;
return 0;
}
P3642 [APIO2016]烟火表演(咕)

注意

  • 直线 \(kx+b\) 向右平移 \(\Delta\) 得到的是 \(k(x-\Delta)+b\),所以应当是 \(b\gets b-k\cdot\Delta\).
  • 由 \((k-1)x+b'=kx+b\),得到从右往左推算函数表达式时,\(b'\gets b+x\).
  • 如果初始条件是 \(x\) 处为 \(v\),其他地方为 \(\infty\),可以在堆中加入若干 \(x\),让斜率陡升。
  • 答案只能从区间 \([L,R]\) 中取得时,注意在无法取 \(0\) 的情况下是否真的取到了 \(L/R\)。
  • 如果斜率变化很大,也许可以用 \((x,cnt)\) 的形式来维护。

[笔记] Slope Trick:解决一类凸代价函数的DP优化问题的更多相关文章

  1. Slope Trick:解决一类凸代价函数DP优化

    [前言] 在补Codeforce的DP时遇到一个比较新颖的题,然后在知乎上刚好 hycc 桑也写了这道题的相关题解,这里是作为学习并引用博客的部分内容 这道题追根溯源发现2016年这个算法已经在API ...

  2. 重修 Slope Trick(看这篇绝对够!)

    Slope Trick 算法存在十余载了,但是我没有找到多少拍手叫好的讲解 blog,所以凭借本人粗拙的理解来写这篇文章. 本文除标明外所有图片均为本人手绘(若丑见谅),画图真的不容易啊 qwq(无耻 ...

  3. ng机器学习视频笔记(一)——线性回归、代价函数、梯度下降基础

    ng机器学习视频笔记(一) --线性回归.代价函数.梯度下降基础 (转载请附上本文链接--linhxx) 一.线性回归 线性回归是监督学习中的重要算法,其主要目的在于用一个函数表示一组数据,其中横轴是 ...

  4. 【学习笔记】动态规划—各种 DP 优化

    [学习笔记]动态规划-各种 DP 优化 [大前言] 个人认为贪心,\(dp\) 是最难的,每次遇到题完全不知道该怎么办,看了题解后又瞬间恍然大悟(TAT).这篇文章也是花了我差不多一个月时间才全部完成 ...

  5. iOS回顾笔记(07) -- UITableView的使用和性能优化

    iOS回顾笔记(07) -- UITableView的使用和性能优化 如果问iOS中最重要的最常用的UI控件是什么,我觉得UITableView当之无愧!似乎所有常规APP都使用到了UITableVi ...

  6. Android编程权威指南笔记2:解决R文件爆红问题和SDK概念

    在android studio中会遇到R文件的丢失,所以遇见这问题怎么解决呢? 重新检查资源文件中xml文件 最近一次编译时如果未生成R.java文件,项目中资源引用的地方都会出错.通常,这是某个xm ...

  7. andorid 开发笔记 -- 问题与解决

    1. SQLiteDataBase 中 TimeStamp 转化为 Date 的问题:java.text.ParseException: Unparseable date: "Sun Jan ...

  8. IntelliJ Idea各种技巧设置笔记和错误解决

    版本控制 GitHub GitHub提示找不到路径: 解决方法:去官方下载gitHub,然后在以下路径找到Git.exe并设置 C:\Users\你的用户\AppData\Local\GitHub\P ...

  9. python笔记-用python解决小学生数学题【转载】

    本篇转自博客:上海-悠悠 原文地址:http://www.cnblogs.com/yoyoketang/tag/python/ 前几天有人在群里给小编出了个数学题: 假设你有无限数量的邮票,面值分别为 ...

随机推荐

  1. Log4j使用(转)

    from:http://www.cnblogs.com/ITtangtang/p/3926665.html 一.Log4j简介Log4j有三个主要的组件:Loggers(记录器),Appenders ...

  2. Springmvc入门基础(五) ---controller层注解及返回类型解说

    0.@Controller注解 作用:通过@Controller注解,注明该类为controller类,即控制器类,需要被spring扫描,然后注入到IOC容器中,作为Spring的Bean来管理,这 ...

  3. Bean 工厂和 Application contexts 有什么区别?

    Application contexts提供一种方法处理文本消息,一个通常的做法是加载文件资源(比如镜像),它们可以向注册为监听器的bean发布事件.另外,在容器或容器内的对象上执行的那些不得不由be ...

  4. java -的字符串hei

    字符串的不可变 String 对象是不可变的.查看 JDK 文档你就会发现,String 类中每一个看起来会修改 String 值的方法,实际上都是创建了一个全新的 String 对象,以包含修改后的 ...

  5. 全方位讲解 Nebula Graph 索引原理和使用

    本文首发于 Nebula Graph Community 公众号 index not found?找不到索引?为什么我要创建 Nebula Graph 索引?什么时候要用到 Nebula Graph ...

  6. 《剑指offer》面试题2:实现Singleton 模式

    面试题2:实现Singleton 模式 题目:设计一个类,我们只能生成该类的一个实例.   只能生成一个实例的类是实现了Singleton (单例)模式的类型.由于设计模式在面向对象程序设计中起着举足 ...

  7. Creating a File Mapping Object

    创建一个文件映射对象 映射一个文件的第一步是通过调用CreateFile函数来打开一个文件.为了确保其他的进程不能对文件已经被映射的那一部分进行写操作,你应该以唯一访问(exclusive acces ...

  8. 面试问题整理之flex 布局

  9. 前端实现导出excel

    结果: 将网页上拿到的数据导出成excel文件 实现: HTML代码 <div> <button type="button" onclick="expo ...

  10. Python程序的流程

    1 """ 2 python程序的流程 3 """ 4 # ------------- 分支结构---------------- 5 # i ...