我用自己的语言解释一下何为二分图:图中所有点分成两部分,就像站队,左边一队,右边一队。同一队两点之间没有关系,不同队的可能存在多种关系。
二分图的最大匹配:比如相亲,左边为男生队,右边为女生队,男女存在多种关系(比如男1喜欢女2和女3),男男之间当然没有关系了。最大匹配就是这么个情况,尽量成全更多的男女成为一对,最多的对数,就是最大匹配数。
匈牙利算法:关键点是找增广路,是这么个情况:男1喜欢女1,但是女1已经和男4好上了,男1很执着,女1思想也不太坚定,于是女1问男4,他可不可以换个女生。如果男4成功换了女伴,那么理所当然男1成功和女1成为情侣!这就是找到了一条增广路
下面拿板子题来说明代码: HDU 2063
题意是中文就不多做解释了
#include<iostream> #include<cstdio> #include<cstring> const int maxn=505; int e[maxn][maxn]; int book[maxn]; int match[maxn]; int k,n,m; using namespace std; int dfs(int x) { for(int i=1;i<=m;i++) { if(!book[i]&&e[x][i]) //关于book数组,我这么解释:1:第2层if没有发生,即男x无法和i配对,说明女i已经有人了,而且女i的男友无法再找到新女生,此时有必要把book[i]=1,因为这个女人碰不得啊,以后大家就不要再找她了。
//2:第二层if 发生了,女i被标记,因为女i的男友想再找其他女生,就不能再找女i了。 { book[i]=1; if(!match[i]||dfs(match[i]))//女i没有男友或者男友可以换个女生,那么x可以和i配对
{ match[i]=x; return 1; } } } return 0; } int main() { while(scanf("%d",&k)) { if(k==0) break; int ans=0; scanf("%d%d",&n,&m); memset(match,0,sizeof(match)); memset(e,0,sizeof(e)); int a,b; for(int i=1;i<=k;i++) { scanf("%d%d",&a,&b); e[a][b]=1; } for(int i=1;i<=n;i++) { for(int j=1;j<=m;j++) //每次清空book { book[j]=0; } if(dfs(i)) // ans++; } cout<<ans<<endl; } return 0; }
二分图最大独立集:实质是个点集,这个集合里的点无论怎样都两两相连不到一起,满足这个要求的点数最多的一个。也就是集合里任意两点之间无关系。用最大匹配求出最大独立集,
最大独立集 = 顶点数 - 最大匹配数
题意:n个同学,一些男女同学会有缘分成为情侣,格式ni:(m) n1 n2 n3表示同学ni有缘与n1,n2,n3成为情侣,求集合中不存在有缘成为情侣的同学的最大同学数
模板,但是要注意,这并不是两个集合,所以最大匹配相当于求了两遍,最大匹配 / 2;
还有,输入格式注意一下,第一次这么搞,没想到scanf还有这种操作。
#include<iostream> #include<cstring> using namespace std; const int maxn=1001; int e[maxn][maxn]; int match[maxn]; int book[maxn]; int n; int sum=0; void init() { memset(e,0,sizeof(e)); int st,m; sum=0; memset(match,-1,sizeof(match)); for(int i=0;i<n;i++) { scanf("%d: (%d)",&st,&m); int to; while(m--) { scanf("%d",&to); e[st][to]=1; } } } int dfs(int x) { for(int i=0;i<n;i++) { if(book[i]==0&&e[x][i]==1) { book[i]=1; if(match[i]==-1||dfs(match[i])) { match[i]=x; return 1; } } } return 0; } int main() { while(~scanf("%d",&n)) { init(); for(int i=0;i<n;i++) { for(int j=0;j<n;j++) book[j]=0; if(dfs(i)) sum++; } printf("%d ",n-sum/2); } return 0; }
下面放个很有意思的题 HDU1281
中文题意,为了防止车子互相攻击,那么车在某一行+某一列只有一个。相当于行与列的匹配,由于存在不能放车的点,用e[][]来存坐标来进行判断能否放车,其实就和之前的一样,e[i][j]为第i行和
第j列没有关系,仅此而已。所谓的重要点,就是去掉它以后,求出的最大匹配数比原来的少,求这些点。那么我们枚举每一个e[][]==1大的点,去掉它,跑一下匈牙利,求出的最大匹配和原来的比较。计数。
1 #include<iostream> 2 #include<cstring> 3 #include<cstdio> 4 using namespace std; 5 const int maxn=105; 6 int e[maxn][maxn]; 7 int match[maxn]; 8 int book[maxn]; 9 int n,m,k; 10 int all=0; 11 void init() 12 { 13 memset(e,0,sizeof(e)); 14 all=0; 15 } 16 int dfs(int x) 17 { 18 for(int i=1;i<=m;i++) 19 { 20 if(!book[i]&&e[x][i]) 21 { 22 book[i]=1; 23 if(!match[i]||dfs(match[i])) 24 { 25 match[i]=x; 26 return 1; 27 } 28 } 29 } 30 return 0; 31 } 32 int xyl() 33 { 34 int ans=0; 35 memset(match,0,sizeof(match)); //注意,每次跑匈牙利都要初始化match! 36 for(int i=1;i<=n;i++) 37 { 38 memset(book,0,sizeof(book)); 39 if(dfs(i)) 40 ans++; 41 } 42 return ans; 43 } 44 int main() 45 { 46 int ww=1; 47 while(~scanf("%d%d%d",&n,&m,&k)) 48 { 49 init(); 50 int x,y; 51 int cnt=0; 52 for(int i=1;i<=k;i++) 53 { 54 scanf("%d%d",&x,&y); 55 e[x][y]=1; 56 } 57 all=xyl(); 58 for(int i=1;i<=n;i++) 59 { 60 for(int j=1;j<=m;j++) 61 { 62 if(e[i][j]) 63 { 64 e[i][j]=0; 65 int ans=xyl(); 66 // cout<<ans<<endl; 67 if(ans<all) 68 cnt++; 69 e[i][j]=1; 70 } 71 } 72 } 73 printf("Board %d have %d important blanks for %d chessmen. ",ww++,cnt,all); 74 } 75 }
二分图的染色法判断: 1. 二分图当且仅当不含奇数环。(手推一下,会出现矛盾,所以不能含有奇数环),不含奇数环一定是二分图。
具体参见ACWING 图论第三讲。对每一个点进行染色,比如某点染1色,那么其相邻点染2色,如果在染的过程中,出现了矛盾,比如i 点本来改染成1色,但是它的当前配色为2,那肯定不是个二分图。ACWING 基本板子题:这里用邻接表来存图
#include<iostream> #include<cstdio> #include<cstring> using namespace std; const int maxn=2e5+10; int n,m; int h[maxn],e[maxn],ne[maxn]; //对e,ne要开大点,我也不知道为撒 int color[maxn]; int ok=0; int idx; void add(int a,int b) { e[idx]=b; ne[idx]=h[a]; h[a]=idx++; } bool dfs(int u,int c) { color[u]=c; for(int i=h[u];i!=-1;i=ne[i]) { int j=e[i]; if(!color[j]) { if(!dfs(j,3-c)) { return false; } } else if(color[j]==c) { return false; } } return true; } int main() { scanf("%d%d",&n,&m); memset(h,-1,sizeof(h)); for(int i=1 ;i<=m ;i++) { int a,b ; scanf("%d%d",&a ,&b ); add(a,b); add(b,a); } for(int i=1 ; i <= n; i++) { if(!color[i]) { if(!dfs(i,1)) { ok=1; //出现矛盾 break; } } } if(ok) cout<<"No"<<endl; else cout<<"Yes"<<endl; }
原题自行打开看吧,我解释一下题意:就是n个人,分成两组,m个不认识关系。把认识的人分一组,分两组。比如输入1 2,就表明1与2不认识,不认识就不能分到同一组。
很明显,判断二分图,不是二分图就输出Poor wyh。是的话先输出大的一组,然后是较小的一组。套基本二分图染色模板,但是要注意,题没这么简单,因为题目中有多个连通块,就是有多个二分图,其次还存在单点。所以呢,运用贪心的思想,分别把单个的二分图求出来,然后用ans加上较小的那一组的人数,这样单点就自动记入较大的一组,n减ans就可以了。因为不同的二分图:比如A组和B组,A组的人肯定和B组不存在不认识关系,所以他们可以加入同一组。很多题解加入了vis等好多不易理解且繁琐的东西,我依据二分图染色模板,改造出了易于小白理解的代码,与原模板没什么两样。
#include<iostream> #include<cstring> #include<cstdio> using namespace std; const int maxn=2e5+10; int h[maxn],e[maxn],ne[maxn]; //邻接表存图 int num[3]; int color[maxn]; bool vis[maxn]; int idx; int ok=0; void init() { idx=0;ok=0; memset(h,-1,sizeof(h)); memset(e,0,sizeof(e)); memset(ne,0,sizeof(ne)); // memset(vis,false,sizeof(vis)); memset(color,0,sizeof(color)); } void add(int a,int b){ e[idx]=b; ne[idx]=h[a]; h[a]=idx++; } bool dfs(int u,int c) { // vis[u]=true; color[u]=c; num[c]++; //计数 for(int i=h[u];i!=-1;i=ne[i]) { int j=e[i]; if(!color[j]) { if(!dfs(j,3-c)) return false; } else if(color[j]==c) return false; } return true; } int main() { int t; scanf("%d",&t); while(t--) { init(); int n,m; scanf("%d%d",&n,&m); int a,b; int mid=m; while(mid--) { scanf("%d%d",&a,&b); add(a,b);add(b,a); } // cout<<m<<endl; if(n<=1) //坑点,记得特判! { printf("Poor wyh "); continue; } if(m==0) { printf("%d 1 ",n-1); continue; } int ans=0; for(int i=1;i<=n;i++) { if(!color[i]) { num[1]=0;num[2]=0; //新的二分图,那么计数器就要清一下 if(!dfs(i,1)) { ok=1;break; } ans+=min(num[1],num[2]); } } if(ok) printf("Poor wyh "); else { printf("%d %d ",n-ans,ans); } } }