网络流

dalao博客

模拟赛考了三道网络流,场上只看出来一道网络流……一点板子都不会写了,遂复习重学+补博客的坑

upd:2025.3.22 突然发现我的当前弧优化写的不对,必须写在判断条件的下面,否则有可能在回溯的时候往回跳

前置

网络流是一个有源点s和汇点t的有向图

容量限制: 对于每条边,都有一个 \(f(u,v)\) 表示它上的流,同有一个 \(c(u,v)\) 表示它的容量。\(f(u,v)\leq c(u,v)\) ,一条边的流不能超过它的容量。

斜对称性: \(w(u,v)=-w(v,u)\)

流守恒: 除非是源点或汇点,剩下所有节点流入和流出的流量相等。源点是产生流量的地方,汇点是流量流至的地方。

剩余流量: 边的剩余流量是 \(c_f(u,v)=c(u,v)-f(u,v)\) ,这定义了剩余网络,它显示可用的容量的多少。注意,即使元网络中 u->v 没有边,在剩余网络中仍可能有 u->v 的边。因为相反方向的流抵消,减少由 v->u 的流等于增加 u->v 的流。

增广路: 就是一条路径,它的起点是s,终点是t,且它经过的边的剩余流量均大于0,表示沿这条路径发送更多流是可能的。当且仅当剩余网络没有增广路时,达到最大流。

最大流

非常顾名思义,就是求s到t的最大可行流

首先,引入反向边。如果我们没有反向边,那么如果直接给流量,可能不会是整体最优决策。那么我们在给流量的时候,给当前边容量减掉流量,反向边加上流量,这样就可以在之后的操作中,通过走反向边,给程序“反悔”的机会。

dinic

dinic又称为“Dinic阻塞流算法”,该算法的关键就在于“阻塞流”

这里我们说的最短路是01最短路,即路径上的边数。我们考虑每次增广最短路上的边(这个跟时间复杂度证明有关)

这里我们引入“分层图”,大概就是对于每个点,按照s到它的最短路长度分组,最短路长度相等的为“一组”或“一层”。这个分层也可以理解为深度。

建图后我们发现,所有存在于分层图上的残余边都在一条残量网络的最短路上。因为不用担心在处理分层图时流被反向退回,我们放心地只用一遍dfs来增广就好了。(增广就是找到一条增广路,并让它流它上面能流的最大流量)

但这并不意味我们在边上处理流量的时候不用管反向边,反向边的残量变更还是要计算的。因为以后的dfs可能会把流再推回去。

我们发现,当把分层图上的最大流完全增广后,s和t在分层图上一定不连通,我们称其为阻塞增广,这样增广出来的流就是阻塞流。

dinic主要由一个bfs和一个dfs组成,其中bfs用于寻找最短路;dfs用于在可行增广路中寻找最小容量,即寻找增广路最多能流的流量。

当前弧优化

保证复杂度的一个东西

我们定义一个now数组,它最初就是h数组,即邻接表存图的头指针。我们在dfs时,从x到了第i条边,就说明前i-1条边已经被榨干了,那么不妨直接改now[x]=i,下次访问从x出发的节点时就可以直接到这里。

这个就是因为,前面所说的“阻塞流”。因为每次跑阻塞流都会把当前路径断开,即,不再能从s到达t,所以这些跑过的边是没用的。

\(O(n^2m)\) 但玄学上界,指复杂度很松,一般都能跑。。

还是贴一下时间复杂度证明吧:

void add(int u,int v,int val){
	nxt[++cnt]=h[u],to[cnt]=v,h[u]=cnt,w[cnt]=val;
	nxt[++cnt]=h[v],to[cnt]=u,h[v]=cnt,w[cnt]=0; //有向图! 
}
int bfs(){
	memset(dep,0,sizeof(dep));
	queue<int>q;
	q.push(s);
	dep[s]=1;
	now[s]=h[s];
	while(!q.empty()){
		int x=q.front();
		q.pop();
		for(int i=h[x];i;i=nxt[i]){
			int y=to[i];
			if(w[i]>0&&dep[y]==0){//直接考虑能走的边 
				q.push(y);
				now[y]=h[y];//当前弧优化 
				dep[y]=dep[x]+1;//分层图 
				if(y==t) return 1;
			}
		}
	}
	return 0;
}
int dfs(int x,int sum){//这里,sum:整条路的最大贡献流量 
	if(x==t) return sum;
	int k,res=0;
	for(int i=now[x];i&&sum;i=nxt[i]){
		int y=to[i];
		if(w[i]>0&&(dep[y]==dep[x]+1)){//保证是最短路+能走 
        	now[x]=i;//当前弧优化,走过的 
			k=dfs(y,min(sum,w[i]));//k流过的流量
			if(k==0) dep[y]=0;//剪枝,增广完毕 
			w[i]-=k,w[i^1]+=k;//正反向边 
			res+=k;//经过该点的流量和 
			sum-=k;//经过该点当前剩余流量 
		}
	}
	return res;
}
void dinic(){
    while(bfs()){
        ans+=dfs(s,inf);
    }
}

费用流

也叫最小费用最大流。保证解是最大流的情况下,费用最大/小。

怎么说,就是把bfs动点手脚,改成spfa,支持最长路最短路就行。

对EK或dinic的最短路改成spfa都行。dinic更快一点,而且和最大流差别不大,就是bfs的按01最短路分层改成了按SPFA最短/长路分层。然后后面为了处理0费用边必须写一个vis,否则会寄。

\(O(n^2m^2)\)

#include<bits/stdc++.h>
using namespace std;
const int maxn=5e3+5,maxm=5e4+5,inf=0x3f3f3f3f;
int n,m,h[maxn],to[maxm<<1],nxt[maxm<<1],cnt=1,w[maxm<<1],f[maxm<<1];
int dis[maxn],vis[maxn],now[maxn],mcost,mflow,s,t;
void add(int u,int v,int val,int flow){
	nxt[++cnt]=h[u],to[cnt]=v,h[u]=cnt,w[cnt]=val,f[cnt]=flow;
	nxt[++cnt]=h[v],to[cnt]=u,h[v]=cnt,w[cnt]=-val,f[cnt]=0;
}
bool SPFA(){
	memset(dis,0x3f,sizeof(dis));
	queue<int>q; q.push(s);
	dis[s]=0,vis[s]=1;
	while(q.size()){
		int x=q.front(); q.pop();
		vis[x]=0,now[x]=h[x];
		for(int i=h[x];i;i=nxt[i]){
			int y=to[i];
			if(f[i]>0&&dis[x]+w[i]<dis[y]){
				dis[y]=dis[x]+w[i];
				if(!vis[y]) q.push(y),vis[y]=1;
			}
		}
	}
	return dis[t]!=inf;
}
int dfs(int x,int sum){
	if(x==t) return sum;
	int k,res=0; vis[x]=1;//处理0费用边,因为后面有一个dis[y]==dis[x]+w[i]否则会来回跑 
	for(int i=now[x];i&&sum;i=nxt[i]){
		int y=to[i];
		if(!vis[y]&&f[i]>0&&dis[y]==dis[x]+w[i]){
            now[x]=i;
			k=dfs(y,min(sum,f[i]));
			if(!k) dis[y]=0;
			mcost+=k*w[i];
			f[i]-=k,f[i^1]+=k;
			sum-=k,res+=k;
		}
	}
	vis[x]=0;
	return res;
}
void dinic(){
	while(SPFA()){
		mflow+=dfs(s,inf);
	}
}
int main(){
	scanf("%d%d%d%d",&n,&m,&s,&t);
	for(int i=1,u,v,val,flow;i<=m;i++){
		scanf("%d%d%d%d",&u,&v,&flow,&val);
		add(u,v,val,flow);
	} 
	dinic();
	printf("%d %d",mflow,mcost);
	return 0;
}

最小割

割: 将所有节点划分成S,T两个集合,源点属于S,汇点属于T。相当于一刀切。

割的径流量: 集合S和T间连接的边中,从S到T边的容量之和。最小割为容量最小的割。

定理:最大流=最小割

感性理解就行(

闲话:

第一眼看题,不一定能看出来是网络流,甚至可能是把一个数据类的抽象到图论上。总之很巧妙,像dp。就先看限制约束条件,可以先思考连边,然后考虑是最大流、最小割还是什么总体-局部的转换,最后建图。

有一个最大权闭合子图,套路性的正边-源点,负边-汇点,答案为正边权和-最小割。

求解一条边能否在最小割和是否一定在最小割中的问题时,考虑在最终的残量网络上用 tarjan 求解。

上下界网络流(可行、最大、最小、费用流)

主要思想就是先让它直接流下界的流量,然后如果是有源汇的就连汇点t到源点s(因为流量应该相等),多余或不足的流量从新的源点汇点S、T补(这里直接理解为“令”它的出/入流量为什么),然后跑最大流,如果把新源点汇点的边跑满,则可行。

最大流就是在残量网络上s-t跑最大流+可行流。
最小流就是删去和S、T连边以及t-s后跑最大流,w(t-s)-最大流。
费用流基本是可行费用流,套板子即可。

这个算法真的,不太用动脑(?只要能用的,按题意模拟即可。(算是网络流中最好想的吧。。)

一些tips

  1. 如果求类似最大值最小,考虑和二分答案相结合,用最大流、可行流check等等

  2. 与源汇点、二分图有关三大套路:格子染色(用于仅与相邻有关的)、数学分奇偶性(包括二进制分组)、矩阵分横纵以点连线

  3. 拆点,每个状态为一个点或入边->出边,中间是限制条件。点数太大了考虑动态加点。

  4. 任意两边的交点都在顶点上的图称为平面图,如果我们把面作为点,相邻的边连线,可以得到对偶图,对于数据点过大的,平面图最小割=对偶图最短路。

  5. 分数规划,其实就是把分母乘过去,然后二分验证。

  6. 对于平方的处理:拆边,假设全一种情况,考虑增量;或根据2k+1来连。

  7. 对于:最多x个满足什么条件,可转化为至少总数-x个满足非这个条件

  8. 二分图上跑最大流的复杂度是 \(O(m\sqrt{n})\)

·如果不刻意卡,6e3-1e5的点数甚至都能过去!网络流就是要敢写!!!

网络流课件

模拟费用流

神秘算法,本质是贪心。大概就是如果数据范围比较大网络流跑不过去,且有一定特殊性质时可以使用。

如 [NOI2019] 序列

我们先考虑“自由流”,即这么流一定不会使答案更劣的流,流满它。然后考虑流其他流,注意模拟退流等等。用优先队列维护。

posted @ 2024-11-06 16:18  theWeeds'Defense  阅读(54)  评论(0)    收藏  举报