DP 与计数

NFLSOJ

A

CF294C Shaass and Light
考虑初始已点亮的灯将所有剩下的灯划分成的连续段。除开头结尾外,每个长为 l 的连续段有 2l 种操作序列。开头结尾的连续段只有 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。设有 k0,则我们判断是否排好序的标准就是前 k 个数里是否有 k0。又发现操作后任意一段前缀里 0 的个数是单调不降的。于是可以设计出 dp 状态:dp[i] 表示前 k 个数里有 i0 时的期望步数。最终答案即为 dp[k]。转移考虑期望经过多少步可以使得前 k 个数里的 0 增加。设当前前 k 个里共有 i0,则要增加就必须选到前面的 1 和后面的 0。可以发现前面 1 的个数和后面 0 的个数都是 ki 个。而总的选择方案共有 (n2) 种。所以此时一次操作给前 k 个增加一个 0 的概率即为 (ki)2(n2),期望次数即为 (n2)(ki)2。于是有 dp[i+1]=dp[i]+(n2)(ki)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=im,则 dp[i][j] 可以从 dp[t][x] 转移。考虑转移系数。显然我们要先在剩下的 nt 个点里选出 it 个赋权。然后要在这选出的 it 个点里相互连边,还要向前面的 t1 个点连边(减 1 是因为不用考虑向 1 连)。新连的边的权值必然是要大于等于 j 的(因为其有一个端点向 1 连了权为 j 的边),于是每条边有 kj+1 种选权值的方案。总共是 (ntit)(kj+1)((it)(it1)2+(it)(t1)),这就是转移系数。上面那个指数可以化简变成 (it)(i+t3)2,于是转移:dp[i][j]=t=1i1(x=0k)(ntit)(kj+1)(it)(i+t3)2。使用前缀和优化即可做到 O(n3)

代码
#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
首先,空子序列共有 mn 个贡献。
其次,考虑对每种子序列计算贡献。发现长度相同的子序列的贡献是相同的,于是只需要枚举子序列的长度 i。为了不算重,我们钦定一个子序列只在它第一次出现的位置被计算。设这个子序列出现的位置为 pos1,pos2,pos3,值为 val1,val2,val3,则 1pos11 之中不能出现 val1, pos1+1pos21 中不能出现 val2,以此类推。而最后出现的位置之后就没有限制了。所以我们再枚举这个子序列最后出现的位置 j,然后就在 j 前面的位置里放 i1 个元素。j 前面别的位置每个有 m1 种放法,之后每个位置有 m 种。于是答案即为 i1nmij=1n(j1i1)(m1)jimnj。接下来开始化简。
(1)i1nmij=1n(j1i1)(m1)jimnj=i=1nj=in(j1i1)(m1)jimnj+i=j=1ni1j(j1i1)(m1)jimnj+i=j=0n1i=0j(ji)(m1)jimnj+imnj=j=0n1mnji=0j(ji)(m1)jimi=j=0n1mnj(2m1)j.
于是就可以一遍循环过去求了。

代码
#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 个后括号,我们钦定必须选这个前括号,则方案数为 i=0min{a1,b1}(a1i)(bi+1)。这可以直接写成 i=0a(a1ai1)(bi+1),因为这里上界变化不会引起答案变化。然后就可以直接使用范德蒙德卷积写成 (a+b1a) 了。

代码
#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]=dp[i1][j]+dp[i][j1]2。初态:dp[0][1n]=1dp[1n][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
考虑最终的排列一定是形如 p1p2p3,其中 a[p3]2a[p2]a[p2]2a[p1],且两个相邻的 p 中间摆的数的两倍不能超过前一个 p 上摆的。我们称这样的 p 为关键点。我们考虑对关键点进行 dp。先将 ai 排序,求出 ki 表示有多少 2ajai。定义 dp[i] 表示 i 为关键点且所有小于等于 a[i]2a 都已经摆好的方案数。考虑枚举上一个关键点,称为 j。显然摆完 j 之后还剩 nkj1 个空位。在这些空位中,i 必然摆在第一个能摆的空位中,否则这个空位上的数一定不合法。除去 i,还剩 nkj2 个空位。我们要在这些空位上摆 kikj1 个数,共有 Akikj1nkj2 种摆法。于是转移方程:dp[i]=2ajaiAkikj1nkj2dp[j]。直接 n2 暴力即可。

代码
#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 十分小,于是可以直接求出 1k1 的 lcm,记为 P。然后将一个数拆成两部分:a=P×aP+amodP。前面一部分的贡献是好算的,因为怎么模它也不会变。后半部分由于 116 的 lcm 只有 720720,所以可以直接 dp。定义 dp[i][j] 表示操作了 i 次之后 j 这个数的期望出现次数。有转移 dp[i+1][jjmod(i+1)]1ndp[i][j],dp[i+1][j](11n)dp[i][j]。这样第 k 次循环产生的期望贡献就是 1nii×dp[k][i]。然后把两部分的答案加起来,乘以 nk 再输出即可。

代码
#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 @   forgotmyhandle  阅读(12)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示