二分图匹配
二分图:把无向图 G = (V,E)分为两个集合 V1,V2,所有的边都在 V1 和 V2 之间,而 V1 或 V2 的内部没有边。V1 中的一个点与 V2 中的一个点关联,称为匹配。
染色法:一个图是否为二分图,一般用“染色法”进行判断。用两种颜色对所有顶点进行染色,要求一条边所连接的两个相邻顶点的颜色不相同。染色结束后,如果所有相邻顶点的颜色都不相同,它就是二分图。
结论:一个图是二分图,当且仅当它不含边的数量为奇数的圈。
二分图最大匹配问题
二分图最大匹配问题:无权图,求包含边数最多的匹配,即二分图的最大匹配。
匈牙利算法
时间复杂度:
匈牙利算法可以看成最大流的一个特殊实现。
由于二分图是一个很简单的图,并不需要做一个标准的最大流,可以进行简化。
- 从上面的图解中发现对 s 和 t 的操作是多余的,直接从 a、b、c 开始找增广路径就可以了。
- 残留网络上的增广路需要覆盖完整的路径,如果在二分图中进行{a、b、c}到{x、y、z}的局部操作,将之用一个数组match[]就有覆盖整个路径的效果。
// 络谷 板子题(https://www.luogu.com.cn/problem/P3386)
#include<bits/stdc++.h>
using namespace std;
const int maxn = 501;
const int maxm = 5e4+1;
struct edge{
int to, next;
}e[maxm<<1]; int head[maxn], tot;
int match[maxn];
bool vis[maxn];
void addedge(int u, int v) {
e[++tot] = {v, head[u]}, head[u] = tot;
}
int dfs(int x) {
for(int i = head[x]; ~i; i = e[i].next) {
int j = e[i].to;
if (vis[j]) continue;
vis[j] = true;
if (!match[j] || dfs(match[j])) {
match[j] = x;
return 1;
}
}
return 0;
}
int main() {
int nx, ny, m; scanf("%d%d%d", &nx, &ny, &m);
memset(head, -1, sizeof head), tot = -1;
for(int i = 1; i <= m; ++ i) {
int u, v; scanf("%d%d", &u, &v);
addedge(u, v);
}
int maxmatch = 0;
for(int i = 1; i <= nx; ++ i) {
memset(vis, false, sizeof vis);
maxmatch += dfs(i);
}
printf("%d\n", maxmatch);
return 0;
}Hopcroft-Krap算法
时间复杂度:
预先找到多条路径最短的增广路,然后匈牙利算法就沿着bfs找的路径去找增广路。
bfs具体步骤:
- 把 V1 中所有没匹配的点加入队列
- 每次出来一个点 x,对于它连的 V2 中的每个点 y,如果 y 没访问过且没匹配过,找到增广路,否则把 y 的匹配点压入队列
// hdu 2063 (http://acm.hdu.edu.cn/showproblem.php?pid=2063)
#include<bits/stdc++.h>
using namespace std;
const int maxn = 501;
const int inf = 0x3f3f3f3f;
vector<int> g[maxn];
int cx[maxn], cy[maxn];
int dx[maxn], dy[maxn];
int vis[maxn];
int bfs(int n) { // bfs搜索多条不想交最短增广路径
memset(dx, 0, sizeof(dx));
memset(dy, 0, sizeof(dy));
queue<int> q;
for(int i = 1; i <= n; ++ i) {
if (!cx[i]) q.push(i);
}
int dis = inf;
while(!q.empty()) {
int t = q.front(); q.pop();
if (dx[t] >= dis) continue;
for(auto i: g[t]) {
if (dy[i]) continue; // 多条路径不想交
dy[i] = dx[t] + 1;
if (!cy[i]) dis = dy[i]; // 最短路径已找到
else {
dx[cy[i]] = dy[i];
q.push(cy[i]);
}
}
}
return dis != inf;
}
int dfs(int x) { // 按照bfs搜索路径进行dfs
for(auto i: g[x]) {
if (vis[i] || dy[i] != dx[x] + 1) continue;
vis[i] = 1;
if (!cy[i] || dfs(cy[i])) {
cx[x] = i, cy[i] = x;
return 1;
}
}
return 0;
}
int HK(int n) { // HK算法
memset(cx, 0, sizeof(cx));
memset(cy, 0, sizeof(cy));
int res = 0;
while(bfs(n)) {
memset(vis, 0, sizeof(vis)); // 模拟dy[] 保证路径不相交
for(int i = 1; i <= n; ++ i) {
if (!cx[i] && dfs(i)) res ++;
}
}
return res;
}
int main() {
int n; while(scanf("%d", &n), n) {
int x, y; scanf("%d%d", &x, &y);
for(int i = 1; i <= x; ++ i) g[i].clear();
for(int i = 1; i <= n; ++ i) {
int u, v; scanf("%d%d", &u, &v);
g[u].push_back(v);
}
printf("%d\n", HK(x));
}
return 0;
}
Dinic 算法
用网络流来写二分图最大匹配。
// 络谷 板子题(https://www.luogu.com.cn/problem/P3386)
#include<bits/stdc++.h>
using namespace std;
const int maxn = 5010 << 1;
const int maxm = 5e4 + maxn;
struct edge{
int to, next, cap, flow;
}e[maxm<<2]; int head[maxn], tot;
int nx, ny, m, s, t, d[maxn], cur[maxn];
void addedge(int u, int v, int w) {
e[++tot] = {v, head[u], 1, 0}, head[u] = tot;
e[++tot] = {u, head[v], 0, 0}, head[v] = tot;
}
void init() { // 连接源点 和 汇点
memset(head, -1, sizeof head);
tot = -1;
s = nx + ny + 1;
for(int i = 1; i <= nx; ++ i) {
addedge(s, i, 1);
}
t = nx + ny + 2;
for(int i = 1; i <= ny; ++ i) {
addedge(i + nx, t, 1);
}
}
bool bfs() {
memset(d, 0x7f, sizeof d);
queue<int> q; q.push(t);
d[t] = 0;
while(!q.empty()) {
int x = q.front(); q.pop();
for(int i = head[x]; ~i; i = e[i].next) {
int j = e[i].to;
if (d[j] > d[x] + 1 && e[i^1].cap > e[i^1].flow) {
d[j] = d[x] + 1;
if (j == s) return true;
q.push(j);
}
}
}
return false;
}
int dfs(int x, int flow, int used = 0) {
if (x == t) return flow;
// for(int i = cur[x]; ~i && flow != used; i = e[ cur[x] = i ].next) { // 当前弧优化
for(int i = head[x]; ~i && flow != used; i = e[i].next) {
int j = e[i].to, f;
if (d[x] - 1 == d[j] && e[i].cap > e[i].flow) {
f = dfs(j, min(flow - used, e[i].cap - e[i].flow));
if (f) e[i].flow += f, e[i^1].flow -= f, used += f;
}
}
if (!used) d[x] = 0; // 剪枝优化
return used;
}
int main() {
scanf("%d%d%d", &nx, &ny, &m);
init();
for(int i = 1; i <= m; ++ i) {
int u, v; scanf("%d%d", &u, &v);
addedge(u, v + nx, 1);
}
int maxflow = 0;
while(bfs()) {
// memcpy(cur, head, sizeof head);
maxflow += dfs(s, INT_MAX);
}
printf("%d\n", maxflow);
return 0;
}
二分图求最大权匹配
KM 算法
时间复杂度:
KM算法:用于求二分图匹配的最佳匹配(最大权完美匹配)。
算法过程:
- 用邻接矩阵存图,给每个点加一个期望(cx[] 或 cy[]),当一对匹配的期望和等于其权值时该匹配才有可能匹配,我们需要最大权值和,所以先假设期望可以最大 cx[i] = max{与它有关的匹配权值},cy[i] = 0。
- 尝试对第 i 个点进行匹配,若匹配成功,则进行下一个;若匹配失败,则降低左边已匹配点和该点的期望,提高右边已匹配点的期望,通过降低期望的方式尝试与独立点进行匹配,重复操作2。
注:初始化各点期望后,实线为可能匹配,虚线暂时不能匹配。(绿色为匹配成功,黄色为增广路,红色为匹配失败)
注: 若经过图四修改后 x2 仍然匹配失败,则重复图四修改即可。
注:代码中 km算法 是完备匹配下的最大期望,若初始化为0,则是最大权值和。
// hdu 2255 (http://acm.hdu.edu.cn/showproblem.php?pid=2255)
#include<bits/stdc++.h>
using namespace std;
const int maxn = 301;
int graph[maxn][maxn];
int match[maxn], hx[maxn], hy[maxn], d[maxn], pre[maxn];
bool vis[maxn];
int km(int n) {
memset(match, 0, sizeof match);
for(int i = 1; i <= n; ++ i) { // 初始化期望权值
hx[i] = INT_MIN, hy[i] = 0;
for(int j = 1; j <= n; ++ j) {
hx[i] = max(graph[i][j], hx[i]);
}
}
for(int i = 1; i <= n; ++ i) { // 尝试匹配
memset(d, 0x7f, sizeof d);
memset(vis, false, sizeof vis);
int pos = 0, y;
match[pos] = i;
do { // 通过不段提高期望来满足匹配
int dis = INT_MAX, x = match[pos];
vis[pos] = true;
for(int j = 1; j <= n; ++ j) {
if (vis[j]) continue;
if (d[j] > hx[x] + hy[j] - graph[x][j]) {
d[j] = hx[x] + hy[j] - graph[x][j];
pre[j] = pos;
}
if (dis > d[j]) {
dis = d[j];
y = j;
}
}
for(int j = 0; j <= n; ++ j) {
if (vis[j]) hx[ match[j] ] -= dis, hy[j] += dis;
else d[j] -= dis;
}
pos = y;
} while(match[pos]);
for(int j = pos; j; j = pre[j]) {
match[j] = match[ pre[j] ];
}
}
int res = 0;
for(int i = 1; i <= n; ++ i) {
res += graph[ match[i] ][i];
}
return res;
}
int main() {
int n; while(~scanf("%d", &n)) {
for(int i = 1; i <= n; ++ i) {
for(int j = 1; j <= n; ++ j) {
scanf("%d", &graph[i][j]);
}
}
printf("%d\n", km(n));
}
return 0;
}

京公网安备 11010502036488号