Lindström-Gessel-Viennot 引理随记
LGV 引理提供了一种在 DAG 上计算不交路径元组数量的方法。当然,完整地说应该是所有满足路径两两不交的 \(\mathcal{P} = (P_i : A_i \to B_{p_i})\) 的带符号和。因此它更常用于 Lattice Paths 的计算,在 OI 中则最常见为网格图 / 格点图。
令 \(G\) 为一带权有向无环图,起点集为 \(A = \{a_1, a_2, \dots, a_n\}\),终点集为 \(B = \{b_1, b_2, \dots, b_n\}\)。记 \(\omega(P)\) 为路径 \(P\) 上所有边权的乘积,\(e(u, v) = \sum \limits_{P : u \to v} \omega(P)\) 为所有从 \(u\) 到 \(v\) 的路径的权值和。我们按如下方式定义矩阵 \(\mathbf{M}\):
则根据 Lindström-Gessel-Viennot 引理,
其中 \(\mathcal{P}\) 为不交路径元组,即 \(i \neq j\) 时,\(P_i\) 与 \(P_j\) 没有相同顶点。
下面对此引理展开证明。直接展开行列式,得到
设 \(U\) 为不相交路径组,\(V\) 为相交路径组,记 \(\omega(\mathcal{P}) = \prod \limits_{i = 1} ^ n \omega(P_i)\),则问题显然可以被转化为,证明
考虑当一个路径组有公共交点时,取其中最小的和其他路径具有交点的 \(i\),记 \(P_i\) 上距离 \(a_i\) 最近的交点为 \(v\),取经过点 \(v\) 的路径中除 \(i\) 外最小的路径编号,记为 \(j\),则将 \(P_i, P_j\) 从 \(v\) 开始的路径进行交换,也即从 \(a_i \to b_{\sigma_i}, a_j \to b_{\sigma_j}\) 变为 \(a_i \to b_{\sigma_j}, a_j \to b_{\sigma_i}\)。若 \(\mathcal{P}\) 交换后的路径组为 \(f(\mathcal{P})\),那么可以证明,\(f\) 是一个双射,而且构成一个对合。
另外地,交换前后显然有 \(\omega(\mathcal{P}) = \omega(\mathcal{P}')\),且我们知道交换 \(\sigma_i, \sigma_j\) 后,排列逆序对数的奇偶性发生改变,即 \(\mathrm{sign}(\sigma) + \mathrm{sign}(\sigma') = 0\)。因此
而 \(\mathcal{P} \in U\) 的部分即为 LGV 引理。\(\square\)
值得一提的是,许多介绍 LGV 引理的博文都在构造 \(f\) 时给出了一种错误的做法,其方法为,取字典序最小的二元组 \((i, j)\) 并交换二者某个交点后的路径。这样做事实上是错误的,考虑这样一种情况:
在原路径组中,最小的符合条件的二元组为 \((1, 3)\),但交换 \((1, 3)\) 在交点后的路径后,新图最小的二元组变更为 \((1, 2)\),不满足 \(f(f(\mathcal{P})) = \mathcal{P}\)。由此可以发现,可能有若干个 \(\mathcal{P}'\) 满足 \(f(\mathcal{P}') = \mathcal{P}\),无法说明 \(f\) 是双射。
P6657 【模板】LGV 引理
显然 \(e(a_i, b_j) = \dbinom{n - 1 + b_j - a_i}{n - 1}\),由定理,列出 \(\mathbf{M}\) 后高斯消元求行列式即可。时间复杂度 \(\Theta(n + m ^ 3)\)。
不过借此题可以发现一个性质:当 \(\sigma \neq [1, 2, \dots, n]\) 时,一定不存在不交的路径组,这保证了 \(\det \mathbf{M}\) 求出的答案为 \(\sigma = [1, 2, \dots, n]\) 时不交路径组的数量。网格图上通常都具有类似的优秀性质,这也是 LGV 引理常见于网格图的原因。否则 \(\det \mathbf{M}\) 求出的是各种排列下的不交路径组数量的符号和,不经过题目特殊构造应该很难有用。
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
constexpr int mod = 998244353, N = 100 + 10, M = 2e6 + 10;
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[M], ifac[M];
inline void fac_init(int n = M - 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;
}
inline int binom(int n, int m) {
if(n < m || m < 0) return 0;
return 1ll * fac[n] * ifac[m] % mod * ifac[n - m] % mod;
}
}
using namespace basic;
struct Determinant {
int n, a[N][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[i][j] != 0) {
it = j;
break;
}
}
if(it == -1) {
return 0;
}
if(i != it) {
swap(a[i], a[it]);
coef = mod - coef;
}
for(int j = i + 1; j <= n; j++) {
int C = 1ll * a[j][i] * inv(a[i][i]) % mod;
a[j][i] = 0;
for(int k = i + 1; 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 a[N], b[N];
void Main() {
int n, m; cin >> n >> m;
for(int i = 1; i <= m; i++) {
cin >> a[i] >> b[i];
}
Determinant A; A.n = m;
for(int i = 1; i <= m; i++) {
for(int j = 1; j <= m; j++) {
A[i][j] = binom(n - 1 + b[j] - a[i], n - 1);
}
}
cout << A.det() << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
fac_init();
int T; cin >> T;
while(T--) {
Main();
}
}
P7736 [NOI2021] 路径交点
发现所求内容与不交路径组相关,因此考虑 LGV 引理。
根据经典式子,记 \(e(a_i, b_j)\) 表示从第一列第 \(i\) 个点到第 \(k\) 列第 \(j\) 个点的路径数,容易 dp 求出。考虑 \(\det \mathbf{M}\) 和题目中所求的内容有什么关联。
注意到唯一的差别在于,题目中所求内容的系数是 \((-1) ^ {\#\text{intersections}}\),而 \(\det \mathbf{M}\) 的系数是 \((-1) ^ {\tau(\sigma)}\)。考虑这样一种调整:一开始路径 \(P_i\) 为从第一列的第 \(i\) 个点出发,每一次都走向下一列的第 \(i\) 个点,在最后一层走向第 \(\sigma_i\) 个点。此时 \(\#\text{intersections} = \tau(\sigma)\)。考虑对于 \([2, k - 1]\) 列中的点作调整,将某条路径上在第 \(j\) 列上的点调整到另一个点上,容易证明交点数的奇偶性是不变的。因此我们可以直接通过 \(\det \mathbf{M}\) 求出答案。时间复杂度 \(\Theta(\sum kn^3)\)。
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
constexpr int mod = 998244353, N = 100 + 10;
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);}
}
using namespace basic;
struct Determinant {
int n, a[N][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) {
swap(a[i], a[it]);
coef = mod - coef;
}
for(int j = i + 1; j <= n; j++) {
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;
}
};
constexpr int M = 20000 + 10;
int k, V[N], E[N], P[N];
vector<int> G[M]; int f[M];
void Main() {
cin >> k;
for(int i = 1; i <= k; i++) {
cin >> V[i];
P[i] = P[i - 1] + V[i];
}
for(int i = 1; i < k; i++) {
cin >> E[i];
}
for(int i = 1; i <= P[k]; i++) {
G[i].clear();
}
for(int i = 1; i < k; i++) {
for(int j = 1; j <= E[i]; j++) {
int u, v; cin >> u >> v;
G[u + P[i - 1]].push_back(v + P[i]);
}
}
Determinant A; A.n = V[1];
for(int i = 1; i <= V[1]; i++) {
for(int j = 1; j <= P[k]; j++) {
f[j] = 0;
}
f[i] = 1;
for(int j = 1; j <= P[k]; j++) {
for(auto v : G[j]) {
ad(f[v], f[j]);
}
}
for(int j = 1; j <= V[1]; j++) {
A[i][j] = f[P[k - 1] + j];
}
}
cout << A.det() << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T; cin >> T;
while(T--) {
Main();
}
}
[ABC216H] Random Robots
首先我们可以将机器人的运动视作格点图上的行走:从 \((0, x_i)\) 起,记当前位于 \((x, y)\),停止一秒视作向上走到 \((x + 1, y)\),移动一秒视作向右上走到 \((x + 1, y + 1)\)。如果所有机器人的路径两两不交,方案合法。
因为涉及不交路径组,直接考虑 LGV 引理。
两点 \((0, x), (n, y)\) 间的路径数显然为 \(\binom{n}{y - x}\),如果我们能固定一个终点序列 \(\{y\}\),那么可以直接构造行列式求出不交路径组的数量。答案为 \(\dfrac{\sum \limits_{\{y\}} \det \mathbf{M}_y}{2 ^ {kn}}\)。
但是 \(\{y\}\) 的情况数太多了,考虑有没有其它的实现方法。\(k \leq 10\) 启发我们可以直接暴力将行列式展开为 \(k!\) 项,即
其中 \(a_{i, \sigma_i} = \binom{n}{y_{\sigma_i} - x_i}\)。
如果直接枚举 \(\sigma\),显然可以 dp 出 \(\sum \limits_{\{y\}} \prod \limits_{i = 1} ^ n a_{i, \sigma_i}\) 的值。
当然我们没有必要枚举 \(\sigma\),类似地,仍然考虑逐项确定 \(\{y\}\) 中的值,找到与之对应的 \(x_i\),最终可以通过 dp 解决。
具体来讲,记 \(f_{S, i}\) 表示起点集合 \(S\) 已被匹配,\(\{y\}\) 的最后一项为 \(i\) 时的答案。则
前缀和优化后可以做到 \(\Theta(2 ^ k nk)\) 转移。答案为 \(\dfrac{\sum_{i} f_{U, i}}{2 ^ {kn}}\)。
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
constexpr int mod = 998244353, N = 1000 + 10;
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;
}
int invx[N];
inline void inv_init(int n = N - 1) {
invx[1] = 1;
for(int i = 2; i <= n; i++)
invx[i] = 1ll * (mod - mod / i) * invx[mod % i] % mod;
}
inline int binom(int n, int m) {
if(n < m || m < 0) return 0;
return 1ll * fac[n] * ifac[m] % mod * ifac[n - m] % mod;
}
}
using namespace basic;
int k, n, x[10];
int f[1 << 10][N << 1], g[1 << 10][N << 1];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
fac_init();
cin >> k >> n;
for(int i = 0; i < k; i++) {
cin >> x[i]; x[i]++;
}
sort(x, x + k);
f[0][0] = 1;
for(int i = 0; i < (N << 1); i++) {
g[0][i] = 1;
}
for(int s = 1; s < (1 << k); s++) {
for(int i = 1; i < (N << 1); i++) {
for(int j = 0; j < k; j++) {
if(s >> j & 1) {
int coef = s >> j + 1;
coef = __builtin_popcount(coef) % 2 == 0 ? 1 : mod - 1;
ad(f[s][i], 1ll * g[s ^ (1 << j)][i - 1] * coef % mod * binom(n, i - x[j]) % mod);
}
}
g[s][i] = add(g[s][i - 1], f[s][i]);
}
}
int ans = g[(1 << k) - 1][(N << 1) - 1];
ans = 1ll * ans * inv(qpow(2, 1ll * k * n % (mod - 1))) % mod;
cout << ans << "\n";
}