决策单调性优化入门

前置知识

先学下面的东西

  • 单调队列

四边形不等式

定义

对于一个二元函数 \(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\) 链接

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

原文地址:https://www.cnblogs.com/juruohjr/p/15780634.html