计数中的容斥
计数中的容斥
容斥原理
式子:
一般的应用是钦定 \(k\) 个不合法,对答案的贡献乘上 \((-1)^k\) 的容斥系数。
P5405 [CTS2019] 氪金手游
给定 \(n\) 个点,第 \(i\) 个点的权值 \(w_i\) 分别有 \(p_{i, 1 \sim 3}\) 的概率取 \(1 \sim 3\) 。
确定所有 \(w_i\) 之后开始游戏:不断选点知道所有点都被选过,点 \(i\) 被选中的概率为 \(\frac{w_i}{\sum w}\) 。
给定 \(n - 1\) 个限制 \((u, v)\) 表示第一次抽到 \(u\) 的时间早于第一次抽到 \(v\) 的时间,保证所有 \((u, v)\) 连无向边后构成一棵树。
求满足所有限制的概率。
\(n \le 1000\)
先考虑 \(u \to v\) 连成外向树的情况,则要求每个点都要比子树内的点早抽到,点 \(u\) 合法的概率为 \(\frac{w_u}{\sum_{v \in subtree(u)} w_v}\) 。设 \(f_{u, i}\) 表示考虑 \(u\) 的子树、\(\sum_{v \in subtree(u)} w_v = i\) 的合法概率,转移类似树形背包,时间复杂度 \(O(n^2)\) 。
接下来考虑反向边的情况,考虑容斥,钦定有 \(i\) 条反向边不满足,即将钦定的这 \(i\) 条反向边反转,图会变成外向树森林,直接容斥可以做到 \(O(2^n n^2)\) 。
考虑优化,发现每次都暴力拆成外向树森林比较傻,考虑直接在树上 DP,DP 过程中决策每条反向边。设 \(f_{u, i, j}\) 表示考虑 \(u\) 的子树、\(\sum_{v \in subtree(u)} w_v = i\) 、钦定 \(j\) 条反向边的合法概率,注意钦定反向边的子树 \(w\) 为 \(0\) ,时间复杂度 \(O(n^3)\) 。
发现记录 \(j\) 只与最后的容斥系数有关,不妨在 DP 中直接乘上容斥系数,就不用记录 \(j\) 这一维了,时间复杂度 \(O(n^2)\) 。
另解:对于反向边的处理,考虑用 “不考虑这条边”的方案数 减去 “考虑这条边为外向边”的方案数,即 \(o \rightarrow o \leftarrow o\) 等价于 \(o \rightarrow o \ \ \ \ o\) 减去 \(o \rightarrow o \rightarrow o\) ,树上 DP 时遇到此类内向边就将权值用上面这种方法计算一下即可。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 1e3 + 7;
struct Graph {
vector<pair<int, int> > e[N];
inline void insert(int u, int v, int w) {
e[u].emplace_back(v, w);
}
} G;
int p[N][5], inv[N * 3], siz[N], f[N][N * 3], g[N * 3];
int n;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int mi(int p, int b) {
int res = 1;
for (; b; b >>= 1, p = 1ll * p * p % Mod)
if (b & 1)
res = 1ll * res * p % Mod;
return res;
}
void dfs(int u, int fa) {
f[u][0] = 1;
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (v == fa)
continue;
dfs(v, u);
int su = siz[u] * 3, sv = siz[v] * 3;
fill(g, g + su + sv + 1, 0);
if (w) {
int sum = accumulate(f[v], f[v] + sv + 1, 0ll) % Mod;
for (int i = 0; i <= su; ++i)
g[i] = 1ll * sum * f[u][i] % Mod;
for (int i = 0; i <= su; ++i)
for (int j = 0; j <= sv; ++j)
g[i + j] = dec(g[i + j], 1ll * f[u][i] * f[v][j] % Mod);
} else {
for (int i = 0; i <= su; ++i)
for (int j = 0; j <= sv; ++j)
g[i + j] = add(g[i + j], 1ll * f[u][i] * f[v][j] % Mod);
}
memcpy(f[u], g, sizeof(int) * (su + sv + 1)), siz[u] += siz[v];
}
int su = siz[u] * 3;
fill(g, g + su + 5, 0);
for (int i = 0; i <= su; ++i) {
g[i + 1] = add(g[i + 1], 1ll * f[u][i] * p[u][1] % Mod * inv[i + 1] % Mod);
g[i + 2] = add(g[i + 2], 2ll * f[u][i] * p[u][2] % Mod * inv[i + 2] % Mod);
g[i + 3] = add(g[i + 3], 3ll * f[u][i] * p[u][3] % Mod * inv[i + 3] % Mod);
}
memcpy(f[u], g, sizeof(int) * (su + 5)), ++siz[u];
}
signed main() {
scanf("%d", &n);
inv[0] = inv[1] = 1;
for (int i = 2; i <= n * 3; ++i)
inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= 3; ++j)
scanf("%d", p[i] + j);
int inv = mi(p[i][1] + p[i][2] + p[i][3], Mod - 2);
for (int j = 1; j <= 3; ++j)
p[i][j] = 1ll * p[i][j] * inv % Mod;
}
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v, 0), G.insert(v, u, 1);
}
dfs(1, 0);
int ans = 0;
for (int i = 0; i <= siz[1] * 3; ++i)
ans = add(ans, f[1][i]);
printf("%d", ans);
return 0;
}
QOJ10781. Permutation Pair
给定序列 \(a_{1 \sim n}\),求排列二元组 \((p_{1 \sim n}, q_{1 \sim n})\) 的数量,满足对于 \(n \times n\) 的矩阵 \(M_{i, j} = [\exists k, p_k = i, q_k = j]\) ,对于所有 \(M_{i, j} = 1\) 的位置 \((i, j)\) ,不存在覆盖 \((i, j)\) 的大小为 \((a_j + 1) \times (a_j + 1)\) 的子矩阵满足其为单位矩阵(主对角线为 \(1\) ,其余为 \(0\) )或单位矩阵的镜像(反对角线为 \(1\) ,其余为 \(0\) )。
\(n \le 5000\)
不难发现可以固定一个排列为 \(1, 2, \cdots, n\) ,最后将答案乘 \(n!\) 即可。问题转化为求排列 \(p_{1 \sim n}\) 的数量,满足对于所有 \(i\),\(p_i\) 所在的下标连续的公差为 \(1\) 或 \(-1\) 的等差数列长度 \(\le a_{p_i}\)。
考虑将 \(1 \sim n\) 做合法分段,然后考虑段内的翻转与段间的顺序,并要求相邻段不能连起来。后者可以容斥,钦定 \(k\) 个间隔连起来,乘上 \((-1)^k\) 的容斥系数。
设 \(f_{i, j, 0/1}\) 表示将 \(1 \sim i\) 分为 \(j\) 段,当前段长度 \(\ge 2\) 时是否需要 \(\times 2\) 的贡献,记录第三维是因为若干段连在一起的段段内不能翻转,只能整体翻转。
考虑 \(i \to i + 1\) 的转移,记 \(l = i + 1\) ,\(r\) 表示最大位置 \(j\) 满足 \(j - i + 1 \le \min_{k = i + 1}^j a_k\) ,若 \(r = n\) ,则可以对答案产生贡献。
分讨转移:
- 新建一段长度为 \(1\) 的段:\(f_{i, j, 0} + f_{i, j, 1} \to f_{i + 1, j + 1, 1}\) 。
- 新建一段长度 \(\ge 2\) 的段:\((f_{i, j, 0} + 2 f_{i, j, 1}) \to f_{l + 1 \sim r, j + 1, 1}\) 。
- 新建一段,但是钦定与当前段连在一起:\(-(f_{i, j, 0} + 2 f_{i, j, 1}) \to f_{l \sim r, j, 0}\) 。
前缀和优化即可做到 \(O(n^2)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 5e3 + 7;
int a[N], fac[N], mn[N][N], f[N][N][2], g[N][N][2];
int n;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
signed main() {
scanf("%d", &n), fac[0] = 1;
for (int i = 1; i <= n; ++i)
scanf("%d", a + i), fac[i] = 1ll * fac[i - 1] * i % Mod;
for (int i = 1; i <= n; ++i) {
mn[i][i] = a[i];
for (int j = i + 1; j <= n; ++j)
mn[i][j] = min(mn[i][j - 1], a[j]);
}
int ans = 0;
f[0][0][1] = 1;
for (int i = 0; i < n; ++i) {
int nxt = i + 1;
while (nxt < n && nxt + 1 - i <= mn[i + 1][nxt + 1])
++nxt;
if (i) {
for (int j = 0; j <= i; ++j)
for (int k = 0; k <= 1; ++k)
g[i][j][k] = add(g[i][j][k], g[i - 1][j][k]);
}
for (int j = 0; j <= i; ++j) {
f[i][j][0] = add(f[i][j][0], g[i][j][0]), f[i][j][1] = add(f[i][j][1], g[i][j][1]);
if (nxt == n) {
ans = add(ans, (n - i >= 2 ? 2ll : 1ll) * f[i][j][1] * fac[j + 1] % Mod);
ans = add(ans, 1ll * f[i][j][0] * fac[j + 1] % Mod);
}
int l = i + 1, r = nxt + 1;
if (l > r)
continue;
f[i + 1][j + 1][1] = add(f[i + 1][j + 1][1], add(f[i][j][0], f[i][j][1]));
g[l][j][0] = dec(g[l][j][0], add(f[i][j][0], 2ll * f[i][j][1] % Mod));
g[r][j][0] = add(g[r][j][0], add(f[i][j][0], 2ll * f[i][j][1] % Mod));
if (l < r) {
g[l + 1][j + 1][1] = add(g[l + 1][j + 1][1], add(f[i][j][0], 2ll * f[i][j][1] % Mod));
g[r][j + 1][1] = dec(g[r][j + 1][1], add(f[i][j][0], 2ll * f[i][j][1] % Mod));
}
}
}
printf("%d", 1ll * ans * fac[n] % Mod);
return 0;
}
CF1613F Tree Coloring
给出一棵树,定义一个排列 \(p_{1 \sim n}\) 合法当且仅当 \(\forall i \in [2, n], p_i \ne p_{fa_i} - 1\) ,求合法排列数。
\(n \le 2.5 \times 10^5\)
不难发现钦定一个点不合法是容易的,此时 \(p_i = p_{fa_i} - 1\) 。
考虑钦定 \(k\) 个点不合法,就只会剩下 \(n - k\) 个自由点,求方案数 \(f_k\) 后乘上系数 \((-1)^k (n - k)!\) 求和。
注意到一个父亲只能有一个儿子不合法,考虑在父亲处统计,方案数为 \(|son(u)|\) ,因此 \(f_k = [x^k] \prod_{u = 1}^n (|son(u)|x + 1)\) ,不难分治 + NTT 做到 \(O(n \log^2 n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353, rt = 3, invrt = (Mod + 1) / 3;
const int N = 2.5e5 + 7;
int deg[N], fac[N];
int n;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int mi(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % Mod)
if (b & 1)
res = 1ll * res * a % Mod;
return res;
}
inline int sgn(int n) {
return n & 1 ? Mod - 1 : 1;
}
namespace Poly {
vector<int> rev;
inline int calc(int n) {
int len = 1;
while (len < n)
len <<= 1;
rev.resize(len);
for (int i = 0; i < len; ++i)
rev[i] = (rev[i >> 1] >> 1) | (i & 1 ? len >> 1 : 0);
return len;
}
inline void NTT(vector<int> &f, int op) {
for (int i = 0; i < f.size(); ++i)
if (i < rev[i])
swap(f[i], f[rev[i]]);
for (int k = 1; k < f.size(); k <<= 1) {
int tG = mi(op == 1 ? rt : invrt, (Mod - 1) / (k << 1));
for (int i = 0; i < f.size(); i += k << 1) {
int buf = 1;
for (int j = 0; j < k; ++j) {
int fl = f[i + j], fr = 1ll * buf * f[i + j + k] % Mod;
f[i + j] = add(fl, fr), f[i + j + k] = dec(fl, fr);
buf = 1ll * buf * tG % Mod;
}
}
}
if (op == -1) {
int invn = mi(f.size(), Mod - 2);
for (int &it : f)
it = 1ll * it * invn % Mod;
}
}
inline vector<int> Mul(vector<int> f, vector<int> g) {
int lim = f.size() + g.size() - 1, len = calc(lim);
f.resize(len), g.resize(len);
NTT(f, 1), NTT(g, 1);
for (int i = 0; i < len; ++i)
f[i] = 1ll * f[i] * g[i] % Mod;
NTT(f, -1), f.resize(lim);
return f;
}
} // namespace Poly
vector<int> solve(int l, int r) {
if (l == r)
return {1, deg[l]};
int mid = (l + r) >> 1;
return Poly::Mul(solve(l, mid), solve(mid + 1, r));
}
signed main() {
scanf("%d", &n);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
++deg[u], ++deg[v];
}
fac[0] = fac[1] = 1;
for (int i = 2; i <= n; ++i)
--deg[i], fac[i] = 1ll * fac[i - 1] * i % Mod;
vector<int> f = solve(1, n);
int ans = 0;
for (int i = 0; i <= n; ++i)
ans = add(ans, 1ll * sgn(i) * fac[n - i] % Mod * f[i] % Mod);
printf("%d", ans);
return 0;
}
还可以优化,记 \(s_i = |son(i)|\) ,\(c_i\) 表示 \(s = i\) 的数量,则 \(\sum_{i = 1}^n s_i = \sum_{i = 1}^n i c_i = n - 1\) 。
考虑降序对于每个 \(i\) ,先 \(O(c_i)\) 二项式展开求出 \((ix + 1)^{c_i}\) ,然后将其卷到最终式上。
时间复杂度 \(O(\sum_{i = 1}^n (\sum_{j = i}^{n} c_j) \log n) = O(\log n \sum_{i = 1}^n i c_i) = O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353, rt = 3, invrt = (Mod + 1) / 3;
const int N = 2.5e5 + 7;
int fac[N], inv[N], invfac[N], deg[N], cnt[N];
int n;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int mi(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % Mod)
if (b & 1)
res = 1ll * res * a % Mod;
return res;
}
inline int sgn(int n) {
return n & 1 ? Mod - 1 : 1;
}
inline void prework(int n) {
fac[0] = fac[1] = 1;
inv[0] = inv[1] = 1;
invfac[0] = invfac[1] = 1;
for (int i = 2; i <= n; ++i) {
fac[i] = 1ll * fac[i - 1] * i % Mod;
inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
}
}
inline int C(int n, int m) {
return m > n ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}
namespace Poly {
vector<int> rev;
inline int calc(int n) {
int len = 1;
while (len < n)
len <<= 1;
rev.resize(len);
for (int i = 0; i < len; ++i)
rev[i] = (rev[i >> 1] >> 1) | (i & 1 ? len >> 1 : 0);
return len;
}
inline void NTT(vector<int> &f, int op) {
for (int i = 0; i < f.size(); ++i)
if (i < rev[i])
swap(f[i], f[rev[i]]);
for (int k = 1; k < f.size(); k <<= 1) {
int tG = mi(op == 1 ? rt : invrt, (Mod - 1) / (k << 1));
for (int i = 0; i < f.size(); i += k << 1) {
int buf = 1;
for (int j = 0; j < k; ++j) {
int fl = f[i + j], fr = 1ll * buf * f[i + j + k] % Mod;
f[i + j] = add(fl, fr), f[i + j + k] = dec(fl, fr);
buf = 1ll * buf * tG % Mod;
}
}
}
if (op == -1) {
int invn = mi(f.size(), Mod - 2);
for (int &it : f)
it = 1ll * it * invn % Mod;
}
}
inline vector<int> Mul(vector<int> f, vector<int> g) {
int lim = f.size() + g.size() - 1, len = calc(lim);
f.resize(len), g.resize(len);
NTT(f, 1), NTT(g, 1);
for (int i = 0; i < len; ++i)
f[i] = 1ll * f[i] * g[i] % Mod;
NTT(f, -1), f.resize(lim);
return f;
}
} // namespace Poly
signed main() {
scanf("%d", &n);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
++deg[u], ++deg[v];
}
++cnt[deg[1]];
for (int i = 2; i <= n; ++i)
++cnt[--deg[i]];
prework(n);
vector<int> f = {1};
for (int i = n; i; --i) {
if (!cnt[i])
continue;
vector<int> g(cnt[i] + 1);
for (int j = 0, pw = 1; j <= cnt[i]; ++j, pw = 1ll * pw * i % Mod)
g[j] = 1ll * C(cnt[i], j) * pw % Mod;
f = Poly::Mul(f, g);
}
int ans = 0;
for (int i = 0; i < f.size(); ++i)
ans = add(ans, 1ll * sgn(i) * fac[n - i] % Mod * f[i] % Mod);
printf("%d", ans);
return 0;
}
P2567 [SCOI2010] 幸运数字
定义:
- 幸运数字:各数位均为 \(6\) 或 \(8\) 的数。
- 近似幸运数字:存在一个因数为幸运数字的数。
求 \([L, R]\) 内近似幸运数字的数量。
\(L \le R \le 10^{10}\)
首先预处理出 \([1, 10^{10}]\) 内的幸运数字集合 \(S\) ,只有 \(2046\) 个。
然后考虑统计近似幸运数字,答案即为 \(\sum_{T \subseteq S} (\lfloor \frac{R}{\mathrm{lcm}(T)} \rfloor - \lceil \frac{L}{\mathrm{lcm}(T)} \rceil + 1)\) ,直接算会 TLE,考虑剪枝:
- \(\mathrm{lcm}(T) > R\) 时无需计算。
- dfs 枚举子集时按幸运数字降序排序,从而使得 \(\mathrm{lcm}(T)\) 增长更快。
- 对于两个幸运数字 \(a, b\) ,若 \(a \mid b\) ,则 \(b\) 的倍数一定是 \(a\) 的倍数,因此可以忽略这个 \(b\) ,幸运数字数量降为 \(943\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
vector<ll> vec;
ll L, R, ans;
void dfs1(ll k) {
if (k > R)
return;
vec.emplace_back(k);
dfs1(k * 10 + 6), dfs1(k * 10 + 8);
}
void dfs2(int x, int sgn, ll lcm) {
ans += sgn * (R / lcm - (L + lcm - 1) / lcm + 1);
for (int i = x + 1; i < vec.size(); ++i) {
ll g = __gcd(lcm, vec[i]);
if ((__int128)lcm / g * vec[i] <= R)
dfs2(i, -sgn, lcm / g * vec[i]);
}
}
signed main() {
scanf("%lld%lld", &L, &R);
dfs1(6), dfs1(8);
sort(vec.begin(), vec.end(), greater<ll>());
vector<ll> tmp;
for (int i = 0; i < vec.size(); ++i) {
bool flag = true;
for (int j = i + 1; j < vec.size(); ++j)
if (!(vec[i] % vec[j])) {
flag = false;
break;
}
if (flag)
tmp.emplace_back(vec[i]);
}
vec = tmp;
for (int i = 0; i < vec.size(); ++i)
dfs2(i, 1, vec[i]);
printf("%lld", ans);
return 0;
}
CF1530F Bingo
有一个 \(n \times n\) 的矩阵,\((i, j)\) 位置有 \(p_{i, j}\) 的概率为 \(1\) ,\(1 - p_{i, j}\) 的概率为 \(0\) 。
求至少满足以下条件其一的概率:
- 有一行全 \(1\) 。
- 有一列全 \(1\) 。
- 有一个对角线全 \(1\) 。
\(n \le 21\)
直接容斥可以做到 \(2^{2n + 2}\) ,无法通过。
考虑只枚举列和对角线的情况,预处理 \(p_{i, s}\) 表示第 \(i\) 行钦定情况为 \(s\) 的概率乘积,则不难 \(O(n)\) 求出 \(2^n\) 种行的情况的概率之和,时间复杂度 \(O(2^{n + 2} n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 31607, Inv = 3973;
const int N = 21;
int p[N][1 << N];
int n, ans;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int sgn(int n) {
return n & 1 ? Mod - 1 : 1;
}
inline int solve(int s) {
int res = 1;
for (int i = 0; i < n; ++i) {
int t = s & ((1 << n) - 1);
if (s >> n & 1)
t |= 1 << i;
if (s >> (n + 1) & 1)
t |= 1 << (n - 1 - i);
res = 1ll * res * dec(p[i][t], p[i][(1 << n) - 1]) % Mod;
}
return res;
}
signed main() {
scanf("%d", &n);
for (int i = 0; i < n; ++i) {
p[i][0] = 1;
for (int j = 0; j < n; ++j)
scanf("%d", p[i] + (1 << j)), p[i][1 << j] = 1ll * p[i][1 << j] * Inv % Mod;
for (int j = 1; j < (1 << n); ++j)
p[i][j] = 1ll * p[i][j & -j] * p[i][(j - 1) & j] % Mod;
}
int ans = 1;
for (int i = 0; i < (1 << (n + 2)); ++i)
ans = add(ans, 1ll * sgn(__builtin_popcount(i) + 1) * solve(i) % Mod);
printf("%d", ans);
return 0;
}
[AGC068D] Sum of Hash of Lexmin
给出一棵树,其中 \(fa_i < i\) 。定义排列 \(p_{1 \sim n}\) 合法当且仅当经过任意次以下操作不能得到一个字典序小于 \(p\) 的排列:
- 选择一个位置 \(1 \le i < n\) ,若 \(p_i\) 和 \(p_{i + 1}\) 在树上呈祖孙关系,交换 \(p_i\) 和 \(p_{i + 1}\) 。
给定常数 \(B\) ,定义一个排列 \(p_{1 \sim n}\) 的权值为 \(\sum_{i = 1}^n B^{i - 1} \times p_i\) ,求所有合法排列的权值和 \(\bmod 998244353\) 。
\(n \le 100\)
考虑如何比较方便地判定一个排列是否合法:
- 首先若存在 \(p_i \in \mathrm{subtree}(p_{i + 1})\) ,则说明 \(p_i > p_{i + 1}\) ,交换二者显然得到字典序更小的排列。
- 其次考虑一般情况,最后一定是交换了一对 \(p_i \in \mathrm{subtree}(p_{i + 1})\) ,但是二者初始不一定相邻。而注意到初始二者之间的数一定都在 \(p_{i + 1}\) 的子树中,只要将初始的 \(p_i, p_{i + 1}\) 交换即可。
因此得到排列合法当且仅当不存在 \(p_i \in \mathrm{subtree}(p_{i + 1})\) 。但是这样仍然不好做,考虑容斥,钦定若干对相邻的位置是祖先-后代关系,这样就连出了若干条链,比较方便 DP 计数。
但是此时还有哈希值的限制,经典的套路有两类:
- 对每一位单独考虑,这样系数就是固定的。
- 对每个数考虑,这样变量就是固定的。
由于链之间的位置关系,每一位是什么并不好处理,因此考虑后者,枚举点 \(p\) 计算其贡献。设 \(f_{u, i, j}\) 表示 \(u\) 子树内 \(p\) 左边钦定了 \(i\) 条链,右边钦定了 \(j\) 条链,并且固定左右内部顺序的贡献和。
首先有子树合并的转移,这个是 trivial 的,直接树上背包即可,注意左右边内部的链顺序任意要乘上组合数。
若 \(u = p\) ,则:
- 将 \(p\) 接在左边的最后一条链的末尾,系数为 \(-p\) 。
- 在中间新开一条链,系数为 \(p\) 。
若 \(u \ne p\) ,则:
- 在左边选一条链,把 \(u\) 接在这条链的末尾,系数为 \(-iB\) 。
- 在左边插入一条只包含 \(u\) 的新链,系数为 \((i + 1) B\) 。
- 在右边选一条链,把 \(u\) 接在这条链的末尾,系数为 \(-j\) 。
- 在右边插入一条只包含 \(u\) 的新链,系数为 \(j + 1\) 。
- 把 \(u\) 接在 \(p\) 所在链的末尾,系数为 \(-1\) ,需要满足 \(p \in \mathrm{subtree}(u)\) 。
直接做可以做到 \(O(n^5)\) ,加一维表示 \(p\) 是否确定即可优化到 \(O(n^4)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 1e2 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int fa[N], siz[N], f[N][N][N][2], g[N][N][2], C[N][N];
int n, base;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
void dfs(int u) {
siz[u] = 1, f[u][0][0][0] = 1;
for (int v : G.e[u]) {
dfs(v);
memset(g, 0, sizeof(g));
for (int i = 0; i <= siz[u]; ++i)
for (int j = 0; i + j <= siz[u]; ++j)
for (int k = 0; k <= 1; ++k)
if (f[u][i][j][k])
for (int x = 0; x <= siz[v]; ++x)
for (int y = 0; x + y <= siz[v]; ++y)
for (int z = 0; k + z <= 1; ++z)
if (f[v][x][y][z])
g[i + x][j + y][k + z] = add(g[i + x][j + y][k + z], 1ll * f[u][i][j][k] *
f[v][x][y][z] % Mod * C[i + x][i] % Mod * C[j + y][j] % Mod);
memcpy(f[u], g, sizeof(g)), siz[u] += siz[v];
}
memset(g, 0, sizeof(g));
for (int i = 0; i <= siz[u]; ++i)
for (int j = 0; i + j <= siz[u]; ++j)
if (f[u][i][j][0]) {
if (i)
g[i - 1][j][1] = dec(g[i - 1][j][1], 1ll * u * f[u][i][j][0] % Mod);
g[i][j][1] = add(g[i][j][1], 1ll * u * f[u][i][j][0] % Mod);
}
for (int i = 0; i <= siz[u]; ++i)
for (int j = 0; i + j <= siz[u]; ++j)
for (int k = 0; k <= 1; ++k)
if (f[u][i][j][k]) {
g[i][j][k] = dec(g[i][j][k], 1ll * i * base % Mod * f[u][i][j][k] % Mod);
g[i + 1][j][k] = add(g[i + 1][j][k], 1ll * (i + 1) * base % Mod * f[u][i][j][k] % Mod);
g[i][j][k] = dec(g[i][j][k], 1ll * j * f[u][i][j][k] % Mod);
g[i][j + 1][k] = add(g[i][j + 1][k], 1ll * (j + 1) * f[u][i][j][k] % Mod);
}
for (int i = 0; i <= siz[u]; ++i)
for (int j = 0; i + j <= siz[u]; ++j)
g[i][j][1] = dec(g[i][j][1], f[u][i][j][1]);
memcpy(f[u], g, sizeof(g));
}
signed main() {
scanf("%d%d", &n, &base);
for (int i = 2; i <= n; ++i)
scanf("%d", fa + i), G.insert(fa[i], i);
C[0][0] = 1;
for (int i = 1; i <= n; ++i) {
C[i][0] = 1;
for (int j = 1; j <= i; ++j)
C[i][j] = add(C[i - 1][j], C[i - 1][j - 1]);
}
dfs(1);
int ans = 0;
for (int i = 0; i <= n; ++i)
for (int j = 0; j <= n; ++j)
ans = add(ans, f[1][i][j][1]);
printf("%d", ans);
return 0;
}
P11197 [COTS 2021] 赛狗游戏 Tiket
给出三个排列 \(a, b, c\) ,求有多少对 \((x, y)\) 满足 \(a_x < a_y \and b_x < b_y \and c_x < c_y\) 。
\(n \le 5 \times 10^5\)
可以直接用 \(O(n \log^2 n)\) 的 cdq 分治卡常冲过去,但并不优美,考虑利用 \(a, b, c\) 都是排列的性质。
记满足某一维偏序的集合为 \(S\) ,则答案为:
注意到任意 \((x, y)\) 和 \((y, x)\) 恰有一者满足二维偏序,故 \(|S_a \cup S_b \cup S_c| = \frac{n(n - 1)}{2}\) ,于是只要 \(O(n \log n)\) 二维偏序求出前三者即可。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 5e5 + 7;
int t[N], a[N], b[N], c[N];
int n;
namespace BIT {
int c[N];
inline void clear() {
memset(c + 1, 0, sizeof(int) * n);
}
inline void update(int x, int k) {
for (; x <= n; x += x & -x)
c[x] += k;
}
inline int query(int x) {
int res = 0;
for (; x; x -= x & -x)
res += c[x];
return res;
}
} // namespace BIT
inline ll solve(int *a, int *b) {
vector<pair<int, int> > vec;
for (int i = 1; i <= n; ++i)
vec.emplace_back(a[i], b[i]);
sort(vec.begin(), vec.end()), memset(BIT::c + 1, 0, sizeof(int) * n);
ll ans = 0;
for (auto it : vec)
ans += BIT::query(it.second), BIT::update(it.second, 1);
return ans;
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", t + i);
for (int i = 1; i <= n; ++i) {
int x;
scanf("%d", &x);
a[x] = i;
}
for (int i = 1; i <= n; ++i) {
int x;
scanf("%d", &x);
b[x] = i;
}
for (int i = 1; i <= n; ++i) {
int x;
scanf("%d", &x);
c[x] = i;
}
printf("%lld", (solve(a, b) + solve(a, c) + solve(b, c) - 1ll * n * (n - 1) / 2) / 2);
return 0;
}
P11363 [NOIP2024] 树的遍历
给定一棵 \(n\) 个点的树,考虑定义一种以边为基础的遍历方式。
定义两条边相邻当且仅当它们有公共端点。初始时,所有边都未被标记。进行如下过程:
- 选择一条边 \(b\) 作为起始边,将它打上标记。
- 假设当前访问边为 \(e\) ,寻找任意一条与 \(e\) 相邻且未被标记的边 \(f\) ,将 \(f\) 作为新的访问边打上此标记,然后再次进入第二步。
- 如果与 \(e\) 相邻的边都被标记,如果 \(e = b\) 则遍历结束,否则将 \(e\) 设为遍历 \(e\) 之前的上一条边,再次进入第二步。
显然这样能够遍历所有边。考虑构建一张新图,新图中的点与原图中的边一一对应,且每次进行第二步时,在新图中将 \(e\) 和 \(f\) 对应的点连边。显然新图也是一棵树。
规定原树上的 \(k\) 条树边为关键边,求以这些边为起始边时,可能得到的本质不同的新树个数。
\(n \le 10^5\)
先考虑 \(k = 1\) 怎么做,不难发现到达一个点之后所有的出边顺序都是任意的,答案即为 \(\prod (\deg(i) - 1)!\) 。
再考虑 \(k = 2\) 的情况,若一棵新树会被二者同时统计到,则对于连接二者的链上的边,每对相邻的两条边在公共点的出边顺序中一定是一首一尾,因此链上的点的贡献为 \((\deg(i) - 2)!\) ,其余点的贡献不变,仍为 \((\deg(i) - 1)!\) 。
接下来考虑考虑容斥,统计同时被 \(i\) 条关键边统计到的新树的数量。
若不存在一条链覆盖这些关键边则显然方案数为 \(0\) ,因为此时虚树上存在三度点,无法钦定一首一尾。
因此只要考虑一条链上的边拿出来容斥统计方案,并且点的贡献只与首尾两条边的位置有关。考虑如此钦定方式:确定首尾两条边,然后中间任意钦定。若中间有 \(x\) 条边,则总的容斥系数为 \(\sum_{i = 0}^x \binom{x}{i} \times (-1)^{i + 2} = [x = 0]\) ,因此只需要对相邻的两条边容斥。
考虑方案计算,不难发现答案即为 \(\prod (\deg(i) - 1)!\) 乘上中间点的 \(\frac{\deg(i) - 2}{\deg(i) - 1}\) 之积,这样比较方便树上 DP。
设 \(f_u\) 表示考虑 \(u\) 子树与 \(u\) 父亲的边时要减去的积的贡献和,\(g_u\) 表示 \(u\) 子树内与 \(u\) 相邻的边与 \(u\) 产生的要减去的贡献和,转移不难做到线性,答案即为 \((m - f_1) \times \prod (\deg(i) - 1)!\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7, inv2 = (Mod + 1) / 2;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
struct Edge {
int u, v;
} e[N];
int fac[N], inv[N], invfac[N];
int fa[N], val[N], f[N], g[N];
bool tag[N];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline void prework() {
fac[0] = fac[1] = 1;
inv[0] = inv[1] = 1;
invfac[0] = invfac[1] = 1;
for (int i = 2; i < N; ++i) {
fac[i] = 1ll * fac[i - 1] * i % Mod;
inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
}
}
void dfs1(int u, int f) {
fa[u] = f;
for (int v : G.e[u])
if (v != f)
dfs1(v, u);
}
void dfs2(int u) {
f[u] = g[u] = 0;
int sum = 0;
for (int v : G.e[u]) {
if (v == fa[u])
continue;
dfs2(v);
f[u] = add(f[u], add(f[v], 1ll * sum * g[v] % Mod * val[u] % Mod));
sum = add(sum, g[v]);
}
if (tag[u])
f[u] = add(f[u], 1ll * sum * val[u] % Mod), g[u] = 1;
else
g[u] = 1ll * sum * val[u] % Mod;
}
signed main() {
prework();
int testid, T;
scanf("%d%d", &testid, &T);
while (T--) {
scanf("%d%d", &n, &m);
G.clear(n);
for (int i = 1; i < n; ++i) {
scanf("%d%d", &e[i].u, &e[i].v);
G.insert(e[i].u, e[i].v), G.insert(e[i].v, e[i].u);
}
dfs1(1, 0);
memset(tag + 1, false, sizeof(bool) * n);
for (int i = 1; i <= m; ++i) {
int id;
scanf("%d", &id);
tag[e[id].v == fa[e[id].u] ? e[id].u : e[id].v] = true;
}
int all = 1;
for (int i = 1; i <= n; ++i) {
all = 1ll * all * fac[G.e[i].size() - 1] % Mod;
if (G.e[i].size() >= 2)
val[i] = 1ll * fac[G.e[i].size() - 2] * invfac[G.e[i].size() - 1] % Mod;
}
dfs2(1);
printf("%d\n", 1ll * dec(m, f[1]) * all % Mod);
}
return 0;
}
P10681 [COTS 2024] 奇偶矩阵 Tablica
求满足如下条件的大小为 \(n \times m\) 的 01 矩阵数量:
- 每行 \(1\) 的数量 \(\in \{ 1, 2 \}\) 。
- 每列 \(1\) 的数量 \(\in \{ 1, 2 \}\) 。
\(n, m \le 5000\)
考虑枚举 \(1\) 的数量为 \(1, 2\) 的行有 \(a, b\) 个,\(1\) 的数量为 \(1, 2\) 的列有 \(c, d\) 个,由于 \(a + b = n\) 、\(c + d = m\) 、\(a + 2b = c + 2d\) ,因此枚举的量为 \(O(\min(n, m))\) 级别。
考虑对于固定的 \(c, d\) ,将这些 \(1\) 分配到行上满足 \(a, b\) 的限制,问题转化为:有 \(c + d\) 种颜色的球,其中有 \(c\) 种颜色只有一个,\(d\) 种颜色有两个,然后将其划分为 \(n\) 个非空集合,每个集合的大小 \(\le 2\) ,且内部球的颜色不同。
考虑将所有球排成一排,取点 \(a\) 个球各自组成集合,剩下按顺序相邻两个球放在一个集合中,但是此时会出现:
- 将同色球放在同一集合中的错误情况:容斥即可。
- 集合内部会带顺序(排成一排的统计天然带顺序),大小为 \(2\) 的集合会被统计两次:将方案除以 \(2^b\) 即可。
答案即为:
时间复杂度 \(O(\min(n, m)^2)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7, inv2 = (Mod + 1) / 2;
const int N = 1e4 + 7;
int fac[N], inv[N], invfac[N], ipw[N];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline void prework(int n) {
fac[0] = fac[1] = 1;
inv[0] = inv[1] = 1;
invfac[0] = invfac[1] = 1;
for (int i = 2; i <= n; ++i) {
fac[i] = 1ll * fac[i - 1] * i % Mod;
inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
}
ipw[0] = 1;
for (int i = 1; i <= n; ++i)
ipw[i] = 1ll * ipw[i - 1] * inv2 % Mod;
}
inline int sgn(int n) {
return n & 1 ? Mod - 1 : 1;
}
inline int C(int n, int m) {
return m > n || m < 0 ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}
signed main() {
scanf("%d%d", &n, &m);
prework(n + m);
int ans = 0;
for (int b = 0; b <= n; ++b) {
int a = n - b, d = (a + b * 2 - m), c = m - d, res = 0;
if (d < 0 || c < 0)
continue;
for (int i = 0; i <= min(b, d); ++i)
res = add(res, 1ll * sgn(i) * C(b, i) % Mod * C(d, i) % Mod * fac[i] % Mod *
fac[a + (b - i) * 2] % Mod * ipw[b + d - i] % Mod);
ans = add(ans, 1ll * C(n, a) * C(m, c) % Mod * res % Mod);
}
printf("%d", ans);
return 0;
}
二项式反演
有四个形式:
通常可以用于恰好 \(k\) 个和至少(钦定)\(k\) 个的转化。
CF285E Positions in Permutations
求长度为 \(n\) 、恰好有 \(m\) 个位置满足 \(|p_i - i| = 1\) 的排列 \(p_{1 \sim n}\) 的数量 \(\bmod (10^9 + 7)\) 。
\(n \le 1000\)
设 \(F(m)\) 为恰好 \(m\) 个位置满足条件的答案,\(G(m)\) 为钦定 \(m\) 个位置满足条件的答案,则:
二项式反演得到:
设 \(f_{i, j, 0/1, 0/1}\) 表示考虑前 \(i\) 个位置,钦定 \(j\) 个位置满足条件, \(i\) 和 \(i + 1\) 是否被选的方案数。转移分类讨论 \(p_i\) 的取值:
- \(p_i = i - 1\) :\(f_{i, j, k, 0} \gets f_{i - 1, j - 1, 0, k}\) 。
- \(p_i = i + 1\) :\(f_{i, j, k, 1} \gets f_{i - 1, j - 1, 0, k} + f_{i - 1, j - 1, 1, k}\) 。
- 其他情况:\(f_{i, j, k, 0} \gets f_{i - 1, j, 0, k} + f_{i - 1, j, 1, k}\) 。
特殊处理一下 \(i = 1, n\) 的情况即可,其中 \(G(i) = (n - i)! (f_{n, i, 0, 0} + f_{n, i, 1, 0})\) ,时间复杂度 \(O(n^2)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e3 + 7;
int f[N][N][2][2], fac[N], inv[N], invfac[N];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int sgn(int n) {
return n & 1 ? Mod - 1 : 1;
}
inline void prework(int n) {
fac[0] = fac[1] = 1;
inv[0] = inv[1] = 1;
invfac[0] = invfac[1] = 1;
for (int i = 2; i <= n; ++i) {
fac[i] = 1ll * fac[i - 1] * i % Mod;
inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
}
}
inline int C(int n, int m) {
return m > n ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}
signed main() {
scanf("%d%d", &n, &m);
prework(n);
f[1][1][0][1] = f[1][0][0][0] = 1;
for (int i = 2; i <= n; ++i) {
f[i][0][0][0] = 1;
for (int j = 1; j <= i; ++j)
for (int k = 0; k <= 1; ++k) {
f[i][j][k][0] = add(f[i - 1][j - 1][0][k], add(f[i - 1][j][0][k], f[i - 1][j][1][k]));
if (i < n)
f[i][j][k][1] = add(f[i - 1][j - 1][0][k], f[i - 1][j - 1][1][k]);
}
}
int ans = 0;
for (int i = m; i <= n; ++i)
ans = add(ans, 1ll * sgn(i - m) * C(i, m) % Mod *
add(f[n][i][0][0], f[n][i][1][0]) % Mod * fac[n - i] % Mod);
printf("%d", ans);
return 0;
}
P4491 [HAOI2018] 染色
有一个长度为 \(n\) 的序列,可以给每个位置染上 \([1, m]\) 的颜色。若恰好出现 \(S\) 次的颜色有 \(k\) 种,则会获得 \(w_k\) 的价值。求所有染色方案的价值和。
\(n \le 10^7\) ,\(m \le 10^5\) ,\(S \le 150\)
不难发现当 \(k > \min(m, \lfloor \frac{n}{s} \rfloor)\) 时 \(w_k\) 不会被统计,下令 \(n = \min(m, \lfloor \frac{n}{s} \rfloor)\) 。
考虑容斥,钦定 \(k\) 种颜色染 \(S\) 次,则:
不难发现这个会算重,设恰好出现 \(S\) 次的颜色有 \(k\) 种的方案数为 \(g(k)\) ,则不难得到:
二项式反演得到:
拆开组合数后 NTT 做差卷积即可。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1004535809;
const int N = 1e7 + 7, M = 1e5 + 7;
int fac[N], inv[N], invfac[N];
int a[M], f[M], g[M], h[M];
int n, m, s;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int mi(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % Mod)
if (b & 1)
res = 1ll * res * a % Mod;
return res;
}
inline int sgn(int n) {
return n & 1 ? Mod - 1 : 1;
}
inline void prework() {
fac[0] = fac[1] = 1;
inv[0] = inv[1] = 1;
invfac[0] = invfac[1] = 1;
for (int i = 2; i < N; ++i) {
fac[i] = 1ll * fac[i - 1] * i % Mod;
inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
}
}
inline int C(int n, int m) {
return m > n ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}
namespace Poly {
#define cpy(f, g, n) memcpy(f, g, sizeof(int) * (n))
#define clr(f, n) memset(f, 0, sizeof(int) * (n))
const int rt = 3, invrt = 334845270;
const int S = 2e6 + 7;
int rev[S];
inline void calrev(int n) {
for (int i = 1; i < n; ++i)
rev[i] = (rev[i >> 1] >> 1) | (i & 1 ? n >> 1 : 0);
}
inline int calc(int n) {
int len = 1;
while (len <= n)
len <<= 1;
return calrev(len), len;
}
inline void NTT(int *f, int n, int op) {
for (int i = 0; i < n; ++i)
if (i < rev[i])
swap(f[i], f[rev[i]]);
for (int k = 1; k < n; k <<= 1) {
int tG = mi(op == 1 ? rt : invrt, (Mod - 1) / (k << 1));
for (int i = 0; i < n; i += k << 1) {
int buf = 1;
for (int j = 0; j < k; ++j) {
int fl = f[i + j], fr = 1ll * buf * f[i + j + k] % Mod;
f[i + j] = add(fl, fr), f[i + j + k] = dec(fl, fr);
buf = 1ll * buf * tG % Mod;
}
}
}
if (op == -1) {
int invn = mi(n, Mod - 2);
for (int i = 0; i < n; ++i)
f[i] = 1ll * f[i] * invn % Mod;
}
}
inline void Times(int *f, int *g, int n) {
NTT(f, n, 1), NTT(g, n, 1);
for (int i = 0; i < n; ++i)
f[i] = 1ll * f[i] * g[i] % Mod;
NTT(f, n, -1);
}
inline void Mul(int *f, int n, int *g, int m, int *res) {
static int a[S], b[S];
int len = calc(n + m - 1);
cpy(a, f, n), clr(a + n, len - n);
cpy(b, g, m), clr(b + m, len - m);
Times(a, b, len), cpy(res, a, n + m - 1);
}
#undef cpy
#undef clr
} // namespace Poly
signed main() {
prework();
n = read(), m = read(), s = read();
for (int i = 0; i <= m; ++i)
a[i] = read();
int lim = min(m, n / s);
for (int i = 0; i <= lim; ++i) {
f[i] = 1ll * C(m, i) * fac[n] % Mod * mi(invfac[s], i) % Mod *
invfac[n - s * i] % Mod * mi(m - i, n - s * i) % Mod * fac[i] % Mod;
h[i] = 1ll * sgn(i) * invfac[i] % Mod;
}
reverse(f, f + lim + 1);
Poly::Mul(f, lim + 1, h, lim + 1, g);
reverse(g, g + lim + 1);
int ans = 0;
for (int i = 0; i <= lim; ++i)
ans = add(ans, 1ll * g[i] * invfac[i] % Mod * a[i] % Mod);
printf("%d", ans);
return 0;
}
P10004 [集训队互测 2023] Permutation Counting 2
给定 \(n\) ,对于每组 \(x, y \in [0, n - 1]\) ,求满足 \(\sum_{i = 1}^{n - 1} [p_i < p_{i + 1}] = x\) 且 \(\sum_{i = 1}^{n - 1} [p^{-1}_i < p^{-1}_{i + 1}] = y\) 的 \(1 \sim n\) 的排列 \(p\) 的数量。
\(n \le 500\)
记 \(f_{i, j}\) 表示在原排列和逆排列分别钦定 \(i, j\) 上升的连续对的方案数,则:
考虑二元二项式反演,形式与一元类似:
求出 \(f_{i, j}\) 后前缀和优化即可做到 \(O(n^3)\) 。
在原排列和逆排列分别钦定 \(i, j\) 上升的连续对,等价于分别钦定 \(n - i, n - j\) 个连续的上升段。
观察 \(p^{-1}\) 上一个连续的上升段,将其对应的下标区间 \([l, r]\) 依次插入 \(p\) 中的 \(i\) 个连续的上升段,只关心每个段插了多少元素,则每一种分配方式恰好对应一组 \(p\) 。
问题转化为计数有多少大小为 \(i \times j\) 的矩阵满足每行每列和非零,且总元素和为 \(n\) 。
考虑容斥,钦定 \(a\) 个行 \(b\) 个列为 \(0\) ,不难插板法直接计算。
时间复杂度 \(O(n^3)\) ,可能需要一些卡常。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e2 + 7, M = 3e5 + 7;
int fac[M], inv[M], invfac[M], c[N][N], f[N][N], g[N][N], s[N];
int n, Mod;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int sgn(int n) {
return n & 1 ? Mod - 1 : 1;
}
inline void prework(int n) {
fac[0] = fac[1] = 1;
inv[0] = inv[1] = 1;
invfac[0] = invfac[1] = 1;
for (int i = 2; i <= n; ++i) {
fac[i] = 1ll * fac[i - 1] * i % Mod;
inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
}
}
inline int C(int n, int m) {
return m > n || m < 0 ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}
signed main() {
scanf("%d%d", &n, &Mod), prework(n * (n + 1));
for (int i = c[0][0] = 1; i <= n; ++i)
for (int j = c[i][0] = 1; j <= i; ++j)
c[i][j] = add(c[i - 1][j], c[i - 1][j - 1]);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
g[i][j] = C(n + i * j - 1, i * j - 1);
for (int i = 1; i <= n; ++i) {
memset(s + 1, 0, sizeof(int) * n);
for (int j = 1; j <= n; ++j)
for (int k = 1; k <= i; ++k)
s[j] = add(s[j], 1ll * sgn(i - k) * c[i][k] % Mod * g[k][j] % Mod);
for (int j = 1; j <= n; ++j)
for (int k = 0; k <= j; ++k)
f[i][j] = add(f[i][j], 1ll * sgn(j - k) * c[j][k] % Mod * s[k] % Mod);
}
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
g[n - i][n - j] = f[i][j];
memset(f, 0, sizeof(f));
for (int i = 0; i < n; ++i) {
memset(s, 0, sizeof(int) * n);
for (int j = 0; j < n; ++j)
for (int k = i; k < n; ++k)
s[j] = add(s[j], 1ll * sgn(k - i) * c[k][i] % Mod * g[k][j] % Mod);
for (int j = 0; j < n; ++j)
for (int k = j; k < n; ++k)
f[i][j] = add(f[i][j], 1ll * sgn(k - j) * c[k][j] % Mod * s[k] % Mod);
}
for (int i = 0; i < n; ++i, puts(""))
for (int j = 0; j < n; ++j)
printf("%d ", f[i][j]);
return 0;
}
子集反演
本质就是容斥,式子为:
P8329 [ZJOI2022] 树
给定 \(N\) 和模数 \(M\) ,对于所有 \(n = 2, 3, \cdots, N\) ,求满足以下条件的树的二元组 \((T_1, T_2)\) 的方案数:
- \(T_1\) 中每个点的父亲编号均小于自己。
- \(T_2\) 中每个点的父亲编号均大于自己。
- \(T_1\) 的叶子集合与 \(T_2\) 的非叶子集合相同,\(T_1\) 的非叶子集合与 \(T_2\) 的叶子集合相同。
\(n \le 500\)
设:
- \(F(S)\) 表示 \(T_1\) 的非叶子集合恰好为 \(S\) 时 \(T_1\) 的方案数。
- \(G(S)\) 表示 \(T_2\) 的非叶子集合恰好为 \(S\) 时 \(T_2\) 的方案数。
答案即为:
考虑子集反演,设:
- \(f(S)\) 表示 \(T_1\) 的非叶子集合为 \(S\) 的子集时 \(T_1\) 的方案数,则 \(f(S) = \sum_{S' \subseteq S} F(S) \iff F(S) = \sum_{S' \subseteq S} (-1)^{|S| - |S'|} f(S)\) 。
- \(g(S)\) 表示 \(T_2\) 的非叶子集合为 \(S\) 的子集时 \(T_2\) 的方案数,则 \(g(S) = \sum_{S' \subseteq S} G(S) \iff G(S) = \sum_{S' \subseteq S} (-1)^{|S| - |S'|} g(S)\) 。
答案即为:
考虑已知 \(S\) 时如何计算 \(f(S)\) ,对于 \(i \in (1, n]\) ,\(i\) 的父亲只可能是 \([1, i)\) 中钦定的非叶子节点,\(g(T)\) 同理。
设 \(f_{i, j, k}\) 表示确定了 \(1 \sim i\) 的点集在 \(S\) 中还是在 \(T\) 中,其中 \(S\) 中 \(1 \sim i\) 放了 \(j\) 个、\(T\) 中 \(i + 1 \sim n\) 放了 \(k\) 个,且确定了 \((1, i]\) 在 \(T_1\) 中的父亲、\([1, i)\) 在 \(T_2\) 中的父亲的方案数。转移时分类讨论 \(i\) 的归属与 \(i\) 在 \(T_1, T_2\) 中的父亲(只能接在钦定的非叶子上):
- \(i \in S\) :\(f_{i - 1, j, k} \times j \times k \to f_{i, j + 1, k}\) 。
- \(i \in T\) :\(f_{i - 1, j, k} \times j \times k \to f_{i, j, k - 1}\) 。
- \(i \notin S \cup T\) :\(f_{i - 1, j, k} \times (-2 \times j \times k) \to f_{i, j, k}\) 。
时间复杂度 \(O(n^3)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e2 + 7;
int f[N][N][N];
int n, Mod;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
signed main() {
scanf("%d%d", &n, &Mod);
fill(f[1][1] + 1, f[1][1] + n, 1);
for (int i = 2; i <= n; ++i) {
for (int j = 1; j < i; ++j)
for (int k = 1; k <= n - (i - 1); ++k) {
if (!f[i - 1][j][k])
continue;
f[i][j][k] = add(f[i][j][k], 1ll * (Mod - 2) * j % Mod * k % Mod * f[i - 1][j][k] % Mod);
f[i][j + 1][k] = add(f[i][j + 1][k], 1ll * j * k % Mod * f[i - 1][j][k] % Mod);
f[i][j][k - 1] = add(f[i][j][k - 1], 1ll * j * k % Mod * f[i - 1][j][k] % Mod);
}
int ans = 0;
for (int j = 1; j < i; ++j)
ans = add(ans, 1ll * f[i - 1][j][1] * j % Mod);
printf("%d\n", ans);
}
return 0;
}