二分图学习笔记(施工中)

定义

对于一个无向图 \(G=(V,E)\),如果存在点集 \(A,B\),满足 \(A\neq\varnothing\)\(B\neq\varnothing\)\(A\cap B=\varnothing\)\(A\cup B=V\),且 \(\forall u,v\in A\)\(u,v\in B\),都有 \((u,v)\notin E\),则称这个图是一个二分图\(A\) 称为这个二分图的左部\(B\) 称为右部

一个二分图可以记作 \(G=(A,B,E)\),下文中使用这种记法时,默认 \(|A|\le|B|\)

判定

一个图是二分图 \(\iff\) 图中没有奇环。

可以用 DFS 染色扩展域并查集 判断。

例题

二分图最大匹配

定义

  • 对于一个图 \(G=(V,E)\),如果一个边集 \(S\subseteq E\) 满足其中任意两条不同的边没有公共端点,则称 \(S\) 为图 \(G\) 的一个匹配

  • 对于匹配 \(S\),属于 \(S\) 的边叫做匹配边,不属于 \(S\) 的边叫做非匹配边

  • 如果点 \(u\) 是一条匹配边的端点,则称 \(u\)匹配点,否则是非匹配点

    匹配边的两端都是匹配点;非匹配边的两端要么都是匹配点,要么一个是匹配点,一个是非匹配点。

  • 对于二分图 \(G=(A,B,E)\),如果它的一个匹配 \(S\) 满足 \(|S|=|A|\),则称 \(S\)\(G\) 的一个完美匹配

求解

二分图最大匹配问题可以用网络流求解。建立超级源点 \(S\) 和超级汇点 \(T\),原图中每条边从左部连向右部,容量为 \(1\),从 \(S\) 向每个左部点连一条容量为 \(1\) 的边,从每个右部点向 \(T\) 连一条容量为 \(1\) 的边,新图的最大流就是最大匹配,其中匹配边流量为 \(1\),非匹配边流量为 \(0\)

在单位容量的网络中,使用 Dinic 的时间复杂度为 \(O(m\sqrt n)\)

我太菜了,不会匈牙利算法。

模板题:Luogu P3386 【模板】二分图最大匹配

点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int N = 1005, M = 2e5 + 5;
int n, m, e, t;
int hd[N], to[M], nxt[M], cap[M], tot = 1;
int dep[N], cur[N];
void add(int u, int v, int w) {
    to[++tot] = v;
    cap[tot] = w;
    nxt[tot] = hd[u];
    hd[u] = tot;
}
bool bfs() {
    memset(dep, 0, sizeof dep);
    queue<int> q;
    q.push(0);
    dep[0] = 1;
    while(!q.empty()) {
        int u = q.front();
        q.pop();
        for(int i = hd[u]; i; i = nxt[i]) 
            if(cap[i] && !dep[to[i]])
                dep[to[i]] = dep[u] + 1, q.push(to[i]);
    }
    return dep[t];
}
int dfs(int u, int flow) {
    if(u == t) return flow;
    int res = 0;
    for(int i = cur[u]; i && flow; i = nxt[i]) {
        cur[u] = i;
        if(dep[to[i]] == dep[u] + 1) {
            int o = dfs(to[i], min(cap[i], flow));
            flow -= o;
            res += o;
            cap[i] -= o;
            cap[i ^ 1] += o;
        }
    }
    return res;
}
int dinic() {
    int ans = 0;
    while(bfs()) {
        rep(i, 0, t) cur[i] = hd[i];
        ans += dfs(0, INT_MAX);
    }
    return ans;
}
signed main() {
#ifdef ONLINE_JUDGE
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
#endif
    cin >> n >> m >> e;
    t = n + m + 1;
    rep(i, 1, e) {
        int u, v; cin >> u >> v;
        v += n;
        add(u, v, 1), add(v, u, 0);
    }
    rep(i, 1, n) add(0, i, 1), add(i, 0, 0);
    rep(i, 1, m) add(i + n, t, 1), add(t, i + n, 0);
    cout << dinic() << endl;
    return 0;
}

常见模型

最小路径覆盖

Luogu P2764 最小路径覆盖问题

对于一个有向无环图 \(G=(V,E)\),如果存在一个简单路径的集合 \(S\),满足 \(V\) 中的每个点都恰好\(S\) 中的一条路径上,则称 \(S\)\(G\) 的一个路径覆盖。求出所含路径条数最少的路径覆盖。

首先每个点单独构成一条路径肯定是一组路径覆盖。考虑通过图中的边合并这些路径使它变小。

将每个点 \(u\) 拆成两个点 \(u_{in}\)\(u_{out}\),对于原图中的每一条边 \((u,v)\),在新图中连接 \((u_{out},v_{in})\)。在路径覆盖中,每个点最多只有 \(1\) 条入边和 \(1\) 条出边,这对应着新图中的每个点最多只被一条边选中,因此求出新图的最大匹配 \(P\),每选择一条边代表合并了一次路径,最小路径覆盖的大小即为 \(|V|-P\)。每条匹配边对应路径覆盖中的一条边,因此也容易构造出最小路径覆盖。

点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int N = 205, M = 6005;
int n, m, t;
int to[M * 4], nxt[M * 4], cap[M * 4], hd[N * 2], tot = 1;
int cur[N * 2], dep[N * 2]; 
int fr[N];
void add(int u, int v, int w) {
    to[++tot] = v;
    cap[tot] = w;
    nxt[tot] = hd[u];
    hd[u] = tot;
}
bool bfs() {
    memset(dep, 0, sizeof dep);
    dep[0] = 1;
    queue<int> q;
    q.push(0);
    while(!q.empty()) {
        int u = q.front();
        cur[u] = hd[u];
        q.pop();
        for(int i = hd[u]; i; i = nxt[i]) {
            int v = to[i];
            if(cap[i] && !dep[v]) 
                dep[v] = dep[u] + 1, q.push(v);
        }
    }
    return dep[t];
}
int dfs(int u, int flow) {
    if(u == t) return flow;
    int res = 0;
    for(int i = cur[u]; i && flow; i = nxt[i]) {
        cur[u] = i;
        int v = to[i];
        if(dep[v] == dep[u] + 1) {
            int o = dfs(v, min(flow, cap[i]));
            flow -= o;
            res += o;
            cap[i] -= o;
            cap[i ^ 1] += o;
        }
    }
    return res;
}
int dinic() {
    int ans = 0;
    while(bfs()) ans += dfs(0, INT_MAX);
    return ans;
}
int find(int x) {
    for(int i = hd[x]; i; i = nxt[i]) 
        if(!(i & 1) && !cap[i]) return to[i] - n;
    return 0;
}
signed main() {
#ifdef ONLINE_JUDGE
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
#endif
    cin >> n >> m;
    t = n * 2 + 1;
    rep(i, 1, n) {
        add(0, i, 1), add(i, 0, 0);
        add(i + n, t, 1), add(t, i + n, 0);
    }
    rep(i, 1, m) {
        int u, v; cin >> u >> v;
        add(u, v + n, 1), add(v + n, u, 0);
    }
    int ans = n - dinic();
    rep(i, 1, n) fr[find(i)] = i;
    rep(i, 1, n) if(!fr[i]) {
        cout << i << ' ';
        int o = find(i);
        while(o) {
            cout << o << ' ';
            o = find(o);
        }
        cout << endl;
    }
    cout << ans << endl;
    return 0;
}

最小可重路径覆盖

对于一个有向无环图 \(G=(V,E)\),如果存在一个简单路径的集合 \(S\),满足 \(V\) 中的每个点都至少\(S\) 中的一条路径上,则称 \(S\)\(G\) 的一个可重路径覆盖。

做传递闭包后转化为不可重路径覆盖。

常用定理

König 定理

对于二分图 \(G=(V,E)\),设 \(P\) 为其最大匹配的大小,则有最小点覆盖 \(=P\)(点覆盖:一个点集 \(S\subseteq V\),满足 \(\forall(u,v)\in E\),都有 \(u\in S\lor v\in S\)。即每条边至少选择一个端点

  • 证明(含构造方案):

    • 首先显然有最小点覆盖 \(\ge P\),因为每条匹配边至少有一个端点被选择。

    • 考虑从右部每个非匹配点开始 DFS,从右往左只能走非匹配边,从左往右只能走匹配边。设 左部被 DFS 到的点 和 右部没被 DFS 到的点 构成的集合为 \(S\)\(S\) 就是一个最小点覆盖。

    • 首先证明 \(|S|=P\)

      • 首先注意到每次 DFS 中,除了起点,被 DFS 到的都是匹配点。

      • 因为从左往右只能走匹配边,也就只能走到匹配点。而从右往左虽然只能走非匹配边,但是走不到非匹配点,因为如果走到了非匹配点,就说明发生了以下这种情况:(红色为匹配边)

      • 显然这存在更大的匹配,与最大匹配矛盾,因此不可能从右部走到非匹配点。

      • 而右部的非匹配点都作为起点被 DFS 过,没被 DFS 过的只能是匹配点。如果右部的一个匹配点没被 DFS 过,它就会被加入 \(S\)。右部的一个匹配点被 DFS 过,当且仅当其对应的左部匹配点也被 DFS 过,因此这个左部匹配点被加入 \(S\)。这说明对于每条匹配边,都有且仅有一个端点被加入了 \(\boldsymbol S\)。因此 \(|S|=P\)

    • 然后证明 \(S\) 是一个点覆盖:

      • 上文已经证明匹配边肯定全被覆盖。一条非匹配边没被 \(S\) 覆盖到的充要条件是,左部端点没被 DFS 过 且 右部端点被 DFS 过。
      • 如果一个右部点被 DFS 过,则所有与它相连的非匹配边都被 DFS 过,因此这个条件不可能满足,即不存在未被覆盖的边,所以 \(S\) 是一个点覆盖。
    • 综上,\(S\) 是一个最小点覆盖。

推论:最大独立集 \(=|V|-P\)(独立集:一个点集 \(S\subseteq V\),满足 \(\forall u,v\in S\),都有 \((u,v)\notin E\)。即选择的点之间不能有边

  • 证明:

    • 对于每个独立集 \(S\),其补集 \(\complement_VS\) 都是一个点覆盖。
    • 所以最大独立集对应最小点覆盖,大小为 \(|V|-P\)

最小边覆盖

对于二分图 \(G=(V,E)\),设 \(P\) 为其最大匹配的大小,则有最小边覆盖 \(=|V|-P\)(边覆盖:一个边集 \(S\subseteq E\),满足 \(\forall u\in V\),都 \(\exists v\in V,(u,v)\in E\)。即每个点至少选择一条出边

  • 证明:

    • 一条边加入边覆盖中,可能覆盖了一个或两个未覆盖的点。
    • 要使边覆盖最小,就要让覆盖了两个未覆盖点的边最多。
    • 这种边就是匹配边,向边覆盖中加入每条匹配边,总共覆盖了 \(2P\) 个点。
    • 剩下的点每个点对应一条边,这些边的数量为 \(|V|-2P\),边覆盖中的总边数就是 \(|V|-2P+P=|V|-P\)

Hall 定理:

对于二分图 \(G=(A,B,E)\),定义函数 \(f(S)\) \((S\subseteq A)\) 表示与 \(S\) 中的点有连边的点的数量,则 \(G\) 存在完美匹配的充要条件是 \(\forall S\subseteq A,f(S)\ge|S|\)

看着很对,证明不会。

例题:CF1009G Allowed Letters

有一个长为 \(n\) 的字符串 \(s\),只包含 \(\texttt a\dots\texttt f\)\(6\) 种字符。你知道每种字符的出现次数,和每个位置可能出现哪些字符,你需要构造出满足条件且字典序最小的 \(s\)

不难发现这是一个位置和字符之间的匹配问题,考虑建图网络流。

设字符 \(c\) 的出现次数为 \(\mathrm{cnt}_c\)。从源点 \(S\) 向每个位置 \(i\) 连容量为 \(1\) 的边,每个位置向这个位置可能出现的字符连容量为 \(1\) 的边,每个字符 \(c\) 向汇点 \(T\) 连容量为 \(\mathrm{cnt}_c\) 的边,跑最大流,如果能流满就有解,否则无解。

然而直接网络流难以找到字典序最小的匹配。网络流算法的流程很复杂,我们把握不住,所以尽量不要尝试改动网络流的板子,而是考虑其他方法。

最大 / 最小化字典序的问题都可以贪心。从前往后枚举位置 \(i\),尽量让 \(i\) 放更小的字符。因此我们需要解决的问题转化为:当位置 \(i\) 流向字符 \(c\) 时,剩下的点还能否流满。

注意到如果不算源点和汇点,这就判断二分图是否有完美多重匹配(多重匹配:每个点 \(u\) 最多被 \(l_u\) 条边覆盖,而非一般匹配的 \(1\) 条边)。当 \(A,B\) 中只有一个集合存在 \(u\) 满足 \(l_u\neq 1\) 时,我们可以通过把每个点拆成 \(l_u\) 个点转化成一般匹配。我们称位置对应的点集为“位置部”,字符对应的点集为“字符部”。

我们考虑使用 Hall 定理判定有没有完美匹配。

我们显然不能枚举位置部的所有子集,但是字符部只有 \(6\) 个本质不同的点,相同的点无论放多少个,对 \(f\) 值都只会产生一个点的影响,所以只需要枚举 \(6\) 种字符的所有子集,只有 \(2^6=64\) 种情况。

对于每个 \(i\),预处理出每个子集的后缀 \(f\) 值,即只考虑 \(i\)\(n\) 这些位置时,每个子集的 \(f\) 值,然后贪心枚举即可。

设字符集为 \(\Sigma\),则时间复杂度为 \(O(n|\Sigma|2^{|\Sigma|})\)

点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int N = 1e5 + 5;
int n, t, cnt[6], sum[64], f[N][64];
bool ava[N][6];
string s;
signed main() {
#ifdef ONLINE_JUDGE
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
#endif
    cin >> s; n = s.size();
    for(auto c : s) cnt[c - 'a']++;
    rep(i, 0, 63) rep(j, 0, 5) if(i >> j & 1) sum[i] += cnt[j];
    cin >> t;
    memset(ava, 1, sizeof ava);
    rep(i, 1, t) {
        int p; cin >> p >> s;
        memset(ava[p], 0, sizeof ava[p]);
        for(auto c : s) ava[p][c - 'a'] = 1;
    }
    per(i, n, 1) {
        memcpy(f[i], f[i + 1], sizeof f[i]);
        rep(j, 0, 63) rep(k, 0, 5)
            if((j >> k & 1) && ava[i][k]) 
                { ++f[i][j]; break; }
    }
    s.clear();
    rep(i, 1, n) {
        bool flg = 0;
        rep(j, 0, 5) if(ava[i][j]) {
            bool fl = 1;
            rep(k, 0, 63) 
                if(k >> j & 1) {
                    if(f[i + 1][k] < sum[k] - 1) { fl = 0; break; }
                }
                else if(f[i + 1][k] < sum[k]) { fl = 0; break; }
            if(fl) {
                flg = 1; s.push_back(j + 'a');
                rep(k, 0, 63) if(k >> j & 1) --sum[k];
                break;
            }
        }
        if(!flg) cout << "Impossible\n", exit(0);
    }
    cout << s << endl;
    return 0;
}

Dilworth 定理

对于一个有向无环图 \(G=(V,E)\),其最长反链的大小等于最小可重路径覆盖的大小。

反链:如果一个点集 \(S\subseteq V\) 满足 \(\forall u,v\in S,u\neq v\),不存在 \(u\)\(v\) 的路径,则称 \(S\) 为图 \(G\) 的一个反链

证明不会。

模板题:Luogu P4298 [CTSC2008] 祭祀

给定一个有向无环图 \(G=(V,E)\, (|V|=n,|E|=m)\),有三问:

  • 求出其最长反链长度。
  • 构造最长反链。
  • 判断每个点是否可能在最长反链中。

\(n\le100,m\le1000\)

第一问,根据 Dilworth 定理,做出传递闭包,做最小不可重路径覆盖即可。构造的二分图 \(G'=(V_{in},V_{out},E')\) 中的一条边 \((u_{out},v_{in})\in E\) 代表 \(u\) 能到达 \(v\)

第二问,找出二分图的最大独立集 \(S\),所有满足 \(u_{in}\in S\land u_{out}\in S\)\(u\) 构成的集合 \(L\) 就是最长反链。最大独立集是最小点覆盖的补集,先求出最小点覆盖再求补集即可,构造方案见上文 König 定理。

因为取出的每个 \(u\) 都满足 \((u_{in},u_{out})\) 都在同一个独立集中,所以这些点也构成独立集,也就是不存在边 \((u_{in},v_{out})\)(代表 \(u\) 能到达 \(v\)),满足反链的定义。

接下来证明这些点构成的反链是最长的:

\(P\)\(G'\) 的最大匹配数,显然 \(P\le n\)\(|S|=|V_{in}|+|V_{out}|-P=2n-P\)。满足在 \(v_{in}\)\(v_{out}\) 中有且仅有一个属于 \(S\) 的节点 \(v\) 最多只有 \(n-|L|\) 个,而每个 \(L\) 中的元素在 \(S\) 中对应两个元素,所以 \(|S|-2|L|\) 就是这样的 \(v\) 的数量,所以 \(|S|-2|L|\le n-|L|\),移项得到 \(|L|\ge|S|-n\)\(|L|\ge n-P\),反链最长也只有 \(n-P\),所以 \(|L|=n-P\)\(L\) 是最长反链。

第三问,枚举每个点分别判断。考虑强制选中这个点,然后再求最长反链,如果最长反链不变,这个点就可以在最长反链中。具体地,如果选中一个点,那么所有和这个点有偏序关系(路径)的点都不能选,所以将这些点都删掉,对剩下的点求最长反链,如果只减少了 \(1\),这个点就合法。

时间复杂度:求传递闭包的时间复杂度为 \(O(n^3)\),求完传递闭包后的边数 \(m'=O(n^2)\),跑一次二分图匹配的时间复杂度是 \(O(m'\sqrt n)=O(n^{2.5})\),第三问要枚举每个点分别跑二分图匹配,所以总时间复杂度为 \(O(n^{3.5})\)

点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int N = 105, M = 1005;
int n, m, c[N][N];
int to[N * N], nxt[N * N], cap[N * N], hd[N * 2], tot = 1;
int cur[N * 2], dep[N * 2], t;
bool del[N * 2], vis[N * 2];
string s1, s2;
void add(int u, int v, int w) {
    to[++tot] = v;
    cap[tot] = w;
    nxt[tot] = hd[u];
    hd[u] = tot;
}
bool bfs() {
    memset(dep, 0, sizeof dep);
    dep[0] = 1;
    queue<int> q;
    q.emplace(0);
    while(!q.empty()) {
        int u = q.front();
        q.pop();
        cur[u] = hd[u];
        for(int i = hd[u]; i; i = nxt[i]) {
            int v = to[i];
            if(cap[i] && !del[v] && !dep[v]) {
                dep[v] = dep[u] + 1;
                q.push(v);
            }
        }
    }
    return dep[t];
}
int dfs(int u, int flow) {
    if(u == t || !flow) return flow;
    int res = 0;
    for(int i = cur[u]; i && flow; i = nxt[i]) {
        cur[u] = i;
        int v = to[i];
        if(!del[v] && dep[v] == dep[u] + 1) {
            int o = dfs(v, min(cap[i], flow));
            flow -= o;
            res += o;
            cap[i] -= o;
            cap[i ^ 1] += o;
        }
    }
    return res;
}
int dinic() {
    int ans = 0;
    while(bfs()) ans += dfs(0, n);
    return ans;
}
void recover() {
    rep(i, 2, tot) 
        if(i & 1) cap[i] = 0;
        else cap[i] = 1;
    memset(del, 0, sizeof del);
}
void dfs(int u) {
    vis[u] = 1;
    for(int i = hd[u]; i; i = nxt[i]) 
        if(!vis[to[i]] && !cap[i]) dfs(to[i]);
}
signed main() {
#ifdef ONLINE_JUDGE
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
#endif
    cin >> n >> m;
    rep(i, 1, m) {
        int u, v; cin >> u >> v;
        c[u][v] = 1;
    }
    rep(k, 1, n) rep(i, 1, n) rep(j, 1, n)
        c[i][j] |= c[i][k] & c[k][j];
    t = n * 2 + 1;
    rep(i, 1, n) rep(j, 1, n) 
        if(c[i][j]) add(i, j + n, 1), add(j + n, i, 0);
    rep(i, 1, n) 
        add(0, i, 1), add(i, 0, 0), add(i + n, t, 1), add(t, i + n, 0);
    int l = n - dinic();
    cout << l << endl;

    vis[0] = vis[t] = 1;
    for(int i = hd[t]; i; i = nxt[i])
        if(!cap[i]) dfs(to[i]);
    rep(i, 1, n) s1.push_back(!vis[i] && vis[i + n] ? '1' : '0');
    cout << s1 << endl;
    
    rep(i, 1, n) {
        recover();
        del[i] = del[i + n] = 1;
        int t = n - 1;
        rep(j, 1, n) if(c[i][j] | c[j][i]) del[j] = del[j + n] = 1, --t;
        s2.push_back(t - dinic() == l - 1 ? '1' : '0');
    }
    cout << s2 << endl;
    return 0;
}

练习题

1. Luogu P2756 飞行员配对方案问题

二分图最大匹配板子。

点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int N = 105, M = 2e4 + 5;
int n, m, t;
int to[M], nxt[M], cap[M], hd[N], tot = 1;
int dep[N], cur[N];
void add(int u, int v, int w) {
    to[++tot] = v;
    cap[tot] = w;
    nxt[tot] = hd[u];
    hd[u] = tot;
}
bool bfs() {
    memset(dep, 0, sizeof dep);
    dep[0] = 1;
    queue<int> q;
    q.push(0);
    cur[0] = hd[0];
    while(!q.empty()) {
        int u = q.front();
        q.pop();
        for(int i = hd[u]; i; i = nxt[i]) {
            int v = to[i];
            if(!dep[v] && cap[i]) 
                cur[v] = hd[v], dep[v] = dep[u] + 1, q.push(v);
        }
    }
    return dep[t];
}
int dfs(int u, int flow) {
    if(u == t) return flow;
    int res = 0;
    for(int i = cur[u]; i && flow; i = nxt[i]) {
        int v = to[i];
        cur[u] = i;
        if(dep[v] == dep[u] + 1) {
            int o = dfs(v, min(cap[i], flow));
            res += o;
            flow -= o;
            cap[i] -= o;
            cap[i ^ 1] += o;
        }
    }
    return res;
}
int dinic(int ans = 0) {
    while(bfs()) ans += dfs(0, INT_MAX);
    return ans;
}
signed main() {
#ifdef ONLINE_JUDGE
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
#endif
    cin >> m >> n;
    t = n + 1;
    int u, v; cin >> u >> v;
    while(u != -1) {
        add(u, v, 1);
        add(v, u, 0);
        cin >> u >> v;
    }
    rep(i, 1, m) add(0, i, 1), add(i, 0, 0);
    rep(i, m + 1, n) add(i, t, 1), add(t, i, 0);
    cout << dinic() << endl;
    rep(u, 1, m) {
        for(int i = hd[u]; i; i = nxt[i]) 
            if(!(i & 1) && !cap[i]) cout << u << ' ' << to[i] << endl;
    }
    return 0;
}

2. Luogu P2763 试题库问题

二分图最大多重匹配板子。

点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int N = 1005, K = 25;
int n, k, T;
int to[N * K * 2], nxt[N * K * 2], cap[N * K * 2], hd[N + K], tot = 1;
int dep[N + K], cur[N + K];
void add(int u, int v, int w) {
    to[++tot] = v;
    cap[tot] = w;
    nxt[tot] = hd[u];
    hd[u] = tot;
}
bool bfs() {
    memset(dep, 0, sizeof dep);
    dep[0] = 1;
    queue<int> q;
    q.push(0);
    while(!q.empty()) {
        int u = q.front(); q.pop();
        cur[u] = hd[u];
        for(int i = hd[u]; i; i = nxt[i]) {
            int v = to[i];
            if(!dep[v] && cap[i])
                dep[v] = dep[u] + 1, q.push(v);
        }
    }
    return dep[T];
}
int dfs(int u, int flow) {
    if(u == T || !flow) return flow;
    int res = 0;
    for(int i = cur[u]; i && flow; i = nxt[i]) {
        cur[u] = i;
        int v = to[i];
        if(dep[v] == dep[u] + 1) {
            int o = dfs(v, min(cap[i], flow));
            flow -= o, res += o;
            cap[i] -= o, cap[i ^ 1] += o;
        }
    }
    return res;
}
void dinic() {
    while(bfs()) dfs(0, INT_MAX);
}
signed main() {
#ifdef ONLINE_JUDGE
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
#endif
    cin >> k >> n;
    T = n + k + 1;
    rep(i, 1, k) {
        int x; cin >> x;
        add(0, i, x), add(i, 0, 0);
    }
    rep(i, k + 1, k + n) {
        int c; cin >> c;
        while(c--) {
            int x; cin >> x;
            add(x, i, 1), add(i, x, 0);
        }
        add(i, T, 1), add(T, i, 0);
    }
    dinic();
    rep(i, 1, k) {
        cout << i << ':';
        for(int j = hd[i]; j; j = nxt[j]) {
            int v = to[j];
            if(v && !cap[j]) cout << ' ' << v - k;
        }
        cout << endl;
    }
    return 0;
}

3. Luogu P3254 圆桌问题

也是二分图最大多重匹配板子。

点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int M = 155, N = 275;
int m, n, T, sum;
int hd[N + M], to[N * M * 2], nxt[N * M * 2], cap[N * M * 2], tot = 1;
int dep[N + M], cur[N + M];
void add(int u, int v, int w) {
    to[++tot] = v;
    cap[tot] = w;
    nxt[tot] = hd[u];
    hd[u] = tot;
}
bool bfs() {
    memset(dep, 0, sizeof dep);
    queue<int> q;
    dep[0] = 1;
    q.push(0);
    while(!q.empty()) {
        int u = q.front();
        q.pop();
        cur[u] = hd[u];
        for(int i = hd[u]; i; i = nxt[i]) 
            if(cap[i] && !dep[to[i]])
                dep[to[i]] = dep[u] + 1, q.push(to[i]);
    }
    return dep[T];
}
int dfs(int u, int flow) {
    if(u == T || !flow) return flow;
    int res = 0;
    for(int i = cur[u]; i && flow; i = nxt[i]) {
        cur[u] = i;
        int v = to[i];
        if(dep[v] == dep[u] + 1) {
            int o = dfs(v, min(flow, cap[i]));
            flow -= o, res += o;
            cap[i] -= o, cap[i ^ 1] += o;
        }
    }
    return res;
}
int dinic() {
    int ans = 0;
    while(bfs()) ans += dfs(0, INT_MAX);
    return ans;
}
signed main() {
#ifdef ONLINE_JUDGE
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
#endif
    cin >> m >> n;
    T = m + n + 1;
    rep(i, 1, m) {
        int r; cin >> r; sum += r;
        add(0, i, r), add(i, 0, 0);
        rep(j, m + 1, m + n) 
            add(i, j, 1), add(j, i, 0);
    }
    rep(i, m + 1, m + n) {
        int c; cin >> c;
        add(i, T, c), add(T, i, 0);
    }
    if(dinic() == sum) cout << "1\n";
    else cout << "0\n", exit(0);
    rep(i, 1, m) {
        for(int j = hd[i]; j; j = nxt[j]) 
            if(to[j] && !cap[j]) cout << to[j] - m << ' ';
        cout << endl;
    }
    return 0;
}

4. Luogu P2765 魔术球问题

构造图 \(G\),编号为 \(i\) 的球对应节点 \(i\),如果 \(i+j\) 是完全平方数且 \(i<j\),那么由 \(i\) 号点向 \(j\) 号点连一条有向边。问题转化为求最大的 \(k\),使得当 \(G\)\(k\) 个节点(编号由 \(1\)\(k\))时,最小路径覆盖 \(\le n\)

\(1\) 开始枚举答案,依次加点最大流即可。二分答案也行,但是代码难度和时间复杂度都不如直接枚举。

点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int N = 60;
int n, T;
int hd[N * N * 2], to[N * N * 16], nxt[N * N * 16], cap[N * N * 16], tot = 1;
int dep[N * N * 2], cur[N * N * 2];
bool vis[N * N];
#define ou(x) ((x) << 1)
#define in(x) ((x) << 1 | 1)
inline bool chk(int x) {
    int o = sqrtl(x);
    if(o * o == x) return 1;
    return 0;
}
inline void add(int u, int v, int w) {
    to[++tot] = v;
    cap[tot] = w;
    nxt[tot] = hd[u];
    hd[u] = tot;
}
bool bfs() {
    memset(dep, 0, sizeof dep);
    dep[1] = 1;
    queue<int> q;
    q.push(1);
    while(!q.empty()) {
        int u = q.front();
        q.pop();
        cur[u] = hd[u];
        for(int i = hd[u]; i; i = nxt[i]) {
            int v = to[i];
            if(!dep[v] && cap[i]) 
                dep[v] = dep[u] + 1, q.push(v);
        }
    }
    return dep[T];
}
int dfs(int u, int flow) {
    if(u == T || !flow) return flow;
    int res = 0;
    for(int i = cur[u]; i && flow; i = nxt[i]) {
        cur[u] = i;
        int v = to[i];
        if(dep[v] == dep[u] + 1) {
            int o = dfs(v, min(cap[i], flow));
            flow -= o, res += o;
            cap[i] -= o, cap[i ^ 1] += o;
        }
    }
    return res;
}
int find(int u) {
    for(int i = hd[ou(u)]; i; i = nxt[i])
        if(to[i] > 1 && !cap[i]) return to[i] >> 1;
    return 0;
}
signed main() {
#ifdef ONLINE_JUDGE
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
#endif
    cin >> n;
    int i = 1, ans = 0;
    T = n * n * 2;
    while(1) {
        add(1, ou(i), 1), add(ou(i), 1, 0);
        add(in(i), T, 1), add(T, in(i), 0);
        rep(j, 1, i - 1) if(chk(i + j))
            add(ou(j), in(i), 1), add(in(i), ou(j), 0);
        while(bfs()) ans += dfs(1, INT_MAX);
        if(i - ans > n) break;
        ++i;
    }
    --i;
    cout << i << endl;
    vis[0] = 1;
    rep(j, 1, i) if(!vis[j]) {
        int o = j;
        do {
            vis[o] = 1;
            cout << o << ' ';
            o = find(o);
        } while(!vis[o]);
        cout << endl;
    }
    return 0;
}

5. Luogu P2172 [国家集训队] 部落战争

相对上一题更容易想到,每个点向它能到达的点连边,即可转化为最小路径覆盖问题。

点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int N = 55;
int n, m, r, c, T, cnt;
char s[N][N];
int hd[N * N * 2], to[N * N * 16], nxt[N * N * 16], cap[N * N * 16], tot = 1;
int dep[N * N * 2], cur[N * N * 2];
inline int id(int x, int y) {
    return (x - 1) * m + y;
}
inline int ou(int x) {
    return x << 1;
}
inline int in(int x) {
    return x << 1 | 1;
}
void add(int u, int v, int w) {
    to[++tot] = v;
    nxt[tot] = hd[u];
    cap[tot] = w;
    hd[u] = tot;
}
bool bfs() {
    memset(dep, 0, sizeof dep);
    dep[0] = 1;
    queue<int> q;
    q.push(0);
    while(!q.empty()) {
        int u = q.front();
        q.pop();
        cur[u] = hd[u];
        for(int i = hd[u]; i; i = nxt[i]) {
            int v = to[i];
            if(cap[i] && !dep[v])
                dep[v] = dep[u] + 1, q.push(v);
        }
    }
    return dep[T];
}
int dfs(int u, int flow) {
    if(u == T || !flow) return flow;
    int res = 0;
    for(int i = cur[u]; i; i = nxt[i]) {
        cur[u] = i;
        int v = to[i];
        if(dep[v] == dep[u] + 1) {
            int o = dfs(v, min(cap[i], flow));
            flow -= o, res += o;
            cap[i] -= o, cap[i ^ 1] += o;
        }
    }
    return res;
}
int dinic() {
    int ans = 0;
    while(bfs()) ans += dfs(0, INT_MAX);
    return ans;
}
signed main() {
    cin >> n >> m >> r >> c;
    const int dx[] = {r, r, c, c}, dy[] = {c, -c, r, -r};
    T = n * m * 2 + 2;
    rep(i, 1, n) scanf("%s", s[i] + 1);
    rep(i, 1, n) rep(j, 1, m) if(s[i][j] == '.') {
        ++cnt;
        int u = id(i, j);
        add(0, ou(u), 1), add(ou(u), 0, 0);
        add(in(u), T, 1), add(T, in(u), 0);
        rep(k, 0, 3) {
            int x = i + dx[k], y = j + dy[k];
            if(x < 1 || y < 1 || x > n || y > m) continue;
            int v = id(x, y);
            add(ou(u), in(v), 1), add(in(v), ou(u), 0);
        }
    }
    cout << cnt - dinic();
    return 0;
}

6. Luogu P4311 士兵占领

将行列看做点,将点看做边,问题转化为选择最少的边,覆盖每个点至少一定次数。发现这个东西和二分图最小边覆盖很相似,套用上文对最小边覆盖的证明即可得出,答案是 \(\sum L+\sum C-F\),其中 \(L,C\) 与题面意义相同,\(F\) 是最大多重匹配。

点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int N = 105;
int n, m, k, T, r[N], c[N];
bool ava[N][N];
int hd[N * 2], to[N * N * 2], nxt[N * N * 2], cap[N * N * 2], tot = 1;
int dep[N * 2], cur[N * 2];
void add(int u, int v, int w) {
    to[++tot] = v;
    nxt[tot] = hd[u];
    cap[tot] = w;
    hd[u] = tot;
}
bool bfs() {
    memset(dep, 0, sizeof dep);
    dep[0] = 1;
    queue<int> q;
    q.push(0);
    while(!q.empty()) {
        int u = q.front();
        q.pop();
        cur[u] = hd[u];
        for(int i = hd[u]; i; i = nxt[i]) {
            int v = to[i];
            if(cap[i] && !dep[v])
                dep[v] = dep[u] + 1, q.push(v);
        }
    }
    return dep[T];
}
int dfs(int u, int flow) {
    if(u == T || !flow) return flow;
    int res = 0;
    for(int i = cur[u]; i; i = nxt[i]) {
        cur[u] = i;
        int v = to[i];
        if(dep[v] == dep[u] + 1) {
            int o = dfs(v, min(cap[i], flow));
            flow -= o, res += o;
            cap[i] -= o, cap[i ^ 1] += o;
        }
    }
    return res;
}
int dinic() {
    int ans = 0;
    while(bfs()) ans += dfs(0, INT_MAX);
    return ans;
}
signed main() {
#ifdef ONLINE_JUDGE
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
#endif
    cin >> n >> m >> k;
    T = n + m + 1;
    int sum = 0;
    rep(i, 1, n) cin >> r[i], sum += r[i], add(0, i, r[i]), add(i, 0, 0);
    rep(i, 1, m) cin >> c[i], sum += c[i], add(i + n, T, c[i]), add(T, i + n, 0);
    rep(i, 1, k) {
        int u, v; cin >> u >> v;
        ++r[u], ++c[v]; ava[u][v] = 1;
        if(m < r[u]) cout << "JIONG!\n", exit(0);
        if(n < c[v]) cout << "JIONG!\n", exit(0);
    }
    rep(i, 1, n) rep(j, 1, m) if(!ava[i][j])
        add(i, j + n, 1), add(j + n, i, 0);
    cout << sum - dinic() << endl;
    return 0;
}
posted @ 2023-07-12 20:21  untitled0  阅读(37)  评论(1编辑  收藏  举报