[学习笔记] 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);
}

例2

传送门

posted @ 2021-06-20 21:36  C202044zxy  阅读(1820)  评论(0编辑  收藏  举报