二分图匹配相关问题
一,二分图匹配问题
1.概念
最大匹配: 图中包含边数最多的匹配称为图的最大匹配。
完美匹配: 所有点都在匹配边上。
最小顶点覆盖: 用最少的点让每条边都至少和一个点关联。可以证明:最小覆盖需要的点集 = 最大匹配数
最小路径覆盖:用最少的不相交的覆盖有向无环图G的所有结点。解决此类问题可以建立一个二分图模型。把所有顶点i拆成两个:X结点集中的i和Y结点集中的i',如果有边i->j,则在二分图中引入边i->j',设二分图最大匹配为m,则结果就是n-m。
最大独立集问题: 在N个点的图中选出m个点,使这m个点两两之间没有边.求m最大值.最大独立集点数 = N - 最小顶点覆盖(最大匹配数),就是把最多能匹配的那对去掉一个点。
2.例题分析
POJ2271: 一群男孩和女孩共N人,某些男孩和女孩之间会发生恋爱关系(满足一定的关系),现在希望找到最多的孩子,他们之间不会发生恋爱关系。
分析:找到最多的孩子,没有恋爱关系,这实质上是找最大独立集。假设男孩在左有a个,女孩在右有b个,那么如果某男孩和某女孩之间有关系,就连线。最大独立集就是找到最多顶点,顶点之间没有联系,正好就是所求,而最大独立集就是 N-最大匹配,所以问题得到解决。试想,如果二分图中没有连线,那么所有的孩子都可选,最大独立集也是N,他们是等价的。如果存在一条连线,那么去掉一个孩子就是所找的孩子,最大独立集此时是N-1.依次类推。在试想,最大匹配其实就找到了几对恋爱对象,我们只需要把他们的另一半去掉,就是我们找的孩子。
POJ3692:一群男孩B个(他们互相认识),一群女孩G个(他们互相认识),某些男孩和某些女孩相识,现在找出最多的孩子,他们互相认识
分析:题目要找的是一群孩子,他们之间都互相认识,也就是说这是一个团(其中任意两个顶点之间都有连线)。可是如果直接去找团,可能比较麻烦。因为这是二分图,自然要利用二分图的性质。在二分图的算法里面没有找团的相关算法,正难则反,找出最多的孩子,他们之间互不认识,这不是就是求最大独立集嘛。建立这样的二分图,左边是男孩,右边是女孩,如果男孩和女孩不认识就连上边,在这样的二分图中,找最大独立集,就是所有的相互认识的孩子个数。
POJ1466:一群人N,某人可能与多人有罗曼史,性别未知,但一定是异性。找出最多人之间无罗曼史
分析:因为性别未知,所以可以把所有的人当成左顶点,右边也是所有人,建立二分图,可以想象,这样求出来的最大匹配是男女分开建立的二分图的最大匹配的二倍。而题目让找最大独立集,所以应该是N-最大匹配/2;
POJ1325:有两台机器,有多个任务,每个任务都可在这两台机器上运行,不过不同的模式需要重启电脑,很浪费时间,现在要找出最好的调度方式,减少调度时间。
分析:最少的顶点覆盖最多的边(任务),所以是最小顶点覆盖问题
POJ2060:有很多人预订出租车,如果出租车做完一个任务能够敢到下一个任务,就不需要在调度一辆出租车了,现在请问最少需要几辆出租车。
分析:最小路径问题,对任务构图,将一个任务拆开成两个点,建立二分图,如果一个任务能够完成之后赶到下个任务就连线,然后就是二分图问题了。最小路径等值于二分图的最大独立集
POJ3041:一个矩阵,某些位置有小行星,有一种炸弹,一次可以炸掉一行或者一列,现在问题是需要最少用多少这样的炸弹。
分析:模型转化,非常巧妙的利用二分图来解决。利用二分图必须有左顶点和右顶点,我们把行作为左顶点,列作为右顶点,如果该行和该列的交点有小行星,就连线。我们要用最少的点覆盖所有的边,最小顶点覆盖等值于最大匹配。
3.代码
匈牙利算法,通过找交错路来倍增一条匹配边
#include <iostream> #include <cstring> #include <cstdio> using namespace std; const int N = 508; int G[N][N], m, n; int link[N], used[N]; bool path(int u) { for (int i = 0; i < m; i++) if (G[u][i] && !used[i]) { used[i] = 1; if (link[i] == -1 || path(link[i])) { link[i] = u; return 1; } } return 0; } void hungary() { int i, ans = 0; memset(link, -1, sizeof(link)); for (i = 0; i < n; i++) { memset(used, 0, sizeof(used)); if (path(i)) ans++; } cout << n - ans / 2 << endl; } int main() { int i, j, a, b, k; while (~scanf("%d%d", &m, &n)) { memset(G, 0, sizeof(G)); for (i = 0; i < n; i++) { scanf("%d%d", &a, &k); for (j = 0; j < k; j++) { scanf("%d", &b); G[a][b] = 1; } } hungary(); } return 0; }
二,多重匹配问题
//HDU3608
#include<iostream> #include<vector> using namespace std; #define N 100005 #define M 12 int G[N][M]; //i适合在j上生存 int cap[M]; //星球的容量 vector<int> link[M]; //星球上居住的人 bool vis[M]; int n, m; bool path(int x) { for (int i = 0; i < m; i++) { //枚举每个星球 if (!vis[i] && G[x][i]) { vis[i] = 1; if (link[i].size() < cap[i]) { //i星球上人数小于容量 link[i].push_back(x); return 1; } for (int j = 0; j < link[i].size(); j++) { if (path(link[i][j])) { //给link[i][j]找新的星球 link[i][j] = x; return 1; } } } } return 0; } int main() { // freopen("data3.txt", "r", stdin); while (~scanf("%d%d", &n, &m)) { bool ans = 1; for (int i = 0; i < n; ++i) for (int j = 0; j < m; ++j) scanf("%d", &G[i][j]); for (int i = 0; i < m; ++i) { scanf("%d", &cap[i]); link[i].clear(); } for (int i = 0; i < n; ++i) { memset(vis, 0, sizeof(vis)); if (!path(i)) { ans = 0; break; } } if (ans) puts("YES"); else puts("NO"); } return 0; }
另外这道题可以使用更为高效的方法,但是必须借助网络流这个理论
因为可去的星球数只有10个,那么就可以通过二进制的思想来表示每个人可去星球的1024种状态,对每种状态求多重匹配
源点与各种状态连线,权值为这种状态的人数,汇点与各个星球相连,权值为星球最大容量,状态与状态可走的星球连线权值为INF
如果结果满流,说明所有人都能到想去的星球
#include<iostream> #include<algorithm> #include<deque> #define MAXN 2005 #define MAXM 20000005 #define INF 0x3fffffff using namespace std; struct node { int x, y, c, next; } line[MAXM]; int Lnum, head[MAXN]; bool Visited[MAXN]; int Layer[MAXN]; int s, t; void init() { memset(head, 0, sizeof(head)); Lnum = 0; } void addline(int x, int y, int c) { line[++Lnum].next = head[x], head[x] = Lnum; line[Lnum].x = x, line[Lnum].y = y, line[Lnum].c = c; line[++Lnum].next = head[y], head[y] = Lnum; line[Lnum].x = y, line[Lnum].y = x, line[Lnum].c = 0; } bool CountLayer() { deque<int> q; memset(Layer, -1, sizeof(Layer)); //都初始化成-1 Layer[s] = 0; q.push_back(s); while (!q.empty()) { int u = q.front(); q.pop_front(); for (int j = head[u]; j; j = line[j].next) { int v = line[j].y; if (line[j].c > 0 && Layer[v] == -1) { //Layer[j] == -1 说明j还没有访问过 Layer[v] = Layer[u] + 1; if (v == t) //分层到汇点即可 return 1; else q.push_back(v); } } } return 0; } int Dinic() { int i, j; int nMaxFlow = 0; deque<int> q; //DFS用的栈 while (CountLayer()) { //只要能分层 q.push_back(s); //源点入栈 memset(Visited, 0, sizeof(Visited)); Visited[s] = 1; while (!q.empty()) { int nd = q.back(); if (nd == t) { // 到达汇点,找到一条dfs路径 int nMinC = INF; int nMinC_vs; //容量最小边的起点 for (i = 1; i < q.size(); i++) { //在栈中找容量最小边 int vs = q[i - 1]; int ve = q[i]; int tmp = 0; for (j = head[vs]; j; j = line[j].next) if (line[j].y == ve) { tmp = line[j].c; break; } if (tmp > 0) { if (nMinC > tmp) { nMinC = tmp; nMinC_vs = vs; } } } nMaxFlow += nMinC; for (i = 1; i < q.size(); i++) { int vs = q[i - 1]; int ve = q[i]; for (j = head[vs]; j; j = line[j].next) if (line[j].y == ve) { line[j].c -= nMinC; break; } //修改边容量 for (j = head[ve]; j; j = line[j].next) if (line[j].y == vs) { line[j].c += nMinC; break; } //添加反向边 } //退栈到 nMinC_vs成为栈顶,继续Dfs while (!q.empty() && q.back() != nMinC_vs) { Visited[q.back()] = 0; q.pop_back(); } } else { //nd不是汇点 for (i = head[nd]; i; i = line[i].next) { int v = line[i].y; if (line[i].c > 0 && Layer[v] == Layer[nd] + 1 && !Visited[v]) { //只往下一层的没有走过的节点走 Visited[v] = 1; q.push_back(v); break; } } if (i == 0) //找不到下一个点出栈 q.pop_back(); } } } return nMaxFlow; } int main() { // freopen("data3.txt", "r", stdin); int n, m, i, j, x; int sum[1200]; while (~scanf("%d%d", &n, &m)) { memset(sum, 0, sizeof(sum)); for (i = 1; i <= n; i++) { int tmp = 0; for (j = 0; j < m; j++) { scanf("%d", &x); if (x) tmp += 1 << j; } sum[tmp]++; } s = 1500, t = s + 1; init(); int p = (1 << m) + 1; for (i = 0; i < (1 << m); i++) { addline(s, i, sum[i]); for (x = 0; x < m; x++) if (i & (1 << x)) addline(i, x + p, INF); } for (i = 0; i < m; i++) scanf("%d", &x), addline(i + p, t, x); if (Dinic() == n) puts("YES"); else puts("NO"); } return 0; }
三,二分图的最优匹配
KM算法、网络流构图
HDU 3488 求n个环的并,每个点只能在一个环里,找到可以遍历所有顶点的边的最小值 显然每个点只能关联其他两个顶点,并且入度和初度都为1,故将一个点拆成入度点和出度点 所以是完全匹配,完全匹配图就是n个环的并,所以求的是完美匹配中的最小权问题 只需将所有的边权值取其相反数,求最大权完备匹配,匹配的值再取相反数即可。 KM算法的运行要求是必须存在一个完备匹配,如果求一个最大权匹配(不一定完备)把不存在的边权值赋为0。 KM算法求得的最大权匹配是边权值和最大,如果我想要边权之积最大,每条边权取自然对数, 然后求最大和权匹配,求得的结果a再算出e^a就是最大积匹配。
#include <iostream> using namespace std; #define INF 10000000 const int N = 205; int G[N][N]; //邻接矩阵 int lx[N], ly[N]; //顶点标号 bool vis_x[N], vis_y[N]; //是否已经搜索过 int link[N]; int n; bool path(int k) { //从x[k]寻找增广路 int i; vis_x[k] = 1; for (i = 0; i < n; i++) { if (!vis_y[i] && (lx[k] + ly[i] == G[k][i])) { //相等子图 vis_y[i] = 1; if (link[i] == -1 || path(link[i])) { link[i] = k; return 1; } } } return 0; } int KM() { //求解最小权匹配 int i, j, k, d, sum; memset(ly, 0, sizeof(ly)); //初始化y的顶点标号 for (i = 0; i < n; i++) { lx[i] = -1; //x中顶点i的编号为与i关联的Y中边的最大权重 for (j = 0; j < n; j++) lx[i] = max(lx[i], G[i][j]); //初始化x的顶点标号 } memset(link, -1, sizeof(link)); for (k = 0; k < n; k++) { while (1) { memset(vis_x, 0, sizeof(vis_x)); memset(vis_y, 0, sizeof(vis_y)); if (path(k)) //匹配成功 break; d = INF; for (i = 0; i < n; i++) { if (vis_x[i]) { //x在交错树中 for (j = 0; j < n; j++) { if (!vis_y[j]) //y不在交错树中,扩大子图 d = min(d, lx[i] + ly[j] - G[i][j]); } } } //若匹配不成功,则修改顶点标号,找到d的值 for (i = 0; i < n; i++) { if (vis_x[i]) lx[i] -= d; //修改x顶点标号 if (vis_y[i]) ly[i] += d; //修改y顶点标号 } } } for (sum = 0, i = 0; i < n; i++) sum += G[link[i]][i]; return sum; } int main() { // freopen("data3.txt", "r", stdin); int T; cin >> T; while (T--) { int m, ans, i, j; int u, v, w; scanf("%d%d", &n, &m); for (i = 0; i < n; i++) for (j = 0; j < n; j++) G[i][j] = -INF; //求最小权匹配,将边的权值取反 while (m--) { scanf("%d%d%d", &u, &v, &w); u--, v--; G[u][v] = max(G[u][v], -w); } ans = KM() * -1; printf("%d\n", ans); } return 0; }
四,匹配的唯一性
POJ1486
先找到一个最大匹配后,对选上的匹配边依次删除,看是否还完美匹配。
#include <cstdio> #include <utility> #include <cstring> using namespace std; pair<int, int> point[30]; struct Node { pair<int, int> l, r; } paper[30]; int G[30][30], link[30], used[30], n; bool path(int u) { for (int i = 0; i < n; i++) if (G[u][i] && !used[i]) { used[i] = 1; if (link[i] == -1 || path(link[i])) { link[i] = u; return true; } } return false; } int hungary() { int i, ans = 0; memset(link, -1, sizeof(link)); for (i = 0; i < n; i++) { memset(used, 0, sizeof(used)); if (path(i)) ans++; } return ans; } bool check(int i, int j) { return point[i].first >= paper[j].l.first && point[i].first <= paper[j].r.first && point[i].second >= paper[j].l.second && point[i].second <= paper[j].r.second; } bool judge() { bool f = 0; for (int j = 0; j < n; j++) for (int i = 0; i < n; i++) if (G[i][j]) { G[i][j] = 0; if (n > hungary()) { //去掉就会不完备匹配,说明次边必须存在 if (f) printf(" "); printf("(%c,%d)", 'A' + j, i + 1); f = 1; } G[i][j] = 1; } if (f) return 1; return 0; } int main() { // freopen("data3.txt", "r", stdin); int x1, y1, x2, y2; int Case = 1; while (scanf("%d", &n) && n) { printf("Heap %d\n", Case++); for (int i = 0; i < n; i++) { scanf("%d%d%d%d", &x1, &x2, &y1, &y2); paper[i].l.first = x1, paper[i].l.second = y1, paper[i].r.first = x2, paper[i].r.second = y2; } for (int i = 0; i < n; i++) { scanf("%d%d", &x1, &y1); point[i].first = x1, point[i].second = y1; for (int j = 0; j < n; j++) if (check(i, j)) G[i][j] = 1; else G[i][j] = 0; } if (judge()) puts(""); else puts("none"); puts(""); } return 0; }
五,稳定婚姻问题
稳定婚姻问题是一个很有意思的匹配问题,有n位男士和n位女士,每一个人都对每个异性有一个喜好度的排序,代表对他的喜爱程度,现在希望给每个男士找一个女士作配偶,使得每人恰好有一个异性配偶。如果男士u和女士v不是配偶但喜欢对方的程度都大于喜欢各自当前配偶的程度,则称他们为一个不稳定对。稳定婚姻问题就是希望找出一个不包含不稳定对的方案。
算法非常简单,称为求婚-拒绝算法,每位男士按照自己喜欢程度从高到低依次给每位女士主动求婚直到有一个女士接受他,对于每个女士,如果当前向她求婚的配偶比她现有的配偶好则抛弃当前配偶,否则不予理睬,循环往复直到所有人都有配偶。有趣的是,看起来是女士更有选择权,但是实际上最后的结果是男士最优的(man-optimal)。
首先说明最后匹配的稳定性,随着算法的执行,每位女士的配偶越来越好,而每位男士的配偶越来越差。因此假设男士u和女士v形成不稳定对,u一定曾经向v求过婚,但被拒绝。这说明v当时的配偶比u更好,因此算法结束后的配偶一定仍比u好,和不稳定对的定义矛盾,类似的,方式我们考虑最后一个被抛弃的男士和抛弃这位男士的女士,不难得出这个算法一定终止的结论。
如果存在一个稳定匹配使得男士i和女士j配对,则称(i,j)是稳定对。对于每个男士i,设所有稳定对(i,j)中i 最喜欢的女士为best(i),则可以证明这里给出的算法对让每位男士i与best(i)配对。对于所有男士来说,不会有比这更好的结果了,而对于女士则恰恰相反,对于她们来说不会有比这更糟的结果了,因此这个算法是男士最优的。
算法一定得到稳定匹配,并且复杂度显然是O(n^2),因为每个男士最多考虑每个女士一次,考虑的时间复杂度是O(1),当然了,需要作一定的预处理得到这个复杂度。
#include <cstdio> #include <cstring> #include <iostream> #include <map> using namespace std; int n, gp_boy[505][505], gp_girl[505][505], boy[505], girl[505], rank[505]; map<string, int> mp_boy, mp_girl; string sboy[505], sgirl[505]; char s[1000]; void solve() { memset(boy, 0, sizeof(boy)); memset(girl, 0, sizeof(girl)); for (int i = 1; i <= n; i++) rank[i] = 1; while (1) { int flag = 0; for (int i = 1; i <= n; i++) { if (!boy[i]) { int g = gp_boy[i][rank[i]++]; if (!girl[g]) boy[i] = g, girl[g] = i; else if (gp_girl[g][i] > gp_girl[g][girl[g]]) boy[girl[g]] = 0, girl[g] = i, boy[i] = g; flag = 1; } } if (!flag) break; } for (int i = 1; i <= n; i++) { cout << sboy[i] << ' ' << sgirl[boy[i]] << endl; } puts(""); } int main() { while (~scanf("%d", &n)) { getchar(); mp_boy.clear(), mp_girl.clear(); int pos = 1, tem; for (int i = 1; i <= n; i++) { scanf("%s", s); sboy[i] = s, mp_boy[s] = i; for (int j = 1; j <= n; j++) { scanf("%s", s); tem = mp_girl[s]; if (tem == 0) mp_girl[s] = tem = pos++, sgirl[tem] = s; gp_boy[i][j] = tem; } } for (int i = 0; i < n; i++) { scanf("%s", s); int x = mp_girl[s]; for (int j = 0; j < n; j++) { scanf("%s", s); int y = mp_boy[s]; gp_girl[x][y] = n - j; } } solve(); } return 0; }