DP 与计数

NFLSOJ

A

CF294C Shaass and Light
考虑初始已点亮的灯将所有剩下的灯划分成的连续段。除开头结尾外,每个长为 \(l\) 的连续段有 \(2 ^ l\) 种操作序列。开头结尾的连续段只有 \(1\) 种操作序列。从前往后将所有的操作序列归并到全局的操作序列里,拿组合数随便乱搞搞就好了。

代码
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
const int P = 1e9 + 7;
int n, m;
int a[1005];
inline int qpow(int x, int y) {
    if (y <= 0) 
        return 1;
    int ret = 1;
    while (y) {
        if (y & 1) 
            ret = ret * x % P;
        y >>= 1;
        x = x * x % P;
    }
    return ret;
}
int inv[1005], ifac[1005], fac[1005];
inline int C(int n, int m) { return (n < m ? 0 : fac[n] * ifac[m] % P * ifac[n - m] % P); }
signed main() {
    cin >> n >> m;
    inv[0] = ifac[0] = fac[0] = inv[1] = ifac[1] = fac[1] = 1;
    for (int i = 2; i <= n; i++) {
        inv[i] = (P - P / i) * inv[P % i] % P;
        fac[i] = fac[i - 1] * i % P;
        ifac[i] = ifac[i - 1] * inv[i] % P;
    }
    for (int i = 1; i <= m; i++) cin >> a[i];
    sort(a + 1, a + m + 1);
    int res = n - m;
    int ans = C(res, a[1] - 1);
    res -= (a[1] - 1);
    ans = ans * C(res, n - a[m]) % P;
    res -= (n - a[m]);
    for (int i = 2; i <= m; i++) {
        ans = ans * qpow(2, a[i] - a[i - 1] - 2) % P;
        ans = ans * C(res, a[i] - a[i - 1] - 1) % P;
        res -= (a[i] - a[i - 1] - 1);
    }
    cout << ans;
    return 0;
}

B

CF1753C Wish I Knew How to Sort
发现最终序列一定是前面全是 \(0\),后面全是 \(1\)。设有 \(k\)\(0\),则我们判断是否排好序的标准就是前 \(k\) 个数里是否有 \(k\)\(0\)。又发现操作后任意一段前缀里 \(0\) 的个数是单调不降的。于是可以设计出 dp 状态:\(dp[i]\) 表示前 \(k\) 个数里有 \(i\)\(0\) 时的期望步数。最终答案即为 \(dp[k]\)。转移考虑期望经过多少步可以使得前 \(k\) 个数里的 \(0\) 增加。设当前前 \(k\) 个里共有 \(i\)\(0\),则要增加就必须选到前面的 \(1\) 和后面的 \(0\)。可以发现前面 \(1\) 的个数和后面 \(0\) 的个数都是 \(k - i\) 个。而总的选择方案共有 \(\binom{n}{2}\) 种。所以此时一次操作给前 \(k\) 个增加一个 \(0\) 的概率即为 \(\frac{(k - i) ^ 2}{\binom{n}{2}}\),期望次数即为 \(\frac{\binom{n}{2}}{(k - i) ^ 2}\)。于是有 \(dp[i + 1] = dp[i] + \frac{\binom{n}{2}}{(k - i) ^ 2}\)。直接算即可。

代码
#include <iostream>
#define int long long
using namespace std;
const int P = 998244353;
inline int qpow(int x, int y) {
    int ret = 1;
    while (y) {
        if (y & 1) 
            ret = ret * x % P;
        y >>= 1;
        x = x * x % P;
    }
    return ret;
}
int dp[200005];
int a[200005];
signed main() {
    int tc;
    cin >> tc;
    while (tc--) {
        int n, k = 0;
        cin >> n;
        for (int i = 1; i <= n; i++) cin >> a[i], a[i] ^= 1, k += a[i];
        for (int i = 1; i <= n; i++) a[i] += a[i - 1];
        dp[a[k]] = 0;
        int S = n * (n - 1) / 2;
        for (int i = a[k] + 1; i <= k; i++) dp[i] = (dp[i - 1] + S % P * qpow((k - i + 1) * (k - i + 1), P - 2) % P) % P;
        cout << dp[k] << "\n";
    }
    return 0;
}

C

CF1657E Star MST
题目限制等价于以 \(1\) 为中心的菊花是图的一棵 MST。也就是每个点到 \(1\) 的边权必须小于等于到其他点的边权,不然换成那条更小的边肯定可以使得 MST 的权值变小。于是可以考虑 dp。我们从小到大给每条与 \(1\) 相连的点赋权,定义 \(dp[i][j]:\) 已经给 \(i\) 个点的与 \(1\) 相连的边赋了权,最后一次赋权赋的是 \(j\) 的方案数。考虑转移。对于一个 \(dp[i][j]\),我们枚举给几个点(边)赋上 \(j\) 的权,记为 \(m\)。然后再枚举上次赋的是什么权,记为 \(x\)。再记 \(t = i - m\),则 \(dp[i][j]\) 可以从 \(dp[t][x]\) 转移。考虑转移系数。显然我们要先在剩下的 \(n - t\) 个点里选出 \(i - t\) 个赋权。然后要在这选出的 \(i - t\) 个点里相互连边,还要向前面的 \(t - 1\) 个点连边(减 \(1\) 是因为不用考虑向 \(1\) 连)。新连的边的权值必然是要大于等于 \(j\) 的(因为其有一个端点向 \(1\) 连了权为 \(j\) 的边),于是每条边有 \(k - j + 1\) 种选权值的方案。总共是 \(\binom{n - t}{i - t}(k - j + 1)^{(\frac{(i - t)(i - t - 1)}{2} + (i - t)(t - 1))}\),这就是转移系数。上面那个指数可以化简变成 \(\frac{(i - t)(i + t - 3)}{2}\),于是转移:\(dp[i][j] = \sum\limits_{t = 1}^{i - 1}(\sum\limits_{x = 0}^{k})\binom{n - t}{i - t}(k - j + 1)^{\frac{(i - t)(i + t - 3)}{2}}\)。使用前缀和优化即可做到 \(O(n^3)\)

代码
#include <iostream>
#define int long long
using namespace std;
const int P = 998244353;
int f[255][255];
int g[255][255];
int inv[255], ifac[255], fac[255];
inline void Cpre(int n) {
    inv[0] = ifac[0] = fac[0] = inv[1] = ifac[1] = fac[1] = 1;
    for (int i = 2; i <= n; i++) {
        inv[i] = (P - P / i) * inv[P % i] % P;
        fac[i] = fac[i - 1] * i % P;
        ifac[i] = ifac[i - 1] * inv[i] % P;
    }
}
inline int C(int n, int m) { return n < m || n < 0 || m < 0 ? 0 : fac[n] * ifac[m] % P * ifac[n - m] % P; }
inline int qpow(int x, int y) {
    int ret = 1;
    while (y) {
        if (y & 1) 
            ret = ret * x % P;
        y >>= 1;
        x = x * x % P;
    }
    return ret;
}
signed main() {
    int n, K;
    cin >> n >> K;
    Cpre(max(n, K));
    f[1][0] = 1;
    for (int i = 0; i <= K; i++) g[1][i] = 1;
    for (int i = 2; i <= n; i++) {
        for (int j = 1; j <= K; j++) {
            for (int t = 1; t < i; t++) 
                f[i][j] = (f[i][j] + g[t][j - 1] * C(n - t, i - t) % P * qpow(K - j + 1, (i - t) * (i + t - 3) / 2) % P) % P;
            g[i][j] = (g[i][j - 1] + f[i][j]) % P;
        }
    }
    cout << g[n][K];
    return 0;
}

D

CF660E Different Subsets For All Tuples
首先,空子序列共有 \(m ^ n\) 个贡献。
其次,考虑对每种子序列计算贡献。发现长度相同的子序列的贡献是相同的,于是只需要枚举子序列的长度 \(i\)。为了不算重,我们钦定一个子序列只在它第一次出现的位置被计算。设这个子序列出现的位置为 \(pos_1, pos_2, pos_3\),值为 \(val_1, val_2, val_3\),则 \(1 \cdots pos_1 - 1\) 之中不能出现 \(val_1\), \(pos_1 + 1 \cdots pos_2 - 1\) 中不能出现 \(val_2\),以此类推。而最后出现的位置之后就没有限制了。所以我们再枚举这个子序列最后出现的位置 \(j\),然后就在 \(j\) 前面的位置里放 \(i - 1\) 个元素。\(j\) 前面别的位置每个有 \(m - 1\) 种放法,之后每个位置有 \(m\) 种。于是答案即为 \(\sum\limits_{i - 1}^n m^i\sum\limits_{j = 1}^n\binom{j - 1}{i - 1}(m - 1)^{j - i}m^{n - j}\)。接下来开始化简。
\(\begin {equation} \begin {split} \sum\limits_{i - 1}^n m^i\sum\limits_{j = 1}^n\binom{j - 1}{i - 1}(m - 1)^{j - i}m^{n - j} &= \sum_{i = 1}^n \sum_{j = i}^n \binom{j - 1}{i - 1}(m - 1)^{j - i}m^{n - j + i} \\ (调换循环顺序) &= \sum_{j = 1}^n\sum_{i - 1}^j \binom{j - 1}{i - 1}(m - 1)^{j - i}m^{n - j + i} \\ (减循环变量)&= \sum_{j = 0}^{n - 1}\sum_{i = 0}^j \binom{j}{i}(m - 1)^{j - i}m^{n - j + i} \\ (提出 m^{n - j}) &= \sum_{j = 0}^{n - 1}m^{n - j}\sum_{i = 0}^j \binom{j}{i}(m - 1)^{j - i}m^i \\ (二项式定理!)&= \sum_{j = 0}^{n - 1}m^{n - j}(2m - 1)^j. \end {split} \end {equation}\)
于是就可以一遍循环过去求了。

代码
#include <iostream>
#define int long long
using namespace std;
const int P = 1e9 + 7;
int qpow(int x, int y) {
    int ret = 1;
    while (y) {
        if (y & 1) 
            ret = ret * x % P;
        y >>= 1;
        x = x * x % P;
    }
    return ret;
}
signed main() {
    int n, m;
    cin >> n >> m;
    int ans = qpow(m, n);
    for (int i = 0; i < n; i++) ans = (ans + qpow(m, n - i) * qpow(2 * m - 1, i) % P) % P;
    cout << ans;
    return 0;
}

E

CF785D Anton and School - 2
考虑在每个合法子序列的最后一个前括号处计算贡献。设这个点前面(含这个点)有 \(a\) 个前括号,后面有 \(b\) 个后括号,我们钦定必须选这个前括号,则方案数为 \(\sum\limits_{i = 0}^{\min\{a - 1, b - 1\}} \binom{a - 1}{i} \binom{b}{i + 1}\)。这可以直接写成 \(\sum\limits_{i = 0}^{a}\binom{a - 1}{a - i - 1} \binom{b}{i + 1}\),因为这里上界变化不会引起答案变化。然后就可以直接使用范德蒙德卷积写成 \(\binom{a + b - 1}{a}\) 了。

代码
#include <iostream>
#define int long long
using namespace std;
const int P = 1e9 + 7;
int inv[200005], ifac[200005], fac[200005];
int a[200005];
int b[200005];
void Cpre(int n) {
    inv[0] = ifac[0] = fac[0] = inv[1] = ifac[1] = fac[1] = 1;
    for (int i = 2; i <= n; i++) {
        inv[i] = (P - P / i) * inv[P % i] % P;
        fac[i] = fac[i - 1] * i % P;
        ifac[i] = ifac[i - 1] * inv[i] % P;
    }
}
inline int C(int n, int m) { return fac[n] * ifac[m] % P * ifac[n - m]; }
signed main() {
    string str;
    cin >> str;
    int n = str.size();
    Cpre(n);
    str = ' ' + str;
    for (int i = 1; i <= n; i++) a[i] = a[i - 1] + (str[i] == '(');
    for (int i = n; i; i--) b[i] = b[i + 1] + (str[i] == ')');
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        if (str[i] == '(') 
            ans = (ans + C(a[i] + b[i] - 1, a[i])) % P;
    }
    cout << ans << "\n";
    return 0;
}

F

CF1540B Tree Array

我们考虑枚举从哪个点开始扩展。然后考虑对于每一对数算出其成为逆序对的概率,加起来搞一搞即可。以当前开始扩展的点为根,则对于两个点,若其中一个是另一个的祖先,则这两个点的扩展顺序就已经被确定了。否则考虑它们的 LCA。在它们的 LCA 被扩展到之前,所有的扩展对这两个点都没有影响。在扩展到它们的 LCA 之后,每一步相当于以 \(p\) 的概率向 \(x\) 走一步,以 \(p\) 的概率向 \(y\) 走一步,另外的概率啥也不做,要求先到达 \(x\) 的概率。发现实际上是每次等概率地向两边之一走一步。于是可以 dp,设 \(dp[i][j]\) 表示当前离 \(x\) 距离为 \(i\),离 \(y\) 距离为 \(j\) 时,先走到 \(x\) 的概率。有转移 \(dp[i][j] = \frac{dp[i - 1][j] + dp[i][j - 1]}{2}\)。初态:\(dp[0][1 \thicksim n] = 1\)\(dp[1 \thicksim n][0] = 0\)。于是就没了。

代码
#include <iostream>
#include <string.h>
#define int long long
using namespace std;
const int P = 1e9 + 7;
int n;
int head[205], nxt[405], to[405], ecnt;
void add(int u, int v) { to[++ecnt] = v, nxt[ecnt] = head[u], head[u] = ecnt; }
int dp[205][205];
inline int qpow(int x, int y) {
    int ret = 1;
    while (y) {
        if (y & 1) 
            ret = ret * x % P;
        y >>= 1;
        x = x * x % P;
    }
    return ret;
}
int son[205], dep[205], top[205], sz[205], f[205];
void dfs1(int x, int fa, int d) {
    dep[x] = d;
    f[x] = fa;
    sz[x] = 1;
    for (int i = head[x]; i; i = nxt[i]) {
        int v = to[i];
        if (v != fa) {
            dfs1(v, x, d + 1);
            sz[x] += sz[v];
            if (sz[v] > sz[son[x]]) 
                son[x] = v;
        }
    }
}
void dfs2(int x, int t) {
    top[x] = t;
    if (!son[x]) 
        return;
    dfs2(son[x], t);
    for (int i = head[x]; i; i = nxt[i]) {
        int v = to[i];
        if (v != son[x] && v != f[x]) 
            dfs2(v, v);
    }
}
int LCA(int x, int y) {
    while (top[x] ^ top[y]) (dep[top[x]] < dep[top[y]]) ? (y = f[top[y]]) : (x = f[top[x]]);
    return (dep[x] < dep[y] ? x : y);
}
int ans;
void Solve(int x) {
    memset(son, 0, sizeof son);
    dfs1(x, 0, 1);
    dfs2(x, x);
    for (int i = 1; i <= n; i++) {
        for (int j = i + 1; j <= n; j++) {
            int t = LCA(i, j);
            ans += dp[dep[j] - dep[t]][dep[i] - dep[t]];
            ans -= (ans >= P ? P : 0);
        }
    }
}
signed main() {
    cin >> n;
    for (int i = 1, u, v; i < n; i++) {
        cin >> u >> v;
        add(u, v);
        add(v, u);
    }
    for (int i = 1; i <= n; i++) dp[i][0] = 0, dp[0][i] = 1;
    int inv2 = qpow(2, P - 2);
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) 
            dp[i][j] = (dp[i - 1][j] + dp[i][j - 1]) * inv2 % P;
    }
    for (int i = 1; i <= n; i++) Solve(i);
    cout << ans * qpow(n, P - 2) % P;
    return 0;
}

H

CF1437F Emotional Fishermen
考虑最终的排列一定是形如 \(\cdots p_1 \cdots p_2 \cdots p_3 \cdots\),其中 \(a[p_3] \ge 2a[p_2]\)\(a[p_2] \ge 2a[p_1]\),且两个相邻的 \(p\) 中间摆的数的两倍不能超过前一个 \(p\) 上摆的。我们称这样的 \(p\) 为关键点。我们考虑对关键点进行 dp。先将 \(a_i\) 排序,求出 \(k_i\) 表示有多少 \(2a_j \le a_i\)。定义 \(dp[i]\) 表示 \(i\) 为关键点且所有小于等于 \(\lfloor \frac{a[i]}{2} \rfloor\)\(a\) 都已经摆好的方案数。考虑枚举上一个关键点,称为 \(j\)。显然摆完 \(j\) 之后还剩 \(n - k_j - 1\) 个空位。在这些空位中,\(i\) 必然摆在第一个能摆的空位中,否则这个空位上的数一定不合法。除去 \(i\),还剩 \(n - k_j - 2\) 个空位。我们要在这些空位上摆 \(k_i - k_j - 1\) 个数,共有 \(A_{k_i - k_j - 1}^{n - k_j - 2}\) 种摆法。于是转移方程:\(dp[i] = \sum\limits_{2a_j \le a_i}A_{k_i - k_j - 1}^{n - k_j - 2}dp[j]\)。直接 \(n^2\) 暴力即可。

代码
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
const int P = 998244353;
int pos[5005];
int a[5005];
int dp[5005];
int inv[5005], ifac[5005], fac[5005];
void Cpre(int n) {
    inv[0] = ifac[0] = fac[0] = inv[1] = ifac[1] = fac[1] = 1;
    for (int i = 2; i <= n; i++) {
        inv[i] = (P - P / i) * inv[P % i] % P;
        fac[i] = fac[i - 1] * i % P;
        ifac[i] = ifac[i - 1] * inv[i] % P;
    }
}
inline int A(int n, int m) { return n < m || n < 0 || m < 0 ? 0 : fac[n] * ifac[n - m] % P; }
signed main() {
    int n;
    cin >> n;
    Cpre(n);
    for (int i = 1; i <= n; i++) cin >> a[i];
    sort(a + 1, a + n + 1);
    for (int i = 1; i <= n; i++) {
        pos[i] = 0;
        while (pos[i] <= i && a[pos[i]] * 2 <= a[i]) ++pos[i];
    }
    dp[0] = 1;
    for (int i = 1; i <= n; i++) {
        for (int j = 0; a[j] * 2 <= a[i]; j++) 
            dp[i] = (dp[i] + dp[j] * A(n - pos[j] - 1, pos[i] - pos[j] - 1) % P) % P;
    }
    cout << (a[n] < a[n - 1] * 2 ? 0 : dp[n]);
    return 0;
}

J

CF1626F A Random Code Problem
考虑对一次操作计算贡献。可以直接把每个数求和除以 \(n\) 算出一次操作的贡献。但是这题里并不能知道每次操作之后所有数的和。发现 \(k\) 十分小,于是可以直接求出 \(1 \sim k - 1\) 的 lcm,记为 \(P\)。然后将一个数拆成两部分:\(a = P \times \lfloor \frac{a}{P} \rfloor + a \bmod P\)。前面一部分的贡献是好算的,因为怎么模它也不会变。后半部分由于 \(1 \sim 16\) 的 lcm 只有 \(720720\),所以可以直接 dp。定义 \(dp[i][j]\) 表示操作了 \(i\) 次之后 \(j\) 这个数的期望出现次数。有转移 \(dp[i + 1][j - j \bmod (i + 1)] \leftarrow \frac{1}{n}dp[i][j], dp[i + 1][j] \leftarrow (1 - \frac{1}{n})dp[i][j]\)。这样第 \(k\) 次循环产生的期望贡献就是 \(\frac{1}{n}\sum\limits_ii\times dp[k][i]\)。然后把两部分的答案加起来,乘以 \(n^k\) 再输出即可。

代码
#pragma GCC optimize(2)
#include <iostream>
#define int long long
using namespace std;
const int P = 998244353;
int a[10000005];
int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }
inline int qpow(int x, int y) {
    int ret = 1;
    while (y) {
        if (y & 1) 
            ret = ret * x % P;
        y >>= 1;
        x = x * x % P;
    }
    return ret;
}
int dp[18][720725];
inline void add(int& x, int y) { x += y, x -= (x >= P ? P : 0); }
signed main() {
    int n, x, y, k, M;
    cin >> n >> a[1] >> x >> y >> k >> M;
    for (int i = 2; i <= n; i++) a[i] = (a[i -1] * x + y) % M;
    int p = 1;
    for (int i = 1; i < k; i++) p = p * i / gcd(p, i);
    int ans = 0, s = 0;
    for (int i = 1; i <= n; i++) s += (a[i] / p) * p, dp[0][a[i] % p]++;
    int invn = qpow(n, P - 2);
    ans = k * s % P * invn % P;
    for (int i = 0; i < k; i++) {
        int tmp = 0;
        for (int j = 0; j < p; j++) {
            add(dp[i + 1][j - j % (i + 1)], invn * dp[i][j] % P);
            add(dp[i + 1][j], (P + 1 - invn) * dp[i][j] % P);
            tmp = (tmp + j * dp[i][j] % P) % P;
        }
        ans = (ans + tmp * invn % P) % P;
    }
    cout << ans * qpow(n, k) % P << "\n";
    return 0;
}
posted @ 2023-11-08 21:56  forgotmyhandle  阅读(46)  评论(0)    收藏  举报