图论——二分图
二分图又称作二部图,是图论中的一种特殊模型。
G=(V, E)是一个无向图 如果G的顶点集V可分割为两个互不相交的子集X和Y,并且E中每 条边连接的两个顶点一个在X中,另一个在Y中,则称图G为二分 图,记为G=(X,Y,E)。
由定义可知,二分图的这两个部分中的任意两个顶点之间没有路径
无向图 G 为二分图的充分必要条件是,G 至少有两个顶点,且其所有回路的长度均为偶数。
Question:给定一个无向联通图,如何判定该图是否为一个二分图?
染色法:参考博文:http://www.cnblogs.com/1227xq/p/6826783.html
https://www.cnblogs.com/digitalhermit/p/5119908.html
(1) 二分图的判定 [邻接表]
对二分图的结点进行染色。如果处于同一组,则应该染成同色,否则为不同色。在染色过程中,如果发生矛盾,说明此图不是二分图。
这种方法就像涂颜色一样,相邻(若有边相连)则染成不同色,反之染成同色,若矛盾,则false,反之true
下面为自作代码+借鉴代码
邻接链表
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<bits/stdc++.h> #define N 100000 using namespace std; struct node{ int to,next; }e[N]; int head[N],tot,n,m; bool vis[N],colar[N]; void add(int u,int v){ e[++tot].to=v;e[tot].next=head[u];head[u]=tot; } bool dfs(int k){ vis[k]=1; for(int i=head[k],v;i,v=e[i].to;i=e[i].next){ if(vis[v]==0){ colar[v]=!colar[k]; }else { if(colar[v]!=colar[k]) return true; } } }//判断函数退出,默认返回false int main() { cin>>n>>m; for(int i=1;i<=n;i++){ int u,v; cin>>u>>v;add(u,v);add(v,u); } for(int i=1;i<=n;i++){ if(vis[i]==0){ if(dfs(i)==1){ printf("NO\n"); break; } } }printf("YES\n"); return 0; }
vector(STL神器)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<bits/stdc++.h> #define N 100000 using namespace std; vector<int>G[N]; int colar[N]; bool dfs(int u,int c){ colar[u]=c; for(int i=0;i<G[u].size();i++){ if(colar[G[u][i]]==c) return false;//如果相邻的顶点同色,就剪掉这一枝,返回false if(colar[G[u][i]]==0&&!dfs(G[u][i],-c)) return false;//如果相邻的顶点还没有染色就把它染成-c }return true; } void solve(){ for(int i=1;i<=n;i++){ if(vis[i]==0){ if(dfs(i,0)==0){ printf("NO\n"); return; } } }printf("YES\n"); } int main() { cin>>n>>m; for(int i=1;i<=n;i++){ int u,v; cin>>u>>v; G[u].push_back(v); G[v].push_back(u); }solve(); return 0; }
这段代码更容易理解:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<iostream> #include<cstdio> #include<cstdlib> using namespace std; #define N 1000 #define M 2000 int col[N],cnt,n,m,head[N],to[M],next[M]; void add(int x,int y){ next[++cnt]=head[x]; to[cnt]=y; head[x]=cnt; } void dfs(int u,int c){ col[u]=c; for(int v,i=head[u];i;i=next[i]){ v=to[i]; if(!col[v]) dfs(v,-c); // 染 -c if(col[v]==col[u]){ // 有冲突 printf("NO"); exit(0); } } } int main(){ scanf("%d%d",&n,&m); for(int x,y,i=1;i<=m;i++){ scanf("%d%d",&x,&y); add(x,y); add(y,x); } for(int i=1;i<=n;i++) if(!col[i]) dfs(i,1); // 默认 染色 1 printf("YES"); return 0; }
并查集(神奇的做法)
我们可以将每个节点抽象出两个节点 v0,v1.
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<iostream> #include<cstdio> #include<cstdlib> using namespace std; #define N 1000 int fa[N<<1],n,m; int find(int k){return fa[k]==k?k:fa[k]=find(fa[k]);} int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n*2;i++)fa[i]=i; for(int fx,fy,x,y,i=1;i<=m;i++){ scanf("%d%d",&x,&y); fx=find(x*2-1); fy=find(y*2); fa[fx]=fy;//一个染白则另一个染黑 fx=find(x*2); fy=find(y*2-1); fa[fx]=fy;//一个染黑则另一个染白 } for(int i=1;i<=n;i++) if(find(i*2)==find(i*2-1)){//既染白又染黑 printf("NO"); return 0; } printf("YES"); return 0; }
粘一波概念:
匹配:给定一个二分图G=(X,Y,E),若存在E的一个子集M,满足 M中的任意两条边都没有公共顶点,则M称为一个G的匹配
匹配边:在匹配中的边
未匹配边:不在匹配中的边
未匹配点:对于一个匹配,不与任何匹配边邻接的点
匹配点:刚好相反
极大匹配:无法再加边的匹配
最大匹配:在所有极大匹配中,边数|M|最大的匹配
完全匹配:如果一个匹配中,图中每个顶点都与一条边相关联 ,则称此匹配为完全匹配
下面给出关于二分图最大匹配的三个定理:
① 最大匹配数+最大独立集数=n
② 二分图的最小覆盖数=最大匹配数
③ 最小路径覆盖=最大独立集 最大独立集是指求一个二分图中最大的一个点集,该点集内的点互不相连。 最小顶点覆盖是指在二分图中,用最少的点,让所有的边至少和一个点有关联。 最小路径覆盖是指一个不含圈的有向图 G 中,G 的一个路径覆盖是一个其结点不相交的路径集合 P,图中 的每一个结点仅包含于 P 中的某一条路径。路径可以从任意结点开始和结束,且长度也为任意值,包括 0。
增广路
增广路径:若 P 是图 G 中一条连通两个未匹配顶点的路径,并且属于 M 的边和不属于 M 的边(即已匹配 和待匹配的边)在 P 上交替出现,则称 P 为相对于 M 的一条增广路径。
由增广路的定义可以推出下述三个结论:
① P 的路径长度必定为奇数,第一条边和最后一条边都不属于 M。
② P 经过取反操作可以得到一个更大的匹配 M'。
③ M 为 G 的最大匹配当且仅当不存在相对于 M 的增广路径。
方法(匈牙利算法):
求二分图的最大匹配常用匈牙利算法,它是通过不断地寻找增广轨来实现的。很明显,如果二分图的两部 分点分别为 X 和 Y,那么最大匹配的数目应该小于等于 min{X,Y}。因此我们可以枚举某一部分里的每一个点, 从每个点出发寻找增广轨,最后,该部分的点找完以后,就找到了最大匹配的数目。当然我们也可以通过记录来找出这些边。
匈牙利算法找一条增广路的复杂度为 O(m),最多找 n 条增广路,故时间复杂度为 O(mn)。