西安多校集训-网络流

前言

最晚想起来学了的内容(雾。

概念

基本概念

网络流就是指一张有向图,有 \(n\) 个点,\(m\) 条边,\(s\) 为源点,\(t\) 为汇点。
边有边权为 \(c(u,v)\) 表示 \(u\to v\) 的容量。
定义 \(f(u,v)\) 表示 \(u\to v\) 的流量。
定义剩余流量为容量与流量之差为 \(c_f(u,v)=c(u,v)-f(u,v)\)
整个网络的流即为:\(\sum_{(u,v)\in E} f(u,v)\)
为了方便,我们定义 \(f(u,v)=0\) 表示 \(u\)\(v\) 之间没有连边,当然也可以表示 \(u\)\(v\) 之间的边已经满流量了。
这些概念其实挺好懂得。

性质

  1. 流量限制:从字面意思知道,流量不能大于容量,形式化的说就是:\(f(u,v)\le c(u,v)\)
  2. 流守恒性:\(\sum_{(u,x)\in E}f(u,x)=\sum_{x,v\in E}(x,v)\space (\forall x ≠s,t)\)。说人话就是对于源点与汇点以外的点,流进多少就流出多少,不会堆积。
  3. 如果我们给网络流建反向边,定义为\(f'(v,u)\),表示 \(v\to u\) 的流量。(用处后面再说)那么就有 \(f(u,v)=-f'(v,u)\)。此时的 \(f'(v,u)\) 可能为负数。

综上可得出:\(c_f(u,v)-f'(v,u)=c(u,v)\)
可以这样想:当我们反向边流量越大时,这条边的剩余流量也越多。(也许在后面求最大流有用?)

最大流最小割定理

最大流总等于最小割。
理性证明不会,感性理解的话可以这样想:我们的最大流受限于最小的容量边,而最小割就是要割去最小的容量边,所以它们相等。

【模板】网络最大流

好了终于来到算法部分了,累死我了。
首先,只有 OiWiki 的代码与解释是符合定义的!如果你真的跟我一样突然脑子发病去死扣概念,请阅读 OiWiki 。要不然你会浪费将近 40 分钟的光阴。
题解区的部分(前三篇)代码都直接将反向边流量与剩余流量混用,可能是方便代码也可能是方便理解,但是它不符合我们上文对反向边的定义。如果你是新学网络流的话,我还是推荐直接阅读 OiWiki 。(尽管 OiWiki 可读性差,但是它可以保证正确性啊)

Edmonds–Karp 算法

复杂度为 \(O(nm^2)\)
具体做法是:

  1. 我们如果可以从图上 \(s\) 出发到 \(t\) ,那么就证明我们找到了一条新的增广路 \(p\)
  2. 去求这条增广路 \(p\) 上的最小剩余容量,也就是 \(minn=min_{(u,v)\in p} c_f(u,v)\)
  3. 对于 \(p\) 上每条边加上 \(minn\) 的流量,并给它们的反向边退掉 \(minn\) 的流量,此时最大流增加 \(minn\)
  4. 重复上述操作直到在图上 \(s\) 无法到达 \(t\)

具体实现如下,代码借鉴抄袭了 OiWiki:

#include<iostream>
#include<vector>
#include<cstring>
#include<queue>
#define int long long
using namespace std;
const int N=2e2+10;
const int M=5e3+10;
const int inf=2e17;
struct Edge{
	int u,v,cap,flow;
};
struct EK{
	int n,m;//点数,边数
	vector<Edge> e;//边
	vector<int> G[N];// G[x] 表示 x 能到达的所有边在 e 中的编号
//  其实是不优美的链式前向星啦
	int a[N],p[N];//a[x]表示点 x 的流量,p[x] 表示点 x 的流量从哪一条边来的。
	void init(int n){
		for(int i=0;i<n;i++) G[i].clear();
		e.clear();
		return;
	}
	void add(int u,int v,int cap){
		e.push_back({u,v,cap,0});
		e.push_back({v,u,0,0});
		m=e.size();
		G[u].push_back(m-2);
		G[v].push_back(m-1);
		return ;
	}
	int maxflow(int s,int t){
		int res=0;
		while(1){
			memset(a,0,sizeof(a));
			queue<int> que;
			que.push(s);
			a[s]=inf;
			while(!que.empty()){
				int u=que.front();
				que.pop();
				for(auto i: G[u]){//遍历以 x 为起点的边
					Edge edge=e[i];
					if(!a[edge.v] && edge.cap > edge.flow){//如果这条边之前没被流过,并且还有剩余流量
						p[edge.v]=i;
						a[edge.v]=min(a[u],edge.cap-edge.flow);//流过来的和剩余流量取最小值
						que.push(edge.v);
					}
				}
				if(a[t]) break; // 如果 t 接到流量,就可以停止 bfs 了
			}
			if(!a[t]) break;//如果 t 没接到流量,就说明图中没有增广路了
			for(int x=t;x!=s;x=e[p[x]].u){//遍历 s->t 的路程,给它们加上最后的流量
				e[p[x]].flow+=a[t];
				e[p[x]^1].flow-=a[t];
			}
			res+=a[t];
		}
		return res;
	}
}ek;
signed main(){
	int n,m,s,t;
	cin>>n>>m>>s>>t;
	ek.n=n;
	for(int i=1;i<=m;i++){
		int u,v,cap;
		cin>>u>>v>>cap;
		ek.add(u,v,cap);
	}
	cout<<ek.maxflow(s,t);
	return 0;
}

Dinic 算法

观察到上述 EK 算法每一次都暴力扩展并不优美,考虑去给它做一个优化,就是 Dinic 算法。复杂度为 \(O(n^2m)\)
具体做法:

  1. 首先跑一遍 BFS 将图分层,此后每个点的流只会流给下一层。
  2. 在分层图上求出最大的增广流,定义为阻塞流。
  3. 将阻塞流加入原本的最大流中,更新图。
  4. 重复 1,2,3 直到不能从 s 出发到达 t 为止。

这个算法还可以优化,就是当前弧优化:

如果某一时刻我们已经知道边 \((u, v)\) 已经增广到极限(边 \((u, v)\) 已无剩余容量或 \(v\) 的后侧已增广至阻塞),则 \(u\) 的流量没有必要再尝试流向出边 \((u, v)\)。据此,对于每个结点 \(u\),我们维护 \(u\) 的出边中第一条还有必要尝试的出边。

——摘自 OiWiki
其实有没有点像 DFS 剪枝时的感觉,好吧其实就是。
code:

#include<iostream>
#include<queue>
#include<cstring>
#define ll long long
#define inf 1e18
#define int long long
using namespace std;
const int N=2e3+10;
struct Edge{
	int to,nxt,cap,flow;
}e[2*N];
int n,m,s,t;
int head[N],tot=1;
int dep[N],cur[N];
ll maxflow=0;
void add(int u,int v,int cap){
	e[++tot].to=v;
	e[tot].nxt=head[u];
	e[tot].cap=cap;e[tot].flow=0;
	head[u]=tot;
	e[++tot].to=u;
	e[tot].nxt=head[v];
	e[tot].cap=0;e[tot].flow=0;
	head[v]=tot;
	return;
}
bool bfs(){
	queue<int> que;
	memset(dep,0,sizeof(dep));
	dep[s]=1;
	que.push(s);
	while(!que.empty()){
		int u=que.front();
		que.pop();
		for(int i=head[u];i;i=e[i].nxt){
			int v=e[i].to;       
			if((!dep[v]) && (e[i].cap>e[i].flow)){
				dep[v]=dep[u]+1;
				que.push(v);
			}
		}
	}
	return dep[t];
}
int dfs(int u,int flow){
	if((u==t) || (!flow)) return flow;
	int res=0;
	for(int& i=cur[u];i;i=e[i].nxt){
		int v=e[i].to,minflow;
		if((dep[v]==dep[u]+1) && (minflow=dfs(v,min(flow-res,e[i].cap-e[i].flow)))){
			res+=minflow;
			e[i].flow+=minflow;
			e[i^1].flow-=minflow;
			if(res==flow) return res;
		}
	}
	return res;
}
int Maxflow(){
	while(bfs()){
		memcpy(cur,head,sizeof(head));
		maxflow+=dfs(s,inf);
	}
	return maxflow;
}
signed main(){
	cin>>n>>m>>s>>t;
	for(int i=1;i<=m;i++){
		int u,v,cap;
		cin>>u>>v>>cap;
		add(u,v,cap);
	}
	cout<<Maxflow();
	return 0;
}

例题

圆桌问题

可以将单位与餐桌连一条流量为 1 的边,然后建立源点与汇点,源点向单位连一条流量为 \(r_i\) 的边,餐桌向汇点连一条流量为 \(c_i\) 的边,跑一个最大流。
至于统计方案,遍历一遍单位,看哪一条边的剩余流量为 0 ,就表示这条边被使用。
code:

#include<iostream>
#include<queue>
#include<cstring>
#define ll long long
using namespace std;
const int N=300;
const int M=1e5+10;
const int inf=2e9;
int n,m,s,t,sum;
int r[N],c[N];
ll maxflow;
int cur[N],dep[N];
struct Edge{
	int to,nxt,c;
}e[M];
int head[N],tot=1;
void add(int u,int v,int c){
	e[++tot].to=v;
	e[tot].nxt=head[u];
	e[tot].c=c;
	head[u]=tot;
	e[++tot].to=u;
	e[tot].nxt=head[v];
	e[tot].c=0;
	head[v]=tot;
	return;
}
bool bfs(){
	queue<int> que;
	memset(dep,0,sizeof(dep));
	dep[s]=1;
	que.push(s);
	while(!que.empty()){
		int u=que.front();
		que.pop();
		for(int i=head[u];i;i=e[i].nxt){
			int v=e[i].to;
			if((!dep[v]) && e[i].c>0){
				dep[v]=dep[u]+1;
				que.push(v);
			}
		}
	}
	return dep[t];
}
int dfs(int u,int flow){
	if((u==t) || !flow) return flow;
	int res=0;
	for(int& i=cur[u];i;i=e[i].nxt){
		int v=e[i].to,d;
		if((dep[v]==dep[u]+1) && (d=dfs(v,min(flow-res,e[i].c)))){
			res+=d;
			e[i].c-=d;
			e[i^1].c+=d;
			if(res==flow) return res;
		}
	}
	return res;
}
int Dinic(){
	while(bfs()){
		memcpy(cur,head,sizeof(head));
		maxflow+=dfs(s,inf);
	}
	return maxflow;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>r[i];
		sum+=r[i];
	}
	for(int i=1;i<=m;i++){
		cin>>c[i];
	}
	s=n+m+1,t=n+m+2;
	for(int i=1;i<=n;i++){
		add(s,i,r[i]);
	}
	for(int i=1;i<=m;i++){
		add(i+n,t,c[i]);
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			add(i,j+n,1);
		}
	}
	int res=Dinic();
	cout<<(res==sum)<<'\n';
	if(res!=sum) return 0;
	for(int i=1;i<=n;i++){
		for(int j=head[i];j;j=e[j].nxt){
			int v=e[j].to;
			if(v!=s && e[j].c==0){
				cout<<v-n<<' ';
			}
		}
		cout<<'\n';
	}
	return 0;
}
posted @ 2025-04-10 21:51  Tighnari  阅读(17)  评论(0)    收藏  举报