-
前言
未经允许,请勿转载。
留个赞吧各位。
如果有逻辑/语言不清楚或者错误的地方欢迎评论教我语文。
这段时间一直在口胡二分图的东西,感觉这个知识点好多。
挖个坑
把自己埋了,看看能不能填完,或者说能填多少。至于为什么很多都没有证明,因为我太菜了我证明看不懂。发现了一个很好的证明博客地址 。
-
二分图是啥
二分图又称作二部图,是图论中的一种特殊模型。 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。 -------百度百科。
说的好复杂啊,画个图理解一下。如图,在一个图中,如果能把点分为两个顶点集,使每个集合中,没有两个点有连边。
也就是说,边只会连接两个点集。
-
二分图判定
因为是分为两个点集,所以每个点要么属于第一个要么属于第二个。
那我们可以暴搜每个点属于什么。显然,对于一条边所连的两个点,一定不可能属于同一个点集。
那问题可以转化一下:看成给出一个 (n) 个点的图,要给每个点染成黑白两色,且相邻的点颜色要不同。
那我们在搜索的时候,若点 (u) 属于黑色,那么与它相连的点 (v) 必定为白色。
这就很贪心,如果发现 (v) 已经被染过颜色,而且染的颜色是黑色,那么这个图就一定不是二分图了。
所以判定过程如下:
bool dfs(int x,int c) { color[x]=c; for(int k=f[x];k!=0;k=p[k].nx) { if(color[p[k].v]==c){return 0;} if(color[p[k].v]==0 && !dfs(p[k].v,-c)){return 0;} } return 1; }
需要注意的是,给出的图不一定是连通图(除非题目有说),所以主程序的时候我们要加一个循环。
for(int i=1;i<=n;i++) if(!color[i]) if(dfs(i,1)==0) {cout<<-1;return 0;}//不是二分图
完整代码如下:
(我以前的码风有点丑哎(?))#include<iostream> #include<cstdio> using namespace std; struct node{ int v; int nx; }p[400001]; int f[4001],n,m,en=0; int color[4001]; bool dfs(int x,int c) { color[x]=c; for(int k=f[x];k!=0;k=p[k].nx) { if(color[p[k].v]==c){return 0;} if(color[p[k].v]==0 && !dfs(p[k].v,-c)){return 0;} } return 1; } void read(int u,int v) { p[++en].v=v; p[en].nx=f[u]; f[u]=en; } int main() { scanf("%d %d",&n,&m); for(int i=1;i<=m;i++) { int u,v; scanf("%d %d",&u,&v); read(u,v); read(v,u); } for(int i=1;i<=n;i++) if(!color[i]) if(dfs(i,1)==0){cout<<-1;return 0;} printf("1 "); for(int i=1;i<=n;i++) if(color[i]==1)printf("1 "); else printf("2 "); return 0; }
-
二分图匹配
二分图匹配是什么?匹配:在图论中,一个「匹配」是一个边的集合,其中任意两条边都没有公共顶点。
记得刚刚上面那个图嘛
(不记得就往上翻),比如这么选就是一种匹配:而这样就不是一个匹配:
所以匹配一抓一大把。所以要求的就成了最大匹配。
例如你谷模板 P3386 【模板】二分图最大匹配。
给定一个二分图,其左部点的个数为 (n) ,右部点的个数为 (m) ,边数为 (e) ,求其最大匹配的边数。
左部点从 (1) 至 (n) 编号,右部点从 (1) 至 (m) 编号。
数据范围:$1 le n,m le 500 , 1 le e le 5×10^4 , 1 le u le n , 1 le v le m $ 。
-
匈牙利算法
求二分图最大匹配的一般是用匈牙利算法
,因为好写。首先,要先知道一个叫增广路的东西。
若P是图G中一条连通两个未匹配顶点的路径,并且属于M的边和不属于M的边(即已匹配和待匹配的边)在P上交替出现,则称P为相对于M的一条增广路径(举例来说,有A、B集合,增广路由A中一个点通向B中一个点,再由B中这个点通向A中一个点……交替进行)。 ----百度百科
好复杂啊...?
那我们先不管他。匈牙利算法就是一个不断找增广路的过程,那来手动模拟一下。
(好多人模拟过程都是男女配对啊??)比如这个图:
第一次匹配,我们先将第一个吃瓜人匹配到第一个吐血人:
接下来,发现第二个吃瓜人是单着,那不管他了。
第三个吃瓜人,匹配到第一个吐血人,然后发现第一个吃瓜人已经和他匹配了。
那么就去协商一下,第一个吃瓜人发现还能和第二个吐血人匹配,于是第一个吐血人就给了第三个吃瓜人。如图:于是轮到了第四个吃瓜人,他想和第一个吐血人匹配,然后发现已经有人匹配了。
于是他和第三个吃瓜人协商,第三个吃瓜人就选择了第二个吐血人,然后发现已经有人匹配了。于是第三个吃瓜人去和第一个吃瓜人协商,第一个吃瓜人选择了第一个吐血人。结果他发现问题又绕回去了,于是第三个吃瓜人和第一个吃瓜人的协商失败,那么第三个吃瓜人和第四个吃瓜人的协商失败。
也就说,第四个人只能和第二个人一起在旁边吃瓜了。
所以最大匹配数为 (2) 。
这大致就是二分图匹配的过程。
而协商过程,其实就是在找增广路。
也就是每个吃瓜人,都是先选择一个未匹配的边,如果对面的点(吐血人)已经被匹配了,那就顺着那个匹配的边找到那个人,那个人再选一个未匹配的边....
复杂度为 (O(nm)) 。
匈牙利代码如下:
(map_{i,j}) 表示连边情况,(vis[]) 用来判断已经走过的点 , (matched[]) 表示改点的匹配点。
bool found(int x) { for(int i=1;i<=m;i++) { if(!map[x][i]||vis[i])continue; vis[i]=1; if(!matched[i]||found(matched[i])) { matched[i]=x; return 1; } } return 0; } int match() { int cnt=0; for(int i=1;i<=n;i++) { memset(vis,0,sizeof(vis)); if(found(i))cnt++;//找增广路 } return cnt; }
其他部分其实没啥亮点。
完整代码如下:
#include<iostream> #include<cstdio> #include<cstring> using namespace std; int n,m,e,matched[2001]; bool vis[2001],map[2001][2001]; bool found(int x) { for(int i=1;i<=m;i++) { if(!map[x][i]||vis[i])continue; vis[i]=1; if(!matched[i]||found(matched[i])) { matched[i]=x; return 1; } } return 0; } int match() { int cnt=0; for(int i=1;i<=n;i++) { memset(vis,0,sizeof(vis)); if(found(i))cnt++;//找增广路 } return cnt; } int main() { scanf("%d%d%d",&n,&m,&e); for(int i=1;i<=e;i++) { int u,v; scanf("%d%d",&u,&v); map[u][v]=1; } printf("%d ",match()); return 0; }
-
( ext{k})(ddot{o})( ext{nig}) 定理
这个名字真难打。点覆盖,在图论中点覆盖的概念定义如下:对于图G=(V,E)中的一个点覆盖是一个集合S⊆V使得每一条边至少有一个端点在S中。 -----百度百科
举个例子:
这个图的,选择 (2,4,5)就是一种点覆盖。
这种情况的点覆盖数为 (3) 。
很明显,每个图的最大点覆盖就是点数。
也就是说,不存在没有点覆盖的图。
所以一般都是求最小点覆盖。
那现在来探讨一下二分图的最小点覆盖
(不能遗忘标题)。然后这就有一个定理:二分图的最小点覆盖=二分图的最大匹配
-
二分图的最小边覆盖。
有最小点覆盖,就有一个东西叫最小边覆盖。
边覆盖是一类覆盖,指一类边子集。具体地说,图的一个边子集,使该图上每一节点都与这个边子集中的一条边关联。 --百度百科
如图,边覆盖为 (3) 。
那么是否每个图都有边覆盖呢?
显然,如果一个图含有一个孤立点,那么这个图不可能有边覆盖。
那么又有一个定理:二分图中最小边覆盖=顶点数-最小顶点覆盖。
也就是说:二分图中最小边覆盖=顶点数-二分图最大匹配
-
最小路径覆盖
洛谷这题正好有P2764 最小路径覆盖问题。
那么我们就用二分图过了这个网络流24题
。给定有向图 (G=(V,E)) 。设 (P) 是 (G) 的一个简单路(顶点不相交)的集合。如果 (V) 中每个定点恰好在P的一条路上,则称 (P) 是 (G) 的一个路径覆盖。 (P) 中路径可以从 V 的任何一个定点开始,长度也是任意的,特别地,可以为 (0) 。(G) 的最小路径覆盖是 (G) 所含路径条数最少的路径覆盖。设计一个有效算法求一个 GAP (有向无环图) (G) 的最小路径覆盖。
如下图,最小路径数为 (3) ,分别为:
1 4 7 10 11 2 5 8 3 6 9
结论其实又双是个定理。考虑建一个二分图。
假设图两边的点数都为 (n) ,左边编号表示为 (i) ,右边编号表示为 (i') 。
每次读入一条边 ((u,v)) 时,在二分图连边 ((u,v')) 。
建完图跑二分图最大匹配,结果为:最小路径覆盖=顶点数-最大匹配数。
稍稍证明一下:
(开始摘抄别人证明)题目中已经说明,一个单独的点也为路径,如果 (u_1,u_2,u_3,...u_n) 为一条路径,那 (u_1) 到 (u_n) 之间不会有其他的有向边。
如果此时最大匹配数为 (0) ,则二分图中无边,显然最小路径覆盖=顶点数-最大匹配数=顶点数。
若此时增加一条匹配边 ((x,y')) ,则在有向图中,(x,y')在同一条路径上,最小路径覆盖减一。
若继续增加匹配边,那么最小路径继续覆盖减一,所以公式得证。
那么路径要怎么求呢。
直接沿着增广路跑循环就可以了。
#include<iostream> #include<cstdio> #include<cstring> using namespace std; const int Maxn=300+5,Maxm=6000+5; int n,m,ans,matched[Maxn]; bool vis[Maxn],mp[Maxn][Maxn]; bool found(int x) { for(int i=n+1;i<=2*n;i++) { if(!mp[x][i]||vis[i])continue; vis[i]=1; if(!matched[i]||found(matched[i])) { matched[i]=x; matched[x]=i; return 1; } } return 0; } int match() { int cnt=0; for(int i=1;i<=n;i++) { memset(vis,0,sizeof(vis)); if(found(i))cnt++; } return cnt; } void prt(int x) { x+=n; do printf("%d ",x=x-n); while(vis[x]=1,x=matched[x]); printf(" "); } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { int u,v; scanf("%d%d",&u,&v); mp[u][v+n]=1; } ans=n-match(); memset(vis,0,sizeof(vis)); for(int i=1;i<=n;i++) if(!vis[i])prt(i); printf("%d ",ans); return 0; }
-
独立集
独立集是指图 G 中两两互不相邻的顶点构成的集合。 ---百度百科
举个例子,如下图,独立集个数为 (3) 。
最小独立集没啥好求的,所以要解决的问题就是二分图的最大独立集。
这
又双叒有一个公式:二分图的最大独立集 = 总点数-最大匹配数
-
KM算法
upt2020/08/05:有模板了 P6577 【模板】二分图最大权完美匹配
洛谷似乎没有模板,那就用这题P3967 [TJOI2014]匹配。
单看第一问就是个模板(确信)。给定一个二分图,两边的点数都为 (n) ,给出若干条边,每条边有一个权值,求最大的完美匹配的值。
KM针对的是带权的完美匹配,就是说二分图两边的数量必须相等,即最大匹配数为 (n) 。
其实KM的复杂度和边没太多关系,所以如果两点没有连边的话,可以假设这两点的边的权值为 (0) 。
首先,每个点有一个顶标,就是有一个值。
假设点 (u) 的顶标为 (l(u)) 。
对于任意一条边 ((u,v)) ,必须满足 (l(u)+l(v) ge w(u,v)) ,其中(w)表示边权。
当一条边满足 (l(u)+l(v)=w(u,v)) 时,就可以把这条边加入二分图中。
如果该图可以跑出完美匹配,那么此时的完美匹配的边权值和的即为结果。
似乎很简单的样子,那么怎么确定顶标的值呢。因为不能直接确定,那算法流程大概是这样子:确认顶标的值->跑匹配->如果匹配数为 (n) ,结束;否则->修改顶标->跑匹配.....
那初始的时候顶标该如何确定呢?
假设二分图左边的顶标为 (ex(i)) ,右边的顶标为 (ey(i)) 。
因为要满足 (ex(x)+ey(y) ge w(x,y)),那我们不妨直接设 (ex(i)) 全部为 (0),(ey(i)) 为所连边的最大值。
调整顶标过程中,其实目的就是要不断的加入新的边,也就是使更多的边满足 (ex(i)+ey(j)=w(i,j)) 。
那么接下来找一条边 ((i,j)) ,使其满足 (i) 不在二分图最大匹配中,而 (j) 在二分图最大匹配中。
我们希望这边加入二分图匹配,就要使这条边满足条件,即顶标和要减少 (d=ex(i)+ey(j)-w(i,j)) 。
因为此时点 (j) 在二分图最大匹配中,如果改变顶标肯定会影响其他边,所以干脆草率一点,对于在二分图最大匹配中的任意点 (i) ,将 (ex(i)+d) 或 (ey(i)-d) 。
为了防止修改完导致部分顶标不满足 (ex(x)+ey(y) ge w(x,y)) ,我们每次找的 (d) 要尽量小。
ok,解决了,算一下复杂度。
每次找边复杂度为 (O(n^2)) ,二分图最大匹配的复杂度为 (O(n^2)) ,也就是说总复杂度为 (O(n^4)) 。
有一说一,这是真的慢。考虑优化,至少优化到 (O(n^3)) 啊!!
每次找一遍 (d) 太慢了,所以我们开个数组:
[slack[j]= min (ex(i)+ey(j)-w(i,j)) ]每次查询的时候就降了一维,(slack) 修改只要在跑增广路的时候修改就好了。
代码:
#include<iostream> #include<cstdio> #include<cmath> #include<cstring> using namespace std; const int Maxn=303; const int inf=1e9; int n,map[Maxn][Maxn],matched[Maxn]; int slack[Maxn],ex[Maxn],ey[Maxn];//ex,ey顶标 bool visx[Maxn],visy[Maxn]; bool match(int x) { visy[x]=1; for(int i=1;i<=n;i++) { if(visx[i])continue; int gap=ex[i]+ey[x]-map[x][i]; if(gap==0) { visx[i]=1; if(!matched[i]||match(matched[i])) { matched[i]=x; return 1; } } else slack[i]=min(slack[i],gap); } return 0; } int KM() { memset(matched,0,sizeof(matched)); memset(ex,0,sizeof(ex)); for(int i=1;i<=n;i++) { ey[i]=map[i][1]; for(int j=2;j<=n;j++) ey[i]=max(ey[i],map[i][j]); } for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++)slack[j]=inf; while(1) { memset(visx,0,sizeof(visx)); memset(visy,0,sizeof(visy)); if(match(i))break; int d=inf; for(int j=1;j<=n;j++) if(!visx[j])d=min(d,slack[j]); for(int j=1;j<=n;j++) { if(visy[j])ey[j]-=d; if(visx[j])ex[j]+=d; else slack[j]-=d; } } } int res=0; for(int i=1;i<=n;i++) res+=map[matched[i]][i]; return res; } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) scanf("%d",&map[i][j]); printf("%d ",KM()); return 0; }
然后就会发现,哎不对啊这个 (O(n^3)) 是假的啊。
只要数据够duliu,匹配的部分跑到 (O(n^2)) ,那么复杂度依然没有降到 (O(n^3)) 。接下来就会发现,因为我们每次只修改一条边,也就是说dfs跑匹配的时候,有一部分和之前是一样的。
所以我们把dfs改成bfs,就可以实现真正的 (O(n^3)) 。
#include<iostream> #include<cstdio> #include<cmath> #include<cstring> using namespace std; const int Maxn=303; const int inf=1e9; int n,map[Maxn][Maxn],matched[Maxn]; int slack[Maxn],pre[Maxn],ex[Maxn],ey[Maxn];//ex,ey顶标 bool visx[Maxn],visy[Maxn]; void match(int u) { int x,y=0,yy=0,delta; memset(pre,0,sizeof(pre)); for(int i=1;i<=n;i++)slack[i]=inf; matched[y]=u; while(1) { x=matched[y];delta=inf;visy[y]=1; for(int i=1;i<=n;i++) { if(visy[i])continue; if(slack[i]>ex[x]+ey[i]-map[x][i]) { slack[i]=ex[x]+ey[i]-map[x][i]; pre[i]=y; } if(slack[i]<delta){delta=slack[i];yy=i;} } for(int i=0;i<=n;i++) { if(visy[i])ex[matched[i]]-=delta,ey[i]+=delta; else slack[i]-=delta; } y=yy; if(matched[y]==-1)break; } while(y){matched[y]=matched[pre[y]];y=pre[y];} } int KM() { memset(matched,-1,sizeof(matched)); memset(ex,0,sizeof(ex)); memset(ey,0,sizeof(ey)); for(int i=1;i<=n;i++) { memset(visy,0,sizeof(visy)); match(i); } int res=0; for(int i=1;i<=n;i++) if(matched[i]!=-1)res+=map[matched[i]][i]; return res; } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) scanf("%d",&map[i][j]); printf("%d ",KM()); return 0; }