• $ ext{1D/1D}$ 动态规划的三种优化


    神必博主的沙雕前言

    参考文献:

    1
    2
    3
    4


    概念明晰

    所谓 ( ext{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) ag{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) ag{$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 - ig|v+1+a[i+1]ig|^P le |u|^P- ig|u+1+a[i+1]ig|^P ]

    由于 (u<v),这也就等价于证明 (|x|^P - |x+z|^P ;;(zin[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
    ", 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] << '
    ';
                //print
                print(n);
            }
            else cout << "Too hard to arrange
    ";
            cout << "--------------------
    ";
        }
        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
    ", s[i]);
    }
    void print() {
    	if(dp[n]>1e18) puts("Too hard to arrange");
    		else {
    			cout << (long long)dp[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;
    }
    

    啊这, 还是两格空格缩进好看, 完全不一样的feel啊。

    分治
    有点难用, 不写了。

    单调队列(太简单了不写了)

    单调队列优化多重背包

    斜率优化

  • 相关阅读:
    SDUT_1743 最优合并问题
    并查集路径压缩方法
    java定时器
    出路在哪里?出路在于思路!
    ztree学习
    sql
    java乱码问题详解值得收藏
    js 增加删除表格的行
    java DataBaseExecutor
    java增删改查
  • 原文地址:https://www.cnblogs.com/tztqwq/p/13470090.html
Copyright © 2020-2023  润新知