Loading

[BZOJ4665] 小 w 的喜糖

思路

坏了这次没啥思路

转化题意, 求存在多少种数列 \(B\) , 使得 \(B\)\(A\) 中, 每种元素出现的次数相同并且满足 \(A_i \neq B_i\)
是这样转化的吗

你考虑直接算, 但是这样无论如何你要记录每种元素当前的出现次数作为状态, 不可能啊
怎么做比较方便? 看下标签发现可以使用「二项式反演」

你考虑到「钦定」\(k\) 个位置出现的元素不同不好处理, 考虑常见转化, 转化成「钦定」\(k\) 个位置出现的元素相同

「钦定」\(n - k\) 个元素相同的方案数我们记为 \(f(k)\)
考虑 \(f(k)\) 的计算, 显然需要对于每种颜色处理
\(n - k\) 个元素分给 \(m\) 种颜色让人联想到背包问题, 那么我们考虑 \(\rm{dp}\) 去做
具体的, 你「钦定」\(n - k\) 个元素相同, 其他的随意排列即可

\(dp_{i, j}\) 表示考虑前 \(i\) 种颜色,「钦定」\(j\) 个位置与原序列相同 (序列初始顺序与答案无关, 视作按颜色排序) 时对于相同部分可能的方案数, 记 \(c_i\) 表示颜色 \(i\) 的元素个数

有转移

\[dp_{i, j} = \sum_{k = 0}^{\min(c_i, j)} dp_{i - 1, j - k} {c_i \choose k} \]

然后你发现剩下的情况是多重集的排列, 所以容易发现

\[f(k) = dp_{m, n - k} \cdot \frac{k!}{(c_i - k_i)!} \]

这个 \(\rm{dp}\) 时处理即可

常见套路

\[f(k) = \sum_{i = 0}^{k} {n - i \choose n - k} g(i) \iff g(k) = \sum_{i = 0}^{k} (-1)^{k - i} {n - i \choose n - k} f(i) \]

代码

#include <bits/stdc++.h>
using namespace std;

#define QwQ01AwA return 0
#define ll long long
#define look_time cerr << 1.0 * clock() / CLOCKS_PER_SEC << '\n'
template <typename T> void ckmax(T &x, T y) {x = max(x, y);}
template <typename T> void ckmin(T &x, T y) {x = min(x, y);}

const int N = 2005;
const int mod = 1e9 + 9;
int ksm(int a, int b) {
    int ans = 1;
    for (; b; b >>= 1, a = 1ll * a * a % mod) {
        if (b & 1) ans = 1ll * ans * a % mod;
    }
    return ans;
}
int n;
int siz[N], f[N], g[N];
int fac[N], inv[N];

void init(int n) {
    fac[0] = 1;
    for (int i = 1; i <= n; i++) fac[i] = 1ll * fac[i - 1] * i % mod;
    inv[n] = ksm(fac[n], mod - 2);
    for (int i = n; i >= 1; i--) inv[i - 1] = 1ll * inv[i] * i % mod;
}
int C(int n, int m) {
    return 1ll * fac[n] * inv[m] % mod * inv[n - m] % mod;
}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    init(2000);
    cin >> n;
    for (int i = 1, x; i <= n; i++) cin >> x, siz[x]++;
    f[0] = 1;
    for (int i = 1; i <= n; i++) {
        if (!siz[i]) continue;
        memset(g, 0, sizeof(g));
        for (int j = 0; j <= n; j++) {
            if (!f[j]) continue;
            for (int k = 0; k <= siz[i]; k++) {
                g[j + k] = (g[j + k] + 1ll * C(siz[i], k) % mod * inv[siz[i] - k] % mod * f[j] % mod) % mod;
                g[j + k] = (g[j + k] % mod + mod) % mod;
            }
        }
        memcpy(f, g, sizeof(f));
    }
    int ans = 0;
    for (int i = 0; i <= n; i++) ans = (ans + ((n - i) % 2 ? -1 : 1) % mod * 1ll * f[n - i] % mod * fac[i] % mod) % mod;
    cout << (ans + mod) % mod << '\n';
    QwQ01AwA;
}

总结

\(\rm{dp}\)\(f\) 其实挺常见的

首先发现只能处理相同元素, 需要分配
背包处理分配问题是常见的

「钦定」意义下, 「至多」和「恰好」的转化

第一次见到组合数学中「多重集的排列」

posted @ 2025-01-01 21:27  Yorg  阅读(3)  评论(0编辑  收藏  举报