网络最大流 学习笔记&详解
Genshin_ZFYX · · 算法·理论
网络流入门:网络最大流
友情提示:请学完图论基本知识再来阅读此篇文章。
这里只有 dfs 求解(Ford),EK 和 Dinic,没有什么神仙 HLPP,因为作者太菜了自己也不会(
文章也是智障作者学完之后口糊的(
只是感觉网络流这个东西挺有意思的 虽然又感觉用处不大。
前置知识
网络是指一张特殊的有向图,图中每条边
在一张网络
流的性质:
- 对于图
G 中每条边(u,v)\in E 的流量f(u,v) 表示从这条边流经了多少流量,其中0\le f(u,v)\le c(u,v) ,即从该条边流过的流量的不能超过该条边的容量。 - 对于图上任意一个节点
u ,设流入的流量为in_u ,流出的为out_u ,如果u 不是源汇点(中间点),那么in_u=out_u ;in_s=0 ,out_t=0 ,out_s=in_t 。即从s 流出的流量全部会流入t ,不会留在中间点。
最大流问题
最大流问题很容易理解:给你一张网络,求出最多可以从
我们可以提出一个设想:我们随便通过搜索找到一条还可以继续流出流量的路径,把这条路径上的流量增满,也就是路径上每条边都加上这条路径上最小的
这张图上,答案应为
1:
如果按我们上面那个贪心思路,我们可能会找到一条错误的增广方式
2(路就没画了,自己脑补吧QwQ):
所以这个贪心思路是错的。不过,我们可以改进它。
怎么办?我们看一下两种增广方式中每条边的流量
我们可以看见,我们在
下面就是最重要的一点:我们可以把它看作沿着
具体一点,我们先沿着
至此,我们有了一个新的贪心思路:对于原图上每条边都建一条反边,按原来的方式增广,每增广一条边
这是对的吗?答案是肯定的。如果要证明,我们则不得不延伸至最大流最小割定理。读者可以先确保消化掉了上面的知识再往下读。
最小割的定义:在一张网络上去掉一些边,使得所有点都被划分为了两个点集,源汇点位于不同的集合中。问去掉的边的容量之和最小是多少。最小割指去掉的边集,最小割的容量就是这个问题的答案。
最大流最小割定理:一张网络的最大流量
假设我们已经跑出了这张网络的最大流,那么这张图上就不存在增广路了,也就是说去掉满流的边图就被分割成了两部分。如果得到的流不是最大流,说明这张图上仍然存在增广路,去掉满流的边网络仍然连通。没有跑满最大流的网络中满流的边不能组成割。所以,一张网络的最大流量
残量网络指的是任意时刻原网络去除满流的边后剩余的网络。
假设在跑完最大流之后的残量网络中,源点
我们知道所有从
所以流出
证毕。
Ford–Fulkerson 增广
Ford-Fulkerson 增广是通过增广路求最大流一类算法的统称,这类算法通过每次在残量网络上寻找增广路并增广求解最大流。 Ford-Fulkerson 有很多种实现方式,例如最简单的 dfs 寻找增广路,bfs 寻找增广路(即 EK 算法)。
dfs 实现
dfs 的实现很简单,dfs 遍历整个图,如果找到一条增广路则立刻回溯停止搜索。
时间复杂度为
时间复杂度很不优秀,T 了(悲)。
核心代码:
const int INF=0x3f3f3f3f;
int n,m,s,t;
bool vis[205];//图上 dfs 标记数组
struct Edge{
int to,cap,nxt;//cap指的是这条边的剩余容量
}e[2000005];
int head[100005],tot=1;//链前存图,tot初始化为 1 是为了方便,第 i 条边的反向边则为 i^1
void add_edge(int u,int v,int cap)
{
e[++tot].to=v,e[tot].cap=cap,e[tot].nxt=head[u],head[u]=tot;
//建反边
e[++tot].to=u,e[tot].cap=0/*初始化为0是因为这条边是原图不存在的附加反向边*/,
e[tot].nxt=head[v],head[v]=tot;
}
int dfs(int x,int flow)//dfs求增广路
{
vis[x]=1;
if(x==t)return flow;//找到了,返回
for(int i=head[x];i;i=e[i].nxt)
{
int v=e[i].to;
if(vis[v])continue;
if(e[i].cap)//如果可以沿着这条边继续增广
{
int k=dfs(v,min(flow,e[i].cap));
if(k)//找到一条增广路
{
e[i].cap-=k;//减少剩余容量
e[i^1].cap+=k;//i的反向边i^1剩余容量增加k
return k;//返回
}
}
}
return 0;//无法继续增广,返回0
}
int max_flow()//求解最大流
{
int f,res=0;
do{
memset(vis,0,sizeof(vis));
f=dfs(s,INF);
res+=f;
}while(f);//重复寻找增广路直到找不到增广路
return res;
}
EK 算法
EK 算法全称 Edmonds-Karp 增广路算法。该算法和 dfs 十分类似:每次增广从源点
对于 bfs 搜索路径这回事,如果要保存经过的结点,那么可以用到一个技巧,记录每一个状态的上一个状态(注意这里是哪条边),最后再通过最后一个状态一直往前回溯到初始状态。
很明显比 dfs 效率要好很多,可以 A。
核心代码:
bool vis[205];//图上 bfs 标记数组
int pre[205];//记录前驱(哪条边)
int fl[205];//到节点 i 时的可以增广的最大流量
bool bfs()//bfs搜最短路
{
memset(vis,0,sizeof(vis));
queue<int>q;
q.push(s);vis[s]=1;fl[s]=INF;
while(!q.empty())
{
int x=q.front();q.pop();
for(int i=head[x];i;i=e[i].nxt)
{
if(e[i].cap)
{
int v=e[i].to;
if(vis[v])continue;
fl[v]=min(fl[x],e[i].cap);
pre[v]=i;//哪条边
q.push(v),vis[v]=1;
if(v==t)return 1;//已经找到一条路径了,返回
}
}
}
return 0;
}
int update()//更新边的剩余容量
{
int x=t;//当前点
while(x!=s)
{
int i=pre[x];
e[i].cap-=fl[t];
e[i^1].cap+=fl[t];//反向边
x=e[i^1].to;//这条边的反向边指向的节点就是前一个节点
}
return fl[t];
}
int max_flow()//求解最大流
{
int res=0;
while(bfs())res+=update();
return res;
}
重点:Dinic 算法
如果消化了上面的两个算法,那么 Dinic 就非常好理解。
Dinic 算法是容易实现的最大流算法中效率教高的,也是求解二分图最大匹配效率最高的算法,求解二分图最大匹配的时间复杂度为
Dinic 跟 EK 类似,也是寻找最短的一条增广路,不同的是 EK 是 bfs 找到一条最短路直接更新,而 Dinic 则是先 bfs 构造分层图,再在分层图上跑 dfs 找增广路。Dinic 一次可以找到多条增广路并更新。
具体一点,在图上 bfs 跑最短路,记到点
我们可以给 Dinic 加上一个当前弧优化,避免多次枚举一条边。具体实现看代码。
请注意,因为用的是 dfs,所以我们如果给点打标记,每 dfs 一次都要 memset
清空
现在的 Dinic 算法时间复杂度为
可以看见跑的飞快
核心代码:
bool bfs()//bfs跑分层图
{
for(int i=1;i<=n;i++)d[i]=-1;
queue<int>q;
q.push(s);
d[s]=0;
while(!q.empty())
{
int fr=q.front();q.pop();
now[fr]=head[fr];//当前弧优化初始化为这个点的第一条边
for(int i=head[fr];i;i=e[i].next)
{
if(e[i].cap>0&&d[e[i].v]==-1)//可以扩展
{
q.push(e[i].v);
now[e[i].v]=head[e[i].v];
d[e[i].v]=d[fr]+1;
if(e[i].v==t)return true;//因为对于d[i]>=d[t]的点i,必定不会到达,所以可以直接返回
}
}
}
return false;
}
int dfs(int x,int f)
{
if(x==t)return f;
int res=0;
for(int i=now[x];i&&f;i=e[i].next)
{
now[x]=i;//当前弧优化
if(e[i].cap>0&&d[e[i].v]>d[x])
{
int k=dfs(e[i].v,min(f,e[i].cap));
if(!k)d[e[i].v]=-INF;//优化2:如果从这个点无法找到增广路,让后面不在搜到这个点
else{
e[i].cap-=k;
e[i^1].cap+=k;
res+=k;f-=k;//这里剩余可以流出的流量要减少
}
}
}
return res;
}
int max_liu()
{
int ret=0;
while(bfs())
ret+=dfs(s,INF);
return ret;
}
其他算法
还有一些算法,就不详细介绍了。
ISAP 与 Dinic 类似,只是每次寻找完一条增广路之后不会再进行 dfs,而是对这条增广路上的点进行更新。
有一种高效的预流推进算法:HLPP 算法,时间复杂度
THE END
希望我的文章为大家带来了帮助。