[19/5/9]XMU ACM 集训队笔记(5) 网络流初步

本期主要内容:二分图(染色法判定二分图,二分图的最大匹配、多重匹配、带权匹配;二分图最大独立集、最小点覆盖、最大团;二分图的最大匹配边与可行边,有向无环图的最小路径覆盖);网络流初步(最大流、最小割、最大流最小割定理,EK, Dinic, ISAP 算法)。

二分图

定义什么的我就不写了,不然的话补到明年都补不完了……

染色法判断二分图

依据二分图的定义和下面的定理:

一张无向图是二分图,当且仅当图中不存在长度为奇数的环。

因为如果存在长度为奇数的环,说明不能将图中的结点分成不相交的两部分。根据此定理可以使用染色法用来进行二分图的判定——将每个结点设置一个状态,0表示未染色,1表示白色,2表示黑色;对所有未染色的点进行 DFS,并将该点所能达到的其它点染上与之相反的颜色。如果在上述过程中发现了颜色重涂,说明该图不是一张二分图。

int vis[MAXN], flag = 1;
void dfs(int x, int cur) {
    vis[x] = cur;
    for (int i = head[x]; i; i = e[i].next) {
        if (vis[e[i].v] == 0)
            dfs(vis[e[i].v], 3 - cur);          // 注意这里的小技巧,3-1=2, 3-2=1.
        else if (vis[e[i].v] == cur) {
            flag = 0;
            return;
        }
    }
}
for (int i = 0; i < n; i++) {
    if (!vis[i] && flag)
        dfs(i, 1);
    if (!flag)
        break;
}
printf(flag ? "Yes" : "No");

二分图的最大匹配

定义部分的介绍略去,通俗的说就是,对于二分图两部分的结点,你要从两部分中选出最多组相连的结点,当然,一个点只能连一个点。

定义已被匹配的点成为“匹配点”,同理定义“非匹配点”、“匹配边”、“非匹配边”。如果二分图中存在一条连接两个非匹配点的路径 $p$, 使得非匹配边与匹配边在 $p$ 上交替出现,称路径 $p$ 是一条增广路。(画个图,随意设定几个匹配点,然后找一条符合上述要求路径,就能明白了)。因为非匹配边和匹配边必须交替出现,且路径两头连接的是非匹配结点,说明路径的第一条边和最后一条边一定是非匹配边,因此 $p$ 的长度一定是奇数。

定理:二分图的一组匹配 S 是最大匹配,当且仅当图中不存在 S 的增广路。

根据上述的定理我们可以使用匈牙利算法来求解问题,算法的步骤如下:

  1. 初始化所有的边为非匹配边
  2. 对二分图左部的点 x 尝试进行匹配:遍历左部结点的每条边,如果通向的右部点 y 未被匹配,则将右部的点 y 与 x 匹配
  3. 若通向右边的点 y 已被匹配,设右部结点已匹配的点为 t,则首先为 t 匹配一个新的左部点
  4. 如果 t 能匹配到左部的结点,那么将 y 与 x 匹配,返回 true(不改变原先答案数的情况下为 x 找到匹配,即找到增广路);否则返回 false (将 x 匹配不会更新答案,也就是没有找到增广路)

    int vis[MAXN], match[MAXN], ans = 0;
    bool dfs(int x) {
    for (int i = head[x], y; i ; i = e[i].next) {
        y = e[i].v;
        if (!vis[y]) {
            vis[y] = 1;
            if (!match[y] || dfs(match[y])) {
                match[y] = x;
                return true;
            }
        }
    }
    return false;
    }
    for (int i = 0; i < n; i++) {
    memset(vis, 0, sizeof vis);     // vis 标记点是否在 dfs 栈中
    if (dfs(i))
        ans++;
    }
    

二分图的最大匹配也可以用网络流的最大流模型来求解,具体的建图方法为:虚拟出两个节点 s, t,将 s 与所有左部结点连一条流量上限为 1 的边,将 t 与所有右部结点连一条流量上限为 1 的边,然后左右部结点之间保持原先的连边关系,每条边的容量设为 1,建完图跑最大流即可。那么为什么要用网络流来解二分图最大匹配呢,因为匈牙利算法(增广路算法)的时间复杂度是 $O(NM)$ 的,数据规模很大的时候是做不了的,但是我们可以引入最大流模型从而使用 Dinic 等算法解题。使用 Dinic 跑二分图最大匹配的复杂度是 $O(m\sqrt{n})$ 的,比匈牙利快到不知道哪里去了。但是鉴于匈牙利算法的编码难度较低,所以在数据规模较小的时候还是可以用该算法的。


在构建二分图匹配模型的时候,我们需要确认模型中存在“0要素”和“1要素”:0要素要求二分图中的结点能分成两个独立的几何,并且每个集合内部有 0 条边;1要素要求每个结点只能与 1 条匹配边相连。

二分图的完备匹配:如果二分图的左右两部分结点数量均为 $n$,且最大匹配包含 $n$ 重匹配边,则该二分图具有完备匹配。

二分图多重匹配

在二分图最大匹配的基础上,将每个左部的点只能匹配一个右部的点的限制改为:每个左部的点 $i$ 最多可以匹配 $k_i$ 个右部的点,每个右部的点 $j$ 最多可以匹配 $k_j$ 个左部的点。这样的问题一般有以下解决方案:

  1. 拆点,将左部第 $i$ 个点拆成 $k_i$ 个结点,右边的点拆成 $k_j$ 个结点。拆点后节点间的连接关系也要复制,然后求解二分图最大匹配。
  2. 如果 $k_j = 1$,那么可以对左部结点跑匈牙利算法的时候跑 $k_i$ 次 DFS;若 $k_i = 1$ 则为右部结点跑匈牙利算法,一个结点跑 $k_j$ 次 DFS。
  3. 网络流模型。设置虚拟的源点 s 和汇点 t,将所有的左部结点 $i$ 到 s 连一条流量为 $k_i$ 的边,将所有的右部结点 $j$ 到 t 连一条流量为 $k_j$ 的边,中间左部结点和右部结点间的边容量设置为 1,建完图跑最大流。

例题:圆桌问题

建图的思路:首先源点到各个单位连一条容量为 r[i] 的边,然后每个代表到每个桌子都连一条容量为 1 的边,最后每个桌子到汇点连一条容量为 cap[i] 的边。

#include <cstdio> 
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int MAXM = 160, MAXN = 270;
const int INF = 0x3f3f3f3f;
int n, m, rep[MAXM], cap[MAXN];
struct Edge {
    int u, v, c, next;
};
struct Dinic {
    Edge e[MAXN * MAXN];
    int head[MAXN << 1], d[MAXN << 1], cnt, s, t;
    queue<int> q;
    
    Dinic(int ts, int tt): s(ts), t(tt) {
        memset(head, 0, sizeof head);
        memset(e, 0, sizeof e);
        cnt = 2;
    }
    
    void add_edge(int u, int v, int c) {
        e[cnt] = (Edge) { u, v, c, head[u] };
        head[u] = cnt++;
        e[cnt] = (Edge) { v, u, 0, head[v] };
        head[v] = cnt++;
    }
    
    bool bfs() {
        while (q.size())
            q.pop();
        memset(d, 0, sizeof d);
        q.push(s);
        d[s] = 1;
        while (!q.empty()) {
            int cur = q.front();
            q.pop();
            for (int i = head[cur]; i; i = e[i].next) {
                if (e[i].c && !d[e[i].v]) {
                    d[e[i].v] = d[cur] + 1;
                    q.push(e[i].v);
                    if (e[i].v == t)
                        return true;
                }
            }
        }
        return false;
    }
    
    int dinic(int x, int flow) {
        if (x == t)
            return flow;
        int rest = flow, k;
        for (int i = head[x]; i && rest; i = e[i].next) {
            if (e[i].c && d[e[i].v] == d[x] + 1) {
                k = dinic(e[i].v, min(rest, e[i].c));
                if (!k)
                    d[e[i].v] = 0;
                e[i].c -= k, e[i ^ 1].c += k;
                rest -= k;
            }
        }
        return flow - rest;
    }
    
    int solve() {
        int maxflow = 0, flow = 0;
        while (bfs())
            while ((flow = dinic(s, INF)))
                maxflow += flow;
        return maxflow;
    }
};

int main() {
    scanf("%d%d", &m, &n);
    
    int sum = 0;
    
    for (int i = 0; i < m; i++) {
        scanf("%d", &rep[i]);
        sum += rep[i];
    }
    for (int i = 0; i < n; i++) {
        scanf("%d", &cap[i]);
    }
    
    int s = n + m, t = n + m + 1;
    Dinic ek = Dinic(s, t);
    
    for (int i = 0; i < m; i++)  {
        ek.add_edge(s, i, rep[i]);
        for (int j = 0; j < n; j++) {
            ek.add_edge(i, m + j, 1);
        }
    }
    for (int i = 0; i < n; i++)
        ek.add_edge(m + i, t, cap[i]);
    int ans = ek.solve();
    if (ans == sum)
        printf("1\n");
    else {
        printf("0");
        return 0;
    }
    for (int i = 0; i < m; i++) {
        vector<int> v;
        int size = 0;
        for (int j = ek.head[i]; j; j = ek.e[j].next) {
            if (ek.e[j].c == 0 && ek.e[j].v != s) {
                v.push_back(ek.e[j].v - m + 1);
                size++;
            }
        }
        for (int j = size - 1; j >= 0; j--)
            printf("%d ", v[j]);
        printf("\n");
    }
    return 0;
}

二分图的带权最大匹配

二分图的带权最大匹配:给定一张二分图,二分图的每条边都带有一个权值,求出该二分图的一组最大匹配,使得匹配边的权值总和最大。注意“权值总和最大”的前提是:当前匹配是二分图的最大匹配。

首先是解决这个问题的一个 $O(n^3)$ 算法——KM 算法,该算法只能在满足:带权最大匹配一定是完备匹配的条件下求解。来看几个定义:

交错树:在匈牙利算法中,如果从某个左部结点出发寻找匹配失败,那么在 DFS 的过程中,所有访问过的结点和访问这些节点所构成的边,构成一棵交错树(因为这棵树的根、叶子都是左部结点,并且左右交错)。
顶点标记值:在二分图中,给第 $i$ 个左部结点一个整数值 $A_i$ 和第 $j$ 个右部结点一个整数值 $B_j$,使其满足 $\forall i, j, A_i + B_j \ge w(i, j)$,其中 $w(i, j)$ 是连接结点 $i, j$ 的边的边权(若 $i,j$ 不连通则 $w(i,j) = -\infty$),称 $A_i$ 和 $B_j$ 为节点 $i, j$ 的顶点标记值。
相等子图:二分图中所有节点和满足 $A_i + B_j = w(i, j)$ 的边构成的子图,称为二分图的相等子图。

于是 KM 算法引入了这样一个定理:

在相等子图中如果存在完备匹配,则这个完备匹配一定是二分图的最大匹配。
在相等子图中完备匹配的边权之和为 \Sigma_{i=1}^{n}(A_i+B_j),也就是所有顶点标记值的和。根据定义顶点标记值之和不会超过边权,所以在整个二分图中,任何一组匹配的边权之和都不可能大于所有顶点标记值的和。

基于上述定理,KM 算法的基本思想就是:首先将每个左部结点的顶点标记值 $A_i$ 设置为经过$i$ 的每条边的最大权值 $max{w(i, j)}$,将每个右部结点的顶点标记值 $B_j$ 设置为 $0$.