LGV 引理 学习笔记
引入
LGV 引理可以解决图上不相交路径计数问题。
注意,LGV 引理只适用于有向无环图。
定义
\(\omega(P)\) 表示路径 \(P\) 的边权积。
记 \(e(u,v)\) 表示 \(u\) 到 \(v\) 的所有路径的边权积的和 \(e(u,v) = \sum\limits_{S:u \rightarrow v} \omega(S)\),当边权为 \(1\) 时,也可以表示为 \(u\) 到 \(v\) 的路径总数。
起点集合 \(A\),有向无环图上的 \(n\) 个点构成的子集。
终点集合 \(B\),有向无环图上的 \(n\) 个点构成的子集。
\(A \rightarrow B\) 的一个不相交路径组 \(S\),满足 \(S_i\) 是一条 \(A_i\) 到 \(B_{\sigma(S)_i}\) 的路径,\(\sigma(S)\) 是一个排列,满足 \(\forall i,j \land i \ne j\), \(S_i\) 和 \(S_j\) 没有公共顶点。
\(\tau(\sigma)\) 表示排列 \(\sigma\) 的逆序对个数。
引理
对于一个行列式:
\(\text{det}(M) = \sum\limits_{S:A \rightarrow B} (-1)^{\tau(\sigma(S))} \prod\limits_{i = 1}^{n}\omega(S_i)\)。
\(S\) 表示不相交路径组的每一条 \(S\)。
因此,\(\text{det}(M)\) 表示不相交路径组的边权积的带符号和,当边权等于一时,就等于不相交路径组方案数的带符号和。
证明
由行列式的定义可得:
显然可以发现:\(\prod\limits_{i = 1} ^ n \sum\limits_{E:A_i \rightarrow B_{\sigma_i} } \omega(E)\) 其实是,点集 \(A\) 到点集 \(B\) 的所有路径组 \(S\) 的 \(\omega(S_i)\) 的和。
即:
此时可以将路径分为两类,一种是相交路径组 \(U\),另一种是不相交路径组 \(V\)。
此时只需要证明 \(\sum\limits_{V:A \rightarrow B} (-1) ^ {\tau(\sigma(V))} \prod\limits_{i = 1}^n \omega(V_i) = 0\) 即可。
那么可以考虑两条 \(V\) 中路径,\(V_i = A_i \rightarrow u \rightarrow B_{\sigma_i}, V_j = A_j \rightarrow u \rightarrow B_{\sigma_j}\)。
此时就肯定存在 \(V_x = A_i \rightarrow u \rightarrow B_{\sigma_j}, V_y = A_j \rightarrow u \rightarrow B_{\sigma_i}\)。
此时,\(\omega(V_i) \times \omega(V_j) = \omega(V_x) \times \omega(V_y)\)。
而两者因为交换了 \(B_{\sigma_i}\) 和 \(B_{\sigma_j}\),所以两者的排列的奇偶性变化,又因为对于 \(\forall i,j\) 都存在对应的 \(x,y\) 使得 \(\omega(V_i) \times \omega(V_j) = \omega(V_x) \times \omega(V_y)\) 且排列的奇偶性不同。
\(\therefore \sum\limits_{V:A \rightarrow B} (-1) ^ {\tau(\sigma)} \prod\limits_{i = 1}^n \omega(V_i) = 0\)
因此 \(\text{det}(M) = \sum\limits_{U:A \rightarrow B} (-1) ^ {\tau(\sigma(U))} \prod\limits_{i = 1}^n \omega(U_i)\)。
实现
以下为 【模板】LGV 引理 代码。
#include <cstdio>
#include <cmath>
#include <algorithm>
using u32 = unsigned int ;
using i64 = long long ;
using u64 = unsigned long long ;
const int N = 105 ;
const int mod = 998244353 ;
i64 fac[N * N * N * 2], inv[N * N * N * 2];
void prework(){
fac[0] = 1;
inv[0] = inv[1] = 1;
for (int i = 1; i <= 2000000; i++) {
fac[i] = fac[i - 1] * i % mod;
}
for (int i = 2; i <= 2000000; i++) {
inv[i] = (mod - mod / i) * inv[mod % i] % mod;
}
for (int i = 1; i <= 2000000; i++) {
inv[i] = inv[i - 1] * inv[i] % mod;
}
}
i64 C(int x, int y){
return fac[x] * inv[y] % mod * inv[x - y] % mod;
}
int n, m;
int a[N], b[N];
i64 ans;
i64 mat[N][N];
void Gauss(){
for (int i = 1; i <= m; i++) {
for (int j = i + 1; j <= m; j++) {
while (mat[i][i]) {
i64 val = mat[j][i] / mat[i][i];
for (int k = 1; k <= m; k++) {
mat[j][k] = (mat[j][k] - mat[i][k] * val % mod + mod) % mod;
}
std::swap(mat[i], mat[j]);
ans *= -1;
}
std::swap(mat[i], mat[j]);
ans *= -1;
}
}
}
void solve(){
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++) {
mat[i][j] = b[j] < a[i]? 0: C(b[j] - a[i] + n - 1, b[j] - a[i]);//通过组合数,求解两点之间路径数量
}
}
ans = 1;
Gauss();
for (int i = 1; i <= m; i++) {
ans = (ans * mat[i][i] % mod + mod) % mod;
}
printf("%lld\n", ans);
}
int main(){
prework();
int T;
scanf("%d", &T);
while (T--) {
solve();
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!