#edu3030. 【教程】网络流入门
【教程】网络流入门
网络流基本概念
网络指的是一个带边权的有向图, 一张有向图,每条边 都有一个边权 ,是这条边流量的上限,称为容量。
图中有两个特殊的点,入度为 的点 和出度为 的点 ,分别称为源点和汇点。
流
设 是定义在节点二元组 上的实数函数,满足:
. 容量限制:对于每条边,流经该边的流量不得超过该边的容量,即 。
. 斜对称性:每条边的流量与其相反边的流量之和为 ,即 。
. 流守恒性:除源点和汇点外任意点,流入此点的流量和等于流出此点的流量和,也就是中间节点不会存储,即 $\forall x \ne S,x \ne T,\sum_{(u,x) \in E}f(u,x)=\sum_{(x,v)\in E}f(x,v)$。
特别地,若 ,则
称为网络的流函数。对于边 称边的流量, 为边的剩余容量,很多教程里也称为残量。
在任意时刻,网络中所有节点以及剩余容量大于 的边构成的子图被称为“残量网络”。
称为整个网络的流量,使得这个值最大,就是最大流问题,本质上最大流问题是 线性规划问题,即给定若干约束条件,使得目标函数值最大。
网络流可以形象地描述为:在不超过容量限制的前提下,“水流”从源点源源不断地产生,流经整个网络,中间节点(除源点和汇点外)不存储流,最终全部流入汇点。
求最大流
求如下图最大流?
网络流 图-1
基于贪心的错误做法
(不能保证每次都能找到最大流)
贪心:
在残量网络上,如果能从源点寻找到汇点的一条增广路(简单路径),循环执行如下操作直到增广路径不存在:
(1)寻找一条增广路;(2)找到这条路径上的瓶颈,也就是最小流量,记为 ; (3)按 流量从源点流入汇点, ,修改路径上的边。
对于一条边后面使用 进行描述,剩余量,也就是残量 。
第一条路径: ,瓶颈是 ,修改路径上边的流量,此时 ,相当于这条边去掉;
第二条路径: ,瓶颈是 ,修改路径上边的流量,此时 满流(阻塞),相当于这条边去掉;
第三条路径: ,整个路径剩余流量瓶颈是 ,给这条路径上各边 ,如下图:
网络流 图-2
之后没有增广路径,网络流是 , 就是这个网络的最大流,但是这种算法并不是任何情况下都能求得最大流。同样是这张图,寻找增广路顺序不同,答案就会有差异。如果寻找增广路顺序如下,网络流是 :
第一条路径: ,流量是 ;
第二条路径: ,流量是 ;
网络流 图-3
之后没有增广路径,此时网络流总和是 ,不是最大流,也就是这种贪心算法不一定找到的是最大流(只是阻塞流)。
简单贪心算法,存在一个明显的缺陷,一旦找到一条路径,不能反悔,这样就会导致求到的网络流不一定是最大的,Ford-Fulkerson 算法通过添加反向边,撤销之前走的路径,保证求到最大流。
Ford-Fulkerson 算法
在残量网络上,循环执行如下操作直到增广路径不存在:
(1)寻找一条增广路;
(2)找到这条路径上的瓶颈,也就是最小流量,记为 ;
(3)按 流量从源点流入汇点, ,修改路径上的边。
(4)在这条路径上所有边添加反向路径,流量是x。
Ford-Fulkerson方法的正确性依赖于这个定理:当残存网络中不存在一条从s到t的增广路径,那么该图已经达到最大流。
这种算法,存在一种可能,一次只能找到流量为 的增广路,如下图:
网络流 图-4
此时,需要循环最大流次去寻找增广路,时间复杂度就是 ,算法效率较低,不过这种算法证明了通过添加反向边,撤销之前的增广路,一定可以得到最大流,后面 等算法都是这种算法的优化。
Edmonds-Karp算法
Ford-Fulkerson 算法时间复杂度依赖于最大流,Edmonds-Karp 证明了通过寻找最短增广路(将图看作无权图,直接BFS),时间复杂不再依赖于最大流,Ford-Fulkson 可以通过任意方法寻找增广路,可以将这种算法看着 FF 的一种特例。
一点重要细节:
与 是成对出现的, , 。 若 是边的编号,那么反向边编号就是 ,不过边的编号要从 开始,那初始 。
参考代码如下:
long long ek(int s,int t)
{
long long maxflow=0;
while(bfs(s,t))
//bfs寻找最短增广路,如果找到返回true,同时记录每个点对应前驱的边号
{
int d=1e9,v=t;
while(v!=s)
{
int i=pre[v];
d=min(d,edge[i].cap-edge[i].flow);
v=edge[i^1].to;
}
maxflow+=d;
v=t;
while(v!=s)
{
int i=pre[v];
edge[i].flow+=d;
edge[i^1].flow-=d;
v=edge[i^1].to;
}
}
return maxflow;
}
算法时间复杂度:
每轮循环需要 时间寻找最短路,Edmonds-Karp证明了最多需要 轮,那么时间复杂度就是 , 是点数, 是边数。 实际应用则达不到这个上界,效率较高,一般能够处理 规模的网络。
P3376【模板】网络最大流 完整参考代码:点击
Dinic算法
Edmons-Karp 每轮可能遍历遍历整个残量网络,但只能找到 条增广路,还有进一步优化的空间。
在残量网络上通过 建分层图,在分层图上寻找的增广路确保是最短路的增广路,然后通过 沿着层数递增 且还有剩余流量 的方向寻找增广路,回溯时增流。一次 可以实现多次增流,这是Dinic 算法巧妙之处。
在残量网络中, 判断从源点到汇点是否存在增广路,同时维护点的深度,搜索树边 构成了“分层图”。
| 过程 |
|---|
| 算法不断重复以下步骤,直到残量网络找不到增广路(S不能到T): |
| a.在残量网络上,通过 建分层图,如果 不能到 ,跳出循环。 |
| b.在层次图上 ,沿着层数增 且 的方向找增广路,回溯时增流。出边有多条,此处多路增广,各点满足总流入大于等于总流出。若点 无法流出,即流量为 ,点 x 在后续 DFS 中就不可能再次被访问,可以剪枝去掉,这就是弧优化。 |
Dinic 算法有两个优化:
- 多路增广:每次找到一条增广路的时候,如果残余流量没有用完怎么办呢?我们可以利用残余部分流量,再找出一条增广路。这样就可以在一次 DFS 中找出多条增广路,大大提高了算法的效率。
- 当前弧优化:如果一条边已经被增广过,那么它就没有可能被增广第二次。那么,我们下一次进行增广的时候,就可以不必再走那些已经被增广过的边。
算法时间复杂度为 ,实际使用中达不到这个上界,是网络流中的高效算法之一,一般可以处理 规模的网络。
特别地, 在解决二分图最大匹配的时间复杂度为 ,实际使用效率更高。
核心代码:
bool bfs()
{
memset(d,0,sizeof(d));
queue<int>que;
que.push(s),d[s]=1;
while(que.size())
{
int x=que.front();que.pop();
for(int i=head[x];i;i=edge[i].next)
{
int y=edge[i].to,w=edge[i].flow; //残量大于0
if(d[y]==0&&w>0)
{
que.push(y);
d[y]=d[x]+1;
if(y==t)return true;
}
}
}
return false;
}
int dfs(int x,int flow)
{
if(x==t)return flow;
int rest=flow;
for(int i=head[x];i&&rest;i=edge[i].next) //多条出边,多路增广
{
int y=edge[i].to,w=edge[i].flow;
if(d[y]==d[x]+1&&w>0)
{
int k=dfs(y,min(rest,w));
if(k==0)d[y]=0; //剪枝 去掉增广过的点
rest-=k;
edge[i].flow-=k; //残量减少
edge[i^1].flow+=k; //反向边残量增加
}
}
return flow-rest;
}
int dinic()
{
int maxflow=0,flow;
while(bfs())maxflow+=dfs(s,inf);
return maxflow;
}
完整代码:点击
弧优化:
我们在按顺序 dfs 时,先被遍历到的边肯定是已经增广过了(或者已经确定无法继续增广了),那么这条边就可以视为“废边”
那么下次我们再到达该节点时,就可以直接无视掉所有废边,只走还有用的边,也就是说,每次 dfs 结束后,下次 dfs 可以更省时间。
只需要每次 之前,复制一次 到 数组,在 中使用 保留没有被增广到的出边。
中:
int dinic()
{
int maxflow=0,flow;
while(bfs())
{
memcpy(cur,head,sizeof(head));
maxflow+=dfs(s,inf);
}
return maxflow;
}
中:
int dfs(int x,int flow)
{
if(x==t)return flow;
int rest=flow;
for(int i=cur[x];i&&rest;i=edge[i].next) //多条出边,多路增广
{
cur[x]=i; //当前弧优化,增广过的边不会第二次增广 去掉增广过的边
int y=edge[i].to,w=edge[i].flow;
if(d[y]==d[x]+1&&w>0)
{
int k=dfs(y,min(rest,w));
if(k==0)d[y]=0; //剪枝 去掉增广过的点
rest-=k;
edge[i].flow-=k; //残量减少
edge[i^1].flow+=k; //反向边残量增加 正边和反边是成对存储的
}
}
return flow-rest;
}
带优化完整代码:点击
ISAP
Dinic每次寻找增广路,都要BFS重新分层,能否进一步优化?
ISAP算法与Dinic相似,先分层,然后在分层图上DFS寻找增广路,DFS的同时修改节点深度。
算法过程:
- 1.从 T 开始 BFS 计算各点深度
- 2.类似于 dinic,从 S 开始 DFS,按照深度增加 -1 寻找增广路,进行增流。某点将之前流入的量全部跑完,意味着后续还有出边,之前的入边断开了,此时直接返回此点的所有流,否则,此点出边必有断开的,将此点深度++,对于数量--,如果出现断层,修改 S 点 d[s]=n+1。
- 3.循环进行步骤2,直到 d[s]>n。
void bfs()
{
memset(d,0,sizeof(d));
queue<int>que;
que.push(t),d[t]=1;
num[d[t]]++;
while(que.size())
{
int x=que.front();que.pop();
for(int i=head[x];i;i=edge[i].next)
{
int y=edge[i].to,w=edge[i].flow;
if(d[y]==0)
{
d[y]=d[x]+1;
num[d[y]]++;
que.push(y);
}
}
}
}
int dfs(int x,int flow)
{
if(x==t)return flow;
int rest=flow;
for(int i=cur[x];i&&rest;i=edge[i].next)
{
cur[x]=i; //当前弧优化
int y=edge[i].to,w=edge[i].flow;
if(d[x]==d[y]+1&&w>0)
{
int k=dfs(y,min(rest,w));
rest-=k;
edge[i].flow-=k;
edge[i^1].flow+=k;
}
if(rest==0)return flow;
}
//否则 当前点x之后还有剩余,出边的边残量为0
num[d[x]]--;
if(num[d[x]]==0)d[s]=n+1; //出现断层 GAP优化
d[x]++;
num[d[x]]++;
return flow-rest;
}
int isap()
{
int maxflow=0;
bfs();
while(d[s]<=n)
{
memcpy(cur,head,sizeof(head));
maxflow+=dfs(s,inf);
}
return maxflow;
}
参考代码:点击
费用流
给定一个网络 ,每条边 除了有容量限制 ,还有一个给定的“单位费用” 。
当 的流量为 ,需要花费 .
单位费用也满足写对称性,即 。
该网络总花费最小的最大流称为“最小费用最大流”,总花费最大的最大流被称为“最大费用最大流”,合称“费用流”模型。
注意:费用流的前提是最大流,然后才考虑费用的最值。
求最大流的所有增广路算法(EK,Dinic,ISAP),都是把“用BFS 寻找最短增广路”改为“用 SPFA 寻找一条单位费用之和最小的增广路”就可以求出最小费用最大流。(将 当做边权,在残量网络上求最短路,反边的费用应该设为 )
核心代码:
int dfs(int x,int flow)
{
if(x==t)return flow;
vis[x]=1;
int rest=flow;
for(int i=cur[x];i&&rest;i=edge[i].next)
{
cur[x]=i;
int y=edge[i].to;
if(vis[y]==0&&edge[i].flow>0&&dist[y]==dist[x]+edge[i].cost)
{
int k=dfs(y,min(rest,edge[i].flow));
rest-=k;
edge[i].flow-=k;
edge[i^1].flow+=k;
mincost+=edge[i].cost*k;
}
}
vis[x]=0;
return flow-rest;
}
void dinic()
{
while(spfa())
{
int flow;
memcpy(cur,head,sizeof(head));
maxflow+=dfs(s,inf);
}
}
完整代码:点击
学习完毕
{{ select(1) }}
- YES
- NO