Loading

20230420 训练记录:dp

Digit Sum

\(1 - k\) 中有多少数是 \(d\) 的倍数。

\(k \leq 10^{10\,000}; d \leq 100\)

数位 dp,\(dp_{i, s, f}\) 表示 \(s \equiv x_{i - 1}x_{i - 2}\cdots x_2x_1x_0 \pmod d\)\(f = [x_{i - 1}x_{i - 2}\cdots x_2x_1x_0 \gt k_{i - 1}k_{i - 2}\cdots k_2k_1k_0]\)。则有(枚举新的位为 \(d'\)):

\[dp_{i, s, f} \rightarrow dp_{i + 1, (s + d') \bmod d, f'} \]

\(f' = [d' > k[i] \lor (d' = k[i] \land f = 1)]\)

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

using ll = long long;

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

    std::string s;
    int d;
    std::cin >> s >> d;

    int n = s.size();
    std::vector k(n, 0);
    for (int i = 0; i < n; i++) {
        k[i] = s[n - i - 1] - '0';
    }

    const int mod = 1000000007;

    std::vector f(n + 1, std::vector(d, std::array<int, 2>{}));
    f[0][0][0] = 1;

    for (int i = 0; i < n; i++) {
        for (int s = 0; s < d; s++) {
            for (int _f : {0, 1}) {
                for (int _d = 0; _d < 10; _d++) {
                    int new_s = (s + _d) % d;
                    int new_f = _d > k[i] || (_d == k[i] && _f == 1);
                    f[i + 1][new_s][new_f] = ((ll) f[i + 1][new_s][new_f] + f[i][s][_f]) % mod;
                }
            }
        }
    }

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

Permutation

Grouping

将物品分组,如果 \(i, j\) 在一组将会得到 \(a_{i, j}\) 的分。问所有分组情况下最大得分。

\(n \leq 16; |a_{i, j}| \leq 10^9\)

分成若干组包含一个子问题:分成两组。

状压,\(f_i\) 表示其中一组的分组情况为 \(i\) 的情况下的最大得分。则按照上面的划分,只需考虑其被分为 \(f_j, f_{i \backslash j}, \,(i \in j)\) 的方案。即:

\[f_i = \max\{f_j + f_{i \backslash j}\} \]

💡 如何枚举非空子集
for (int j = i; j; --j &= i) { }

当然,本题需要非空、真子集,因此初始 int j = i & (i - 1) (抹去最后一个 \(0\))。

复杂度为 \(\mathcal O(n^2 2^n + 3^n)\)

依二项式定理,\(\displaystyle\sum\limits_{k = 0}^{n} {n \choose k} 2^k = \sum_{k=0}^n { n \choose k } (1)^{n-k} (2)^k = (1 + 2) ^ n\) [1]

sh %%%
展开代码
#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(n, 0));
    for (auto &i : g) for (auto &j : i) std::cin >> j;

    std::vector f(1 << n, -1LL);
    f[0] = 0;
    std::cout << [&, dp{[&](auto &&self, int i) -> ll {
        if (~f[i]) return f[i];
        f[i] = 0;
        for (int j = 0; j < n; j++) if (i >> j & 1) {
            for (int k = j + 1; k < n; k++) if (i >> k & 1) {
                f[i] += g[j][k];
            }
        }
        for (int j = i & (i - 1); j; --j &= i) {
            f[i] = std::max(f[i], self(self, j) + self(self, i ^ j));
        }
        return f[i];
    }}]{
        return dp(dp, (1 << n) - 1);
    }();

    return 0;
}

Subtree

将树的节点染成黑白,问 \(i\; (1 \leq i \leq n)\) 在树上唯一黑色连通块的方案数。

\(n \leq 10^5\),不保证模数为素数。

容易处理出以 \(u\) 为根的答案 \(f_u = \prod\limits_{v \in u} (\underline{f_v}_{black} + \underline{1}_{white})\),考虑换根求其他点答案:

实际上 \(f_u\)\(u\) 以下的答案,而另一部分当然是 \(u\) 以上的答案,即其兄弟的答案 \(\dfrac{f_p}{f_u + 1}\),其中 \(u \in p\)。然而不保证模数的素性就不一定有逆元,因此考虑维护 \(u\) 的兄弟节点 \(s\)\(\{f_s + 1\}\) 前缀乘积、后缀乘积。将上下两部分乘起来即可。

展开代码

可惜没有 \(n = m = 1\) 的数据。

#include <bits/stdc++.h>

using ll = long long;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    int n, m;
    std::cin >> n >> m;
    
    if (n == 1) {
        std::cout << 1 % m << '\n';
        std::exit(0);
    }
    
    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);
    }
    
    std::vector f(n, 1);
    std::vector prefix{g}, suffix{g};
    [&, dfs{[&](auto &&self, int u, int p) -> void {
        for (int v : g[u]) if (v != p) {
            self(self, v, u);
            f[u] = (ll) f[u] * (1 + f[v]) % m;
        }

        std::fill(prefix[u].begin(), prefix[u].end(), 1);
        for (int i = 1; i < (int) g[u].size(); i++) {
            prefix[u][i] = prefix[u][i - 1];
            if (int v = g[u][i - 1]; v != p) {
                prefix[u][i] = (ll) prefix[u][i] * (f[v] + 1) % m;
            }
        }

        std::fill(suffix[u].begin(), suffix[u].end(), 1);
        for (int i = (int) g[u].size() - 2; ~i; i--) {
            suffix[u][i] = suffix[u][i + 1];
            if (int v = g[u][i + 1]; v != p) {
                suffix[u][i] = (ll) suffix[u][i] * (f[v] + 1) % m;
            }
        }
    }}]{
        dfs(dfs, 0, -1);
    }();
    
    std::vector h(n, 1);
    [&, dfs{[&](auto &&self, int u, int p, int pid) -> void {
        if (~p) {
            h[u] = ((ll) h[u] + (ll) h[p] * prefix[p][pid] % m * suffix[p][pid] % m) % m;
        }
        for (int i = 0; i < (int) g[u].size(); i++) if (int v = g[u][i]; v != p) {
            self(self, v, u, i);
        }
    }}]{
        dfs(dfs, 0, -1, -1);
    }();
    
    for (int i = 0; i < n; i++) {
        std::cout << (ll) f[i] * h[i] % m << '\n';
    }
    
    return 0;
}

Intervals

Tower

Grid 2

Grid 1,但给出所有障碍点的坐标。

障碍点数 \(n \leq 10^3; h, w \leq 10^5\)

很容易计算出没有任何障碍点的答案,即从 \((1, 1)\)\((n, m)\) 的答案实际上是二项式系数 \(\displaystyle{n - 1 + m - 1 \choose n - 1}\),实现上 \(0\)-indexed 更容易。

只需要将障碍部分都减去。不用担心减重了,由容斥原理可知许多项实际上被减去、抵消、减去、抵消…… 即用 \(f_i\) 表示考虑走过前 \(i\) 个障碍的方案数,为了方便可以再加一个障碍 \((h - 1, w - 1)\),这样 \(f_{n + 1}\) 即为所求。对于每个 dp 值,都减去其前面的障碍点贡献:

\[f_i = {x_i + y_i \choose x_i} - \sum_{j < i} [x_j \leq x_i \land y_j \leq y_i] \times f_j \times {(x_i - x_j) + (y_i - y_j) \choose x_i - x_j} \]

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

using ll = long long;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);
    
    int h, w, n;
    std::cin >> h >> w >> n;
    
    std::vector p(n, std::pair{0, 0});

    for (auto &[x, y] : p) {
        std::cin >> x >> y;
        x -= 1, y -= 1;
    }
    
    p.emplace_back(h - 1, w - 1);
    
    const int N = 200001;
    const int mod = 1000000007;
    std::vector<int> fac(N, 0), inv(N, 0), ifc(N, 0);
    fac[0] = fac[1] = inv[0] = inv[1] = ifc[0] = ifc[1] = 1;
    
    for (int i = 2; i < N; i++) {
        fac[i] = (ll) fac[i - 1] * i % mod;
        inv[i] = ((ll) mod - mod / i) % mod * inv[mod % i] % mod;
        ifc[i] = (ll) ifc[i - 1] * inv[i] % mod;
    }
    
    auto binom = [&](int n, int m) -> ll {
        if (n < m || m < 0) return 0ll;
        return (ll) fac[n] * ifc[m] % mod * ifc[n - m] % mod;
    };
    
    std::vector f(n + 1, 0);
    std::sort(p.begin(), p.end());
    
    for (int i = 0; i < n + 1; i++) {
        auto [x, y] = p[i];
        f[i] = binom(x + y, x);
        for (int j = 0; j < i; j++) {
            auto [nx, ny] = p[j];
            if (x >= nx && y >= ny) {
                f[i] = ((ll) f[i] - (ll) f[j] * binom(x - nx + y - ny, x - nx) % mod + mod) % mod;
            }
        }
    }
    
    std::cout << f[n] << '\n';
    return 0;
}

Frog 3


  1. https://math.stackexchange.com/questions/525266/prove-sum-binomnk2k-3n-using-the-binomial-theorem ↩︎

posted @ 2023-04-20 02:23  PatrickyTau  阅读(20)  评论(0编辑  收藏  举报