• 决策单调性优化入门


    前置知识

    先学下面的东西

    • 单调队列

    四边形不等式

    定义

    对于一个二元函数 \(w(l,r)\),如果有下面的命题成立,则称 \(w\) 满足四边形不等式:

    \[\forall a \leq b < c \leq d,~ w(a,c)+w(b,d) \leq w(a,d) + w(b,c) \]

    定理

    如果二元函数 \(w(a,b)\) 满足 \(w(a+1,b+1)+w(a,b) \leq w(a,b+1)+w(a+1,b)\) ,那么 \(w(a,b)\) 满足四边形不等式。

    证明

    • 对于 \(a<b\), 有 \(w(a,b)+w(a+1,b+1)\leq w(a,b+1) + w(a+1,b)\)
    • 对于 \(a+1<b\),有 \(w(a+1,b)+w(a+2,b+1)\leq w(a+1,b+1) + w(a+2,b)\)

    将两式相加,然后化简:

    \[\begin{aligned} w(a,b)+w(a+1,b+1)+w(a+1,b)+w(a+2,b+1)&\leq w(a,b+1)+w(a+1,b)+w(a+1,b+1)+w(a+2,b)\\ w(a,b)+w(a+2,b+1)&\leq w(a,b+1)+w(a+2,b) \end{aligned} \]

    以此类推,可以得到:

    \[\forall a \leq b < c, w(a,c)+w(b,c+1) \leq w(a,c+1)+w(b,c) \]

    可以对第二个数做类似的操作,可以得到:

    \[\forall a \leq b < c \leq d,~ w(a,c)+w(b,d) \leq w(a,d) + w(b,c) \]

    \(\rm Q.E.D\)

    正文

    定义

    考虑一个形如下面式子的 \(\rm dp\) 转移:

    \[f(i)=\min_{1\leq j< i} \{f(j)+w(j,i) \} \]

    \(p(i)\) 表示 \(f(i)\) 的最优决策点,即:

    \[p(i)=\arg\min_{1\leq j < i} \{f(j)+w(j,i) \} \]

    而这个 \(\rm dp\) 有决策单调性,当且仅当 \(p(i)\) 单调不降。

    定理

    转移参数 \(w(j,i)\) 满足四边形不等式 \(\Rightarrow\) \(f(i)\) 满足决策单调性。

    证明

    \(\forall a < b < i < j\), 设 \(p(i) = b\),则有 \(f(b)+w(b,i)\leq f(a)+w(a,i)\)

    根据四边形不等式,有

    \[w(a,i)+w(b,j) \leq w(a,j)+w(b,i) \]

    将两条式子相加,然后化简,得到:

    \[f(b)+w(b,j)\leq f(a)+w(a,j) \]

    由此可以看出,对于 \(i\) 之后的所有状态,其最优决策点都可以不是 \(a\) ,因此可以有 \(p(j)\ge b\) ,因此满足决策单调性。

    碎碎念

    \(Q\):上面的内容似乎只针对 \(\min\) ,那么对于 \(\max\) 应该怎么办呢?

    \(A\):把四边形不等式的不等号取反即可。

    决策单调性和斜率优化关联性挺大的,但是本 \(\rm juruo\) 觉得两个东西本质上有点不同。

    应用

    [NOI 2009] 诗人小G

    \(\Rightarrow\) 原题链接

    可以得到转移:

    \[f(i)=\min_{0\leq j < i} \left\{f(j)+\left|s_i-s_j+(i-j-1)-L\right|^P\right\} \]

    其中 \(s_i = s_{i-1}+|\mathrm{str}_i|\)

    \(w(i,j) = \left|s_i-s_j+(i-j-1)-L\right|^P\),显然当前就是要证明这个东西满足四边形不等式。

    根据四边形不等式的性质,可以转化为证明

    \[w(i+1,j+1)+w(i,j) \leq w(i,j+1)+w(i+1,j) \]

    这个证明不太显然,需要稍微搞一搞。教给各位 \(\rm dalao\) 了。这里主要介绍知道决策单调性之后的做法。

    显然,对于每个决策点 \(x\),都有一个贡献区间 \([l_x, r_x] (\forall k \in [l_x, r_x], p(k)=x)\) ,可以用二分求出对于决策点 \(x\),第一个比决策点 \(y\) 优的位置,这个明显可以使用二分求出,设其为 \(g(x,y)\)

    接着用单调队列维护出 \(l\) 单调递增的决策点序列。

    显然,对于队列中相邻的两个决策 \(x,y~(x<y)\),显然是 \(r_x = g(y,x)-1\)

    • 当队头的 \(r\) 小于 \(i\) 的时候就推出。
    • 考虑加入一个新的决策点 \(p\), 设这时候队尾的两个元素分别为 \(x,y~(x<y)\),显然,如果 \(g(y,x) \ge g(p, y)\) ,那么显然,将 \(y\) 弹出,直到队列长度只有 \(1\) ,或者 \(g(y,x) < g(p,y)\) ,再压入 \(p\)

    为了优化常数,可以在队列中记录 \(r_x\)

    #include <bits/stdc++.h>
    
    using namespace std;
    typedef long double LL;
    
    const int maxn = 1e5 + 5;
    const LL INF = 1e18;
    int n, P, q[maxn], ql, qr;
    LL L, s[maxn], fr[maxn], f[maxn];
    string str[maxn];
    
    LL qpow(LL a, int b) {
        LL ret = 1;
        while (b) {
            if (b & 1) ret = ret * a;
            a = a * a, b >>= 1;
        }
        return ret;
    }
    
    inline LL calc(int x, int y) { return f[x] + qpow(abs(s[y] - s[x] + (y - x - 1) - L), P); }
    
    int bsearch(int x, int y) {
        if (calc(x, n) < calc(y, n)) return n + 1;
        int l = y, r = n, ans = -1;
        while (l <= r) {
            int mid = (l + r) >> 1;
            if (calc(y, mid) <= calc(x, mid)) ans = mid, r = mid - 1;
            else l = mid + 1;
        }
        return ans;
    }
    
    void print_plan(int x) {
        if (!x) return ;
        print_plan(fr[x]);
        for (int i = fr[x] + 1; i < x; i++) cout << str[i], putchar(' ');
        cout << str[x];
        putchar('\n');
    }
    void solve() {
        scanf("%d%Lf%d", &n, &L, &P);
        memset(q, 0, sizeof(q)), fill(f, f + n + 1, INF);
        for (int i = 1; i <= n; i++)
            cin >> str[i], s[i] = s[i - 1] + str[i].size();
        f[0] = q[ql = qr = 1] = 0;
        for (int i = 1; i <= n; i++) {
            while (ql < qr && bsearch(q[ql], q[ql + 1]) <= i) ql++;
            f[i] = calc(q[ql], i), fr[i] = q[ql];
            while (ql < qr && bsearch(q[qr - 1], q[qr]) >= bsearch(q[qr], i)) qr--;
            q[++qr] = i;
        }
        if (f[n] > INF) { printf("Too hard to arrange\n--------------------\n"); return ; }
        printf("%.0Lf\n", f[n]), print_plan(n);
        printf("--------------------\n");
    }
    int main() {
        int T; scanf("%d", &T);
        while (T--) solve();
        return 0;
    }
    

    [UOJ 285] 数据分块鸡

    \(\Rightarrow\) 原题链接

    和上一题的转移方程非常类似,只不过 \(w(i,j)\) 的计算方法不同,需要使用数据结构优化,就不讲了。

    [CF-868F] Yet Another Minimization Problem

    \(\Rightarrow\) \(\rm luogu\) 链接

    题意非常清楚明白,可以直接设出 \(f(i,j)\) 表示将前 \(j\) 个元素分成 \(i\) 段的最小权值,可以得到转移:

    \[f(i,j) = \min_{0 \leq k < j} \{f(i-1,k)+w(k+1,j) \} \]

    其中 \(w(i,j)\) 表示区间 \([l,r]\) 中重复元素的对数,可以看出这个函数满足四边形不等式,但是这个函数的求值时间复杂度是 \(O(n)\) 的,因此不能使用先前的单调队列优化了。这里提出另一种优化决策单调性的方法:分治

    这种方法适用于 \(\rm dp\) 的转移不依赖同一维状态的转移,比如这道题的转移 \(f(i,*)\) 依赖于 \(f(i-1,*)\) ,但是 \(f(i,*)\) 的状态之间不存在依赖关系。这个也可以计算形如下面的 "\(\rm dp\)"。:

    \[f(i) = \min_{0\leq j < i}\{w(j,i)\} \]

    设立一个函数 \(\mathrm{calc}(l, r, x, y)\) 用于计算状态在 \([l,r]\) 之间的所有 \(f\) 值,其中决策点只能在 \([x,y]\) 中找。

    分为下面的步骤:

    • \(m = \left\lfloor \frac{l+r}{2}\right\rfloor\)
    • 计算出 \(f(m)\) ,同时得到其最优决策点 \(p(m)\)
    • 分治 \(\mathrm{calc}(l, m - 1, x, p(m)), \mathrm{calc}(m + 1,r,p(m), y)\)

    如果 \(w(i,j)\) 的求值时间复杂度为 \(O(1)\) 的,这个分治过程显然是 \(O(n\log_2 n)\) 的。

    但关键是现在 \(w(i,j)\) 的计算是 \(O(n)\) 的。

    这里可以采用类似莫队的双指针移动,维护 \(w(i,j)\) 。首先可以想到将两个指针的移动分开计算复杂度。然后就会发现两个指针移动的次数都是 \(O(n\log_2n)\) 的。

    因此一次分治的时间复杂度为 \(O(n\log_2 n)\) ,总的时间复杂度为 \(O(kn \log_2 n)\)

    #include <bits/stdc++.h>
    
    using namespace std;
    typedef long long LL;
    
    const int maxn = 1e5 + 5, maxk = 25;
    const LL INF = 1e18;
    
    int n, m, a[maxn];
    int cl, cr; LL f[maxk][maxn], cnt[maxn], cw;
    
    inline void upd_cnt(int col, int vl) {
        cw -= cnt[col] * (cnt[col] - 1) / 2;
        cnt[col] += vl;
        cw += cnt[col] * (cnt[col] - 1) / 2;
    }
    LL calc_w(int l, int r) {
        while (cl < l) upd_cnt(a[cl], -1), cl++;
        while (cl > l) cl--, upd_cnt(a[cl], 1);
        while (cr > r) upd_cnt(a[cr], -1), cr--;
        while (cr < r) cr++, upd_cnt(a[cr], 1);
        return cw;
    }
    
    int cur_m;
    void solve(int l, int r, int fl, int fr) {
        if (fl > fr || l > r) return ;
        int pos = 0; LL res = INF;
        int mid = (l + r) >> 1, rgl = max(1, fl), rgr = min(mid, fr);
        for (int i = rgl; i <= rgr; i++) {
            LL vl = f[cur_m - 1][i - 1] + calc_w(i, mid);
            if (vl < res) res = vl, pos = i;
        }
        f[cur_m][mid] = res;
        solve(l, mid - 1, fl, pos), solve(mid + 1, r, pos, fr);
    }
    int main() {
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
        cnt[a[1]] = 1, cw = 0, cl = cr = 1;
        f[0][0] = 0;
        for (int i = 1; i <= n; i++) f[0][i] = INF;
        for (cur_m = 1; cur_m <= m; cur_m++)
            solve(1, n, 1, n);
        printf("%lld\n", f[m][n]);
        return 0;
    }
    

    [POI2011]Lightning Conductor

    \(\Rightarrow\) \(\rm luogu\) 链接

    这题两种方法都可以,不讲了。

  • 相关阅读:
    linux中内存使用,swap,cache,buffer的含义总结
    haproxy启动时提示失败
    pcs与crmsh命令比较
    用yum下载rpm包(不安装)到指定目录
    Openstack的镜像属性
    ceph 对接openstack liberty
    ceph 创建和删除osd
    linux之fstab文件详解
    OpenStack Swift集群部署流程与简单使用
    swift(Object Storage对象存储服务)(单节点)
  • 原文地址:https://www.cnblogs.com/juruohjr/p/15780634.html
Copyright © 2020-2023  润新知