网络流
二分图
二分图的判定
P1525 [NOIP2010 提高组] 关押罪犯
其实对于二分图判定的做法还是比较好想的,(因为我太蒻想不到并查集还要有边权)也易于实现
由于本题要求把罪犯划分到两个监狱中(我理解为划分到两个不同的集合中)那么我不禁想到图论的二分图
首先,抛来一个二分图的定义:
如果一张无向图的n个节点(n>=2)可以分为A,B两个集合,
且满足A ∩ B = ∅ ,而且在同一集合内的点之间都没有边相连,那么这张无向图被称为二分图,其中A和B分别叫做二分图的左部和右部
那么对于本题,我们就是要把所有人分为两个部分,其间不出现矛盾,显然很符合二分图的要求
别太高兴,问题来了:如何判定这个“矛盾图”是不是二分图
二分图判定定理:
一张无向图是二分图:
当且仅当图中不存在奇环(奇环是指长度为奇数的环)
既然有了判定定理,我们就可以使用染色法进行二分图判定
染色法基本实现如下:
1.大多数情况基于dfs(深度优先搜索)
2.我们尝试用黑和白两种颜色标记图中的点,当一个节点被标记了,那么所有与它相连的点应全部标记为相反的颜色
如果在标记过程中出现冲突,那么算法结束,证明本图中存在奇环,即本图不为二分图;反之,如果算法正常结束,那么证明本图是二分图
时间复杂度显然为\(O(n)\)
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=2*1e4+5,maxm=1e5+5;
int mid,vis[maxn],n,m;
bool ans;
struct node{
	int x,y,v;
}a[maxm];
struct edge{
	int to,v;
};
vector<edge>d[maxn]; 
bool cmp(node x,node y){
	return x.v>y.v;
}
bool check(int now,int c){
//	cout<<now<<" "<<c<<endl;
	vis[now]=c;
	for(int i=0;i<d[now].size();i++){
		if(d[now][i].v<a[mid].v)continue;
		int Next=d[now][i].to;
		if(vis[Next]==vis[now])return false;
		if(!vis[Next]&&!check(Next,-c))return false;
	}
	return true;
}
bool solve(){
	memset(vis,0,sizeof vis);
	for(int i=1;i<=n;i++){
		if(!vis[i]){
			if(!check(i,1))return false;
		}
	}
	return true;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		cin>>a[i].x>>a[i].y>>a[i].v;
		d[a[i].x].push_back({a[i].y,a[i].v});
		d[a[i].y].push_back({a[i].x,a[i].v});
	}
	sort(a+1,a+m+1,cmp);
	int l=0,r=m+1;
	while(l+1<r){
		ans=true;
		mid=(l+r)/2;
		if(solve())l=mid;
		else r=mid;
	}
	cout<<a[l+1].v;
	return 0;
}
Fairy
这道题我们直接暴力删边检查是否为二分图是会超时的
不难发现二分图的一些性质:
- 二分图不存在奇环;
- 一棵树一定是二分图;
- 一个二分图删掉一条边也是二分图;
- 假设现在有一棵树,在树上取两个节点 u 和 v ,如果 u 和 v 的最短路是奇数,连接 u , v ,则出现偶环(长度为偶数的环);如果 u 和 v 的最短路是偶数,连接 u , v ,则出现奇环(长度为奇数的环);
对于性质二,我们随意求出一棵生成树,然后由性质四,我们可以把剩下的边一条条地放在树上,判断这条边会不会构成奇环,并统计奇环个数 sum 。
首先我们明确一点,答案一定是在奇环上的,所以如果有奇环而边又不在奇环上的话,那一定不能成为答案
进一步的,答案一定是在所有奇环的并集上的
由性质三,如果 sum = 0 ,输出所有边即可。
对于其他情况,
我们先考虑非树边,如果有一个奇环的话,那么显然这条非树边是其中一种选择,如果有不止一个奇环的话,这个非树边就不会是答案
对于树边而言,
 1.它在所有奇环的并集上,而且没有包含偶环,那么这条树边是答案
 2.它在所有奇环的并集上,而且包含偶环,这时我们去掉该边,就会发现:奇环 + 偶环 - 消去两次的树边 = 奇环
所以只有它在所有奇环的并集上,而且没有包含偶环,他才是答案
因此我们考虑树上差分,对于奇环上的所有树边我们加1,如果边的权值等于奇环的个数就是答案
对于偶环上的树边,他们在有奇环的情况下永远不可能成为答案,我们只用对他们减1即可
只有一个奇环时还要加上非树边
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e4+5;
int WHI,cnt_ji,n,m,w[maxn],x[maxn],y[maxn],cnt=0,CNT=0,ANS[maxn],vis[maxn],USED[maxn],NUM[maxn];
bool mp[maxn],vis2[maxn];
struct node{
	int to,pos;
};
vector<node>d[maxn];
void dfs(int now,int last){
	vis[now]=vis[last]+1;
	for(int i=0;i<d[now].size();i++){
		int Next=d[now][i].to;
		if(Next==last)continue;
		if(vis[Next]){
			if(vis[Next]<vis[now]){
				mp[d[now][i].pos]=false;
				USED[++cnt]=d[now][i].pos;
			}
			continue;
		}
		w[Next]=d[now][i].pos;
		dfs(Next,now);
	}
	return ;
}
void dfs2(int now,int last){
	vis2[now]=true;
	for(int i=0;i<d[now].size();i++){
		int Next=d[now][i].to;
		if(Next==last||!mp[d[now][i].pos]||vis2[Next])continue;
		dfs2(Next,now);
		NUM[now]+=NUM[Next];
	}
	if(NUM[now]==cnt_ji)ANS[++CNT]=w[now];
	return ;
}
int main(){
	memset(mp,true,sizeof mp);
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		cin>>x[i]>>y[i];
		d[x[i]].push_back({y[i],i});
		d[y[i]].push_back({x[i],i});
	}
	for(int i=1;i<=n;i++)
		if(!vis[i])
			dfs(i,i);
	
	for(int i=1;i<=cnt;i++){
		if(vis[x[USED[i]]]>vis[y[USED[i]]])swap(x[USED[i]],y[USED[i]]);
		if((vis[y[USED[i]]]-vis[x[USED[i]]])%2==0){
			if(WHI==0)WHI=USED[i];
			else if(WHI!=0)WHI=-1;
			NUM[y[USED[i]]]++,NUM[x[USED[i]]]--;
			cnt_ji++;
		}
		else
			NUM[y[USED[i]]]--,NUM[x[USED[i]]]++;
	}
	for(int i=1;i<=n;i++)
		if(!vis2[i])
			dfs2(i,i);
	if(WHI==0){
		cout<<m<<endl;
		for(int i=1;i<=m;i++)cout<<i<<" ";
	}
	else {
		if(WHI!=-1)ANS[++CNT]=WHI;
		sort(ANS+1,ANS+CNT+1);
		cout<<CNT<<endl;
		for(int i=1;i<=CNT;i++)cout<<ANS[i]<<" ";
	}
	return 0;
}
P1285 队员分组
这题还是比较简单的,如果不是互相认识,那么我们就对他们连一下边
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=105;
int cnt=1,vis[maxn],ans[2][maxn],n,ANS[maxn],x,dp[maxn][maxn];
bool f[maxn][maxn];
vector<int>d[maxn];
bool dfs(int now,int c){
	vis[now]=c;
	if(c>0)
		ans[0][cnt]++;
	else ans[1][cnt]++;
	for(int i=0;i<d[now].size();i++){
		int Next=d[now][i];
		if(vis[Next]==vis[now])return false;
		if(!vis[Next]&&!dfs(Next,-c))return false;
	}
	return true;
}
int main(){
	memset(dp,-1,sizeof dp);
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		while(1){
			scanf("%d",&x);
			if(!x)break;
			f[i][x]=true;
		}
		f[i][i]=true;
	}
	bool flag=true;
	for(int i=1;i<=n;i++){
		for(int j=i+1;j<=n;j++){
			if((!f[i][j])||(!f[j][i]))d[i].push_back(j),d[j].push_back(i);
		}
	}
	
	for(int i=1;i<=n;i++){
		if(!vis[i]){
			if(!dfs(i,cnt)){
				flag=false;
				break;
			}
			else cnt++;
		}
	}
	cnt--;
	if(!flag){
		cout<<"No solution\n";
		return 0;
	}
	dp[0][0]=0;
	for(int i=1;i<=cnt;i++){
		for(int j=0;j<=n;j++){
			if(dp[i-1][j]==-1)continue;
			if(j+ans[0][i]<=n)dp[i][j+ans[0][i]]=j;
			if(j+ans[1][i]<=n)dp[i][j+ans[1][i]]=j;
		}
	}
	int maxx=1e5,maxxi;
	for(int i=0;i<=n;i++){
		if(dp[cnt][i]==-1)continue;
		if(abs(n-i-i)<maxx){
			maxx=abs(n-i-i);
			maxxi=i;
		}
	}
	int st=cnt;
	int maxxi2=n-maxxi;
	cout<<maxxi<<" ";
	while(st!=0){
		if(maxxi==dp[st][maxxi]+ans[0][st])ANS[st]=1,maxxi-=ans[0][st];
		else ANS[st]=-1,maxxi-=ans[1][st];
		st--;
	}
	for(int i=1;i<=n;i++){
		if(ANS[abs(vis[i])]*vis[i]>0)cout<<i<<" ";
	}
	cout<<endl;
	cout<<maxxi2<<" ";
	for(int i=1;i<=n;i++){
		if(ANS[abs(vis[i])]*vis[i]<0)cout<<i<<" ";
	}
	return 0;
}
二分图的最大匹配
P3386 【模板】二分图最大匹配
常用的二分图匹配算法是匈牙利算法,其正确性基于 hall 定理,本质是不断寻找增广路来扩大匹配数。但是其正确性证明比较复杂,在此略去。
匈牙利算法的过程是,枚举每一个左部点 u ,然后枚举该左部点连出的边,对于一个出点 v,如果它没有被先前的左部点匹配,那么直接将 u 匹配 v,否则尝试让 v 的“原配”左部点去匹配其他右部点,如果“原配”匹配到了其他点,那么将 u 匹配 v,否则 u 失配。
尝试让“原配”寻找其他匹配的过程可以递归进行。需要注意的是,在一轮递归中,每个右部点只能被访问一次。
算法的时间复杂度为$ O(n \times e + m)$,其中 n是左部点个数,e是图的边数,m是右部点个数。
不难发现其实交换左右部点后的最大匹配数是一样的,而对于 \(m < n\),有 \(m \times e + n < n \times e + m\)。所以有一个小 trick 是当右部点的个数比左部点多的时候,交换左右部能有更高的效率。
实际上,在dfs里面,x表示的是左部点,i表示的是右部点,而match[i]记录的是右部点匹配的是哪一个左部点,vis记录右部点是否访问过,我们使用邻接矩阵存图
代码(邻接矩阵)
#include<bits/stdc++.h>
using namespace std;
const int maxn=505;
int n,m,vis[maxn],match[maxn],d[maxn][maxn],e,x,y,ans;
int dfs(int x){
	for(int i=1;i<=m;i++){
		if(d[x][i]&&!vis[i]){
			vis[i]=1;
			if(!match[i]||dfs(match[i])){
				match[i]=x;
				return 1;
			}
		}
	}
	return 0;
}
int main(){
	cin>>n>>m>>e;
	for(int i=1;i<=e;i++){
		cin>>x>>y;
		d[x][y]=1;
	}
	for(int i=1;i<=n;i++){
		memset(vis,0,sizeof vis);
		ans+=dfs(i);
	}
	cout<<ans;
	return 0;
}
代码(vector)
#include<bits/stdc++.h>
using namespace std;
const int maxn=505;
vector<int>d[maxn];
int vis[maxn],match[maxn],n,m,e,x,y;
int check(int now){
	for(int i=0;i<d[now].size();i++){
		int Next=d[now][i];
		if(!vis[Next]){
			vis[Next]=true;
			if(!match[Next]||check(match[Next])){
				match[Next]=now;
				return 1;
			}
		}
	}
	return 0;
}
int main(){
	cin>>n>>m>>e;
	for(int i=1;i<=e;i++){
		cin>>x>>y;
		d[x].push_back(y);
	}
	int ans=0;
	for(int i=1;i<=n;i++){
		memset(vis,0,sizeof vis);
		ans+=check(i);
	}
	cout<<ans;
	return 0;
}
P1129 [ZJOI2007] 矩阵游戏
我们令左部点为行的编号,右部点为列的编号,显然,对于一个\((i,j)=1\)来说,我们可以将左边的i连向右边的j
此时我们可以发现,对于行的交换操作,实质上就是对左部点进行交换
对于列的交换操作,实质上就是对右部点进行交换,都不影响最大匹配
所以最终最大匹配为n时就输出Yes
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=205;
int match[maxn],vis[maxn],T,n,cnt,a[maxn][maxn];
vector<int>d[maxn];
int dfs(int now){
	for(int i=0;i<d[now].size();i++){
		int Next=d[now][i];
		if(!vis[Next]){
			vis[Next]=1;
			if(!match[Next]||dfs(match[Next])){
				match[Next]=now;
				return 1;
			}
		}
	}
	return 0;
}
int main(){
	cin>>T;
	while(T--){
		memset(match,0,sizeof match);
		cin>>n;
		cnt=0;
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				cin>>a[i][j];
				if(a[i][j])
					d[i].push_back(j);
			}
		}
		int ans=0;
		for(int i=1;i<=n;i++){
			memset(vis,0,sizeof vis);
			ans+=dfs(i);
		}
		if(ans==n) cout<<"Yes\n";
		else cout<<"No\n";
		for(int i=1;i<=n;i++)d[i].clear();
	}
	
	return 0;
}
二分图的其他性质
最大独立集:
独立集是指图 G 中两两互不相邻的顶点构成的集合。
最小点覆盖:
如果说一条边的任意一个顶点被选中那么这条边也被选中,简而言之就是我们希望用尽可能少的点去使所有的边都被选中
最大独立集点数加最大匹配数等于总点数:

在无向图中,总点数 = 最大独立集+最小点覆盖:
反证法证明: 用V表示点集,V1表示一个点覆盖集。
设V2=V-V1。假设V2不是独立集,那么V2中的存在两个点U,V有一条边,
因为点覆盖集要包含每条边的其中一个点,
那么V1不是点覆盖集,矛盾。
所以 ,点覆盖集和独立集是互补。
所以,总点数 = 最大独立集+最小点覆盖。
最大匹配边数等于最小点覆盖
由上显然
网络流
Dinic算法
P3376 【模板】网络最大流
算法思想
1、先用bfs对图进行分层,如果图不连通则结束
2、用dfs寻找增广路,对于一个节点来说,只找比他深度多一的增广路,找到就返回流量,正向边加,反向边减。
3、使用当前弧优化
我们定义一个数组cur记录当前边(弧)(功能类比邻接表中的head数组,只是会随着dfs的进行而修改),
每次我们找过某条边(弧)时,修改cur数组,改成该边(弧)的编号,
那么下次到达该点时,会直接从cur对应的边开始(也就是说从head到cur中间的那一些边(弧)我们就不走了)。
有点抽象啊,感觉并不能加快,然而实际上确实快了很多。
时间复杂度为\(O (n^2*m)\)
代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxm=1e5+5,INF=1e18,maxn=205;
int n,m,s,t,u,v,w;
struct node{
	int v,nxt,to;
}edge[maxm];
int head[maxn];
int dep[maxn],whi[maxn];
queue<int>q; 
int cnt=1;//注意开始一定为奇数
void add_edge(int u,int v,int w){
	cnt++;
	edge[cnt].v=w;
	edge[cnt].to=v;
	edge[cnt].nxt=head[u];
	head[u]=cnt;
	return ;
}
int bfs(){
	for(int i=1;i<=n;i++)dep[i]=-1;
	while(!q.empty())q.pop();
	dep[s]=1;
	whi[s]=head[s];
	q.push(s);
	while(!q.empty()){
		int now=q.front();q.pop();
		for(int i=head[now];i;i=edge[i].nxt){
			int next=edge[i].to;
			if(edge[i].v>0&&dep[next]==-1){
				q.push(next);
				whi[next]=head[next];
				dep[next]=dep[now]+1;
				if(next==t)return 1;
			}
		}
	}
	return 0;
}
int dfs(int now,int flow){
	if(now==t)return flow;
	int ans=0;
	for(int i=whi[now];i&&flow;i=edge[i].nxt){
		int next=edge[i].to;
		whi[now]=i;
		if(edge[i].v>0&&dep[next]==dep[now]+1){
			int now_flow=dfs(next,min(flow,edge[i].v));
			if(!now_flow)dep[next]=-1;
			edge[i].v-=now_flow;
			edge[i^1].v+=now_flow;
			ans+=now_flow;
			flow-=now_flow;
		}
	}
	return ans;
}
void dinic(){
	int max_flow=0;
	while(bfs())
		max_flow+=dfs(s,INF);
	cout<<max_flow<<endl;
	return ;
}
signed main(){
	scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
	for(int i=1;i<=m;i++){
		scanf("%lld%lld%lld",&u,&v,&w);
		add_edge(u,v,w);
		add_edge(v,u,0);
	}
	dinic();
	return 0;
}
P2756 飞行员配对方案问题
这道题是比较常规的二分图,我们可以考虑使用dinic算法实现
对于二分图而言,我们建立一个超级源点与超级汇点,边权都为1,然后二分图上的边权都为正无穷
然后就是常规的dinic,
二分图上dinic的时间复杂度为\(O(n^2\sqrt n)\)
如何查错
dinic算法在实现上有几个要注意的点
1、边的编号起始要为偶数,由于连边时我们先cnt++,所以我们初始化cnt=-1或1
2、连边的时候权值给错
3、遍历边的时候,一般是
for(int i=head[now];i;i=edge[i].nxt)
在使用当前弧优化时就是用whi[now]
4、每一次都要检查edge[now].v是否大于0
5、在dfs的时候,如果一条边已经无法再流,则要让dep[now]=-1
6、dfs的时候,每一次循环我们都先要更新whi[now]的值
如果还是找不到问题,我们可以做完bfs后,先输出所有点的dep值,然后dfs完之后输出所有边及反向边的流量
代码
#include<bits/stdc++.h>
using namespace std;
const int maxm=1e4+5,maxn=105,oo=1e7;
int head[maxn],dep[maxn],whi[maxn];
int n,m,x,y,st,en;
int cnt=1;
queue<int>q;
struct edge{
	int to,v,nxt;
}edge[maxm];
void add_edge(int x,int y,int c){
	cnt++;
	edge[cnt].to=y,edge[cnt].v=c,edge[cnt].nxt=head[x];
	head[x]=cnt;
}
bool bfs(){
	for(int i=0;i<=n+1;i++)dep[i]=-1;
	whi[st]=head[st];
	dep[st]=0;
	q.push(st);
	while(!q.empty()){
		int now=q.front();q.pop();
		for(int i=head[now];i;i=edge[i].nxt){
			int Next=edge[i].to;
			if(edge[i].v>0&&dep[Next]==-1){
				dep[Next]=dep[now]+1;
				whi[Next]=head[Next];
				q.push(Next);
			}
		}
	}
	return dep[en]!=-1;
}
int dfs(int now,int flow){
	if(now==en)return flow;
	int ans=0;
	for(int i=whi[now];i&&flow;i=edge[i].nxt){
		int Next=edge[i].to;
		whi[now]=i;
		if(edge[i].v>0&&dep[Next]==dep[now]+1){
			int now_flow=dfs(Next,min(flow,edge[i].v));
			if(!now_flow)dep[Next]=-1;
			edge[i].v-=now_flow;
			edge[i^1].v+=now_flow;
			ans+=now_flow;
			flow-=now_flow;
		}
	}
	return ans;
}
void dinic(){
	int ans=0;
	while(bfs()){
//		for(int i=0;i<=n+1;i++)cout<<dep[i]<<" ";cout<<endl;
		ans+=dfs(st,oo);
//		for(int i=0;i<=n+1;i++){
//			for(int j=head[i];j;j=edge[j].nxt){
//				printf("%d %d %d %d\n",i,edge[j].to,edge[j].v,edge[j^1].v);
//			}
//		}
	}
	cout<<ans<<endl;
	return ;
}
int main(){
	cin>>m>>n;
	while(1){
		cin>>x>>y;
		if(x==-1&&y==-1)break;
		add_edge(x,y,oo);
		add_edge(y,x,0);
	}
	st=0,en=n+1;
	for(int i=1;i<=m;i++)add_edge(st,i,1),add_edge(i,st,0);
	for(int i=m+1;i<=n;i++)add_edge(i,en,1),add_edge(en,i,0);
	
	dinic();
	for(int i=1;i<=m;i++){
		for(int j=head[i];j;j=edge[j].nxt){
			if(edge[j].to!=st&&edge[j^1].v)printf("%d %d\n",i,edge[j].to);
		}
	}
	return 0;
}

 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号