二分图 题目小结
脑子都要废了最近,终于有空整理题目了,学了两天二分图,然后机房大佬讲,你二分图写的我网络流都能写,心神俱疲;
我不想在这里过多介绍二分图,必将网上大佬博客都很多;
二分图
如果一张无向图的 n(n≥2) 个节点可以分成 A,BA,B 两个非空集合,
其中 A⋂B 为空,并且在同一集合内的点之间都没有边相连,那么称这张无向图为一张二分图。 A,B 分别称为二分图的左部和右部。
二分图判定定理
无向图是二分图⇔图中无奇环(长度为奇数的环)。
常用方法dfs染色法,判断一张图是否为二分图;
有1表示黑色,2表示白色;
inline bool dfs(int x,int color) {
v[x]=color;
for(int i=0;i<edge[x].size();i++) {
int y=edge[x][i].first;
if(v[y]==color) return 0;
if(!v[y]&&!dfs(y,3-color)) return 0;
}
return 1;
}
这里想总结一下算法进阶的例题,写一下自己的理解;
最开始使用并差集写的,以前写过一篇并查集的博客,这里我们考虑用二分图的做法写这道题目;
最大的怒气值最小,考虑一个判定问题,市长能否在一种方案分配下,看到怒气值最大为mid;显然当对于较小的mid成立时,较大的mid也成立,所以答案具有可二分性;
所以我们转化为一个判定问题;
二分答案,我们假设当前怒气值为mid,那么我们将大于mid的怒气值的罪犯(当作节点)连边,如果建出来一张二分图,答案显然合理,令r=mid即可;
二分图code;
#include<bits/stdc++.h> using namespace std; #define N 500100 #define pii pair<int,int> int n,m,maxn,v[N]; template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48); ch=getchar();} x*=f; } struct gg { int x,y,v; }a[N<<1]; vector<pii> edge[N]; inline bool mycmp(gg x,gg y) { return x.v>y.v; } inline bool dfs(int x,int color) { v[x]=color; for(int i=0;i<edge[x].size();i++) { int y=edge[x][i].first; if(v[y]==color) return 0; if(!v[y]&&!dfs(y,3-color)) return 0; } return 1; } inline bool check(int mid) { for(int i=1;i<=n;i++) edge[i].clear(); for(int i=1;i<=m;i++) { if(a[i].v<=mid) break; edge[a[i].x].push_back(make_pair(a[i].y,a[i].v)); edge[a[i].y].push_back(make_pair(a[i].x,a[i].v)); } memset(v,0,sizeof(v)); for(int i=1;i<=n;i++) { if(!v[i]&&!dfs(i,1)) return false; } return true; } int main() { read(n); read(m); for(int i=1;i<=m;i++) { read(a[i].x); read(a[i].y); read(a[i].v); maxn+=a[i].v; } sort(a+1,a+m+1,mycmp); int l=0,r=maxn; while(l<r) { int mid=(l+r)>>1; if(check(mid)) r=mid; else l=mid+1; } cout<<l<<endl; return 0; }
并差集code1;
#include<bits/stdc++.h> using namespace std; #define N 500001 template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} while(isdigit(ch)) {x=x*10+ch-'0'; ch=getchar();} x*=f; } int n,m,d[N],f[N]; struct pink { int x,y,v; }a[N<<1]; bool mycmp(pink x,pink y) { return x.v>y.v; } int find(int x) { return f[x]==x?x:f[x]=find(f[x]); } int main() { int flag=0; read(n);read(m); for(int i=1;i<=m;i++) { read(a[i].x);read(a[i].y);read(a[i].v); } for(int i=1;i<=2*n;i++) f[i]=i; sort(a+1,a+m+1,mycmp); for(int i=1;i<=m;i++) { int x,y; x=a[i].x,y=a[i].y; int xx=find(a[i].x); int yy=find(a[i].y); if(xx==yy) { cout<<a[i].v<<endl; return 0; } f[yy]=find(a[i].x+n); f[xx]=find(a[i].y+n); } puts("0"); return 0; }
并差集code2;
#include<bits/stdc++.h> using namespace std; #define N 500001 template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} while(isdigit(ch)) {x=x*10+ch-'0'; ch=getchar();} x*=f; } int n,m,d[N],f[N]; struct pink { int x,y,v; }a[N<<1]; bool mycmp(pink x,pink y) { return x.v>y.v; } int find(int x) { return f[x]==x?x:f[x]=find(f[x]); } int main() { int flag=0; read(n);read(m); for(int i=1;i<=m;i++) { read(a[i].x);read(a[i].y);read(a[i].v); } for(int i=1;i<=n;i++) f[i]=i; sort(a+1,a+m+1,mycmp); for(int i=1;i<=m;i++) { int x,y; x=a[i].x,y=a[i].y; if(find(x)==find(y)) { printf("%d\n",a[i].v); flag=1; break; } else { if(!d[x]) d[x]=y; else { int p=find(d[x]); f[p]=find(y); } if(!d[y]) d[y]=x; else { int q=find(d[y]); f[q]=find(x); } } } if(!flag) printf("%d\n",0); return 0; }
二分图最大匹配
图的匹配
任意两条边都没有公共端点的边的集合被称为图的一组匹配。
二分图最大匹配
在二分图中,包含边数最多的一组匹配被称为二分图的最大匹配。
其他相关定义
对于任意一组匹配 S(边集),属于 S 的边被称为匹配边,不属于 S 的边被称为非匹配边。
匹配边的端点被称为匹配点,其他节点被称为非匹配点。
如果二分图中存在一条连接两个非匹配点的路径 path ,使得非匹配边与匹配边在 path 上交替出现,那么称 path是匹配 S 的增广路(也称交错路)。
增广路的性质
- 长度为奇数
- 奇数边是非匹配边,偶数边是匹配边。
- 如果把路径上所有边的状态(是否为匹配边)取反,那么得到的新的边集 S' 仍然是一组匹配,并且匹配的边数增加了 1 。
结论
二分图的一组匹配 S 是最大匹配⇔图中不存在 S 的增广路。
匈牙利算法(增广路算法)
主要过程
- 设 SS 为空集,即所有边都是非匹配边。
- 寻找增广路 path ,把 path 上所有边的匹配状态取反,得到一个更大的匹配 S' 。
- 重复第 22 步,直至图中不存在增广路。
寻找增广路
依次尝试给每一个左部节点 x 寻找一个匹配的右部节点 y 。
y 与 x 匹配需满足下面两个条件之一:
- y 是非匹配点。
- y 已与 x' 匹配,但从 x' 出发能找到另一个 y' 与之匹配。
时间复杂度
O(nm);
1.棋盘覆盖;
对于1*2的骨牌放置在N*M的矩阵中的方案数,我们可以用状压DP来写,
但对于这道题,有些各自有限制,并且数据范围限制,无法用状压写;
所以我们为什么考虑到二分图匹配的呢;
对于二分图匹配,他一定具有一个两个性质,我们成为0元素,1元素;
0元素:节点分为两个集合,集合内部0条边;
1要素:每个节点只能和一条匹配边相连;
映射到这个题目;
1元素对应,一个各自只能被一个骨牌覆盖,覆盖两个相邻的格子,那么假设这些格子没有被限制,我们将其连边,我们将棋盘进行黑白染色,我们显然发现相同颜色无法连边,对应0元素;
所以黑色格子为一个集合,白色格子为一个集合;
让骨牌不重叠放置最多,是一个二分图最大匹配问题;
#include<bits/stdc++.h> using namespace std; #define N 20010 struct gg { int x,y,next; }a[N<<1]; template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48); ch=getchar();} x*=f; } int n,m,tot,x,y,b[210][210],v[N],match[N<<1],lin[N]; inline void add(int x,int y) { a[++tot].y=y; a[tot].next=lin[x]; lin[x]=tot; } inline bool dfs(int x) { for(int i=lin[x];i;i=a[i].next) { int y=a[i].y; if(!v[y]) { v[y]=1; if(!match[y]||dfs(match[y])) { match[y]=x; match[x]=y; return 1; } } } return 0; } int main() { read(n); read(m); for(int i=1;i<=m;i++) { read(x); read(y); b[x][y]=1; } for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) { if(b[i][j]) continue; int id=(i-1)*n+j;//记录是第几个格子; if(!b[i][j-1]&&j>1) { add(id,id-1);//放置一个横着的骨牌; } if(!b[i][j+1]&&j<n) { add(id,id+1); } if(!b[i-1][j]&&i>1) { add(id,id-n); } if(!b[i+1][j]&&i<n) { add(id,id+n); } } int ans=0; for(int i=1;i<=n*n;i++){ if(!match[i]) { if(dfs(i)==1) ans++; memset(v,0,sizeof(v)); } } cout<<ans<<endl; return 0; }
2.车的放置;
类比刚才那道题,以及是有限制,我们尝试寻找一个01要素;
1要素:每行每列只能放1辆车,某个格子(i,j)连边,说明第i行第j列占据一个名额;
0要素:每个车不能即在第i行又在第j行,所以两个行对应没有连边;
求二分图最大匹配即可;
#include<bits/stdc++.h> using namespace std; int n,m,T,tot,x,y,a[201][201],match[400010],lin[100010],v[100010]; template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48); ch=getchar();} x*=f; } struct gg { int y,next; }e[400010]; inline void add(int x,int y) { e[++tot].y=y; e[tot].next=lin[x]; lin[x]=tot; } inline bool dfs(int x) { for(int i=lin[x];i;i=e[i].next) { int y=e[i].y; if(!v[y]) { v[y]=1; if(!match[y]||dfs(match[y])) { match[y]=x; match[x]=y; return 1; } } } return 0; } int main() { read(n); read(m); read(T); for(int i=1;i<=T;i++) { read(x); read(y); a[x][y]=1; } for(int i=1;i<=n;i++) { for(int j=1;j<=m;j++) { if(a[i][j]) continue; add(i,j+n); } } int ans=0; for(int i=1;i<=n;i++) { if(!match[i]) { if(dfs(i)) ans++; memset(v,0,sizeof(v)); } } cout<<ans<<endl; return 0; }
二分图最小点覆盖
给定一张二分图,求出一个最小的点集 S,使得图中任意一条边都已至少一个端点属于 S 。这个问题被称为二分图的最小点覆盖,简称最小覆盖。
定理
二分图最小点覆盖包含的点数 == 二分图最大匹配包含的边数。
Machine Schedule
在二分图最大匹配中,我们寻找的时01要素,而在二分图最小点覆盖时,我们寻找的时2要素;
即每条边有两个端点,两者至少选择一个;
而在这道题中,每个任务要么在A中完成,要么在B中完成,二者必选其一,因为我们将A的M中模式作为左部点,将B的M种模式作为右部点,
将每个任务作为边连接a[i]和b[i],那么显然最少启动次数就是二分图最小匹配;
时间复杂度O(NM);
#include<bits/stdc++.h> using namespace std; int n,m,k; const int maxx=1000001; struct node{ int y; int next; }e[maxx]; int kk,lin[maxx],v[20002]; int match[maxx]; void add(int u,int v) { e[++kk].y=v; e[kk].next=lin[u]; lin[u]=kk; } int dfs(int u) { for(int i=lin[u];i;i=e[i].next) { int y=e[i].y; if(!v[y]) { v[y]=1; if(!match[y]||dfs(match[y])) { match[y]=u; match[u]=y; return 1; } } } return 0; } int main() { while(1) { memset(match,0,sizeof(match)); memset(lin,0,sizeof(lin)); int sum=0; scanf("%d",&n); if(n==0) break; scanf("%d%d",&m,&k); for(int i=1;i<=k;i++) { int x,y,z; scanf("%d%d%d",&x,&y,&z); if(y==0||z==0) continue; else add(y,z+n); } for(int i=1;i<=n;i++) { memset(v,0,sizeof(v)); if(dfs(i)) sum++; } printf("%d\n",sum); } }
POJ2226 Muddy Fields
每块泥地要么被一块横着的木板覆盖,要么被一块竖着的木板覆盖,二者至少选择一个;
因为木板不能覆盖干净的地面,所以我们处理出来行泥泞块和列泥泞块,作为二分图的左右部点;
求出二分图最小点覆盖;
#include<bits/stdc++.h> using namespace std; #define N 1100 int v[N],match[N],lin[N]; int n,m,k,cnt,tot1,tot2; char Map[60][60]; int a[1100][1100],b[1100][1100]; template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48); ch=getchar();} x*=f; } struct gg { int y,next; }e[400010]; inline void add(int x,int y) { e[++cnt].y=y; e[cnt].next=lin[x]; lin[x]=cnt; } inline bool dfs(int x) { for(int i=lin[x];i;i=e[i].next) { int y=e[i].y; if(!v[y]) { v[y]=1; if(!match[y]||dfs(match[y])) { match[y]=x; return 1; } } } return 0; } int main() { read(n); read(m); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) { cin>>Map[i][j]; } for(int i=1;i<=n;i++) { for(int j=1;j<=m;j++) { if(Map[i][j]=='*') { if(Map[i][j-1]=='*') { a[i][j]=a[i][j-1]; } else a[i][j]=++tot1; } } }//横着放; for(int i=1;i<=n;i++) { for(int j=1;j<=m;j++) { if(Map[i][j]=='*') { if(Map[i-1][j]=='*') { b[i][j]=b[i-1][j]; } else { b[i][j]=++tot2; } } } } for(int i=1;i<=n;i++) { for(int j=1;j<=m;j++) { if(Map[i][j]=='*') { add(a[i][j],b[i][j]); } } } int ans=0; for(int i=1;i<=tot1;i++) { memset(v,0,sizeof(v)); if(dfs(i)) ans++; } cout<<ans<<endl; return 0; }
二分图最大独立集
图的独立集
在一张无向图中,满足任意两点之间都没有边相连的点集被称为图的独立集。包含点数最多的一个被称为图的最大独立集。
图的团
在一张无向图中,满足任意两点之间都有边相连的子图被称为图的团。包含点数最多的一个被称为图的最大团。
定理
- 无向图 G 的最大团 == 补图 G' 的最大独立集。
- 对于一般无向图,最大团、最大独立集是 NPC 问题。
- 设 G 是有 n个节点的二分图, G 的最大独立集大小 == n- 最小点覆盖数 == n-最大匹配数。
P3355 骑士共存问题
ACWing(两道题目输入不一样);
与棋盘覆盖类似;
黑白相间染色棋盘,把黑、白色格子分别作为左、右部节点。若两个格子是“日”字对角,则在对应的节点之间连边。“日”字对角的两个节点显然颜色一定不同。
求上述二分图的最大独立集即可。
#include<bits/stdc++.h> using namespace std; #define N 10010 template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48); ch=getchar();} x*=f; } const int dx[8]={-1,-2,-2,-1,1,2,2,1}; const int dy[8]={-2,-1,1,2,2,1,-1,-2}; int n,m,t,ans,tot,x,y,match[N],lin[N],v[N]; int Map[110][110]; vector<int> e; struct gg { int x,y,next; }a[100*100<<2]; inline void add(int x,int y) { a[++tot].y=y; a[tot].next=lin[x]; lin[x]=tot; } inline bool dfs(int x) { for(int i=lin[x];i;i=a[i].next) { int y=a[i].y; if(!v[y]) { v[y]=1; if(!match[y]||dfs(match[y])) { match[y]=x; return 1; } } } return 0; } int main() { read(n); read(m); read(t); for(int i=1;i<=t;i++) { read(x); read(y); Map[x][y]=1; } for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) if(!Map[i][j]&&(i+j)%2) { e.push_back(m*(i-1)+j); for(int k=0;k<8;k++) { int x=i+dx[k],y=j+dy[k]; if(x<1||x>n||y<1||y>m) continue; if(!Map[x][y]) add(m*(i-1)+j,m*(x-1)+y); } } for(int i=0;i<e.size();i++) { memset(v,0,sizeof(v)); if(dfs(e[i])) ans++; } cout<<n*m-t-ans<<endl; return 0; }