【C++】二分图

百科名片

在图论中,二分图是一类特殊的图,又称为双分图、二部图、偶图。二分图的顶点可以分成两个互斥的独立集 U 和 V 的图,使得所有边都是连结一个U 中的点和一个V 中的点。顶点集U、V 被称为是图的两个部分。等价的,二分图可以被定义成图中所有的环都有偶数个顶点。

设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。在这里插入图片描述
在这里插入图片描述

举例

众所周知,二分图最典型的例子就是一个班级里的男女生谈恋爱。

左边代表男生,右边代表女生,连线代表有好感,匹配就是把他们互相有好感的撮合起来。当然,是一夫一妻制度,不能一个人脚踏很多船。

所谓最大匹配,就是撮合最多对。

完美匹配就是所有的人都不落单。

定义

给定一个二分图G,在G的一个子图M中,M的边集{E}中的任意两条边都不依附于同一个顶点,则称M是一个匹配。

选择这样的边数最大的子集称为图的最大匹配问题(Maximal Matching Problem)
如果一个匹配中,图中的每个顶点都和图中某条边相关联,则称此匹配为完全匹配,也称作完备匹配。

最大匹配在实际中有广泛的用处,求最大匹配的一种显而易见的算法是:先找出全部匹配,然后保留匹配数最多的。但是这个算法的复杂度为边数的指数级函数。因此,需要寻求一种更加高效的算法。

二分图的一个性质:最大匹配数+最大独立集=X+Y(结点总数)。

如果边上带权的话,找出权和最大的匹配叫做求最佳匹配。

二分图的判定——黑白染色

我们只要对一个图进行黑白染色,即一条边的两个点一定是黑色和白色,如果没有矛盾,就是二分图,否则就不是二分图,方法类似灌水。

例1 二分图判定

HihoCoder-1121
链接:https://hihocoder.com/problemset/problem/1121
https://vjudge.net/problem/HihoCoder-1121

在这里插入图片描述

思路

上面已讲解(灌水、黑白染色)

代码

用链式前向星

#pragma GCC optimize(3,"Ofast","inline")
#pragma G++ optimize(3,"Ofast","inline")

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))

using namespace std;

typedef long long ll;

int const M=80005;
int const N=10005;

struct Edge {
    int to,nt;
} e[M];

int n,m,cnt,check;
int h[N],vis[N];

inline void add(int a,int b) {
    e[cnt].to=b;
    e[cnt].nt=h[a];
    h[a]=cnt++;
}

void dfs(int k,int c) {
    vis[k]=c;
    for(int i=h[k]; i!=-1; i=e[i].nt) {
        int v=e[i].to;
        if(vis[v]==c) check=0;
            else if(vis[v]==-1) dfs(v,1^c);
    }
}

int main() {
    int cas;
    scanf("%d",&cas);
    while(cas--) {
        cnt=0;
        memset(h,-1,sizeof(h));
        scanf("%d%d",&n,&m);
        for(int i=0; i<m; i++) {
            int a,b;
            scanf("%d%d",&a,&b);
            add(a,b);
            add(b,a);
        }
        memset(vis,-1,sizeof(vis));
        check=1;
        for(int i=1; i<=n; i++) if(vis[i]==-1) dfs(i,0);
        puts(check ? "Correct" : "Wrong");
    }
    return 0;
}

用动态数组

#pragma GCC optimize(3,"Ofast","inline")
#pragma G++ optimize(3,"Ofast","inline")

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#include <vector>

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))

using namespace std;

typedef long long ll;

int const N=10005;

vector<int> map[N];

int n,m,check;
int vis[N];

int dfs(int k,int c) {
    if(vis[k]) return (vis[k]==c+1);
    vis[k]=c+1;
    for(int i=0; i<map[k].size(); i++)
        if(!dfs(map[k][i],c^1)) return 0;
    return 1;
}

int main() {
    int cas;
    scanf("%d",&cas);
    while(cas--) {
        scanf("%d%d",&n,&m);
        check=1;
        for(int i=1; i<=n; i++) map[i].clear();
        memset(vis,0,sizeof(vis));
        for(int i=1; i<=m; i++) {
            int x,y;
            scanf("%d%d",&x,&y);
            map[x].push_back(y);
            map[y].push_back(x);
        }
        for(int i=1; i<=n; i++) if((!vis[i]) && (!dfs(i,0))) {
            check=0;
            break;
        }
        puts(check ? "Correct" : "Wrong");
    }
    return 0;
}

二分图的最大匹配

使用匈牙利算法,即增广路算法。
下面给出简单的代码和注释

#include <iostream>
#include <cstring>
using namespace std;
#define M 100
#define N 100
int m,n;
//X类点集有n个结点,Y类点集有m个结点,map[i][j]=true表示结点i到结点j有一条边(结点计数由1开始)
bool map[N][M];
//表示结点i在dfs过程中已匹配
bool chk[M];

int match[M];
//match[m」存储了匹配的方案(matchLiJ表示与 i匹配的左边的点的编号),最初值为0,表示无匹配

//深度优先搜索,从元素p出发寻找增广路
int dfs(int p) {
	int i;
	for(i=1; i<=m; i++) {
	//如果结点i还未匹配并且p到i是有边的,令p匹配i,然后递归继续搜索
		if (map[p][i] && !chk[i]) {
			chk[i] = 1;
			if(!match[i] | | dfs (match[i])) { 
				match[i] = p; 
				return 1; 
			}
	}
	return 0;
}

int main() {
	int i,res=0;
	int x,y;
	cin>>n>>m;
	memset(match, 0sizeof(match));
	while(cin>>x>>y && x && y) map[x][y] = true;
	
	//循环n次,每次确定一个元素被匹配
	for(i = 1; i <= n; i++) {//此写法可以前一类点集顺序匹配的输出!
		//初始化chk数组
		memset(chk, falsesizeof(chk);
		
		//深度优先搜索寻找增广路,若果找到,匹配数加1
		if(dfs(i)) res++;
	}
	// res是最大匹配数.
	printf("res = %d\n",res);|
	return 0;

总结:匈牙利算法的递归写法是用交错轨每次寻找一个在Y类节点中没有匹配的点,类似灌水的方法,时间复杂度(VE)。

优点是简单易懂,容易实现。

例2 二分图最大匹配之匈牙利算法

HihoCoder-1122
链接:https://hihocoder.com/problemset/problem/1122
https://vjudge.net/problem/HihoCoder-1122

在这里插入图片描述

思路

使用匈牙利算法,上面已讲解

代码

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))

using namespace std;

typedef long long ll;

int const N=10005;
int const M=50005;

struct Edge {
    int to,nt;
} e[M<<1];

int cnt,n,m,t;
int h[N],vis[N],p[N];

inline void add(int a,int b) {
    e[cnt].to=b;
    e[cnt].nt=h[a];
    h[a]=cnt++;
}

int find(int k) {
    for(int i=h[k]; i!=-1; i=e[i].nt) {
        int v=e[i].to;
        if(vis[v]) continue;
        vis[v]=1;
        if(!p[v] || find(p[v])) {
            p[v]=k;
            return 1;
        }
    }
    return 0;
}

int main() {
    scanf("%d%d",&n,&m);
    ms(-1,h);
    for(int i=0; i<m; i++) {
        int a,b;
        scanf("%d%d",&a,&b);
        add(a,b);
        add(b,a);
    }
    int ans=0;
    for(int i=1; i<=n; i++) {
        ms(0,vis);
        if(find(i)) ans++;
    }
    printf("%d\n",ans>>1);
    return 0;
}

例3 The Perfect Stall

POJ-1274
链接:http://poj.org/problem?id=1274
https://vjudge.net/problem/POJ-1274
在这里插入图片描述

题目大意

本题的英文题面应该可以看得懂,只要初中英语水平就可以看懂。这里不在给出。有疑问可以使用翻译工具和词典。

思路

这是一题较裸的题,不给思路。
提醒一下,本题有多组数据。

代码

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))

using namespace std;

typedef long long ll;

int const N=205;

struct Edge {
    int to,nt;
} e[N*N];

int cnt,n,m;
int h[N],vis[N],match[N];

inline void add(int a,int b) {
    e[cnt].to=b;
    e[cnt].nt=h[a];
    h[a]=cnt++;
}

int dfs(int k) {
    for(int i=h[k]; i!=-1; i=e[i].nt) {
        int v=e[i].to;
        if(vis[v]) continue;
        vis[v]=1;
        if(!match[v] || dfs(match[v])) {
            match[v]=k;
            return 1;
        }
    }
    return 0;
}

int main() {
    while(scanf("%d%d",&n,&m)!=EOF) {
        memset(h,-1,sizeof(h));
        memset(match,0,sizeof(match));
        cnt=0;
        for(int i=1; i<=n; i++) {
            int k;
            scanf("%d",&k);
            for(int j=1; j<=k; j++) {
                int t;
                scanf("%d",&t);
                add(i,t);
            }
        }
        int ans=0;
        for(int i=1; i<=n; i++) {
            memset(vis,0,sizeof(vis));
            if(dfs(i)) ans++;
        }
        printf("%d\n",ans);
    }
    return 0;
}

二分图的最大独立集

独立集和最大团是两个相同的概念,之前我们就讲过求解一般图的最大团的方法,是一个优化了的搜索+dp的算法。但是对于二分图这个特殊的图而言,其最大独立集=总的点数-最大匹配数,为我们提供了一个很不错的特殊算法。

例4 Girls and Boys

HDU-1068
链接:http://acm.hdu.edu.cn/showproblem.php?pid=1068
https://vjudge.net/problem/HDU-1068

在这里插入图片描述

代码

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))

using namespace std;

typedef long long ll;

int const N=1005;

int d[N*N],n[N*N],p[N],u[N];

int find(int pos) {
    int now=pos;
    while(n[pos]) {
        pos=n[pos];
        int data=d[pos];
        if(!u[data]) {
            u[data]=1;
            if(p[data]==-1 || find(p[data])) {
                p[data]=now;
                p[now]=data;
                return 1;
            }
        }
    }
    return 0;
}

int main() {
    int v,pos;
    while(scanf("%d",&v)!=EOF) {
        int all,num;
        pos=v;
        for(int i=0; i<v; i++) {
            scanf("%d: (%d)",&num,&all);
            int now=i;
            while(all--) {
                scanf("%d",&d[pos]);
                n[now]=pos;
                now=pos++;
            }
            n[now]=0;
            p[i]=-1;
        }
        int sum=0;
        for(int i=0; i<v; i++) {
            if(p[i]==-1) {
                for(int t=0; t<v; t++) u[t]=0;
                sum+=find(i);
            }
        }
        printf("%d\n",v-sum>sum ? v-sum:sum);
    }
    return 0;
}

例5 Knights in Chessboard

LightOJ-1010
链接:http://lightoj.com/volume_showproblem.php?problem=1010
https://vjudge.net/problem/LightOJ-1010

在这里插入图片描述

思路

这题的正解是找规律,但用二分图也可以做。

二分图的最小点覆盖

即用最少的点去关联所有的边。
二分图的最小点覆盖=最大匹配数。

例6 Muddy Fields

POJ-2226
链接:http://poj.org/problem?id=2226
https://vjudge.net/problem/POJ-2226

在这里插入图片描述

代码

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))

using namespace std;

typedef long long ll;

int const N=55;
int const M=1005;

int n,m,sx,sy,cnt;
int match[M],vis[M],h[M];
int bx[N][N],by[N][N];

char mat[N][N];

struct Edge {
    int to,nt;
} e[M<<1];

void add(int a,int b) {
    e[cnt].to=b;
    e[cnt].nt=h[a];
    h[a]=cnt++;
}

int dfs(int k) {
    for(int i=h[k]; i!=-1; i=e[i].nt) {
        int v=e[i].to;
        if(!vis[v]) {
            vis[v]=1;
            if(!match[v] || dfs(match[v])) {
                match[v]=k;
                return 1;
            }
        }
    }
    return 0;
}

int main() {
    scanf("%d%d",&n,&m);
    for(int i=0; i<n; i++) scanf("%s",mat[i]);
    for(int i=0; i<n; i++) {
        int last=-1;
        for(int j=0; j<m; j++) {
            if(mat[i][j]=='.') {
                if(last==j-1) {
                    last=j;
                    continue;
                }
                sx++;
                for(int k=last+1; k<j; k++) bx[i][k]=sx;
                last=j;
            }
        }
        if(mat[i][m-1]=='*') {
            sx++;
            for(int k=last+1; k<m; k++) bx[i][k]=sx;
        }
    }
    for(int j=0; j<m; j++) {
        int last=-1;
        for(int i=0; i<n; i++) {
            if(mat[i][j]=='.') {
                if(last==i-1) {
                    last=i;
                    continue;
                }
                sy++;
                for(int k=last+1; k<i; k++) by[k][j]=sy;
                last=i;
            }
        }
        if(mat[n-1][j]=='*') {
            sy++;
            for(int k=last+1; k<n; k++) by[k][j]=sy;
        }
    }
    ms(-1,h);
    for(int i=0; i<n; i++) for(int j=0; j<m; j++) 
        if(mat[i][j]=='*') add(bx[i][j],by[i][j]);
    int ans=0;
    for(int i=1; i<=sx; i++) {
        ms(0,vis);
        if(dfs(i)) ans++;
    }
    printf("%d\n",ans);
    return 0;
}

例7 Machine Schedule

HDU-1150
链接:http://acm.hdu.edu.cn/showproblem.php?pid=1150
https://vjudge.net/problem/HDU-1150
在这里插入图片描述

代码

#pragma GCC optimize(3,"Ofast","inline")
#pragma G++ optimize(3,"Ofast","inline")

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))

using namespace std;

typedef long long ll;

int const N=105;
int const M=2005;

int n,m,t,cnt;
int vis[N],h[N],match[N];

struct Edge {
    int to,nt;
} e[M];

void add(int a,int b) {
    e[cnt].to=b;
    e[cnt].nt=h[a];
    h[a]=cnt++;
}

int dfs(int k) {
    for(int i=h[k]; i!=-1; i=e[i].nt) {
        int v=e[i].to;
        if(!vis[v]) {
            vis[v]=1;
            if(match[v]==-1 || dfs(match[v])) {
                match[v]=k;
                return 1;
            }
        }
    }
    return 0;
}

int main() {
    while(scanf("%d",&n) && n) {
        scanf("%d%d",&m,&t);
        ms(-1,h);
        cnt=0;
        ms(-1,match);
        for(int i=0; i<t; i++) {
            int a,b,c;
            scanf("%d%d%d",&c,&a,&b);
            if(a && b) add(a,b);
        }
        int ans=0;
        for(int i=0; i<n; i++) {
            ms(0,vis);
            if(dfs(i)) ans++;
        }
        printf("%d\n",ans);
    }
    return 0;
}

二分图的最小边覆盖

即用最少数量的边覆盖所有点
最少边覆盖=总的点数-最大匹配数

例8 Antenna Placement

POJ-3020
链接:http://poj.org/problem?id=3020
https://vjudge.net/problem/POJ-3020

在这里插入图片描述

题目大意

一个矩形中,有N个城市’*’,现在这n个城市都要覆盖无线,若放置一个基站,那么它至多可以覆盖相邻的两个城市。
问至少放置多少个基站才能使得所有的城市都覆盖无线?

代码

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#include <queue>
 
#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))
 
using namespace std;
 
typedef long long ll;
 
int const N=405;
int const dx[4]={0,0,1,-1};
int const dy[4]={1,-1,0,0};

struct Edge {
    int to,nt;
} e[N*10];

int n,m,cnt;
int match[N],h[N],vis[N];
char mat[45][45];

inline void add(int a,int b) {
    e[cnt].to=b;
    e[cnt].nt=h[a];
    h[a]=cnt++;
}

int dfs(int k) {
    for(int i=h[k]; i!=-1; i=e[i].nt) {
        int v=e[i].to;
        if(!vis[v]) {
            vis[v]=1;
            if(match[v]==-1 || dfs(match[v])) {
                match[v]=k;
                return 1;
            }
        }
    }
    return 0;
}

int main() {
    int cas;
    scanf("%d",&cas);
    while(cas--) {
        scanf("%d%d",&n,&m);
        for(int i=0; i<n; i++) scanf("%s",mat[i]);
        cnt=0;
        memset(h,-1,sizeof(h));
        memset(match,-1,sizeof(match));
        for(int i=0; i<n; i++) for(int j=0; j<=m; j++) 
            if(mat[i][j]=='*') for(int k=0; k<4; k++) {
                int tx=i+dx[k];
                int ty=j+dy[k];
                if(tx<0 || ty<0 || tx>=n || ty>=m) continue;
                if(mat[tx][ty]=='o') continue;
                add(i*m+j,tx*m+ty);
            }
        int ans=0;
        for(int i=0; i<n*m; i++) {
            memset(vis,0,sizeof(vis));
            if(dfs(i)) ans++;
        }
        int sum=0;
        for(int i=0; i<n; i++) for(int j=0; j<m; j++)
            if(mat[i][j]=='*') sum++;
        printf("%d\n",sum-(ans>>1));
    }
    return 0;
}

二分图的最小路径覆盖

在一个DAG(有向无环图)中,路径覆盖就是在图中找一些路径,使之覆盖了图中的所有顶点,且任何一个顶点有且只有一条路径与之关联;(如果把这些路径中的每条路径从它的起始点走到它的终点,那么恰好可以经过图中的每个顶点一次且仅一次);如果不考虑图中存在回路,那么每条路径就是一个弱连通子集.

最小路径覆盖数=点数-最大匹配数

例9 Taxi Cab Scheme

POJ-2060 LA=3126
链接:
POJ: http://poj.org/problem?id=2060
LA: https://icpcarchive.ecs.baylor.edu/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=1127
vjudge: https://vjudge.net/problem/POJ-2060
在这里插入图片描述

题目大意

有 n 个乘客需要乘出租车出行,给出他们出发时间,出发地点,目的地(input 保证数据按照出发时间升序给出),问最少需要安排多少辆车才能满足他们的需求。

代码

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))

using namespace std;

typedef long long ll;

int const N=505;
int const M=300005;

int n,m,t,cnt;
int vis[N],h[N],ho[N],x[N<<1],y[N<<1],match[N],mt[N];

struct Edge {
    int to,nt;
} e[M];

void add(int a,int b) {
    e[cnt].to=b;
    e[cnt].nt=h[a];
    h[a]=cnt++;
}

inline int calc(int x,int y) {
    return ho[x]*60+mt[x]-(ho[y]*60+mt[y]);
}

inline int dist(int a,int b) {
    return abs(x[a]-x[b])+abs(y[a]-y[b]);
}

int dfs(int k) {
    for(int i=h[k]; i!=-1; i=e[i].nt) {
        int v=e[i].to;
        if(vis[v]) continue;
        vis[v]=1;
        if(!match[v] || dfs(match[v])) {
            match[v]=k;
            return 1;
        }
    }
    return 0;
}

int main() {
    int cas;
    scanf("%d",&cas);
    while(cas--) {
        scanf("%d",&n);
        cnt=0;
        ms(-1,h);
        ms(0,match);
        for(int i=1; i<=n; i++) scanf("%d:%d %d %d %d %d",&ho[i],&mt[i],&x[2*i-1],&y[2*i-1],&x[2*i],&y[2*i]);
        for(int i=1; i<n; i++) for(int j=i+1; j<=n; j++) {
            int delta=dist(2*i-1,2*i)+dist(2*i,2*j-1);
            int need=calc(j,i);
            if(delta<need) add(i,j);
        }
        int ans=0;
        for(int i=1; i<=n; i++) {
            ms(0,vis);
            if(dfs(i)) ans++;
        }
        printf("%d\n",n-ans);
    }
    return 0;
}

其他练习

发布了73 篇原创文章 · 获赞 94 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/Ljnoit/article/details/104667427
今日推荐