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

\[\mathbf{M} = \begin{Bmatrix} e(a_1, b_1) & e(a_1, b_2) & \cdots & e(a_1, b_n) \\ e(a_2, b_1) & e(a_2, b_2) & \cdots & e(a_2, b_n) \\ \vdots & \vdots & \ddots & \vdots \\ e(a_n, b_1) & e(a_n, b_2) & \cdots & e(a_n, b_n) \end{Bmatrix} \]

则根据 Lindström-Gessel-Viennot 引理,

\[\det \mathbf{M} = \sum_{\mathcal{P} = (P_i : a_i \to b_{\sigma_i})} \mathrm{sign}(\sigma) \prod_{i = 1} ^ {n} \omega(P_i) \]

其中 \(\mathcal{P}\) 为不交路径元组,即 \(i \neq j\) 时,\(P_i\)\(P_j\) 没有相同顶点。

下面对此引理展开证明。直接展开行列式,得到

\[\begin{aligned} \det \mathbf{M} &= \sum_{\sigma \in S_n} \mathrm{sign}(\sigma) \prod_{i = 1} ^ n e(a_i, b_{\sigma_i}) \\ &= \sum_{\mathcal{P} = (P_i : a_i \to b_{\sigma_i})} \mathrm{sign}(\sigma) \prod_{i = 1} ^ n \omega(P_i) \end{aligned} \]

\(U\) 为不相交路径组,\(V\) 为相交路径组,记 \(\omega(\mathcal{P}) = \prod \limits_{i = 1} ^ n \omega(P_i)\),则问题显然可以被转化为,证明

\[\sum_{\mathcal{P} = (P_i : a_i \to b_{\sigma_i}), \mathcal{P} \in V} \mathrm{sign}(\sigma) \omega(\mathcal{P}) = 0 \]

考虑当一个路径组有公共交点时,取其中最小的和其他路径具有交点的 \(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\)。因此

\[\begin{aligned} \sum_{\mathcal{P} = (P_i:a_i \to b_{\sigma_i}), \mathcal{P} \in V} \mathrm{sign}(\sigma) \omega(\mathcal{P}) &= \frac 12 \sum_\mathcal{P} (\mathrm{sign}(\sigma) \omega(\mathcal{P}) + \mathrm{sign}(\sigma')\omega(\mathcal{P}')) \\ &= 0 \end{aligned} \]

\(\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!\) 项,即

\[\sum_{\{y\}} \sum_{\sigma} \mathrm{sign}(\sigma) \prod_{i = 1} ^ n a_{i, \sigma_i} \]

其中 \(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\) 时的答案。则

\[f_{S, i} = \sum_{j \in S} \sum_{t < i} f_{S \backslash \{j\}, t} \times (-1) ^ {\sum_{p \in S} [p > j]} \times \binom{n}{i - x_j} \]

前缀和优化后可以做到 \(\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";
}
posted @ 2024-01-01 15:53  ChroneZ  阅读(123)  评论(0)    收藏  举报