[学习笔记] Slope trick 折线算法
前言
这个东西 slope trick on codeforces 已经讲得很清楚了,我把他翻译成中文版,这能叫引进算法吗?
好像没有听说过它的中文名,我就叫他折线算法吧。
原理
折线算法是描述函数的一种方式,我称适用于折线算法的函数为折线函数,折线函数通常满足下列性质:
- 它是连续的。
- 它可以被分成若干个直线函数,有其固定的斜率。
- 它具有凸凹性,也就是每个直线函数斜率单增或单减。
举个栗子:\(f(x)=|x|\) 就是最常见的折线函数。
定义转折点为折线函数斜率突变的点的横坐标,绝对值函数转折点就是 \(0\)
根据上述可以得出表示表示折线函数的方式,可重集 \(S\) 里的元素表示转折点,并且转折点出现一次代表折线函数斜率突变 \(1\),再加上最后一段的直线函数方程即可,绝对值函数可以表示成:\([y=x,S=\{0,0\}]\)
折线函数有一个最重要的性质:可合并性(\(\tt mergable\)),如果 \(f(x)\) 和 \(g(x)\) 是具有相同凸凹性的两个折线函数,那么合并之后的 \(h(x)\) 满足 \(S_h=S_f\cup S_g\),最后一段的直线函数重新计算一下即可。
原理讲完了,可以来做题了(\(\tt zy\):一节课讲课十分钟,做题三十分钟)
例1
题目描述
解法
把严格递增变成不严格递增要好做一些,执行 a[i]-=i
的操作即可。
然后设计出暴力 \(dp\),设 \(dp[i][x]\) 表示让前 \(i\) 个元素递增,第 \(i\) 个元素是 \(\leq x\) 的最小操作数。
用折线算法维护这东西,设 \(f_i(x)=dp[i][x]\),首先要证明 \(f_i\) 是折线函数,定义辅助函数 \(g_i(x)\) 表示 \(a_i=x\) 时的最小操作数,不难发现 \(f_i\) 其实就是 \(g_i\) 的前缀最小值。
可以考虑归纳证明,\(f_0\) 是折线函数,假设 \(f_{i-1}\) 是折线函数,那么 \(g_i=f_{i-1}+|x-a_i|\),所以 \(g_i\) 也是折线函数,因为 \(f_i\) 是 \(g_i\) 的前缀最小值,等价于把后面斜率 \(>0\) 的一段变平,所以 \(f_i\) 也是折线函数。
算法就蕴含在证明中,每次直接合并一个绝对值函数上来,然后更新最低点的函数值,最后把斜率为 \(1\) 的那一段掐掉即可,因为合并前斜率最大是 \(0\) 所以合并后最大是 \(1\),时间复杂度 \(O(n\log n)\)
总结
折线算法需要单点函数值的合并,所以设计暴力状态的时候需要注意一下。
证明折线函数可以使用归纳法,还可以定义辅助函数,主要利用的就是 \(\tt mergable\) 的性质。
#include <cstdio>
#include <queue>
using namespace std;
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,ans;priority_queue<int> q;
int Abs(int x)
{
return x>0?x:-x;
}
signed main()
{
n=read();
q.push(-2e9);
for(int i=1;i<=n;i++)
{
int x=read()-i;
q.push(x);q.push(x);
ans+=q.top()-x;
q.pop();
}
printf("%lld\n",ans);
}