二分图相关概念 二分图最大匹配 二分图最大权匹配 poj3041 poj2195
2016-12-10 20:09
471 查看
昨天在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):
还有一类经典的问题就是二分图的最大权匹配,求权值和 最大的完美匹配。解决这类问题,需要改进上面的算法。为每个顶点添加一个节点函数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):
上面的求最大权匹配的算法时间复杂度为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改进)
先安利一波概念和性质:
二分图:假设图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; }
相关文章推荐
- 二分图相关概念及匈牙利算法求解最大匹配(附代码实现)
- POJ3041 二分图最大匹配(网络流算法)
- 二分图最大匹配:匈牙利算法(poj3041)
- poj2195(二分图最大匹配,最小费用流)
- POJ3041 二分图(性质)最小点覆盖等于最大匹配数(匈牙利模板题)
- 二分图最大匹配相关问题
- POJ3041--Asteroids--二分图最大匹配--Konig
- 编程之美之质数相关--二分图最大匹配+素数筛选
- poj3041 匈牙利算法 二分图最大匹配
- 二分图的最大匹配 ————匈牙利算法 (转载了一个大神的趣味算法) poj3041(Asteroids)
- poj3041 Asteroids 匈牙利算法 最小点集覆盖问题=二分图最大匹配
- poj3041-二分图最大匹配
- (二分图最大匹配) poj3041 Asteroids
- 二分图的基本概念+二分图的最大匹配问题(匈牙利算法)
- poj3041 二分图最大匹配(匈牙利算法)
- POJ3041 Asteroids(二分图最大匹配)
- poj3041-Asteroids , 二分图的最小顶点覆盖数 = 最大匹配数
- poj3041 最小点覆盖==二分图最大匹配 匈牙利算法求解最大匹配问题(运用DFS)
- poj3041_匈牙利算法_二分图最小覆盖点(最大匹配数)
- POJ2195 Going Home[费用流|二分图最大权匹配]