$\text{1D/1D}$ 動態規劃的三種優化
神必博主的沙雕前言
參考文獻:
概念明晰
所謂 \(\text{1D/1D}\) 動態規劃, 指的是狀態數和單狀態決策數都是 \(O(n)\) 的動態規劃方程, 暴力求解的時間複雜度為 \(O(n^2)\)。
四邊形不等式
\(f[i] = min/max_{j \in [1,i-1]} \{ f[j] + w(j,i) \}\) 形式及四邊形不等式的定義
下面只考慮取 \(min\)。
決策單調性是指對於 \(a<b<c<d\), 若對於 \(c\) 從 \(b\) 轉移來不比從 \(a\) 轉移來差, 那麼對於 \(d\) 從 \(b\) 轉移來就不比從 \(a\)
\[f[b] + w(b, c) \leq f[a] + w(a, c) \Rightarrow f[b] + w(b, d) \leq f[a] + w(a, d) \tag{1} \]
那麼顯然對於 \(w\) 函式來說, 如果滿足這個等式:\(-w(b,c)+w(b,d) \le -w(a,c)+w(a,d)\), 就可以使得 \((1)\) 成立。
將此等式轉化一下, 就得到了 四邊形不等式:
\[w(a,c)+w(b,d) \le w(a,d)+w(b,c) \tag{$a<b<c<d$} \]
對於 \(max\) 的情況也差不多, 只是等號的方向變了一下。
可以看出四邊形不等式與決策單調性有著很親密的關係。
四邊形不等式的判定與性質
還是以 \(min\) 來說明。
有一個與四邊形不等式等價的式子, 若函式 \(w\) 對於任意 \(a<b\), \(w(a,b)+w(a+1,b+1) \le w(a+1,b) + w(a,b+1)\), 則函式 \(w\) 滿足四邊形不等式。
不會證。
3道練證明的例題
HNOI2008玩具裝箱
CF868F
太簡單了不寫了。
詩人小G
很顯然的 \(DP\) 方程:
\[f[i] = \min_{j=0}^{i-1}\{f[j]+w(j,i)\} \]
其中, \(w(j,i) = \Bigg| [i-(j+1)+1-1] + \sum_{k=j+1}^i a[k] -L \Bigg|^P\)
如果 \(w\) 滿足四邊形不等式, 那麼這個 \(DP\) 方程就滿足決策單調性。
只需證明 \(w(i,j) + w(i+1,j+1) \le w(i+1,j) + w(i,j+1)\) 。
展開, 得到
\[\Bigg|i-j-1+s[i]-s[j]-L \Bigg|^P + \Bigg|i-j-1+s[i+1]-s[j+1]-L \Bigg|^P \le \]
\[\Bigg|i-j+s[i+1]-s[j]-L \Bigg|^P + \Bigg|i-j-2+s[i]-s[j+1]-L \Bigg|^P \]
設 \(u = i-j-2+s[i]-s[j+1]-L\), \(v = i-j-1+s[i]-s[j]-L\), 則原式變成
\[|v|^P - \big|v+1+a[i+1]\big|^P \le |u|^P- \big|u+1+a[i+1]\big|^P \]
由於 \(u<v\),這也就等價於證明 \(|x|^P - |x+z|^P \;\;(z\in[1,+\infty])\) 單調不增。
分類討論:
- \(x \in [0,+\infty]\)
\(|x|^P - |x+z|^P = x^P - (x+z)^P\)
導數是 \(Px^{P-1} - P(x+z)^{P-1}\)
顯然是小於等於 \(0\) 的。
2.\(x \in (-\infty, 0)\) 且 \(P\) 為偶數
\(|x|^P - |x+z|^P = x^P - (x+z)^P\)
導數依然是 \(Px^{P-1} - P(x+z)^{P-1}\), 由於 \(P-1\) 是奇數, 所以依然是小於等於 \(0\) 的。
3.\(x \in (-\infty, 0)\) 且 \(P\) 為奇數, \(x+z \ge 0\)
\(|x|^P - |x+z|^P = -x^p - (x+z)^P\)
導數為 \(-Px^{P-1} - P(x+z)^{P-1}\)
顯然是小於等於 \(0\) 的。
4.\(x \in (-\infty, 0)\) 且 \(P\) 為奇數, \(x+z < 0\)
\(|x|^P - |x+z|^P = -x^p + (x+z)^P\)
導數為 \(-Px^{P-1} + P(x+z)^{P-1}\)
顯然 \(x+z \ge x\), 但 \(x+z\) 為負數, 大於 \(x\) 的負數中沒有絕對值比 \(x\) 大的, 故這個導數也是小於等於 \(0\) 的。
Q.E.D.
可以放心用決策單調性優化了。
實現方法
二分棧
從左往右掃, 用掃到的狀態更新它後面的狀態。由於一個狀態只會從它左邊的狀態轉移來, 所以此演算法的正確性得以保證。
由於決策單調性, 每次遭到更新的狀態集一定是序列的一段字尾, 可以快速計算。
具體實現的時候用棧維護幾個連續的段, 每個段記錄其左端點,就可以描繪出整個轉移序列。每掃到 \(i\) 的時候, 先把 \(i\) 的 \(dp\) 值計算出來, 再用其更新後面狀態的轉移。
實現的時候有幾個關鍵點, 決定著程式的常數。
以 詩人小G 這道題為例。
首先是一個糟糕的實現, 雖然能過, 但是耗時並不優秀。
(由於沒有寫註釋, 觀看的時候只看程式碼的醜陋程度就行了)
//對於每段不僅維護了左端點還維護了右端點, 並且加入了繁雜的分類討論
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5;
int n,stn,mdzz;
char s[maxn][35];
int S[maxn];
int tot, q[maxn], l[maxn], r[maxn];
long double f[maxn];
long double ksm(long double a, int b) {
long double res = 1;
for(;b;b>>=1, a*=a)
if(b&1) res *= a;
return res;
}
long double val(int pr, int nx) {
long double res = f[pr];
// nx - pr + S[nx] - S[pr] - stn
res += ksm(abs(S[nx]-S[pr] + (nx-pr-1) - stn), mdzz);
return res;
}
int pre[maxn];
void fuck(int i)
{
int L=1, R=tot;
while(L!=R) {
int mid = (L+R+1) >> 1;
if(l[q[mid]] > i) R = mid-1;
else L = mid;
}
int pr = q[L];
pre[i] = pr;
f[i] = val(pr, i);
//cout << pr << ' ';
}
void print(int x)
{
if(!x) return;
int pr = pre[x];
print(pr);
for(int i=pr+1; i<x; ++i) printf("%s ", s[i]);
printf("%s\n", s[x]);
}
int main() {
int t;
cin >> t;
while(t--)
{
scanf("%d%d%d", &n, &stn, &mdzz);
for(int i=1; i<=n; ++i) {
scanf("%s", s[i]);
S[i] = S[i-1] + strlen(s[i]);
}
q[tot=1] = 0;
l[0]=1, r[0]=n;
for(int i=1; i<n; ++i) {
fuck(i);
int L=1, R=tot;
while(L!=R) {
int mid = (L+R+1) >> 1;
if(val(i,l[q[mid]]) < val(q[mid],l[q[mid]])) R = mid-1;
else L=mid;
}
int nowb = L;
L=l[q[nowb]], R=r[q[nowb]];
while(L!=R) {
int mid = (L+R) >> 1;
if(val(i, mid) < val(q[nowb], mid)) R=mid;
else L=mid+1;
}
int nowp = L;
if(val(i, nowp) > val(q[nowb], nowp)) ++nowp;
if(nowp == n+1) continue;
while(l[q[tot]] > nowp) --tot;
if(l[q[tot]] == nowp) q[tot]=i, l[i] = nowp, r[i] = n;
else {
r[q[tot]] = nowp-1;
q[++tot] = i;
l[i] = nowp;
r[i] = n;
}
}
fuck(n);
if(f[n] <= 1e18) {
cout << (long long)f[n] << '\n';
//print
print(n);
}
else cout << "Too hard to arrange\n";
cout << "--------------------\n";
}
return 0;
}
接下來是比較優美的實現。
//這份實現充分體現了二分棧演算法的特性, 理解這份實現對更好理解二分棧演算法有幫助
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
int n,l,p;
char s[N][33];
int a[N];
long double ksm(long double x, int b) {
long double res = 1;
for(;b;b>>=1, x=x*x)if(b&1) res*=x;
return res;
}
long double dp[N];
long double val(int j, int i) {
return dp[j] + ksm(abs(i-j-1+a[i]-a[j]-l), p);
}
int fr[N], lp[N], tp, tra;
int fid(int x) {
int l = lp[tp], r=n+1;
while(l!=r) {
int mid = (l+r) >> 1;
if(val(x,mid) < val(fr[tp],mid)) r=mid;
else l = mid+1;
}
return l;
}
void solve() {
memset(lp,0,sizeof lp);
tp = tra = 1;
lp[1]=1, fr[1]=0;
for(int i=1;i<=n;++i) {
if(i==lp[tra+1]) ++tra;
dp[i] = val(fr[tra], i);
while(lp[tp]>i && val(i, lp[tp]) < val(fr[tp],lp[tp]) ) --tp;
int tmp = fid(i);
if(i<=n) ++tp, fr[tp]=i, lp[tp]=tmp;
}
}
int pre[N];
void Prin(int i) {
if(!i) return;
Prin(pre[i]);
for(int j=pre[i]+1;j<i;++j) printf("%s ",s[j]);
printf("%s\n", s[i]);
}
void print() {
if(dp[n]>1e18) puts("Too hard to arrange");
else {
cout << (long long)dp[n] << '\n';
// 這裡偷懶寫了大常數 owo
// awsl
int r = n;
while(tp) {
while(r>=lp[tp]) pre[r--]=fr[tp];
--tp;
}
Prin(n);
}
}
int main() {
int t; cin>>t; while(t--) {
memset(a,0,sizeof a);
scanf("%d%d%d",&n,&l,&p);
for(int i=1;i<=n;++i) {
scanf("%s",s[i]); a[i]= a[i-1] + strlen(s[i]);
}
a[n+1] = a[n] + 1;
solve();
print();
puts("--------------------");
}
return 0;
}
分治
有點難用, 不寫了。