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\)

\[\mathbf{M}=\begin{Bmatrix}e_{A_1,B_1}&e_{A_1,B_2}&\cdot\cdot\cdot&e_{A_1,B_n}\\e_{A_2,B_1}&e_{A_2,B_2}&\cdot\cdot\cdot&e_{A_2,B_n}\\\vdots&\vdots&\ddots&\vdots\\e_{A_n,B_1}&e_{A_n,B_2}&\cdot\cdot\cdot&e_{A_n,B_n}\end{Bmatrix} \]

有结论:

\[\mathrm{det}(\mathbf{M})=\sum_{S:A\rightarrow B}\mathrm{sgn}(\mathcal{P})\prod_{i=1}^n w_{p_i} \]

其中 \(S:A\rightarrow B\) 是指枚举满足上述条件的合法不相交路径集合。

应用——以三道题目为例


CF348D Turtles

一张 \(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;
}

P6657 【模板】LGV 引理

\(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}\) 它等于:

\[e_{a_i,b_j}=\begin{cases}\dbinom{b_j-a_i+n-1}{n-1}&b_j\ge a_i\\0&\tt otherwise\end{cases} \]

边权设为 \(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;
}

P7736 [NOI2021] 路径交点

\(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;
}
posted @ 2022-03-17 21:40  zhiyangfan  阅读(613)  评论(1编辑  收藏  举报