Slope Trick 算法存在十余载了,但是我没有找到多少拍手叫好的讲解 blog,所以凭借本人粗拙的理解来写这篇文章。

本文除标明外所有图片均为本人手绘(若丑见谅),画图真的不容易啊 qwq(无耻求赞)。

Slope Trick 是啥?

凸代价函数DP优化。

具体哪种题目?

AcWing273. 分级

CF713C Sonya and Problem Wihtout a Legend

CF13C Sequence

P2893 [USACO08FEB]Making the Grade G

P4331 [BalticOI 2004]Sequence 数字序列

P4597 序列 sequence

P8118 Mystery

千万题目汇成一句话:

给定长度为 \(N(\le 10^6)\) 的序列 \(A\),构造一个长度为 \(N\) 的非降序列 \(B\),最小化 \(S=\sum\limits^N_{i=1}|A_i−B_i|\),求出 \(S\) 的最小值和 \(B\) 的构造方案。

注意 Slope Trick 不止能够解决这个问题,这个题目只是便于举例而已。

暴力咋做?

就是 \(O(n^2)\) 的 DP。

设 \(f_{i,j}\) 表示 \(A\) 中第 \(i\) 个数变为 \(j\),前 \(i\) 个数变为非降序列的最小代价,即

\[\min_{B_1\le B_2\le\dots\le B_i=j}\sum_{k=1}^{i}|A_k−B_k|
\]

则有递推式

\[f_{i,j}=|A_i-j|+\min_{k=minV}^{j} f_{i-1,k}
\]

其中 \(minV\) 指值域下界。

当然,为了后续的拓展,我们设

\[g_{i,j}=\min_{k=minV}^{j}f_{i,k}
\]

则递推式改成

\[f_{i,j}=|A_i-j|+g_{i-1,j}
\]

是不是非常美观?

过渡

在下一步之前,我们需要几个定义和引理。

引理 A

两个斜率分别为 \(a,b\) 的一次函数相加的斜率为 \(a+b\)。

定义 1

我们称这样的函数为美妙的函数

  • 函数连续。
  • 函数由若干条一次函数(或常数函数)拼接而成(所以是分段函数),且一次函数的斜率为整数。
  • 函数下凸,即若干条一次函数的斜率从左往右单调非减。

引理 B

任意两个美妙的函数相加还是美妙的函数。

定义 2

设一个连续函数 \(f(x)\) 的前缀最小函数 \(g(x)\) 为

\[g(x)=\min_{x'\le x}f(x')
\]

引理 C

一个美妙的函数的前缀最小函数还是美妙的函数,且最后一段(至 \(x\to\infty\))为常数函数。

咋拓展到 Slope Trick?(正题)

先回忆一下递推式

\[f_{i,j}=|A_i-j|+g_{i-1,j}
\]

我们设 \(F_i(x)\) 函数

\[F_i(x)=f_{i,x}
\]

类似地

\[G_i(x)=g_{i,x}
\]

最后设

\[H_i(x)=|x-A_i|
\]

我们再次改写递推式

\[F_i(x)=H_i(x)+G_{i-1}(x)
\]

简洁美观!(请牢记这个公式)

由数学归纳法得到 \(F,G,H\) 都是美妙的函数。

我们维护 \(S_1,S_2,\dots,S_c\) 为 \(G\) 函数的转折点。

来几张图演示一下。

设这个 \(G_i\) 从一次函数到常数函数的转折点为 \(P_i\)。

值得注意的是,若一个转折点左边斜率 \(>\) 右边斜率 \(+1\),则这个点是要再重复 \((\) 左边斜率 \(-\) 右边斜率 \(-1)\) 次的,即:

然后加上 \(H_i\),要分类:

\(A_i\ge P_{i-1}\)

这样答案(为最右边水平部分的 \(y\) 坐标)不变,即

\[\begin{aligned}
F_i(P_i)&=H_i(P_i)+G_{i-1}(P_i)
\\
&=G_{i-1}(P_i)
\\
&=G_{i-1}(P_{i-1})
\\
&=F_{i-1}(P_{i-1})
\end{aligned}
\]

没有贡献。

而且对于 \(\{S\}\),只用将 \(A_i\) 插入 \(\{S\}\) 末尾即可。

\(A_i<P_{i-1}\)

这样答案为

\[\begin{aligned}
F_i(P_i)&=H_i(P_i)+G_{i-1}(P_i)
\\
&=P_i-A_i+F_{i-1}(P_i)
\\
&=P_i-A_i+F_{i-1}(P_{i-1})+P_{i-1}-P_i
\\
&=F_{i-1}(P_{i-1})+P_{i-1}-A_i
\end{aligned}
\]

即增加

\[F_i(P_i)-F_i(P_{i-1})=P_{i-1}-A_i
\]

所以将 \(Ans+\!=P_{i-1}-A_i\)。

此时 \(A_i\) 插入 \(\{S\}\) 要两次,因为他是绝对值函数的转折点,所以两边斜率差值为 \(2\)。


相信很多人都蒙了,我们具象一下。

想象有一条左右无限长的铁丝。

初始化:一开始平放在高度 \(0\) 的位置(\(F_0(x)=G_0(x)=0\))

然后进行 \(n\) 次操作,第 \(i\) 次:

  1. 铁丝横坐标为 \(A_i\) 的地方用尖嘴钳固定住。

  2. 将尖嘴钳左边的部分向上翘 \(1\) 单位的斜率(这部分铁丝每一点都要弯 \(1\) 斜率)。

  3. 尖嘴钳右边的部分同理向上翘 \(1\) 单位的斜率。这样整条铁丝更像「U」形。

这样我们从 \(G_{i-1}\) 得到了 \(F_i\)。

  1. 将右边翘起的部分压平。即在右边找到一个点使得这里的铁丝是水平的(导数为 \(0\))然后从这里往右全部捋成水平的。

这样我们从 \(F_i\) 得到了 \(G_i\)。

  1. 松开尖嘴钳。

最后答案为整个铁丝的最低高度值。

相信前文仔细阅读的小可爱们一定懂了这段扭铁丝具体在对凸函数干嘛……


所以用堆维护 \(S_1,S_2,\dots,S_c\) 即可。

总时间 \(O(n\log n)\)。

代码?

int n,x,ans=0,b[N];
priority_queue<int> q;
int main(){
cin>>n;
For(i,1,n){
cin>>x;
q.push(x);
if(q.top()!=x){
ans+=q.top()-x;
q.pop();
q.push(x);
}
b[i]=q.top();
}
Rof(i,n-1,1) ckmn(b[i],b[i+1]);
cout<<ans<<endl;
For(i,1,n) cout<<b[i]<<" "; cout<<endl;
return 0;
}

更多?

当然,Slope Trick 不止这种建模和推导。

为了证明这一点,我们再举一个例子。

题面

题目:abc250_g Stonks

大意:

已知接下来 \(n(\le 2\times 10^5)\) 天的股票价格 \(1\le P_1,P_2,\dots,P_n\le 10^9\)。

每天你可以(三选一):

  • 买进一股股票
  • 卖出一股股票
  • 什么也不做

\(n\) 天之后你拥有的股票应为 \(0\)。

你最初有足够多的钱,求 \(n\) 天结束后能获得的最大利润。

解答

带悔贪心

我们可以快速想出一种贪心策略:买入价格最小的股票,在可以赚钱的当天卖出。

显然我们可以发现,上面的贪心策略是错误的,因为我们买入的股票可以等到可以赚最多的当天在卖出。

我们考虑设计一种反悔策略,使所有的贪心情况都可以得到全局最优解。(即设计反悔自动机的反悔策略)

我们先把当前的价格放入小根堆一次(这次是以上文的贪心策略贪心),判断当前的价格是否比堆顶大,若是比其大,我们就将差值计入全局最优解,再将当前的价格放入小根堆(这次是反悔操作)。相当于我们把当前的股票价格若不是最优解,就没有用,最后可以得到全局最优解。

上面的等式即被称为反悔自动机的反悔策略,因为我们并没有反复更新全局最优解,而是通过差值消去中间项的方法快速得到的全局最优解。

Slope Trick

首先我们考虑最朴素的 \(O(n^2)\) DP 做法:\(f_{i,j}\) 表示前 \(i\) 天过完,现在手上 \(j\) 张股票,所盈利的最大价值。

\[f_{i,j}=\max\{f_{i-1,j+1}+P_i\, ,\, f_{i-1,j}\, ,\, f_{i-1,j-1}-P_i\}
\]

然后我们设函数 \(F_i(x)=f_{i,x}\)(老套路了)。

\[F_i(x)=\max\{F_{i-1}(x+1)+P_i\, ,\, F_{i-1}(x)\, ,\, F_{i-1}(x-1)-P_i\}
\]

也就是说将函数 \(F_{i-1}\)

  • 向上 \(P_i\) 单位,向左 \(1\) 单位复制一份,设为 \(F^+_{i-1}\)。
  • 向下 \(P_i\) 单位,向右 \(1\) 单位复制一份,设为 \(F^-_{i-1}\)。
  • 自己 \(F_{i-1}\) 也保留。

再求三者的上凸包(\(\max\))即为 \(F_i\)。

这里引用 Atcoder 官方题解的图:

我们发现 \(F_{i-1}\) 只有左边斜率 \(>-P_i\) 且右边斜率 \(<-P_i\) 的点才会相对于 \(F_{i-1}^+,F_{i-1}^-\)「露在外面」。

这时会在本来的斜率序列中插入两个斜率为 \(-P_i\) 的线段,同时将本来最靠左的线段去掉。所以用堆维护这个斜率序列,插入两个 \(P_i\),弹出一次堆顶。

当然,如果 \(P_i\) 小于堆顶,则只要插入 \(P_i\) 即可。

代码

两种解法代码一样神奇吧。

#include<bits/stdc++.h>
using namespace std;
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
#define For(i,j,k) for(int i=j;i<=k;i++)
#define int long long
#define N 200010 int n,ans=0,x;
priority_queue<int,vector<int>,greater<int> > q;
signed main(){IOS;
cin>>n;
For(i,1,n){
cin>>x;
if(i!=1 && q.top()<x){
ans+=x-q.top();
q.pop();
q.push(x);
}
q.push(x);
}
cout<<ans<<endl;
return 0;}

重修 Slope Trick(看这篇绝对够!)的更多相关文章

  1. ASP.NET Core WebApi使用Swagger生成api说明文档看这篇就够了

    引言 在使用asp.net core 进行api开发完成后,书写api说明文档对于程序员来说想必是件很痛苦的事情吧,但文档又必须写,而且文档的格式如果没有具体要求的话,最终完成的文档则完全取决于开发者 ...

  2. .NET Core实战项目之CMS 第二章 入门篇-快速入门ASP.NET Core看这篇就够了

    作者:依乐祝 原文链接:https://www.cnblogs.com/yilezhu/p/9985451.html 本来这篇只是想简单介绍下ASP.NET Core MVC项目的(毕竟要照顾到很多新 ...

  3. 想了解SAW,BAW,FBAR滤波器的原理?看这篇就够了!

    想了解SAW,BAW,FBAR滤波器的原理?看这篇就够了!   很多通信系统发展到某种程度都会有小型化的趋势.一方面小型化可以让系统更加轻便和有效,另一方面,日益发展的IC**技术可以用更低的成本生产 ...

  4. [译]ASP.NET Core Web API 中使用Oracle数据库和Dapper看这篇就够了

    [译]ASP.NET Core Web API 中使用Oracle数据库和Dapper看这篇就够了 本文首发自:博客园 文章地址: https://www.cnblogs.com/yilezhu/p/ ...

  5. ExpandoObject与DynamicObject的使用 RabbitMQ与.net core(一)安装 RabbitMQ与.net core(二)Producer与Exchange ASP.NET Core 2.1 : 十五.图解路由(2.1 or earler) .NET Core中的一个接口多种实现的依赖注入与动态选择看这篇就够了

    ExpandoObject与DynamicObject的使用   using ImpromptuInterface; using System; using System.Dynamic; names ...

  6. Vue学习看这篇就够

    Vue -渐进式JavaScript框架 介绍 vue 中文网 vue github Vue.js 是一套构建用户界面(UI)的渐进式JavaScript框架 库和框架的区别 我们所说的前端框架与库的 ...

  7. 【转】ASP.NET Core WebApi使用Swagger生成api说明文档看这篇就够了

    原文链接:https://www.cnblogs.com/yilezhu/p/9241261.html 引言 在使用asp.net core 进行api开发完成后,书写api说明文档对于程序员来说想必 ...

  8. Pycharm新手教程,只需要看这篇就够了

    pycharm是一款高效的python IDE工具,它非常强大,且可以跨平台,是新手首选工具!下面我给第一次使用这款软件的朋友做一个简单的使用教程,希望能给你带来帮助! 目前pycharm一共有两个版 ...

  9. Python GUI之tkinter窗口视窗教程大集合(看这篇就够了) JAVA日志的前世今生 .NET MVC采用SignalR更新在线用户数 C#多线程编程系列(五)- 使用任务并行库 C#多线程编程系列(三)- 线程同步 C#多线程编程系列(二)- 线程基础 C#多线程编程系列(一)- 简介

    Python GUI之tkinter窗口视窗教程大集合(看这篇就够了) 一.前言 由于本篇文章较长,所以下面给出内容目录方便跳转阅读,当然也可以用博客页面最右侧的文章目录导航栏进行跳转查阅. 一.前言 ...

  10. C#实现多级子目录Zip压缩解压实例 NET4.6下的UTC时间转换 [译]ASP.NET Core Web API 中使用Oracle数据库和Dapper看这篇就够了 asp.Net Core免费开源分布式异常日志收集框架Exceptionless安装配置以及简单使用图文教程 asp.net core异步进行新增操作并且需要判断某些字段是否重复的三种解决方案 .NET Core开发日志

    C#实现多级子目录Zip压缩解压实例 参考 https://blog.csdn.net/lki_suidongdong/article/details/20942977 重点: 实现多级子目录的压缩, ...

随机推荐

  1. chubby 是什么,和 zookeeper 比你怎么看?

    chubby 是 google 的,完全实现 paxos 算法,不开源.zookeeper 是 chubby的开源实现,使用 zab 协议,paxos 算法的变种.

  2. 用maven建立一个工程5

    在命令行里面输入cd myapp再按回车 再输入mvn compile再按回车 再输入 cd target按回车 再输入cd../按回车 再输入mvn package按回车 最后输入java -cla ...

  3. 用maven建立一个工程4

    在命令行里面输入cd C:\Users\admin\Documents\hello 然后按回车 再输入这行代码 mvn archetype:generate -DgroupId=com.liyongz ...

  4. C++ | 虚函数产生条件

    虚函数产生的条件 能否成为虚函数主要有以下两种判断依据,如果以下两种条件均满足,则具有成为虚函数的条件. 1.虚函数机制为动多态提供支持,而虚函数表中存放着虚函数的地址.因此虚函数必须是可以取地址的函 ...

  5. 访问控制protected是不同包中对子类可见,什么意思?

    2.2 以下例子说明:protected是不同包中对子类可见,对非子类不可见. 例1.2.2.a:---本例为正常用法. package p1;public class A {    protecte ...

  6. ps让图片背景透明

    效果图:  jpg=>png,背景透明 步骤: 1.选择橡皮工具的第三个  魔术橡皮 保存为png,    按住Ctrl+alt+shift+s    保存:

  7. 使用html5绘图技术事项调用摄像头拍照;

    在mui框架中调用手机摄像头进行拍照可以直接使用原声的HTML5: 以下是HTML代码 <video id="video" width="640" hei ...

  8. C++---初识C++

    C和C++的关系 C语言是结构化和模块化的语言, 面向过程. C++是在C语言的基础上, 增加了面向对象的机制, 并对C语言的功能进行了扩充. 变量的定义可以出现在程序中的任何行 提供了标准输入输出流 ...

  9. Struts2中将表单数据封装到List和Map集合中

    一.将表单数据封装到Map集合中 1.创建MapAction类 import cn.entity.User; import com.opensymphony.xwork2.ActionSupport; ...

  10. Edu Cf Round 105 (Div. 2) B. Berland Crossword 1.读懂题, 2. 思维

    一. 原题链接 https://codeforces.com/contest/1494/problem/B   二. 题意 + 题解: 没看懂题目, 懵了好久, 先狡辩一下当时误解的句子, 英语是硬伤 ...