图论计数
图论计数
Prufer 序列
Prufer 序列可以将一棵带标号 \(n\) 个点的树用 \(n - 2\) 个值域为 \(1 \sim n\) 的整数序列表示。具体地,每次删去编号最小的一度点,然后在序列中记录其所连的点,重复 \(n - 2\) 次后结束。
性质:
- 构建完 Prufer 序列后原树中会剩下两个点,其中必然有一个为 \(n\) 。
- 每个节点在序列中出现的次数就是其度数 \(-1\) 。
- Prufer 序列与完全图的生成树构成双射。
由于 \(n\) 一定不会被删掉,因此不妨钦定树的根为 \(n\) 。
利用 Prufer 序列可以证明 Cayley 公式:\(n\) 个有标号点组成的无向完全图有 \(n^{n - 2}\) 棵生成树。
推论:
- \(n\) 个点的有标号无根树数量为 \(n^{n - 2}\) 。
- \(n\) 个点的有标号有根树数量为 \(n^{n - 1}\) 。
线性构建 Prufer 序列
\(O(n \log n)\) 是简单的,用堆维护一度点即可。
维护指针 \(p\) ,其指向编号最小的叶子,初值为 \(1\) 。同时维护每个节点的度数以判断是否产生新的叶子。不断重复如下过程直至构建出的序列长度达到 \(n - 2\) :
- 让 \(p\) 自增直到 \(p\) 指向一个未被删除的叶子。
- 删去 \(p\) 并更新 Prufer 序列。
- 若删去 \(p\) 后其父亲为新的叶子且编号 \(< p\) ,则立即删除其父亲,并更新 Prufer 序列。重复此操作直到不满足条件。
inline vector<int> BuildPrufer(vector<int> fa, int n) {
vector<int> deg(n + 1), seq;
for (int i = 1; i < n; ++i)
++deg[i], ++deg[fa[i]];
for (int p = 1; seq.size() < n - 2; ++p) {
while (deg[p] > 1)
++p;
--deg[fa[p]], seq.emplace_back(fa[p]);
for (int x = fa[p]; seq.size() < n - 2 && deg[x] == 1 && x < p; x = fa[x])
--deg[fa[x]], seq.emplace_back(fa[x]);
}
return seq;
}
线性重建树
由 Prufer 序列可以得到每个点的度数,同样维护一个指针 \(p\) ,模拟构建过程。重建过程中同样会不断产生新的叶子,连边即可。
inline vector<int> BuildTree(vector<int> seq, int n) {
vector<int> deg(n + 1, 1), fa(n);
for (int it : seq)
++deg[it];
for (int i = 0, p = 1; i < seq.size(); ++p) {
while (deg[p] > 1)
++p;
--deg[fa[p] = seq[i++]];
for (int x = fa[p]; i < seq.size() && deg[x] == 1 && x < p; x = fa[x])
--deg[fa[x] = seq[i++]];
}
return fa[seq.back()] = n, fa;
}
应用
CF156D Clues
给出一个 \(n\) 个点 \(m\) 条边的带标号无向图,记其有 \(k\) 个连通块,求添加 $ k - 1$ 条边使得图连通的方案数。
\(n, m \le 10^5\)
建立新图并将每个联通块视为点 \(1 \sim k\) ,令 \(s_i\) 为每个连通块中点的数量, \(d_i\) 为新图中 \(i\) 的度数。
首先有:
由于 Prufer 序列中 \(i\) 的出现次数即为 \(d_i - 1\) ,因此对于给定的序列 \(d\) ,在新图中的 Prufer 序列的方案数为:
对于第 \(i\) 个连通块所连的 \(d_i\) 条边,每条边都可以选择 \(s_i\) 种内部点,因此归于给定的 \(d\) 序列,使图联通的方案数是:
枚举所有的 \(d\) 序列,总方案数即为:
根据多元二项式定理:
令 \(e_i = d_i - 1\) ,则 \(\sum_{i = 1}^k e_i = k - 2\) ,于是原式转化为:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
bool vis[N];
int n, m, Mod;
int dfs(int u) {
vis[u] = true;
int siz = 1;
for (int v : G.e[u])
if (!vis[v])
siz += dfs(v);
return siz;
}
signed main() {
scanf("%d%d%d", &n, &m, &Mod);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
int ans = 1, cnt = 0;
for (int i = 1; i <= n; ++i)
if (!vis[i])
++cnt, ans = 1ll * ans * dfs(i) % Mod;
if (cnt == 1)
return puts(Mod == 1 ? "0" : "1"), 0;
for (int i = 1; i <= cnt - 2; ++i)
ans = 1ll * ans * n % Mod;
printf("%d", ans);
return 0;
}
P5454 [THUPC2018]城市地铁规划
给出一个 \(k\) 次多项式 \(F(x)\) ,构造一棵 \(n\) 个点的树,记 \(d_i\) 为每个点的度数,求:
\[\max \left( \sum_{i = 1}^n (F(d_i) \bmod 59393) \right) \]并给出方案。
\(n \le 3000\) ,\(k \le 10\)
考虑求解一组 \(d_i\) 使上式达到最大,最后任意构造一组 Prufer 序列去重建树即可。
先给每个节点分配 \(1\) 的度数,剩余总度数和为 \(n - 2\) 。设 \(f_i\) 表示 Prufer 序列中分配完 \(i\) 位可获得的最大权值和,枚举每个点的出现次数,则:
时间复杂度 \(O(n^2 k)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 59393;
const int N = 3e3 + 7, K = 1e1 + 7;
int a[K], w[N], f[N], g[N], prufer[N], fa[N];
int n, k;
inline void prework() {
for (int i = 0; i <= n; ++i)
for (int j = k; ~j; --j)
w[i] = (w[i] * i + a[j]) % Mod;
}
inline vector<int> BuildTree(vector<int> seq, int n) {
vector<int> deg(n + 1, 1), fa(n);
for (int it : seq)
++deg[it];
for (int i = 0, p = 1; i < seq.size(); ++p) {
while (deg[p] > 1)
++p;
--deg[fa[p] = seq[i++]];
for (int x = fa[p]; i < seq.size() && deg[x] == 1 && x < p; x = fa[x])
--deg[fa[x] = seq[i++]];
}
return fa[seq.back()] = n, fa;
}
signed main() {
scanf("%d%d", &n, &k);
for (int i = 0; i <= k; ++i)
scanf("%d", a + i);
prework();
printf("%d ", n - 1);
if (n == 1)
return printf("%d", w[0]), 0;
else if (n == 2)
return printf("%d\n1 2", w[1]), 0;
f[0] = n * w[1];
for (int i = 2; i <= n; ++i)
for (int j = i - 1; j <= n - 2; ++j)
if (f[j - i + 1] + w[i] - w[1] > f[j])
f[j] = f[j - i + 1] + w[i] - w[1], g[j] = j - i + 1;
printf("%d\n", f[n - 2]);
vector<int> seq;
for (int i = n - 2, x = 1; i; i = g[i], ++x)
seq.insert(seq.end(), i - g[i], x);
vector<int> fa = BuildTree(seq, n);
for (int i = 1; i < n; ++i)
printf("%d %d\n", i, fa[i]);
return 0;
}
CF917D Stranger Trees
有一张 \(n\) 个点的完全图,给定该图的一个生成树。对于 \(i = 0, 1, \cdots, n - 1\) ,求有多少棵这个完全图的生成树,使得这些生成树与给定的生成树恰好有 \(i\) 条边重合。
\(n \le 100\)
恰好 \(i\) 条边是不好处理的,考虑二项式反演,则只需求钦定重合 \(i\) 条边的方案数。
钦定 \(i\) 条边重合相当于给原树划分为 \(n - i\) 个连通块,连通块之间任意连边形成一棵树,任意连边的方案数即为 \(n^{k - 2} \prod_{i = 1}^k s_i\) 。
考虑组合意义,\(\prod_{i = 1}^k s_i\) 等价于给每个连通块内部任意定根的方案数,设 \(f_{u, i, 0/1}\) 表示 \(u\) 子树内划分了 \(i\) 个连通块,\(u\) 所在连通块是否定根的方案数,转移就是树形背包,不难做到 \(O(n^2)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
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 fac[N], inv[N], invfac[N], siz[N], f[N][N][2], g[N][2], ans[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 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;
}
void dfs(int u, int fa) {
f[u][1][0] = f[u][1][1] = siz[u] = 1;
for (int v : G.e[u]) {
if (v == fa)
continue;
dfs(v, u);
memset(g, 0, sizeof(g));
for (int i = 1; i <= siz[u]; ++i)
for (int j = 1; j <= siz[v]; ++j) {
g[i + j][0] = add(g[i + j][0], 1ll * f[u][i][0] * f[v][j][1] % Mod);
g[i + j][1] = add(g[i + j][1], 1ll * f[u][i][1] * f[v][j][1] % Mod);
g[i + j - 1][0] = add(g[i + j - 1][0], 1ll * f[u][i][0] * f[v][j][0] % Mod);
g[i + j - 1][1] = add(g[i + j - 1][1],
add(1ll * f[u][i][0] * f[v][j][1] % Mod, 1ll * f[u][i][1] * f[v][j][0] % Mod));
}
memcpy(f[u], g, sizeof(g)), siz[u] += siz[v];
}
}
signed main() {
scanf("%d", &n);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
dfs(1, 0);
for (int i = 2, mul = 1; i <= n; ++i, mul = 1ll * mul * n % Mod)
ans[n - i] = 1ll * f[1][i][1] * mul % Mod;
ans[n - 1] = 1, prework(n);
for (int i = 0; i < n; ++i)
for (int j = i + 1; j < n; ++j)
ans[i] = add(ans[i], 1ll * sgn(j - i) * C(j, i) % Mod * ans[j] % Mod);
for (int i = 0; i < n; ++i)
printf("%d ", ans[i]);
return 0;
}
P9536 [YsOI2023] Prüfer 序列
给定 \(n\) 和长度为 \(m\) 的序列 \(a\) ,其中 \(a_i \in [1, n]\) 。
对于 \(i = 1, 2, \cdots, n\) ,等概率选取 \(a\) 的一个长度为 \(n - 2\) 的子序列作为 Prufer 序列,生成一棵树,求 \(\mathrm{dist}(i, n)\) 的期望。
数据范围不好描述
考虑重建树 \(O(n \log n)\) 的算法,维护 \(S\) 为当前度数为 \(1\) 的点集,\(T\) 为已经被删除的点集。每次遇到 \(x \notin S \cup T\) 时,则令 \(\min S\) 连向 \(x\) ,然后删除 \(\min S\) 并将其加入 \(T\) ,而 \(x\) 可能会加入 \(S\) 也可能不会。
由于需要实时维护度数为 \(1\) 的点集,而点的度数是否为 \(1\) 仅与它是否出现在 Prufer 序列中有关,因此考虑从后往前 DP,这样当前的叶子集合就是所有未在 Prufer 序列中出现过的点集。
设 \(f_{i, j, S, T}\) 表示考虑了 \(i \sim m\) 的后缀 \(\mathrm{dist}(j, n)\) 的总和,其中 \(S\) 为当前度数为 \(1\) 的点集,\(T\) 为已经被删除的点集。由于 \(S \cap T = \emptyset\) ,时间复杂度 \(O(3^n nm)\) 。
记 \(A = S \cup T\) ,则 \(S\) 就是 \(A\) 的一段后缀加上最多一个新加的 \(x\) ,因此状态可以被优化到 \(O(2^n n^2)\) ,时间复杂度 \(O(2^n n^3 m)\) 。
可以发现由于只需要考虑 \(\mathrm{dist}(j, n)\) ,因此只需关心 \(j \to n\) 这条链上的情况。若新加的点不为 \(j\) ,则无需关心它具体是哪个点,因此只要记录 \(S\) 中的最小点是否为 \(j\) 即可,时间复杂度 \(O(2^n n^2 m)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 15, M = 2.5e3 + 7;
int a[M], p[1 << N][N], f[N][N][1 << N][2], g[N][N][1 << N][2];
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 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;
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i)
scanf("%d", a + i), --a[i];
for (int s = 0; s < (1 << n); ++s)
for (int i = 0, j = 0; i < n; ++i)
if (s >> i & 1)
p[s][j++] = i;
g[n - 2][n - 1][(1 << n) - 1][0] = 1;
for (int i = 0; i < n - 1; ++i)
f[n - 2][i][(1 << n) - 1][1] = g[n - 2][i][(1 << n) - 1][1] = 1;
for (int i = m; i; --i)
for (int j = 0; j < n - 2; ++j)
for (int s = 0; s < (1 << n); ++s)
if ((~s >> a[i] & 1) && j + 1 < __builtin_popcount(s))
for (int k = 0; k < n; ++k)
for (int l = 0; l <= 1; ++l) {
int x = (l ? a[i] : k);
g[j][k][s][l] = add(g[j][k][s][l], add(g[j + 1][x][s][x == p[s][j + 1]],
g[j + 1][x][s | (1 << a[i])][x == min(p[s][j + 1], a[i])]));
f[j][k][s][l] = add(f[j][k][s][l], add(f[j + 1][x][s][x == p[s][j + 1]],
f[j + 1][x][s | (1 << a[i])][x == min(p[s][j + 1], a[i])]));
if (l)
f[j][k][s][l] = add(f[j][k][s][l], add(g[j + 1][x][s][x == p[s][j + 1]],
g[j + 1][x][s | (1 << a[i])][x == min(p[s][j + 1], a[i])]));
}
int all = 1;
for (int i = 1; i <= n - 2; ++i)
all = 1ll * all * i % Mod * mi(m - i + 1, Mod - 2) % Mod;
for (int i = 0; i < n - 1; ++i) {
int ans = 0;
for (int s = 0; s < (1 << n); ++s)
ans = add(ans, f[0][i][s][i == p[s][0]]);
printf("%d ", 1ll * ans * all % Mod);
}
return 0;
}
Matrix-Tree 定理
规定:图都允许存在重边,但是不允许存在自环。
对于无向图:
-
定义度数矩阵 \(D(G)\) ,其中 \(D_{i,j}(G) = \begin{cases} d(i) & i = j \\ 0 & i \ne j \end{cases}\) 。
-
定义邻接矩阵 \(A\) ,其中 \(A_{i, j}(G) = A_{j, i}(G)\) 表示 \((i, j)\) 之间相连的边数。
-
定义 Laplace 矩阵(亦称 Kirchhoff 矩阵) \(L(G) = D(G) - A(G)\) 。
-
记图 \(G\) 的所有生成树个数为 \(t(G)\) 。
对于有向图:
- 定义出度矩阵 \(D^{out}(G)\) ,其中 \(D_{i,j}^{out}(G) = \begin{cases} d^{out}(i) & i = j \\ 0 & i \ne j \end{cases}\) 。
- 定义入度矩阵 \(D^{in}(G)\) ,其中 \(D_{i,j}^{in}(G) = \begin{cases} d^{in}(i) & i = j \\ 0 & i \ne j \end{cases}\) 。
- 定义邻接矩阵 \(A\) ,其中 \(A_{i, j}(G)\) 表示 \((i, j)\) 之间相连的边数。
- 定义出度 Laplace 矩阵 \(L^{out}(G) = D^{out}(G) - A(G)\) 。
- 定义入度 Laplace 矩阵 \(L^{in}(G) = D^{in}(G) - A(G)\) 。
- 记图 \(G\) 以 \(r\) 为根的所有根向树形图数量为 \(t^{root}(G, r)\) ,叶向树形图数量为 \(t^{leaf}(G, r)\) 。
无向图形式
对于任意 \(i\) ,都有:
其中 \(L(G) \begin{pmatrix} 1 & 2 & \cdots & i - 1 & i + 1 & \cdots & n \\ 1 & 2 & \cdots & i - 1 & i + 1 & \cdots & n \end{pmatrix}\) 表示矩阵 \(L(G)\) 的构成去掉第 \(i\) 行和第 \(i\) 列的子矩阵。
这也说明无向图的 Laplace 矩阵的所有 \(n - 1\) 阶主子式都相等。
有向图形式
对于任意的 \(i\) ,都有:
因此如果要统计一张图所有的根向/叶向树形图,只要枚举所有的根并求和即可。
边带权形式
以无向图为例,有向图类似。
记 \(w(i, j)\) 表示所有边 \((i, j)\) 的边权和,修改 Laplace 矩阵的定义为:
并记 \(L_i(G) = L(G) \begin{pmatrix} 1 & 2 & \cdots & i - 1 & i + 1 & \cdots & n \\ 1 & 2 & \cdots & i - 1 & i + 1 & \cdots & n \end{pmatrix}\) ,则 :
其中 \(\tau(G)\) 表示 \(G\) 的所有生成树构成的集合,\(E_T\) 表示 \(T\) 的边集。
即 Laplace 矩阵任意 \(n - 1\) 阶主子式等于每个生成树边权之积的和。
BEST 定理
无向图欧拉回路计数是 NP 问题,考虑有向欧拉图的情况。
设 \(G\) 是有向欧拉图,\(k\) 为任意点,则 \(G\) 不同的欧拉回路数量为:
这也说明对于有向欧拉图 \(G\) 的任意两个点 \(x, y\) ,均有 \(t^{root}(G, x) = t^{root}(G, y)\) 。
注意 BEST 定理统计的欧拉回路是无起点的,以 \(i\) 为起点的欧拉回路数量要乘上 \(d^{out}(i)\) 。
应用
P3317 [SDOI2014] 重建
有 \(n\) 个点,\((i, j)\) 之间有 \(p_{i, j} = p_{j, i} \in [0, 1]\) 的概率存在一条无向边,求图恰为一棵树的概率。
\(n \le 50\)
枚举每棵树 \(T\) ,则答案为:
后者使用矩阵树定理求即可。
但是有可能会出现分母 \(1 - p_{u, v}\) 为 \(0\) 的情况,此时需要将 \(1 -p_{u, v}\) 设为一个 \(< eps\) 的极小值。
时间复杂度 \(O(n^3)\) 。
#include <bits/stdc++.h>
using namespace std;
const double eps = 1e-9;
const int N = 5e1 + 7;
double g[N][N];
int n;
inline double Gauss(int n) {
double res = 1;
for (int i = 1; i <= n; ++i) {
int p = i;
while (p <= n && fabs(g[p][i]) < eps)
++p;
if (p > n)
return 0;
if (mxp != i)
swap(g[i], g[p]), res *= -1;
res *= g[i][i];
for (int j = i + 1; j <= n; ++j) {
double div = g[j][i] / g[i][i];
for (int k = i; k <= n; ++k)
g[j][k] -= g[i][k] * div;
}
}
return res;
}
signed main() {
scanf("%d", &n);
double mul = 1;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j) {
double x;
scanf("%lf", &x);
if (x < eps)
x = eps;
else if (1 - x < eps)
x = 1 - eps;
g[i][j] -= x / (1 - x), g[i][i] += x / (1 - x);
if (i < j)
mul *= 1 - x;
}
printf("%.9lf", Gauss(n - 1) * mul);
return 0;
}
P4336 [SHOI2016] 黑暗前的幻想乡
给出一张无向图(可能有重边),每条边有颜色 \(\in [1, n - 1]\) ,求选出 \(n - 1\) 条颜色互异的边构成一棵树的方案数。
\(n \le 17\)
考虑容斥,则问题转化为求颜色 \(\in S\) 的边组成的生成树数量,不难用 Matrix-Tree 定理求解。
时间复杂度 \(O(2^n n^3)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 19;
vector<pair<int, int> > e[N];
int g[N][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 int Gauss(int n) {
int res = 1;
for (int i = 1; i <= n; ++i) {
int p = i;
while (p <= n && !g[p][i])
++p;
if (p > n)
return 0;
if (p != i)
swap(g[i], g[p]), res = 1ll * res * (Mod - 1) % Mod;
res = 1ll * res * g[i][i] % Mod;
for (int j = i + 1; j <= n; ++j) {
int div = 1ll * g[j][i] * mi(g[i][i], Mod - 2) % Mod;
for (int k = i; k <= n; ++k)
g[j][k] = dec(g[j][k], 1ll * g[i][k] * div % Mod);
}
}
return res;
}
signed main() {
scanf("%d", &n);
for (int i = 0; i < n - 1; ++i) {
int k;
scanf("%d", &k);
e[i].resize(k);
for (auto &it : e[i])
scanf("%d%d", &it.first, &it.second);
}
int ans = 0;
for (int i = 0; i < 1 << (n - 1); ++i) {
memset(g, 0, sizeof(g));
for (int j = 0; j < n - 1; ++j)
if (i >> j & 1) {
for (auto it : e[j]) {
int u = it.first, v = it.second;
++g[u][u], ++g[v][v];
g[u][v] = dec(g[u][v], 1), g[v][u] = dec(g[v][u], 1);
}
}
ans = add(ans, 1ll * sgn(n - 1 - __builtin_popcount(i)) * Gauss(n - 1) % Mod);
}
printf("%d", ans);
return 0;
}
P4208 [JSOI2008] 最小生成树计数
给出一张图,求 MST 的数量 \(\bmod 31011\) 。
\(n \le 100\) ,\(m \le 1000\) ,相同权值的边不超过 \(10\) 条
考虑 MST 的两个性质:
- 对于所有 MST,每种权值的边出现次数相同。
- 对于所有 MST 的每一个边权 \(w\) ,加入边权为 \(w\) 的边后连通块的状态都是一样的。
考虑先用 Kruskal 求出原图的一个 MST,记为 \(T\) 。然后枚举 \(T\) 中的每一个边权 \(w\) ,求解边权 \(w\) 的边的选取方案数,最后用乘法原理合并即可。
这个方案数只要断掉边权为 \(w\) 的边,此时最多只会剩下 \(k \le 10\) 个连通块,求生成树数量即可。
时间复杂度 \(O(nk^3)\) ,注意模数不是质数。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 31011;
const int N = 1e2 + 7, M = 1e3 + 7;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + 1 + n, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu;
struct Edge {
int u, v, w;
} e[M];
vector<Edge> T;
vector<int> vec;
int g[N][N], bel[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 bool Kruskal() {
dsu.prework(n);
sort(e + 1, e + 1 + m, [](const Edge &x, const Edge &y) {
return x.w < y.w;
});
for (int i = 1; i <= m; ++i) {
int fx = dsu.find(e[i].u), fy = dsu.find(e[i].v);
if (fx == fy)
continue;
dsu.merge(fx, fy), T.emplace_back(e[i]);
if (vec.empty() || e[i].w != vec.back())
vec.emplace_back(e[i].w);
}
return T.size() == n - 1;
}
inline int Gauss(int n) {
int res = 1;
for (int i = 1; i <= n; ++i)
for (int j = i + 1; j <= n; ++j) {
while (g[i][i]) {
int div = g[j][i] / g[i][i];
for (int k = i; k <= n; ++k)
g[j][k] = dec(g[j][k], 1ll * g[i][k] * div % Mod);
swap(g[i], g[j]), res = 1ll * res * (Mod - 1) % Mod;
}
swap(g[i], g[j]), res = 1ll * res * (Mod - 1) % Mod;
}
for (int i = 1; i <= n; ++i)
res = 1ll * res * g[i][i] % Mod;
return res;
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i)
scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
if (!Kruskal())
return puts("0"), 0;
int ans = 1;
for (int w : vec) {
dsu.prework(n);
for (auto it : T)
if (it.w != w)
dsu.merge(it.u, it.v);
int tot = 0;
for (int i = 1; i <= n; ++i)
if (dsu.find(i) == i)
bel[i] = ++tot;
for (int i = 1; i <= n; ++i)
if (dsu.find(i) != i)
bel[i] = bel[dsu.find(i)];
memset(g, 0, sizeof(g));
for (int i = 1; i <= m; ++i)
if (e[i].w == w) {
int u = bel[e[i].u], v = bel[e[i].v];
if (u == v)
continue;
g[u][u] = add(g[u][u], 1), g[v][v] = add(g[v][v], 1);
g[u][v] = dec(g[u][v], 1), g[v][u] = dec(g[v][u], 1);
}
ans = 1ll * ans * Gauss(tot - 1) % Mod;
}
printf("%d", ans);
return 0;
}
CF917D Stranger Trees
对于一个 \(n\) 个点的无向完全图,给出一棵生成树 \(T\) ,对于每个 \(k \in [0, n - 1]\) ,求与 \(T\) 交集大小为 \(k\) 的生成树数量。
\(n \le 100\)
考虑 Matrix-Tree 定理的边带权形式,对于不在 \(T\) 中的边,边权定为 \(1\) ,否则边权定为 \(x\) 。则求出答案的多项式后 \(k\) 的答案即为 \(x^k\) 的系数。
直接行列式套多项式是不行的,考虑带入 \(n\) 个点值,最后拉插回来即可。
时间复杂度 \(O(n^4)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e2 + 7;
int g[N][N], f[N], ans[N];
bool a[N][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 Gauss(int n) {
int res = 1;
for (int i = 1; i <= n; ++i) {
int p = i;
while (p <= n && !g[p][i])
++p;
if (p > n)
return 0;
if (p != i)
swap(g[i], g[p]), res = 1ll * res * (Mod - 1) % Mod;
res = 1ll * res * g[i][i] % Mod;
for (int j = i + 1; j <= n; ++j) {
int div = 1ll * g[j][i] * mi(g[i][i], Mod - 2) % Mod;
for (int k = i; k <= n; ++k)
g[j][k] = dec(g[j][k], 1ll * g[i][k] * div % Mod);
}
}
return res;
}
inline void Lagrange(int *y, int n, int *f) {
static int g[N];
memset(g, 0, sizeof(int) * (n + 1));
g[0] = 1;
for (int i = 1; i <= n; ++i)
for (int j = i; ~j; --j)
g[j] = add(1ll * (Mod - i) * g[j] % Mod, g[j - 1]);
memset(f, 0, sizeof(int) * n);
for (int i = 1; i <= n; ++i) {
int mul = 1;
for (int j = 1; j <= n; ++j)
if (i != j)
mul = 1ll * mul * dec(i, j) % Mod;
mul = 1ll * y[i] * mi(mul, Mod - 2) % Mod;
for (int j = n - 1, res = g[n]; ~j; --j)
f[j] = add(f[j], 1ll * mul * res % Mod), res = add(g[j], 1ll * i * res % Mod);
}
}
signed main() {
scanf("%d", &n);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
a[u][v] = a[v][u] = true;
}
for (int i = 1; i <= n; ++i) {
memset(g, 0, sizeof(g));
for (int u = 1; u <= n; ++u)
for (int v = u + 1; v <= n; ++v) {
int k = a[u][v] ? i : 1;
g[u][u] = add(g[u][u], k), g[v][v] = add(g[v][v], k);
g[u][v] = dec(g[u][v], k), g[v][u] = dec(g[v][u], k);
}
f[i] = Gauss(n - 1);
}
Lagrange(f, n, ans);
for (int i = 0; i < n; ++i)
printf("%d ", ans[i]);
return 0;
}
P6624 [省选联考 2020 A 卷] 作业题
给出一张无向图,求:
\[\sum_T (\sum_{e_i \in T} w_{e_i}) \times (\gcd_{e_i \in T} w_{e_i}) \]其中 \(\sum_T\) 表示枚举所有生成树集合。
\(n \le 30\) ,\(w \le 152501\)
考虑欧拉反演:
考虑枚举 \(d\) ,然后计算后面的式子,即所有生成树的边权和。
暴力的枚举一条边,计算包含这条边的生成树数量。由于需要枚举 \(n^2\) 条边,时间复杂度过高。
考虑将所有的边一起处理,将边的权值赋为一次多项式 \(wx + 1\) ,则最终行列式的一次项即为所有生成树的边权和,带入 \(n\) 个点值最后拉插回来即可做到 \(O(n^4)\) 。
总时间复杂度 \(O(W n^4)\) ,无法通过。但是注意到事实上有用的 \(w\) 的上界为 \(O(\frac{n^2 d(W)}{n - 1}) = O(n d(W))\) ,并且这个上界很松,如此剪枝即可通过。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 1e2 + 7, V = 1.6e5 + 7;
struct Edge {
int u, v, w;
} e[N * N];
int pri[V], phi[V], g[N][N], f[N], h[N];
bool isp[V];
int n, m, pcnt;
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 void prework() {
memset(isp, true, sizeof(isp));
isp[1] = false, phi[1] = 1;
for (int i = 2; i < V; ++i) {
if (isp[i])
pri[++pcnt] = i, phi[i] = i - 1;
for (int j = 1; j <= pcnt && i * pri[j] < V; ++j) {
isp[i * pri[j]] = false;
if (i % pri[j])
phi[i * pri[j]] = phi[i] * phi[pri[j]];
else {
phi[i * pri[j]] = phi[i] * pri[j];
break;
}
}
}
}
inline int Gauss(int n) {
int res = 1;
for (int i = 1; i <= n; ++i) {
int p = i;
while (p <= n && !g[p][i])
++p;
if (p > n)
return 0;
if (p != i)
swap(g[i], g[p]), res = 1ll * res * (Mod - 1) % Mod;
res = 1ll * res * g[i][i] % Mod;
for (int j = i + 1; j <= n; ++j) {
int div = 1ll * g[j][i] * mi(g[i][i], Mod - 2) % Mod;
for (int k = i; k <= n; ++k)
g[j][k] = dec(g[j][k], 1ll * g[i][k] * div % Mod);
}
}
return res;
}
inline void Lagrange(int *y, int n, int *f) {
static int g[N];
memset(g, 0, sizeof(int) * (n + 1));
g[0] = 1;
for (int i = 1; i <= n; ++i)
for (int j = i; ~j; --j)
g[j] = add(1ll * (Mod - i) * g[j] % Mod, g[j - 1]);
memset(f, 0, sizeof(int) * n);
for (int i = 1; i <= n; ++i) {
int mul = 1;
for (int j = 1; j <= n; ++j)
if (i != j)
mul = 1ll * mul * dec(i, j) % Mod;
mul = 1ll * y[i] * mi(mul, Mod - 2) % Mod;
for (int j = n - 1, res = g[n]; ~j; --j)
f[j] = add(f[j], 1ll * mul * res % Mod), res = add(g[j], 1ll * i * res % Mod);
}
}
inline int solve(int d) {
int cnt = 0;
for (int i = 1; i <= m; ++i)
cnt += !(e[i].w % d);
if (cnt < n - 1)
return 0;
for (int i = 1; i <= n; ++i) {
memset(g, 0, sizeof(g));
for (int j = 1; j <= m; ++j) {
if (e[j].w % d)
continue;
int u = e[j].u, v = e[j].v, k = add(1ll * e[j].w * i % Mod, 1);
g[u][u] = add(g[u][u], k), g[v][v] = add(g[v][v], k);
g[u][v] = dec(g[u][v], k), g[v][u] = dec(g[v][u], k);
}
f[i] = Gauss(n - 1);
}
Lagrange(f, n, h);
return h[1];
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i)
scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
prework();
int ans = 0;
for (int i = 1; i < V; ++i)
ans = add(ans, 1ll * phi[i] * solve(i) % Mod);
printf("%d", ans);
return 0;
}
P5406 [THUPC 2019] 找树
对于 \(w\) 位二进制数,定义一种新的位运算 \(\operatorname{op}\) ,其中每一位的运算都是与、或、异或中的一中。
给出一张无向图,求一棵生成树,记边权为 \(w_{1 \sim n - 1}\) ,最大化 \(w_1 \operatorname{op} w_2 \operatorname{op} \cdots \operatorname{op} w_{n + 1}\) 。
\(n \le 70\) ,\(m \le 5000\) ,\(w \le 12\)
发现这个最优化没有什么性质,但是值域很小,考虑对每个答案判定是否能取到。但是只判定存在性还是比较困难,考虑统计每个答案的出现次数,这样就可以随便对一个大质数取模,方案数恰好为其倍数的概率可以忽略不计。
考虑 Matrix-Tree 定理,将每条边的权值设为 GF。但是直接对多项式矩阵求行列式是困难的,考虑 FWT 变换,这样每一位点值就是独立的,对每个点值求行列式,之后再 IFWT 变换回去即可。
由于 \(\operatorname{op}\) 对每一位仍然是独立的,因此判断一下该位是哪种运算,使用相对应的系数矩阵即可。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7, inv2 = (Mod + 1) / 2;
const int C[2][3][2][2] = {{{{Mod - 1, 1}, {1, 0}}, {{1, 0}, {Mod - 1, 1}}, {{inv2, inv2}, {inv2, Mod - inv2}}},
{{{0, 1}, {1, 1}}, {{1, 0}, {1, 1}}, {{1, 1}, {1, Mod - 1}}}};
const int N = 7e1 + 7, V = 1 << 12;
int a[N][N][V], g[N][N], ans[V];
char str[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 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 void FWT(int *f, int n, int op) {
for (int k = 1, d = 0; k < n; k <<= 1, ++d) {
int c[2][2];
memcpy(c, C[op == 1][(str[d] == '&' ? 0 : (str[d] == '|' ? 1 : 2))], sizeof(c));
for (int i = 0; i < n; i += k << 1)
for (int j = 0; j < k; ++j) {
int a[2] = {f[i + j], f[i + j + k]};
f[i + j] = add(1ll * c[0][0] * a[0] % Mod, 1ll * c[0][1] * a[1] % Mod);
f[i + j + k] = add(1ll * c[1][0] * a[0] % Mod, 1ll * c[1][1] * a[1] % Mod);
}
}
}
inline int Det(int n) {
int res = 1;
for (int i = 1; i <= n; ++i) {
int p = i;
while (p <= n && !g[p][i])
++p;
if (p > n)
return 0;
else if (p != i)
swap(g[i], g[p]), res = dec(0, res);
res = 1ll * res * g[i][i] % Mod;
for (int j = i + 1; j <= n; ++j) {
int div = 1ll * g[j][i] * mi(g[i][i], Mod - 2) % Mod;
for (int k = i; k <= n; ++k)
g[j][k] = dec(g[j][k], 1ll * g[i][k] * div % Mod);
}
}
return res;
}
signed main() {
scanf("%d%d%s", &n, &m, str);
while (m--) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
a[u][u][w] = add(a[u][u][w], 1), a[v][v][w] = add(a[v][v][w], 1);
a[u][v][w] = dec(a[u][v][w], 1), a[v][u][w] = dec(a[v][u][w], 1);
}
m = 1 << strlen(str);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
FWT(a[i][j], m, 1);
for (int i = 0; i < m; ++i) {
for (int j = 1; j <= n; ++j)
for (int k = 1; k <= n; ++k)
g[j][k] = a[j][k][i];
ans[i] = Det(n - 1);
}
FWT(ans, m, -1);
for (int i = m - 1; ~i; --i)
if (ans[i])
return printf("%d", i), 0;
puts("-1");
return 0;
}
P5296 [北京省选集训2019] 生成树计数
给定一棵树,定义一棵生成树的权值为边权和,求所有生成树权值的 \(k\) 次方和 \(\bmod 998244353\) ,其中定义 \(0^0 = 1\) 。
\(n, k \le 30\)
先考虑两条边的情况,不难发现:
因此考虑将边权设为 EGF \(e^{wx}\) ,这样乘积 \(x^k\) 项系数的 \(k!\) 倍即为原权值和的 \(k\) 次方。
由于数据范围很小,直接暴力卷积即可,运算时只要保留 \(x^k\) 及更低次的项,时间复杂度 \(O(n^3 k^2)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 3e1 + 7;
vector<int> g[N][N];
int fac[N], inv[N], invfac[N], a[N][N];
int n, k;
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 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 vector<int> Mul(vector<int> f, vector<int> g) {
vector<int> res(k + 1);
for (int i = 0; i <= k; ++i)
for (int j = 0; i + j <= k; ++j)
res[i + j] = add(res[i + j], 1ll * f[i] * g[j] % Mod);
return res;
}
inline vector<int> Inv(vector<int> f) {
vector<int> g(k + 1), res(k + 1);
res[0] = mi(f[0], Mod - 2);
for (int i = 1; i <= k; ++i)
g[i] = 1ll * f[i] * res[0] % Mod;
for (int i = 1; i <= k; ++i)
for (int j = 1; j <= i; ++j)
res[i] = dec(res[i], 1ll * g[j] * res[i - j] % Mod);
return res;
}
signed main() {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
scanf("%d", a[i] + j), g[i][j].resize(k + 1);
prework(k);
for (int i = 1; i <= n; ++i)
for (int j = 1; j < i; ++j)
for (int l = 0, pw = 1; l <= k; ++l, pw = 1ll * pw * a[i][j] % Mod) {
int val = 1ll * pw * invfac[l] % Mod;
g[i][j][l] = g[j][i][l] = dec(0, val);
g[i][i][l] = add(g[i][i][l], val), g[j][j][l] = add(g[j][j][l], val);
}
vector<int> ans(k + 1);
ans[0] = 1, --n;
for (int i = 1; i <= n; ++i) {
ans = Mul(ans, g[i][i]);
for (int j = i + 1; j <= n; ++j) {
auto div = Mul(g[j][i], Inv(g[i][i]));
for (int l = i; l <= n; ++l) {
auto res = Mul(g[i][l], div);
for (int p = 0; p <= k; ++p)
g[j][l][p] = dec(g[j][l][p], res[p]);
}
}
}
printf("%d", 1ll * ans[k] * fac[k] % Mod);
return 0;
}
AT_agc051_d [AGC051D] C4
给出一张四个点、四条边的无向图,并给定每条边的经过次数,求有多少条 \(1\) 开始、\(1\) 结束的回路满足限制。
经过次数 \(\le 5 \times 10^5\)
考虑枚举连接两个点的 \(n\) 条无向边拆为 \(i\) 和 \(n - i\) 条有向边的数量,然后对所有方案求和。
固定了一条边的分解后,剩下边的分解由度数关系可以求得。
注意:
- 由于每一条有向边之间是不做区分的,需要将其去重。
- 由于 BEST 定理是无起点的,因此需要乘上 \(\mathrm{deg}(1)\) 。
视求行列式的复杂度为常数,时间复杂度 \(O(V)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int V = 5e5 + 7;
int fac[V], inv[V], invfac[V];
int A, B, C, D;
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 void prework() {
fac[0] = fac[1] = 1;
inv[0] = inv[1] = 1;
invfac[0] = invfac[1] = 1;
for (int i = 2; i < V; ++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 Gauss(int g[5][5], int n) {
int res = 1;
for (int i = 1; i <= n; ++i) {
int p = i;
while (p <= n && !g[p][i])
++p;
if (p > n)
return 0;
if (p != i)
swap(g[i], g[p]), res = 1ll * res * (Mod - 1) % Mod;
res = 1ll * res * g[i][i] % Mod;
for (int j = i + 1; j <= n; ++j) {
int div = 1ll * g[j][i] * mi(g[i][i], Mod - 2) % Mod;
for (int k = i; k <= n; ++k)
g[j][k] = dec(g[j][k], 1ll * g[i][k] * div % Mod);
}
}
return res;
}
signed main() {
scanf("%d%d%d%d", &A, &B, &C, &D);
if (((A + B) & 1) || ((B + C) & 1) || ((C + D) & 1) || ((D + A) & 1))
return puts("0"), 0;
prework();
int ans = 0;
for (int a = 0; a <= A; ++a) {
int b = a + (B - A) / 2, c = b + (C - B) / 2, d = c + (D - C) / 2;
if (b < 0 || b > B || c < 0 || c > C || d < 0 || d > D)
continue;
int deg[5] = {0, a + D - d, b + A - a, c + B - b, d + C - c}, g[5][5];
if (!deg[1] || !deg[2] || !deg[3] || !deg[4])
continue;
memset(g, 0, sizeof(g));
g[1][1] = d + A - a, g[2][2] = a + B - b, g[3][3] = b + C - c, g[4][4] = c + D - d;
g[1][2] = dec(0, a), g[2][1] = dec(0, A - a);
g[2][3] = dec(0, b), g[3][2] = dec(0, B - b);
g[3][4] = dec(0, c), g[4][3] = dec(0, C - c);
g[4][1] = dec(0, d), g[1][4] = dec(0, D - d);
ans = add(ans, 1ll * Gauss(g, 3) * deg[1] % Mod *
fac[deg[1] - 1] % Mod * fac[deg[2] - 1] % Mod * fac[deg[3] - 1] % Mod * fac[deg[4] - 1] % Mod *
invfac[a] % Mod * invfac[b] % Mod * invfac[c] % Mod * invfac[d] % Mod *
invfac[A - a] % Mod * invfac[B - b] % Mod * invfac[C - c] % Mod * invfac[D - d] % Mod);
}
printf("%d", ans);
return 0;
}
P7531 [USACO21OPEN] Routing Schemes P
给出一张有向图,以及起点集合 \(S\) 和终点集合 \(T\) ,保证 \(|S| = |T|\) 且 \(S \cap T = \emptyset\) 。
一个路径方案由 \(|S|\) 条路径组成,每条路径的起点都属于 \(S\) ,终点都属于 \(T\) ,且任意两条路径的起点、终点均不同。
求有多少种路径方案使得每条有向边恰好被经过一次。
\(n \le 100\)
考虑新建一个虚点,虚点向每个起点连边,每个起点向虚点连边,则问题转化为欧拉回路数量。
用 BEST 求出以虚点为起点的欧拉回路数量,由于起点-终点的路径的访问顺序是无序的,因此需要除以 \(|S|!\) 。
时间复杂度 \(O(n^3)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e2 + 7;
int g[N][N], in[N], out[N], id[N];
int fac[N], inv[N], invfac[N];
char str[N], e[N][N];
int n, k;
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 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 Gauss(int n) {
int res = 1;
for (int i = 1; i <= n; ++i) {
int p = i;
while (p <= n && !g[p][i])
++p;
if (p > n)
return 0;
else if (p != i)
swap(g[i], g[p]), res = 1ll * res * (Mod - 1) % Mod;
res = 1ll * res * g[i][i] % Mod;
for (int j = i + 1; j <= n; ++j) {
int div = 1ll * g[j][i] * mi(g[i][i], Mod - 2) % Mod;
for (int k = i; k <= n; ++k)
g[j][k] = dec(g[j][k], 1ll * g[i][k] * div % Mod);
}
}
return res;
}
signed main() {
prework();
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d%s", &n, &k, str + 1);
memset(in, 0, sizeof(in)), memset(out, 0, sizeof(out));
for (int i = 1; i <= n; ++i) {
if (str[i] == 'S')
++out[n + 1], ++in[i];
else if (str[i] == 'R')
++out[i], ++in[n + 1];
}
for (int i = 1; i <= n; ++i) {
scanf("%s", e[i] + 1);
for (int j = 1; j <= n; ++j)
if (e[i][j] == '1')
++out[i], ++in[j];
}
bool flag = true;
for (int i = 1; i <= n + 1; ++i)
if (in[i] != out[i]) {
flag = false;
break;
}
if (!flag) {
puts("0");
continue;
}
memset(id, 0, sizeof(id));
int tot = 0;
for (int i = 1; i <= n; ++i)
if (out[i])
id[i] = ++tot;
memset(g, 0, sizeof(g));
auto insert = [](int u, int v) {
u = id[u], v = id[v];
g[u][u] = add(g[u][u], 1), g[u][v] = dec(g[u][v], 1);
};
for (int i = 1; i <= n; ++i) {
if (str[i] == 'S')
insert(n + 1, i);
else if (str[i] == 'R')
insert(i, n + 1);
}
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
if (e[i][j] == '1')
insert(i, j);
int ans = Gauss(tot);
for (int i = 1; i <= n + 1; ++i)
if (out[i])
ans = 1ll * ans * fac[out[i] - 1] % Mod;
printf("%d\n", 1ll * ans * out[n + 1] % Mod * invfac[out[n + 1]] % Mod);
}
return 0;
}
LGV 引理
LGV 引理常被用来处理 DAG 上不相交路径计数等问题。
定义:
- \(\omega(P)\) 表示 \(P\) 这条路径上所有边的边权之积。
- \(e(u, v)\) 表示 \(u\) 到 \(v\) 每一条路径的 \(\omega(P)\) 之和。
- \(\pi(\sigma)\) 表示排列 \(\sigma\) 的逆序对数量。
约定:
- \(A\subseteq V\) 为起点集合,\(B \subseteq V\) 为终点集合,其中 \(|A| = |B| = k\) 。
- 一组 \(A \to B\) 的不相交路径 \(S\) :\(S_i\) 是一条 \(A_i\) 到 \(B_{p_i}\) 的路径(\(p_{1 \sim k}\) 为排列),满足对于任意 \(i \ne j\) ,\(S_i\) 与 \(S_j\) 无公共点。
引理内容:
其中 \(\sum_{S : A \to B}\) 表示 \(A \to B\) 的每一组不相交路径 \(S\) ,\(\sigma(S)\) 表示对应关系(排列)。
LGV 引理说明 \(\det M\) 即为所有 \(A \to B\) 每一组不相交路径边权积的带符号和。
证明:由行列式定义可得:
\[\begin{align} \det(M) &= \sum_{\sigma} (-1)^{\pi(\sigma)} \prod_{i = 1}^n e(a_i, b_{\sigma(i)}) \\ &= \sum_{\sigma} (-1)^{\pi(\sigma)} \prod_{i = 1}^n \sum_{P : a_i \to b_{\sigma(i)}} \omega(P) \end{align} \]不难发现 \(\prod_{i = 1}^n \sum_{P : a_i \to b_{\sigma(i)}} \omega(P)\) 即为 \(A \to B\) 排列为 \(\sigma\) 的路径组 \(P\) 的 \(\omega(P)\) 之和(\(P\) 可能相交),因此:
\[\begin{align} & \sum_{\sigma} (-1)^{\pi(\sigma)} \prod_{i = 1}^n \sum_{P : a_i \to b_{\sigma(i)}} \omega(P) \\ =& \sum_{\sigma} (-1)^{\pi(\sigma)} \sum_{P = \sigma} \omega(P) \\ =& \sum_{P : A \to B} (-1)^{\pi(\sigma)} \prod_{i = 1}^n \omega(P_i) \end{align} \]设 \(U\) 为不交路径组,\(V\) 为相交路径组,则:
\[\begin{align} & \sum_{P : A \to B} (-1)^{\pi(\sigma)} \prod_{i = 1}^n \omega(P_i) \\ =& \left( \sum_{U : A \to B} (-1)^{\pi(\sigma)} \prod_{i = 1}^n \omega(U_i) \right) + \left( \sum_{V : A \to B} (-1)^{\pi(\sigma)} \prod_{i = 1}^n \omega(V_i) \right) \end{align} \]设 \(P\) 中存在一个相交路径组 \(P_i : a_1 \to u \to b_1, P_j : a_2 \to u \to b_2\) ,则必然存在和它相对的一个相交路径组 \(P'_i : a_1 \to u \to b_2, P'_j : a_2 \to u \to b_1\) ,其余路径不变,因此 \(\omega(P) = \omega(P')\) ,且 \(\pi(P) = \pi(P') \pm 1\) ,因此:
\[\sum_{V : A \to B} (-1)^{\pi(\sigma)} \prod_{i = 1}^n \omega(V_i) = 0 \]故:
\[\det M = \sum_{U : A \to B} (-1)^{\pi(\sigma)} \prod_{i = 1}^n \omega(U_i) \]
P6657 【模板】LGV 引理
有一个 \(n \times n\) 的棋盘,每个棋子一次只能向上或向右走一格。
现有 \(m\) 个棋子,第 \(i\) 个棋子一开始在 \((a_i, 1)\) ,最终要走到 \((b_i, n)\) 。
求不相交路径的方案数。
\(n \le 10^6\) ,\(m \le 100\) ,\(1 \le a_1 \le a_2 \le \cdots \le a_m \le n\) ,\(1 \le b_1 \le b_2 \le \cdots \le b_m \le n\)
由于 \(a_{1 \sim m}\) 和 \(b_{1 \sim m}\) 均不降,因此若对应排列关系 \(\sigma \ne \{ 1, 2, \cdots, m \}\) ,则路径必然相交,因此原问题可以弱化为任意匹配后不相交路径的方案数。
求路径方案数可以通过将所有边的边权设为 \(1\) ,这样路径权值乘积即为 \(1\) 。
此时 \(e(i, j)\) 即为 \((a_i, 1) \to (b_j, n)\) 路径的方案数,即 \(\binom{b_j - a_i + n - 1}{n - 1}\) 。
最后用高斯消元实现 LGV 引理即可。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 2e6 + 7, M = 1e2 + 7;
int fac[N], inv[N], invfac[N];
int a[M], b[M], g[M][M];
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 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 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;
}
inline int Gauss(int n) {
int res = 1;
for (int i = 1; i <= n; ++i) {
int p = i;
while (p <= n && !g[p][i])
++p;
if (p > n)
return 0;
else if (p != i)
swap(g[i], g[p]), res = 1ll * res * (Mod - 1) % Mod;
res = 1ll * res * g[i][i] % Mod;
for (int j = i + 1; j <= n; ++j) {
int div = 1ll * g[j][i] * mi(g[i][i], Mod - 2) % Mod;
for (int k = i; k <= n; ++k)
g[j][k] = dec(g[j][k], 1ll * g[i][k] * div % Mod);
}
}
return res;
}
signed main() {
prework();
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i)
scanf("%d%d", a + i, b + i);
for (int i = 1; i <= m; ++i)
for (int j = 1; j <= m; ++j)
g[i][j] = C(b[j] - a[i] + n - 1, n - 1);
printf("%d\n", Gauss(m));
}
return 0;
}
CF348D Turtles
有一个 \(n \times n\) 的棋盘,其中一些格子是障碍,每次只能向上或向右走一格,求 \((1, 1)\) 到 \((n, m)\) 的所有路径中选两条不相交路径的方案数。
\(n, m \le 3000\)
不难发现 \((1, 1) \to (n, m)\) 两条不相交路径一定起点为 \((1, 2)\) 和 \((2, 1)\) ,终点为 \((n - 1, m)\) 和 \((n, m - 1)\) ,因此对这四个点应用 LGV 引理即可。路径的方案数可以 DP 求得,时间复杂度 \(O(nm)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 3e3 + 7;
int f[N][N], g[3][3];
char str[N][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 solve() {
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
if (str[i][j] != '#')
f[i][j] = add(f[i][j], add(f[i - 1][j], f[i][j - 1]));
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%s", str[i] + 1);
if (str[1][2] == '#' || str[2][1] == '#' || str[n - 1][m] == '#' || str[n][m - 1] == '#')
return puts("0"), 0;
f[1][2] = 1, solve(), g[1][1] = f[n - 1][m], g[1][2] = f[n][m - 1];
memset(f, 0, sizeof(f));
f[2][1] = 1, solve(), g[2][1] = f[n - 1][m], g[2][2] = f[n][m - 1];
printf("%d", dec(1ll * g[1][1] * g[2][2] % Mod, 1ll * g[1][2] * g[2][1] % Mod));
return 0;
}
P7736 [NOI2021] 路径交点
给出一张 \(k\) 层分层图,第 \(i\) 层有 \(n_i\) 个点。其中 \(n_1 = n_k\) ,且对于 \(i \in [2, k - 1]\) 都有 \(n_i \in [n_1, 2n_1]\) 。
对于 \(i \in [1, k - 1]\) 层的点,以它们为起点的边只会连向 \(i + 1\) 层的点,第一层没有入边,第 \(k\) 层没有出边。
现需要选出 \(n_1\) 条第一层到第 \(k\) 层的点不交路径。对于两条路径 \(P, Q\) ,设它们在第 \((i, i + 1)\) 层的点为 \((P_i, P_{i + 1})\) 和 \((Q_i, Q_{i + 1})\) ,若 \((P_i - Q_i) \times (P_{i + 1} - Q_{i + 1}) < 0\) ,则 \(P, Q\) 在第 \((i, i + 1)\) 有交。
对于一组 \(n_1\) 条路径,称其交点数为两两不同路径间的交点数量和。
求有偶数个交点的路径方案数比有奇数个交点的路径方案数多的数量。
\(k, n_1 \le 100\)
不难发现偶数减去奇数可以转化为 \((-1)^{交点数量}\) 然后求带符号和。
对于一组对应关系排列 \((p_1, p_2, \cdots, p_k)\) ,其中 \(p_i\) 表示 \(n_{1, i} \to n_{k, p_i}\) 。对于其中的一对逆序对 \((i, j)\) ,它们的路径交点数量必然为奇数,否则交点数量必然为偶数。
因此一组路径的交点数量奇偶性与逆序对数量奇偶性相同,于是可以直接套用 LGV 引理。
最后考虑求 \(e(n_{1, i}, n_{k, j})\) ,即 \(n_{1, i} \to n_{k, j}\) 的路径数量,只要将相邻层之间的邻接矩阵做矩阵乘法即可。
时间复杂度 \(O(n^4)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 2e2 + 7;
int n[N], m[N], a[N][N][N], g[N][N];
int k;
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 Gauss(int n) {
int res = 1;
for (int i = 1; i <= n; ++i) {
int p = i;
while (p <= n && !g[p][i])
++p;
if (p > n)
return 0;
else if (p != i)
swap(g[i], g[p]), res = 1ll * res * (Mod - 1) % Mod;
res = 1ll * res * g[i][i] % Mod;
for (int j = i + 1; j <= n; ++j) {
int div = 1ll * g[j][i] * mi(g[i][i], Mod - 2) % Mod;
for (int k = i; k <= n; ++k)
g[j][k] = dec(g[j][k], 1ll * g[i][k] * div % Mod);
}
}
return res;
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d", &k);
for (int i = 1; i <= k; ++i)
scanf("%d", n + i);
for (int i = 1; i < k; ++i)
scanf("%d", m + i);
for (int i = 1; i < k; ++i) {
for (int j = 1; j <= n[i]; ++j)
memset(a[i][j] + 1, 0, sizeof(int) * n[i + 1]);
for (int j = 1; j <= m[i]; ++j) {
int u, v;
scanf("%d%d", &u, &v);
a[i][u][v] = 1;
}
}
memcpy(g, a[1], sizeof(g));
for (int i = 2; i < k; ++i) {
static int h[N][N];
memset(h, 0, sizeof(h));
for (int j = 1; j <= n[1]; ++j)
for (int k = 1; k <= n[i]; ++k)
for (int l = 1; l <= n[i + 1]; ++l)
h[j][l] = add(h[j][l], 1ll * g[j][k] * a[i][k][l] % Mod);
memcpy(g, h, sizeof(g));
}
printf("%d\n", Gauss(n[1]));
}
return 0;
}
图上结构计数
环
简单环
考虑状压 DP,设 \(f_{s, u}\) 表示满足当前经过结点集合为 \(s\) ,现在在 \(u\) ,且起点在 \(s\) 中编号最小的路径条数。
对于状态 \(f_{s, u}\) ,枚举下一个到达的点 \(v\) ,若 \(v\) 为 \(s\) 中编号最小的点(起点),则答案加上 \(f_{s, u}\) ,否则有转移 \(f_{s, u} \to f_{s \cup \{ v \}, v}\) 。
这样会把二元环(即重边)也算上,并且每个非二元环会被计算两次(因为固定起点可以向两个方向走),需要减去。
时间复杂度 \(O(2^n m)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 19;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
ll f[1 << N][N];
int n, m;
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1, u, v; i <= m; ++i) {
scanf("%d%d", &u, &v);
G.insert(u - 1, v - 1), G.insert(v - 1, u - 1);
}
ll ans = 0;
for (int i = 0; i < n; ++i)
f[1 << i][i] = 1;
for (int s = 1; s < (1 << n); ++s)
for (int u = 0; u < n; ++u) {
if (!f[s][u])
continue;
for (int v : G.e[u]) {
if ((s & -s) == (1 << v))
ans += f[s][u];
else if ((s & -s) < (1 << v) && (~s >> v & 1))
f[s | (1 << v)][v] += f[s][u];
}
}
printf("%lld", (ans - m) / 2);
return 0;
}
三元环
将每个点重标号为 \(p_{1 \sim n}\) ,满足 \(d(x) < d(y) \iff p_x < p_y\) ,其中 \(d\) 为度数。此时对于任一点 \(x\) ,\(x\) 出边 \((x, y)\) 中满足 \(p_y > p_x\) 的边至多只有 \(\sqrt{2m}\) 条。
证明:考虑反证法,若超过 \(\sqrt{2m}\) 条,则每条出边连到的点的度数均 \(> \sqrt{2m}\) ,因此总度数 \(> 2m\) ,矛盾。
考虑枚举每个点 \(u\) 作为三元环中 \(p\) 最大的点,将 \(u\) 的所有邻居打标记,再枚举 \(u\) 每个邻居 \(v\) 的邻居 \(w\) ,若 \(w\) 被打标记则三个点构成三元环。
因此只需保留 \(p_x < p_y\) 的边 \((x, y)\) 作为有向边,枚举每个点 \(u\) ,将所有 \(u\) 有出边的点都打上标记,再枚举所有打上标记的点 \(v\) 的所有出点 \(w\) ,若 \(w\) 被标记,那么 \((u, v, w)\) 就是一个三元环。
时间复杂度 \(O(m \sqrt{m})\) ,由此可得无向图三元环的数量上界是 \(O(m \sqrt{m})\) 的。
其他图的三元环:
- 有向图:转化为无向图三元环计数,找到三元环时判断方向即可。
- 竞赛图:容斥得到答案为 \(\binom{n}{3} - \sum_{i = 1}^n \binom{\deg^{out}_i}{2}\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7, M = 2e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
struct Edge {
int u, v;
} e[M];
int deg[N], tag[N];
int n, m;
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
scanf("%d%d", &e[i].u, &e[i].v);
++deg[e[i].u], ++deg[e[i].v];
}
auto cmp = [](const int &a, const int &b) {
return deg[a] == deg[b] ? a < b : deg[a] < deg[b];
};
for (int i = 1; i <= m; ++i) {
if (cmp(e[i].u, e[i].v))
G.insert(e[i].u, e[i].v);
else
G.insert(e[i].v, e[i].u);
}
int ans = 0;
for (int u = 1; u <= n; ++u) {
for (int v : G.e[u])
tag[v] = u;
for (int v : G.e[u])
for (int w : G.e[v])
if (tag[w] == u)
++ans;
}
printf("%d", ans);
return 0;
}
四元环
考虑对于一个四元环 \((a, b, c, d)\) ,若固定了 \(a, c\) ,则在所有与 \(a, c\) 均有连边的点中任取两个都可以成为四元环。
同样将每个点重标号为 \(p_{1 \sim n}\) ,满足 \(d(x) < d(y) \iff p_x < p_y\) 。
考虑枚举 \(p\) 最大的点 \(a\) ,再枚举 \(a\) 的出边中 \(p_b < p_a\) 的点 \(b_1, b_2\) ,若 \(b_1, b_2\) 同时连到了点 \(c\) 满足 \(p_c < p_a\) ,则这四个点可以连成一个四元环,因此只要对每个 \(c\) 统计 \(b\) 的数量即可。
时间复杂度 \(O(m \sqrt{m})\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G, nG;
int deg[N], cnt[N];
int n, m;
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1, u, v; i <= m; ++i) {
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
++deg[u], ++deg[v];
}
auto cmp = [](const int &a, const int &b) {
return deg[a] == deg[b] ? a < b : deg[a] < deg[b];
};
ll ans = 0;
for (int a = 1; a <= n; ++a) {
for (int b : G.e[a])
if (cmp(b, a)) {
for (int c : G.e[b])
if (cmp(c, a))
ans += cnt[c]++;
}
for (int b : G.e[a])
if (cmp(b, a)) {
for (int c : G.e[b])
if (cmp(c, a))
--cnt[c];
}
}
printf("%lld", ans);
return 0;
}
独立集/团
Meet-in-the-Middle
给定一张无向图,求最大团和最大独立集,需要给出一组方案以及方案数量。
\(n \le 50\)
团可以转化为补图的的独立集,下面讨论独立集的求解。
考虑 Meet-in-the-Middle,将点集均分为两部分,枚举右半部分的独立集,问题转化为求左半部分某个集合的最大独立集。
设 \(f_S\) 表示 \(S\) 的最大独立集,记 \(u = \min S\) ,\(E_u\) 为 \(u\) 的邻居集合:
- 若选 \(u\) ,则 \(f_S \gets f_{S \cap (U \setminus E_u \setminus \{ u \})} + 1\) 。
- 若不选 \(u\) ,则 \(f_S \gets f_{S \setminus \{ u \}}\) 。
时间复杂度 \(O(2^{\frac{n}{2}})\) ,该方法可以统计任意大小的独立集。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 51;
ll E[N];
int n, m;
inline void solve() {
int nl = n / 2, nr = n - nl;
vector<int> f(1 << nl), g(1 << nl);
g[0] = 1;
for (int s = 1; s < (1 << nl); ++s) {
int u = __builtin_ctz(s), t1 = s & ~(E[u] | (1 << u)), t2 = s ^ (1 << u);
f[s] = f[t1] | (1 << u), g[s] = g[t1];
int a = __builtin_popcountll(f[s]), b = __builtin_popcountll(f[t2]);
if (b > a)
f[s] = f[t2], g[s] = g[t2];
else if (b == a)
g[s] += g[t2];
}
vector<int> el(1 << nr), er(1 << nr);
el[0] = (1 << nl) - 1;
for (int s = 1; s < (1 << nr); ++s) {
int u = __builtin_ctz(s);
el[s] = el[s ^ (1 << u)] & ~E[u + nl], er[s] = er[s ^ (1 << u)] | (E[u + nl] >> nl);
}
pair<ll, int> ans = make_pair(0, 0);
for (int s = 0; s < (1 << nr); ++s) {
if (er[s] & s)
continue;
pair<ll, int> res = make_pair(((ll)s << nl) | f[el[s]], g[el[s]]);
int a = __builtin_popcountll(ans.first), b = __builtin_popcountll(res.first);
if (b > a)
ans = res;
else if (b == a)
ans.second += res.second;
}
printf("%d %d\n", __builtin_popcountll(ans.first), ans.second);
for (ll s = ans.first; s; s &= s - 1)
printf("%d ", __builtin_ctzll(s) + 1);
puts("");
}
signed main() {
scanf("%d%d", &n, &m);
ll all = (1ll << n) - 1;
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
--u, --v, E[u] |= 1ll << v, E[v] |= 1ll << u;
}
for (int i = 0; i < n; ++i)
E[i] = (~E[i] & all) ^ (1ll << i);
solve();
for (int i = 0; i < n; ++i)
E[i] = (~E[i] & all) ^ (1ll << i);
solve();
return 0;
}
搜索剪枝
给定一张无向图,求最大团和最大独立集,需要给出一组方案以及方案数量。
\(n \le 50\)
团可以转化为补图的的独立集,下面讨论独立集的求解。
若只统计最大独立集,可以考虑搜索,枚举度数最大的点是否在独立集里面,如果在就把其邻域内所有点都删掉,在所有点的度数均 \(\le 2\) 时直接计算,此时图形如若干条链和环,不难直接计算最大独立集。
时间复杂度 \(T(n) = T(n - 1) + T(n - 4) = O(n \times 1.3803^n)\) 。
#include <bits/stdc++.h>
#define ppc __builtin_popcountll
#define ctz __builtin_ctzll
typedef long long ll;
using namespace std;
const int N = 51;
ll E[N];
ll all;
int n, m;
pair<ll, int> dfs1(ll s) {
if (!s)
return make_pair(0, 1);
int u = -1;
for (ll x = s; x; x &= x - 1) {
int v = ctz(x);
if (u == -1 || ppc(E[v] & s) > ppc(E[u] & s))
u = v;
}
if (ppc(E[u] & s) <= 2) {
ll v = s, res = 0, t[2];
int cnt = 1;
bool circle;
function<void(int, bool)> dfs = [&](int x, bool op) {
v ^= 1ll << x, t[op] |= 1ll << x, circle &= (ppc(E[x] & s) == 2);
while (v & E[x])
dfs(ctz(v & E[x]), op ^ 1);
};
while (v) {
t[0] = t[1] = 0, circle = true;
dfs(ctz(v), 0);
int len = ppc(t[0]) + ppc(t[1]);
if (circle && (len & 1))
res |= (ppc(t[0]) < ppc(t[1]) ? t[0] : t[1]);
else
res |= (ppc(t[0]) > ppc(t[1]) ? t[0] : t[1]);
if (circle)
cnt *= (len & 1 ? len : 2);
else
cnt *= (len & 1 ? 1 : len / 2 + 1);
}
return make_pair(res, cnt);
}
auto res1 = dfs1(s ^ (1ll << u)), res2 = dfs1(s & ~(E[u] | (1ll << u)));
res2.first |= 1ll << u;
if (ppc(res1.first) > ppc(res2.first))
return res1;
else if (ppc(res1.first) < ppc(res2.first))
return res2;
else
return res1.second += res2.second, res1;
}
inline void solve() {
auto res = dfs1(all);
printf("%d %d\n", ppc(res.first), res.second);
for (ll x = res.first; x; x &= x - 1)
printf("%d ", ctz(x) + 1);
puts("");
}
signed main() {
scanf("%d%d", &n, &m), all = (1ll << n) - 1;
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
--u, --v, E[u] |= 1ll << v, E[v] |= 1ll << u;
}
for (int i = 0; i < n; ++i)
E[i] = (~E[i] & all) ^ (1ll << i);
solve();
for (int i = 0; i < n; ++i)
E[i] = (~E[i] & all) ^ (1ll << i);
solve();
return 0;
}
基于边数统计团的做法
给定一张无向图,求团的数量 \(\bmod (10^9 + 7)\) 。
\(n, m \le 1000\)
将每个点重标号为 \(p_{1 \sim n}\) ,满足 \(d(x) < d(y) \iff p_x < p_y\) 。此时每个点最多往右连 \(\sqrt{2m}\) 条边,因此每个点向右的出度被控制在一个比较小的范围,使用 Meet-in-the-Middle 可以在 \(O(2^{\frac{out_x}{2}})\) 的时间内求出点数为 \(out_x\) 的图的团数量。
由于所有点的出度之和为 \(m\) ,最坏情况下可以分成 \(O(\frac{m}{\sqrt{2m}}) = O(\sqrt{m})\) 个出度为 \(\sqrt{2m}\) 的点,因此时间复杂度为 \(O(\sqrt{m} \times 2^{\frac{\sqrt{2m}}{2}})\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e3 + 7;
int deg[N];
bool e[N][N];
int n, m;
inline bool cmp(const int &a, const int &b) {
return deg[a] == deg[b] ? a < b : deg[a] < deg[b];
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
--u, --v;
e[u][v] = e[v][u] = true, ++deg[u], ++deg[v];
}
vector<int> id(n);
iota(id.begin(), id.end(), 0), sort(id.begin(), id.end(), cmp);
int ans = 0;
for (int x : id) {
vector<int> vec;
for (int i = 0; i < n; ++i)
if (e[x][i] && cmp(x, i))
vec.emplace_back(i);
int siz = vec.size();
vector<ll> e2(siz);
for (int i = 0; i < siz; ++i)
for (int j = 0; j < siz; ++j)
if (e[vec[i]][vec[j]])
e2[i] |= 1ll << j;
int mid = siz / 2;
vector<int> f(1 << mid);
f[0] = 1;
for (int s = 1; s < (1 << mid); ++s) {
int u = __builtin_ctz(s);
f[s] += f[s ^ (1 << u)] + f[e2[u] & s];
}
vector<int> g(1 << (siz - mid)), h(1 << (siz - mid));
g[0] = 1, ans += f[h[0] = (1 << mid) - 1];
for (int s = 1; s < (1 << (siz - mid)); ++s) {
int u = __builtin_ctz(s), t = s ^ (1 << u);
g[s] = g[t] && ((e2[u + mid] >> mid & t) == t);
h[s] = h[t] & e2[u + mid];
if (g[s])
ans = (ans + f[h[s]]) % Mod;
}
}
printf("%d", ans);
return 0;
}
四元团
给定一张无向图,求四元团的数量。
\(n, m \le 10^5\)
将每个点重标号为 \(p_{1 \sim n}\) ,满足 \(d(x) < d(y) \iff p_x < p_y\) 。
枚举 \(u\) 作为 \(p\) 最小的点,问题转化为需要统计其至多 \(\sqrt{2m}\) 个出点 \(a_{1 \sim k}\) 中三元环的数量。
对于每个 \(a_i\) ,用 bitset 维护 \(a_{1 \sim k}\) 与其的连边关系,记为 \(b_i\) 。
枚举三元环中的任意一条边,用按位与操作求出两端点同时有连边的点的数量,即可求出这条边存在的三元环数量。
时间复杂度 \(O(m \sqrt{m} \times \frac{\sqrt{m}}{\omega}) = O(\frac{m^2}{\omega})\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7, B = 447;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
struct Edge {
int u, v;
} e[N];
bitset<B> f[B];
int deg[N], id[N];
int n, m;
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i)
scanf("%d%d", &e[i].u, &e[i].v), ++deg[e[i].u], ++deg[e[i].v];
auto cmp = [](const int &a, const int &b) {
return deg[a] == deg[b] ? a < b : deg[a] < deg[b];
};
for (int i = 1; i <= m; ++i) {
if (cmp(e[i].u, e[i].v))
G.insert(e[i].u, e[i].v);
else
G.insert(e[i].v, e[i].u);
}
ll ans = 0;
for (int u = 1; u <= n; ++u) {
int tot = 0;
for (int v : G.e[u])
id[v] = ++tot;
for (int v : G.e[u])
for (int w : G.e[v])
if (id[w])
f[id[v]].set(id[w]);
for (int v : G.e[u])
for (int w : G.e[v])
if (id[w])
ans += (f[id[v]] & f[id[w]]).count();
for (int v : G.e[u])
f[id[v]].reset(), id[v] = 0;
}
printf("%lld", ans);
return 0;
}
应用
CF985G Team Players
给定一张无向图,若一个三元组 \((x, y, z) (x < y < z)\) 两两没有边相连,那么它的贡献为 \(Ax + By + Cz\) ,求所有三元组的贡献和 \(\bmod 2^{64}\) 。
\(n, m \le 2 \times 10^5\)
考虑容斥,以下讨论时钦定节点编号为 \(1 \sim n\) 。
-
所有三元组:枚举 \(i \in [1, n]\) ,分类讨论 \(i\) 在三元组的位置:
- \(x = i\) :则 \(y, z > x\) 即可,贡献为 \((i - 1) \times A \times \frac{(n - i) (n - i - 1)}{2}\) 。
- \(y = i\) :则 \(x \in [1, y), z \in (y, n]\) ,贡献为 \((i - 1) \times B \times (n - i)(i - 1)\) 。
- \(z = i\) :此时 \(x, y < z\) ,贡献为 \((i - 1) \times C \times \frac{(i - 1)(i - 2)}{2}\) 。
-
至少有一条边相连:枚举所有边 \((u, v)\) ,不妨设 \(u < v\) ,分类讨论 \(w\) 与 \(u, v\) 的大小关系:
- \(u\) 的贡献:
- \(x = u\) :此时 \(w > x\) ,贡献为 \((u - 1) \times A \times (n - u - 1)\) 。
- \(y = u\) :此时 \(w < x\) ,贡献为 \((u - 1) \times B \times (u - 1)\) 。
- \(v\) 的贡献:
- \(y = v\) :此时 \(w > y\) ,贡献为 \((v - 1) \times B \times (n - v)\) 。
- \(x = v\) :此时 \(w < y\) ,贡献为 \((v - 1) \times C \times (v - 2)\) 。
- \(w\) 的贡献:
- \(x = w\) :此时 \(w \in [1, u)\) ,贡献为 \(A \times \frac{(u - 1)(u - 2)}{2}\) 。
- \(y = w\) :此时 \(w \in (u, v)\) ,贡献为 \(B \times \frac{(u + v - 2)(v - u - 1)}{2}\) 。
- \(z = w\) :此时 \(w \in (v, n]\) ,贡献为 \(C \times \frac{(n + v - 1)(n - v)}{2}\) 。
- \(u\) 的贡献:
-
至少有两条边相连:考虑枚举每个点的出边,先将出边按照到达节点的编号排序。设当前枚举的点为 \(u\) ,到达的点为 \(v\) ,在 \(u\) 的所有出点中排名为 \(rk\) ,分类讨论:
- \(v\) 的贡献:
- \(v < u\) :
- 若 \(w < v\) :贡献为 \((v - 1) \times A \times (deg_u - rk - 1)\)
- 若 \(w > v\) :贡献为 \((v - 1) \times B \times (rk - 1)\)
- \(v > u\)
- 若 \(w < v\) :贡献为 \((v - 1) \times C \times (rk - 2)\)
- 若 \(w > v\) :贡献为 \((v - 1) \times B \times (deg_u - rk)\)
- \(v < u\) :
- \(u\) 的贡献:
- \(x = u\) :贡献为 \((u - 1) \times A \times \frac{(deg_u - rk)(deg_u - rk - 1)}{2}\)
- \(y = u\) :贡献为 \((u - 1) \times B \times (deg_u - rk)(rk - 1)\)
- \(z = u\) :贡献为 \((u - 1) \times C \times \frac{(rk - 1)(rk - 2)}{2}\)
- \(v\) 的贡献:
-
至少有三条边相连:套三元环板子即可。
#include <bits/stdc++.h>
typedef unsigned long long ull;
using namespace std;
const int N = 2e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G, nG;
struct Edge {
int u, v;
} e[N];
int deg[N], tag[N];
ull A, B, C;
int n, m;
inline ull solve0() {
ull res = 0;
for (int i = 1; i <= n; ++i) {
res += A * (n - i) * (n - i - 1) / 2 * (i - 1);
res += B * (i - 1) * (n - i) * (i - 1);
res += C * (i - 1) * (i - 2) / 2 * (i - 1);
}
return res;
}
inline ull solve1() {
ull res = 0;
for (int i = 1; i <= m; ++i) {
int u = e[i].u, v = e[i].v;
if (u > v)
swap(u, v);
res += A * (u - 1) * (n - u - 1) + B * (u - 1) * (u - 1);
res += B * (v - 1) * (n - v) + C * (v - 1) * (v - 2);
res += A * (u - 1) * (u - 2) / 2 + B * (u + v - 2) * (v - u - 1) / 2 + C * (n + v - 1) * (n - v) / 2;
}
return res;
}
inline ull solve2() {
ull res = 0;
for (int u = 1; u <= n; ++u) {
G.e[u].emplace_back(u), sort(G.e[u].begin(), G.e[u].end());
for (int i = 0; i < G.e[u].size(); ++i) {
int v = G.e[u][i], rk = i + 1;
if (v != u) {
if (v < u)
res += A * (v - 1) * (deg[u] - rk) + B * (v - 1) * i;
else
res += B * (v - 1) * (deg[u] - rk + 1) + C * (v - 1) * (rk - 2);
} else {
res += A * (u - 1) * (deg[u] - rk + 1) * (deg[u] - rk) / 2;
res += B * (u - 1) * (deg[u] - rk + 1) * (rk - 1);
res += C * (u - 1) * (rk - 2) * (rk - 1) / 2;
}
}
}
return res;
}
inline ull solve3() {
ull res = 0;
for (int u = 1; u <= n; ++u) {
for (int v : nG.e[u])
tag[v] = u;
for (int v : nG.e[u])
for (int w : nG.e[v])
if (tag[w] == u) {
int tmp[3] = {u - 1, v - 1, w - 1};
sort(tmp, tmp + 3), res += A * tmp[0] + B * tmp[1] + C * tmp[2];
}
}
return res;
}
signed main() {
scanf("%d%d%llu%llu%llu", &n, &m, &A, &B, &C);
for (int i = 1; i <= m; ++i) {
scanf("%d%d", &e[i].u, &e[i].v);
++deg[++e[i].u], ++deg[++e[i].v];
}
for (int i = 1; i <= m; ++i) {
int u = e[i].u, v = e[i].v;
G.insert(u, v), G.insert(v, u);
if (deg[u] == deg[v] ? u > v : deg[u] > deg[v])
nG.insert(u, v);
else
nG.insert(v, u);
}
printf("%llu", solve0() - solve1() + solve2() - solve3());
return 0;
}
QOJ 6441. Ancient Magic Circle in Teyvat
给出一张无向图,求四元团的数量与四元独立集的数量之差,取绝对值。
\(n, m \le 10^5\)
考虑容斥,记 \(U\) 为所有四元组,\(E_{1 \sim 6}\) 表示第 \(i\) 条边是否存在,则:
因此:
将有边标为红色,无限制标为蓝色,则右式只会有十种情况:

分别计算即可,需要用到三元环、四元环计数,时间复杂度 \(O(m \sqrt{m})\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7, M = 2e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
struct Graph2 {
vector<pair<int, int> > e[N];
inline void insert(int u, int v, int w) {
e[u].emplace_back(v, w);
}
} nG;
struct Edge {
int u, v;
} e[M];
int deg[N], tag[N], dc3[N], ec3[M], cnt[N];
int n, m;
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++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);
++deg[e[i].u], ++deg[e[i].v];
}
auto cmp = [](const int &a, const int &b) {
return deg[a] == deg[b] ? a < b : deg[a] < deg[b];
};
for (int i = 1; i <= m; ++i) {
if (cmp(e[i].u, e[i].v))
nG.insert(e[i].u, e[i].v, i);
else
nG.insert(e[i].v, e[i].u, i);
}
ll ans = (long double)n * (n - 1) * (n - 2) * (n - 3) / 24; // case 1
ans -= 1ll * m * (n - 2) * (n - 3) / 2; // case 2
for (int i = 1; i <= n; ++i) {
ans += 1ll * deg[i] * (deg[i] - 1) / 2 * (n - 3); // case 3
ans -= 1ll * deg[i] * (deg[i] - 1) * (deg[i] - 2) / 6; // case 5
}
ll res = 0;
for (int i = 1; i <= m; ++i) {
int u = e[i].u, v = e[i].v;
res += m - deg[u] - deg[v] + 1;
ans -= 1ll * (deg[u] - 1) * (deg[v] - 1); // case 6
}
ans += res / 2; // case 4
for (int u = 1; u <= n; ++u) {
for (auto it : nG.e[u])
tag[it.first] = it.second;
for (auto itv : nG.e[u]) {
int v = itv.first, idv = itv.second;
for (auto itw : nG.e[v]) {
int w = itw.first, idw = itw.second;
if (tag[w]) {
++dc3[u], ++dc3[v], ++dc3[w];
++ec3[idv], ++ec3[idw], ++ec3[tag[w]];
ans -= n - 6; // case 7
}
}
}
for (auto it : nG.e[u])
tag[it.first] = 0;
}
for (int i = 1; i <= n; ++i)
ans += 1ll * dc3[i] * (deg[i] - 2); // case 8
for (int a = 1; a <= n; ++a) {
for (int b : G.e[a])
if (cmp(b, a)) {
for (int c : G.e[b])
if (cmp(c, a))
ans += cnt[c]++; // case 9
}
for (int b : G.e[a])
if (cmp(b, a)) {
for (int c : G.e[b])
cnt[c] = 0;
}
}
for (int i = 1; i <= m; ++i)
ans -= 1ll * ec3[i] * (ec3[i] - 1) / 2;
printf("%lld", abs(ans));
return 0;
}
特殊图计数
DAG
P6846 [CEOI 2019] Amusement Park
给出一张有向图,可以改变一些边的方向,需要使其变为一张 DAG。
求所有变成 DAG 的变向方案的变向边数量和,答案对 \(998244353\) 取模。
\(n \le 18\) ,\(m \le \frac{n(n - 1)}{2}\)
首先可以发现,若用 \(x\) 次变向操作变为 DAG,则操作剩下的 \(m - x\) 条边即可得到反图 DAG。因此考虑将这些 DAG 匹配,问题转化为给无向边定向后对 DAG 计数,最后将答案乘上 \(\frac{m}{2}\) 即可。
考虑每次删掉所有零入度点,但是并不好恰好删去所有零入度点。于是考虑容斥,钦定有 \(i\) 个点是零度,则要求这 \(i\) 个点之间没有边。
设 \(f_S\) 表示 \(S\) 为 DAG 的方案数,则有:
其中 \(e(T)\) 表示 \(T\) 的出边点集,时间复杂度 \(O(3^n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353, inv2 = (Mod + 1) / 2;
const int N = 18;
int e[1 << N], f[1 << 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;
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
--u, --v;
e[1 << u] |= 1 << v, e[1 << v] |= 1 << u;
}
for (int i = 1; i < (1 << n); ++i)
e[i] = e[i ^ (i & -i)] | e[i & -i];
f[0] = 1;
for (int s = 1; s < (1 << n); ++s)
for (int t = s; t; t = (t - 1) & s)
if (!(e[t] & t))
f[s] = add(f[s], 1ll * sgn(__builtin_popcount(t) - 1) * f[s ^ t] % Mod);
printf("%d", 1ll * f[(1 << n) - 1] * m % Mod * inv2 % Mod);
return 0;
}
[ABC306Ex] Balance Scale
有 \(n\) 个数,给出 \(m\) 个二元组 \((u_{1 \sim m}, v_{1 \sim m})\) ,可以给每个数附上权值,记为权值序列 \(S\) 。
定义 \(f(S)\) 为长度为 \(m\) 的字符串,其中第 \(i\) 个字符表示 \(u_i, v_i\) 的大小关系
<、=、>。求有多少种不同的 \(f(S)\) 。\(n \le 17\) ,\(m \le \frac{n(n - 1)}{2}\)
考虑对最终状态 \(f(S)\) 计数。若只有 < 和 > ,则判定其合法当且仅当将边定向后存在一个合法拓扑序,不难 \(O(3^n)\) 求出无向边定向的 DAG 计数。
有 = 的情况也是类似的,钦定选出的零度点若连通则将它们合并为一个点,则容斥系数 \(-1\) 的指数部分变为连通块的数量 \(-1\) 。
时间复杂度 \(O(3^n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 19;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu;
int e[1 << N], f[1 << N], g[1 << 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;
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
--u, --v;
e[1 << u] |= 1 << v, e[1 << v] |= 1 << u;
}
for (int i = 1; i < (1 << n); ++i)
e[i] = e[i ^ (i & -i)] | e[i & -i];
for (int s = 1; s < (1 << n); ++s) {
dsu.prework(n);
for (int i = 0; i < n; ++i)
if (s >> i & 1) {
for (int j = 0; j < n; ++j)
if ((s & e[1 << i]) >> j & 1)
dsu.merge(i + 1, j + 1);
}
for (int i = 0; i < n; ++i)
if ((s >> i & 1) && dsu.find(i + 1) == i + 1)
++g[s];
}
f[0] = 1;
for (int s = 1; s < (1 << n); ++s)
for (int t = s; t; t = (t - 1) & s)
f[s] = add(f[s], 1ll * sgn(g[t] - 1) * f[s ^ t] % Mod);
printf("%d", f[(1 << n) - 1]);
return 0;
}
连通图
[ABC213G] Connectivity 2
给出一张无向图,对于 \(k \in [2, n]\) ,求有多少种保留边的方案使得 \(k\) 和 \(1\) 连通。
\(n \le 17\) ,\(m \le \frac{n (n - 1)}{2}\)
设 \(f_S\) 表示 \(1\) 能到达集合 \(S\) 的方案数,\(e(S)\) 表示 \(S\) 导出子图的边数。
考虑补集转化,将 \(S\) 划分为两部分 \(T, S \setminus T\) ,则要求两部分之间不存在边,因此:
时间复杂度 \(O(3^n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 17;
int f[1 << N], g[1 << N], pw[N], e[N], ans[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;
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
e[u - 1] |= 1 << (v - 1);
}
pw[0] = 1;
for (int i = 1; i <= n; ++i)
pw[i] = 2ll * pw[i - 1] % Mod;
for (int s = 0; s < (1 << n); ++s) {
g[s] = 1;
for (int i = 0; i < n; ++i)
if (s >> i & 1)
g[s] = 1ll * g[s] * pw[__builtin_popcount(e[i] & s)] % Mod;
}
f[0] = 1;
for (int s = 1; s < (1 << n); ++s) {
if (~s & 1)
continue;
f[s] = g[s];
for (int t = (s - 1) & s; t; t = (t - 1) & s)
f[s] = dec(f[s], 1ll * f[t] * g[s ^ t] % Mod);
for (int i = 1; i < n; ++i)
if (s >> i & 1)
ans[i] = add(ans[i], 1ll * f[s] * g[((1 << n) - 1) ^ s] % Mod);
}
for (int i = 1; i < n; ++i)
printf("%d\n", ans[i]);
return 0;
}
P4841 [集训队作业2013] 城市规划
求 \(n\) 个点的有标号连通图数量。
\(n \le 1.3 \times 10^5\)
设 \(f_n\) 表示 \(n\) 个点的无向连通图数量,\(g_n\) 为 \(n\) 个点的无向图数量。枚举 \(1\) 号点所在连通块大小,不难得到:
变形得到:
不难发现后者是一个卷积的形式,因此可以用多项式求逆求出 \(f\) ,时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1004535809, rt = 3, invrt = 334845270;
const int N = 5e5 + 7;
int fac[N], inv[N], invfac[N];
int f[N], g[N], h[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 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;
}
}
namespace Poly {
#define cpy(f, g, n) memcpy(f, g, sizeof(int) * (n))
#define clr(f, n) memset(f, 0, sizeof(int) * (n))
int rev[N];
inline int calc(int n) {
int len = 1;
while (len < n)
len <<= 1;
for (int i = 0; i < len; ++i)
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) ? (len >> 1) : 0);
return 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 Mul(int *f, int n, int *g, int m, int *res) {
static int a[N], b[N];
int len = calc(n + m - 1);
cpy(a, f, n), clr(a + n, len - n);
cpy(b, g, m), clr(b + m, len - m);
NTT(a, len, 1), NTT(b, len, 1);
for (int i = 0; i < len; ++i)
a[i] = 1ll * a[i] * b[i] % Mod;
NTT(a, len, -1), cpy(res, a, n + m - 1);
}
inline void Inv(int *f, int n, int *res) {
static int a[N], b[N], c[N];
c[0] = mi(f[0], Mod - 2);
for (int len = 1; len < (n << 1); len <<= 1) {
cpy(a, f, len), clr(a + len, len);
cpy(b, c, len), clr(b + len, len);
calc(len << 1), NTT(a, len << 1, 1), NTT(b, len << 1, 1);
for (int i = 0; i < (len << 1); ++i)
c[i] = 1ll * dec(2, 1ll * a[i] * b[i] % Mod) * b[i] % Mod;
NTT(c, len << 1, -1), clr(c + len, len);
}
cpy(res, c, n);
}
#undef cpy
#undef clr
} // namespace Poly
signed main() {
scanf("%d", &n);
prework(n);
for (int i = 1; i <= n; ++i)
h[i] = 1ll * mi(2, 1ll * i * (i - 1) / 2 % (Mod - 1)) * invfac[i - 1] % Mod;
for (int i = 0; i <= n; ++i)
g[i] = 1ll * mi(2, 1ll * i * (i - 1) / 2 % (Mod - 1)) * invfac[i] % Mod;
Poly::Inv(g, n + 1, g), Poly::Mul(g, n + 1, h, n + 1, f);
printf("%d", 1ll * f[n] * fac[n - 1] % Mod);
return 0;
}
也可以用生成函数的理论得到两个 EGF 的关系: \(e^{F(x)} = G(x)\) 。由于 \(g_i = 2^{\binom{i}{2}} \) ,于是可以直接做多项式 \(\ln\) 做到 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1004535809, rt = 3, invrt = 334845270;
const int N = 5e5 + 7;
int fac[N], inv[N], invfac[N];
int f[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 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;
}
}
namespace Poly {
#define cpy(f, g, n) memcpy(f, g, sizeof(int) * (n))
#define clr(f, n) memset(f, 0, sizeof(int) * (n))
int rev[N];
inline int calc(int n) {
int len = 1;
while (len < n)
len <<= 1;
for (int i = 0; i < len; ++i)
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) ? (len >> 1) : 0);
return 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 Mul(int *f, int n, int *g, int m, int *res) {
static int a[N], b[N];
int len = calc(n + m - 1);
cpy(a, f, n), clr(a + n, len - n);
cpy(b, g, m), clr(b + m, len - m);
NTT(a, len, 1), NTT(b, len, 1);
for (int i = 0; i < len; ++i)
a[i] = 1ll * a[i] * b[i] % Mod;
NTT(a, len, -1), cpy(res, a, n + m - 1);
}
inline void Inv(int *f, int n, int *res) {
static int a[N], b[N], c[N];
c[0] = mi(f[0], Mod - 2);
for (int len = 1; len < (n << 1); len <<= 1) {
cpy(a, f, len), clr(a + len, len);
cpy(b, c, len), clr(b + len, len);
calc(len << 1), NTT(a, len << 1, 1), NTT(b, len << 1, 1);
for (int i = 0; i < (len << 1); ++i)
c[i] = 1ll * dec(2, 1ll * a[i] * b[i] % Mod) * b[i] % Mod;
NTT(c, len << 1, -1), clr(c + len, len);
}
cpy(res, c, n);
}
inline void Derivative(int *f, int n) {
for (int i = 1; i < n; ++i)
f[i - 1] = 1ll * f[i] * i % Mod;
f[n - 1] = 0;
}
inline void Intergral(int *f, int n) {
for (int i = n; i; --i)
f[i] = 1ll * f[i - 1] * inv[i] % Mod;
f[0] = 0;
}
inline void Ln(int *f, int n, int *res) { // f[0] = 1
static int a[N], b[N];
cpy(a, f, n), Derivative(a, n);
cpy(b, f, n), Inv(b, n, b);
Mul(a, n, b, n, a), Intergral(a, n), cpy(res, a, n);
}
#undef cpy
#undef clr
} // namespace Poly
signed main() {
scanf("%d", &n);
prework(n);
for (int i = 0; i <= n; ++i)
f[i] = 1ll * mi(2, 1ll * i * (i - 1) / 2 % (Mod - 1)) * invfac[i] % Mod;
Poly::Ln(f, n + 1, f);
printf("%d", 1ll * f[n] * fac[n] % Mod);
return 0;
}
欧拉图
欧拉图计数
求 \(n\) 个点的有标号简单连通无向欧拉图的数量。
先考虑求出 \(n\) 个点满足无奇度点的有标号简单无向图数量,考虑 \(1 \sim n - 1\) 的点随便连,最后用 \(n\) 调节奇偶性,方案数即为 \(g_n = 2^{\binom{n - 1}{2}}\) 。
再考虑连通,考虑补集转化,枚举 \(1\) 所在连通块大小,则
直接递推可以做到 \(O(n^2)\) 。
也可以用生成函数的理论得到两个 EGF 的关系: \(e^{F(x)} = G(x)\) ,直接求多项式 \(\ln\) 可以做到 \(O(n \log n)\) 。
非连通欧拉子图计数
给出一个无向图,求有多少中边的选取方案满足不存在奇度点。
先考虑连通图的情况,考虑求出其一棵生成树,则对于不在生成树内的边可以任意选取,最后再用生成树上的边调整。
具体地,一开始每条树边都不选,若选了一条非树边,则将与其组成环的树边的选取情况反转即可。答案即为 \(2^{m - n + 1}\) 。
对于不连通的无向图,因为每个连通分量独立,于是直接乘法原理计算即可。记连通分量数为 \(c\) ,答案即为 \(2^{m - n + c}\) 。
二分图
[ARC105F] Lights Out on Connected Graph
给出一张无向图,求有多少种保留边的方案使得图为连通二分图。
\(n \le 17\) ,\(m \le \frac{n (n - 1)}{2}\)
设 \(g_S\) 表示点集 \(S\) 二分染色图(先对每个点黑白染色,再连边成二分图)的方案数(不一定连通),\(e(S)\) 表示 \(S\) 导出子图的边数。
\(g\) 的转移就枚举一个子集为黑色,其余点染白色,即:
设 \(f_S\) 表示点集 \(S\) 二分染色连通图的方案数,枚举一个点集与 \(\min S\) 连通,其余不连通,则:
考虑二分染色图和二分图的区别,对于每个连通分量,其都有两种染色方式,因此答案即为 \(\frac{f_U}{2}\) ,时间复杂度 \(O(3^n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353, inv2 = (Mod + 1) / 2;
const int N = 17;
int f[1 << N], g[1 << N], cnt[1 << N], pw[N * N], e[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;
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
e[u - 1] |= 1 << (v - 1);
}
pw[0] = 1;
for (int i = 1; i <= m; ++i)
pw[i] = 2ll * pw[i - 1] % Mod;
for (int s = 0; s < (1 << n); ++s)
for (int i = 0; i < n; ++i)
if (s >> i & 1)
cnt[s] += __builtin_popcount(e[i] & s);
for (int s = 1; s < (1 << n); ++s) {
g[s] = 1;
for (int t = s; t; t = (t - 1) & s)
g[s] = add(g[s], pw[cnt[s] - cnt[t] - cnt[s ^ t]]);
}
for (int s = 1; s < (1 << n); ++s) {
f[s] = g[s];
for (int t = s; t; t = (t - 1) & s)
if (t & (s & -s))
f[s] = dec(f[s], 1ll * f[t] * g[s ^ t] % Mod);
}
printf("%d", 1ll * f[(1 << n) - 1] * inv2 % Mod);
return 0;
}
P7364 有标号二分图计数
对于值域范围内的所有 \(n\) ,求 \(n\) 个点的有标号连通二分图数目。
\(n \le 10^5\)
设有标号二分图的 EGF 为 \(F(x)\) ,二分染色图的 EGF 为 \(G(x)\) ,则:
不难用 \(i j = \binom{i + j}{2} - \binom{i}{2} - \binom{j}{2}\) 化式子卷积求得。
考虑 \(F(x)\) 与 \(G(x)\) 的关系,对于每一个连通块,确定了最小点的颜色即可确定整个连通块的点的颜色。因此对于一张 \(i\) 个连通块的图,其会被计算 \(2^i\) 次。
考虑钦定若干连通块的最小点为黑色,其余为白色,则得到:
不难用多项式开根求得 \(F(x)\) ,时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353, rt = 3, invrt = (Mod + 1) / 3;
const int N = 1e6 + 7;
int fac[N], inv[N], invfac[N], f[N];
int n = 1e5;
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 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))
int rev[N];
inline int calc(int n) {
int len = 1;
while (len < n)
len <<= 1;
for (int i = 0; i < len; ++i)
rev[i] = (rev[i >> 1] >> 1) | (i & 1 ? (len >> 1) : 0);
return 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 Mul(int *f, int n, int *g, int m, int *res) {
static int a[N], b[N];
int len = calc(n + m - 1);
cpy(a, f, n), clr(a + n, len - n);
cpy(b, g, m), clr(b + m, len - m);
NTT(a, len, 1), NTT(b, len, 1);
for (int i = 0; i < len; ++i)
a[i] = 1ll * a[i] * b[i] % Mod;
NTT(a, len, -1), cpy(res, a, n + m - 1);
}
inline void Inv(int *f, int n, int *res) {
static int a[N], b[N], c[N];
c[0] = mi(f[0], Mod - 2);
for (int len = 1; len < (n << 1); len <<= 1) {
cpy(a, f, len), clr(a + len, len);
cpy(b, c, len), clr(b + len, len);
calc(len << 1), NTT(a, len << 1, 1), NTT(b, len << 1, 1);
for (int i = 0; i < (len << 1); ++i)
c[i] = 1ll * dec(2, 1ll * a[i] * b[i] % Mod) * b[i] % Mod;
NTT(c, len << 1, -1), clr(c + len, len);
}
cpy(res, c, n);
}
inline void Sqrt(int *f, int n, int *res) {
static int a[N], b[N], c[N];
c[0] = 1; // c[0] = sqrt(f[0]), normally f[0] = 1
for (int len = 1; len < (n << 1); len <<= 1) {
cpy(a, f, len), clr(a + len, len);
Inv(c, len, b), clr(b + len, len);
calc(len << 1), NTT(a, len << 1, 1), NTT(b, len << 1, 1);
for (int i = 0; i < (len << 1); ++i)
a[i] = 1ll * a[i] * b[i] % Mod;
NTT(a, len << 1, -1);
for (int i = 0; i < len; ++i)
c[i] = 1ll * add(c[i], a[i]) * inv[2] % Mod;
}
cpy(res, c, n);
}
#undef cpy
#undef clr
} // namespace Poly
signed main() {
prework();
for (int i = 0; i <= n; ++i)
f[i] = 1ll * invfac[i] * mi(inv[2], 1ll * i * (i - 1) / 2 % (Mod - 1)) % Mod;
Poly::Mul(f, n + 1, f, n + 1, f);
for (int i = 0; i <= n; ++i)
f[i] = 1ll * f[i] * mi(2, 1ll * i * (i - 1) / 2 % (Mod - 1)) % Mod;
Poly::Sqrt(f, n + 1, f);
for (int i = 1; i <= n; ++i)
printf("%d\n", 1ll * f[i] * fac[i] % Mod);
return 0;
}
强连通图
P11714 [清华集训 2014] 主旋律
给出一张有向图,求该图的强连通子图的数量。
\(n \le 15\)
设 \(f_{S, i}\) 表示点集 \(S\) 由 \(i\) 个孤立 SCC 组成的导出子图的数量,记 \(E(S, T)\) 表示 \(S \to T\) 的边数。
钦定 \(S\) 的导出子图缩点后零入度点数量为 \(i\) ,再枚举 \(i\) 个 SCC 对应的原图点集 \(T\) ,容斥得出下式:
令 \(g_S = \sum_{i = 1}^{|S|} (-1)^{i - 1} f_{S, i}\) ,则:
因此得到转移:
不难 \(O(3^n n)\) 求出所有 \(g\) 。
考虑建立 \(f\) 和 \(g\) 之间的联系,根据定义式得到:
因此得到转移:
不难 \(O(3^n n)\) 求出 \(f_{*, 1}\) ,答案即为 \(f_{\{ 1, 2, \cdots, n \}, 1}\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 15;
int e[N], pw[N * N], f[1 << N], g[1 << 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 calc(int S, int T) {
int cnt = 0;
for (int i = 0; i < n; ++i)
if (S >> i & 1)
cnt += __builtin_popcount(e[i] & T);
return cnt;
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
e[u - 1] |= 1 << (v - 1);
}
pw[0] = 1;
for (int i = 1; i <= m; ++i)
pw[i] = 2ll * pw[i - 1] % Mod;
for (int s = 0; s < (1 << n); ++s) {
g[s] = pw[calc(s, s)];
for (int t = (s - 1) & s; t; t = (t - 1) & s)
g[s] = dec(g[s], 1ll * pw[calc(s, s ^ t)] * g[t] % Mod);
}
for (int s = 0; s < (1 << n); ++s) {
f[s] = g[s];
for (int t = (s - 1) & s; t; t = (t - 1) & s)
if (t & (s & -s))
f[s] = add(f[s], 1ll * f[t] * g[s ^ t] % Mod);
}
printf("%d", f[(1 << n) - 1]);
return 0;
}
P10221 [省选联考 2024] 重塑时光
对于一个排列 \(p\) ,考虑将其随机划分为 \(k + 1\) 段(可能为空)。具体的,对于第 \(i\) 次划分,排列 \(p\) 与之前所有的划分点构成了一个长度为 \(n + i - 1\) 的序列,在开头、相邻、末尾共 \(n + i\) 个位置中等概率随机选取一个位置划分。
然后将这 \(k + 1\) 段重排列,需要满足 \(m\) 个限制,第 \(i\) 个限制 \((u_i, v_i)\) 表示 \(u_i\) 的出现位置必须小于 \(v_i\) 的出现位置。
从 \(n!\) 中排列中等概率随机出一个排列 \(p\) ,将其随机划分为 \(k + 1\) 段后,求存在一种给 \(k + 1\) 段重排列的合法方案的概率。
\(n \le 15\) ,\(m \le \frac{n(n - 1)}{2}\) ,\(k \le n\)
先考虑求解划分 \(\le k\) 块,块内、块间都可以重排列,求合法方案数。
设划分为 \(i\) 块的方案数为 \(f_i\) ,则最终划分 \(k\) 次恰好是这 \(i\) 块的概率为:
- 这 \(i\) 块块间有 \(i!\) 种重排列方案。
- 从 \(k + 1\) 块中选 \(i\) 块为这 \(i\) 块,方案数为 \(\binom{k + 1}{i}\) 。
- \(k\) 次划分顺序任意,方案数为 \(k!\) 。
乘法原理整合得到为 \(f_i \times i! \times \binom{k + 1}{i} \times k!\) ,最后除以总方案数 \((n + k)!\) 即为答案。
接下来考虑求解 \(f\) 。先求出 \(h_S\) 表示 \(S\) 块内任意重排列满足条件的方案数,设 \(e(S, T)\) 表示是否存在一条边为 \(S \to T\) ,则:
不难 \(O(2^n n)\) 预处理出 \(h\) 。
然后考虑块间满足条件,即块间拓扑序的数量。设 \(g_{S, i}\) 表示将 \(S\) 划分为 \(i\) 个连通块的数量,则:
不难 \(O(3^n)\) 预处理出 \(g\) 。
然后考虑求解 \(f\) 。设 \(f_{S, i}\) 表示将 \(S\) 划分为 \(i\) 个块的数量,考虑容斥,每次钦定若干零度点,则:
直接做是 \(O(3^n n^2)\) 的,考虑优化。发现第二维是一个卷积的形式,设:
则:
带入 \(n + 1\) 个点值再拉插回来,即可在 \(O(3^n n + n^2)\) 的时间复杂度内求得 \(f\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 17;
int e[1 << N], h[1 << N], g[1 << N][N];
int fac[N << 1], inv[N << 1], invfac[N << 1];
int val[N];
int n, m, k;
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;
}
inline bool check(int S, int T) {
for (int i = 0; i < n; ++i)
if (S >> i & 1 && (e[i] & T))
return true;
return false;
}
inline int solve(int x) {
static int F[1 << N], G[1 << N];
memset(G, 0, sizeof(int) << n);
for (int i = 0; i < (1 << n); ++i)
for (int j = 0, pwx = 1; j <= n; ++j, pwx = 1ll * pwx * x % Mod)
G[i] = add(G[i], 1ll * sgn(j + 1) * g[i][j] % Mod * pwx % Mod);
memset(F, 0, sizeof(int) << n);
F[0] = 1;
for (int s = 1; s < (1 << n); ++s)
for (int t = s; t; t = (t - 1) & s)
if (!(e[t] & (s ^ t)))
F[s] = add(F[s], 1ll * F[s ^ t] * G[t] % Mod);
return F[(1 << n) - 1];
}
namespace Lagrange {
int f[N], g[N];
inline void solve(int *y, int n) {
memset(g, 0, sizeof(int) * (n + 1));
g[0] = 1;
for (int i = 1; i <= n; ++i)
for (int j = i; ~j; --j)
g[j] = add(1ll * (Mod - i) * g[j] % Mod, g[j - 1]);
memset(f, 0, sizeof(int) * n);
for (int i = 1; i <= n; ++i) {
int mul = 1;
for (int j = 1; j <= n; ++j)
if (i != j)
mul = 1ll * mul * dec(i, j) % Mod;
mul = 1ll * y[i] * mi(mul, Mod - 2) % Mod;
for (int j = n - 1, res = g[n]; ~j; --j)
f[j] = add(f[j], 1ll * mul * res % Mod), res = add(g[j], 1ll * i * res % Mod);
}
}
} // namespace Lagrange
signed main() {
scanf("%d%d%d", &n, &m, &k);
prework(n + k);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
e[1 << (u - 1)] |= 1 << (v - 1);
}
for (int i = 1; i < (1 << n); ++i)
e[i] = e[i ^ (i & -i)] | e[i & -i];
h[0] = 1;
for (int i = 1; i < (1 << n); ++i)
for (int j = 0; j < n; ++j)
if ((i >> j & 1) && !(e[1 << j] & i))
h[i] = add(h[i], h[i ^ (1 << j)]);
g[0][0] = 1;
for (int s = 1; s < (1 << n); ++s)
for (int t = s; t; t = (t - 1) & s)
if (!(e[t] & (s ^ t)) && !(e[s ^ t] & t) && (t & (s & -s))) {
for (int i = 1; i <= n; ++i)
g[s][i] = add(g[s][i], 1ll * g[s ^ t][i - 1] * h[t] % Mod);
}
for (int i = 1; i <= n + 1; ++i)
val[i] = solve(i);
Lagrange::solve(val, n + 1);
int ans = 0;
for (int i = 0; i <= k + 1; ++i)
ans = add(ans, 1ll * Lagrange::f[i] * fac[i] % Mod * C(k + 1, i) % Mod * fac[k] % Mod);
printf("%d", 1ll * ans * invfac[n + k] % Mod);
return 0;
}
应用
P3343 [ZJOI2015] 地震后的幻想乡
给出一张无向图,每条边的边权为 \([0, 1]\) 均匀分布的随机实数,求该图生成树的最大边权的期望。
\(n \le 10\) ,\(m \le \frac{n(n - 1)}{2}\)
记选前 \(k\) 小边后恰好连通的概率为 \(P(k)\) ,则答案为 \(\sum_{k = 1}^m \frac{k}{m + 1} P(k)\) 。
选 \(k\) 条边后恰好连通的概率可以转化为选 \(k - 1\) 条边后不连通的概率减去选 \(k\) 条边后不联通的概率。
设 \(f_{s, i}\) 表示点集 \(s\) 的导出子图中选 \(i\) 条边连通的方案数,\(g_{s, i}\) 表示表示点集 \(s\) 的导出子图中选 \(i\) 条边不连通的方案数。记 \(d_s\) 为 \(s\) 导出子图的边数,则:
考虑 \(g_{s, i}\) 的转移,枚举子集连通块 \(t\) ,满足 \(t\) 和 \(s - t\) 之间没有边,为了避免算重需要强制一个特定的点 \(k \in t\) :
答案即为:
时间复杂度 \(O(3^n \times m^2)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 11, M = 47;
double C[M][M], f[1 << N][M], g[1 << N][M];
int e[N], d[1 << N];
int n, m;
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1, u, v; i <= m; ++i) {
scanf("%d%d", &u, &v);
e[u - 1] |= (1 << v - 1), e[v - 1] |= (1 << u - 1);
}
for (int s = 1; s < (1 << n); ++s) {
int cur = __lg(s);
d[s] = d[s ^ (1 << cur)] + __builtin_popcount(e[cur] & s);
}
C[0][0] = 1;
for (int i = 1; i <= m; ++i) {
C[i][0] = 1;
for (int j = 1; j <= i; ++j)
C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
}
for (int s = 0; s < (1 << n); ++s)
for (int i = 0; i <= d[s]; ++i) {
for (int t = (s - 1) & s; t; t = (t - 1) & s)
if (t & (s & -s))
for (int j = 0; j <= min(i, d[t]); ++j)
g[s][i] += f[t][j] * C[d[s ^ t]][i - j];
f[s][i] = C[d[s]][i] - g[s][i];
}
double ans = 0;
for (int i = 0; i <= m; ++i)
ans += g[(1 << n) - 1][i] / C[m][i];
printf("%.6lf", ans / (m + 1));
return 0;
}
P6789 寒妖王
给定一张无向图,边带边权。每条边有一半的概率消失,求图中最大基环树森林的边权和。
\(n \le 15\) ,\(m \le 60\)
类似 Kruskal,考虑按边权降序加边,对于每一条边统计其被加入的概率即可。一条边能被加入,当且仅当两段同时位于一棵树内,或两端不连通且至少一边为树。
设:
- \(f_{i, S}\) 表示考虑前 \(i\) 条边中端点属于 \(S\) 的边时,\(S\) 为极大连通图的方案数。
- \(g_{i, S}\) 表示考虑前 \(i\) 条边中端点属于 \(S\) 的边时,\(S\) 为极大树的方案数。
- \(cnt_S\) 表示前 \(i\) 条边中端点属于 \(S\) 的边的数量。
则:
统计第 \(i\) 条边被加入的概率时考虑补集转化,统计其不被贡献的概率,即 \(u, v\) 所在连通块均存在环,而 \(S\) 中存在环的概率即为 \(f_{i - 1, S} - g_{i - 1, S}\) ,则:
接下来考虑统计第 \(i\) 条边被加入的概率:
- 若两端在同一树内,贡献为 \(\sum_{u, v \in S} 2^{cnt(U \setminus S)} g_S\) 。
- 若两端不连通,贡献为 \(\sum_{S \cup T \subseteq U, u \in S, v \in T} 2^{cnt(U \setminus S \setminus T)} (f_{i - 1, S} \times g_{i - 1, T} + f_{i - 1, T} \times g_{i - 1, S} - g_{i - 1, S} \times g_{i - 1, T})\) 。
时间复杂度 \(O(m 3^n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353, inv2 = (Mod + 1) / 2;
const int N = 15, M = 61;
struct Edge {
int u, v, w;
inline bool operator < (const Edge &rhs) const {
return w > rhs.w;
}
} e[M];
int pw[M], ipw[M], f[1 << N], g[1 << N], cnt[1 << 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;
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i)
scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w), --e[i].u, --e[i].v;
sort(e + 1, e + m + 1);
pw[0] = ipw[0] = 1;
for (int i = 1; i <= m; ++i)
pw[i] = 2ll * pw[i - 1] % Mod, ipw[i] = 1ll * ipw[i - 1] * inv2 % Mod;
for (int i = 0; i < n; ++i)
f[1 << i] = g[1 << i] = 1;
int all = (1 << n) - 1, ans = 0;
for (int i = 1; i <= m; ++i) {
int u = e[i].u, v = e[i].v, res = 0;
for (int s = 0; s <= all; ++s)
if (s >> u & 1)
for (int t = (all ^ s); t; t = (t - 1) & (all ^ s))
if (t >> v & 1)
res = add(res, 1ll * dec(add(1ll * f[s] * g[t] % Mod, 1ll * f[t] * g[s] % Mod),
1ll * g[s] * g[t] % Mod) * pw[cnt[all ^ s ^ t]] % Mod);
for (int s = 0; s <= all; ++s)
if ((s >> u & 1) && (s >> v & 1))
res = add(res, 1ll * g[s] * pw[cnt[all ^ s]] % Mod);
ans = add(ans, 1ll * res * ipw[i] % Mod * e[i].w % Mod);
for (int s = 0; s <= all; ++s) {
if ((~s >> u & 1) || (~s >> v & 1))
continue;
++cnt[s], f[s] = 2ll * f[s] % Mod;
for (int t = s; t; t = (t - 1) & s)
if ((t >> u & 1) && ((s ^ t) >> v & 1))
f[s] = add(f[s], 1ll * f[t] * f[s ^ t] % Mod), g[s] = add(g[s], 1ll * g[t] * g[s ^ t] % Mod);
}
}
printf("%d", ans);
return 0;
}

浙公网安备 33010602011771号