Loading

20230419 训练记录:dp

Deque

Alice 和 Bob 玩游戏,轮流从 deque 的头/尾取走一个元素,Alice 拿走的数和为 \(X\),Bob 拿走的数和为 \(Y\)。Alice 想最大化 \(X - Y\) 而 Bob 想最小化 \(X - Y\)。问双方都采取最优策略情况下的 \(X - Y\) 为多少。

\(n \leq 3000; a_i \leq 10^9\)

\(f_{l, r, 0 / 1}\) 表示 Bob / Alice 先手情况下,在区间 \([l, r]\) 的最佳答案,所求即 \(f_{0, n - 1, 1}\)。有:

\[\begin{aligned} f_{l, r, 0} &= \min(f_{l + 1, r, 1} - a_l, f_{l, r - 1, 1} - a_r)\\ f_{l, r, 1} &= \max(f_{l + 1, r, 0} + a_l, f_{l, r - 1, 0} + a_r)\\ \end{aligned} \]

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

using ll = long long;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);

    int n;
    std::cin >> n;
    std::vector<int> a(n);
    for (int &i : a) std::cin >> i;

    const ll inf = 1e18;
    std::vector f(n, std::vector(n, std::array<ll, 2>{inf, inf}));

    std::cout << [&, dp{[&](auto &&self, int l, int r, bool alice) -> ll {
        if (f[l][r][alice] != inf) return f[l][r][alice];

        ll ans = 0;
        if (l == r) ans = a[l] * (alice ? 1 : -1);
        else if (alice) ans = std::max(self(self, l + 1, r, 0) + a[l], self(self, l, r - 1, 0) + a[r]);
        else ans = std::min(self(self, l + 1, r, 1) - a[l], self(self, l, r - 1, 1) - a[r]);

        return f[l][r][alice] = ans;
    }}]{
        return dp(dp, 0, n - 1, 1);
    }();

    return 0;
}

Candies

\(k\) 拆成 \(n\) 个整数的方案数(整数划分数),但是 \(0 \leq x_i \leq a_i\)

\(n \leq 100; k \leq 10^5\)

整数划分数

整数划分数其实就是完全背包,有:

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

带上限制,用 \(f_{i, j}\) 表示使用前 \(i\) 个满足题意的数组成的和为 \(j\) 的方案数,所求即 \(f_{n, k}\)。有:

\[\begin{aligned} f_{i, j} = \sum\limits_{k = 0}^{\min(a_i, j)} f_{i - 1, j - k} = \sum\limits_{k = \max(0, j - a_i)}^{j} f_{i - 1, k} \end{aligned} \]

右侧为一段区间和,考虑使用前缀和优化。

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

using ll = long long;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    int n, k;
    std::cin >> n >> k;
    
    std::vector<int> a(n);
    for (int &i : a) std::cin >> i;
        
    std::vector f(n + 1, std::vector(k + 1, 0LL)), s{f};
    
    const int mod = 1000000007;
    
    f[0][0] = s[0][0] = 1;
    for (int i = 1; i <= k; i++) {
        s[0][i] = (s[0][i - 1] + f[0][i]) % mod;
    }

    for (int i = 1; i <= n; i++) {
        f[i][0] = s[i][0] = 1;
        for (int j = 1; j <= k; j++) {
            if (int ai = a[i - 1]; j <= ai) {
                f[i][j] = s[i - 1][j];
            } else {
                f[i][j] = ((s[i - 1][j] - s[i - 1][(j - ai) - 1]) % mod + mod) % mod;
            }
            s[i][j] = (s[i][j - 1] + f[i][j]) % mod;
        }
    }

    std::cout << f[n][k] << '\n';

    return 0;
}

Slimes

同合并石子,区间 dp 模板题。满足四边形不等式。

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

int read() {
    int x = 0, f = 1, c = getchar();
    for (; !isdigit(c); c = getchar()) if (c == '-') f = -1;
    for (; isdigit(c); x = x * 10 + c - '0', c = getchar());
    return x * f;
}

const int N = 410;
using ll = long long;

const ll inf = 1E18;
int n, a[N], s[N][N];
ll f[N][N], sum[N];

#define w(i, j) sum[j] - sum[i - 1]

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

    for (int i = 1; i <= n; i++) {
        sum[i] = sum[i - 1] + a[i];
        s[i][i] = i;
    }

    for (int l = 2; l <= n; l++) {
        for (int i = 1, j = l; j <= n; i++, j++) {
            f[i][j] = inf;
            for (int k = s[i][j - 1]; k <= s[i + 1][j]; k++) {
                if (f[i][j] > f[i][k] + f[k + 1][j] + w(i, j)) {
                    f[i][j] = f[i][k] + f[k + 1][j] + w(i, j);
                    s[i][j] = k;
                }
            }
        }
    }

    printf("%lld\n", f[1][n]);

    return 0;
}

Matching

\(n\)\(n\) 女,不同性别之间配对,如果 \(a_{i, j} = 1\) 表示 \(i\)\(j\) 合适。只能一男一女,问多少合法方案数。

\(n \leq 21\)

\(n\) 很小,考虑枚举子集。\(f_{i, s}\) 表示 \([1, i]\) 编号的男女,配对情况为 \(s\) 的方案数。则有:

\[f_{i + 1, s \lor j} := f_{i + 1, s \lor j} + f_{i, s} \]

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

using ll = long long;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    int n;
    std::cin >> n;
    
    std::vector a(n, std::vector(n, 0));
    for (auto &i : a) for (auto &j : i) std::cin >> j;
    
    std::vector f(n + 1, std::vector(1 << n, 0LL));
    f[0][0] = 1;
    
    const int mod = 1000000007;
    
    for (int s = 0; s < 1 << n; s++) {
        for (int i = 0; i < n; i++) if (f[i][s]) {
            for (int j = 0; j < n; j++) if ((~s >> j & 1) && a[i][j]) {
                (f[i + 1][s | (1 << j)] += f[i][s]) %= mod;
            }
        }
    }
    
    std::cout << f.back().back() << '\n';
    
    return 0;
}

Indepedent Set

将树的所有节点染成黑白两色,求相邻节点不同时为黑的方案数。

\(n \leq 10^5\)

乘法原理,一路乘上去,如果当前节点是白色就两种加起来,否则为黑色就乘以儿子涂成白色的方案数。

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

using ll = long long;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    int n;
    std::cin >> n;
    
    std::vector g(n, std::vector(0, 0));
    for (int i = 1; i < n; i++) {
        int u, v;
        std::cin >> u >> v;
        u -= 1, v -= 1;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    const int mod = 1000000007;
    std::vector f(n, std::array<int, 2>{});

    [&, dfs{[&](auto &&self, int u, int p) -> void {
        f[u][0] = f[u][1] = 1;
        for (auto v : g[u]) if (v != p) {
            self(self, v, u);
            f[u][0] = (ll) f[u][0] * ((ll) f[v][0] + f[v][1]) % mod;
            f[u][1] = (ll) f[u][1] * f[v][0] % mod;
        }
    }}]{
        dfs(dfs, 0, -1);
    }();

    std::cout << ((ll)f[0][0] + f[0][1]) % mod << '\n';

    return 0;
}

Flowers

给定 \(\{h\}_n, \{a\}_n\),求最长子序列 \(\{p\}_k\) 使得 \(h_{p_i}\) 上升的最大的 \(\sum \limits_{i = 1} ^ k a_{p_i}\)

\(h_i \leq n \leq 2 \times 10^5; a_i \leq 10^9\)

\(f_{i}\) 表示考虑到第 \(i\) 个元素结尾的最长上升子序列对应的最大和,则:

\[f_{i} = \max_{h_j \lt h_i} \{ f_j \} + a_i \]

查询前缀最大值,此事树状数组能胜任。注意到 \(h_i\) 很小,可以用下标表示 \(h_i\),值表示 dp 的值。

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

using ll = long long;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    int n;
    std::cin >> n;
    
    std::vector h(n, 0), a(n, 0);
    std::vector p(n + 1, 0LL);
    for (auto &i : h) std::cin >> i;
    for (auto &i : a) std::cin >> i;
    
    ll f = 0, ans = 0;
    for (int i = 0; i < n; i++) {
        ll res = 0;
        for (int x = h[i] - 1; x; x -= x & -x)
            res = std::max(ans, p[x]);
        f = res + a[i];
        for (int x = h[i]; x <= n; x += x & -x)
            p[x] = std::max(p[x], f);
        ans = std::max(ans, f);
    }
    
    std::cout << ans << '\n';
    
    return 0;
}

Walk

求长度为 \(k\) 的路径条数。

邻接矩阵 \(n \leq 50; k \leq 10^{18}\)

熟知 Floyd 算法的 \(f_{i, j}\) 循环 \(k\) 次表示的是走 \(k\) 步的最短路径(这也是矩阵乘法的本质之一)。又因为 \(f_k = f_1^k\) 所以这是一道矩乘快速幂模板题。

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

using ll = long long;

const int N = 50;
const int mod = 1000000007;

using matrix = std::array<std::array<int, N>, N>;

int n;

matrix operator *(const matrix &lhs, const matrix &rhs) {
    matrix res{};
    for (int i = 0; i < n; i++) {
        for (int k = 0; k < n; k++) {
            for (int j = 0; j < n; j++) {
                res[i][j] = ((ll) res[i][j] + (ll) lhs[i][k] * rhs[k][j] % mod) % mod;
            }
        }
    }
    return res;
}

matrix power(matrix x, ll k) {
    matrix res{};
    for (int i = 0; i < n; i++) res[i][i] = 1;
    for (; k; k /= 2, x = x * x)
        if (k & 1) res = res * x;
    return res;
}

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    ll k;
    std::cin >> n >> k;
    
    matrix x{};
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            std::cin >> x[i][j];
        }
    }

    matrix ans = power(x, k);
    
    int res{};
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            res = ((ll) res + ans[i][j]) % mod;
        }
    }

    std::cout << res << '\n';
    
    return 0;
}
🙁 今日放下狠话,天梯赛打崩了就放弃这个我坚持了两年多的算法竞赛。说句心里话还是不太舍得,我幻想过太多次能够在大学里有一个支撑我走下去的理由,算法竞赛真是太合适了,可惜我削平了脚也穿不上这水晶鞋。我感到悲伤,但也十分释怀,一直以来我都只在乎此事,连恋爱、学业和工作都没有时间好好打理和准备...... 不过要是能坚持下去也会是一段十分难得的回忆呢!现阶段还是比较希望自己能够全力以赴吧!
posted @ 2023-04-20 02:19  PatrickyTau  阅读(22)  评论(0编辑  收藏  举报