匹配问题
1. 算法分析
匹配问题可以转换为二分匹配来处理,匈牙利算法求解;选择问题中有部分可以转换为点覆盖集问题,选择a或者b,那么a<->b,然后每条边只能选择其中一个。
1.1 几个重要概念
1.交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路
2.增广路:从一个未匹配的点出发,走交替路,如果途径另一个未匹配点,则这条交替路称为增广路
1.2 二分图判定
一张图是二分图当且仅当图中没有奇数环,二分图必然不含有奇数环
1.3 二分图点覆盖、独立集和最小路径点覆盖
最大匹配数=最小点覆盖=总点数-最大独立集=总点数-最小路径点覆盖
1.3.1 二分图的点覆盖
最小点覆盖: 求一个最小的点集S,使得图中任意一条边都有至少一个端点属于S。
定理: 最小覆盖集中点数目等于二分图的最大匹配包含的边数
1.3.2 二分图的独立集
定义: 一个独立集内的点没有连边
定理: 一个二分图的最大独立集内点个数等于总个数n-最大匹配个数
最大团: 一个点集的所有点间都有边相连,一张图G的最大独立集的补集就是最大团
1.3.3 DAG的最小路径点覆盖
DAG的最小路径点覆盖是描述: 给定一张有向无环图,要求用尽量少的不相交的简单路径,覆盖有向无环图的所有顶点(也就是每个顶点恰好被覆盖一次)。
拆点: 把原图中的每个点拆分成两个点,比如i点拆成i点和i’点,把原来的连边和新点连接起来,比如说原图i->j,那么拆点后,把i和j’连边(i和j不用连边),即i->j’,这样的方式组成一张新图
![Screenshot_20201224_133504.jpg](https://i-blog.csdnimg.cn/blog_migrate/50fe2cf871da75fe80f822c930338800.jpeg)
定理: 最小路径点覆盖的数目等于原先所有的点数n-新图最大二分匹配的数目
优化: i->j’的边可以直接建为i->j。由于这里新建完的图是二分图,左部图是<=n的点,右部图是>=n的点,那么本来i->j’的边可以直接建为i->j,也就是说把左部图里的点j放在右部图内,然后对于左右部图都做二分匹配,本来这样计算答案要除以2,但是左部图和右部图规模都减小了1/2,因此抵消了。
1.3.4 DAG的最小路径可重复点覆盖
1.利用传递闭包把DAG变成一张没有重复点的图
2.套用上述求DAG的最小路径覆盖的算法
1.3.5 有向环覆盖问题
问题描述: 给你一个N个顶点M条边的带权有向图,要你把该图分成一个或多个不相交的有向环(点和边都不相交)。且所有定点都被有向环覆盖。问你该有向环所有权值的总和最小是多少?
结论: 原图有向环最大权值覆盖=新图最优匹配。把任意一个顶点i都分成两个,即i和i’. 如果原图存在i->j的边,那么二分图有i->j’的边.然后跑最优匹配。
拓展: 如果,改为无向图,问你无向环最大权值覆盖?答案也是一样的。只是在建图的时候把有向改为无向即可。
进一步结论:
① 如果原图能由多个不相交的有向环覆盖,那么二分图必然存在完备匹配。他们互为充要条件,也就是说如果二分图存在完备匹配,那么原图必定能由几个不想交的有向环覆盖。因此如果只需要判定十分存在有向环覆盖,只需要判断跑最大匹配判断即可。
② 如果原图存在权值最大的有向环覆盖,那么二分图的最优匹配一定就是这个值。即权值最大的有向环覆盖在数值上等于改图的最优匹配值。
1.4 各类二分匹配问题
1.4.1 最大匹配
在一张无权的二分图中,一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。找最大匹配数可以使用匈牙利算法实现 O ( n 2 ) O(n^2) O(n2),也可以使用最大流算法(dinic)解决 O ( n m 2 ) O(nm^2) O(nm2)。
1.4.2 完美(备)匹配
在一张无权的二分图中,如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。完美匹配一定是最大匹配,但并非每个图都存在完美匹配。
1.4.3 最大权匹配/最优匹配
最优匹配就是指在带权边的二分图中,求一个匹配使得匹配边上的权值和最大。如果匹配是完美匹配,那么这个问题可以使用km算法来实现,它的算法复杂度是 O ( n 3 ) O(n^3) O(n3)。如果匹配不一定是完美匹配,那么将其转化为最小费用最大流来做,它的复杂度是 O ( n 2 m ) O(n^2m) O(n2m)。
1.4.4 多重匹配
多重匹配就是解决一连多的问题.
1.4.4.1 多重最大匹配
在一张无权二分图中,找到一对多的最大匹配。使用网络流算法解决:在原图上建立源点S和汇点T,S向每个X方点连一条容量为该X方点L值的边,每个Y方点向T连一条容量为该Y方点L值的边,原来二分图中各边在新的网络中仍存在,容量为1(若该边可以使用多次则容量大于1),求该网络的最大流,就是该二分图多重最大匹配的值。也可以使用匈牙利算法解决,只需要一个点维护多个匹配点即可。
1.4.4.2 多重最优匹配
在一张带权二分图中,找到一对多的最大权匹配。使用网络流算法解决。在原图上建立源点S和汇点T,S向每个X方点连一条容量为该X方点L值、费用为0的边,每个Y方点向T连一条容量为该Y方点L值、费用为0的边,原来二分图中各边在新的网络中仍存在,容量为1(若该边可以使用多次则容量大于1),费用为该边的权值。求该网络的最大费用最大流,就是该二分图多重最优匹配的值。
1.5 一般图匹配
带花树算法 O ( n 3 ) O(n^3) O(n3)
2. 模板
2.1 染色法判断是否为二分图
#include <bits/stdc++.h>
using namespace std;
int const N = 1e5 + 10;
int e[N * 2], ne[N * 2], h[N], idx, color[N]; // color记录每个点颜色
int n, m;
// 建立邻接表
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// 判断是否u号点可以打上c的颜色
int dfs(int u, int c) {
color[u] = c; // 给u号点打上c的颜色
for (int i = h[u]; i != -1; i = ne[i]) {
// 遍历所有与i号点相连的点
int j = e[i]; // 看j号点
if (!color[j]) {
// 如果j号点没有染色
if (!dfs(j, 3 - c)) return 0; // 如果j号点染色3-c过程中失败
}
if (color[j] == c) return 0; // 如果j号点也染色c颜色
}
return 1; // 如果u号点的所有邻点都没有染色失败
}
int main() {
memset(h, -1, sizeof h); // 初始化h
cin >> n >> m; // 输入顶点数和边数
for (int i = 0; i < m ; ++i) {
// 读入边信息
int a, b;
scanf("%d %d", &a, &b);
add(a, b), add(b, a); // 无向边
}
int flg = 1; // flg记录染色是否成功,成功为1,失败为0
for (int i = 1; i <= n; ++i) {
// 从1号点开始枚举
if (!color[i]) {
// 如果i号点为染色
if (!dfs(i, 1)) {
// 如果i号点染色失败
flg = 0; // flg 打上失败标记
break;
}
}
}
if (flg) cout << "Yes\n";
else cout << "No\n";
return 0;
}
2.2 二分图最大匹配
2.2.1 匈牙利算法(O(VE))
#include <bits/stdc++.h>
using namespace std;
int const N = 110, M = 1010;
int e[M], ne[M], idx, h[N], match[N], st[N]; // match[j] = x: 右半部分的j匹配左半部分的x,st[j] = 1:右半部分的j匹配到人了
int n, m, k;
// 建立邻接表
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
/* 邻接矩阵写法
bool find(int x) {
for (int i = 1; i <= n; ++i) {
if (l[x][i]) continue; // 没有边
if (!st[i]) {
st[i] = 1;
if (!match[i] || find(match[i])) {
match[i] = x;
return true;
}
}
}
return false;
}
*/
// 找左半部分的x是否能够在右半部分找到匹配的对象
bool find(int x) {
for (int i = h[x]; i != -1; i = ne[i]) {
// 遍历所有与x之间相连的点
int j = e[i]; // 点为j
if (!st[j]) {
// 如果j没有匹配过
st[j] = 1; // 记录j匹配过
if (!match[j] || find(match[j])) {
// 如果右半部分的j没有匹配到左半部分的人或者j匹配到的可以去匹配其他右半部分的人
match[j] = x; // 记录右半部分的j和左半部分的x匹配
return true; // 找到x的匹配对象
}
}
}
return false; // 全部都遍历仍然没有成功,返回false
}
int hungary() {
int res = 0; // 记录结果数目
for (int i = 1; i <= n; ++i) {
// 从左半部分向右半部分匹配
memset(st, 0, sizeof st); // 每次匹配时对右半部分的女生情况匹配情况
if (find(i)) res++; // 如果能够找到,res加一
}
return res;
}
int main() {
while (scanf("%d %d %d", &n, &m, &k) != EOF && n != 0) {
memset(h, -1, sizeof h); // 初始化h
memset(e, 0, sizeof e);
memset(ne, 0, sizeof ne);
memset(match, 0, sizeof match);
idx = 0;
for (int i = 0; i < k; ++i) {
// 读入边信息
int a, b, c;
scanf("%d %d %d", &c, &a, &b);
if (!a || !b) continue; // 起始点是0,不用覆盖
add(a, b); // 只需要add一次即可,因为二分匹配时只需要从左半部分向右半部分匹配就行
}
cout << hungary() << endl;
}
return 0;
}
2.2.2 hopcroft-karp算法(O(sqrt(V) * E ))
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
int const MAXN = 3e3 + 10, MAXM = MAXN * MAXN, INF = 1e9 + 10;
int n, m, T, t, kase = 1;
int idx, h[MAXN], ne[MAXM], e[MAXM], xline[MAXN], yline[MAXN], dx[MAXN], dy[MAXN], st[MAXN], dis;
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
struct Node {
int x, y, v;
}node[MAXN];
int check(int a, int x, int y) {
double dis = sqrt((node[a].x - x) * (node[a].x - x) + (node[a].y - y) * (node[a].y - y));
return dis/(double)node[a].v <= t;
}
int bfs() {
queue<int> q;
dis = INF; // dis是全局变量
memset(dx, -1, sizeof(dx)); // dx和dy每次bfs都需要初始化
memset(dy, -1, sizeof(dy));
for (int i = 1; i <= m; i++) // 左部点
if (xline[i] == -1) {
q.push(i);
dx[i] = 0;
}
while (!q.empty()) {
int t = q.front();
q.pop();
if (dx[t] > dis) break;
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if (dy[j] == -1) {
dy[j] = dx[t] + 1;
if (yline[j] == -1) dis = dy[j];
else {
dx[yline[j]] = dy[j] + 1;
q.push(yline[j]);
}
}
}
}
return dis != INF;
}
int find(int t) {
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if (!st[j] && dy[j] == dx[t] + 1) {
st[j] = 1;
if (yline[j] != -1 && dy[j] == dis) continue;
if (yline[j] == -1 || find(yline[j])) {
yline[j] = t, xline[t] = j;
return 1;
}
}
}
return 0;
}
int Maxmatch() {
//最大匹配
int res = 0;
while (bfs()) {
memset(st, 0, sizeof(st));
for (int i = 1; i <= m; i++) // 遍历左部图每个点
if (xline[i] == -1 && find(i)) res++;
}
return res;
}
int main() {
cin >> T;
while(T--) {
memset(xline, -1, sizeof(xline)); // 初始化左边标记
memset(yline, -1, sizeof(yline)); // 初始化右边标记
memset(h, -1, sizeof h);
scanf("%d", &t);
scanf("%d", &m); // 左部m个点,右部n个点
idx = 0;
for (int i = 1; i <= m; ++i) scanf("%d%d%d", &node[i].x, &node[i].y, &node[i].v);
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
int x, y;
scanf("%d%d", &x, &y);
for (int j = 1; j <= m; ++j) {
if (check(j, x, y)) add(j, i); // 如果能够建边
}
}
printf("Scenario #%d:\n", kase++);
cout << Maxmatch() << endl << endl;
}
return 0;
}
2.3 二分图最优匹配(必须是完美匹配)
// 本板子是求最大权的最优匹配,如果是求最小权值的最优匹配,那么只需要将所有的边权取负值,然后再次跑km算法,最后答案取反即可。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
LL const INF = 0x3f3f3f3f3f3f3f3f;
int const MAXN = 500;
int n;
LL g[MAXN][MAXN], hl[MAXN], hr[MAXN], slk[MAXN];
int fl[MAXN], fr[MAXN], pre[MAXN], vl[MAXN], vr[MAXN], q[MAXN], ql, qr;
LL a[MAXN], b[MAXN], p[MAXN], c[MAXN];
inline int check(int i) {
vl[i] = 1;
if (fl[i] != -1) {
q[qr++] = fl[i];
vr[fl[i]] = 1;
return 1;
}
while (i != -1) {
fl[i] = pre[i];
swap(i, fr[fl[i]]);
}
return 0;
}
void bfs(int s) {
for (int i = 1; i <= n; i++) vl[i] = vr[i] = 0, slk[i] = INF;
for (vr[q[ql = 0] = s] = qr = 1;;) {
for (LL d; ql < qr;) {
for (int i = 1, j = q[ql++]; i <= n; i++) {
if (!vl[i] && slk[i] >= (d = hl[i] + hr[j] - g[i][j])) {
pre[i] = j;
if (d)
slk[i] = d;
else if (!check(i))
return;
}
}
}
LL d = INF;
for (int i = 1; i <= n; i++) {
if (!vl[i] && d > slk[i]) d = slk[i];
}
for (int i = 1; i <= n; i++) {
if (vl[i])
hl[i] += d;
else
slk[i] -= d;
if (vr[i]) hr[i] -= d;
}
for (int i = 1; i <= n; i++)
if (!vl[i] && !slk[i] && !check(i)) return;
}
}
LL KM() {
for (int i = 1; i <= n; i++) fl[i] = fr[i] = -1, hr[i] = 0; // 初始化标杆:左标杆fl,右标杆fr
for (int i = 1; i <= n; i++) hl[i] = *max_element(g[i] + 1, g[i] + n + 1); // 一开始左标杆的值
for (int j = 1; j <= n; j++) bfs(j); // O(n^3)的bfs解法
LL re = 0;
for (int i = 1; i <= n; i++)
if (g[i][fl[i]])
re += g[i][fl[i]];
else
fl[i] = 0;
return re;
}
int main() {
while (scanf("%d", &n) != EOF) {
/* 如果是求最小权值的最优匹配,这里建图要这样初始化
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
g[i][j] = -INF;
*/
for (int i = 1; i <= n; ++i) // 读入任意两个点的权值关系
for (int j = 1; j <= n; ++j) scanf("%lld", &g[i][j]);
// 如果是求最小权值的最优匹配,那么这里g[i][j] = -g[i][j]
LL ans = KM();
printf("%lld\n", ans); // 如果是求最小权值的最优匹配,这里ans = -ans
}
return 0;
}
2.4 二分图多重匹配
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXN = 1010;
int st[MAXN], g[MAXN][MAXN], n, m;
struct NODE {
int cnt;
int matchs[MAXN];
} match[MAXN];
bool dfs_solve(int x, int limit) {
for (int i = 1; i <= m; ++i) {
// 枚举右部图的每个点
if (!st[i] && g[x][i]) {
st[i] = 1;
if (match[i].cnt < limit) {
// 如果当前点的匹配数目小于limit,那么直接匹配。如果每个点能够匹配的数目不同,那么这里使用num[i](替代limit)表示右部图第i个点能够匹配的点数目
match[i].matchs[++match[i].cnt] = x;
return 1;
}
for (int j = 1; j <= match[i].cnt; j++) {
// 如果当前点的匹配数目等于limit
if (dfs_solve(match[i].matchs[j], limit)) {
// 那么让当前所匹配的点去匹配其他点
match[i].matchs[j] = x;
return 1;
}
}
}
}
return 0;
}
int hungary(int limit) {
// 多重匹配的最大匹配
int res = 0;
memset(match, 0, sizeof(match));
for (int i = 1; i <= n; ++i) {
memset(st, 0, sizeof(st));
if (dfs_solve(i, limit)) res++; // 这里还可以优化,改为if (!dfs_solve(i, l, r)) return 0;意思为如果当前这个点没有找到匹配点,那么则不可能找到使得左部图每个点都匹配。
}
return res; // 如果40行采用优化写法,这个改为:return 1;
}
int main() {
while (scanf("%d%d", &n, &m) != EOF) {