您的位置:首页 > 其它

二分图相关概念 二分图最大匹配 二分图最大权匹配 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):

#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;
}


  
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息