笔记

字符串

KMP 模式匹配

作用

可以以 \(\mathcal{O}(n)\) 的时间复杂度判定字符串 \(A\) 是否为字符串 \(B\) 的子串。并求出 \(A\)\(B\) 中的出现次数和位置。

流程

  1. 自我匹配:求出一个数列 \(next\)\(next\) 表示\(A\) 中以 \(i\) 为结尾的非前缀子串\(A\) 的前缀能够匹配的最长长度。即:

    \[next_i = \max \{j\}(j\lt i, \forall k\in[1, j]\ A_{i - k + 1} = A_{k}) \]

    当不存在这样的 \(j\) 时,\(next_i = 0\)

  2. \(A\)\(B\) 进行匹配。求出另一个数列 \(f\)\(f\) 表示 \(B\) 中以 \(i\) 为结尾的子串\(A\) 的前缀能够匹配的最长长度。即:

    \[f_i = \max \{j\}(j \le i,\forall k\in[1,j]\ B_{i - k + 1} = A_k) \]

先考虑 \(next_i\) 的计算方法,定义满足 \(j\lt i, \forall k\in[1, j]\ A_{i - k + 1} = A_{k}\)\(j\)\(next_i\) 的候选项。

数学

欧几里得算法

\[\forall a,b\in\mathbb{N},b\not=0,\quad\gcd(a, b) = \gcd(b, a \bmod b) \]

证明

  1. \(a\lt b\),则 \(\gcd(b, a\bmod b) = \gcd(b, a) = \gcd(a, b)\)
  2. 否则,令 \(a = k \times b + a\bmod b\),有 \(\gcd(a, b) | a, \gcd(a, b)|k\times b\),显然 \(\gcd(a, b)\mid(a - k\times b)\),即 \(gcd(a, b)\mid a\bmod b\)

综上所述 \(\forall a,b\in\mathbb{N},b\not=0,\quad\gcd(a, b) = \gcd(b, a \bmod b)\)

根据该定理,可以写出求最大公约数的代码。

int gcd(int a, int b) {
  return b ? gcd(b, a % b) : a; // gcd(a, 0) = a
}

裴蜀定理

定理内容:

\[\forall a, b\in \mathbb{Z},\quad\exist x, y\in \mathbb{Z},ax+by = \gcd(a, b) \]

证明

在欧几里得算法的最后,有 \(x = 1, y = 0\),满足 \(a \times 1 + b\times 0 = a = \gcd(a, 0)\)

在欧几里得算法过程中,如果存在整数 \(x, y\),满足 \(b\times x +(a\bmod b)\times y = \gcd(b, a \bmod b) = \gcd(a, b)\),则令 \(x' = y, y' = x - \lfloor \frac{a}{b}\rfloor\times y\)\(ax'+by' = \gcd(a, b)\)

根据上述,总有 \(ax + by = \gcd(a, b)\)

应用

两个互质的数 \(a, b\),\(\{p\mid p = ax + by,x\in \mathbb{Z},y\in\mathbb{Z}\} = \mathbb{Z}\)

这是因为存在一对 \(x, y\),满足 \(ax + by = 1\)。则 \(kax+kby = k, k\in \mathbb{Z}\)

集合 \(M = \{u\mid u = 12m+8n+4l,\ m, n, l\in \mathbb{Z}\}\) 与集合 \(N = \{u\mid u = 20p + 16q + 12r,\ p,q,r\in \mathbb{Z}\}\) 的关系是__ 。

\(M = N\)

\(\because M = \{u\mid u = 4(3m+2n+l)\}, N = \{u\mid u = 4(5p+4q+3r)\}\),其中 \((3, 2, 1) = 1, (5, 4, 3) = 1\)

由裴蜀定理可知,\(M = N = \{t\mid 4t ,\ t \in \mathbb{Z}\}\)

逆元

线性逆元求法

模数 \(p\in \mathbb{P}\),令 \(k = p / i\)\(r = p \bmod i\)。得 \(k\times i + r\equiv 0 \pmod p\)

两边同时乘上 \(i^{-1}r^{-1}\) ,移项,得 \(i^{-1} \equiv -k\times r^{-1}\pmod p\)

\(inv_i = -\left\lfloor\dfrac{p}{i}\right\rfloor\times inv_{p\bmod i}\)。可以线性推导。

  inv[1] = 1;
  for (int i = 2; i <= 6e6; i++)
    inv[i] = (mod - mod / i) * inv[mod % i] % mod;

组合数

有质数模数的组合数求法

在模意义下,\(C_n^m = \dfrac{n!}{m!(n - m)!} = m!^{-1}\times(n - m)!^{-1}\times n!\)

\(\mathcal{O}(n)\) 地预处理后逆元,即可 \(\mathcal{O}(1)\) 地计算出范围内的组合数。

[!NOTE]

\(0! = 1\)

void init() {
  fac[0] = 1; for (int i = 1; i <= 6e6; i++) fac[i] = fac[i - 1] * i % mod;
  inv[1] = 1; for (int i = 2; i <= 6e6; i++) inv[i] = (mod - mod / i) * inv[mod % i] % mod;
  finv[0] = 1; for (int i = 1; i <= 6e6; i++) finv[i] = finv[i - 1] * inv[i] % mod;
}

i64 C(int x, int y) {
  if (x == y) return 1;
  if (x < y) return 0;
  if (y < 0) return 0;
  return fac[x] * finv[x - y] % mod * finv[y] % mod;
}

多项式

定义

\(f(x) = \sum \limits_{i = 0}^{n} a_i \times x^i\) 称为关于 \(x\)\(n\) 次多项式。同时为关于 \(x\)\(n\) 次函数 \(y = f(x)\)

给定 \((n + 1)\) 个点的坐标,可以唯一确定一个 \(n\) 次函数。

点值表示法:用函数图像上 \(n + 1\) 个不同的坐标表示函数的表示方法。

系数表示法:形如 \(f(x) = \sum \limits_{i = 0}^{n} a_i \times x^i\) 的表示方法。

运算

  • 多项式加减法:两个 \(n\) 次多项式 \(A(x)\)\(B(x)\) 相加减的结果为一个 \(n\) 次多项式

    \[f(x) = A(x) \pm B(x) = \sum_{i = 0}^n(a_i\pm b_i)x^i \]

  • 多项式乘法:两个 \(n\) 次多项式 \(A(x)\)\(B(x)\) 相加乘的结果为一个 \(2n\) 次多项式

    \[f(x) = A(x)\times B(x) = \sum_{i = 0}^{n}\sum_{j = 0}^{n}a_i\times b_j \times x^{i+j} \]

    同时可以写成这种形式

    \[f(x) = \sum_{i = 0}^{2n}\sum_{j = 0}^{min(i, n)}a_j\times b_{i-j}\times x^{i} \]

多项式算法

快速傅里叶变换

以下 \(n\)\(2^k, k\in \mathbb{N}^*\)

单位根的性质

常数 \(i\):满足 \(i^2 = -1\) 的向量。

  1. \((\omega_n^a)^b = \omega_n^{a+b}\)
  2. \(\omega_n^k = \cos k\frac{2\pi}{n} + i\sin k\frac{2\pi}{n}\)
  3. \(\omega_n^k = \omega_{2n}^{2k}\)
  4. \(\omega_n^{k + \frac{n}{2}} = -\omega_n^{k}\)
  5. \(\omega_n^0 = \omega_n^n = 1\)
离散傅里叶变换(DFT)

应用:求出 \(m\) 次多项式 \(A(x)\) 的点值表示法。

\(A(x)\) 补为 \(n\) 次多项式,补上的系数为 \(0\)

求出 \(n\) 个元素的点集 \(B\)\(B\) 的第 \(k\) 个元素为 \((\omega_n^{k}, A(\omega_n^{k}))\),即 \((\omega_n^k, \sum\limits_{i = 0}^{n - 1}a_i\times\omega_n^{ik})\)

暴力求出该点集是 \(\mathcal{O}(n^2)\) 的。

快速傅里叶变换(FFT)\(\mathcal{O}(n\log n)\) 地完成。

快速傅里叶变换(FFT)

用分治的思想,对 \(A(x)\) 的系数按照角标的奇偶性分类,使

\[A(x) = \sum \limits_{i = 0}^{n/2 - 1}a_{2i}x^{2i}+\sum \limits_{i=0}^{n/2 - 1}a_{2i + 1}x^{2i+1} \]

提出一个 \(x\),可得:

\[A(x) = \sum \limits_{i = 0}^{n/2 - 1}a_{2i}x^{2i}+x\sum \limits_{i=0}^{n/2 - 1}a_{2i + 1}x^{2i} \]

\[A_0(x) = \sum \limits_{i = 0}^{n/2 - 1}a_{2i}x^{i}, A_1(x) = \sum \limits_{i=0}^{n/2 - 1}a_{2i + 1}x^{i} \]

\[A(x) = A_0(x^2) + xA_1(x^2) \]

对于

\[0 \le k \lt \frac{n}{2},A(\omega_n^{k}) = A_0(\omega_n^{2k}) + \omega_n^{k}A_1(\omega_n^{2k}) \]

\[A(\omega_n^{k}) = A_0(\omega_{n/2}^{k}) + \omega_n^{k}A_1(\omega_{n/2}^{k}) \]

\[A(\omega_n^{n/2 + k}) = A_0(\omega_n^{n + 2k}) + \omega_n^{n / 2 + k}A_1(\omega_n^{n + 2k}) \]

\(\because \omega_n^{n + 2k}= \omega_n^{2k},\omega_n^{n/2 + k} = \omega_n^{-k}\)

\(\therefore A(\omega_n^{n/2 + k}) = A_0(\omega_n^{2k}) - \omega_n^{k}A_1(\omega_n^{2k})\)

观察上式,发现 \(A(\omega_n^{k}),A(\omega_n^{n/2 + k})\) 仅有一项系数不同,所以得出 \(A(\omega_n^{k})\) 后可以 \(\mathcal{O}(1)\) 地求 \(A(\omega_n^{n/2 + k})\)

显然求 \(A_0(\omega_{n/2}^k), A_1(\omega_{n/2}^{k})\) 与求原问题相同,所以可以 \(\mathcal{O}(n\log n)\) 地求出点集 \(B\)

离散傅里叶逆变换(IDFT)

应用:将用点集 \(B = \{(\omega_n^{k}, A(\omega_n^{k})),(\omega_n^{2k}, A(\omega_n^{2k})),\dots,(\omega_n^{n}, A(\omega_n^{n}))\}\) 表示的多项式用系数表示法表示。

点集 \(B\) 的第二项构成数列 \(b\)

求出一个数列 \(C\),其中 \(C_k = \dfrac{1}{n}\sum \limits_{i = 0}^{n - 1}b_i\omega_n^{-ki}\)

\(C\) 即为原系数数列。

证明:由 FFT 部分可得,\(C_k = \dfrac{1}{n}\sum \limits_{i = 0}^{n - 1}(\sum\limits_{j = 0}^{n - 1}a_j\omega_n^{ij})\omega_n^{-ki}\)

\(C_k = \dfrac{1}{n}\sum\limits_{i=0}^{n-1}\sum\limits_{j = 0}^{n - 1}a_j\omega_n^{i(j-k)}\)

化简可得:\(C_k = \dfrac{1}{n}\sum\limits_{j = 0}^{n - 1}a_j\sum \limits_{i = 0}^{n - 1}\omega_n^{i(j-k)}\)

\(j = k\) 时,\(\sum \limits_{i = 0}^{n - 1}\omega_n^{i(j-k)} = n\)

\(j \not=k\) 时,\(\sum \limits_{i = 0}^{n - 1}\omega_n^{i(j - k)}\)\(j - k \not= 0\),是等比数列。

可得 \(\sum \limits_{i=0}^{n - 1}\omega_n^{i(j-k)} = \dfrac{1-\omega_n^{n(j-k)}}{1-\omega_{n}^{j-k}} = \dfrac{1-1}{1-\omega_{n}^{j-k}} = 0\)

所以 \(C_k = \dfrac{1}{n}\sum\limits_{j = 0}^{n - 1}a_j\sum \limits_{i = 0}^{n - 1}\omega_n^{i(j-k)} = \dfrac{1}{n}\sum\limits_{j = 0}^{n - 1}a_j \times [j = k] = a_k\)

所以 \(C\) 即为原系数数列。

快速傅里叶变换在多项式乘法上的应用

过程

现有 \(n\) 次多项式 \(A(x)\)\(m\) 次多项式 \(B(x)\),求 \(A(x)\)\(B(x)\) 的卷积 \(F(x)\)

因为 \(F(x) = A(x) \times B(x)\),所以 \(F(x)\) 是一个 \(n + m - 1\) 次多项式。

\(k = 2^p, p \in \mathbb{N}^*, k\ge n + m - 1, 2^{p-1}\lt n+m-1\)

\(F(\omega_k^{i}) = A(\omega_k^{i})\times B(\omega_k^{i})\)。用 FFT \(\mathcal{O}(n\log n)\) 地求出 \(A(\omega_k^{i}),B(\omega_k^{i})\)。得到 \(F(x)\) 的点集表示。

对于 \(F(x)\) 的点集表示用 IFFT,得到 \(F(x)\) 原来的系数。

代码实现

FFT 的分类可以 \(\mathcal{O}(n\log n)\) 地预处理,这个过程叫做位逆序置换。

void rev(int k) {
  int p = 0, d = k >> 1;
  tax[p++] = 0, tax[p++] = d;

  for (int i = 2; i <= k; i <<= 1) {
    d >>= 1;
    LF(j, 0, i - 1) tax[p++] = tax[j] | d;
  }
}

在预处理后,FFT 是一个不断合并相邻区间的过程的过程。

void FFT(cd *A, int k) {
  LF(i, 1, k - 1) if (tax[i] > i)
    swap(A[tax[i]], A[i]); 

  for (int len = 2, M = 1; len <= k; M = len, len <<= 1) {
    cd W(cos(Pi / M), sin(Pi / M));

    for (int L = 0, R = len - 1; R < k; L += len, R += len) {
      cd w(1.0, 0.0);
      for (int p = L, lim = L + M; p < lim; p++) {
        cd x = A[p] + w * A[p + M], y = A[p] - w * A[p + M]; 
        A[p] = x, A[p + M] = y;
        w *= W;
      }
    }
  }
}

拉格朗日插值法

给定 \(n\) 个点 \((x_i, y_i)\),求出经过所有点的多项式 \(f(x)\)\(x\) 处的结果。

拉格朗日插值法可以 \(\mathcal{O}(n^2)\) 地解决这个问题。

拉格朗日插值法

拉格朗日基本多项式为:

\[\lambda_i(x) = \prod_{j = 0, j \not= i}^{n}\dfrac{x - x_j}{x_i - x_j} \]

显然 \(\lambda_i(x_i) = 1, \lambda_i(x_j) = 0\)

构造多项式 \(P(x)\)

\[P(x) = \sum_{i = 0}^ny_i\lambda_i(x) \]

\(P(x_i) = y_i\)

\(\lambda_i(x)\) 代入,得:

\[P(x) = \sum_{i = 0}^ny_i\prod_{j = 0,j\not=i}^{n}\dfrac{x - x_j}{x_i - x_j} \]

因为 \(n + 1\) 个点可以确定一个 \(n\) 次多项式,所以 \(P(x)\) 即为所求多项式,带入求值即可得到结果。

【模板】拉格朗日插值代码

i64 n, k;
i64 x[N], y[N];

i64 Pow(i64 a, i64 b) {
  i64 res = 1;

  for (; b; b >>= 1) {
    if (b & 1) res = res * a % mod;
    a = a * a % mod;
  }

  return res;
}

i64 Lagrange(i64 *x, i64 *y, i64 n, i64 k) {
  i64 res = 0;

  for (int i = 1; i <= n; i++) {
    i64 s1 = 1, s2 = 1;

    for (int j = 1; j <= n; j++) if (i ^ j) {
      s1 = s1 * (k - x[j]) % mod;
      s2 = s2 * (x[i] - x[j]) % mod;
    }

    res = (res + s1 * Pow(s2, mod - 2) % mod * y[i] % mod) % mod;
  }

  return ((res % mod) + mod) % mod;
}

int main() {
  cin >> n >> k;
  for (int i = 1; i <= n; i++)
    cin >> x[i] >> y[i];
  cout << Lagrange(x, y, n, k) << endl;
  return 0;
}

拉格朗日插值法求系数

\(P(x)\) 分为两部分,即:

\[K = \sum_{i = 0}^ny_i\prod_{j = 0, j \not= i}^n\dfrac{1}{x_i-x_j},f(x) = \prod_{j=0}^n(x - x_j) \]

得到:

\[P'(x) = Kf(x)=P(x)(x - x_i) \]

定义 \(P(x)_i\) 为多项式 \(P(x)\) 的第 \(i\) 项系数。

\[P'(x)_i = P(x)_{i - 1}-P(x)_ix_i\\P(x)_i = \dfrac{P(x)_{i - 1} - P'(x)_i}{x_i} \]

这就得到多项式的系数了。

杨氏矩阵(杨表)

定义

杨图:行长非严格单调递减的网格集合。

标准杨表:在杨图的 \(n\) 个方格中任意填入 \(1\)\(n\)。各行各列的数字严格递增。

臂长:单元格右边的单元格个数。

腿长:单元格下面的单元格个数。

臂长和腿长均不包括本单元格。

勾长:单元格右边、下面和自身的单元格个数。勾长等于臂长加腿长加 \(1\)

RSK 插入法

情景:将一个数 \(x\) 插入标准杨表中。

过程:

  1. 移动到第一行。
  2. 在这一行中找到大于 \(x\) 的最小数 \(k\)
    • 如果 \(k\) 存在,将 \(k\) 所在的单元格的值改为 \(x\)\(x \leftarrow k\),移动到下一行重复步骤二。
    • 否则将 \(x\) 直接插入这一行的末尾。

图论

无向图连通性

相关定义

在无向图中,删去后使得连通分量增加的点是割点,删去后使得连通分量增加的边称为割边,也称。不存在割点的无向连通图称为点双连通图,不存在割边的无向连通图称为边双连通图。一张图的极大点双连通子图称为点双连通分量(V-BCC),简称点双,极大边双连通子图称为边双连通分量(E-BCC),简称边双。若 \(u, v\) 在同一点双中,称 \(u,v\) 点双连通。若 \(u,v\) 在同一边双中,称 \(u,v\) 边双连通

边双连通分量

断开所有割边,整张图会裂成割边条数 \(w + 1\) 个连通块,这些连通块就是原图的边双。这说明边双由割边连接。将边双缩点,由相应的割边连接,将得到一颗树。在这颗树上任意连边,将得到一个环,环会将原来的环上的边双节点合并成更大的边双节点。

因为边双由割边相连,所以每个节点都恰属于一个边双。可以的到结论:边双连通具有传递性,即 \(a, b\) 边双连通,\(b, c\) 边双连通,则 \(a, c\) 边双连通。

边双中的任意一条边 \((u, v)\)。将其删去后,\(u,v\) 仍然连通,所以原边双中存在经过 \((u,v)\) 的回路。

而对于边双内任意两点 \(u, v(u\not=v)\),都需要至少断开两条边它们才不连通,所以对于边双内任意两点 \(u,v\),存在经过 \(u,v\) 的回路。

结论总结

  1. 边双连通分量具有传递性
  2. 对于边双内任意一条边 \((u, v)\),存在经过 \((u, v)\) 的回路。
  3. 对于边双内任意一点 \(u\),存在经过 \(u\) 的回路。
  4. 对于边双内任意两点 \(u, v(u\not= v)\),存在经过 \(u, v\) 的回路。

点双连通分量

Menger 定理及结论

边双连通分量缩点

考虑 \(G\) 的 DFS 树 \(T\),找到其中任意一条割边 \((u, v)\),满足 \(dfn_u \lt dfn_v\),其中 \(v\) 的子树中没有割边。\(v\) 的子树就是 \(G\) 上的边双。将 \(subtree(v)\) 从树上删去,再将该割边删去,剩下的图也是原图的边双。

与 Trajan 算法结合,具体的做法是:维护栈 \(S\),保存已经访问过还未删去的节点。若回溯时判断 \((u,v)\) 为割边,则将栈顶到 \(v\) 弹出。

栈中还会剩下一些点,独自形成一个点双。

P8436 【模板】边双连通分量 代码

#include <bits/stdc++.h>

using namespace std;

const int N = 5e5 + 5;
const int M = 4e6 + 5;

int head[N], ver[M], Next[M], tot = 1;
int n, m;
int dfn[N], low[N], dn;
int st[N], top;
vector<vector<int>> ans;

void add(int u, int v) {
    ver[++tot] = v;
    Next[tot] = head[u], head[u] = tot;
}

void form(int u) {
    vector<int> s;
    for (int x = 0; x != u; ) s.push_back(x = st[top--]);
    ans.push_back(s);
}

void tarjan(int u, int e) {
    dfn[u] = low[u] = ++dn;
    st[++top] = u;

    for (int i = head[u]; i; i = Next[i]) {
        if (i == (e ^ 1)) continue;
        int v = ver[i];

        if (!dfn[v]) {
            tarjan(v, i); low[u] = min(low[u], low[v]);
            if (low[v] > dfn[u]) form(v);
        } else low[u] = min(low[u], dfn[v]);
    }
}

int main() {
    cin >> n >> m;

    for (int i = 1, u, v; i <= m; i++) {
        cin >> u >> v;
        add(u, v), add(v, u);
    }

    for (int i = 1; i <= n; i++) 
        if (!dfn[i]) tarjan(i, 0), form(i);

    cout << ans.size() << endl;

    for (vector<int> s : ans) {
        cout << s.size() << " ";
        for (auto e : s) cout << e << " ";
        cout << endl;
    }
    return 0;
}

二分图

二分图:无向图 \((V,E)\) 存在两个非空无交集点集集合 \(A,B,A\cup B = V\),同一集合内部无边相连,则该无向图为一张二分图,\(A, B\) 分别为二分图的左部右部

集合B

集合A

a

b

c

d

e

f

g

二分图判定

定理:无向图是二分图,当且仅当图中无奇环

利用二分图定理,用 dfs 判断是否为二分图。

int dfs(int u, int cl) {  
  for (auto v : e[u]) {
    if (c[v] && c[u] == c[v]) return 0;
    if (!c[v]) {
      if (!dfs(v, -cl)) return 0;
    }
  }

  return 1;
}

二分图最大匹配

定义

图的匹配:一张无向图 \((V,E)\) 的一个边集 \(E'\),满足 \(\forall p, q \in E'\)\(p, q\) 不存在公共顶端点。\(E’\) 称为无向图的一组匹配。

二分图最大匹配:在二分图中包含边数最多的一组匹配。

对于一组匹配 \(S\)\(\forall e \in S\)\(e\)匹配边\(\forall e\in E \backslash S\)\(e\)非匹配边。匹配边的端点称为匹配点,其他点称为非匹配点

二分图中,一条连接两个非匹配点的路径 \(P\),如果非匹配边与匹配边在 \(P\) 上交替出现,称 \(P\)增广路

如图,\(a\rightarrow b \rightarrow c \rightarrow g \rightarrow d \rightarrow f\) 是一条增广路 \(P\)。其中粗线是 \(P\) 上的匹配边,虚线是 \(P\) 上非匹配边。该增广路连接了 \(a, f\) 两个非匹配点。

集合B

集合A

a

b

c

d

e

f

g

增广路的性质:

  1. 长度为奇数。
  2. 奇数边为匹配边,偶数边为非匹配边。
  3. 将增广路上的边翻转后,会得到比原匹配边数大 \(1\) 的匹配。

根据性质 \(3\) 可以得到结论:当二分图中不存在增广路时,找到最大匹配。

网络流

约定

网络:一个网络 \(G = (V, E)\) 是一张有向图。

容量:\(\forall (x,y) \in E\),有给定的权值 \(c(x,y)\),为边的容量。若 \((x,y)\notin E,c(x,y) = 0\)

源点,汇点:两个特殊节点 \(S, T\)\(S\in V, T\in V, S \not=T\)

流函数 \(f\):满以下足三条性质的实数函数。

  • 容量限制:\(f(x, y) \le c(x, y)\)
  • 斜对称:\(f(x,y) = -f(y,x)\)
  • 容量守恒:\(\forall x\not=S,x\not=T,\sum\limits_{(u,x)\in E}f(u,x)=\sum\limits_{(x,v)\in E}f(x,v)\),即流入量等于流出量。

流量:\(f(x,y)\) 称为边 \((x,y)\) 的流量。

剩余容量:\(c(x,y)-f(x,y)\) 为边 \((x,y)\) 的剩余容量。

网络的流量:\(\sum \limits_{(S,v)\in E} f(S,v)\) 的值。

最大流

使网络的流量最大。

Edmonds-Karp 增广路算法

算法思路

增广路:一条从 \(S\)\(T\) 的路径上剩余容量均大于 \(0\) 的路径。

如果存在增广路,让一股流从 \(S\) 沿增广路流向 \(T\) 能使网络的流量增大。

EK 算法的思想是不断用 bfs 寻找增广路,直到网络上不再存在增广路。

具体流程

找到一条\(S\)\(T\) 的增广路,同时计算贡献。

需要注意,该过程考虑反边。

实质:考虑反边是反悔贪心,在找到错误的增广路后,反边的剩余容量将会增加,流经过反边是撤销错误的流量。

Dinic 算法

EK 算法的局限

残量网络:网络中所有节点及剩余容量大于 \(0\) 的边构成的子图。

EK 算法每轮可能遍历整个残量网络,却只能找的一条增广路。

Dinic 算法的优化

考虑 EK 算法中的 bfs,可以用 \(d_x\) 表示 \(S\)\(x\) 的路径长度,称为深度。

分层图:在残量网络上满足 \(d_y = d_x + 1\) 的边 \((x,y)\) 构成和所有节点的子图,是有向无环图。

Dinic 算法重复以下步骤,直到残量网络中 \(S\) 不能到 \(T\)

  1. 在残量网络中构造分层图。
  2. 在分层图中通过 dfs 寻找增广路,同时计算贡献。每个点可以流出多条出边。

在 Dinic 过程,还有优化:

  • 当前弧优化:不加 vis 的 dfs 是指数级的复杂度。

    然而 Dinic 算法不能加 vis。 流需要多次经过一个点。

    显然如果点 \(x\) 之前被遍历到过,那么会有一些边不能经过,却会被遍历,需要删掉。

  • 无用点优化:dfs 过程中如果有一个点返回 \(0\),把其层次制为 \(0\),相当于把其删去。

    因为之后可能在遍历到它,但肯定不会有返回值。

复杂度上界为 \(\mathcal{O}(n^2m)\),很难达到,实际上效率更高。

代码实现
const int N = 205;
const int M = 5005;
const int inf = 2147483647;

int n, m, s, t;
int head[N], ver[M << 1], edge[M << 1], Next[M << 1], tot = 1;
int d[N], now[N];
queue<int> que;

void add(int u, int v, int w) {
  Next[++tot] = head[u];
  edge[tot] = w;
  ver[head[u] = tot] = v;
}

bool bfs() {
  memset(d, 0, sizeof(d));
  while (que.size()) que.pop();
  que.push(s), d[s] = 1; now[s] = head[s];

  while (que.size()) {
    int u = que.front(); que.pop();

    for (int i = head[u]; i; i = Next[i]) {
      if (edge[i] && !d[ver[i]]) {
        que.push(ver[i]);
        now[ver[i]] = head[ver[i]];
        d[ver[i]] = d[u] + 1;
        if (ver[i] == t) return 1;
      }
    }
  }

  return 0;
}

int dinic(int u, int f) {
  if (u == t || !f) return f;
  int res = f, k;

  for (int &i = now[u]; i; i = Next[i]) {
    if (edge[i] && d[ver[i]] == d[u] + 1) {
      k = dinic(ver[i], min(res, edge[i]));
      if (!k) d[ver[i]] = 0;
      edge[i] -= k; edge[i ^ 1] += k;
      res -= k;
      if (res <= 0) break;
    }
  } 

  return f - res;
}

int main() {
  cin >> n >> m >> s >> t;
  LF(i, 1, m) {
    int u, v, w; cin >> u >> v >> w;
    add(u, v, w), add(v, u, 0);
  }

  i64 ans = 0, f;
  while (bfs()) {
    while (f = dinic(s, inf)) ans += f;
  }

  cout << ans;
  return 0;
}

写网络流出过的锅

  • for (int &i = now[u]; i && res; i = Next[i])

    当前弧优化的跳出条件不能这么写。因为有最后一条边可能没流完,却被忽略。

    所以跳出条件要写在里面。

最小割

割:边集 \(E'\),删去后 \(S\)\(T\) 不连通。

最小割:\(\sum \limits_{(x,y)\in E'} c(x,y)\) 最小的割。

最大流最小割定理

最大流等于最小割。

posted @   FRZ_29  阅读(13)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示