Kirchhoff's Matrix Tree 定理与 BEST 定理随记

2025-12-03 13:11:52 限时副本 527

Kirchhoff 矩阵树定理适用于解决图的生成树相关计数问题。BEST 定理则适用于解决有向欧拉图的欧拉路径计数问题。

本文中的图允许重边,不允许自环(其实可以有自环,甚至不会对定理内容产生任何影响,但是为方便叙述不允许自环)。下文记矩阵 \(A_{[S], [T]}\) 为选取 \(A_{i, j} (i \in S, j \in T)\) 组成的子矩阵,\([n] = \{1, 2, \dots, n\}\)。

Matrix Tree 定理(无向图形式)

记 \(A\) 为邻接矩阵,\(D\) 为度数矩阵,其中 \(D_{i, i} = \deg_i\),当 \(i \neq j\) 时 \(D_{i, j} = 0\)。记 \(\mathcal{T}\) 表示图的生成树集合,定义一棵生成树 \(T\) 的权值为 \(\omega(T) = \prod \limits_{e \in T} \omega(e)\),其中 \(\omega(e)\) 为边 \(e\) 的边权。

定义 Laplace 矩阵 \(L = D - A\),则 \(\det L_{[n] \backslash \{k\}, [n] \backslash \{k\}} = \sum \limits_{T \in \mathcal{T}} \omega(T)\),其中 \(k\) 任意。用自然语言描述即,生成树的权值之和等于 \(L\) 任一 \(n - 1\) 阶主子式的 \(\det\)。

下面对此定理展开证明。

首先有一个重要引理,Cauchy-Binet 引理:对于一个 \(n \times m\) 的矩阵 \(A\) 与一个 \(m \times n\) 的矩阵 \(B\),有

\[\det AB = \sum_{S \subseteq [m], |S| = n} (\det A_{[n], S}) (\det B_{S, [n]})

\]

当 \(m < n\) 时显然 \(AB\) 不满秩,等式右侧也可被认为是 \(0\),引理自然成立。

这里主要探讨 \(m \geq n\) 的情况。作为线性代数中的一条重要公式,学界已经给出了各式各样的代数证明。此处参考 OI wiki,给出一份简短而巧妙的证明。前置知识:[NOI2021] 路径交点。

考虑“路径交点”的模型,构造一个层数为 \(3\) 的 DAG:左部点与右部点均有 \(n\) 个,中部点 \(m\) 个。左部点 \(i\) 向中部点 \(j\) 连权为 \(a_{i, j}\) 的边,中部点 \(i\) 向右部点 \(j\) 连权为 \(b_{i, j}\) 的边。

容易发现 \(AB\) 即为 LGV 引理中的 \(\mathbf{M}\),而引理中等式右侧计算的值等于“路径交点”模型中交点数为偶数的不交路径组的权值和,减去交点数为奇数的不交路径组的权值和。根据前置经验,我们知道 \(\det \mathbf{M}\) 与之相等,故等式成立。\(\square\)

回到 Matrix Tree 定理的证明上。一个事实是,\(L\) 矩阵有一个很好的关于“关联矩阵”的性质。其中关联矩阵 \(P\) 为一大小为 \(|V| \times |E|\) 的矩阵,对于边 \(e_i = (u, v, w), u < v\),\(P_{u, i} = 1, P_{v, i} = -1\),其余元素均为 \(0\)。定义另一个关联矩阵 \(Q\) 为一大小为 \(|E| \times |V|\) 的矩阵,对于边 \(e_i = (u, v, w), u < v\),\(Q_{i, u} = w, Q_{i, v} = -w\),其余元素均为 \(0\)。则 \(L\) 矩阵具有这样的性质:\(L = PQ\)。

上述转化是证明的核心步骤之一。记 \(n = |V|, m = |E|\),记 \(L' = L_{[n] \backslash \{1\}, [n] \backslash \{1\}}\) 为 \(L\) 去掉第一行第一列的子矩阵,\(P', Q'\) 分别为 \(P, Q\) 去掉第一行的子矩阵,则 \(L' = P'Q'\)。对 \(L' = P'Q'\) 应用 Cauchy-Binet 引理,得到 \(\det L' = \sum \limits_{S \subseteq [m], |S| = n - 1} (\det P'_{[n - 1], S}) (\det Q'_{S, [n - 1]})\)。

考虑 \(\det P'_{[n - 1], S}\) 的组合意义:对标号为 \(2, 3, \dots, n\) 的点,分别选择一条 \(S\) 中的边(若点 \(i\) 选择边 \(j\),则认为 \(i\) 是边 \(j\) 的起点),所有方案的带符号权值和。考虑边集 \(S\) 生成了至少一个环的情况。由于我们的选择是有向的,因此任意一个无向环都有两种定向方式,对应行列式上两种不同贡献。将点编号最小值最小的环反向,记环长为 \(k\),由于所有边的起点终点交换,相当于 \(P\) 上对应列的 \(1\) 与 \(-1\) 交换,因此贡献乘以了 \((-1) ^ k\)。另一方面是行列式逆序对数的变化,交换对应列的 \(1\) 与 \(-1\) 后形成的新排列可以通过原排列执行 \(k - 1\) 次 swap 操作形成,因此贡献还需乘以 \((-1) ^ {k - 1}\)。无论 \(k\) 是奇数还是偶数,将最小环反转后贡献都会乘以 \(-1\),而我们的反转操作显然构成对合映射,因此若 \(S\) 生成至少一个环,则 \(\det P'_{[n - 1], S}\) 为 \(0\)。

否则边集 \(S\) 成功生成了一棵树,而我们对这张基图也有唯一的定向方式:以 \(1\) 为根的根向树。记在计算 \(\det P'_{[n - 1], S}\) 时这种定向方式下产生的逆序对数为 \(t\),每一列选择的权值的积为 \((-1) ^ x\),则 \(\det P'_{[n - 1], S} = (-1) ^ {t + x}\)。显然 \(Q'\) 和 \(P'\) 的情况是对称的,它的 \(\det\) 值是 \((-1) ^ {t + x} \times \prod \limits_{e \in S} \omega(e)\)。故最后算出的值等于 \(S\) 边集对应的 \(\omega(T)\)。

求和后自然得到 \(\sum \limits_{T \in \mathcal{T}} \omega(T)\)。由于无向情况下答案与钦定的有根树根节点并无关系,因此任一 \(n - 1\) 阶主子式的 \(\det\) 值均相等,至此完成了无向图部分的证明。\(\square\)

其实矩阵树定理还有另一种形式:所求即为 Laplace 矩阵所有非零特征值(共 \(n - 1\) 个)的乘积除以 \(n\),证明咕。一些情况下这种形式可以更便于推导问题,下面是一个例子,不过这种形式应该不怎么常见。

给定一张 \(n\) 部完全图的 \(n\) 个部分的顶点数量 \(a_1, a_2, \dots, a_n\),记 \(N = \sum a_i\),那么该图的生成树数量为 \(N^{n - 2}\prod\limits_{i = 1} ^ n (N - a_i) ^ {a_i - 1}\)。

Matrix Tree 定理(有向图形式)

较无向图下的矩阵树定理而言,有向图下的定理乃至证明均只有一些细微的区别。

记 \(A\) 为邻接矩阵,\(D^{\text{in}}\) 为入度矩阵,\(D^{\text{out}}\) 为出度矩阵。定义 Laplace 矩阵 \(L^{\text{in}} = D^{\text{in}} - A\),\(L^{\text{out}} = D^{\text{out}} - A\)。

则以 \(k\) 为根的根向树数量为 \(\det {L^{\text{out}}}_{[n] \backslash \{k\}, [n] \backslash \{k\}}\),叶向树数量为 \(\det {L^{\text{in}}}_{[n] \backslash \{k\}, [n] \backslash \{k\}}\)。

考虑证明。仍然类似地定义关联矩阵 \(P, Q\),对于每条边 \(e_i = (u, v, w)\),

若所求为叶向树,\(P_{u, i} = -1, P_{v, i} = 1, Q_{i, u} = 0, Q_{i, v} = w\)。此时有 \(L ^ {\text{in}} = PQ\)。

否则对于根向树,\(P_{u, i} = w, P_{v, i} = 0, Q_{i, u} = 1, Q_{i, v} = -1\)。此时有 \(L ^ {\text{out}} = PQ\)。

接下来的证明流程完全同前,如果要求树以 \(k\) 为根,仍然令 \(P', Q'\) 分别为 \(P, Q\) 去掉第 \(k\) 行形成的子矩阵,那么 \(L' = P'Q'\)。应用 Cauchy-Binet 引理得到 \(\det L' = \sum \limits_{S \subseteq [m], |S| = n - 1} (\det P'_{[n - 1], S}) (\det Q'_{S, [n - 1]})\)。

以叶向树为例,\(\det P'_{[n - 1], S}\) 的组合意义与无向情况完全一致,而由于 \(\det Q'_{S, [n - 1]}\) 的 \(n - 1\) 行分别只有一项有值,因此这一项非 \(0\) 当且仅当每列都有恰好一项有值,对应到图上意味着有向边集 \(S\) 生成的子图中每个点的入度恰好为 \(1\)。如果满足,则说明边集 \(S\) 构成以 \(k\) 为根的叶向树,此时定向方式唯一,\(\det P'_{[n - 1], S}\) 在每一列上也一定会选择正值项,因此最后求出的即为 \(\omega(T)\)。求和后自然得到 \(\sum \limits_{T \in \mathcal{T}} \omega(T)\)。\(\square\)

BEST 定理

记 \(f ^ {\text{root}}(G, k)\) 表示有向图 \(G\) 的以 \(k\) 为根的根向生成树数量,由 Matrix Tree 定理我们知道它等于 \(\det {L ^ {\text{out}}}_{[n] \backslash \{k\}, [n] \backslash \{k\}}\)。定义有向欧拉图 \(G\) 的不同欧拉回路数(这里认为重边算作互不相同的边)为 \(\mathrm{ec}(G)\)。那么 BEST 定理表明

\[\mathrm{ec}(G) = f ^ {\text{root}}(G, k) \prod (\deg_u - 1)!

\]

这里的 \(k\) 可以任取,在证明中它代表了欧拉回路的起点。有向欧拉图上 \(\deg^{\text{in}}_u = \deg^{\text{out}}_u\),统称之为 \(\deg_u\)。

注意这是考虑循环同构的情况。如果认为欧拉回路从 \(k\) 点出发,只考虑边的经过顺序是否相同而不考虑是否循环同构时,此式需要乘以 \(\deg_k\)。下文始终考虑循环同构。

证明需要考虑这样一种刻画欧拉回路的方法:对于所有的以 \(k\) 为根节点的根向生成树,我们对所有结点的非树边出边钦定顺序,对 \(k\) 结点而言有 \((\deg_k - 1)!\) 种(减一是为了去除循环同构),其余为 \((\deg_u - 1)!\) 种。从点 \(k\) 出发,如果树边是唯一一条未经过的出边,那么走树边。否则,按照钦定的顺序走下一条未经过的出边。

如果我们能证明这样做一定能唯一地得到欧拉回路,且每一条欧拉回路都能被唯一对应,那么双射自然成立。

一个显然的事实是,在有向欧拉图上的任意不经过重复边的游走过程中,如果路径起点为 \(k\),游走过程中行至 \(u\) 点,若 \(u = k\),则此时每个点的入度与出度相等;若 \(u \neq k\),则 \(k\) 点的入度为出度减一,\(u\) 点的出度为入度减一,其余点均满足入度出度相等。游走将在没有未经过的出边时终止。

那么第一个命题的证明就简单了。首先游走的过程只可能在 \(k\) 点终止,否则当前点一定满足出度为入度减一,即还有至少一条出边可行。而会不会有边没有被经过呢?由于树边一定是每个非 \(k\) 点的最后一条出边,若存在一条树边没有被经过,它会给父结点带来一个入度,因此父结点的树边也一定未被经过。这样归纳下去,\(k\) 结点将有一条入边未被经过,而由欧拉图可知,此时对应地有一条出边未被经过,矛盾,故所有边都将被经过,这种方法一定得到了欧拉回路。容易发现树边即为每个点最后一条经过的出边,所以这样得到的欧拉回路是互不相同的。

第二个命题的证明仍然考虑提出所有非 \(k\) 点的最后一条出边,这样形成的图一定是一棵根向树,其余边按照访问顺序对应即可。故每条欧拉回路都能被对应且有唯一的对应方式。

综合以上两个命题,BEST 定理成立。\(\square\)

扩展:如果认为重边是本质相同的,而不考虑循环同构时,只需将所求式除以 \(\prod A_{i, j}!\) 即可,其中 \(A_{i, j}\) 为 \(i \to j\) 的有向边数量。

既要去除重边,又要考虑循环同构的情况下,由于重边的缘故,从某个点出发的回路可能本质相同。假如我们将回路的异同关系用编号表示,则这个编号的可重集可以是多种的。因此我们很难确定每条欧拉回路被统计的次数,在这种情况下可能不具有通用的公式。

但事实上这种问题已经有了很成熟的解决方法:Polya 定理。鉴于这不是本文的主体因此只说明一下做法。

记 \(A_{i, j}\) 表示 \(i \to j\) 的有向边数量,\(t = \gcd(A_{i, j})\)。若 \(d \nmid t\),定义 \(g(d) = 0\),否则 \(g(d)\) 表示将所有重边的重复次数除以 \(d\) 形成的新图 \(G'\) 中,从任意点出发,不考虑循环同构,重边本质相同时的欧拉回路数:\(m' \times f ^ {\text{root}}(G', k) \times \prod (\deg'_u - 1)! \times \prod \dfrac{1}{A'_{i, j}!}\)。则所求等价于下式:

\[\frac{1}{m} \sum_{d \mid m} \varphi(d) g(d)

\]

可以以 \(\Theta(t ^ {\frac{1}{3}} n ^ 3)\) 的时间复杂度计算。

接下来是一些题目。

P6178 【模板】Matrix-Tree 定理

模板题,行变换求行列式即可。时间复杂度 \(\Theta(n ^ 3)\)。

Code

#include

using namespace std;

using i64 = long long;

constexpr int mod = 1e9 + 7, N = 300 + 5;

namespace basic {

inline int add(int x, int y) {return (x + y >= mod ? x + y - mod : x + y);}

inline int dec(int x, int y) {return (x - y < 0 ? x - y + mod : x - y);}

inline void ad(int &x, int y) {x = add(x, y);}

inline void de(int &x, int y) {x = dec(x, y);}

inline int qpow(int a, int b) {

int r = 1;

while(b) {

if(b & 1) r = 1ll * r * a % mod;

a = 1ll * a * a % mod; b >>= 1;

}

return r;

}

inline int inv(int x) {return qpow(x, mod - 2);}

int fac[N], ifac[N];

inline void fac_init(int n = N - 1) {

fac[0] = 1;

for(int i = 1; i <= n; i++)

fac[i] = 1ll * fac[i - 1] * i % mod;

ifac[n] = inv(fac[n]);

for(int i = n - 1; i >= 0; i--)

ifac[i] = 1ll * ifac[i + 1] * (i + 1) % mod;

}

}

using namespace basic;

struct Determinant {

int a[N][N], n;

Determinant() {memset(a, 0, sizeof(a));}

inline int* operator [] (int x) {return a[x];}

inline int det() {

int coef = 1;

for(int i = 1; i <= n; i++) {

int it = -1;

for(int j = i; j <= n; j++) {

if(a[j][i] != 0) {

it = j;

break;

}

}

if(it == -1) {

return 0;

}

if(it != i) {

swap(a[it], a[i]);

coef = mod - coef;

}

for(int j = i + 1; j <= n; j++) {

if(a[j][i] == 0) {

continue;

}

int C = 1ll * a[j][i] * inv(a[i][i]) % mod;

for(int k = i; k <= n; k++) {

de(a[j][k], 1ll * C * a[i][k] % mod);

}

}

}

int ret = coef;

for(int i = 1; i <= n; i++) {

ret = 1ll * ret * a[i][i] % mod;

}

return ret;

}

};

int n, m, type;

namespace Undirected {

int D[N][N], A[N][N];

void Main() {

for(int i = 1; i <= m; i++) {

int u, v, w; cin >> u >> v >> w;

if(u == v) {

continue;

}

ad(A[u][v], w), ad(A[v][u], w);

ad(D[u][u], w), ad(D[v][v], w);

}

Determinant L;

for(int i = 1; i <= n; i++) {

for(int j = 1; j <= n; j++) {

L[i][j] = dec(D[i][j], A[i][j]);

}

}

L.n = n - 1;

cout << L.det() << "\n";

}

}

namespace Directed {

int D_in[N][N], D_out[N][N], A[N][N];

void Main() {

for(int i = 1; i <= m; i++) {

int u, v, w; cin >> u >> v >> w;

if(u == v) {

continue;

}

ad(A[u][v], w);

ad(D_out[u][u], w), ad(D_in[v][v], w);

}

Determinant L;

for(int i = 2; i <= n; i++) {

for(int j = 2; j <= n; j++) {

L[i - 1][j - 1] = dec(D_in[i][j], A[i][j]);

}

}

L.n = n - 1;

cout << L.det() << "\n";

}

}

int main() {

ios::sync_with_stdio(false);

cin.tie(nullptr), cout.tie(nullptr);

cin >> n >> m >> type;

if(type == 0) {

Undirected::Main();

} else {

Directed::Main();

}

}

P5807 【模板】BEST 定理 | Which Dreamed It

模板题。注意是求从 \(1\) 出发的经过顺序不同的欧拉回路数,因此要用循环同构意义下不同的欧拉回路数乘以 \(\deg_1\)。本题还需判断图是否是欧拉图,等价于所有结点出入度相等,且含有 \(1\) 的极大强连通分量是否包含了所有非零度点。后者也即非零度点是否强连通,可以用 \(\Theta(n ^ 3)\) 的暴力实现。注意去除孤立点。单组时间复杂度 \(\Theta(n ^ 3)\)。

Code

#include

using namespace std;

using i64 = long long;

constexpr int mod = 1e6 + 3, N = 100 + 5, M = 2e5 + 10;

namespace basic {

// ...

}

using namespace basic;

struct Determinant {

// ...

};

int D_in[N][N], D_out[N][N], A[N][N]; bool f[N][N];

void Main() {

memset(f, 0, sizeof(f));

memset(D_in, 0, sizeof(D_in)), memset(D_out, 0, sizeof(D_out));

memset(A, 0, sizeof(A));

int n; cin >> n;

for(int i = 1; i <= n; i++) {

f[i][i] = 1;

int E; cin >> E;

for(int j = 1; j <= E; j++) {

int v; cin >> v;

A[i][v]++, D_out[i][i]++, D_in[v][v]++;

f[i][v] = 1;

}

}

for(int k = 1; k <= n; k++) {

for(int i = 1; i <= n; i++) {

for(int j = 1; j <= n; j++) {

f[i][j] |= f[i][k] && f[k][j];

}

}

}

bool fl = 1;

for(int i = 1; i <= n; i++) {

fl &= D_in[i][i] == D_out[i][i];

for(int j = 1; j <= n; j++) {

if(D_in[i][i] && D_in[j][j]) {

fl &= f[i][j];

}

}

}

if(!fl) {

return cout << 0 << "\n", void();

}

vector p;

for(int i = 1; i <= n; i++) {

if(D_out[i][i]) {

p.push_back(i);

}

}

if(p.size() == 0) {

return cout << 1 << "\n", void();

}

if(p[0] != 1) {

return cout << 0 << "\n", void();

}

int res = fac[D_out[p[0]][p[0]]];

for(int i = 1; i < p.size(); i++) {

res = 1ll * res * fac[D_out[p[i]][p[i]] - 1] % mod;

}

Determinant L; L.n = p.size() - 1;

for(int i = 1; i <= L.n; i++) {

for(int j = 1; j <= L.n; j++) {

L[i][j] = dec(D_out[p[i]][p[j]], A[p[i]][p[j]]);

}

}

res = 1ll * res * L.det() % mod;

cout << res << "\n";

}

int main() {

ios::sync_with_stdio(false);

cin.tie(nullptr), cout.tie(nullptr);

fac_init();

int T; cin >> T;

while(T--) {

Main();

}

}

*P6624 [省选联考 2020 A 卷] 作业题

这个关于 \(\gcd\) 的式子可以直接莫比乌斯反演掉,这里直接给出反演后的结果:记 \(f(k)\) 表示取所有 \(k \mid w\) 的边,其所能形成的所有生成树 \(T\) 的 \(\sum \limits_{e \in T} \omega(e)\) 之和。则所求内容可以表示为下式:

\[\sum \limits_{k \geq 1} f(k) \varphi(k)

\]

问题来到了如何计算所有的 \(f(k)\)。涉及到生成树相关计数,考虑 Matrix Tree 定理。但该定理要求 \(\omega(T) = \prod \limits_{e \in T} \omega(e)\),此处我们遇到的是 \(\omega(T) = \sum \limits_{e \in T} \omega(e)\),故尝试用乘法来刻画加法。

通常加乘转化可以用到 \(\exp, \ln\),但在这里没什么用。为了用乘法刻画加法,一种更强大的工具是多项式。考虑两个多项式 \(1 + px, 1 + qx\),它们的乘积为 \((1 + px)(1 + qx) \equiv 1 + (p + q)x \pmod{x ^ 2}\)。因此我们可以令一条边的权值多项式为 \(W_e = 1 + \omega(e)x\),那么 \([x](\prod \limits_{e \in T} W_e)\) 即为 \(\sum \limits_{e \in T} \omega(e)\)。依此建立行列式并应用矩阵树定理即可。

如果对所有的 \(f(k), k \in [1, V]\) 计算答案,复杂度是 \(\Theta(n ^ 3 V)\) 的,较高。如果将边集大小小于 \(n - 1\) 的 \(k\) 跳过,合法的 \(k\) 将只有 \(\Theta(V ^ {\frac{1}{3}})\) 种,复杂度降低为 \(\Theta(n ^ 3 V ^ {\frac{1}{3}})\),绰绰有余。

当然本题还有一个很大的注意点,这在官方数据里没有体现,但存在于 uoj 的 hack 数据中。我们知道多项式存在乘法逆当且仅当其常数项存在逆元,因此我们会选择当前列常数项非 \(0\) 的行,可以通过它完成行变换。如果不存在常数项非 \(0\) 的元素,也并不说明行列式的结果为 \(\mathbf{0}\)。正确的做法是,若该列存在至少一个元素的一次项系数非 \(0\),则取之完成行变换,否则返回 \(\mathbf{0}\)。

Code

#include

using namespace std;

using i64 = long long;

constexpr int mod = 998244353, N = 30 + 5, M = 2e5;

namespace basic {

// ...

}

using namespace basic;

int phi[M], vis[M];

vector pr;

void sieve(int n = M - 1) {

// ...

}

struct Poly_2 {

int a, b;

inline friend Poly_2 operator + (Poly_2 x, Poly_2 y) {

return {add(x.a, y.a), add(x.b, y.b)};

}

inline friend Poly_2 operator - (Poly_2 x, Poly_2 y) {

return {dec(x.a, y.a), dec(x.b, y.b)};

}

inline friend Poly_2 operator * (Poly_2 x, Poly_2 y) {

return {1ll * x.a * y.a % mod, add(1ll * x.a * y.b % mod, 1ll * x.b * y.a % mod)};

}

inline Poly_2 Inv() {

assert(a);

int inva = inv(a);

return {inva, 1ll * dec(0, b) * inva % mod * inva % mod};

}

inline friend Poly_2 operator / (Poly_2 x, Poly_2 y) {

return x * y.Inv();

}

};

struct Determinant {

Poly_2 a[N][N]; int n;

Determinant() {

for(int i = 0; i < N; i++) {

for(int j = 0; j < N; j++) {

a[i][j] = {0, 0};

}

}

}

inline Poly_2* operator [] (int x) {return a[x];}

inline Poly_2 det() {

int coef = 1;

for(int i = 1; i <= n; i++) {

int it = -1;

for(int j = i; j <= n; j++) {

if(a[j][i].a) {

it = j;

break;

}

}

if(it == -1) {

for(int j = i; j <= n; j++) {

if(a[j][i].b) {

it = j;

break;

}

}

if(it == -1) {

return {0, 0};

}

if(it != i) {

swap(a[it], a[i]);

coef = mod - coef;

}

for(int j = i + 1; j <= n; j++) {

int C = 1ll * a[j][i].b * inv(a[i][i].b) % mod;

for(int k = i; k <= n; k++) {

de(a[j][k].b, 1ll * C * a[i][k].b % mod);

}

}

} else {

if(it != i) {

swap(a[it], a[i]);

coef = mod - coef;

}

for(int j = i + 1; j <= n; j++) {

Poly_2 C = a[j][i] / a[i][i];

for(int k = i; k <= n; k++) {

a[j][k] = a[j][k] - C * a[i][k];

}

}

}

}

Poly_2 ret = {coef, 0};

for(int i = 1; i <= n; i++) {

ret = ret * a[i][i];

}

return ret;

}

};

int n, m;

struct Edge {int u, v, w;} e[N * N];

Poly_2 A[N][N], D[N][N];

int f(vector E) {

if(E.size() < n - 1) {

return 0;

}

for(int i = 1; i <= n; i++) {

for(int j = 1; j <= n; j++) {

A[i][j] = D[i][j] = {0, 0};

}

}

for(auto id : E) {

auto [u, v, w] = e[id];

Poly_2 W = {1, w};

A[u][v] = A[u][v] + W, A[v][u] = A[v][u] + W;

D[u][u] = D[u][u] + W, D[v][v] = D[v][v] + W;

}

Determinant L;

for(int i = 1; i <= n; i++) {

for(int j = 1; j <= n; j++) {

L[i][j] = D[i][j] - A[i][j];

}

}

L.n = n - 1;

return L.det().b;

}

vector buc[M], rec[M];

int main() {

ios::sync_with_stdio(false);

cin.tie(nullptr), cout.tie(nullptr);

cin >> n >> m;

int V = 0;

for(int i = 1; i <= m; i++) {

auto &[u, v, w] = e[i];

cin >> u >> v >> w;

V = max(V, w);

}

sieve(V);

for(int i = 1; i <= V; i++) {

for(int j = i; j <= V; j += i) {

buc[j].push_back(i);

}

}

for(int i = 1; i <= m; i++) {

int w = e[i].w;

for(auto d : buc[w]) {

rec[d].push_back(i);

}

}

int ans = 0;

for(int i = 1; i <= V; i++) {

ad(ans, 1ll * phi[i] * f(rec[i]) % mod);

}

cout << ans << "\n";

}

P4111 [HEOI2015] 小 Z 的房间

这个就是练手题了。对题目建立图论模型,发现所求即为生成树数量,直接应用 Matrix Tree 定理。当然行列式求值的部分需要注意,因为这里 \(p\) 不是质数,因此要规避逆元,用类似辗转相除法的方式消元,可以先做做【模板】行列式求值。

这里给出行列式求值部分的实现,时间复杂度是 \(\Theta(n ^ 3 + n ^ 2\log p)\):

Code

struct Determinant {

int a[M][M], n;

Determinant() {}

Determinant(int _) {n = _;}

inline int* operator [] (int x) {return a[x];}

inline int det() {

int coef = 1;

for(int i = 1; i <= n; i++) {

int it = -1;

for(int j = i; j <= n; j++) {

if(a[j][i] != 0) {

it = j;

break;

}

}

if(it == -1) {

return 0;

}

if(it != i) {

coef = mod - coef;

swap(a[it], a[i]);

}

for(int j = i + 1; j <= n; j++) {

while(1) {

if(a[j][i] > a[i][i]) {

swap(a[j], a[i]);

coef = mod - coef;

}

if(a[j][i] == 0) {

break;

}

int d = a[i][i] / a[j][i];

for(int k = i; k <= n; k++) {

de(a[i][k], 1ll * a[j][k] * d % mod);

}

}

}

}

int ret = coef;

for(int i = 1; i <= n; i++) {

ret = 1ll * ret * a[i][i] % mod;

}

return ret;

}

};

P2144 [FJOI2007] 轮状病毒

还是练手题。可以直接运用 Matrix Tree 定理建行列式,然后用高精度进行行变换,推荐使用上一题的辗转相消法,时间复杂度 \(\tilde {O}(n ^ 3)\)。当然这个行列式比较特殊,可以作更进一步的分析得到递推式,此处略。

Code

#include

using namespace std;

using i64 = long long;

struct BigInt {

int sign;

std::vector v;

BigInt() : sign(1) {}

BigInt(const std::string &s) { *this = s; }

BigInt(int v) {

char buf[21];

sprintf(buf, "%d", v);

*this = buf;

}

void zip(int unzip) {

if (unzip == 0) {

for (int i = 0; i < (int)v.size(); i++)

v[i] = get_pos(i * 4) + get_pos(i * 4 + 1) * 10 + get_pos(i * 4 + 2) * 100 + get_pos(i * 4 + 3) * 1000;

} else

for (int i = (v.resize(v.size() * 4), (int)v.size() - 1), a; i >= 0; i--)

a = (i % 4 >= 2) ? v[i / 4] / 100 : v[i / 4] % 100, v[i] = (i & 1) ? a / 10 : a % 10;

setsign(1, 1);

}

int get_pos(unsigned pos) const { return pos >= v.size() ? 0 : v[pos]; }

BigInt &setsign(int newsign, int rev) {

for (int i = (int)v.size() - 1; i > 0 && v[i] == 0; i--)

v.erase(v.begin() + i);

sign = (v.size() == 0 || (v.size() == 1 && v[0] == 0)) ? 1 : (rev ? newsign * sign : newsign);

return *this;

}

std::string val() const {

BigInt b = *this;

std::string s;

for (int i = (b.zip(1), 0); i < (int)b.v.size(); ++i)

s += char(*(b.v.rbegin() + i) + '0');

return (sign < 0 ? "-" : "") + (s.empty() ? std::string("0") : s);

}

bool absless(const BigInt &b) const {

if (v.size() != b.v.size()) return v.size() < b.v.size();

for (int i = (int)v.size() - 1; i >= 0; i--)

if (v[i] != b.v[i]) return v[i] < b.v[i];

return false;

}

BigInt operator-() const {

BigInt c = *this;

c.sign = (v.size() > 1 || v[0]) ? -c.sign : 1;

return c;

}

BigInt &operator=(const std::string &s) {

if (s[0] == '-')

*this = s.substr(1);

else {

for (int i = (v.clear(), 0); i < (int)s.size(); ++i)

v.push_back(*(s.rbegin() + i) - '0');

zip(0);

}

return setsign(s[0] == '-' ? -1 : 1, sign = 1);

}

bool operator<(const BigInt &b) const {

return sign != b.sign ? sign < b.sign : (sign == 1 ? absless(b) : b.absless(*this));

}

bool operator==(const BigInt &b) const { return v == b.v && sign == b.sign; }

BigInt &operator+=(const BigInt &b) {

if (sign != b.sign) return *this = (*this) - -b;

v.resize(std::max(v.size(), b.v.size()) + 1);

for (int i = 0, carry = 0; i < (int)b.v.size() || carry; i++) {

carry += v[i] + b.get_pos(i);

v[i] = carry % 10000, carry /= 10000;

}

return setsign(sign, 0);

}

BigInt operator+(const BigInt &b) const {

BigInt c = *this;

return c += b;

}

void add_mul(const BigInt &b, int mul) {

v.resize(std::max(v.size(), b.v.size()) + 2);

for (int i = 0, carry = 0; i < (int)b.v.size() || carry; i++) {

carry += v[i] + b.get_pos(i) * mul;

v[i] = carry % 10000, carry /= 10000;

}

}

BigInt operator-(const BigInt &b) const {

if (b.v.empty() || b.v.size() == 1 && b.v[0] == 0) return *this;

if (sign != b.sign) return (*this) + -b;

if (absless(b)) return -(b - *this);

BigInt c;

for (int i = 0, borrow = 0; i < (int)v.size(); i++) {

borrow += v[i] - b.get_pos(i);

c.v.push_back(borrow);

c.v.back() -= 10000 * (borrow >>= 31);

}

return c.setsign(sign, 0);

}

BigInt operator*(const BigInt &b) const {

if (b < *this) return b * *this;

BigInt c, d = b;

for (int i = 0; i < (int)v.size(); i++, d.v.insert(d.v.begin(), 0))

c.add_mul(d, v[i]);

return c.setsign(sign * b.sign, 0);

}

BigInt operator/(const BigInt &b) const {

BigInt c, d;

BigInt e = b;

e.sign = 1;

d.v.resize(v.size());

double db = 1.0 / (b.v.back() + (b.get_pos((unsigned)b.v.size() - 2) / 1e4) +

(b.get_pos((unsigned)b.v.size() - 3) + 1) / 1e8);

for (int i = (int)v.size() - 1; i >= 0; i--) {

c.v.insert(c.v.begin(), v[i]);

int m = (int)((c.get_pos((int)e.v.size()) * 10000 + c.get_pos((int)e.v.size() - 1)) * db);

c = c - e * m, c.setsign(c.sign, 0), d.v[i] += m;

while (!(c < e))

c = c - e, d.v[i] += 1;

}

return d.setsign(sign * b.sign, 0);

}

BigInt operator%(const BigInt &b) const { return *this - *this / b * b; }

bool operator>(const BigInt &b) const { return b < *this; }

bool operator<=(const BigInt &b) const { return !(b < *this); }

bool operator>=(const BigInt &b) const { return !(*this < b); }

bool operator!=(const BigInt &b) const { return !(*this == b); }

};

constexpr int N = 100 + 5;

struct Determinant {

BigInt a[N][N]; int n;

Determinant() {}

Determinant(int _) {n = _;}

inline BigInt* operator [] (int x) {return a[x];}

inline BigInt det() {

BigInt coef = 1;

for(int i = 1; i <= n; i++) {

int it = -1;

for(int j = i; j <= n; j++) {

if(a[j][i] != 0) {

it = j;

break;

}

}

if(it == -1) {

return 0;

}

if(it != i) {

coef = -coef;

swap(a[it], a[i]);

}

for(int j = i + 1; j <= n; j++) {

while(1) {

BigInt x = a[j][i], y = a[i][i];

if(x < 0) {

x = -x;

}

if(y < 0) {

y = -y;

}

if(x > y) {

swap(a[j], a[i]);

coef = -coef;

}

if(a[j][i] == 0) {

break;

}

BigInt d = a[i][i] / a[j][i];

for(int k = i; k <= n; k++) {

a[i][k] = a[i][k] - a[j][k] * d;

}

}

}

}

BigInt ret = coef;

for(int i = 1; i <= n; i++) {

ret = ret * a[i][i];

}

return ret;

}

};

int n, D[N][N], A[N][N];

int main() {

ios::sync_with_stdio(false);

cin.tie(nullptr), cout.tie(nullptr);

cin >> n;

if(n == 1) {

cout << 1 << "\n";

return 0;

}

for(int i = 1; i <= n; i++) {

D[i][i] = 3;

int l = i == 1 ? n : i - 1;

A[l][i]++, A[i][l]++;

A[n + 1][i]++, A[i][n + 1]++;

}

Determinant L(n);

for(int i = 1; i <= n; i++) {

for(int j = 1; j <= n; j++) {

L[i][j] = D[i][j] - A[i][j];

}

}

cout << L.det().val() << "\n";

}

*[ABC336G] 16 Integers

首先进行图论建模:对于 \((i, j, k, l)\),从 \(v_{i, j, k}\) 向 \(v_{j, k, l}\) 建 \(X_{i, j, k, l}\) 条有向边,则此图的欧拉路径(回路)与原问题中的合法序列成双射。

首先判掉此图不是欧拉图或半欧拉图的情况,考虑此图是欧拉图的情况。欧拉回路计数,自然运用 BEST 定理。这里的欧拉回路显然可以从任一顶点开始,且重边是本质相同的,因此所求为 \(m \times f ^ {\text{root}}(G, k) \times \prod (\deg_u - 1)! \times \prod \frac{1}{A_{i, j}!}\)。

若此图是半欧拉图,我们要对欧拉路径进行计数。考虑从终点向起点连一条边,并钦定这条边一定是最后走的,则问题转化为对欧拉回路进行计数。容易发现这等价于 \(f ^ {\text{root}}(G, k) \times \prod (\deg_u - 1)! \times \prod \frac{1}{A_{i, j}!}\),注意处理重边时不要将新加的那条边算在内,因为我们已经钦定它被最后经过。

至此我们以 \(\Theta(n ^ 3 + m)\) 的复杂度解决了本题,在本题中 \(n = 8\)。

一个注意点是自环的问题,其实我们不用刻意管它,可以发现它无论在 BEST 定理中还是 Matrix Tree 定理中都完全没有影响。

Code

#include

using namespace std;

using i64 = long long;

constexpr int mod = 998244353, N = 1e6 + 10;

namespace basic {

// ...

}

using namespace basic;

constexpr int M = 8;

struct Determinant {

// ...

};

int e[2][2][2][2], m;

int D_in[M][M], D_out[M][M], A[M][M];

namespace Cycle {

void Main() {

int ans = m;

vector p;

for(int i = 0; i < 8; i++) {

if(D_out[i][i]) {

p.push_back(i);

}

}

Determinant L(p.size() - 1);

for(int i = 0; i < L.n; i++) {

for(int j = 0; j < L.n; j++) {

L[i][j] = dec(D_out[p[i]][p[j]], A[p[i]][p[j]]);

}

}

ans = 1ll * ans * L.det() % mod;

for(int i = 0; i < p.size(); i++) {

ans = 1ll * ans * fac[D_out[p[i]][p[i]] - 1] % mod;

}

for(int i = 0; i < 8; i++) {

for(int j = 0; j < 8; j++) {

ans = 1ll * ans * ifac[A[i][j]] % mod;

}

}

cout << ans << "\n";

}

}

namespace Path {

void Main() {

int S = -1, T = -1;

for(int i = 0; i < 8; i++) {

if(D_out[i][i] == D_in[i][i] + 1) {

S = i;

}

if(D_in[i][i] == D_out[i][i] + 1) {

T = i;

}

}

int ans = 1;

for(int i = 0; i < 8; i++) {

for(int j = 0; j < 8; j++) {

ans = 1ll * ans * ifac[A[i][j]] % mod;

}

}

A[T][S]++, D_in[S][S]++, D_out[T][T]++;

vector p;

for(int i = 0; i < 8; i++) {

if(D_out[i][i]) {

p.push_back(i);

}

}

Determinant L(p.size() - 1);

for(int i = 0; i < L.n; i++) {

for(int j = 0; j < L.n; j++) {

L[i][j] = dec(D_out[p[i]][p[j]], A[p[i]][p[j]]);

}

}

ans = 1ll * ans * L.det() % mod;

for(int i = 0; i < p.size(); i++) {

ans = 1ll * ans * fac[D_out[p[i]][p[i]] - 1] % mod;

}

cout << ans << "\n";

}

}

bool C1[M][M], C2[M][M];

int main() {

ios::sync_with_stdio(false);

cin.tie(nullptr), cout.tie(nullptr);

fac_init();

for(int i = 0; i < 2; i++) {

for(int j = 0; j < 2; j++) {

for(int k = 0; k < 2; k++) {

for(int l = 0; l < 2; l++) {

int u = (i << 2) | (j << 1) | k, v = (j << 2) | (k << 1) | l;

cin >> e[i][j][k][l];

if(e[i][j][k][l]) {

C1[u][v] = 1, C2[u][v] = C2[v][u] = 1;

}

D_in[v][v] += e[i][j][k][l], D_out[u][u] += e[i][j][k][l];

A[u][v] += e[i][j][k][l];

m += e[i][j][k][l];

}

}

}

}

for(int i = 0; i < 8; i++) {

C1[i][i] = C2[i][i] = 1;

}

for(int k = 0; k < 8; k++) {

for(int i = 0; i < 8; i++) {

for(int j = 0; j < 8; j++) {

C1[i][j] |= C1[i][k] && C1[k][j];

C2[i][j] |= C2[i][k] && C2[k][j];

}

}

}

bool fl = 1;

for(int i = 0; i < 8; i++) {

fl &= D_in[i][i] == D_out[i][i];

for(int j = 0; j < 8; j++) {

if(D_in[i][i] && D_in[j][j]) {

fl &= C1[i][j];

}

}

}

if(fl) {

Cycle::Main();

return 0;

}

fl = 1; int S = 0, T = 0;

for(int i = 0; i < 8; i++) {

if(abs(D_in[i][i] - D_out[i][i]) > 1) {

fl = 0;

}

S += D_out[i][i] == D_in[i][i] + 1;

T += D_in[i][i] == D_out[i][i] + 1;

for(int j = 0; j < 8; j++) {

if((D_in[i][i] + D_out[i][i]) && (D_in[j][j] + D_out[j][j])) {

fl &= C2[i][j];

}

}

}

if(fl && S == 1 && T == 1) {

Path::Main();

return 0;

}

cout << 0 << "\n";

}

*P5296 [北京省选集训2019] 生成树计数

前言:我做这个题时先入为主地想到了斯特林拆幂,其实二项式定理化成 EGF 的形式好像更简单!

发现是生成树相关内容的计数,直接考虑 Matrix Tree 定理。容易发现难点在于如何表示边权,使得边权之积包含了 \((\sum \omega(e)) ^ k\) 这个信息。

根据经典思路,先试试斯特林拆幂:\(n ^ k = \sum \limits_{i = 0} ^ k {k \brace i} \binom{n}{i} i!\)。则我们可以把问题进一步转化为,找到一种表示边权的方式,使得边权之积等于 \(\binom{\sum \omega(e)}{k}\)。

这等价于,我们需要通过关于 \(\binom{p}{k}\) 与 \(\binom{q}{k}\) 的信息,求积得到关于 \(\binom{p + q}{k}\) 的信息。这个 \(\binom{p + q}{k}\) 的形式容易让我们联想到范德蒙德卷积:\(\binom{p + q}{k} = \sum \limits_{i = 0} ^ {k} \binom{p}{i} \binom{q}{k - i}\),至此思路已经一目了然:将边权定为 \(W(e) = \sum \limits_{i = 0} ^ k \binom{\omega(e)}{i} x ^ i\),则 \([x ^ k]\prod \limits_{e \in T} W(e) = \binom{\sum \omega(e)}{k}\)。

运用 Matrix Tree 定理建立行列式后,通过行变换对行列式求值即可。将结果带回至斯特林拆幂的形式中即可得到答案。由于数据范围很小,暴力 \(\Theta(k ^ 2)\) 的卷积和求逆是比大常数的 \(\Theta(k \log k)\) 快的,因此采用暴力。时间复杂度 \(\Theta(k ^ 2 n ^ 3)\)。

行变换时需要用到多项式乘法逆,注意常数项为 \(0\) 的问题。

Code

#include

using namespace std;

using i64 = long long;

constexpr int mod = 998244353, N = 30 + 5;

namespace basic {

// ...

}

using namespace basic;

struct TinyPoly {

vector a;

TinyPoly() {}

inline void resize(int n) {

a.resize(n, 0);

}

TinyPoly(int n) {resize(n);}

TinyPoly(vector b) {a = b;}

inline int & operator [] (int x) {

// assert(x < a.size());

return a[x];

}

inline int size() {

return a.size();

}

inline void Rev() {

reverse(a.begin(), a.end());

}

inline int fir() {

for(int i = 0; i < a.size(); i++) {

if(a[i]) {

return i;

}

}

return -1;

}

inline void popfront(int k) {

assert(k <= a.size());

reverse(a.begin(), a.end());

while(k--) {

a.pop_back();

}

reverse(a.begin(), a.end());

}

inline friend TinyPoly operator + (TinyPoly x, TinyPoly y) {

TinyPoly ret(max(x.size(), y.size()));

for(int i = 0; i < x.size() || i < y.size(); i++) {

if(i < x.size()) {

ad(ret[i], x[i]);

}

if(i < y.size()) {

ad(ret[i], y[i]);

}

}

return ret;

}

inline friend TinyPoly operator - (TinyPoly x, TinyPoly y) {

TinyPoly ret(max(x.size(), y.size()));

for(int i = 0; i < x.size() || i < y.size(); i++) {

if(i < x.size()) {

ad(ret[i], x[i]);

}

if(i < y.size()) {

de(ret[i], y[i]);

}

}

return ret;

}

inline friend TinyPoly operator * (TinyPoly x, TinyPoly y) {

if(x.size() == 0 || y.size() == 0) {

return {};

}

TinyPoly ret(x.size() + y.size() - 1);

for(int i = 0; i < x.size(); i++) {

for(int j = 0; j < y.size(); j++) {

ad(ret[i + j], 1ll * x[i] * y[j] % mod);

}

}

return ret;

}

inline TinyPoly Inv(int n) {

assert(a[0]);

TinyPoly ret(n);

ret[0] = inv(a[0]);

for(int i = 1; i < n; i++) {

int C = 0;

for(int j = 0; j < i; j++) {

de(C, 1ll * a[i - j] * ret[j] % mod);

}

ret[i] = 1ll * C * ret[0] % mod;

}

return ret;

}

inline TinyPoly operator += (TinyPoly y) {

return (*this) = (*this) + y;

}

inline TinyPoly operator -= (TinyPoly y) {

return (*this) = (*this) - y;

}

inline TinyPoly operator *= (TinyPoly y) {

return (*this) = (*this) * y;

}

};

int k;

struct Determinant {

int n; TinyPoly a[N][N];

Determinant() {};

Determinant(int _) {n = _;}

inline TinyPoly* operator [] (int x) {

return a[x];

}

inline TinyPoly det() {

TinyPoly ret(k + 1); ret[0] = 1;

for(int i = 1; i <= k; i++) {

ret[i] = 0;

}

for(int i = 1; i <= n; i++) {

int it = -1, fir = k + 1;

for(int j = i; j <= n; j++) {

int p = a[j][i].fir();

if(p != -1 && p < fir) {

fir = p, it = j;

}

}

if(it == -1) {

return ret[0] = 0, ret;

}

if(it != i) {

swap(a[i], a[it]);

ret[0] = mod - ret[0];

}

TinyPoly Inv = a[i][i]; Inv.popfront(fir), Inv.resize(k + 1);

Inv = Inv.Inv(k + 1);

for(int j = i + 1; j <= n; j++) {

TinyPoly p = a[j][i]; p.popfront(fir);

TinyPoly D = p * Inv; D.resize(k + 1);

for(int t = i; t <= n; t++) {

a[j][t] -= D * a[i][t];

a[j][t].resize(k + 1);

}

}

}

for(int i = 1; i <= n; i++) {

ret *= a[i][i];

ret.resize(k + 1);

}

return ret;

}

};

int n; TinyPoly D[N][N], A[N][N];

inline TinyPoly generate(int w) {

TinyPoly ret(k + 1); ret[0] = 1;

for(int i = 0; i < k; i++) {

ret[i + 1] = 1ll * ret[i] * dec(w, i) % mod * inv(i + 1) % mod;

}

return ret;

}

int S2[N][N];

int main() {

ios::sync_with_stdio(false);

cin.tie(nullptr), cout.tie(nullptr);

fac_init();

cin >> n >> k;

for(int i = 1; i <= n; i++) {

for(int j = 1; j <= n; j++) {

int w; cin >> w;

if(i < j) {

TinyPoly W = generate(w);

D[i][i] += W, D[j][j] += W;

A[i][j] += W, A[j][i] += W;

}

}

}

Determinant L(n - 1);

for(int i = 1; i <= n; i++) {

for(int j = 1; j <= n; j++) {

L[i][j] = D[i][j] - A[i][j];

}

}

TinyPoly res = L.det();

// for(int i = 0; i <= k; i++) {

// cout << res[i] << " \n"[i == k];

// }

S2[0][0] = 1;

for(int i = 1; i <= k; i++) {

for(int j = 1; j <= i; j++) {

S2[i][j] = add(S2[i - 1][j - 1], 1ll * S2[i - 1][j] * j % mod);

}

}

int ans = 0;

for(int i = 0; i <= k; i++) {

ad(ans, 1ll * S2[k][i] * res[i] % mod * fac[i] % mod);

}

cout << ans << "\n";

}

【2025】網路正常 YouTube 跑不動?6 個最佳修復方案不容錯過!
Windows 10 版本 1909 終止服務 (家用版與專業版) - Microsoft Lifecycle