1. 程式人生 > 實用技巧 >$\text{1D/1D}$ 動態規劃的三種優化

$\text{1D/1D}$ 動態規劃的三種優化

神必博主的沙雕前言

參考文獻:

1
2
3
4


概念明晰

所謂 \(\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\)

, 若記 \(s[i] = \sum_{k=1}^i a[k]\), 則 \(w(j,i) = \Bigg|i-j-1+s[i]-s[j]-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])\) 單調不增。

分類討論:

  1. \(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;
}

分治
有點難用, 不寫了。

單調佇列

斜率優化