[學習筆記] 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

傳送門


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM