前言
這個東西 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);
}