跳转至

决策单调性优化 DP

考虑一类 dp 转移式:

\[ f_i=\min_{j<i}\{f_j+w(j,i)\} \]

\(f_j\) 的贡献只和 \(j\) 有关,可以一并由 \(w(j,i)\) 计算。

\(w(j,i)\) 取到最小值的 \(j\)\(i\) 单调不降,那么称 \(w\) 具有决策单调性

四边形不等式

\(w(j,i)\) 对任意的 \(a\le b\le c\le d\) 都满足

\[ w(a,c)+w(b,d)\le w(a,d)+w(b,c) \]

则称 \(w(j,i)\) 满足四边形不等式。

  • 性质 1\(w\) 满足四边形不等式的充要条件是,对任意的 \(a\le b\le c\),满足 \(w(a,c+1)-w(a,c)\ge w(b,c+1)-w(b,c)\)
  • 说人话就是,给长区间加一个元素,比给短区间加一个元素代价更大。
  • 性质 2:若 \(w\) 满足四边形不等式,则 \(w\) 满足(完全)决策单调性,强于决策单调性。

二分队列

\(w\) 满足四边形不等式(完全决策单调性)的时候,可以使用二分队列。二分队列的转移是在线的。

采用刷表法进行转移,依次加入每一个 \(j\),注意到 \(j\) 一定是贡献到一个后缀的 \(i\),因此用单调队列维护相同决策点构成的连续段,找到 \(j\) 产生贡献的分界点所在的段并在段内部进行二分,然后把这个段 split 掉。每加入一个 \(j\) 都会进行一次二分,因此时间复杂度 \(O(n\log n)\)

P3195 [HNOI2008] 玩具装箱

模板
#include<iostream>
#include<deque>
#define int long long
using namespace std;
const int N = 5E4 + 10;

struct myPair {
    int l, r, p;
};

int n, c;
int s[N], f[N];

inline int w(int l, int r) {
    return (s[r] - s[l] + c) * (s[r] - s[l] + c);
}

myPair que[N];
int hd = 1, tl;

signed main() {

    cin >> n >> c;
    for(int i = 1; i <= n; i++) {
        cin >> s[i];
        s[i] += s[i - 1] + 1;
    }

    c = -c - 1;

    que[++tl] = {1, n, 0};

    for(int i = 1; i <= n; i++) {
        if(que[hd].r < i) hd++;
        que[hd].l = i;
        f[i] = f[que[hd].p] + w(que[hd].p, i);
        while(hd <= tl && f[i] + w(i, que[tl].l) < f[que[tl].p] + w(que[tl].p, que[tl].l)) --tl;
        if(hd > tl) {
            que[++tl] = {i + 1, n, i};
            continue;
        }
        int l = que[tl].l, r = que[tl].r + 1, p = que[tl].p;
        while(l < r) {
            int mid = (l + r) >> 1;
            if(f[i] + w(i, mid) < f[p] + w(p, mid)) {
                r = mid;
            } else l = mid + 1;
        }
        que[tl].r = l - 1;
        if(l <= n) que[++tl] = {l, n, i};
    }

    cout << f[n] << endl;

    return 0;
}

P3515 [POI 2011] Lightning Conductor

给定一个长为 \(n\) 的数列 \(a_i\),你需要对每个 \(i\) 求出

\[ \max_{j=1}^{i-1}\{\Big\lceil a_j+\sqrt{i-j}\Big\rceil \} \]

注意到 \(w(j,i)=-\sqrt{i-j}\) 满足四边形不等式。直接套用模板即可。

\(y=\sqrt{x}\) 是上凸函数,但 \(y=\Big\lceil\sqrt{x}\Big\rceil\) 不具有凸性。因此本题需要在浮点数类型下计算,将上取整提到 \(\max\) 外。

代码
#include<iostream>
#include<algorithm>
#include<cmath>
#define int long long
#define ld long double
using namespace std;
const int N = 5E5 + 10;
const ld eps = 1e-8;

struct Range {
    int l, r, p;
};

int n;
int a[N];
ld w[N], mx[N];

Range que[N];
int hd, tl;

signed main() {

    cin >> n;
    for(int i = 1; i <= n; i++) {
        cin >> a[i];
    }

    for(int i = 1; i <= n; i++) {
        w[i] = sqrt(i);
    }

    hd = 1, tl = 0;
    que[++tl] = {1, n, 1};
    for(int i = 2; i <= n; i++) {
        if(que[hd].r < i) hd++;
        que[hd].l = i;
        mx[i] = max(mx[i], a[que[hd].p] + w[i - que[hd].p]);
        while(hd < tl && a[i] + w[que[tl].l - i] >= a[que[tl].p] + w[que[tl].l - que[tl].p]) --tl;
        int l = que[tl].l, r = que[tl].r + 1, p = que[tl].p;
        while(l < r) {
            int mid = (l + r) >> 1;
            if(a[i] + w[mid - i] >= a[p] + w[mid - p]) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        que[tl].r = l - 1;
        if(l <= n) que[++tl] = {l, n, i};
    }

    // 因为原题没有限制 j<i,因此需要反着跑一边 DP
    reverse(a + 1, a + 1 + n);

    hd = 1, tl = 0;
    que[++tl] = {1, n, 1};
    for(int i = 2; i <= n; i++) {
        if(que[hd].r < i) hd++;
        que[hd].l = i;
        mx[n - i + 1] = max(mx[n - i + 1], a[que[hd].p] + w[i - que[hd].p]);
        while(hd <= tl && a[i] + w[que[tl].l - i] >= a[que[tl].p] + w[que[tl].l - que[tl].p]) --tl;
        if(hd > tl) {
            que[++tl] = {i + 1, n, i};
            continue;
        }
        int l = que[tl].l, r = que[tl].r + 1, p = que[tl].p;
        while(l < r) {
            int mid = (l + r) >> 1;
            if(a[i] + w[mid - i] >= a[p] + w[mid - p]) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        que[tl].r = l - 1;
        if(l <= n) que[++tl] = {l, n, i};
    }

    for(int i = 1; i <= n; i++) {
        // 向上取整
        cout << max((int)(mx[i] + 1 - eps) - a[n - i + 1], 0ll) << '\n';
    }

    return 0;
}

P1912 [NOI2009] 诗人小G

代码
#include<iostream>
#include<cstring>
#define int long long
#define ld long double
using namespace std;
const int N = 1E5 + 10;
const ld V = 1E18;
const ld INF = 1E20;

struct myPair {
    int l, r, p;
};

int T;
int n, L, P;

int s[N], p[N];
ld f[N];
char str[N][40];

myPair que[N];
int hd, tl;

inline ld qpow(ld a, int b) {
    ld res = 1;
    while(b) {
        if(b & 1) res = res * a;
        a = a * a;
        b >>= 1;
    }
    return res;
}

inline ld w(int i, int j) {
    return qpow(abs(s[j] - s[i] - L - 1), P);
}

void solve() {
    hd = 1, tl = 0;
    que[++tl] = {1, n, 0};
    for(int i = 1; i <= n; i++) {
        if(que[hd].r < i) hd++;
        que[hd].l = i;
        f[i] = f[que[hd].p] + w(que[hd].p, i);
        p[i] = que[hd].p;
        while(hd <= tl && f[i] + w(i, que[tl].l) < f[que[tl].p] + w(que[tl].p, que[tl].l)) --tl;
        if(hd > tl) {
            que[++tl] = {i + 1, n, i};
            continue;
        }
        int l = que[tl].l, r = que[tl].r + 1, p = que[tl].p;
        while(l < r) {
            int mid = (l + r) >> 1;
            if(f[i] + w(i, mid) < f[p] + w(p, mid)) {
                r = mid;
            } else l = mid + 1;
        }
        que[tl].r = l - 1;
        if(l <= n) que[++tl] = {l, n, i};
    }
}

void outPut(int x) {
    if(x == 0) return;
    outPut(p[x]);
    for(int i = p[x] + 1; i <= x; i++) {
        for(int j = 0; j < s[i] - s[i - 1] - 1; j++) {
            cout << str[i][j];
        }
        if(i < x) cout << ' ';
    }
    cout << '\n';
}

signed main() {

    cin >> T;
    while(T--) {
        cin >> n >> L >> P;
        getchar();
        for(int i = 1; i <= n; i++) {
            cin.getline(str[i], 40, '\n');
            s[i] = strlen(str[i]);
            s[i] += s[i - 1] + 1;
        }
        for(int i = 1; i <= n; i++) f[i] = 0;
        for(int i = 1; i <= n; i++) que[i] = {0, 0, 0};
        solve();
        if(f[n] > V) {
            cout << "Too hard to arrange\n";
            cout << "--------------------\n";
            continue;
        }
        cout << (int)f[n] << '\n';
        outPut(n);
        cout << "--------------------\n";
    }

    return 0;
}

分治

分治法与二分队列不同,它只要求 \(w\) 满足普通决策单调性。并且,分治法一次只需要 \(w(j,i)\)\(i\) 相同时 \(j\) 取一段连续区间时的取值。某些 \(w(j,i)\) 比较奇怪的时候需要使用分治法。

分治法没有细节,非常好写。然而普通分治法只能处理离线的问题,可以套一层 cdq\(O(n\log^2n)\) 的代价做到在线转移。洛谷上有一篇科技,说可以 \(O(n\log n)\) 做到在线转移。

CF1603D Artistic Partition

对于两个正整数 \(l,r\)\(l\le r\)),定义 \(c(l,r)\) 表示满足 \(l\le x\le y\le r\)\(\gcd(x,y)\ge l\) 的数对 \((x,y)\) 的数量。

对于两个正整数 \(n,k\)\(k\le n\)),定义 \(f(n,k)\) 表示将 \([1,n]\) 区间划分为 \(k\) 段连续区间 \([l_1,r_1],[l_2,r_2],\cdots,[l_k,r_k]\)\(l_1=1,\ r_k=n,\ l_i=r_{i-1}+1\)),\(\sum_{i=1}^k{c(l_i,r_i)}\) 的最小值。

多组询问,每次给定两个数 \(n,k\),输出 \(f(n,k)\)

\(T\le 3\times 10^5,\ 1\le k\le n\le 10^5\)

首先有一些显然的观察:若 \(r<2l\),那么 \(c(l,r)=r-l+1\)。这样,当 \(n<2^k\) 时,答案可以取到最小值 \(n\)。其余情况 \(k\)\(\log n\) 级别的。

考虑转移:

\[ f(n,k)=\min_{1\le i<n}\bigl\{f(i,k-1)+c(i+1,n)\big\} \]

\(c(l,r)\) 枚举数对 \((x,y)\) 似乎没什么前途,改为枚举 \(\gcd\)

\[ \begin{align*} c(l,r)&=\sum_{d=l}^{r}\sum_{x=l}^{r}\sum_{y=x}^{r}[\gcd(x,y)=d]\\ &=\sum_{d=l}^{r}\sum_{y=1}^{\lfloor\frac rd\rfloor}\sum_{x=1}^{y}[\gcd(x,y)=1]\\ &=\sum_{d=l}^{r}\sum_{y=1}^{\lfloor\frac rd\rfloor}\varphi(y)\\ &=\sum_{d=l}^{r}s\bigl(\lfloor\frac rd\rfloor\bigr) \end{align*} \]

其中 \(s(x)\) 表示欧拉函数前缀和。这个式子可以用数论分块做到单次 \(O(\sqrt n)\)

考察 \(c(a,d)+c(b,c)-c(a,c)-c(b,d)\)

\[ \begin{align*} c(a,d)+c(b,c)-c(a,c)-c(b,d)&=\bigl(c(a,d)-c(b,d)\big)-\bigl(c(a,c)-c(b,c)\big)\\ &=\sum_{i=a}^{b-1}s(\lfloor\frac di\rfloor)-s(\lfloor\frac ci\rfloor) \end{align*} \]

由于 \(s(x)\) 单调递增,因此上式显然非负,因此 \(c(l,r)\) 满足四边形不等式,相应可以进行决策单调性优化。时间复杂度来到 \(O(n\sqrt n\log^2 n)\)

实际上,我们无需推式子就可以判断出四边形不等式。考虑性质 1,显然长区间加一个东西的代价比短区间加一个东西的代价大。证毕。

注意到 \(c(l,r)\)\(r\) 强相关,容易得出递推式 \(c(l,r)=c(l+1,r)+s(\lfloor\frac rl\rfloor)\)。因此考虑分治法。分治法一次需要 \(r\) 相等时 \(l\) 的一段区间。这样做似乎是对的,我不太会分析。

实际上我们可以 \(O(n\sqrt n)\) 预处理,\(O(1)\) 查询 \(c(l,r)\)。注意到数论分块的第一次迭代后,\(p\gets \left\lfloor\frac r{\lfloor\frac rp\rfloor}\right\rfloor\) 只有 \(O(\sqrt r)\) 种取值。因此预处理出第一次迭代后,\((r,p)\) 的所有答案即可。

代码
#include<iostream>
#include<cmath>
#define int long long
using namespace std;
const int N = 1e5 + 10;
const int INF = 0x3f3f3f3f;

int T;
int n, m;

int npri[N], pri[N], pcnt;
int phi[N], s[N];

int f[25][N];

int calc(int l, int r) {
    int res = 0;
    for(int i = l, j; i <= r; i = j + 1) {
        j = min(r, r / (r / i));
        res += (j - i + 1) * s[r / i];
    }
    return res;
}

void solve(int i, int l, int r, int tl, int tr) {
    if(l > r) return;
    int mid = (l + r) >> 1;
    int w = calc(tr + 1, mid);
    int mp = tl;
    f[i][mid] = INF;
    for(int j = min(tr, mid); j >= tl; j--) {
        w += s[mid / j];
        if(f[i - 1][j - 1] + w < f[i][mid]) {
            f[i][mid] = f[i - 1][j - 1] + w;
            mp = j;
        }
    }
    solve(i, l, mid - 1, tl, mp);
    solve(i, mid + 1, r, mp, tr);
}

signed main() {

    ios::sync_with_stdio(0);
    cin.tie(0);

    phi[1] = 1;
    for(int i = 2; i < N; i++) {
        if(!npri[i]) {
            pri[++pcnt] = i;
            phi[i] = i - 1;
        }
        for(int j = 1; j <= pcnt; j++) {
            if(i * pri[j] >= N) break;
            npri[i * pri[j]] = 1;
            if(i % pri[j] == 0) {
                phi[i * pri[j]] = phi[i] * pri[j];
                break;
            } else phi[i * pri[j]] = phi[i] * (pri[j] - 1);
        }
    }

    for(int i = 1; i < N; i++) s[i] = s[i - 1] + phi[i];

    m = 18;
    n = 100000;

    for(int i = 1; i <= n; i++) f[0][i] = INF;
    for(int i = 1; i <= n; i++) f[1][i] = i * (i + 1) / 2;

    for(int i = 2; i <= m; i++) {
        solve(i, 1, n, 1, n);
    }

    cin >> T;
    while(T--) {
        cin >> n >> m;
        if(m > 18 || n < (1 << m)) {
            cout << n << '\n';
        } else cout << f[m][n] << '\n';
    }

    return 0;
}

CF868F Yet Another Minimization Problem

使用分治优化决策单调性 dp 时,用莫队暴力调整左右端点的方式计算区间代价的时间复杂度是对的,即 \(O(n\log n)\),分析是容易的。

CF1832F Zombies

咕咕咕