网络流

概述

网络流就是一张有向图,每一条边上有一个容量c(u,v)。
在整张图中有两个特殊的点:源点S和汇点T
然后有一个像水流一样的东西可以从S到T顺着有向边流过去,这个称之为
f(u,v)为一条边的流,其中f(u,v)<=c(u,v)
剩余容量\(c_f(u,v)=c(u,b)-f(u,v)\)

最大流

好吧好吧FF增广和EK算法似懂非懂,但这俩也不常用啊,那就偷工减料了 大体说说思路,术语不规范请见谅

最大流,字面意思,就是一张图的最大的流量。比如下面这张图的最大流为23:

image

求一张图的最大流有一个显著的思路就是从s开始随便找一条到t的路径增广,增加某些边的流量,也就是把这条路上的最小剩余容量变为0(这条路径增广后就相当于断开了)。然后不断重复这一操作就可以得到最终的最大流。

反向边:在不断增广的过程中会出现一些问题,比如下面这张图:

image

第一个图是我们一次增广后的结果,但是实际上u到v的这条变是不需要流过的,否则答案会被u到v的这条小边限制。那么建一条边(v,u),原边剩余容量减小,反向边流量增大。就相当于一个反悔操作,并没有断开,可以在另外的增广操作中把流回退回去。因为你增广是随意选的一条路,不一定是最优的选择,而反向边恰好保证了答案最优。

Dinic算法

会发现上面的思路复杂度不是很优。考虑在增广前先对 整张图做 BFS 分层,即根据结点 u 到源点 s 的距离 d(u) 把结点分成若干层.令经过 u 的流量只能流向下一层的结点 v,即删除 u 向层数标号相等或更小的结点的出边。然后在剩下的这张图里跑 DFS ,多路增广。

当前弧优化:可以理解为一条边已经被增广过,那么它就没有可能被增广第二次,那么每次从一个节点走的时候就不必再走那些已经被增广过的边。

总而言之它的复杂度是O(n^2m)的。所以一般最大流的题数据范围都很小。

Dinic
#include<bits/stdc++.h>
#define inf 1e18
#define int long long
using namespace std;
const int maxn=1e5+5;
int n, m, s, t, ans, vis[maxn], dep[maxn], cur[maxn];
int head[maxn], edgenum=1;
struct edge{
    int next;
    int to;
    int w;
}edge[maxn<<1];
void add(int from,int to,int w)
{
    edge[++edgenum].next=head[from];
    edge[edgenum].to=to;
    edge[edgenum].w=w;
    head[from]=edgenum;
}
bool bfs(int s,int t)
{
	memset(dep, 0, sizeof(dep));
	queue<int> q;
	q.push(s);
	dep[s]=1;
	cur[s]=head[s];
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		for(int i=head[u];i;i=edge[i].next)
		{
			int v=edge[i].to;
			if(edge[i].w>0&&dep[v]==0)
			{
				dep[v]=dep[u]+1;
				cur[v]=head[v];
				if(v==t) return 1;
				q.push(v);
			}
		}
	}
	return 0;
}
int dfs(int s,int flow)
{
	if(s==t) return flow;
	int res=0;
	for(int i=cur[s];i&&flow;i=edge[i].next)
	{
		cur[s]=i;
		int v=edge[i].to;
		if(edge[i].w>0&&dep[v]==dep[s]+1)
		{
			int k=dfs(v, min(flow, edge[i].w));
			if(k==0) dep[v]=inf;
			edge[i].w-=k;
			edge[i^1].w+=k;
			res+=k;
			flow-=k;
		}
	}
	return res;
}
int Dinic(int s,int t)
{
	int ans=0;
	while(bfs(s, t))
	{
		ans+=dfs(s, inf);
	}
	return ans;
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin>>n>>m>>s>>t;
	for(int i=1;i<=m;i++)
	{
		int u, v, w;
		cin>>u>>v>>w;
		add(u, v, w);
		add(v, u, 0);
	}
	cout<<Dinic(s, t);	
	return 0;
}

但其实网络流最难的是建边啊啊啊111

练1:[SCOI2007] 蜥蜴

建边:建立源点S,将每个石柱拆为上下两点,其中边的容量为石柱高度。从S向每只蜥蜴所在石柱上端连一条容量为1的边。互相可达的石柱上下连边。能到达外界的石柱下端向汇点T连容量为inf的边。然后跑最大流。

其中拆点操作就是为了将所有到达石柱的蜥蜴汇聚到一起,共同消耗石柱高度。从S向蜥蜴连1的边代表只有一只蜥蜴从石柱出发。总结就是这种可以消耗某种物品的状物可以看作流,流的最初大小(比如一只蜥蜴)可以从起始点与S连边的大小固定。拆点便于汇聚经过边的流。

练2:[SDOI2015] 星际战争

转化:会发现答案有单调性。先来一个二分。二分这个最少时间。然后就能知道每个激光器攻击的大小,对于一个激光器有多个敌人可以攻击,那么就是很显然的网络流了。

建边:建立源点S,s向每个激光器连一条边,边的容量为二分时间tim*b,然后每个激光器向可以攻击的敌人连一条容量为inf的边。每个敌人向汇点t连一条容量为a的边。

会发现只需知道激光器的攻击总量即可,这样就能任意分配给敌人。(因为时间可以是小数)check判断图的最大流是否等于a之和就行了。

练3:士兵占领

一开始的思路是二分人数,格子连对应行列,但这样没法满足每个士兵(相当于流)都一定要走行and列。

转化:既然没法二分人数,就考虑怎么直接算出最少人数。人数最少,就意味着空位越多。那么我们先给每个格子放置士兵,考虑在满足条件下删除人数的最大值。障碍格不放。

建边:S向每行连接m-l-障碍数的边,每行向每列连接容量为1的的边(不含障碍格),每列向T连接n-r-障碍数的边。

行放了列就要放,就意味着它们是前后关系不是并列关系,因此行与列连边。多想想正难则反。

练4:[HNOI2007] 紧急疏散(EVACUATE)

转化:二分答案。

建边:考虑把门按照时间拆点。S向每个空位连1的边,每个空位到达每个门的时间为x,每个空位向每个门的第x个连1的边。每个门向T连inf的边,每个时刻的门向后面的连边。

总结:把门按时间拆开不是很好想。主要是因为人可以在格子处原地等待,因此时间尤为重要。


下面是dzb大佬的技巧整理,言简意赅,精辟到位。在此引用一下,拜谢OOOOOOOOOrz

最大流建模技巧

  • 基础技巧
    多源多汇时建立超级源点和超级汇点。
  • 拆点
    对于权值在点上的题目,将每个点拆成入点和出点。
    如果整张图有时间上的限制,考虑把每个点按照时间拆点。也就是把一个点拆成在不同时刻的点。
    如果每一个点有不同的状态,将每一个状态拆成一个点。(其实时间也可以理解成一个点的不同状态)
  • 网格类问题
    对于网格问题,如果行列上有限制,考虑将一个点 x 到 y 连边。
    对于网格问题,如果点之间会互相影响,考虑黑白染色。然后从黑点向白点连边(准确来讲并不只限制于黑白两种颜色,其实可以是红黄蓝绿等等,染色的主要目的在于让原先颜色间互相影响变为人为规定的单向影响,方便网络流连边)。

以及一些转化类技巧:常见的二分、正难则反(常用于求最小值)。


最小割

你只需要知道 最小割就是割掉一些边,在S与T不连通的条件下,使得边集容量和最小。

最小割=最大流。

练1:[国家集训队] happiness

建边:S为选文科,T为选理科,这样子给每个同学向S和T各连一条喜悦值的边。再考虑额外喜悦值怎么处理,拿文科举例子,考虑给相邻的同学建一个虚点,S向虚点连接对应额外喜悦值,虚点向这两个同学分别连inf的边。跑最小割即可。输出所有数之和减去最小割。

会发现inf的边是绝对不能割的,那这样就保证只会删那些我们不想选的边。首先从每个同学考虑,它肯定得割一条连向S或T的边,也就是只选理/文。虚点连接的两点若同选了理/文,就不用割虚点的边了,否则就要消耗掉这个额外喜悦值。

练2:[TJOI2015] 线性代数

好吧自己想没有想到所谓的“选择关系”,其实就差了一步。

建边:S向点b[i][j]连b[i][j]的边,点b[i][j]向点c[i]和点c[j]连inf的边,点c[i]和点c[j]向T连对应的值的边(对不起说得有点粗糙)。

对于每一个B矩阵中的元素b[i][j],如果不要他(即a[i]和a[j]中有至少一个为0),就损失 b[i][j],否则就必须损失c[i]和c[j](因为此时a[i]和a[j]皆为1)。c向T的n条连边的割与不割就相当于a每一位是1还是0。(0是不割,1是割)。根据这个选择关系来建图。

练3,练4,练5离家出走了。。。

切糕模型看一下:https://www.luogu.com.cn/problem/P3227

练6:[AHOI2009] 最小割

最小割的可行边和必须边。这个题倒是不用想怎么建边了。
首先明确的一点是可行边包含必须边。
先跑一遍最小割。把边权为0的边视作断掉。这样剩下的残量网络S一定到不了T。
对于边(u,v),满足可行边条件:

  • 满流
    这不用多说,如果你都不是满流,你选择这条边不就是徒增花费么。
  • 不存在另外u到v的路径
    显然可以理解。由于反边,这张图多了v到u的路径,如果还存在其他u到v的路径,那么u和v就构成了一个强联通分量。跑个tarjan判断下即可。

对于边(u,v),满足必须边条件:

  • 满流
    这不用多说,跟上面一样。
  • 存在s到u的路径,v到t的路径
    显然可以理解。路径的判断也跟上面一样。

练7:[CQOI2016] 不同的最小割

最小割树。巧妙!

先来解决一个微乎其微的问题:双向边咋处理?双向边只需像原来一样建正反两条边,反边的权值和正边一样。

考虑先任意选两个点s和t求最小割res,这样子就分成两个点集。s和t之间连一条res的边。注意到如果从两个点集中任意各选一个点,那这两点最小割的答案就有可能是res。那就分别对两个点集递归处理(选点跑最小割加边),直到只剩一个点为止。那么n个点就能形成一个树,树上两点之间路径的最小值就是两点的最小割啦。拿set存一下边直接输出set大小即可。

找点集直接暴力跳dfs即可,复杂度过得去。


然后我们再次引用一下dzb大佬的博客吧,O(∩_∩)O拜谢!

  • 如果出现类似二者选其一的模型,考虑将两者分别建立源点和汇点,然后求出最小割就可以将所有元素分成两部分。
  • 在上一条的基础上,如果两个元素都选 / 都放在一边才能产生贡献,我们称这种结构为与结构。此时可以新建一个节点,从源(汇)点向这个点连容量为贡献的边,然后从这个点向那两个元素连容量为 ∞ 的边。这样跑最小割时就可以满足刚才说到的条件。
  • 求解一条边能否在最小割和是否一定在最小割中的问题时,考虑在最终的残量网络上用 tarjan 求解。

费用流

就是给每条边再加了个单位流量花费,每条边的花费就是经过它的流f(u,v)×w(u,v)。费用流一般分为最小费用最大流和最大费用最大流,即满足最大流的条件下使得总费用最小/最大。

SPFA+Dinic

当然还有SPFA+EK的,但个人还是比较喜欢用Dinic。

最小费用最大流问题。首先费用是在跑最大流时一并算出来的。考虑增加了费用的限制后对增广的影响。对于多条长度相同的从S到T路径,我们肯定是希望先走总花费最小的那条路,如果把费用移到边权上,那就相当于找最短路。反边也需要将花费反一反,cost变成-cost,这样跑dfs才能把花费也退回去。因为有负边,所以采用SPFA跑最短路。剩下的就和最大流没什么区别了。

哦再提一嘴最大费用最大流问题。这个只需要加边时改成加花费的相反数即可,同样跑最小费用最大流,答案取相反数。

点击查看代码
#include<bits/stdc++.h>
#define inf 0x3f3f3f3f3f3f3f3f
#define int long long
using namespace std;
const int maxn=1e5+5;
int n, m, s, t, ans, vis[maxn], dep[maxn], cur[maxn], mincost, dis[maxn];
int head[maxn], edgenum=1;
struct edge{
    int next;
    int to;
    int w;
    int cost;
}edge[maxn<<1];
void add(int from,int to,int w,int c)
{
    edge[++edgenum].next=head[from];
    edge[edgenum].to=to;
    edge[edgenum].w=w;
    edge[edgenum].cost=c;
    head[from]=edgenum;
}
bool spfa(int s,int t)
{
	memset(dis, 0x3f, sizeof(dis));
	memset(dep, 0, sizeof(dep));
	memset(vis, 0, sizeof(vis));
	queue<int> q;
	q.push(s);
	dis[s]=0;
	vis[s]=1;
	cur[s]=head[s];
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		vis[u]=0;
		for(int i=head[u];i;i=edge[i].next)
		{
			int v=edge[i].to;
			if(edge[i].w>0&&dis[v]>dis[u]+edge[i].cost)
			{
				dis[v]=dis[u]+edge[i].cost;
				cur[v]=head[v];
				if(!vis[v])
				{
					q.push(v);
					vis[v]=1;
				}
			}
		}
	}
	return dis[t]^inf;
}
int dfs(int s,int flow)
{
	if(s==t) return flow;
	vis[s]=1;
	int res=0;
	for(int i=cur[s];i&&flow;i=edge[i].next)
	{
		cur[s]=i;
		int v=edge[i].to;
		if(!vis[v]&&edge[i].w>0&&dis[v]==dis[s]+edge[i].cost)
		{
			int k=dfs(v, min(flow, edge[i].w));
			if(k==0) dis[v]=inf;
			edge[i].w-=k;
			edge[i^1].w+=k;
			res+=k;
			flow-=k;
			mincost+=k*edge[i].cost;
		}
	}
	vis[s]=0;
	return res;
}
void Dinic(int s,int t)
{
	ans=0;
	while(spfa(s, t))
	{
		ans+=dfs(s, inf);
	}
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin>>n>>m>>s>>t;
	for(int i=1;i<=m;i++)
	{
		int u, v, w, c;
		cin>>u>>v>>w>>c;
		add(u, v, w, c);
		add(v, u, 0, -c);
	}
	Dinic(s, t);
	cout<<ans<<" "<<mincost;	
	return 0;
}

练1:[SDOI2016] 数字配对

转化:保证当前可行流最大费用的情况下,最大费用具有单调性,也就是可以二分。

建边:二分最大流也就是给最大流一个限制,因此将源点拆点,连容量为mid的边。然后可以像二分图一样将每个点拆为入点和出点,S向入点连b的边,出点向T连b的边,两两可连的点之间入点出点相互连。

练2:[SCOI2007]修车

建边:建立S与每辆车连1的边。注意到人是可以等待的,那么我们直接想到那个拆门的题。这个题也是同样道理,给每个师傅拆点。将m个师傅拆成nm个点,第i个师傅的第j个点表示第i个师傅修的倒数第j辆车。为啥是倒数呢?考虑一个t[i][j]产生的影响。显然它只会影响后面等的人的时间,那么就需要知道它是倒数第几个修的。那么每辆车i与每个师傅j的n个点都连一遍,容量为t[i][j]倒数k辆车。最后拆的n*m个点与T连1的边即可。

练3:[NOI2012] 美食节

转化:注意到此题和上一道几乎是完全一样的,唯一的区别是选同一个菜/车的人变多了,数据范围变大了。那么只需将上面的建边照搬下来,然后将S与每道菜的连边容量改为p[i],m个厨师拆成msum个点(sum为p之和)。这样你就可以获得60pts。由题解可知msum个点不一定都能用到,考虑边跑最大流边加新拆的点。对于第j个厨师记录它最新拆到第几个点了,记为lst[j],判断lst[j]是否被增广后直接暴力加点就行。

点击查看代码
void Dinic(int s,int t)
{
	ans=0;
	while(spfa(s, t))
	{
		ans+=dfs(s, inf);
		for(int j=1;j<=m;j++)
		{
			for(int k=1;k<sum;k++)
			{
				if(fl[n+(j-1)*sum+lst[j]])
				{
					lst[j]++;
					add(n+(j-1)*sum+lst[j], t, 1, 0);
					for(int i=1;i<=n;i++)
					{
						add(i, n+(j-1)*sum+lst[j], 1, T[i][j]*lst[j]);
					}
				}
			}
		}
	}
}

但动态加点需要注意的是:一定要满足先加的点比后面点更优才去加!

最大权闭合子图

一个很神奇的思路。一开始觉得这玩意没用,后来才发现挺重要的。

  • Q:什么是最大权闭合子图?

先解释一下有向图的闭合图:闭合图内任意点的任意后继也一定还在闭合图中。
最大权闭合子图,顾名思义就是有向图中点权之和最大的闭合图。

  • Q:怎么做呢?

将所有点根据正负性分成两部分。
正点权:S向它连 它的点权 的边。
负点权:它向T连 它的点权的相反数 的边。

剩下的就根据原来有向图中的连边关系连边即可,边权为inf。

跑最小割,所有正点权的和减去最小割就是答案!

左边是网络流的连边,右边是原来的有向图。
image

解决这种问题的套路是,找出题目中有依赖关系的点/值(只要有固定的数值就行,往往会有负数),根据依赖关系互相连边(比如选a的前提是选b,或者选了a就一定选了b,那么a向b连边),就完了。

心情不好不写练习了。

上下界网络流

半个月前学的东西,现在忘光了。。。

上下界,顾名思义,就是每条边有一个流量限制[a,b],然后在这个基础上的延伸许多问题。

无源汇上下界网络流

没有源汇点。考虑正常的网络流下界都为0,那么就想到先让每条边的流量都为a,然后在这个基础上增加流量满足每个点流量守恒(就相当于在差值网络上跑)。每条边已有初始流量,它的容量即为a-b,记m为每个点的流入量与流出量的差值,那么:

  • m>0
    新建源点S,S向这个点连一条容量为m的边。
  • m<=0
    新建汇点T,这个点向T连一条容量为-m的边。

然后从S到T跑最大流即可。

理解一下,拿m>0举例子,这个点一开始流入量比流出量就要大,希望最终能守恒,那么就相当于在差值网络上流入量要比流出量小,添加一条来自源点的边分担了一部分流入量,那么实际上的流入量就一定比流出量小。

是否有可行流就判断一下S和T的连边是否都满流即可。

有源汇上下界网络流

考虑从t向s(原本的源汇点)连一条边,那么就变成了无源汇的情况。此时它的可行流就是t到s边的流量。

最大流

这个只需跑完可行流之后再从s到t跑一遍最大流,这个最大流加上可行流就是最终的流量。

理解一下就是s和t的流量可以不守恒。

最小流

跟上面差不多。跑完可行流后,交换s和t,跑最大流即可。就相当于退流,从t到s在反向边上反着跑最大流。

最小费用可行流

把跑可行流时的最大流换成费用流即可。

平面图&对偶图

关于平面图到对偶图的转化

对偶图对于平面图最小割的求解

对偶图上的每一条边刚好横跨所割的平面图上的边,所以形成了所有割的情况。优化了复杂度,常用于网格一类问题。

练习

练1:[清华集训 2017] 无限之环

神秘题。

黑白染色!!! 然后将白格钦定为源点,黑格钦定为汇点。黑白格相邻之处连边,然后根据水管方向格内连边。就会发现满流就等价于不会漏。至于旋转操作,只需在每个格子里加一些而外边(下图的蓝边):

image

这些边的边权就是旋转消耗的次数。建完图后跑费用流即可。

代码比较长,但很好写。

网格类题,只要提到相邻格子(形成图形啊,联通啊,满足某种条件啊),想想染色!

练2:[NOI2010] 海拔

先放证明吧,写得很清楚:

下文「连通块」指「内部点高度相同的极大四连通块」。

对于一个方案,若存在高度小于 0 的点,则令所有这些点的高度为 0 更优,因为这些点之间的贡献减小为 0,而其他点的高度无论在调整前后都大于等于这些点的,所以这些点与其他点的贡献一定减小了。同理若存在高度大于 1 的点,则令所有这些点的高度为 1 更优。

现在每个点高度在 [0,1] 区间内,假设其不全为 0 或 1。对于所有不包含西北角的连通块,找到高度最小的一个(称为 S),这个连通块一定存在且不包含东南角。

  • 若 S 不与西北角所在的连通块相邻,则将其高度调整为与其相邻的点中的最小高度一定更优,因为 S 内点之间的贡献不变为 0,而相邻点的高度无论在调整前后都大于等于这些点的,所以 S 内的点与其相邻>点的贡献一定减小了。
  • 若 S 与西北角所在的连通块相邻,则找到与之相邻的点中高度不为 0 且最小的(假设高度为 h),S 的高度一定在 0 与 h 之间。如果将 S 的高度看作自变量 x∈[0,h],那么令 x=0 或 x=h 一定不更劣,因为 S 内点之间的贡献不变为 0,而 S 内的点与其相邻点的贡献是线性函数,最小值一定能在两端取到。
    无论是哪种情况,都存在一种不更劣的、连通块更少的方案。不断进行调整,最终每个点的高度一定会停于 > 0 或 1。从而,一定存在一组最优方案使得点的高度都为 0 或 1。

嗯。。那个线性函数自己随便推推就出来了,就是个一次函数,所以取0/1最优。又根据第一个黑点后面的证明,它最终的图一定是个左上角有一片海拔高度为0,右下角有一片海拔高度为1的样子。

然后就是要找到那个从0→1的断崖,就相当于求这张图的最小割。但是你能显而易见的发现复杂度是过不去的,因此你需要学习平面图到对偶图的转化(详见上面)。然后跑个最短路就可以了。

练3:[NOI2008] 志愿者招募

嗯这个题的建图想出来一半。当时一直在思考如何回收流量,然后就卡住了。

建图:把n天拆点看成一条线,每一天向下一天连边,下界为ai。如果有志愿者在[l,r]之间工作,就从r向l连一条无限制的,费用为ci的边。然后跑无源汇最小费用流即可。

练4:[JSOI2009] 球队收益 / 球队预算

奇妙拆边题。不难想到对于每场比赛,建一个虚点连向两支队伍,S连虚点,(容量都为1)每支队伍连T(容量为最多赢的场数)。最初默认每支队伍都是失败的,这样子每走一条边就相当于让这个队伍赢,增加了赢的收益。但会发现这个收益是建立在知道前面赢了多少场的情况才能算出来,因此我们改成从每支队伍向t连 最多赢的场数 条边,费用依次递增,容量为1。最后记得加上初始代价。

练5:[CQOI2017] 老C的方块

诡异染色题。首先能想到这个题大概是个最小割,但这个讨厌的形状很难建边,毕竟也不能跑一堆dfs。考虑找到这些形状的通性()。假如我们用四个颜色蓝绿红黄对这四个图形进行染色的话,就会发现,一定会存在这样一条路径:黄—蓝—特殊边—绿—红。
image
然后整张图都可以这样染色下来:
image

相邻颜色连边跑最大流就完了。


“希望我们都能像对方一样勇敢。”

posted @ 2026-07-01 08:06  zhouyiran2011  阅读(8)  评论(0)    收藏  举报