LGV 引理
刚学完矩阵树定理,这不得再学个 LGV 引理。
前置知识:行列式,如果不会可以看矩阵树定理那篇博客。
正文——LGV 引理的内容
对于一个有向无环有权图,我们定义:
- \(s\) 为图上的一条路径。
- \(w_s\) 为路径上边的权值积。
- \(e_{u,v}\) 为 \(u,v\) 两点之间所有路径 \(s\) 的 \(w_s\) 之和,即:\[\sum_{s:u\rightarrow v}w_s \]
- 起点集合 \(A\) 和终点集合 \(B\) 均为原图点集的一个子集,且满足 \(|A|=|B|=n\)。
- 一组 \(A\rightarrow B\) 的不相交路径集合 \(S\),满足 \(s_i\) 是 \(A_i\) 到 \(B_{\mathcal{P}_i}\) 的一条路径(\(\mathcal{P}\) 是 \(1\sim |B|\) 的排列)且 \(\forall i\ne j\),都有 \(s_i,s_j\) 没有公共点。
- \(\mathrm{sgn}(\mathcal{P})\) 是 \(-1\) 当且仅当排列 \(\mathcal{P}\) 中逆序对个数为奇数,是 \(1\) 当且仅当 \(\mathcal{P}\) 中逆序对个数为偶数。
则对于矩阵 \(\bf M\):
有结论:
其中 \(S:A\rightarrow B\) 是指枚举满足上述条件的合法不相交路径集合。
应用——以三道题目为例
一张 \(n\) 行 \(m\) 列的网格图,图的某些格子上有障碍物,求出满足从 \((1,1)\) 到 \((n,m)\) 的两条不相交且均不经过障碍物的路径个数,答案对 \(10^9+7\) 取模,\((x,y)\) 一步只能走到 \((x+1,y)\) 或 \((x,y+1)\)。(\(2\le n,m\le 3\times10^3\))
看到不相交考虑 LGV 引理,因为所有路径不能在起点终点处相交,所以选定起点集合 \(A=\{(1,2),(2,1)\}\),终点集合 \(B=\{(n-1,m),(n,m-1)\}\),边权均设为 \(1\)。跑 LGV 引理即可,其中 \(e_{i,j}\) 可以通过 \(\rm dp\) 求解。这样做是对的原因是只有当 \(\mathcal{P}=(1,2)\) 时路径才不会交叉,而此时 \(\mathrm{sgn}(\mathcal{P})=1\)。时间复杂度 \(\mathcal{O}(nm)\)。
#include <cstdio>
#include <cstring>
const int N = 3e3 + 10, mod = 1e9 + 7; char mp[N][N]; int dp[N][N], n, m;
inline int f(int x1, int y1, int x2, int y2)
{
if (mp[x1][y1] == '#' || mp[x2][y2] == '#') return 0;
memset(dp, 0, sizeof (dp)); dp[x1][y1] = 1;
for (int i = 1; i <= x2; ++i) for (int j = 1; j <= y2; ++j)
if (mp[i][j] == '.') (dp[i][j] += dp[i - 1][j]) %= mod, (dp[i][j] += dp[i][j - 1]) %= mod;
return dp[x2][y2];
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) scanf("%s", mp[i] + 1);
int f11 = f(1, 2, n - 1, m), f12 = f(1, 2, n, m - 1), f21 = f(2, 1, n - 1, m), f22 = f(2, 1, n, m - 1);
int ans = (1ll * f11 * f22 % mod - 1ll * f21 * f12 % mod + mod) % mod;
printf("%d\n", ans); return 0;
}
\(T\) 组数据,每组给出一个 \(n\times n\) 的棋盘,棋子从 \((x,y)\) 一步只能走到 \((x+1,y)\) 或 \((x,y+1)\),有 \(m\) 个棋子,初始时第 \(i\) 个放在 \((1,a_i)\),有 \(m\) 个终点,第 \(i\) 个终点是 \((n,b_i)\)。求出有多少种方案,能使每个棋子都能从起点走到终点,且对于所有的棋子它们的路径不交,答案对 \(998,244,353\) 取模。(\(1\le T\le 5,2\le n\le 10^6,1\le m\le 100,1\le a_1\le a_2\le \cdot\cdot\cdot\le a_m\le n,1\le b_1\le b_2\le\cdot\cdot\cdot\le b_m\le n\))
注意到对于本题,只有当 \(\mathcal{P}=(1,2\cdot\cdot\cdot,m)\) 时才能找到这样的路径,此时 \(\mathrm{sgn}(\mathcal{P})=1\),可以套用 LGV 引理直接做。对于 \(e_{a_i,b_j}\) 它等于:
边权设为 \(1\),预处理阶乘,求出 \(\bf M\) 矩阵后直接做行列式即得答案,时间复杂度 \(\mathcal{O}(Tm^3+n)\)。
#include <cstdio>
#include <algorithm>
const int M = 110, N = 2e6 + 10, mod = 998244353;
int fac[N], ifac[N], *a[M], _a[M][M], x[M], y[M], n, m;
inline int ksm(int a, int b)
{
int ret = 1;
while (b)
{
if (b & 1) ret = 1ll * ret * a % mod;
a = 1ll * a * a % mod; b >>= 1;
}
return ret;
}
inline int C(int n, int m) { return 1ll * fac[n] * ifac[m] % mod * ifac[n - m] % mod; }
inline int det()
{
int ans = 1, f = 1;
for (int j = 1; j <= m; ++j)
{
for (int i = j; i <= m; ++i)
{
if (!a[i][j]) continue;
if (i != j) std::swap(a[i], a[j]), f *= -1;
break;
}
if (!a[j][j]) return 0;
ans = 1ll * ans * a[j][j] % mod; int inv = ksm(a[j][j], mod - 2);
for (int k = j; k <= m; ++k) a[j][k] = 1ll * a[j][k] * inv % mod;
for (int i = j + 1; i <= m; ++i) for (int k = j, t = a[i][j]; k <= m; ++k)
a[i][k] = (a[i][k] - 1ll * t * a[j][k] % mod + mod) % mod;
}
return (ans * f + mod) % mod;
}
int main()
{
int T; scanf("%d", &T); fac[0] = ifac[0] = 1;
for (int i = 1; i < N; ++i) fac[i] = 1ll * fac[i - 1] * i % mod;
ifac[N - 1] = ksm(fac[N - 1], mod - 2);
for (int i = N - 2; i >= 1; --i) ifac[i] = 1ll * ifac[i + 1] * (i + 1) % mod;
while (T--)
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) scanf("%d%d", &x[i], &y[i]), a[i] = _a[i];
for (int i = 1; i <= m; ++i)
for (int j = 1; j <= m; ++j) a[i][j] = x[i] <= y[j] ? C(y[j] - x[i] + n - 1, n - 1) : 0;
printf("%d\n", det());
}
return 0;
}
\(T\) 组数据,每组给出 \(k\) 层点,第 \(i\) 层点有 \(n_i\) 个,其中 \(n_1=n_k,n_1\le n_i\le 2n_1\)。第 \(i(1\le i<k)\) 层的点仅会向第 \(i+1\) 层的点连 \(m_i\) 条有向边,第 \(k\) 层点不向任何点连边。对于两条路径 \(P,Q\),设它们在第 \(j\) 层的连边为 \((P_j,P_{j+1}),(Q_j,Q_{j+1})\),则称这两个路径在第 \(j\) 层有交点,当且仅当:
\[(P_j-Q_j)(P_{j+1}-Q_{j+1})<0 \]定义一个路径的总相交次数为所有边两两的相交次数之和。求在选出 \(n_1\) 条互不相交的路径,满足均以第一层点为起点,第 \(k\) 层点为终点的所有方案中,总相交次数为偶数的方案减去总相交次数为奇数的方案是多少,答案对 \(998,244,353\) 取模。(\(2\le k,n_1\le 100,1\le T\le 5\))
偶数减去奇数已经很暗示行列式了,再考虑这个相交次数,对于 \(k=2\) 的情况,两个路径相交,如果顺次匹配的话,当且仅当其对应的 \(\mathcal{P}\) 中产生了一个逆序对。所以如果 \(k=2\),我们可以直接把原图的邻接矩阵当做 \(\bf M\) 矩阵做 LGV 引理,这样得到的就是相交偶数次减去相交奇数次。
那对于 \(k>2\) 的情况呢,一个显然的想法是拓展上面的思路,LGV 引理要求的是路径条数,那我们就把每一层对应的邻接矩阵乘起来,就能得到方案了,记这个矩阵为 \(\bf M\) 跑 LGV 引理,这题就做完了,比较严谨的证明可以去看题解区。时间复杂度 \(\mathcal{O}(Tn^3k)\)。
#include <cstdio>
#include <algorithm>
const int N = 300, mod = 998244353; int n[N], m[N], *a[N];
int ksm(int a, int b)
{
int ret = 1;
while (b)
{
if (b & 1) ret = 1ll * ret * a % mod;
a = 1ll * a * a % mod; b >>= 1;
}
return ret;
}
struct Matrix
{
int a[N][N], n, m;
Matrix operator*(const Matrix& x)
{
Matrix ret; ret.n = n; ret.m = x.m;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= x.m; ++j)
{
int add = 0;
for (int k = 1; k <= m; ++k) (add += 1ll * a[i][k] * x.a[k][j] % mod) %= mod;
ret.a[i][j] = add;
}
return ret;
}
void init(int tn, int tm)
{
n = tn; m = tm;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j) a[i][j] = 0;
}
}A, B;
int det(int n)
{
int ans = 1, f = 1;
for (int i = 1; i <= n; ++i) a[i] = A.a[i];
for (int j = 1; j <= n; ++j)
{
for (int i = j; i <= n; ++i)
{
if (!a[i][j]) continue;
if (i != j) std::swap(a[i], a[j]), f *= -1;
break;
}
if (!a[j][j]) return 0;
ans = 1ll * ans * a[j][j] % mod; int inv = ksm(a[j][j], mod - 2);
for (int k = j; k <= n; ++k) a[j][k] = 1ll * a[j][k] * inv % mod;
for (int i = j + 1; i <= n; ++i) for (int k = j, t = a[i][j]; k <= n; ++k)
a[i][k] = (a[i][k] - 1ll * t * a[j][k] % mod + mod) % mod;
}
return (ans * f + mod) % mod;
}
int main()
{
int T; scanf("%d", &T);
while (T--)
{
int k; 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]);
A.init(n[1], n[2]);
for (int i = 1, x, y; i <= m[1]; ++i) scanf("%d%d", &x, &y), A.a[x][y] = 1;
for (int i = 2; i < k; ++i)
{
B.init(n[i], n[i + 1]);
for (int j = 1, x, y; j <= m[i]; ++j) scanf("%d%d", &x, &y), B.a[x][y] = 1;
A = A * B;
}
printf("%d\n", det(n[1]));
}
return 0;
}