DP 杂题

Trick

  1. 优化 dp 状态找题目的特殊性质。(XXYX Binary Tree)

  2. 对于有多组修改,但只是独立修改一个位置权值的 dp 题,可以考虑去讨论是否选择被修改的这个位置。(Contest with Drinks Hard)

  3. 对于不好优化的序列 dp,可以考虑放在分治上做。(Yet Another Partiton Problem)

  4. 对于 \(\min(B_j+C_k-i,A_i+C_k-j,A_i+B_j-k)\) 这样的形式,可以变成 \(A_i+B_j+C_k-\max(i+A_i,j+B_j,k+C_k)\) 进行求解。(赌局)

题目

[ARC157E] XXYX Binary Tree

一个很显然的暴力,\(f_{u,a,b,c}\) 表示在在 \(u\) 的子树中,是否有 \(a\)XX\(b\)XY\(c\)YX

这个状态 \(O(n^4)\) 的,考虑优化,可以先省去一个 \(c\),变成 \(f_{u,a,b}\),因为 \(a+b+c\) 的总和是知道的。

然后优化不了了……

只能重新设计,发现都没有用上二叉树的条件,显然要找一点性质的。

可以发现 \(Y\) 的个数好像可以确定,如果 \(Y\) 为非根节点,那么 \(Y\) 的数量有 \(B\) 个,否则就有 \(B+1\) 个,因为一个除了根节点的 \(Y\) 肯定可以贡献一个 XY

还可以发现非叶子的 \(Y\) 可以贡献两个 YX

然后就可以刻画 XYYX 的数量了,这里分根节点是否为 \(Y\) 两种情况。

如果根不为 \(Y\),那么一共有 \(B\)\(Y\),记叶子节点为 \(Y\) 的个数为 \(sum\),那么 YX 的数量就是 \(2(B-sum)\),所以 \(sum\) 满足 \(sum=B-\frac{C}{2}\)

如果根为 \(Y\),那么一共有 \(B+1\)\(Y\),记叶子节点为 \(Y\) 的个数为 \(sum\),那么 YX 的数量就是 \(2(B+1-sum)\),所以 \(sum\) 满足 \(sum=B+1-\frac{C}{2}\)

所以设 \(f_{u,i,0/1}\) 表示 \(u\) 的子树中有 \(i\) 个叶子为 \(Y\)\(u\) 是否是 \(Y\) 时最多有多少个非叶子的 \(Y\)

这里可以 \(O(n^2)\) 树上背包转移。

代码
#include <bits/stdc++.h>

void Freopen() {
    freopen("", "r", stdin);
    freopen("", "w", stdout);
}

using namespace std;
const int N = 1e4 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;

int n, A, B, C;
int f[N][N][2], siz[N];

vector< int> G[N];

void dfs( int u) {
    siz[u] = 0;
    if (! G[u].size()) {
        siz[u] = 1;
        f[u][0][0] = 0, f[u][1][1] = 0;
        return ;
    }

    f[u][0][0] = 0, f[u][0][1] = 1;

    for ( auto v : G[u]) {
        dfs(v);

        // 这里注意,必须要倒叙枚举,这样才能保证不会用到已经被转移过的信息
        for ( int i = siz[u]; i >= 0; i --)
            for ( int j = siz[v]; j >= 0; j --) {
                f[u][i + j][0] = max(f[u][i + j][0], f[u][i][0] + max(f[v][j][0], f[v][j][1]));
                f[u][i + j][1] = max(f[u][i + j][1], f[u][i][1] + f[v][j][0]);
            }

        siz[u] += siz[v];
    }
}

void solve() {
    cin >> n >> A >> B >> C;

    for ( int i = 1; i <= n; i ++) {
        G[i].clear();

        for ( int j = 0; j <= B + 1; j ++)
            f[i][j][0] = f[i][j][1] = -inf;
    }

    for ( int i = 2, fa; i <= n; i ++)
        cin >> fa, G[fa].push_back(i);

    if (C & 1) return cout << "No\n", void();

    dfs(1);

    if (B - C / 2 >= 0 && f[1][B - C / 2][0] >= C / 2) {
        cout << "Yes\n";
        return ;
    }

    if (B + 1 - C / 2 >= 0 && f[1][B + 1 - C / 2][1] >= C / 2) {
        cout << "Yes\n";
        return ;
    }
    
    cout << "No\n";
}

signed main() {
    ios :: sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    int t; cin >> t;
    while (t --) solve();

    return 0;
}

[ARC066F] Contest with Drinks Hard

它这个贡献形式就已经表明了这是一个 1d 问题。

\(f_i\) 表示考虑了前 \(i\) 题的最大值。

转移就是枚举 \(j\)\((j+1,i]\) 的题目都要做。

当然也可以不做第 \(i\) 题,直接从 \(f_{i-1}\) 转移,记 \(cost(l,r)=\dfrac{(r-l+1)(r-l+2)}{2}-sum_r+sum_{l-1}\)

那么 \(f_i\) 转移就有:

\[\begin{cases} f_i\leftarrow f_{i-1} \\ f_i\leftarrow f_j+cost(j+1,i),j\lt i \end{cases} \]

\(cost(j+1,i)\) 拆开,可以直接斜率优化。

但是这个题麻烦的是有多组修改,直接做 \(O(nm)\) 过不了。

但特殊在于,每个修改都是独立的,所以考虑讨论是否选择被修改的那道题。

\(f_i\) 表示考虑了前 \(i\) 个题的最大值;\(g_i\) 表示考虑了后 \(i\) 个题的最大值;\(h_i\) 表示考虑了所有题,但是钦定第 \(i\) 题必做的最大值。

假设题目 \(i\) 被修改后,考虑是否选择它,如果不选择 \(i\),那么答案就是 \(f_{i-1}+g_{i+1}\);如果选择,答案就是 \(h_i-x+a_i\)

所以最后的答案就是就是 \(\max(f_{i-1}+g_{i+1},h_i-x+a_i)\)

\(f_i\) 转移和上述一样,斜率优化即可;\(g_i\) 也就是倒序 dp,和 \(f_i\) 相似。

考虑 \(h_i\) 怎么求?直接转移就是:

\[h_i\leftarrow f_{l-1}+g_{r+1}+cost(l,r),i\in(l,r) \]

这里转移是 \(O(n^3)\) 的。

因为 \(l,r\) 要跨过 \(i\),那么不妨考虑分治。

\(dp_i\) 左端点为 \(i\)\(i\in[l,mid]\),右端点为 \(j-1\)\(j\in[mid+1,r+1]\) 时,\(f_{i-1}+g_{j}+cost(i,j-1)\) 的最大值。

这里也是可以斜率优化去求的,求出来后 \(dp_i\) 可以给 \(h_{i\sim mid}\) 贡献最大值。

当然,因为 \(dp_i\) 只能给 \(h_{i\sim mid}\) 贡献最大值,那么再设一个和 \(dp_i\) 相反的能够给 \(h_{mid+1\sim i}\) 贡献最大值的 \(dp'_i\) 即可。

实现这块,我用的是李超线段树,多带一个 \(\log\) 但是无伤大雅。

代码
#include <bits/stdc++.h>
#define int long long

void Freopen() {
    freopen("", "r", stdin);
    freopen("", "w", stdout);
}

using namespace std;
const int N = 3e5 + 10, M = 2e5 + 10, inf = 1e18, mod = 998244353;

int n, m;
int a[N], sum[N];
int f[N], g[N], h[N];

int tot, rt;

struct sgt {
    #define mid ((l + r) >> 1)

    int tag[N * 18], ls[N * 18], rs[N * 18];
    struct line {
        int k, b;
    } l[N];

    int cal( int id, int x) {
        return l[id].k * x + l[id].b;
    }

    void insert( int u, int & k, int l = 1, int r = n) {
        if (! k) return k = ++ tot, ls[k] = 0, rs[k] = 0, tag[k] = u, void();
        
        int & v = tag[k];
        if (cal(v, mid) < cal(u, mid)) swap(v, u);
        if (cal(u, l) > cal(v, l)) insert(u, ls[k], l, mid);
        if (cal(u, r) > cal(v, r)) insert(u, rs[k], mid + 1, r);
    }

    int ask( int x, int k, int l = 1, int r = n) {
        if (! k) return -inf;

        return max(cal(tag[k], x), x <= mid ? ask(x, ls[k], l, mid) : ask(x, rs[k], mid + 1, r));
    }    

    #undef mid
} tr;

void solve( int l, int r) {
    if (l == r) {
        h[l] = max(h[l], 1 - a[l]);
        return ;
    }

    int mid = (l + r) >> 1;
    solve(l, mid), solve(mid + 1, r);

    rt = tot = 0;
    for ( int i = mid + 1; i <= r + 1; i ++) {
        tr.l[i] = {-i, g[i] - sum[i - 1] + (i * i + i) / 2};
        tr.insert(i, rt);
    }

    int mx = -inf;
    for ( int i = l; i <= mid; i ++) {
        mx = max(mx, f[i - 1] + tr.ask(i, rt) + (i * i - i) / 2 + sum[i - 1]);
        h[i] = max(h[i], mx);
    }

    rt = tot = 0;
    for ( int i = mid; i >= l - 1; i --) {
        tr.l[i] = {-i, f[i] + sum[i] + (i * i - i) / 2};
        tr.insert(i, rt);
    }

    mx = -inf;
    for ( int i = r; i > mid; i --) {
        mx = max(mx, g[i + 1] + tr.ask(i, rt) + (i * i + i) / 2 - sum[i]);
        h[i] = max(h[i], mx);
    }
}

signed main() {
    ios :: sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    cin >> n;

    for ( int i = 1; i <= n; i ++) cin >> a[i], sum[i] = sum[i - 1] + a[i];

    memset(h, 128, sizeof h);

    rt = tot = 0;
    tr.l[0] = {0, 0};
    tr.insert(0, rt);

    for ( int i = 1; i <= n; i ++) {
        f[i] = f[i - 1];
        f[i] = max(f[i], tr.ask(i, rt) + (i * i + i) / 2 - sum[i]);
        tr.l[i] = {-i, f[i] + sum[i] + (i * i - i) / 2};
        tr.insert(i, rt);
    }

    rt = tot = 0;
    tr.l[n + 1] = {-(n + 1), -sum[n] + ((n + 1) * (n + 1) + n + 1) / 2};
    tr.insert(n + 1, rt);

    for ( int i = n; i; i --) {
        g[i] = g[i + 1];
        g[i] = max(g[i], tr.ask(i, rt) + (i * i - i) / 2 + sum[i - 1]);
        tr.l[i] = {-i, g[i] - sum[i - 1] + (i * i + i) / 2};
        tr.insert(i, rt);
    }

    solve(1, n);

    cin >> m;

    while (m --) {
        int i, x; cin >> i >> x;
        cout << max(f[i - 1] + g[i + 1], h[i] - x + a[i]) << '\n';
    }

    return 0;
}

Yet Another Partiton Problem

2d 问题。

暴力的 dp 是简单的,设 \(f_{i,l}\) 表示考虑到第 \(i\) 个数,已经分了 \(l\) 段的最小代价。

转移是 \(O(n^2k)\) 的,所以要优化转移的复杂度。

这个东西,显然是没有决策单调性的,数据结构啥的也优化不了,主要是最大值的贡献太难表示了。

遇到不会优化的序列题就分治一下,考虑 \([l,mid]\) 转移到 \((mid,r]\)

这里设 \(g_i\) 为上一轮的 dp 值,\(f_i\) 为此轮的。

\(mx1_i\)\((i,mid]\) 的最大值,\(mx2_i\)\((mid,i]\) 的最大值。

那么转移有 \(f_i\leftarrow g_j+(i-j)\times\max(mx1_j,mx2_i)\)

这里显然可以讨论 \(mx1_j\)\(mx2_i\) 的大小。

如果 \(mx1_j\le mx2_i\),那么就有 \(f_i\leftarrow g_j-j\times mx2_i+i\times mx2_i\)

这个东西可以直接上李超树去做,因为 \(mx1_j\)\(mx2_i\) 本来就有序,就可以像 CDQ 那样搞:对于 \(mx1_j \le mx2_i\)\(j\),把 \(y=-jx+g_j\) 这条直线插入李超,询问 \(x=mx2_i\) 处的最小值即可。

如果 \(mx1_j\gt mx2_i\),做法和上述同理。

代码
#include <bits/stdc++.h>

void Freopen() {
    freopen("", "r", stdin);
    freopen("", "w", stdout);
}

using namespace std;
const int N = 2e5 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;

int Mx;
int n, k;
int a[N];
int f[N], g[N];

struct line {
    int k, b;
} l[N];

int cal( int id, int x) {
    return l[id].k * x + l[id].b;
}

int tag[N * 10], ls[N * 10], rs[N * 10];
int rt, tot;

#define mid ((l + r) >> 1)

void insert( int & k, int l, int r, int u) {
    if (! k) return k = ++ tot, ls[k] = rs[k] = 0, tag[k] = u, void();
    
    int & v = tag[k];
    if (cal(v, mid) > cal(u, mid)) swap(v, u);
    if (cal(u, l) < cal(v, l)) insert(ls[k], l, mid, u);
    if (cal(u, r) < cal(v, r)) insert(rs[k], mid + 1, r, u);
}

int ask( int k, int l, int r, int x) {
    if (! k) return inf;

    return min(cal(tag[k], x), x <= mid ? ask(ls[k], l, mid, x) : ask(rs[k], mid + 1, r, x));
}

#undef mid

int mx1[N], mx2[N];

void CDQ( int l, int r) {
    if (l == r) return ;

    int mid = (l + r) >> 1;
    CDQ(l, mid), CDQ(mid + 1, r);

    rt = tot = 0;

    mx1[mid] = mx2[mid] = 0;
    for ( int i = mid - 1; i >= l; i --)
        mx1[i] = max(mx1[i + 1], a[i + 1]);

    for ( int i = mid + 1; i <= r; i ++)
        mx2[i] = max(mx2[i - 1], a[i]);

    int j = mid;
    for ( int i = mid + 1; i <= r; i ++) {
        while (mx1[j] <= mx2[i] && j >= l) {
            :: l[j] = {-j, g[j]};
            insert(rt, 1, Mx, j);
            j --;
        }

        f[i] = min(f[i], ask(rt, 1, Mx, mx2[i]) + i * mx2[i]);
    }

    rt = tot = 0;
    j = l;

    for ( int i = r; i > mid; i --) {
        while (mx1[j] > mx2[i] && j <= mid) {
            :: l[j] = {mx1[j], g[j] - j * mx1[j]};
            insert(rt, 1, n, j);
            j ++;
        }

        f[i] = min(f[i], ask(rt, 1, n, i));
    }
}

signed main() {
    ios :: sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    cin >> n >> k;

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

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

    k -= 1;

    while (k --) {
        for ( int i = 1; i <= n; i ++) g[i] = f[i];
        for ( int i = 1; i <= n; i ++) f[i] = inf;
        CDQ(1, n);
    }

    cout << f[n] << '\n';

    return 0;
}

赌局

没有原题。

可以考虑背包去做。

\(A_i\) 表示 \(A\) 赢的时候亏损 \(i\) 元时,\(A\) 输了可以获得的最大价值,\(B_i\)\(C_i\) 同理。

转移是 \(O(n^2V)\) 的。

那么答案就是 \(\min(B_j+C_k-i,A_i+C_k-j,A_i+B_j-k)\)

发现求答案的复杂度是 \(O(n^3V^3)\) 的,有点炸。

考虑把答案形式化一下,变成 \(A_i+B_j+C_k-\max(i+A_i,j+B_j,k+C_k)\),这就很典了。

考虑钦定 \(\max(i+A_i,j+B_j,k+C_k)\) 是哪一个,然后用树状数组维护一个前缀最大值即可。

复杂度 \(O(n^2V+nV\log{nV})\)

代码
#include <bits/stdc++.h>

void Freopen() {
    freopen("", "r", stdin);
    freopen("", "w", stdout);
}

using namespace std;
const int N = 5e5 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;

int n;
int A[N], B[N], C[N];

struct bit {
    int tr[N];

    void init() {
        for ( int u = 1; u < N; u ++) tr[u] = -inf;
    }

    void add( int u, int v) {
        for (; u < N; u += (u & -u)) tr[u] = max(tr[u], v);
    }

    int ask( int u, int res = -inf) {
        for (; u; u -= (u & -u)) res = max(res, tr[u]);
        return res;
    }
} ta, tb, tc;

signed main() {
    ios :: sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    memset(A, 128, sizeof A);
    memset(B, 128, sizeof B);
    memset(C, 128, sizeof C);

    A[0] = B[0] = C[0] = 0;
    ta.init(), tb.init(), tc.init();

    cin >> n;
    for ( int i = 1; i <= n; i ++) {
        int t, a, b; cin >> t >> a >> b;

        for ( int j = n * 500; j >= 0; j --)
            if (j - a >= 0) {
                if (t == 1) A[j] = max(A[j], A[j - a] + b);
                else if (t == 2) B[j] = max(B[j], B[j - a] + b);
                else C[j] = max(C[j], C[j - a] + b);
            }
    }

    int ans = 0;

    for ( int i = 0; i <= n * 500; i ++) {
        if (A[i] > -inf) ta.add(i + A[i] + 1, A[i]);
        if (B[i] > -inf) tb.add(i + B[i] + 1, B[i]);
        if (C[i] > -inf) tc.add(i + C[i] + 1, C[i]);
    }

    for ( int i = 0; i <= n * 500; i ++) {
        if (A[i] > -inf) ans = max(ans, tb.ask(i + A[i] + 1) + tc.ask(i + A[i] + 1) - i);
        if (B[i] > -inf) ans = max(ans, ta.ask(i + B[i] + 1) + tc.ask(i + B[i] + 1) - i);
        if (C[i] > -inf) ans = max(ans, ta.ask(i + C[i] + 1) + tb.ask(i + C[i] + 1) - i);
    }

    cout << ans << '\n';

    return 0;
}
posted @ 2025-09-12 20:36  咚咚的锵  阅读(5)  评论(0)    收藏  举报