区间 DP

区间 DP

按分割点转移

P1880 [NOI1995] 石子合并

\(n\) 堆石子围成一圈,第 \(i\) 堆有 \(a_i\) 个。每次可以选择相邻两堆石子合并,得分和新的石子数为两堆石子的数量和。求将所有石子合并成一堆的最小、最大得分。

\(n \leq 100\)

先破环为链,把 \(a_{1 \sim n}\) 复制一边接到后面。设 \(f_{l, r}\) 表示合并 \(l \sim r\) 的石子的最小代价,则:

\[f_{l, r} = \min_{k = l}^{r - 1} f_{l, k} + f_{k + 1, r} + s_{l, k} \times s_{k + 1, r} \]

其中 \(s_{l, r} = \sum_{i = l}^r a_i\) ,最大值同理,时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e3 + 7;

int f[N][N], g[N][N];
int a[N], s[N];

int n;

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i);

    memcpy(a + n + 1, a + 1, sizeof(int) * n);

    for (int i = 1; i <= n * 2; ++i)
        s[i] = s[i - 1] + a[i];

    memset(f, inf, sizeof(f)), memset(g, -inf, sizeof(g));

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

    for (int len = 2; len <= n; ++len)
        for (int l = 1, r = len; r <= n * 2; ++l, ++r)
            for (int k = l; k < r; ++k) {
                f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
                g[l][r] = max(g[l][r], g[l][k] + g[k + 1][r] + s[r] - s[l - 1]);
            }

    int ans1 = inf, ans2 = -inf;

    for (int i = 1; i <= n; ++i)
        ans1 = min(ans1, f[i][i + n - 1]), ans2 = max(ans2, g[i][i + n - 1]);

    printf("%d\n%d", ans1, ans2);
    return 0;
}

P3146 [USACO16OPEN] 248 G

给定 \(a_{1 \sim n}\) ,每次可以合并相邻两个相同的数,新的数为原数 \(+1\) ,最大化若干次操作后序列最大值。

\(n \leq 248\)

\(f_{l, r}\) 表示 \(l \sim r\) 合并成的数,若无法完全合并则为 \(0\) ,则只有 \(f_{l, k} = f_{k + 1, r}\) 可以转移,答案即为 \(\max f\)

#include <bits/stdc++.h>
using namespace std;
const int N = 3e2 + 7;

int f[N][N], a[N];

int n;

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i);

    int ans = 0;

    for (int i = 1; i <= n; ++i)
        ans = max(ans, f[i][i] = a[i]);

    for (int len = 2; len <= n; ++len)
        for (int l = 1, r = len; r <= n; ++l, ++r)
            for (int k = l; k < r; ++k)
                if (f[l][k] && f[l][k] == f[k + 1][r])
                    ans = max(ans, f[l][r] = max(f[l][r], f[l][k] + 1));

    printf("%d", ans);
    return 0;
}

HDU4283 You Are the One

\(n\) 个人上场,第 \(i\) 个人有个权值 \(a_i\) 。有一个栈可以改变出场顺序,按顺序决策,可以选择:

  • 令当前人上场。
  • 令当前人进入栈。
  • 若栈非空,则令栈顶上场。

记第 \(i\) 个人第 \(b_i\) 个上场,最小化 \(\sum_{i = 1}^n (b_i - 1) a_i\)

\(n \leq 100\)

考虑区间 DP,设 \(f_{l, r}\) 表示 \(l \sim r\) 全上场的答案,\(s_{l, r} = \sum_{i = l}^r a_i\) 。枚举分界点 \(k\) ,分类讨论:

  • 左区间先全上场,然后右区间再全上场:\(f_{l, r} \gets f_{l, k} + f_{k + 1, r} + (k - l + 1) \times s_{k + 1, r}\)
  • 左区间先全入栈,然后右区间再全上场,最后左区间栈里的全上场:\(f_{l, r} \gets f_{k + 1, r} + (r - k) \times s_{l, k} + w(l, k)\) ,其中 \(w(l, r)\) 表示 \(l \sim r\) 倒序上场的权值和。

时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e2 + 7;

int f[N][N], w[N][N];
int a[N], s[N];

int n;

signed main() {
    int T;
    scanf("%d", &T);

    for (int task = 1; task <= T; ++task) {
        scanf("\n%d", &n);

        for (int i = 1; i <= n; ++i)
            scanf("%d", a + i), s[i] = s[i - 1] + a[i];

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

            for (int j = i + 1; j <= n; ++j)
                w[i][j] = w[i][j - 1] + s[j - 1] - s[i - 1];
        }

        memset(f, inf, sizeof(f));

        for (int i = 1; i <= n; ++i)
            f[i][i] = 0;

        for (int len = 2; len <= n; ++len)
            for (int l = 1, r = len; r <= n; ++l, ++r)
                for (int k = l; k < r; ++k) {
                    f[l][r] = min(f[l][r], min(f[l][k] + f[k + 1][r] + (s[r] - s[k]) * (k - l + 1),
                        f[k + 1][r] + (s[k] - s[l - 1]) * (r - k) + w[l][k]));
                }

        printf("Case #%d: %d\n", task, f[1][n]);
    }

    return 0;
}

AT TTPC2024 Div.2 E. ReTravel

给出二维平面上的 \(n\) 个点 \((x_i, y_i)\) ,初始时在 \((0, 0)\) ,每次可以从三种操作中选择操作:

  • 令当前 \(x\) 坐标增加 \(1\) ,花费 \(1\) 的代价。
  • 令当前 \(y\) 坐标增加 \(1\) ,花费 \(1\) 的代价。
  • 回退上一次操作,无需付出代价。

求按顺序遍历所有点的最小代价。

\(n \leq 500\)\(0 \leq x_i, y_i \leq 10^9\)

定义 \(P_{l, r} = (X(l, r), Y(l, r)) = (\min_{i = l}^r x_i, \min_{i = l}^r y_i)\)

考虑区间 DP,设 \(f_{l, r}\) 表示从 \(P_{l, r}\) 开始按顺序遍历 \(l \sim r\) 的点的最小代价,答案即为 \(X(1, n) + Y(1, n) + f_{1, n}\)

考虑转移,有:

\[f_{l, r} = \min_{k = l}^{r - 1} f_{l, k} + f_{k + 1, r} + X(l, k) + X(k + 1, r) - 2X(l, r) + Y(l, k) + Y(k + 1, r) - 2Y(l, r) \]

时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 5e2 + 7;

struct Point {
    int x, y;
} p[N];

ll f[N][N];
int X[N][N], Y[N][N];

int n;

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i)
        scanf("%d%d", &p[i].x, &p[i].y);

    for (int i = 1; i <= n; ++i) {
        X[i][i] = p[i].x, Y[i][i] = p[i].y;

        for (int j = i + 1; j <= n; ++j)
            X[i][j] = min(X[i][j - 1], p[j].x), Y[i][j] = min(Y[i][j - 1], p[j].y);
    }

    memset(f, inf, sizeof(f));

    for (int i = 1; i <= n; ++i)
        f[i][i] = 0;

    for (int len = 2; len <= n; ++len)
        for (int l = 1, r = len; r <= n; ++l, ++r)
            for (int k = l; k < r; ++k)
                f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + 
                    X[l][k] + X[k + 1][r] - X[l][r] * 2 + Y[l][k] + Y[k + 1][r] - Y[l][r] * 2);

    printf("%lld", X[1][n] + Y[1][n] + f[1][n]);
    return 0;
}

HDU5115 Dire Wolf

\(n\) 头狼排成一排,每只狼有攻击力和加成值,狼的实际攻击力等于自身攻击力加相邻狼的加成值。杀死一只狼会受其攻击力大小的伤害,并使得左右的狼相邻。求杀死所有狼的最小伤害。

\(n \leq 200\)

\(f_{l, r}\) 表示杀死 \(l \sim r\) 的狼的代价。

如果是枚举当前要杀的狼 \(k\) ,则对于分割出的左右区间,另一边的附加伤害是不确定的,难以处理。

考虑枚举最后杀的狼 \(k\) ,则附加伤害一定是 \(b_{l - 1} + b_{r + 1}\) ,并且分割出的左右区间的附加伤害互不影响,因此有转移:

\[f_{l, r} = \min_{k = l}^r \{ f_{l, k - 1} + f_{k + 1, r} + a_k + b_{l - 1} + b_{r + 1} \} \]

时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e2 + 7;

int a[N], b[N], f[N][N];

int n;

signed main() {
    int T;
    scanf("%d", &T);

    for (int task = 1; task <= T; ++task) {
        scanf("%d", &n);

        for (int i = 1; i <= n; ++i)
            scanf("%d", a + i);

        for (int i = 1; i <= n; ++i)
            scanf("%d", b + i);

        b[0] = b[n + 1] = 0;

        for (int i = 1; i <= n; ++i)
            f[i][i] = a[i] + b[i - 1] + b[i + 1];

        for (int len = 2; len <= n; ++len)
            for (int l = 1, r = len; r <= n; ++l, ++r) {
                f[l][r] = inf;

                for (int k = l; k <= r; ++k)
                    f[l][r] = min(f[l][r], f[l][k - 1] + f[k + 1][r] + a[k] + b[l - 1] + b[r + 1]);
            }

        printf("Case #%d: %d\n", task, f[1][n]);
    }

    return 0;
}

从两端转移

P1005 [NOIP 2007 提高组] 矩阵取数游戏

给出一个 \(n \times m\) 的矩阵,每个元素 \(a_{i, j}\) 非负,按如下规则进行 \(m\) 次:

  • 从每行的首或尾取走一个元素,获得 \(n\) 行所有所选数的和 \(\times 2^k\) 的得分,其中 \(k\) 表示当前是第 \(k\) 轮操作。

最大化得分。

\(n, m \leq 80\)

不难发现每行操作独立,因此只要对每行分别求解最后求和即可。

\(f_{l, r}\) 表示只剩 \(l \sim r\) 时目前的最大得分,转移就考虑取走的是 \(l - 1\) 还是 \(r + 1\) 即可。注意这里 DP 中枚举 \(len\) 应从大到小枚举,才能满足只能从两端取的限制。

时间复杂度 \(O(nm^2)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll B = 1e18;
const int N = 8e1 + 7;

__int128 f[N][N], pw[N];
int a[N];

int n, m;

signed main() {
    scanf("%d%d", &n, &m);
    pw[0] = 1;

    for (int i = 1; i <= m; ++i)
        pw[i] = pw[i - 1] << 1;

    __int128 ans = 0;

    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= m; ++j)
            scanf("%d", a + j);

        f[1][m] = 0;

        for (int len = m - 1; len; --len)
            for (int l = 1, r = len; r <= m; ++l, ++r) {
                f[l][r] = -1;

                if (l > 1)
                    f[l][r] = max(f[l][r], f[l - 1][r] + pw[m - len] * a[l - 1]);

                if (r < m)
                    f[l][r] = max(f[l][r], f[l][r + 1] + pw[m - len] * a[r + 1]);
            }

        __int128 mx = -1;

        for (int j = 1; j <= m; ++j)
            mx = max(mx, f[j][j] + pw[m] * a[j]);
        
        ans += mx;
    }

    if (ans >= B)
        printf("%lld%018lld", (ll)(ans / B), (ll)(ans % B));
    else
        printf("%lld", (ll)ans);

    return 0;
}

P3205 [HNOI2010] 合唱队

对于满足元素互异的序列 \(a_{1 \sim n}\) ,进行如下变换:

  • \(a_1\) 插入新序列。
  • 对于 \(i \geq 2\) ,若 \(a_i < a_{i - 1}\) ,则从前面插入序列,否则从后面插入序列。

求满足变换后得到给定的序列的序列种数 \(\bmod{19650827}\)

\(n \leq 1000\)

可以发现变换区间的过程就是从两端扩展区间的过程。设 \(f_{l, r, 0/1}\) 表示现序列为 \(l \sim r\) ,上一个插入的元素在 \(l\)\(r\) 的方案数,转移就尝试向两端扩展即可,时间复杂度 \(O(n^2)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 19650827;
const int N = 1e3 + 7;

int a[N], f[N][N][2];

int n;

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i), f[i][i][0] = 1;

    for (int len = 2; len <= n; ++len)
        for (int l = 1, r = len; r <= n; ++l, ++r) {
            if (a[l] < a[l + 1])
                f[l][r][0] = (f[l][r][0] + f[l + 1][r][0]) % Mod;

            if (a[l] < a[r])
                f[l][r][0] = (f[l][r][0] + f[l + 1][r][1]) % Mod;

            if (a[r] > a[l])
                f[l][r][1] = (f[l][r][1] + f[l][r - 1][0]) % Mod;

            if (a[r] > a[r - 1])
                f[l][r][1] = (f[l][r][1] + f[l][r - 1][1]) % Mod;
        }

    printf("%d", (f[1][n][0] + f[1][n][1]) % Mod);
    return 0;
}

P1220 关路灯

在一条线段上有 \(n\) 个路灯分别在 \(p_i\) 的位置上,每个单位时间造成 \(c_i\) 的代价。一个人起初在 \(S\) ,每个单位时间可以移动一个单位距离,走到路灯的位置可以关掉路灯,使其停止造成代价。求关掉所有路灯造成的最小代价。

\(n \leq 50\)

不难发现关路灯一定是从 \(S\) 开始不断左右交替走,并且沿途的灯都关掉。

考虑费用提前计算,设 \(f_{l, r, 0/1}\) 表示关掉了区间 \([l, r]\) 的灯,现在在左/右端点的最小花费,转移就枚举关掉的时左边还是右边的路灯即可,时间复杂度 \(O(n^2)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 5e1 + 7;

ll f[N][N][2], s[N];
int a[N], b[N];

int n, c;

template <class T = int>
inline T read() {
    char c = getchar();
    bool sign = (c == '-');
    
    while (c < '0' || c > '9')
        c = getchar(), sign |= (c == '-');
    
    T x = 0;
    
    while ('0' <= c && c <= '9')
        x = (x << 1) + (x << 3) + (c & 15), c = getchar();
    
    return sign ? (~x + 1) : x;
}

signed main() {
    n = read(), c = read();

    for (int i = 1; i <= n; ++i)
        a[i] = read(), s[i] = s[i - 1] + (b[i] = read());
    
    memset(f, inf, sizeof(f));
    f[c][c][0] = f[c][c][1] = 0;

    for (int len = 2; len <= n; ++len)
        for (int l = 1, r = len; r <= n; ++l, ++r) {
            f[l][r][0] = min(f[l + 1][r][0] + (a[l + 1] - a[l]) * (s[n] - s[r] + s[l]),
                f[l + 1][r][1] + (a[r] - a[l]) * (s[n] - s[r] + s[l]));
            f[l][r][1] = min(f[l][r - 1][1] + (a[r] - a[r - 1]) * (s[n] - s[r - 1] + s[l - 1]),
                    f[l][r - 1][0] + (a[r] - a[l]) * (s[n] - s[r - 1] + s[l - 1]));
        }

    printf("%lld", min(f[1][n][0], f[1][n][1]));
    return 0;
}

CF149D Coloring Brackets

给定一个长度为 \(n\) 的合法括号序列,求有多少染色方案满足:

  • 一个括号要么染红色,要么染蓝色,或者不染色。
  • 一对匹配的括号恰有一边染色。
  • 相邻两个括号若均染色,则颜色必须不同。

答案对 \(10^9 + 7\) 取模。

\(n \leq 700\)

\(f_{l, r, a, b}\) 表示对 \(l \sim r\) 染色,其中 \(l\) 为左括号、颜色为 \(a\)\(r\) 为右括号、颜色为 \(b\) 的方案数。其中 \(a, b\)\(0\) 时表示不染色,为 \(1\)\(2\) 时表示红色或蓝色。考虑转移:

  • \(l, r\) 是一对匹配的括号,则从 \(f_{l + 1, r - 1}\) 转移,注意判断相邻括号颜色不同。
  • 否则记 \(p\) 为与 \(l\) 匹配的括号,则用乘法原理从 \(f_{l, p}\)\(f_{p + 1, r}\) 转移,若 \(p\) 不存在则不转移。

时间复杂度 \(O(n^2)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 7e2 + 7;

int f[N][N][3][3];
int sta[N], match[N];
char str[N];

int n;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

signed main() {
    scanf("%s", str + 1), n = strlen(str + 1);

    for (int i = 1, top = 0; i <= n; ++i) {
        if (str[i] == '(')
            sta[++top] = i;
        else
            match[sta[top--]] = i;
    }

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

    for (int len = 3; len <= n; ++len)
        for (int l = 1, r = len; r <= n; ++l, ++r) {
            if (match[l] == r) {
                for (int a = 1; a <= 2; ++a)
                    for (int b = 0; b <= 2; ++b)
                        for (int c = 0; c <= 2; ++c) {
                            if (!a || a != b)
                                f[l][r][a][0] = add(f[l][r][a][0], f[l + 1][r - 1][b][c]);

                            if (!a || a != c)
                                f[l][r][0][a] = add(f[l][r][0][a], f[l + 1][r - 1][b][c]);
                        }

            } else if (match[l] < r) {
                int p = match[l];

                for (int a = 0; a <= 2; ++a)
                    for (int b = 0; b <= 2; ++b)
                        for (int c = 0; c <= 2; ++c)
                            for (int d = 0; d <= 2; ++d)
                                if (!b || b != c)
                                    f[l][r][a][d] = add(f[l][r][a][d], 1ll * f[l][p][a][b] * f[p + 1][r][c][d] % Mod);
            }
        }

    int ans = 0;

    for (int a = 0; a <= 2; ++a)
        for (int b = 0; b <= 2; ++b)
            ans = add(ans, f[1][n][a][b]);

    printf("%d", ans);
    return 0;
}

P10207 [JOI 2024 Final] 马拉松比赛 2 / Marathon Race 2

\(n\)个球,第 \(i\) 个球放在坐标 \(a_i\) 处。

\(q\) 次询问,每次询问给出 \(x, y, t\) ,求从 \(x\) 走到 \(y\) 并拿起所有球的最小时间是否 \(\leq t\) ,其中带着 \(x\) 个球的行动速度为 \(\frac{1}{x + 1}\).

\(n, q, a_i, x, y, t \leq 5 \times 10^5\)

注意到如果有 \(m\) 个位置不同的 \(a_i\) ,最好情况就是都排在一起,然后一次拿完,时间至少为 \(\frac{m(m + 1)}{2}\) ,因此 \(m\) 只有 \(O(\sqrt{t})\) 级别,若 \(m\) 太大直接对所有询问输出 No 即可。

注意到一个球所在位置如果被经过多次,肯定是最后一次经过时拿上最优。因此从任意一个起点出发,一定可以视为先走到 \(1\)\(n\) 之后再出发,且前后两部分独立。并且任意时刻拿的球都是一段后缀和一段前缀。

\(f_{l, r, 0/1, 0/1}\) 表示 \([l, r]\) 内的球还没拿,从 \(1\)\(n\) 出发,当前在 \(l\)\(r\) 的最小时间,转移不难做到 \(O(m^2) = O(t)\)

查询时枚举起点是 \(1\) 还是 \(n\) ,找到最接近终点的节点判断最小时间即可,该部分复杂度为 \(O(q \log m)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 5e5 + 7, M = 1e3 + 7;

ll f[M][M][2][2];
int a[N], cnt[N];

int n, m, q;

signed main() {
	scanf("%d%d", &n, &m);

	for (int i = 1; i <= n; ++i)
		scanf("%d", a + i), ++cnt[a[i]];

	for (int i = 1; i <= m; ++i)
		cnt[i] += cnt[i - 1];

	scanf("%d", &q);
	sort(a + 1, a + n + 1), n = unique(a + 1, a + n + 1) - a - 1;

	if (n > M) {
		while (q--)
			puts("No");

		return 0;
	}

	memset(f, inf, sizeof(f));
	f[1][n][0][0] = f[1][n][1][1] = 0;

	for (int len = n - 1; len; --len)
		for (int l = 1, r = len; r <= n; ++l, ++r) {
			int s = cnt[a[l] - 1] + cnt[m] - cnt[a[r]] + 1;

			for (int x = 0; x <= 1; ++x) {
				f[l][r][x][0] = min(f[l - 1][r][x][0] + 1ll * s * (a[l] - a[l - 1]), 
					f[l][r + 1][x][1] + 1ll * s * (a[r + 1] - a[l]));
				f[l][r][x][1] = min(f[l - 1][r][x][0] + 1ll * s * (a[r] - a[l - 1]),
					f[l][r + 1][x][1] + 1ll * s * (a[r + 1] - a[r]));
			}
		}

	while (q--) {
		int s, t, k;
		scanf("%d%d%d", &s, &t, &k);
		ll ans = inf;

		if (a[1] <= t) {
			int p = upper_bound(a + 1, a + n + 1, t) - a - 1;
			ans = min(ans, f[p][p][0][0] + abs(s - a[1]) + 1ll * (cnt[m] + 1) * abs(a[p] - t));
			ans = min(ans, f[p][p][1][0] + abs(s - a[n]) + 1ll * (cnt[m] + 1) * abs(a[p] - t));
		}

		if (t <= a[n]) {
			int p = lower_bound(a + 1, a + n + 1, t) - a;
			ans = min(ans, f[p][p][0][0] + abs(s - a[1]) + 1ll * (cnt[m] + 1) * abs(a[p] - t));
			ans = min(ans, f[p][p][1][0] + abs(s - a[n]) + 1ll * (cnt[m] + 1) * abs(a[p] - t));
		}

		puts(ans + cnt[m] <= k ? "Yes" : "No");
	}

	return 0;
}

染色问题

P4170 [CQOI2007] 涂色

有一段初始无色的序列,每次可以选择一段区间覆盖颜色,求达到目标状态的最少操作次数。

\(n \leq 50\)

首先可以发现一定存在最优方案满足染色区间不交或不共端点的包含,证明可以采用调整法。

\(f_{l, r}\) 表示区间 \([l, r]\) 达到目标状态的最小操作数,分类讨论:

  • \(l, r\) 目标状态同色:显然有 \(f_{l, r} \geq f_{l, r - 1}\) ,事实上一定可以取等。考虑在 \(f_{l, r - 1}\) 中染 \(l\) 的操作区间为 \([l, k]\) ,则将其改为 \([l, r]\) ,并将所有在 \([k + 1, r - 1]\) 上原来的染色操作放到该操作后即可。
  • \(l, r\) 目标状态异色:此时必然存在一个分界点 \(k\) 使得 \([l, k]\)\([k + 1, r]\) 的染色不交,枚举 \(k\) 取最小值即可。

时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 5e1 + 7;

int f[N][N];
char str[N];

int n;

signed main() {
    scanf("%s", str + 1), n = strlen(str + 1);

    for (int i = 1; i <= n; ++i)
        f[i][i] = 1;

    for (int len = 2; len <= n; ++len)
        for (int l = 1, r = len; r <= n; ++l, ++r) {
            if (str[l] == str[r])
                f[l][r] = f[l][r - 1];
            else {
                f[l][r] = inf;

                for (int k = l; k < r; ++k)
                    f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r]);
            }
        }

    printf("%d", f[1][n]);
    return 0;
}

HDU2476 String painter

对于一个长度为 \(n\) 的序列,每次可以选择一段区间覆盖颜色,求初始状态达到目标状态的最少操作次数。

\(n \leq 100\)

先求出未着色序列达到目标状态的最少操作次数,再考虑有初始状态的情况。

对于一个本来就匹配的位置,可以选择刷或不刷,否则和未着色的情况是一样的。

\(g_i\) 表示 \(1 \sim i\) 匹配的最少操作次数,若 \(i\) 这个位置初始状态与目标状态相同,令 \(g_i \gets g_{i - 1}\) ,否则只能类似的分割区间,从即 \(g_i \gets g_k + f_{k + 1, i}\)

时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e2 + 7;

int f[N][N], g[N];
char a[N], b[N];

int n;

signed main() {
    while (~scanf("%s%s", a + 1, b + 1)) {
        n = strlen(a + 1);

        for (int i = 1; i <= n; ++i)
            f[i][i] = 1;

        for (int len = 2; len <= n; ++len)
            for (int l = 1, r = len; r <= n; ++l, ++r) {
                if (b[l] == b[r])
                    f[l][r] = f[l][r - 1];
                else {
                    f[l][r] = inf;

                    for (int k = l; k < r; ++k)
                        f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r]);
                }
            }

        for (int i = 1; i <= n; ++i) {
            g[i] = (a[i] == b[i] ? g[i - 1] : inf);

            for (int k = 0; k < i; ++k)
                g[i] = min(g[i], g[k] + f[k + 1][i]);
        }

        printf("%d\n", g[n]);
    }

    return 0;
}

CF1922F Replace on Segment

给定 \(a_{1 \sim n}\) ,其中 \(a_i \in [1, x]\) 。每次操作可以选择一个 \(k \in [1, x]\) 和一个不存在 \(k\) 的区间 \([l, r]\) ,然后将 \([l, r]\) 全部变为 \(k\) 。求使得序列所有数相同的最少操作次数。

\(n, x \leq 100\)

\(f_{l, r, c}, g_{l, r, c}\) 分别表示将区间 \([l, r]\) 变为 \(= c\)\(\neq c\) 的方案数,直接转移是 \(O(n^3 x)\) 的。

还可以更优,考虑交换下标和值,设 \(f_{i, l, c}, g_{i, l, c}\) 分别表示 \(\leq i\) 次操作内能把左端点为 \(l\) 的区间变为 \(= c\)\(\neq c\) 的方案数,其中 \(f_{i, l, c, 0/1}\) 表示最远的右端点。

先考虑 \(f\) 的转移,则:

\[f_{i, l, c} = \max(g_{i - 1, l, c}, \max_{j = 0}^i f_{i - j, f_{j, l, c} + 1, c}) \]

再考虑 \(g\) 的转移,类似有:

\[g_{i, l, c} = \max_{j = 0}^i \max_{c' \neq c} g_{i - j, f_{j, l, c'} + 1, c} \]

由于 \(ans \leq \lfloor \frac{n}{x} \rfloor + 1\) (把每个数变成出现次数最少的颜色),时间复杂度 \(O(\frac{n^3}{x})\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e2 + 7;

int a[N], f[N][N][N], g[N][N][N];

int n, x;

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d", &n, &x);

        for (int i = 1; i <= n; ++i)
            scanf("%d", a + i);

        if (count(a + 1, a + n + 1, a[1]) == n) {
            puts("0");
            continue;
        }

        fill(f[0][n + 1], f[0][n + 1] + x + 1, n), fill(g[0][n + 1], g[0][n + 1] + x + 1, n);

        for (int l = n; l; --l)
            for (int c = 1; c <= x; ++c) {
                f[0][l][c] = (a[l] == c ? f[0][l + 1][c] : l - 1);
                g[0][l][c] = (a[l] != c ? g[0][l + 1][c] : l - 1);
            }

        for (int i = 1; i <= n / x + 1; ++i) {
            fill(f[i][n + 1], f[i][n + 1] + x + 1, n), fill(g[i][n + 1], g[i][n + 1] + x + 1, n);

            for (int l = n; l; --l) {
                for (int c = 1; c <= x; ++c) {
                    f[i][l][c] = g[i - 1][l][c], g[i][l][c] = 0;

                    for (int j = 0; j <= i; ++j)
                        f[i][l][c] = max(f[i][l][c], f[i - j][f[j][l][c] + 1][c]);
                }

                for (int j = 0; j <= i; ++j) {
                    int fir = 0, sec = 0;

                    for (int c = 1; c <= x; ++c) {
                        if (!fir || f[j][l][c] > f[j][l][fir])
                            sec = fir, fir = c;
                        else if (!sec || f[j][l][c] > f[j][l][sec])
                            sec = c;
                    }

                    for (int c = 1; c <= x; ++c)
                        g[i][l][c] = max(g[i][l][c], g[i - j][f[j][l][c == fir ? sec : fir] + 1][c]);
                }
            }

            if (*max_element(f[i][1] + 1, f[i][1] + x + 1) == n) {
                printf("%d\n", i);
                break;
            }
        }
    }

    return 0;
}

字符串问题

[AGC020E] Encoding Subsets

定义一个 01 串的压缩是满足如下方式的字符串变化过程:

  • \(0 \to 0\)\(1 \to 1\)
  • \(A \to P\)\(B \to Q\) 均合法,则 \(AB \to PQ\) 也合法。
  • \(S = A^k\)\(k\)\(A\) 的拼接),则 \(S \to (A \times k)\) 也合法,其中 ()× 为字符,\(n\) 为数字(算作一个字符,无法压缩),可以多次使用这一步骤压缩。

给定一个 01 串 \(S\) ,求它所有子集的合法压缩得到的字符串种类的数量总和。

\(n \leq 100\)

根据第二条变化方式不难想到区间 DP。先不考虑所有子集,对于一个 01 串 \(S\) ,设 \(f_{l, r}\) 表示 \(l \sim r\) 的压缩方案数,\(g_{l, r}\) 表示 \(l \sim r\) 不能表示为 \(P + Q\) 的压缩方案数,则:

  • \(f_{l, r} = \sum_{k = l}^r g_{l, k} \times f_{k + 1, r}\)
  • \(g_{l, r} = \sum_{k = 1}^{r - l} f_{l, l + k - 1}\) ,其中 \(k\)\(S[l, r]\) 的循环节。

接下来考虑所有子集,设 \(f(S)\) 表示 \(S\) 所有子集的划压缩方案总数,\(g(S)\) 表示 \(S\) 所有子集不能表示为 \(P + Q\) 的压缩方案总数,下标从 \(0\) 开始,则:

  • \(f(S) = \sum_{i = 1}^n g(S[0, i - 1]) \times f(S[i, n - 1])\)
  • \(g(S) = \sum_{d | n, d \neq n} F(S[0, d - 1] \and S[d, 2d - 1] \and \cdots \and S[n - d, n - 1])\)

使用记忆化搜索,时间复杂度 \(O(n^{3.5})\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 1e2 + 7;

map<string, int> f, g;
string str;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

int G(string str);

int F(string str) {
    if (str == "" || str == "0")
        return 1;
    else if (str == "1")
        return 2;
    else if (f.find(str) != f.end())
        return f[str];

    int res = 0, n = str.length();

    for (int i = 1; i <= n; ++i)
        res = add(res, 1ll * G(str.substr(0, i)) * F(str.substr(i, n - i + 1)) % Mod);

    return f[str] = res;
}

int G(string str) {
    if (str == "" || str == "0")
        return 1;
    else if (str == "1")
        return 2;
    else if (g.find(str) != g.end())
        return g[str];

    int res = 0, n = str.length();

    for (int k = 1; k < n; ++k) {
        if (n % k)
            continue;

        string t = "";

        for (int i = 0; i < k; ++i) {
            char c = '1';

            for (int j = i; j < n; j += k)
                c &= str[j];

            t.push_back(c);
        }

        res = add(res, F(t));
    }

    return g[str] = res;
}

signed main() {
    cin >> str;
    printf("%d", F(str));
    return 0;
}

P7914 [CSP-S 2021] 括号序列

合法字符串是由 ()* 组成的字符串,并对于给定的常数 \(k\) ,定义为(下面 \(S\) 为由不超过 \(k\)* 组成的字符串):

  • ()(S) 均合法。
  • 若串 A 和串 B 合法,则 ABASB 也合法。
  • 若串 A 合法,则 (A)(SA)(AS) 也合法。

给出长度为 \(n\) 的字符串 \(S\) ,其仅由 ()*? 组成,求所有将 ? 替换为 ()* 三者之一的方案中 \(S\) 合法的方案数。

\(k \leq n \leq 500\)

考虑区间 DP,根据判定合法的条件,不同串的状态所能转移到串的状态是不一样的,考虑加一维表示串的形态。设 \(f_{l, r, 0 \sim 5}\) 表示 \(l \sim r\) 半合法的方案数,半合法的形态具体分为五种:

  • \(f_{l, r, 0}\) :全是 * 的串。
  • \(f_{l, r, 1}\) :左右端为一对匹配的括号的串。
  • \(f_{l, r, 2}\) :左端为 ( 、右端为 * 的串。
  • \(f_{l, r, 3}\) :左端为 ( 、右端为 ) 的串,包括 \(f_{l, r, 1}\)
  • \(f_{l, r, 4}\) :左端为 * 、右端为 ) 的串。
  • \(f_{l, r, 5}\) :左右端均为 * 的串,包括 \(f_{l, r, 0}\)

接下来考虑转移:

  • \(f_{l, r, 0}\) :直接预处理即可。
  • \(f_{l, r, 1} = (f_{l + 1, r - 1, 0} + f_{l + 1, r - 1, 2} + f_{l + 1, r - 1, 3} + f_{l + 1, r - 1, 4}) \times cmp(l, r)\) ,其中 \(cmp(l, r)\) 表示 \(l, r\) 是否能括号匹配。
  • \(f_{l, r, 2} = \sum_{k = l}^{r - 1} f_{l, k, 3} \times f_{k + 1, r, 0}\)
  • \(f_{l, r, 3} = f_{l, r, 1} + \sum_{k = l}^{r - 1} (f_{l, k, 2} + f_{l, k, 3}) \times f_{k + 1, r, 1}\)
  • \(f_{l, r, 4} = \sum_{k = l}^{r - 1} f_{l, k, 0} \times f_{k + 1, r, 3}\)
  • \(f_{l, r, 5} = f_{l, r, 0} + \sum_{k = l}^{r - 1} f_{l, k, 4} \times f_{k + 1, r, 0}\)

答案即为 \(f_{1, n, 3}\) ,时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 5e2 + 7;

int f[N][N][6];
char str[N];

int n, k;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

signed main() {
    scanf("%d%d%s", &n, &k, str + 1);

    for (int i = 1; i <= n; ++i)
        for (int j = i; j <= min(i + k - 1, n) && (str[j] == '*' || str[j] == '?'); ++j)
            f[i][j][0] = 1;

    for (int i = 1; i <= n; ++i)
        if (str[i] == '*' || str[i] == '?')
            f[i][i][5] = 1;

    for (int i = 1; i < n; ++i) {
        f[i][i + 1][1] = f[i][i + 1][3] = ((str[i] == '(' || str[i] == '?') && (str[i + 1] == ')' || str[i + 1] == '?'));
        f[i][i + 1][5] = ((str[i] == '*' || str[i] == '?') && (str[i + 1] == '*' || str[i + 1] == '?'));
    }

    for (int len = 3; len <= n; ++len)
        for (int l = 1, r = len; r <= n; ++l, ++r) {
            if ((str[l] == '(' || str[l] == '?') && (str[r] == ')' || str[r] == '?'))
                f[l][r][1] = add(add(f[l + 1][r - 1][0], f[l + 1][r - 1][2]), add(f[l + 1][r - 1][3], f[l + 1][r - 1][4]));

            f[l][r][3] = f[l][r][1], f[l][r][5] = f[l][r][0];

            for (int k = l; k < r; ++k) {
                f[l][r][2] = add(f[l][r][2], 1ll * f[l][k][3] * f[k + 1][r][0] % Mod);
                f[l][r][3] = add(f[l][r][3], 1ll * add(f[l][k][2], f[l][k][3]) * f[k + 1][r][1] % Mod);
                f[l][r][4] = add(f[l][r][4], 1ll * f[l][k][0] * f[k + 1][r][3] % Mod);
                f[l][r][5] = add(f[l][r][5], 1ll * f[l][k][4] * f[k + 1][r][0] % Mod);
            }
        }

    printf("%d", f[1][n][3]);
    return 0;
}

[ABC261G] Replace

给出两个字符串 \(S, T\) ,有 \(n\) 种可以使用的操作,第 \(i\) 种操作表示将 \(S\) 中的一个字符 \(c_i\) 替换为字符串 \(a_i\)

最小化 \(S\) 变为 \(T\) 的操作数量,或报告无解。

\(n, |S|, |T|, |a_i| \leq 50\)

\(dp_{i, j}\) 表示 \(S[1, i] \to T[1, j]\) 的最少操作数,转移时考虑逐位转移,需要求出 \(S[i] \to T[j + 1, k]\) 的最小代价。因此再设 \(f_{c, l, r}\) 表示 \(c \to T[l, r]\) 的最小代价,则 \(dp_{i, j} \gets dp_{i - 1, k - 1} + f_{S[i], k, j}\)

考虑 \(f\) 的转移。对于 \(|a_i| = 1\) 的情况,可以用 Floyd 做到 \(O(26^3)\) 转移出 \(d_{i, j}\) 表示字符 \(i\) 变成字符 \(j\) 的最少操作数,则 \(f_{i, l, r} \gets f_{j, l, r} + d_{j, i}\) ,这个转移可以通过 Dijkstra 实现。

再考虑 \(|a_i| > 1\) 的情况,设 \(g_{i, j, l, r}\) 表示 \(a_i[1, j] \to T[l, r]\) 的最小代价,则用区间 DP 转移 \(g_{i, j, l, r} \gets g_{i, j - 1, l, k} + f_{a_i[j], k + 1, r}\) (注意这里无法更新 \(g_{i, 1, l, r}\) ),然后再转移 \(f_{c_i, l, r} \gets g_{i, |a_i|, l, r} + 1\) ,最后转移 \(g_{i, 1, l, r} \gets f_{a_i[1], l, r}\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 5e1 + 7, S = 26;

int len[N], d[S][S], dp[N][N], f[N][N][N], g[N][N][N][N];
char tr[N][N], s[N], t[N];

int n, m, q;

signed main() {
    scanf("%s%s%d", s + 1, t + 1, &q);
    n = strlen(s + 1), m = strlen(t + 1);
    memset(d, inf, sizeof(d));

    for (int i = 0; i < S; ++i)
        d[i][i] = 0;

    for (int i = 1; i <= q; ++i) {
        scanf("%s", tr[i]), scanf("%s", tr[i] + 1);
        len[i] = strlen(tr[i] + 1);

        if (len[i] == 1)
            d[tr[i][1] - 'a'][tr[i][0] - 'a'] = 1;
    }

    for (int k = 0; k < S; ++k)
        for (int i = 0; i < S; ++i)
            for (int j = 0; j < S; ++j)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);

    memset(f, inf, sizeof(f)), memset(g, inf, sizeof(g));

    for (int L = 1; L <= m; ++L)
        for (int l = 1, r = L; r <= m; ++l, ++r) {
            if (l == r)
                f[t[l] - 'a'][l][r] = 0;

            for (int i = 1; i <= q; ++i) {
                if (len[i] == 1)
                    continue;

                for (int j = 2; j <= len[i]; ++j)
                    for (int k = l; k < r; ++k)
                        g[i][j][l][r] = min(g[i][j][l][r], g[i][j - 1][l][k] + f[tr[i][j] - 'a'][k + 1][r]);

                f[tr[i][0] - 'a'][l][r] = min(f[tr[i][0] - 'a'][l][r], g[i][len[i]][l][r] + 1);
            }
            
            auto Dijkstra = [&]() {
            	priority_queue<pair<int, int> > q;
	            
	            for (int i = 0; i < S; ++i)
	            	q.emplace(-f[i][l][r], i);
	            
	            while (!q.empty()) {
	            	auto c = q.top();
	            	q.pop();
	            	
	            	if (-c.first != f[c.second][l][r])
	            		continue;
	            		
	            	int u = c.second;
	            	
	            	for (int v = 0; v < S; ++v)
	            		if (f[v][l][r] > f[u][l][r] + d[u][v])
	            			f[v][l][r] = f[u][l][r] + d[u][v], q.emplace(-f[v][l][r], v);
	            }
            };
            
            Dijkstra();

            for (int i = 1; i <= q; ++i)
                g[i][1][l][r] = f[tr[i][1] - 'a'][l][r];
        }

    memset(dp, inf, sizeof(dp)), dp[0][0] = 0;

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
            for (int k = 1; k <= j; ++k)
                dp[i][j] = min(dp[i][j], dp[i - 1][k - 1] + f[s[i] - 'a'][k][j]);

    printf("%d", dp[n][m] == inf ? -1 : dp[n][m]);
    return 0;
}

费用提前计算

UVA10559 方块消除 Blocks

给定长度为 \(n\) 的方块序列,每个方块有一个颜色,每次消除一段颜色相同长度为 \(x\) 的方块,并获得 \(x^2\) 的分数,消除后剩下的方块会合并起来,求最大得分。

\(n \leq 200\)

注意到这个得分函数是二次的关系,不能直接用现在的决策推未来的费用。

不妨设 \(f_{l, r, k}\) 表示消掉了 \([l, r]\) 这个区间,且后面有 \(k\) 个位置和 \(r\) 位置合并在一起消掉了。

  • 如果 \(r\) 和后面 \(k\) 个块一起消掉,就有 \(f_{l, r, k} \gets f_{l, r - 1, 0} + (k + 1)^2\)

  • 如果在 \([l, r - 1]\) 内还有与 \(r\) 颜色相同的块 \(x\) ,那么可以先消除 \([x + 1, r - 1]\) 然后合并 \(x, j\) 再一起消除,即 \(f_{l, r, k} \gets f_{l, x, k + 1} + f_{x + 1, r - 1, 0}\)

最后答案即为 \(f_{1, n, 0}\) ,时间复杂度 \(O(n^4)\)

注意这个转移并没有利用贡献式子的形式,因此更一般化地可以解决 CF1107E Vasya and Binary String

#include <bits/stdc++.h>
using namespace std;
const int N = 2e2 + 7;

int f[N][N][N], col[N], cnt[N];

int n, m;

int dfs(int l, int r, int k) {
    if (~f[l][r][k])
        return f[l][r][k];

    if (l == r)
        return (cnt[l] + k) * (cnt[l] + k);

    f[l][r][k] = dfs(l, r - 1, 0) + (cnt[r] + k) * (cnt[r] + k);

    for (int i = l; i + 1 <= r - 1; ++i)
        if (col[i] == col[r])
            f[l][r][k] = max(f[l][r][k], dfs(l, i, cnt[r] + k) + dfs(i + 1, r - 1, 0));

    return f[l][r][k];
}

signed main() {
    int T;
    scanf("%d", &T);

    for (int task = 1; task <= T; ++task) {
        scanf("%d", &n), m = 0;

        for (int i = 1, lst = -1; i <= n; ++i) {
            int x;
            scanf("%d", &x);

            if (x != lst)
                col[++m] = x, cnt[m] = 0;

            ++cnt[m], lst = x;
        }

        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n; ++j)
                memset(f[i][j], -1, sizeof(int) * n);

        printf("Case %d: %d\n", task, dfs(1, m, 0));
    }

    return 0;
}

P4870 [BalticOI 2009 Day1] 甲虫

数轴上有 \(n\) 滴水,每滴水最开始有 \(m\) 的大小。每个单位时间内可以移动一个单位长度,同时所有水减小 \(1\),求最多能喝多少水。

\(n \leq 300\)

考虑到这个露水会变成 \(0\) 就不会减小了,所以考虑枚举必吃的露水的数量。这样最优解一定会被枚举到,而且因为取得是最大值,就算减成负数也不影响。

首先对露水排个序。因为已知露水总量,并且时间会对每一个还没有吃掉的露水造成负贡献。因为这个时间不好存储,于是考虑费用提前计算,设 \(f_{l, r, 0/1}\) 表示吃完了 \([l, r]\) 的露水,现在在左/右端点的最多喝水量。令 \(s = n - r + l\) ,则有

\[f_{l, r, 0} = \max(f_{l + 1, r, 0} - (a_{l + 1} - a_l) \times s, f_{l + 1, r, 1} - (a_r - a_l)\times s) + m \\ f_{l, r, 1} = \max(f_{l, r - 1, 1} - (a_r - a_{r - 1}) \times s, f_{l, r - 1, 0} - (a_r - a_l) \times s) + m \]

考虑解释这个方程,可以理解成为周围的露水都还在丢失,在这里减去贡献;相对认为当前这个露水没有损失水量,因为损失的水量我们已经丢去了。时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 3e2 + 7;

int f[N][N][2];
int a[N];

int n, m;

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i);

    sort(a + 1, a + 1 + n);
    int ans = 0;

    for (int num = 1; num <= n; ++num) {
        memset(f, 0, sizeof(f));

        for (int i = 1; i <= n; ++i)
            ans = max(ans, f[i][i][0] = f[i][i][1] = m - abs(a[i]) * num);

        for (int len = 2; len <= num; ++len)
            for (int l = 1, r = len; r <= n; ++l, ++r) {
                f[l][r][0] = max(f[l + 1][r][0] + m - (a[l + 1] - a[l]) * (num - r + l),
                    f[l + 1][r][1] + m - (a[r] - a[l]) * (num - r + l));
                f[l][r][1] = max(f[l][r - 1][1] + m - (a[r] - a[r - 1]) * (num - r + l),
                    f[l][r - 1][0] + m - (a[r] - a[l]) * (num - r + l));
                ans = max(ans, max(f[l][r][0], f[l][r][1]));
            }
    }

    printf("%d", ans);
    return 0;
}

辅助数组优化

担心

\(n\) 个人,第 \(i\) 个人的权值为 \(a_i\) 。每次随机从序列中选择相邻的两个数 \(i, i + 1\) ,有 \(\frac{a_i}{a_i + a_{i + 1}}\) 的概率保留 \(a_i\)\(\frac{a_{i + 1}}{a_i + a_{i + 1}}\) 的概率保留 \(a_{i + 1}\) ,会且只会保留一个。求每个人留到最后的概率。

\(n \leq 500\)

\(f_{l, r, i}\) 表示考虑区间 \([l, r]\)\(i\) 保留到最后的概率,转移就枚举分界点和左右保留的人,可以做到 \(O(n^5)\)

注意到一个人若留到最后,则两边的情况是独立的。设 \(f_{l, r}\) 表示考虑区间 \([l, r]\)\(l\) 保留到最后的概率,\(g_{l, r}\)\(r\) 保留到最后的概率,则 \(i\) 在区间 \([l, r]\) 保留到最后的概率即为 \(g_{l, i} \times f_{i, r}\)

\(f\) 的求解为例,\(g\) 的求解是类似的。枚举分界点 \(k\) 与另一区间获胜的人 \(t\) ,有转移:

\[f_{l, r} = \frac{1}{r - l} \sum_{k = l}^{r - 1} \sum_{t = k + 1}^{r} f_{l, k} \times g_{k + 1, t} \times f_{t, r} \times \frac{a_l}{a_l + a_t} \\ g_{l, r} = \frac{1}{r - l} \sum_{k = l}^{r - 1} \sum_{t = l}^k g_{l, t} \times f_{t, k} \times g_{k + 1, r} \times \frac{a_r}{a_r + a_t} \]

最后那个分数并不好拆,考虑将其拿出来:

\[f_{l, r} = \frac{1}{r - l} \sum_{t = l + 1}^r f_{t, r} \times \frac{a_l}{a_l + a_t} \sum_{k = l}^{t - 1} f_{l, k} \times g_{k + 1, t} \\ g_{l, r} = \frac{1}{r - l} \sum_{t = l}^{r - 1} g_{l, t} \times \frac{a_r}{a_r + a_t} \sum_{k = t}^{r - 1} f_{t, k} \times g_{k + 1, r} \]

动态维护 \(s_{l, r} = \sum_{i = l}^{r - 1} f_{l, i} \times g_{i + 1, r}\) ,则每次可以 \(O(n)\) 求出 \(f_{l, r}, g_{l, r}\) ,然后 \(O(n)\) 修改 \(s\) ,时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 5e2 + 7;

int a[N], f[N][N], g[N][N], p[N][N], s[N][N];

int n;

inline int add(int x, int y) {
    x += y;

    if (x >= Mod)
        x -= Mod;

    return x;
}

inline int dec(int x, int y) {
    x -= y;

    if (x < 0)
        x += Mod;

    return x;
}

inline int mi(int a, int b) {
    int res = 1;

    for (; b; b >>= 1, a = 1ll * a * a % Mod)
        if (b & 1)
            res = 1ll * res * a % Mod;

    return res;
}

inline void update(int l, int r) {
    for (int i = r + 1; i <= n; ++i)
        s[l][i] = add(s[l][i], 1ll * f[l][r] * g[r + 1][i] % Mod);

    for (int i = 1; i < l; ++i)
        s[i][r] = add(s[i][r], 1ll * f[i][l - 1] * g[l][r] % Mod);
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i);

    for (int i = 1; i <= n; ++i)
        for (int j = i + 1; j <= n; ++j)
            p[i][j] = mi(add(a[i], a[j]), Mod - 2);

    for (int i = 1; i <= n; ++i)
        f[i][i] = g[i][i] = 1, update(i, i);

    for (int len = 2; len <= n; ++len)
        for (int l = 1, r = len; r <= n; ++l, ++r) {
            for (int k = l + 1; k <= r; ++k)
                f[l][r] = add(f[l][r], 1ll * s[l][k] * f[k][r] % Mod * p[l][k] % Mod);
            
            for (int k = l; k < r; ++k)
                g[l][r] = add(g[l][r], 1ll * s[k][r] * g[l][k] % Mod * p[k][r] % Mod);

            f[l][r] = 1ll * f[l][r] * a[l] % Mod * mi(len - 1, Mod - 2) % Mod;
            g[l][r] = 1ll * g[l][r] * a[r] % Mod * mi(len - 1, Mod - 2) % Mod;
            update(l, r);
        }

    for (int i = 1; i <= n; ++i)
        printf("%d\n", 1ll * g[1][i] * f[i][n] % Mod);

    return 0;
}

事实上还可以从时光倒流的角度思考,那么过程就变为不断向区间内加数字。

\(f_{l, r}\) 表示 \([l, r]\) 只存在两个端点,中间均为空的概率,于是 \(i\) 保留到最后的概率即为 \(f_{0, i} \times f_{i, n + 1}\) 。转移考虑枚举当前放的数 \(k\) ,则:

\[f_{l, r} = \frac{1}{r - l - 1} \sum_{k = l + 1}^{r - 1} f_{l, k} \times f_{k, r} \times (\frac{a_l}{a_l + a_k} + \frac{a_r}{a_r + a_k}) \]

边界:\(a_0 = a_{n + 1} = 0\)\(f_{i, i + 1} = 1\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 5e2 + 7;

int a[N], f[N][N], p[N][N];

int n;

inline int add(int x, int y) {
    x += y;

    if (x >= Mod)
        x -= Mod;

    return x;
}

inline int dec(int x, int y) {
    x -= y;

    if (x < 0)
        x += Mod;

    return x;
}

inline int mi(int a, int b) {
    int res = 1;

    for (; b; b >>= 1, a = 1ll * a * a % Mod)
        if (b & 1)
            res = 1ll * res * a % Mod;

    return res;
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i);

    for (int i = 1; i <= n; ++i)
        for (int j = i + 1; j <= n; ++j)
            p[i][j] = mi(add(a[i], a[j]), Mod - 2);

    for (int i = 0; i <= n; ++i)
        f[i][i + 1] = 1;

    for (int len = 2; len <= n; ++len)
        for (int l = 0, r = len; r <= n + 1; ++l, ++r) {
            for (int k = l + 1; k < r; ++k)
                f[l][r] = add(f[l][r], 1ll * f[l][k] * f[k][r] % Mod * 
                    add(1ll * a[l] * p[l][k] % Mod, 1ll * a[r] * p[k][r] % Mod) % Mod);

            f[l][r] = 1ll * f[l][r] * mi(len - 1, Mod - 2) % Mod;
        }

    for (int i = 1; i <= n; ++i)
        printf("%d\n", 1ll * f[0][i] * f[i][n + 1] % Mod);

    return 0;
}

P5336 [THUSC 2016] 成绩单

给出 \(n\) 个数 \(w_{1 \sim n}\) 和常数 \(a, b\) ,每次可以花费 \(a + b \times (\max_{i = l}^r a_i - \min_{i = l}^r a_i)^2\) 的代价删去一个子区间 \([l, r]\) (删完之后后面的数会补上来),求删掉所有数的最小代价。

\(n \leq 50\)

考虑区间 DP,设 \(f_{l, r}\) 表示删完 \([l, r]\) 的最小代价。但是这样并不好转移,因为删除区间中的某些部分后不好得知区间的极值。

不妨将极值计入状态,再设 \(g_{l, r, p, q}\) 表示区间内剩余数 \(p \leq \min \leq \max \leq q\) 的最小代价。

首先可以得到:

\[f_{l, r} = \min \{ g_{l, r, p, q} + a + b \times (q - p)^2 \} \]

接下来考虑 \(g\) 的转移,枚举断点 \(k\)

  • 左半区间选空:\(f_{l, r, p, q} \gets \min \{ f_{l, k} + g_{k + 1, r, p, q} \}\)
  • 右半区间选空:\(f_{l, r, p, q} \gets \min \{ g_{l, k, p, q} + f_{k + 1, r} \}\)
  • 同时选掉左右区间剩下的数:\(g_{l, r, p, q} = \min \{ g_{l, k, p, q} + g_{k + 1, r, p, q} \}\)

离散化 \(w_{1 \sim n}\) ,时间复杂度 \(O(n^5)\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 5e1 + 7;

int val[N], f[N][N], g[N][N][N][N];

int n, a, b;

signed main() {
    scanf("%d%d%d", &n, &a, &b);
    vector<int> vec;

    for (int i = 1; i <= n; ++i)
        scanf("%d", val + i), vec.emplace_back(val[i]);
    
    sort(vec.begin(), vec.end()), vec.erase(unique(vec.begin(), vec.end()), vec.end());
    int m = vec.size();

    for (int i = 1; i <= n; ++i)
        val[i] = lower_bound(vec.begin(), vec.end(), val[i]) - vec.begin();

    memset(f, inf, sizeof(f)), memset(g, inf, sizeof(g));

    for (int i = 1; i <= n; ++i) {
        f[i][i] = a;

        for (int j = 0; j <= val[i]; ++j)
            for (int k = val[i]; k < m; ++k)
                g[i][i][j][k] = 0;
    }

    for (int len = 2; len <= n; ++len)
        for (int l = 1, r = len; r <= n; ++l, ++r)
            for (int p = 0; p < m; ++p)
                for (int q = p; q < m; ++q) {
                    for (int k = l; k < r; ++k)
                        g[l][r][p][q] = min(g[l][r][p][q], 
                            min(f[l][k], g[l][k][p][q]) + min(f[k + 1][r], g[k + 1][r][p][q]));

                    f[l][r] = min(f[l][r], g[l][r][p][q] + a + b * (vec[q] - vec[p]) * (vec[q] - vec[p]));
                }

    printf("%d", f[1][n]);
    return 0;
}

P10202 [湖北省选模拟 2024] 沉玉谷 / jade

给定长度为 \(n\) 的序列 \(a_{1 \sim n}\) ,每次可以删去一个同色连续段,求删空方案数。

\(n \leq 50\)\(1 \leq a_i \leq n\)

\(f_{l, r, k}\) 表示区间 \([l, r]\)\(k\) 次删空的方案数,记录 \(k\) 的原因是左右区间需要插板法合并。转移时枚举最后一次删的最左边的数 \(a_i\) ,那么 \([l, i - 1]\) 部分必须都删完,\([i, r]\) 部分要么删完要么留下若干与 \(a_i\) 相等的数一起删。

发现右边区间不好处理“留下若干与 \(a_i\) 相等的数”的限制,考虑记辅助数组 \(g_{l, r, k, c}\) 表示 \([l, r]\)\(k\) 次留下若干 \(c\) 的方案数,则:

\[f_{l, r, k} = \sum_{c = 1}^n g_{l, r, k - 1, c} \]

该式子简化了第一步枚举 \(a_i\) ,可以发现是等价的。

下面考虑 \(g\) 的转移,枚举保留下来最左边的位置 \(k\) 和左右删除次数 \(x, y\) 可以得到:

\[g_{l, r, x + y, a_k} \gets f_{l, k - 1, x} \times (g_{k + 1, r, y, a_k} + f_{k + 1, r, y}) \times \binom{x + y}{x} \]

时间复杂度 \(O(n^5)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 5e1 + 7;

int f[N][N][N], g[N][N][N][N], C[N][N], a[N];

int n;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i);

    for (int i = C[0][0] = 1; i <= n; ++i)
        for (int j = C[i][0] = 1; j <= i; ++j)
            C[i][j] = add(C[i - 1][j], C[i - 1][j - 1]);

    for (int i = 1; i <= n; ++i)
        f[i][i][1] = 1, g[i][i][0][a[i]] = 1;

    for (int len = 2; len <= n; ++len)
        for (int l = 1, r = len; r <= n; ++l, ++r) {
            for (int k = l; k <= r; ++k)
                for (int x = 0; x <= k - l; ++x)
                    for (int y = 0; y <= r - k; ++y)
                        g[l][r][x + y][a[k]] = add(g[l][r][x + y][a[k]], 1ll * (k == l ? 1 : f[l][k - 1][x]) * 
                            add(g[k + 1][r][y][a[k]], k == r ? 1 : f[k + 1][r][y]) % Mod * C[x + y][x] % Mod);

            for (int k = 1; k <= r - l + 1; ++k)
                for (int i = 1; i <= n; ++i)
                    f[l][r][k] = add(f[l][r][k], g[l][r][k - 1][i]);
        }

    int ans = 0;

    for (int i = 1; i <= n; ++i)
        ans = add(ans, f[1][n][i]);

    printf("%d", ans);
    return 0;
}

CF888F Connecting Vertices

\(1 \sim n\) 排成一圈,需要将其连成一棵树,求满足以下条件的树的方案数:

  • 给定 \(n \times n\) 的 01 矩阵 \(a\) ,若 \(a_{i, j} = 0\)\(i, j\) 不能连边。
  • 两条边不能在端点之外的地方相交。

\(n \leq 500\)

考虑区间 DP,设 \(f_{l, r}\) 表示只考虑 \([l, r]\) 的点的生成树数量,转移就枚举 \(l\) 往右连的最后一条边 \((l, k)\) ,那么 \([k, r]\) 都不能往前连。

再设 \(g_{i, j}\) 表示只考虑 \([l, r]\) 的点,且连边 \((l, r)\) 的生成树数量,则:

\[f_{l, r} = \sum_{k = l + 1}^r a_{l, k} \times g_{l, k} \times f_{k, r} \]

再考虑求 \(g\) ,由于连边前 \(l, r\) 一定不连通,因此可以枚举 \(k\) 作为断点,则:

\[g_{l, r} = a_{l, r} \times \sum_{k = l}^{r - 1} f_{l, k} \times f_{k + 1, r} \]

时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 5e2 + 7;

int a[N][N], f[N][N], g[N][N];

int n;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j)
            scanf("%d", a[i] + j);

    for (int i = 1; i <= n; ++i)
        f[i][i] = g[i][i] = 1;

    for (int len = 2; len <= n; ++len)
        for (int l = 1, r = len; r <= n; ++l, ++r) {
            if (a[l][r]) {
                for (int k = l; k < r; ++k)
                    g[l][r] = add(g[l][r], 1ll * f[l][k] * f[k + 1][r] % Mod);
            }

            for (int k = l + 1; k <= r; ++k)
                if (a[l][k])
                    f[l][r] = add(f[l][r], 1ll * g[l][k] * f[k][r] % Mod);
        }

    printf("%d", f[1][n]);
    return 0;
}
posted @ 2025-03-26 11:56  EverythingsGone  阅读(23)  评论(0)    收藏  举报