欢迎来到 xht37 的博客

二分图与网络流

本文包括了二分图与网络流的绝大部分重要知识点,并融汇了著名的网络流24题中的21题(剩下3题不是网络流题不知道为啥混进去了)。

限于篇幅和作者水平,本文省略了较多的证明过程和推导过程。作为一个OIer你要相信这并不重要QwQ

原文:https://www.cnblogs.com/xht37/p/10457051.html

二分图

如果一张无向图的 \(n(n \geq 2)\) 个节点可以分成 \(A,B\) 两个非空集合,其中 \(A \bigcap B\) 为空,并且在同一集合内的点之间都没有边相连,那么称这张无向图为一张二分图\(A,B\) 分别称为二分图的左部和右部。

二分图判定

定理

无向图是二分图⇔图中无奇环(长度为奇数的环)。

染色法

bool dfs(int x, int color) {
    v[x] = color;
    for (unsigned int i = 0; i < e[x].size(); i++) {
        int y = e[x][i];
        if (v[y] == color) return 0;
        if (!v[y] && !dfs(y, 3 - color)) return 0;
    }
    return 1;
}

inline bool pd() {
    for (int i = 1; i <= n; i++)
        if (!v[i] && !dfs(i, 1)) return 0;
    return 1;
}

【例题】P1525 关押罪犯

这道题可以用扩展域并查集解决。

#include <bits/stdc++.h>
using namespace std;
const int N = 4e4 + 6, M = 1e5 + 6;
int n, m, fa[N];
struct P {
    int a, b, c;
    inline bool operator < (const P o) const {
        return c > o.c;
    }
} p[M];

int get(int x) {
    if (fa[x] == x) return x;
    return fa[x] = get(fa[x]);
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= m; i++)
        scanf("%d %d %d", &p[i].a, &p[i].b, &p[i].c);
    sort(p + 1, p + m + 1);
    for (int i = 1; i <= n << 1; i++) fa[i] = i;
    for (int i = 1; i <= m; i++) {
        int x = get(p[i].a), y = get(p[i].b);
        int xx = get(p[i].a + n), yy = get(p[i].b + n);
        if (x == y) {
            cout << p[i].c << endl;
            return 0;
        }
        fa[x] = yy;
        fa[y] = xx;
    }
    puts("0");
    return 0;
}

也可以用二分图判定配合二分答案解决。

#include <bits/stdc++.h>
#define pii pair<int, int>
using namespace std;
const int N = 2e4 + 6, M = 2e5 + 6;
struct P {
    int x, y, z;
    bool operator < (const P w) const {
        return z > w.z;
    }
} p[M];
int n, m, v[N];
vector<pii> e[N];

bool dfs(int x, int color) {
    v[x] = color;
    for (unsigned int i = 0; i < e[x].size(); i++) {
        int y = e[x][i].first;
        if (v[y] == color) return 0;
        if (!v[y] && !dfs(y, 3 - color)) return 0;
    }
    return 1;
}

inline bool pd(int now) {
    for (int i = 1; i <= n; i++) e[i].clear();
    for (int i = 1; i <= m; i++) {
        if (p[i].z <= now) break;
        e[p[i].x].push_back(make_pair(p[i].y, p[i].z));
        e[p[i].y].push_back(make_pair(p[i].x, p[i].z));
    }
    memset(v, 0, sizeof(v));
    for (int i = 1; i <= n; i++)
        if (!v[i] && !dfs(i, 1)) return 0;
    return 1;
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= m; i++)
        scanf("%d %d %d", &p[i].x, &p[i].y, &p[i].z);
    sort(p + 1, p + m + 1);
    int l = 0, r = p[1].z;
    while (l < r) {
        int mid = (l + r) >> 1;
        if (pd(mid)) r = mid;
        else l = mid + 1;
    }
    cout << l << endl;
    return 0;
}

【练习】UVA1627 团队分组 Team them up!

二分图最大匹配

图的匹配

任意两条边都没有公共端点的边的集合被称为图的一组匹配

二分图最大匹配

在二分图中,包含边数最多的一组匹配被称为二分图的最大匹配

其他相关定义

对于任意一组匹配 \(S\) (边集),属于 \(S\) 的边被称为匹配边,不属于 \(S\) 的边被称为非匹配边

匹配边的端点被称为匹配点,其他节点被称为非匹配点

如果二分图中存在一条连接两个非匹配点的路径 \(path\) ,使得非匹配边与匹配边在 \(path\) 上交替出现,那么称 \(path\) 是匹配 \(S\)增广路(也称交错路)。

增广路的性质

  1. 长度为奇数
  2. 奇数边是非匹配边,偶数边是匹配边。
  3. 如果把路径上所有边的状态(是否为匹配边)取反,那么得到的新的边集 \(S'\) 仍然是一组匹配,并且匹配的边数增加了 \(1\)

结论

二分图的一组匹配 \(S\) 是最大匹配⇔图中不存在 \(S\) 的增广路。

匈牙利算法(增广路算法)

主要过程

  1. \(S\) 为空集,即所有边都是非匹配边。
  2. 寻找增广路 \(path\) ,把 \(path\) 上所有边的匹配状态取反,得到一个更大的匹配 \(S'\)
  3. 重复第 \(2\) 步,直至图中不存在增广路。

寻找增广路

依次尝试给每一个左部节点 \(x\) 寻找一个匹配的右部节点 \(y\)

\(y\)\(x\) 匹配需满足下面两个条件之一:

  1. \(y\) 是非匹配点。
  2. \(y\) 已与 \(x'\) 匹配,但从 \(x'\) 出发能找到另一个 \(y'\) 与之匹配。

时间复杂度

\(O(nm)\)

【模板】P3386 【模板】二分图匹配

#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 6;
int n, m, t, f[N], ans;
vector<int> e[N];
bitset<N> v;

bool dfs(int x) {
    for (unsigned int i = 0; i < e[x].size(); i++) {
        int y = e[x][i];
        if (v[y]) continue;
        v[y] = 1;
        if (!f[y] || dfs(f[y])) {
            f[y] = x;
            return 1;
        }
    }
    return 0;
}

int main() {
    cin >> n >> m >> t;
    while (t--) {
        int x, y;
        scanf("%d %d", &x, &y);
        if (x > n || y > m) continue;
        e[x].push_back(y + n);
        e[y+n].push_back(x);
    }
    for (int i = 1; i <= n; i++) {
        v.reset();
        ans += dfs(i);
    }
    cout << ans << endl;
    return 0;
}

【练习】P2756 飞行员配对方案问题

【练习】P1402 酒店之王

【例题】U64949 棋盘覆盖

如果 \(n,m\) 较小,可以用状压DP解决。但本题的数据范围并不允许我们这么做。

二分图匹配模型的两个要素

  1. 节点能分成两个集合,每个集合内部有 \(0\) 条边。简称 \(0\) 要素。
  2. 每个节点只能与 \(1\) 条匹配边相连。简称 \(1\) 要素。

两个要素在本题中的体现

\(1\) 要素:每个格子只能被一张骨牌覆盖,一张骨牌覆盖 \(2\) 个相邻的格子。

把棋盘上没有被禁止的格子作为节点,骨牌作为(两个相邻的格子对应的节点之间连边)。

\(0\) 要素:把棋盘黑白相间染色,相同颜色的格子不可能被同一骨牌覆盖。

那么,若把白色格子作为左部节点,黑色格子作为右部节点,则刚才建立的无向图是二分图。

使用匈牙利算法求上述二分图的最大匹配,时间复杂度为 \(O(n^2m^2)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 106;
const int dx[4] = {0,0,1,-1};
const int dy[4] = {1,-1,0,0};
int n, m, ans, f[N*N];
bool b[N][N], v[N*N];
vector<int> e[N*N];

bool dfs(int x) {
    for (unsigned int i = 0; i < e[x].size(); i++) {
        int y = e[x][i];
        if (v[y]) continue;
        v[y] = 1;
        if (f[y] == -1 || dfs(f[y])) {
            f[y] = x;
            return 1;
        }
    }
    return 0;
}

int main() {
    cin >> n >> m;
    while (m--) {
        int x, y;
        scanf("%d %d", &x, &y);
        b[x-1][y-1] = 1;
    }
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            if (!b[i][j])
                for (int k = 0; k < 4; k++) {
                    int x = i + dx[k], y = j + dy[k];
                    if (x >= 0 && x < n && y >= 0 && y < n && !b[x][y]) {
                        e[i*n+j].push_back(x * n + y);
                        e[x*n+y].push_back(i * n + j);
                    }
                }
    memset(f, -1, sizeof(f));
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++) {
            if ((i ^ j) & 1) continue;
            memset(v, 0, sizeof(v));
            ans += dfs(i * n + j);
        }
    cout << ans << endl;
    return 0;
}

【例题】U64965 車的放置

\(1\) 要素:每行、每列只能放 \(1\) 个車。

把行、列作为节点,如果一个格子没有被禁止,则在此格子所在行和列对应的节点连

\(0\) 要素:行节点之间没有边,列节点之间也没有边。

那么,若把行节点作为左部节点,列节点作为右部节点,则刚才建立的无向图是二分图。

使用匈牙利算法求上述二分图的最大匹配,时间复杂度为 \(O((n+m)nm)\)

【练习】ZOJ1654 Place the Robots

【练习】P1129 [ZJOI2007]矩阵游戏

【练习】P1963 [NOI2009]变换序列

完备匹配

给定一张二分图,其左部、右部节点数均为 \(n\) 个节点。如果该二分图的最大匹配包含 \(n\) 条匹配边,则称该二分图具有完备匹配

多重匹配

给定一张包含 \(n\) 个左部节点、 \(m\) 个右部节点的二分图。从中选出尽量多的边,使第 \(i\) 个左部节点至多与 \(kl_i\) 条选出的边相连,第 \(j\) 个右部节点至多与 \(kr_i\) 条选出的边相连。该问题被称为二分图的多重匹配

方法

  1. 拆点求解二分图最大匹配。
  2. 网络流。

二分图带权匹配

给定一张二分图,二分图的每条边都带有一个权值。求出该二分图的一组最大匹配,使得匹配边的权值总和最大。这个问题称为二分图的带权最大匹配(二分图最优匹配)。

解法

  1. KM算法。
  2. 费用流。

KM算法的优劣

  • 优:程序实现简单,稠密图效率一般高于费用流,时间复杂度 \(O(n^3)\)
  • 劣:只能求解带权最大匹配一定是完备匹配的问题。

本文选择不介绍KM算法,感兴趣的读者可自行研究。其实是因为作者不会

二分图最小点覆盖

给定一张二分图,求出一个最小的点集 \(S\) ,使得图中任意一条边都已至少一个端点属于 \(S\) 。这个问题被称为二分图的最小点覆盖,简称最小覆盖。

定理

二分图最小点覆盖包含的点数 \(=\) 二分图最大匹配包含的边数。

【例题】UVA1194 Machine Schedule

二分图最小覆盖模型的要素

每条边有两个端点,二者至少选择一个。简称 \(2\) 要素。

\(2\) 要素在本题中的体现

每个任务要么在 \(A\) 上以 \(a_i\) 模式执行,要么在机器 \(B\) 上以 \(b_i\) 模式执行。

\(A,B\)\(m\) 种模式分别作为 \(m\) 个左部点和右部点,每个任务作为边连接左部 \(a_i\) 节点和右部 \(b_i\) 节点。

求这张二分图的最小覆盖,时间复杂度为 \(O(nm)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 106;
int n, m, k, f[N], ans;
bool v[N];
vector<int> e[N];

bool dfs(int x) {
    for (unsigned int i = 0; i < e[x].size(); i++) {
        int y = e[x][i];
        if (v[y]) continue;
        v[y] = 1;
        if (!f[y] || dfs(f[y])) {
            f[y] = x;
            return 1;
        }
    }
    return 0;
}

inline void Machine_Schedule() {
    cin >> m >> k;
    for (int i = 1; i <= n; i++) e[i].clear();
    for (int i = 0; i < k; i++) {
        int x, y;
        scanf("%d %d %d", &i, &x, &y);
        e[x].push_back(y);
    }
    memset(f, 0, sizeof(f));
    ans = 0;
    for (int i = 1; i <= n; i++) {
        memset(v, 0, sizeof(v));
        ans += dfs(i);
    }
    cout << ans << endl;
}

int main() {
    while (cin >> n && n) Machine_Schedule();
    return 0;
}

【例题】POJ2226 Muddy Fields

在一块 \(n \times m\) 的地面上,有一些格子是泥泞的,有一些格子是干净的。现在需要用一些宽度为 \(1\) 、长度任意的木板把泥地盖住,同时不能盖住干净的地面,木板可以重叠。求最少需要多少块木板。 \(n,m \leq 50\)

\(2\) 要素:每块泥地要么被横着的木板盖住,要么被竖着的木板盖住。

横着的木板只能放在同一行若干个连续的泥地上,称这种连续的泥地为行泥泞块;竖着的木板只能放在同一列若干个连续的泥地上,称这种连续的泥地为列泥泞块

把行泥泞块作为左部节点,列泥泞块作为右部节点。对于每块泥地,在其对应的行泥泞块和列泥泞块之间连边。

求出这张二分图的最小覆盖即可。

二分图最大独立集

图的独立集

在一张无向图中,满足任意两点之间都没有边相连的点集被称为图的独立集。包含点数最多的一个被称为图的最大独立集

图的团

在一张无向图中,满足任意两点之间都有边相连的子图被称为图的团。包含点数最多的一个被称为图的最大团

定理

  1. 无向图 \(G\) 的最大团 \(=\) 补图 \(G'\) 的最大独立集。
  2. 对于一般无向图,最大团、最大独立集是 NPC 问题。
  3. \(G\) 是有 \(n\) 个节点的二分图, \(G\) 的最大独立集大小 \(=\) \(n-\) 最小点覆盖数 \(=\) \(n-\) 最大匹配数。

【例题】P3355 骑士共存问题

黑白相间染色棋盘,把黑、白色格子分别作为左、右部节点。若两个格子是“日”字对角,则在对应的节点之间连边。“日”字对角的两个节点显然颜色一定不同。

求上述二分图的最大独立集即可。

#include <bits/stdc++.h>
using namespace std;
const int N = 206;
const int dx[8] = {-2,-2,-1,-1,1,1,2,2};
const int dy[8] = {-1,1,-2,2,-2,2,-1,1};
int n, m, ans, f[N][N][2];
bool a[N][N], v[N][N];

inline bool dfs(int x, int y) {
    for (int i = 0; i < 8; i++) {
        int nx = x + dx[i], ny = y + dy[i];
        if (nx < 1 || ny < 1 || nx > n || ny > n) continue;
        if (a[nx][ny] || v[nx][ny]) continue;
        v[nx][ny] = 1;
        if (!f[nx][ny][0] || dfs(f[nx][ny][0], f[nx][ny][1])) {
            f[nx][ny][0] = x;
            f[nx][ny][1] = y;
            return 1;
        }
    }
    return 0;
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int x, y;
        scanf("%d %d", &x, &y);
        a[x][y] = 1;
    }
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++) {
            if (((i ^ j) & 1) || a[i][j]) continue;
            memset(v, 0, sizeof(v));
            ans += dfs(i, j);
        }
    cout << n * n - m - ans << endl;
    return 0;
}

注意:此程序无法AC,会TLE若干个点,供学习匈牙利算法参考,正解为网络流。

【例题】P2423 [HEOI2012]朋友圈

观察原图:A国中友善度奇偶性不同的点之间有边,B国中所有奇数之间有边、所有偶数之间有边、奇偶不同的数之间一部分有边。

观察朋友圈的定义:朋友圈是一个团。

一般图的最大团问题是 NPC 的,然而这个图的补图很特殊。

观察补图:A国的奇数值点构成完全图,偶数值点构成完全图;B国的奇数点和偶数点构成二分图。

补图上朋友圈的定义:朋友圈是一个独立集(最大团 \(=\) 补图最大独立集)。

A国最多取两个点,可以枚举这两个点 \(x,y\) ;数据给定了A和B国之间有边的情况,删除与 \(x\)\(y\) 之间无边的B国点;然后在B国剩余的点上求二分图的最大独立集。

有向无环图的最小路径点覆盖

给定一张有向无环图,要求用尽量少的不相交的简单路径,覆盖有向无环图的所有顶点(也就是每个顶点恰好被覆盖一次)。这个问题被称为有向无环图的最小路径点覆盖,简称最小路径覆盖

解法

把每个点拆成编号为 \(x\)\(x + n\) 的两个点。建立一张新的二分图, \(1\) ~ \(n\) 是左部点, \(n + 1\) ~ \(2n\) 是右部点。对原图的每条有向边 \((x,y)\) ,在二分图的左部点 \(x\) 与 右部点 \(y + n\) 之间连边。最终得到的二分图成为原图的拆点二分图

定理

有向无环图的最小路径点覆盖包含的路径条数 \(=\) \(n-\) 其拆点二分图的最大匹配数。

【练习】P2764 最小路径覆盖问题

【练习】UVA1184 Air Raid

有向无环图的最小可重复路径点覆盖

给定一张有向无环图,要求用尽量少的可相交的简单路径,覆盖有向无环图的所有顶点(也就是每个顶点可以被覆盖多次)。这个问题被称为有向无环图的最小可重复路径点覆盖

解法

对有向图进行传递闭包,转化为最小路径覆盖问题。

网络流

一个网络 \(G = (V,E)\) 是一张有向图,图中每条有向边 \((x,y) \in E\) 都有一个给定的权值 \(c(x,y)\) ,称为边的容量。特别地,若 \((x,y) \notin E\) ,则 \(c(x,y) = 0\) 。图中还有两个指定的特殊节点 \(S,T \in V(S \neq T)\) 分别被称为源点汇点

\(f(x,y)\) 是定义在节点二元组 \((x \in V,y \in V)\) 上的实数函数,且满足:

  1. 容量限制\(f(x,y) \leq c(x,y)\)
  2. 斜对称\(f(x,y)=-f(y,x)\)
  3. 流量守恒\(\forall x \neq S,x \neq T\)\(\sum_{(u,x)\ \in E} f(u,x) = \sum_{(x,v)\ \in E} f(x,v)\)

\(f\) 称为网络的函数。对于 \((x,y) \in E\)\(f(x,y)\) 称为边的流量\(c(x,y) - f(x,y)\) 称为边的剩余流量

\(\sum_{(S,v)\ \in E} f(S,v)\) 称为整个网络的流量( \(S\) 为源点)。

通俗地讲,我们想象一下自来水厂到你家的水管网是一个复杂的有向图,每一节水管都有一个最大承载流量。自来水厂不放水,你家就断水了。但是就算自来水厂拼命的往管网里面注水,你家收到的水流量也是上限(毕竟每根水管承载量有限)。你想知道你能够拿到多少水,这就是一种网络流问题。

最大流

对于一个给定的网络,使整个网络的流量最大的合法的流函数被称为网络的最大流,此时的流量被称为网络的最大流量

简单来说就是水流从一个源点 \(S\) 通过很多路径,经过很多点,到达汇点 \(T\) ,问你最多能有多少水能够到达 \(T\) 点。

EK(Edmond—Karp)增广路算法

增广路

增广路是指从 \(S\)\(T\) 的一条路,流过这条路,使得当前的流量可以增加。

那么求最大流问题可以转换为不断求解增广路的问题,并且,显然当图中不存在增广路时就达到了最大流。

具体怎么操作呢?

其实很简单,直接从 \(S\)\(T\) 广搜即可,从 \(S\) 开始不断向外广搜,通过权值大于 \(0\) 的边(因为后面会减边权值,所以可能存在边权为0的边),直到找到 \(T\) 为止,然后找到该路径上边权最小的边,记为 \(minf\) ,然后最大流加 \(minf\) ,然后把该路径上的每一条边的边权减去 \(minf\)然后把该路径上的每一条边的反向边的边权加上 \(minf\),直到找不到一条增广路为止。

时间复杂度

\(O(nm^2)\) ,实际运用中远远达不到,能够处理 \(10^3\) ~ \(10^4\) 规模的网络。

【模板】P3376 【模板】网络最大流

#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 6, M = 2e5 + 6, inf = 1e9;
int n, m, s, t, ans, now[N], pre[N];
int Head[N], Edge[M], Leng[M], Next[M], tot = 1;
bitset<N> v;

inline void add(int x, int y, int z) {
    Edge[++tot] = y;
    Leng[tot] = z;
    Next[tot] = Head[x];
    Head[x] = tot;
}

inline bool bfs() {
    v.reset();
    queue<int> q;
    q.push(s);
    v[s] = 1;
    now[s] = inf;
    while (q.size()) {
        int x = q.front();
        q.pop();
        for (int i = Head[x]; i; i = Next[i]) {
            int y = Edge[i], z = Leng[i];
            if (v[y] || !z) continue;
            now[y] = min(now[x], z);
            pre[y] = i;
            if (y == t) return 1;
            q.push(y);
            v[y] = 1;
        }
    }
    return 0;
}

inline void upd() {
    ans += now[t];
    int x = t;
    while (x != s) {
        int i = pre[x];
        Leng[i] -= now[t];
        Leng[i^1] += now[t];
        x = Edge[i^1];
    }
}

int main() {
    cin >> n >> m >> s >> t;
    for (int i = 1; i <= m; i++) {
        int x, y, z;
        scanf("%d %d %d", &x, &y, &z);
        add(x, y, z);
        add(y, x, 0);
    }
    while (bfs()) upd();
    cout << ans << endl;
    return 0;
}

【练习】P2740 [USACO4.2]草地排水Drainage Ditches

Dinic算法

残量网络

在任意时刻,网络中所有节点以及剩余容量大于 \(0\) 的边构成的子图被称为残量网络。EK算法每轮可能会遍历整个残量网络,但只找出 \(1\) 条增广路。

分层图

节点层次 \(d_x\) 表示 \(S\)\(x\) 最少需要经过的边数。在残量网络中,满足 \(d_y = d_x + 1\) 的边 \((x,y)\) 构成的子图被称为分层图。显然,分层图是一张有向无环图

算法步骤

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

  1. 在残量网络上 BFS 求出节点层次,构造分层图。
  2. 在分层图上 DFS 寻找增广路,在回溯时实时更新剩余容量。另外,每个点可以流向多条出边,同时还加入若干剪枝。

时间复杂度

\(O(n^2m)\) ,实际运用中远远达不到,能够处理 \(10^4\) ~ \(10^5\) 规模的网络。

【模板】P3376 【模板】网络最大流

#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 6, M = 2e5 + 6, inf = 1e9;
int n, m, s, t, ans, d[N];
int Head[N], Edge[M], Leng[M], Next[M], tot = 1;

inline void add(int x, int y, int z) {
    Edge[++tot] = y;
    Leng[tot] = z;
    Next[tot] = Head[x];
    Head[x] = tot;
}

inline bool bfs() {
    memset(d, 0, sizeof(d));
    queue<int> q;
    q.push(s);
    d[s] = 1;
    while (q.size()) {
        int x = q.front();
        q.pop();
        for (int i = Head[x]; i; i = Next[i]) {
            int y = Edge[i], z = Leng[i];
            if (d[y] || !z) continue;
            q.push(y);
            d[y] = d[x] + 1;
            if (y == t) return 1;
        }
    }
    return 0;
}

int dinic(int x, int flow) {
    if (x == t) return flow;
    int rest = flow;
    for (int i = Head[x]; i && rest; i = Next[i]) {
        int y = Edge[i], z = Leng[i];
        if (d[y] != d[x] + 1 || !z) continue;
        int k = dinic(y, min(rest, z));
        if (!k) d[y] = 0;
        else {
            Leng[i] -= k;
            Leng[i^1] += k;
            rest -= k;
        }
    }
    return flow - rest;
}

int main() {
    cin >> n >> m >> s >> t;
    for (int i = 1; i <= m; i++) {
        int x, y, z;
        scanf("%d %d %d", &x, &y, &z);
        add(x, y, z);
        add(y, x, 0);
    }
    int now = 0;
    while (bfs())
        while ((now = dinic(s, inf)))
            ans += now;
    cout << ans << endl;
    return 0;
}

最大流解决二分图多重匹配

新建一个源点 \(S\) 和一个汇点 \(T\) ,从 \(S\) 向每个左部点连一条容量为 \(kl_i\) 的边,从每个右部点向 \(T\) 连一条容量为 \(kr_i\) 的边,把原二分图的每条边看作从左部点到右部点连的一条容量为 \(1\) 的边。

那么,二分图多重匹配 \(=\) 网络最大流。

Dinic 算法求解二分图最大匹配的时间复杂度为 \(O(m \sqrt n)\) ,比匈牙利算法快,实际表现则更快。

二分图最大匹配的必须边与可行边

最大匹配是完备匹配

把二分图中的非匹配边看作从左部向右部的有向边,匹配边看作从右部向左部的有向边,构成一张新的有向图。

  • 必须边: \((x,y)\) 是当前二分图的匹配边 && \(x,y\) 在新的有向图中属于不同的强连通分量。
  • 可行边: \((x,y)\) 是当前二分图的匹配边 || \(x,y\) 在新的有向图中属于同一个强连通分量。

最大匹配不一定是完备匹配

  • 必须边: \((x,y)\) 的流量为 \(1\) && 在残量网络上属于不同的强连通分量。
  • 可行边: \((x,y)\) 的流量为 \(1\) || 在残量网络上属于同一个强连通分量。

【练习】UVA663 Sorting Slides

【练习】UVA1327 King's Quest

最小割

给定一个网格 \(G=(V,E)\) ,源点为 \(S\) ,汇点为 \(T\) 。若一个边集被删去后,源点 \(S\) 和汇点 \(T\) 不再连通,则称改边集为网络的。边的容量之和最小的割称为网络的最小割

最大流最小割定理

最大流 \(=\) 最小割。

【例题】UVA1660 电视网络 Cable TV Network

枚举两个不直接连通的点 \(S\)\(T\) ,求在剩余的 \(n-2\) 个节点中最少去掉多少个可以使 \(S\)\(T\) 不连通,在每次枚举的结构中取 \(min\) 就是本题的答案。

点边转化

把原来无向图中的每个点 \(x\) ,拆成入点 \(x\) 和出点 \(x'\) 。在无向图中删去一个点⇔在网络中断开 \((x,x')\) 。对 \(\forall x \neq S,x \neq T\) 连有向边 \((x,x')\) ,容量为 \(1\) 。对原无向图的每条边 \((x,y)\) ,连有向边 \((x',y),(y',x)\) ,容量为 \(+ \infty\) (防止割断)。

求最小割即可。

#include <bits/stdc++.h>
using namespace std;
const int N = 56, M = 2e4 + 6, inf = 0x3f3f3f3f;
int n, m, s, t;
int a[N*N], b[N*N], d[N<<1];
int Head[N<<1], Edge[M], Leng[M], Next[M], tot;

inline void add(int x, int y, int z) {
    Edge[++tot] = y;
    Leng[tot] = z;
    Next[tot] = Head[x];
    Head[x] = tot;
    Edge[++tot] = x;
    Leng[tot] = 0;
    Next[tot] = Head[y];
    Head[y] = tot;
}

inline bool bfs() {
    memset(d, 0, sizeof(d));
    queue<int> q;
    q.push(s);
    d[s] = 1;
    while (q.size()) {
        int x = q.front();
        q.pop();
        for (int i = Head[x]; i; i = Next[i]) {
            int y = Edge[i], z = Leng[i];
            if (z && !d[y]) {
                q.push(y);
                d[y] = d[x] + 1;
                if (y == t) return 1;
            }
        }
    }
    return 0;
}

inline int dinic(int x, int f) {
    if (x == t) return f;
    int rest = f;
    for (int i = Head[x]; i && rest; i = Next[i]) {
        int y = Edge[i], z = Leng[i];
        if (z && d[y] == d[x] + 1) {
            int k = dinic(y, min(rest, z));
            if (!k) d[y] = 0;
            Leng[i] -= k;
            Leng[i^1] += k;
            rest -= k;
        }
    }
    return f - rest;
}

inline void Cable_TV_Network() {
    for (int i = 0; i < m; i++) {
        char str[20];
        scanf("%s", str);
        a[i] = b[i] = 0;
        int j;
        for (j = 1; str[j] != ','; j++) a[i] = a[i] * 10 + str[j] - '0';
        for (j++; str[j] != ')'; j++) b[i] = b[i] * 10 + str[j] - '0';
    }
    int ans = inf;
    for (s = 0; s < n; s++)
        for (t = 0; t < n; t++)
            if (s != t) {
                memset(Head, 0, sizeof(Head));
                tot = 1;
                int maxf = 0;
                for (int i = 0; i < n; i++)
                    if (i == s || i == t) add(i, i + n, inf);
                    else add(i, i + n, 1);
                for (int i = 0; i < m; i++) {
                    add(a[i] + n, b[i], inf);
                    add(b[i] + n, a[i], inf);
                }
                while (bfs()) {
                    int num;
                    while ((num = dinic(s, inf))) maxf += num;
                }
                ans = min(ans, maxf);
            }
    if (n <= 1 || ans == inf) ans = n;
    cout << ans << endl;
}

int main() {
    while (cin >> n >> m) Cable_TV_Network();
    return 0;
}

当然,在点边转化中也可以将一条边截成两半,中间插入一个点,把边的各种信息反映在这个点上。

学了这么多就一次性多给点练习题吧

【练习】P2472 [SCOI2007]蜥蜴

【练习】P2774 方格取数问题

【练习】P2766 最长不下降子序列问题

【练习】P2765 魔术球问题

【练习】P2754 [CTSC1999]家园

【练习】P3254 圆桌问题

【练习】P2857 [USACO06FEB]稳定奶牛分配Steady Cow Assignment

【练习】P2763 试题库问题

【练习】[SHOI2007]善意的投票

【练习】P2598 [ZJOI2009]狼和羊的故事

最小割的可行边与必须边

【例题】P4126 [AHOI2009]最小割

首先求最大流,那么最小割的可行边与必须边都必须是满流

  • 可行边:在残量网络中不存在 \(x\)\(y\) 的路径(强连通分量);
  • 必须边:在残量网络中 \(S\) 能到 \(x\) && \(y\) 能到 \(T\)
#include <bits/stdc++.h>
using namespace std;
const int N = 4e3 + 6, M = 6e4 + 6, inf = 1e9;
int n, m, s, t, d[N], f[N];
int Head[N], Edge[M<<1], Leng[M<<1], Next[M<<1], tot = 1;
struct E {
    int x, y, z;
} e[M<<1];
int dfn[N], low[N], num, st[N], top, ins[N], c[N], cnt;

inline void add(int x, int y, int z) {
    Edge[++tot] = y;
    Leng[tot] = z;
    Next[tot] = Head[x];
    Head[x] = tot;
}

inline bool bfs() {
    memset(d, 0, sizeof(d));
    queue<int> q;
    d[s] = 1;
    q.push(s);
    while (q.size()) {
        int x = q.front();
        q.pop();
        for (int i = Head[x]; i; i = Next[i]) {
            int y = Edge[i], z = Leng[i];
            if (d[y] || !z) continue;
            d[y] = d[x] + 1;
            q.push(y);
            if (y == t) return 1;
        }
    }
    return 0;
}

int dinic(int x, int flow) {
    if (x == t) return flow;
    int rest = flow;
    for (int i = Head[x]; i && rest; i = Next[i]) {
        int y = Edge[i], z = Leng[i];
        if (d[y] != d[x] + 1 || !z) continue;
        int k = dinic(y, min(z, rest));
        if (!k) d[y] = 0;
        else {
            Leng[i] -= k;
            Leng[i^1] += k;
            rest -= k;
        }
    }
    return flow - rest;
}

void dfs(int x, int k) {
    f[x] = k;
    for (int i = Head[x]; i; i = Next[i]) {
        int y = Edge[i], z = Leng[i^(k-1)];
        if (f[y] || !z) continue;
        dfs(y, k);
    }
}

void tarjan(int x) {
    dfn[x] = low[x] = ++num;
    st[++top] = x;
    ins[x] = 1;
    for (int i = Head[x]; i; i = Next[i]) {
        int y = Edge[i], z = Leng[i];
        if (!z) continue;
        if (!dfn[y]) {
            tarjan(y);
            low[x] = min(low[x], low[y]);
        } else if (ins[y])
            low[x] = min(low[x], dfn[y]);
    }
    if (dfn[x] == low[x]) {
        ++cnt;
        int y;
        do {
            y = st[top--];
            ins[y] = 0;
            c[y] = cnt;
        } while (x != y);
    }
}

int main() {
    cin >> n >> m >> s >> t;
    for (int i = 1; i <= m; i++) {
        scanf("%d %d %d", &e[i].x, &e[i].y, &e[i].z);
        add(e[i].x, e[i].y, e[i].z);
        add(e[i].y, e[i].x, 0);
    }
    while (bfs())
        while (dinic(s, inf));
    dfs(s, 1);
    dfs(t, 2);
    for (int i = 1; i <= n; i++)
        if (!dfn[i]) tarjan(i);
    for (int i = 1; i <= m; i++) {
        int k = i << 1;
        if (Leng[k]) puts("0 0");
        else printf("%d %d\n", c[e[i].x] != c[e[i].y], f[e[i].x] == 1 && f[e[i].y] == 2);
    }
    return 0;
}

【例题】P4001 [ICPC-Beijing 2006]狼抓兔子

平面图

边与边只在顶点相交的图。

对偶图

对于一个平面图,都有其对应的对偶图。

  • 平面图被划分出的每一个区域当作对偶图的一个点;
  • 平面图中的每一条边两边的区域对应的点用边相连,特别地,若两边为同一区域则加一条回边(自环)。

这样构成的图即为原平面图的对偶图。

定理

平面图最小割 \(=\) 对偶图最短路。感觉很显然?

#include <bits/stdc++.h>
#define pii pair<int, int>
#define X first
#define Y second
#define mp make_pair
#define ui unsigned int
using namespace std;
const int N = 2e6 + 6;
int n, m, s, t, d[N];
vector<pii> e[N];
priority_queue<pii> q;
bitset<N> v;

inline void add(int x, int y, int z) {
    e[x].push_back(mp(y, z));
}

inline int get(int i, int j, int k) {
    return 2 * (m - 1) * (i - 1) + 2 * (j - 1) + k;
}

inline void ins(int x, int y) {
    int z;
    scanf("%d", &z);
    add(x, y, z);
    add(y, x, z);
}

inline void dijkstra() {
    memset(d, 0x3f, sizeof(d));
    d[s] = 0;
    q.push(mp(0, s));
    while (q.size()) {
        int x = q.top().Y;
        if (x == t) return;
        q.pop();
        if (v[x]) continue;
        v[x] = 1;
        for (ui i = 0; i < e[x].size(); i++) {
            int y = e[x][i].X, z = e[x][i].Y;
            if (d[y] > d[x] + z) {
                d[y] = d[x] + z;
                q.push(mp(-d[y], y));
            }
        }
    }
}

int main() {
    cin >> n >> m;
    t = 2 * (n - 1) * (m - 1) + 1;
    for (int j = 1; j < m; j++) ins(get(1, j, 2), t);
    for (int i = 2; i < n; i++)
        for (int j = 1; j < m; j++)
            ins(get(i - 1, j, 1), get(i, j, 2));
    for (int j = 1; j < m; j++) ins(get(n - 1, j, 1), s);
    for (int i = 1; i < n; i++) {
        ins(get(i, 1, 1), s);
        for (int j = 2; j < m; j++)
            ins(get(i, j - 1, 2), get(i, j, 1));
        ins(get(i, m - 1, 2), t);
    }
    for (int i = 1; i < n; i++)
        for (int j = 1; j < m; j++)
            ins(get(i, j, 1), get(i, j, 2));
    dijkstra();
    cout << d[t] << endl;
    return 0;
}

【练习】P2046 [NOI2010]海拔

费用流

给定一个网络 \(G = (V,E)\) ,每条边 \((x,y)\) 除了有容量限制 \(c(x,y)\) ,还有一个给定的“单位费用” \(w(x,y)\) 。当边 \((x,y)\) 的流量为 \(f(x,y)\) 时,就要花费 \(f(x,y) \times w(x,y)\) 。该网格中总花费最小的最大流被称为最小费用最大流,总花费最大的最大流被称为最大费用最大流

EK(Edmond—Karp)增广路算法

在EK(Edmond—Karp)增广路算法求解最大流的基础上,把BFS改为SPFA,把 \(w(x,y)\) 当作边权,在残量网络上求最短路,即可求出最小费用最大流。注意:一条反向边边权应为其正向边边权的相反数。

【模板】P3381 【模板】最小费用最大流

#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 6, M = 2e5 + 6, inf = 0x3f3f3f3f;
int n, m, s, t, maxflow, ans, d[N], now[N], pre[N];
int Head[N], Edge[M], Leng[M], Cost[M], Next[M], tot = 1;
bitset<N> v;

inline void add(int x, int y, int z, int w) {
    Edge[++tot] = y;
    Leng[tot] = z;
    Cost[tot] = w;
    Next[tot] = Head[x];
    Head[x] = tot;
}

inline bool spfa() {
    v.reset();
    memset(d, 0x3f, sizeof(d));
    queue<int> q;
    q.push(s);
    v[s] = 1;
    d[s] = 0;
    now[s] = inf;
    while (q.size()) {
        int x = q.front();
        q.pop();
        v[x] = 0;
        for (int i = Head[x]; i; i = Next[i]) {
            int y = Edge[i], z = Leng[i], w = Cost[i];
            if (!z || d[y] <= d[x] + w) continue;
            d[y] = d[x] + w;
            now[y] = min(now[x], z);
            pre[y] = i;
            if (!v[y]) {
                q.push(y);
                v[y] = 1;
            }
        }
    }
    return d[t] != inf;
}

inline void upd() {
    maxflow += now[t];
    ans += d[t] * now[t];
    int x = t;
    while (x != s) {
        int i = pre[x];
        Leng[i] -= now[t];
        Leng[i^1] += now[t];
        x = Edge[i^1];
    }
}

int main() {
    cin >> n >> m >> s >> t;
    for (int i = 1; i <= m; i++) {
        int x, y, z, w;
        scanf("%d %d %d %d", &x, &y, &z, &w);
        add(x, y, z, w);
        add(y, x, 0, -w);
    }
    while (spfa()) upd();
    cout << maxflow << " " << ans << endl;
    return 0;
}

最大费用最大流解决二分图带权最大匹配

类似最大流解决二分图多重匹配,每条边的权值就是他的单位费用。

【例题】P2045 方格取数加强版

  1. 点边转化:把每个格子 \((i,j)\) 拆成一个入点一个出点。
  2. 从每个入点向对应的出点连两条有向边:一条容量为 \(1\) ,费用为格子 \((i,j)\) 中的数;另一条容量为 \(k-1\) ,费用为 \(0\)
  3. \((i,j)\) 的出点到 \((i,j+1)\)\((i+1,j)\) 的入点连有向边,容量为 \(k\) ,费用为 \(0\)
  4. \((1,1)\) 的入点为源点, \((n,n)\) 的出点为汇点,求最大费用最大流。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e3 + 6, M = 2e5 + 6;
const int inf = 0x3f3f3f3f, _inf = 0xcfcfcfcf;
int Head[N], Edge[M], Leng[M], Cost[M], Next[M], tot = 1;
int d[N], f[N], p[N];
bool v[N];
int n, k, s = 1, t, ans;

inline void add(int x, int y, int z, int c) {
    Edge[++tot] = y;
    Leng[tot] = z;
    Cost[tot] = c;
    Next[tot] = Head[x];
    Head[x] = tot;
    Edge[++tot] = x;
    Leng[tot] = 0;
    Cost[tot] = -c;
    Next[tot] = Head[y];
    Head[y] = tot;
}

inline int num(int i, int j, int k) {
    return (i - 1) * n + j + k * n * n;
}

inline bool spfa() {
    queue<int> q;
    memset(d, 0xcf, sizeof(d));
    memset(v, 0, sizeof(v));
    q.push(s);
    d[s] = 0;
    v[s] = 1;
    f[s] = inf;
    while (q.size()) {
        int x = q.front();
        v[x] = 0;
        q.pop();
        for (int i = Head[x]; i; i = Next[i]) {
            if (!Leng[i]) continue;
            int y = Edge[i];
            if (d[y] < d[x] + Cost[i]) {
                d[y] = d[x] + Cost[i];
                f[y] = min(f[x], Leng[i]);
                p[y] = i;
                if (!v[y]) {
                    q.push(y);
                    v[y] = 1;
                }
            }
        }
    }
    return d[t] != _inf;
}

void upd() {
    int x = t;
    while (x != s) {
        int i = p[x];
        Leng[i] -= f[t];
        Leng[i^1] += f[t];
        x = Edge[i^1];
    }
    ans += d[t] * f[t];
}

int main() {
    cin >> n >> k;
    t = 2 * n * n;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++) {
            int c;
            scanf("%d", &c);
            add(num(i, j, 0), num(i, j, 1), 1, c);
            add(num(i, j, 0), num(i, j, 1), k - 1, 0);
            if (j < n) add(num(i, j, 1), num(i, j + 1, 0), k, 0);
            if (i < n) add(num(i, j, 1), num(i + 1, j, 0), k, 0);
        }
    while (spfa()) upd();
    cout << ans << endl;
    return 0;
}

【练习】POJ2195 Going Home

【例题】P2053 [SCOI2007]修车

费用提前计算

注意到每位车主的等待时间除了跟自己的车所需的维修时间有关之外,还跟同一位技术人员之前维修所花的时间有关,这导致我们很难直观地建模。

但是仔细观察可以发现,一个人维修所花的时间,对同一位技术人员之后的维修造成的影响是已知且固定的。

那么,我们将费用提前计算,即,把第 \(i\) 位车主的车由第 \(j\) 位维修人员倒数第 \(k\) 个维修所花的时间(费用)当作 \(k \times t_{i,j}\)

从源点向每位车主连边,容量为 \(1\) ,费用为 \(0\)

每位维修人员拆成 \(n\) 个点,向汇点连边,容量为 \(1\) ,费用为 \(0\)

\(i\) 位车主向第 \(j\) 位维修人员拆成的第 \(k\) 个点连边,容量为 \(1\) ,费用为 \(k \times t_{i,j}\)

求最小费用最大流即可。

【例题】P2050 [NOI2012]美食节

此题为 P2053 [SCOI2007]修车 的数据加强版。

P2053 [SCOI2007]修车 的建图方式,但是硬求最小费用最大流只能拿到 \(60\) 分。

动态开点

起初每个厨师只拆成一个点,每次增广时,把当下的厨师拆出一个新点。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 6, M = 4e7 + 6, inf = 0x3f3f3f3f;
int n, m, s, t, ans, d[N], pre[N], now[N], num, p[N];
int Head[N], Edge[M], Leng[M], Cost[M], Next[M], tot = 1;
bitset<N> v;
int a[46][106];

inline void add(int x, int y, int z, int w) {
    Edge[++tot] = y;
    Leng[tot] = z;
    Cost[tot] = w;
    Next[tot] = Head[x];
    Head[x] = tot;
}

inline bool spfa() {
    v.reset();
    memset(d, 0x3f, sizeof(d));
    queue<int> q;
    q.push(s);
    v[s] = 1;
    d[s] = 0;
    now[s] = inf;
    while (q.size()) {
        int x = q.front();
        q.pop();
        v[x] = 0;
        for (int i = Head[x]; i; i = Next[i]) {
            int y = Edge[i], z = Leng[i], w = Cost[i];
            if (!z || d[y] <= d[x] + w) continue;
            d[y] = d[x] + w;
            now[y] = min(now[x], z);
            pre[y] = i;
            if (!v[y]) {
                q.push(y);
                v[y] = 1;
            }
        }
    }
    return d[t] != inf;
}

inline void upd() {
    ans += d[t] * now[t];
    int x = t;
    while (x != s) {
        int i = pre[x];
        Leng[i] -= now[t];
        Leng[i^1] += now[t];
        x = Edge[i^1];
    }
    x = Edge[pre[t]^1];
    p[++num] = p[x];
    add(num, t, 1, 0);
    add(t, num, 0, 0);
    for (int i = Head[x]; i; i = Next[i]) {
        int y = Edge[i], w = Cost[i^1];
        if (y == t) continue;
        w += a[y][p[x]];
        add(y, num, 1, w);
        add(num, y, 0, -w);
    }
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        int x;
        scanf("%d", &x);
        add(0, i, x, 0);
        add(i, 0, 0, 0);
    }
    num = t = n + m + 1;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++) {
            int x;
            scanf("%d", &x);
            a[i][j] = x;
            add(i, n + j, 1, x);
            add(n + j, i, 0, -x);
        }
    for (int i = 1; i <= m; i++) {
        add(n + i, t, 1, 0);
        add(t, n + i, 0, 0);
        p[n+i] = i;
    }
    while (spfa()) upd();
    cout << ans << endl;
    return 0;
}

学了这么多就一次性多给点练习题吧(真·人类的本质)

【练习】P2604 [ZJOI2010]网络扩容

【练习】P2153 [SDOI2009]晨跑

【练习】P2469 [SDOI2010]星际竞速

【练习】P2770 航空路线问题

【练习】P3356 火星探险问题

【练习】P3358 最长k可重区间集问题

【练习】P3357 最长k可重线段集问题

【练习】P4009 汽车加油行驶问题

【练习】P4012 深海机器人问题

【练习】P4013 数字梯形问题

【练习】P4014 分配问题

【练习】P4015 运输问题

【练习】P4016 负载平衡问题

上下界网络流

无源汇上下界可行流

给定 \(n\) 个点, \(m\) 条边的网络,求一个可行解,使得边 \((x,y)\) 的流量介于 \([B(x,y),C(x,y)]\) 之间,并且整个网络满足流量守恒。

如果把 \(C-B\) 作为容量上界, \(0\) 作为容量下界,就是一般的网络流模型。

然而求出的实际流量为 \(f(x,y)+B(x,y)\) ,不一定满足流量守恒,需要调整。

\(inB[u]=\sum B(i,u)\)\(outB[u]=\sum B(u,i)\)\(d[u]=inB[u]-outB[u]\)

新建源汇, \(S\)\(d>0\) 的点连边, \(d<0\) 的点向 \(T\) 连边,容量为相应的 \(d\)

在该网络上求最大流,则每条边的流量 \(+\) 下界就是原网络的一个可行流。

具体实现时,可省略 \(inB,outB\) 数组,直接在 \(d\) 数组上修改。

有源汇上下界可行流

\(T\)\(S\) 连一条下界为 \(0\) ,上界为 \(+inf\) 的边,把汇流入的流量转移给源流出的流量,转化为无源汇的网络,然后求解无源汇上下界可行流

有源汇上下界最大流

两个方法:

  1. 二分答案 \(ans\) ,从 \(T\)\(S\) 连一条下界为 \(ans\) ,上界为 \(+inf\) 的边,转化为无源汇网络。找到最大的 \(ans\) ,使得该无源汇上下界网络有可行流。
  2. \(T\)\(S\) 连一条下界为 \(0\) ,上界为 \(+inf\) 的边,转化为无源汇网络。按照无源汇上下界可行流的做法求一次无源汇上下界超级源到超级汇的最大流。回到原网络,在上一步的残量网络基础上,求一次 \(S\)\(T\) 的最大流。

有源汇上下界最小流

两个方法:

  1. 二分答案 \(ans\) ,从 \(T\)\(S\) 连一条下界为 \(0\) ,上界为 \(ans\) 的边,转化为无源汇网络。找到最小的 \(ans\) ,使得该无源汇上下界网络有可行流。
  2. 类似有源汇上下界可行流的构图方法,但先不添加 \(T\)\(S\) 的边,求一次超级源到超级汇的最大流。然后再添加一条从 \(T\)\(S\) 下界为 \(0\) ,上界为 \(+inf\) 的边,在残量网络上再求一次超级源到超级汇的最大流。流经 \(T\)\(S\) 的边的流量就是最小流的值。该算法的思想是在第一步中尽可能填充循环流,以减小最小流的代价。

【例题】P4843 清理雪道

连边:

  1. \((s,i,0,+inf)\)
  2. \((i,t,0,+inf)\)
  3. 对每条雪道,连边 \((i,j,1,+inf)\)

对网络 \(s-t\)有源汇上下界最小流

这里使用方法二。

#include <bits/stdc++.h>
using namespace std;
const int N = 106, M = 2e4 + 6, inf = 1e9;
int n, s, t, S, T, d[N], ans;
int Head[N], Edge[M], Leng[M], Next[M], tot = 1;

inline void add(int x, int y, int z) {
    Edge[++tot] = y;
    Leng[tot] = z;
    Next[tot] = Head[x];
    Head[x] = tot;
    Edge[++tot] = x;
    Leng[tot] = 0;
    Next[tot] = Head[y];
    Head[y] = tot;
}

inline void ins(int x, int y, int l, int r) {
    add(x, y, r - l);
    d[x] -= l;
    d[y] += l;
}

inline bool bfs() {
    memset(d, 0, sizeof(d));
    queue<int> q;
    q.push(S);
    d[S] = 1;
    while (q.size()) {
        int x = q.front();
        q.pop();
        for (int i = Head[x]; i; i = Next[i]) {
            int y = Edge[i], z = Leng[i];
            if (d[y] || !z) continue;
            q.push(y);
            d[y] = d[x] + 1;
            if (y == T) return 1;
        }
    }
    return 0;
}

int dinic(int x, int flow) {
    if (x == T) return flow;
    int rest = flow;
    for (int i = Head[x]; i && rest; i = Next[i]) {
        int y = Edge[i], z = Leng[i];
        if (d[y] != d[x] + 1 || !z) continue;
        int k = dinic(y, min(rest, z));
        if (!k) d[y] = 0;
        else {
            Leng[i] -= k;
            Leng[i^1] += k;
            rest -= k;
        }
    }
    return flow - rest;
}

int main() {
    cin >> n;
    s = n + 1, t = n + 2, S = n + 3, T = n + 4;
    for (int i = 1; i <= n; i++) {
        ins(s, i, 0, inf);
        ins(i, t, 0, inf);
        int k;
        scanf("%d", &k);
        while (k--) {
            int x;
            scanf("%d", &x);
            ins(i, x, 1, inf);
        }
    }
    for (int i = 1; i <= t; i++) {
        if (d[i] > 0) add(S, i, d[i]);
        else if (d[i] < 0) add(i, T, -d[i]);
    }
    while (bfs()) while (dinic(S, inf));
    ins(t, s, 0, inf);
    while (bfs()) while (dinic(S, inf));
    cout << Leng[tot] << endl;
    return 0;
}

上下界最小费用可行流

类似上下界可行流,求最大流改为求最小费用最大流。

【例题】P4553 80人环游世界

每个国家拆成两个点(入点 \(i\) 和出点 \(i+n\)),建立源 \(s\)\(t\) 附加源 \(s\_\)

连边:

  1. \((s,s\_,m,m,0)\)
  2. \((s\_,i,0,m,0)\)
  3. \((i+n,t,0,m,0)\)
  4. \((i,i+n,V[i],V[i],0)\)
  5. \(i,j\) 两个国家通航,连边 \((i+n,j,0,m,Cost_{i,j})\)

对网络 \(s-t\)有源汇上下界最小费用可行流

#include <bits/stdc++.h>
using namespace std;
const int N = 206, M = 1e5 + 6, inf = 0x3f3f3f3f;
int n, m, S, T, s, s_, t, d[N], now[N], pre[N], ans;
int Head[N], Edge[M], Leng[M], Cost[M], Next[M], tot = 1;
bitset<N> v;

inline void add(int x, int y, int z, int w) {
    Edge[++tot] = y;
    Leng[tot] = z;
    Cost[tot] = w;
    Next[tot] = Head[x];
    Head[x] = tot;
    Edge[++tot] = x;
    Leng[tot] = 0;
    Cost[tot] = -w;
    Next[tot] = Head[y];
    Head[y] = tot;
}

inline void ins(int x, int y, int l, int r, int w) {
    add(x, y, r - l, w);
    d[x] -= l;
    d[y] += l;
}

inline bool spfa() {
    v.reset();
    memset(d, 0x3f, sizeof(d));
    queue<int> q;
    q.push(S);
    v[S] = 1;
    d[S] = 0;
    now[S] = m;
    while (q.size()) {
        int x = q.front();
        q.pop();
        v[x] = 0;
        for (int i = Head[x]; i; i = Next[i]) {
            int y = Edge[i], z = Leng[i], w = Cost[i];
            if (!z || d[y] <= d[x] + w) continue;
            d[y] = d[x] + w;
            now[y] = min(now[x], z);
            pre[y] = i;
            if (!v[y]) {
                q.push(y);
                v[y] = 1;
            }
        }
    }
    return d[T] != inf;
}

inline void upd() {
    ans += d[T] * now[T];
    int x = T;
    while (x != S) {
        int i = pre[x];
        Leng[i] -= now[T];
        Leng[i^1] += now[T];
        x = Edge[i^1];
    }
}

int main() {
    cin >> n >> m;
    s = n * 2 + 1, s_ = s + 1, t = s_ + 1;
    S = t + 1, T = S + 1;
    ins(s, s_, m, m, 0);
    for (int i = 1; i <= n; i++) {
        ins(s_, i, 0, m, 0);
        ins(i + n, t, 0, m, 0);
        int x;
        scanf("%d", &x);
        ins(i, i + n, x, x, 0);
    }
    for (int i = 1; i <= n; i++)
        for (int j = i + 1; j <= n; j++) {
            int x;
            scanf("%d", &x);
            if (~x) ins(i + n, j, 0, m, x);
        }
//  ins(t, s, 0, m, 0);
    for (int i = 1; i <= t; i++) {
        if (d[i] > 0) add(S, i, d[i], 0);
        else if (d[i] < 0) add(i, T, -d[i], 0);
    }
    while (spfa()) upd();
    cout << ans << endl;
    return 0;
}

注意:代码中注释的那一条语句加不加都可以AC,具体原因有待研究(讨论)。

【例题】P3980 [NOI2008]志愿者招募

解法一:请参考BYVoid的题解NOI 2008 志愿者招募 employee

这是传统解法,构图复杂而精巧,但几乎不可能想到。

解法二:把每一天看作一个点。

连边:

  1. \((i,i+1,a_i,+inf,0)\)

  2. \((t_i +1, s_i, 0, +inf, Cost_i)\)

在这个无源汇网络中,招募 \(1\) 个第 \(i\) 类志愿者,就会产生一个从 \(s_i\)\(t_i+1\) 的环流,并且使花费加 \(Cost_i\) .

这与题目的实际意义正好相对应,在这个网络中求无源汇上下界最小费用可行流即可。

【练习】P4043 [AHOI2014/JSOI2014]支线剧情

【练习】P1251 餐巾计划问题

最大权闭合子图

若有向图 \(G\) 的子图 \(V\) 满足: \(V\) 中顶点的所有出边均指向 \(V\) 内部的顶点,则称 \(V\)\(G\) 的一个闭合子图

\(G\) 中的点有点权,则点权和最大的闭合子图称为有向图 \(G\)最大权闭合子图

构图方法

建立源点 \(S\) 和汇点 \(T\) ,源点 \(S\) 连所有点权为正的点,容量为该点点权;其余点连汇点 \(T\) ,容量为该点点权的相反数,对于原图中的边 \((x,y)\) ,连边 \((x,y,+inf)\)

定理

  • 最大权闭合图的点权和 \(=\) 所有正权点权值和 – 最小割。
  • 上述图的最小割包含 \(S\)不在最大权闭合图内的正权节点的边和在最大权闭合图内的负权节点\(T\) 的边。

推论(最大权闭合图方案)

残量网络中由源点 \(S\) 能够访问到的点,就构成一个点数最少的最大权闭合图。

【例题】P2805 [NOI2009]植物大战僵尸

把每个植物当做一个顶点,植物携带的能源数目为顶点的权值。

如果植物 \(b\) 在植物 \(a\) 的攻击范围内,连接一条有向边 \((a,b)\) ,表示 \(a\) 可以保护 \(b\)

由于僵尸从右向左进攻,可以认为每个植物都被它右边相邻的植物保护,对于每个植物 \(a\) (除最左边一列),向其左边的相邻植物 \(b\) ,连接一条有向边 \((a,b)\)

此时可能有一些植物是互相保护的,都不能被吃掉,这样的点(和与其相连的边)应该全部删掉,拓扑排序一遍即可。

如果要吃掉一个植物,就应该把所有保护它的植物全部吃掉。

对应在图中,如果我们将图转置(即所有边转成其反向边),那么可以吃掉的植物应该构成一个闭合子图,而最优解就是最大权闭合子图。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 6, M = 1e6 + 6, inf = 1e9;
int n, m, s, t, a[N], ans, d[N], deg[N], v[N];
int Head[N], Edge[M], Leng[M], Next[M], tot = 1;
queue<int> q;
vector<int> e[N];

inline void add(int x, int y, int z) {
    Edge[++tot] = y;
    Leng[tot] = z;
    Next[tot] = Head[x];
    Head[x] = tot;
}

inline bool bfs() {
    memset(d, 0, sizeof(d));
    queue<int> q;
    q.push(s);
    d[s] = 1;
    while (q.size()) {
        int x = q.front();
        q.pop();
        for (int i = Head[x]; i; i = Next[i]) {
            int y = Edge[i], z = Leng[i];
            if (deg[y] || d[y] || !z) continue;
            q.push(y);
            d[y] = d[x] + 1;
            if (y == t) return 1;
        }
    }
    return 0;
}

int dinic(int x, int flow) {
    if (x == t) return flow;
    int rest = flow;
    for (int i = Head[x]; i && rest; i = Next[i]) {
        int y = Edge[i], z = Leng[i];
        if (d[y] != d[x] + 1 || !z) continue;
        int k = dinic(y, min(rest, z));
        if (!k) d[y] = 0;
        else {
            Leng[i] -= k;
            Leng[i^1] += k;
            rest -= k;
        }
    }
    return flow - rest;
}

int main() {
    cin >> n >> m;
    s = n * m, t = s + 1;
    for (int i = 0; i < s; i++) {
        scanf("%d", &a[i]);
        int k;
        scanf("%d", &k);
        while (k--) {
            int x, y;
            scanf("%d %d", &x, &y);
            e[i].push_back(x * m + y);
            ++deg[x*m+y];
        }
    }
    for (int i = 0; i < n; i++)
        for (int j = 1; j < m; j++) {
            e[i*m+j].push_back(i * m + j - 1);
            ++deg[i*m+j-1];
        }
    for (int i = 0; i < s; i++)
        if (!deg[i]) q.push(i), v[i] = 1;
    while (q.size()) {
        int x = q.front();
        q.pop();
        for (unsigned int i = 0; i < e[x].size(); i++) {
            int y = e[x][i];
            if (!v[y] && !--deg[y]) q.push(y), v[y] = 1;
        }
    }
    for (int x = 0; x < s; x++) {
        if (!v[x]) continue;
        for (unsigned int i = 0; i < e[x].size(); i++) {
            int y = e[x][i];
            if (!v[y]) continue;
            add(y, x, inf);
            add(x, y, 0);
        }
        if (a[x] > 0) add(s, x, a[x]), add(x, s, 0), ans += a[x];
        if (a[x] < 0) add(x, t, -a[x]), add(t, x, 0);
    }
    int now = 0;
    while (bfs())
        while ((now = dinic(s, inf)))
            ans -= now;
    cout << ans << endl;
    return 0;
}

【练习】P2762 太空飞行计划问题

【练习】P4174 [NOI2006]最大获利(这道题还可用最大密度子图实现)

致谢 & 参考资料

感谢 @Edgration @ButterflyDew 以及鹳狸猿大大帮忙审核。

胡伯涛《最小割模型在信息学竞赛中的应用》

李煜东《算法竞赛进阶指南》(应该有很多人知道我是这本书的std-Author吧)

广告:《算法竞赛进阶指南》购买地址:淘宝京东

李煜东《二分图与网络流模型设计》

当然你们是找不到这个课件的鸭

特别说明:本文中对李煜东老师的书籍及课件的引用均经过了本人授权,任何人在未经过我和李煜东老师本人的授权下禁止引用本文中的任何内容。

posted @ 2019-03-01 16:16 xht37 阅读(...) 评论(...) 编辑 收藏