二分图相关概念 二分图最大匹配 二分图最大权匹配 poj3041 poj2195

来源:互联网 发布:流程图描述KMP算法 编辑:程序博客网 时间:2024/06/05 07:39

昨天在codefoces上见了一个二分图相关的题目(http://codeforces.com/problemset/problem/741/C),今天周末没事。就复习《算法竞赛入门经典》总结了一下二分图的相关概念,以及经典的二分图最大匹配算法,二分图最大权匹配算法。

先安利一波概念和性质:

二分图:假设图G = (V, E)是一个无向图,若顶点集可以分解成两个互不相交的子集(A, B),并且图中的所有边(i, j)的端点分别属于子集A,B中的元素,则称图G是一个二分图。常记为G(A,E,B)

匹配: 没有公共顶点的边的集合

最大匹配:最多的匹配数(选尽量多的边,使得任意两条选中的边中没有 公共的端点)

最大边独立集:最多的没有公共点的边数 (最大边独立 = 最大匹配)

最大独立点(独立集):最大的任意两点之间不存在边的子集 (最大点独立 = 顶点总数 - 最大匹配)

最小点集覆盖:覆盖所有边的最小的点集  (最小点集覆盖 = 最大匹配)

最小边覆盖:覆盖所有点的最小的边集 (最小边覆盖 = 顶点总数- 最大匹配)

最小路径覆盖:在图中找一些路径,使之覆盖了图中的所有顶点,且任何一个顶点有且只有一条路径与之关联。其中,路径数目最少的就是最小路径覆盖。(最小路径覆盖 = 顶点总数- 最大匹配)

                最小路径覆盖针对有向图:《算法竞赛入门》是这样总结的:

DAG最小路径覆盖的解决办法:把所有的节点i拆为X节点i和Y接点i',如果图G中存在有向边i->j,则在二分图中引入边( i->j' )。设二分图的最大匹配数为m, 则结果就是n-m(n为顶点个数)。因为匹配和路径覆盖是一一对应的。对于路径覆盖中的每条简单路径,除了最后一个“结尾节点”之外都有唯一的后继和它对应(即匹配节点),因此,匹配数就是非结尾节点的个数。当匹配数最大时,非结尾节点的个数也将达到最大。此时结尾节点的个数最少,即路径最少。


由二分图的性质可以看出,求二分图的最大匹配是关键问题。许多问题都可以转化为求二分图的最大匹配。今天也学习了一下求二分图最大匹配的匈牙利算法:对于X集合中的每一个节点x,每次去寻找对应的Y集合中的匹配的顶点,如果正好找到就记录(下面代码用left[]数组存Y集合中对应X集合中的点),如果在寻找过程中发现x的匹配的点已经被前面的点占用,就回溯,尝试让前面的已经匹配的节点腾出空间。如果都不能,就放弃,即这个点就不在最大匹配中。

具体实现代码如下(poj3041):

#include <cstdio>#include <cstring>#include <algorithm>const int MAX = 501;int a[MAX][MAX], left[MAX], vis[MAX];bool match(int r, int n){for (int i = 1; i<=n; i++) if (a[r][i] && !vis[i]){vis[i] = true;if (!left[i] || match(left[i], n)){left[i] = r;return true;}}return false;}int main(int argc, char const *argv[]){int n, k;while (scanf("%d%d", &n, &k) != EOF){memset(a, 0, sizeof(a));memset(left, 0, sizeof(left));int x, y;for (int i = 0; i<k; i++){scanf("%d%d", &x, &y);a[x][y] = 1;}for (int i = 1; i<=n; i++){memset(vis, 0, sizeof(vis));    //vis[]标记,每次寻找时已经匹配过的Y集合中的节点,所以每次寻找时都需要重新标记match(i, n);}int res = 0;for (int i = 1; i<=n; i++) if (left[i]){//printf("%d %d\n", left[i], i);res++;}printf("%d\n", res);}return 0;}


还有一类经典的问题就是二分图的最大权匹配,求权值和 最大的完美匹配。解决这类问题,需要改进上面的算法。为每个顶点添加一个节点函数L来控制每次匹配的边都是符合条件的权值最大的那个,使得对于任意的边(x, y),都有Lx(x) + Ly(y) >= w(x, y)(注:w(x, y)为边(x, y)的权值)。Lx(X)的初始化值为以x为顶点的边中最大的权值。Ly(Y)的初始化为0。在对X集合中的每一个点进行找匹配边的时候,需要让其满足w(x, y) == Lx(x) + Ly(y), 如果没有匹配的,则更新本次查找得到的匈牙利树中节点t:如果节点t属于X集合,则Lx(t) -= a, 如果节点t属于集合Y,则Ly(t) +=a。其中a =  min{Lx(x) + Ly(y) - w(x, y) | x属于属于X集合且在匈牙利树中,y属于Y集合且y不再匈牙利树中}。

具体实现代码如下(poj2195):

#include <cstdio>#include <cstring>#include <cmath>#include <algorithm>using namespace std;const int MAX = 101;const int INF = 1000000000;char s[MAX*1000];struct Node{int x, y;}p[MAX], q[MAX];int a[MAX][MAX], left[MAX], l[MAX], r[MAX];bool S[MAX], T[MAX];bool match(int root, int n){S[root] = true;for (int i = 1; i<=n; i++) if (l[root] + r[i] == a[root][i] && !T[i]){T[i] = true;if (!left[i] || match(left[i], n)){left[i] = root;return true;}}return false;}void update(int n){int aw = INF;for (int i = 1; i<=n; i++) if (S[i]){for (int j = 1; j<=n; j++) if (!T[j]) {aw = min(aw, l[i] + r[j] - a[i][j]);}}for (int i = 1; i<=n; i++){ if (S[i]) l[i] -= aw; if (T[i]) r[i] += aw;}}int main(int argc, char const *argv[]){/* code */int n, m;while (scanf("%d%d", &n,&m) && n != 0){int cnth = 0, cntm = 0;for (int i = 0; i<n; i++){scanf("%s", s);for (int j = 0; j<m; j++){if (s[j] == 'H'){p[++cnth] = (Node){i, j};}else if (s[j] == 'm'){q[++cntm] = (Node){i, j};}}}memset(a, 0, sizeof(a));for (int i = 1; i<=cntm; i++){for (int j = 1; j<=cnth; j++){a[i][j] = -(abs(p[i].x - q[j].x) + abs(p[i].y-q[j].y));}}/*for (int i = 1; i<=cntm; i++){for (int j = 1; j<=cnth; j++){printf("%d ", a[i][j]);}printf("\n");}*/n = cnth;for (int i = 1; i<=n; i++){l[i] = -INF;r[i] = 0;for (int j = 1; j<=n; j++){l[i] = max(l[i], a[i][j]);}}memset(left, 0, sizeof(left));for (int i = 1; i<=n; i++){for (;;){memset(S, false, sizeof(S));memset(T, false, sizeof(T));if (match(i, n)){break;}else{update(n);}}}int res = 0;for (int i = 1; i<=n; i++){res += -(l[i]+r[i]);}printf("%d\n", res);} return 0;}

上面的求最大权匹配的算法时间复杂度为O(n^4),其中每次更新a值的复杂度为O(n^2)。可以给Y中每个节点y定义一个松弛量 slack[y] = min{Lx(x) + Ly(y) - w(x, y)}。每次寻找匹配边的时候初始化slack,然后匹配的时候当遇到Lx(x) + Ly(y) != w(x, y)时,去更新slack。然后,在找最小的a时,只需要找slack中的最小值,时间复杂度为O(n),然后总时间复杂度降为O(n^3)。

具体时实现如下(poj 2195改进)

#include <cstdio>#include <cstring>#include <cmath>#include <algorithm>using namespace std;const int MAX = 101;const int INF = 1000000000;char s[MAX*1000];struct Node{int x, y;}p[MAX], q[MAX];int a[MAX][MAX], left[MAX], l[MAX], r[MAX], slack[MAX];bool S[MAX], T[MAX];bool match(int root, int n){S[root] = true;for (int i = 1; i<=n; i++) {if (T[i]){continue;}if (l[root] + r[i] !=  a[root][i]){slack[i] = min(slack[i], l[root]+r[i] - a[root][i]);continue;}if (l[root] + r[i] == a[root][i] && !T[i]){T[i] = true;if (!left[i] || match(left[i], n)){left[i] = root;return true;}}}return false;}void update(int n){int aw = INF;/*for (int i = 1; i<=n; i++) if (S[i]){for (int j = 1; j<=n; j++) if (!T[j]) {aw = min(aw, l[i] + r[j] - a[i][j]);}}*/for (int i = 1; i<=n; i++){aw = min(aw, slack[i]);}for (int i = 1; i<=n; i++){ if (S[i]) l[i] -= aw; if (T[i]) r[i] += aw;}}int main(int argc, char const *argv[]){/* code */int n, m;while (scanf("%d%d", &n,&m) && n != 0){int cnth = 0, cntm = 0;for (int i = 0; i<n; i++){scanf("%s", s);for (int j = 0; j<m; j++){if (s[j] == 'H'){p[++cnth] = (Node){i, j};}else if (s[j] == 'm'){q[++cntm] = (Node){i, j};}}}memset(a, 0, sizeof(a));for (int i = 1; i<=cntm; i++){for (int j = 1; j<=cnth; j++){a[i][j] = -(abs(p[i].x - q[j].x) + abs(p[i].y-q[j].y));}}n = cnth;for (int i = 1; i<=n; i++){l[i] = -INF;r[i] = 0;for (int j = 1; j<=n; j++){l[i] = max(l[i], a[i][j]);}}memset(left, 0, sizeof(left));for (int i = 1; i<=n; i++){for (;;){memset(S, false, sizeof(S));memset(T, false, sizeof(T));for (int j = 1; j<=n; j++) slack[j] = INF;if (match(i, n)){break;}else{update(n);}}}int res = 0;for (int i = 1; i<=n; i++){res += -(l[i]+r[i]);}printf("%d\n", res);} return 0;}




  






0 0
原创粉丝点击