[学习笔记] Slope trick 折线算法

前言

这个东西 slope trick on codeforces 已经讲得很清楚了,我把他翻译成中文版,这能叫引进算法吗?

好像没有听说过它的中文名,我就叫他折线算法吧。

原理

折线算法是描述函数的一种方式,我称适用于折线算法的函数为折线函数,折线函数通常满足下列性质:

  • 它是连续的。
  • 它可以被分成若干个直线函数,有其固定的斜率。
  • 它具有凸凹性,也就是每个直线函数斜率单增或单减。

举个栗子:(f(x)=|x|) 就是最常见的折线函数。

定义转折点为折线函数斜率突变的点的横坐标,绝对值函数转折点就是 (0)

根据上述可以得出表示表示折线函数的方式,可重集 (S) 里的元素表示转折点,并且转折点出现一次代表折线函数斜率突变 (1),再加上最后一段的直线函数方程即可,绝对值函数可以表示成:([y=x,S={0,0}])

折线函数有一个最重要的性质:可合并性( t mergable)),如果 (f(x))(g(x)) 是具有相同凸凹性的两个折线函数,那么合并之后的 (h(x)) 满足 (S_h=S_fcup S_g),最后一段的直线函数重新计算一下即可。

原理讲完了,可以来做题了(( t 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(nlog n))

总结

折线算法需要单点函数值的合并,所以设计暴力状态的时候需要注意一下。

证明折线函数可以使用归纳法,还可以定义辅助函数,主要利用的就是 ( t 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
",ans);
}

例2

传送门

原文地址:https://www.cnblogs.com/C202044zxy/p/14908532.html