二分图与网络流

  • 参考资料-洛谷日报

二分图

  • 如果一张无向图的 n n n ( n ≥ 2 ) (n≥2) (n2) 个节点可以分成 A , B A,B A,B 两个非空集合,其中 A ⋂ B A \bigcap B AB为空,并且在同一集合内的点之间都没有边相连,那么称这张无向图为一张二分图。 A , B A,B A,B分别称为二分图的左部和右部。

二分图判定

  • 无向图是二分图 ⇔ ⇔ 图中无奇环(长度为奇数的环)
  • 使用染色法,一个顶点涂黑色,另一个顶点涂白色,若搜到颜色不相符则不是二分图,否则是;
bool dfs_(int u,int color) {
   c[u]=color;
   for(int i=head[u];~i;i=e[i].nxt) {
   	int v=e[i].v;
   	if(c[v]&&c[v]==color) return false;
   	if(!c[v]&&!dfs_(v,3-color)) return false;
   } 
   return true;
}

inline pd_() {
   memset(c,0,sizeof(c));
   for(int i=1;i<=n;++i) {
   	if(!c[i]&&!dfs_(i,1))  return false;
   }
   return true;
}

关押罪犯

  • 按照敌人的敌人是朋友的原则,用并查集实现
#include 
using namespace std;
#define maxn 20010
#define maxm 100010

int n,m,f[maxn<<1];
struct node {
   int x,y,z;
}e[maxm];

inline bool cmp_(node aa,node bb) {
   return aa.z > bb.z;
}

int find_(int x) {
   if(f[x]==x) return x;
   return f[x]=find_(f[x]);
}

void readda_() {
   n=read_();m=read_();
   for(int i=1;i<=m;++i) {
   	e[i].x=read_();e[i].y=read_();e[i].z=read_();
   }sort(e+1,e+m+1,cmp_);
   for(int i=0;i<=(n<<1);++i) f[i]=i;
   for(int i=1;i<=m;++i) {
   	int x=find_(e[i].x),y=find_(e[i].y);
   	if(x==y) {printf("%d",e[i].z);return;}
   	int xx=find_(e[i].x+n),yy=find_(e[i].y+n);
   	f[x]=yy;f[y]=xx;
   }
   printf("0");
}
  • 也可以用二分 + + +二分图判定来做
  • 二分最小值,将怒气值大于 m i d mid mid的两人连边,看能否将两人分开,也就是能否形成二分图
#include 
using namespace std;
#define maxn 20010
#define maxm 100010

int n,m,size=0,head[maxn],vis[maxn];
struct node {
	int x,y,z;
}AKIOI[maxm];
struct edge {
	int v,nxt;
}e[maxm<<1];

inline bool cmp_(node aa,node bb) {
	return aa.z > bb.z;
}

inline void add_(int u,int v) {
	e[++size].v=v;
	e[size].nxt=head[u];
	head[u]=size;
}

bool dfs_(int u,int color) {
	vis[u]=color;
	for(int i=head[u];~i;i=e[i].nxt) {
		int v=e[i].v;
		if(vis[v]==color) return false;
		if(vis[v]==-1&&!dfs_(v,color^1)) return false;
	}
	return true;
}

inline bool pd_(int now) {
	memset(head,-1,sizeof(head));
	size=0;
	for(int i=1;i<=m;++i) {
		if(AKIOI[i].z<=now) break;
		add_(AKIOI[i].x,AKIOI[i].y);
		add_(AKIOI[i].y,AKIOI[i].x);
	}
	memset(vis,-1,sizeof(vis));
	for(int i=1;i<=n;++i) {
		if(vis[i]==-1&&!dfs_(i,1)) return false;
	}
	return true;
}

void readda_() {
	n=read_();m=read_();
	for(int i=1;i<=m;++i) {
		AKIOI[i].x=read_();AKIOI[i].y=read_();AKIOI[i].z=read_();
	}sort(AKIOI+1,AKIOI+1+m,cmp_);
	int l=0,r=AKIOI[1].z,mid;
	while(l<=r) {
		mid=(l+r)>>1;
		if(pd_(mid)) r=mid-1;
		else l=mid+1;
	}
	printf("%d",l);
}

二分图最大匹配

图的匹配定义

  • 两条边都没有公共端点的边的集合被称为图的一组匹配。
  • 即每个点只有一条连边

二分图最大匹配

  • 在二分图中,包含边数最多的一组匹配被称为二分图的最大匹配

其他相关定义

  • 对于任意一组匹配 S S S(边集),属于 S S S的边被称为匹配边,不属于 S S S的边被称为非匹配边。
  • 匹配边的端点被称为匹配点,其他节点被称为非匹配点。
  • 如果二分图中存在一条连接两个非匹配点的路径 p a t h path path ,使得非匹配边与匹配边在 p a t h path path上交替出现,那么称 p a t h path path 是匹配 S S S 的增广路(也称交错路)。

增广路的性质:

  • 长度为奇数
  • 奇数边是非匹配边,偶数边是匹配边.
  • 如果把路径上所有边的状态(是否为匹配边)取反,那么得到的新的边集 S S S,仍然是一组匹配,并且匹配的边数增加了 1 1 1

结论

  • 二分图的一组匹配 S S S是最大匹配 ⇔ 图中不存在 S S S的增广路。

匈牙利算法(增广路算法)

主要过程

  • S S S为空集,即所有边都是非匹配边。
  • 寻找增广路 p a t h path path,把 p a t h path path上所有边的匹配状态取反,得到一个更大的匹配 S S S
  • 重复第上一步,直至图中不存在增广路。

寻找增广路

  • 依次尝试给每一个左部节点 x x x寻找一个匹配的右部节点 y y y
  • y y y x x x匹配需满足下面两条件之一:
    y y y是非匹配点
    y y y已于 x ′ x' x匹配,但从 x ′ x' x出发能找到另外一个 y ′ y' y与之匹配,则 y y y可与 x x x匹配

时间复杂度

  • O ( n ∗ m ) O(n*m) O(nm)

【模板】二分图最大匹配

  • b i t s e t bitset bitset r e s e t reset reset函数是全赋值为 0 0 0
#include 
using namespace std;
#define maxn 1010
#define maxm 1000010

int n,m,T,head[maxn<<1],size=0,f[maxn<<1];
struct edge {
	int v,nxt;
}e[maxm<<1];
bitset<(maxn<<1)>vis;

inline void add_(int u,int v) {
	e[++size].v=v;
	e[size].nxt=head[u];
	head[u]=size;
}

bool dfs_(int u) {
	for(int i=head[u];~i;i=e[i].nxt) {
		int v=e[i].v;
		if(vis[v]) continue;
		vis[v]=1;
		if(!f[v]||dfs_(f[v])) {
			f[v]=u;
			return 1;
		}
	}
	return 0;
}

void readda_() {
	n=read_();m=read_();T=read_();
	memset(head,-1,sizeof(head));
	while(T--) {
		int x=read_(),y=read_();
		if(x>n||y>m) continue;
		add_(x,y+n);add_(y+n,x);
	}int ans=0;
	for(int i=1;i<=n;++i) {
		vis.reset();
		ans+=dfs_(i);
	}
	printf("%d",ans);
}

飞行员配对方案问题

  • 给出 a a a飞行员可以和哪些 b b b飞行员配对,使配对数最多,最大匹配问题
  • 顺带记录一下匹配方案就行了
#include 
using namespace std;
#define maxn 220
#define maxm 10010

int n,m,size=0,head[maxn],f[maxn];
struct edge {
	int v,nxt;
}e[maxm];
bool vis[maxn];

inline void add_(int u,int v) {
	e[++size].v=v;
	e[size].nxt=head[u];
	head[u]=size;
}

int dfs_(int u) {
	for(int i=head[u];~i;i=e[i].nxt) {
		int v=e[i].v;
		if(vis[v]) continue;
		vis[v]=1;
		if(!f[v]||dfs_(f[v])) {
			f[v]=u;
			return 1;
		}
	}
	return 0;
}

void readda_() {
	m=read_();n=read_();
	int x,y;
	memset(head,-1,sizeof(head));
	while(1) {
		x=read_();y=read_();
		if(x==-1&&y==-1) break;
		add_(x,y);
	}int ans=0;
	for(int i=1;i<=m;++i) {
		memset(vis,0,sizeof(vis));
		ans+=dfs_(i);
	}
	printf("%d\n",ans);
	for(int i=m+1;i<=m+1+n;++i) {
		if(!f[i]) continue;
		printf("%d %d\n",f[i],i);
	}
}

酒店之王

  • 给出 n n n个人,给出每个人喜欢的房间和食物,只有一个人住到喜欢的房间,吃到喜欢的食物才满足,求最多能满足的人?
  • 与以往不同的是,这题是一个量去与两个量匹配
  • 可以让人先与房间匹配,再与食物匹配,若两个都匹配的上, a n s + + ans++ ans++
  • 若两个都不能匹配,下一个人
  • 若只能匹配其中一个,则也不能使这个人满足,但注意要还原 f f f数组 ! ! ! ! ! ! ! ! !!!!!!!! !!!!!!!!
#include 
using namespace std;
#define maxn 220
#define maxm 10010

int AKIOI,AKNOI,n,m,p,f_1[maxn],f_2[maxn],head_1[maxn],head_2[maxn],size_1=0,size_2=0;
struct edge {
	int v,nxt;
}e_1[maxm],e_2[maxm];
bool vis_1[maxn],vis_2[maxn];

inline void add_1(int u,int v) {
	e_1[++size_1].v=v;
	e_1[size_1].nxt=head_1[u];
	head_1[u]=size_1;
}

inline void add_2(int u,int v) {
	e_2[++size_2].v=v;
	e_2[size_2].nxt=head_2[u];
	head_2[u]=size_2;
}

int dfs_1(int u) {
	for(int i=head_1[u];~i;i=e_1[i].nxt) {
		int v=e_1[i].v;
		if(vis_1[v]) continue;
		vis_1[v]=1;
		if(!f_1[v]||dfs_1(f_1[v])) {
			f_1[v]=u;
			if(AKNOI==u) AKIOI=v;
			return 1;
		}
	}
	return 0;
}

int dfs_2(int u) {
	for(int i=head_2[u];~i;i=e_2[i].nxt) {
		int v=e_2[i].v;
		if(vis_2[v]) continue;
		vis_2[v]=1;
		if(!f_2[v]||dfs_2(f_2[v])) {
			f_2[v]=u;
			return 1;
		}
	}
	return 0;
}

void readda_() {
	n=read_();m=read_();p=read_();
	memset(head_1,-1,sizeof(head_1));
	memset(head_2,-1,sizeof(head_2));
	//房间
	int x; 
	for(int i=1;i<=n;++i) {
		for(int j=1;j<=m;++j) {
			x=read_();
			if(x) add_1(i,j+n);
		}	
	}
	//食品 
	for(int i=1;i<=n;++i) {
		for(int j=1;j<=p;++j) {
			x=read_();
			if(x) add_2(i,j+n);
		}
	}
	//匹配
	int ans=0;
	for(int i=1;i<=n;++i) {
		memset(vis_1,0,sizeof(vis_1));
		AKNOI=i;		
		int pdc=dfs_1(i);
		if(!pdc) continue;
		memset(vis_2,0,sizeof(vis_2));
		pdc+=dfs_2(i);
		if(pdc==2) ++ans;
		else if(pdc==1) f_1[AKIOI]=0;//还原
	}
	printf("%d",ans);
}

棋盘覆盖

  • 给出棋盘大小 n ∗ n n*n nn,再给出 m m m个禁止的格点,问在不重叠的情况的下最多能放多少张长度为 2 2 2,宽为 1 1 1骨牌?
  • 本题数据范围不允许使用状压DP

二分图匹配模型的两个要素

  • 节点能分成两个集合,每个集合内部有 0 0 0条边。简称 0 0 0元素
  • 每个节点只能与 1 1 1条匹配边相连。简称 1 1 1元素。

两个元素在本题中的体现

  • 1 1 1要素:每个格子只能被一张骨牌覆盖,一张骨牌覆盖相邻的 2 2 2个格子。
    把棋盘上没有被禁止的格子作为节点,骨牌作为

  • 0 0 0要素:把棋盘黑白相间染色,相同颜色的格子不可能被同一骨牌覆盖。
    将白色格子看成左部节点,黑色格子作为右部节点,进行最大匹配

  • 时间复杂度 O ( n 2 m 2 ) O(n^{2}m^{2}) O(n2m2)

#include 
using namespace std;
#define maxn 110
#define maxm maxn*maxn*8

int n,m,size=0,head[maxn*maxn],f[maxn*maxn];
int dx[]={1,-1,0,0};int dy[]={0,0,1,-1};
bool vis[maxn*maxn],a[maxn][maxn];
struct edge {
	int v,nxt;
}e[maxm];

inline void add_(int u,int v) {
	e[++size].v=v;
	e[size].nxt=head[u];
	head[u]=size;
}

int dfs_(int u) {
	for(int i=head[u];~i;i=e[i].nxt) {
		int v=e[i].v;
		if(vis[v]) continue;
		vis[v]=1;
		if(f[v]==-1||dfs_(f[v])) {
			f[v]=u;
			return 1;
		}
 	}
	return 0;
}

void readda_() {
	n=read_();m=read_();
	while(m--) {
		int x=read_(),y=read_();
		a[x-1][y-1]=1;
	}memset(head,-1,sizeof(head));
	for(int i=0;i<n;++i) {
		for(int j=0;j<n;++j) {
			if(a[i][j]) continue;
			for(int k=0;k<4;++k) {
				int px=i+dx[k];
				int py=j+dy[k];
				if(px<0||py<0||px>=n||py>=n) continue;
				if(a[px][py]) continue;//bug
				add_(i*n+j,px*n+py);
				add_(px*n+py,i*n+j);
			}
		}
	}int ans=0;
	memset(f,-1,sizeof(f));
	for(int i=0;i<n;++i) {
		for(int j=0;j<n;++j) {
			if((i^j)&1) continue;
			memset(vis,0,sizeof(vis));
			ans+=dfs_(i*n+j);
		}
	}
	printf("%d",ans);
}

車的放置

  • 给出棋盘大小 n ∗ m n*m nm,给出 t t t个禁止的点,一个車能攻击同一行或同一列的車,问最多能放多少个不互相攻击的車?

  • 1 1 1元素:每行,没列只能放 1 1 1个車
    把行和列作为节点,若此格子未被禁止,则在此格子的行和列之间连边

  • 0 0 0元素:行之间没有连边,列之间没有连边

  • 上图为二分图,将行节点作为左部节点,列节点作为右部节点,求最大匹配

  • 时间复杂度: O ( ( n + m ) n m ) O((n+m)nm) O((n+m)nm)

#include 
using namespace std;
#define maxn 210

int n,m,T,size=0,head[maxn+maxn],f[maxn+maxn];
bool a[maxn][maxn],vis[maxn+maxn];
struct edge {
	int v,nxt;
}e[maxn*maxn];

inline void add_(int u,int v) {
	e[++size].v=v;
	e[size].nxt=head[u];
	head[u]=size;
}

int dfs_(int u) {
	for(int i=head[u];~i;i=e[i].nxt) {
		int v=e[i].v;
		if(vis[v]) continue;
		vis[v]=1;
		if(!f[v]||dfs_(f[v])) {
			f[v]=u;
			return 1;
		}
	}
	return 0;
} 

void readda_() {
	n=read_();m=read_();T=read_();
	while(T--) {
		int x=read_(),y=read_();
		a[x][y]=1;
	}memset(head,-1,sizeof(head));
	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) {
		memset(vis,0,sizeof(vis));
		ans+=dfs_(i);
	}
	printf("%d",ans);
}

[ZJOI2007]矩阵游戏

  • 给定一个 01 01 01矩阵,可以操作使两行或者两列颜色交换,问最后能否使左上角到右下角的对角线上全为 1 1 1
  • 我们的目标状态就是 a [ i ] [ i ] = 1 a[i][i]=1 a[i][i]=1
  • a [ i ] [ j ] 为 1 a[i][j]为1 a[i][j]1则将 i i i j j j连边,我们发现,若交换行或者列,原图仍然不变,也就是只要第 i i i有一列为 1 1 1,就一定能构造出 1 1 1,如果全部 i i i都有对应的 j j j,则满足条件
  • 用匈牙利算法求最大匹配,看最大匹配是否为 n n n即可
#include 
using namespace std;
#define maxn 210

int T,n,head[maxn<<1],f[maxn<<1],size=0;
bool vis[maxn<<1];
struct edge {
	int v,nxt;
}e[maxn*maxn];

inline void add_(int u,int v) {
	e[++size].v=v;
	e[size].nxt=head[u];
	head[u]=size; 
}

int dfs_(int u) {
	for(int i=head[u];~i;i=e[i].nxt) {
		int v=e[i].v;
		if(vis[v]) continue;
		vis[v]=1;
		if(!f[v]||dfs_(f[v])) {
			f[v]=u;
			return 1;
		}
	}
	return 0;
}

void readda_() {
	T=read_();
	while(T--) {
		n=read_();size=0;
		memset(head,-1,sizeof(head));
		memset(f,0,sizeof(f));
		for(int i=1;i<=n;++i) {
			for(int j=1;j<=n;++j) {
				int x=read_();
				if(x) add_(i,j+n);
			} 
		}int ans=0;
		for(int i=1;i<=n;++i) {
			memset(vis,0,sizeof(vis));
			ans+=dfs_(i);
		}
		if(ans==n) printf("Yes\n");
		else printf("No\n");
	}
}

[HNOI2006]超级英雄

  • 给出 n n n个问题, m m m个解决方案,每个问题 i i i给出两个解决方案,一个解决方案只能被用一次,问最多能回答多少问题?注意:这是闯关游戏,即这个问题无法解决则无法回答下个问题,游戏结束。
  • 二分图最大匹配模板题,注意回答不了就结束。
#include 
using namespace std;
#define maxn 2050
#define maxm 1010

int n,m,head[maxn],f[maxn],ans[maxn],size=0;
bool vis[maxn];
struct edge {
	int v,nxt;
}e[maxm<<1];

inline void add_(int u,int v) {
	e[++size].v=v;
	e[size].nxt=head[u];
	head[u]=size;
}

bool dfs_(int u) {
	for(int i=head[u];~i;i=e[i].nxt) {
		int v=e[i].v;
		if(vis[v]) continue;
		vis[v]=true;
		if(!f[v]||dfs_(f[v])) {
			f[v]=u;ans[u]=v;
			return 1;
		}
	}
	return 0;
}

void readda_() {
	n=read_();m=read_();
	memset(head,-1,sizeof(head));
	for(int i=1;i<=m;++i) {
		int x=read_(),y=read_();
		add_(i,x+m+1);add_(i,y+m+1);
	}int AKIOI=0;
	for(int i=1;i<=m;++i) {
		memset(vis,0,sizeof(vis));
		if(dfs_(i)) ++AKIOI;
		else break;
	}
	printf("%d\n",AKIOI);
	for(int i=1;i<=AKIOI;++i) {
		printf("%d\n",ans[i]-m-1); 
	}
}

座位安排

  • n n n个人,每个人有自己喜欢的两排座位,每排座位有坐二人,问最多能使多少人坐在自己喜欢的座位上
  • 将每排作为拆点,拆为两个点,注意数组要开够大
#include 
using namespace std;
#define maxn 12010
#define maxm 4010

int n,head[maxn],m,size=0,f[maxn];
bool vis[maxn];
struct edge {
	int v,nxt;
}e[maxm<<2];

inline void add_(int u,int v) {
	e[++size].v=v;
	e[size].nxt=head[u];
	head[u]=size;
}

bool dfs_(int u) {
	for(int i=head[u];~i;i=e[i].nxt) {
		int v=e[i].v;
		if(vis[v]) continue;
		vis[v]=1;
		if(!f[v]||dfs_(f[v])) {
			f[v]=u;
			return true;
		}
	}
	return false;
}

void readda_() {
	n=read_();m=n<<1;
	memset(head,-1,sizeof(head));
	for(int i=1;i<=m;++i) {
		int x=read_(),y=read_();
		add_(i,x+m);add_(i,y+m);
		add_(i,x+m+m);add_(i,y+m+m);
	}int ans=0;
	for(int i=1;i<=m;++i) {
		memset(vis,0,sizeof(vis));
		if(dfs_(i)) ++ans;
	}
	printf("%d",ans);
}

网络流

最大流

【模板】网络最大流

  • 判断 r e s t rest rest为正不要在循环节判断
#include 
using namespace std;
#define maxn 10050
#define maxm 100050
#define INF 1000000007

int n,m,s,t,head[maxn],cur[maxn],d[maxn],sze=1;
struct edge {
   int v,w,nxt;
}e[maxm<<1];

inline void add_(int u,int v,int w) {
   e[++sze].v=v;
   e[sze].w=w;
   e[sze].nxt=head[u];
   head[u]=sze;
}

inline bool bfs_() {
   memset(d,0,sizeof(d));
   d[s]=1;queue<int>q;
   q.push(s);
   while(!q.empty()) {
   	int u=q.front();q.pop();
   	for(int i=head[u];~i;i=e[i].nxt) {
   		int v=e[i].v;
   		if(e[i].w&&!d[v]) {
   			d[v]=d[u]+1;
   			q.push(v);
   			if(v==t) return true ;
   		}
   	}
   }
   return false ;
}

int dinic_(int u,int flow) {
   if(u==t||!flow) return flow;
   int rest=flow;
   for(int &i=cur[u];~i;i=e[i].nxt) {
   	int v=e[i].v;
   	if(e[i].w&&d[v]==d[u]+1) {
   		int k=dinic_(v,min(rest,e[i].w));
   		if(!k) d[v]=0;
   		e[i].w-=k;
   		e[i^1].w+=k;
   		rest-=k;
   		if(!rest) break;
   	}
   }
   return flow-rest ;
}

void readda_() {
   n=read_();m=read_();
   s=read_();t=read_();
   memset(head,-1,sizeof(head));
   int x,y,z;
   for(int i=1;i<=m;++i) {
   	x=read_(),y=read_(),z=read_();
   	add_(x,y,z);add_(y,x,0);
   }int ans=0;
   while(bfs_()) {
   	for(int i=1;i<=n;++i) cur[i]=head[i];
   	ans+=dinic_(s,INF);
   }
   printf("%d",ans);
}

你可能感兴趣的:(二分图,网络流)