概念
可以想象成一个运水系统:
从源点流出,通过管道运输,途径多个中转点,最后达到汇点
▪ 源点:无限的水
▪ 管道:运输的水量有限制
▪ 中转点:接收多少水,输出多少水,但是可以改变水流的方向
▪ 汇点:一般求可以接受的最大水量
如下面这个图:
最大流量是5:
1->3提供2流量
1->2->3提供2流量
1->2->4->3提供1流量
其中,1->2->3是一条增广路(从源点到汇点的路径,其上所有边的残余容量均大于0),扣除掉这条路提供的2流量之后,1->2的容量变成1,2->3的容量变成0,这时的容量称为残余容量,1->2->4->3也是一条增广路
EK算法
思想:不断用BFS寻找增广路并不断更新最大流量值,直到网络上不存在增广路为止
(FF算法是用DFS搜索,效率不高,一般不使用)
如果只是用BFS搜索增广路会出现一些问题:
如果先找到了1->2->3->4这条边,那么残余网络就会变成:
此时算出来的最大流是1,但其实这个图的最大流是走1->3->4和1->2->4,为2
所以可以通过引入反向边解决:
在建边的同时,在反方向建一条边权为0的边,当扣除正向边的流量的时候,对应的反向边要增加相等的流量
此时如果再选择1->2->3->4,正向边-1,反向边+1,得到
就找到另外一条增广路:1->3->2->4
此时,我们同时选择了2->3和3->2两条边,就可以认为,这两条边上的水流抵消了,
所以实际上选择的路径就是1->3->4和1->2->4
反向边在这里就类似一种撤销操作,给程序一种反悔的机会。
加入了反向边这种反悔机制后,我们就可以保证,当找不到增广路的时候,流到汇点的流量就是最大流
模板题:P3376 网络最大流
代码:
#include <bits/stdc++.h>
using namespace std;
int n,m,s,t,u,v;
long long w,ans,dis[520010];
int tot=1,vis[520010],pre[520010],head[520010],flag[2510][2510];
struct node
{
int to,net;
long long val;
} e[520010];
inline void add(int u,int v,long long w)
{
e[++tot].to=v;
e[tot].val=w;
e[tot].net=head[u];
head[u]=tot;
e[++tot].to=u;
e[tot].val=0;
e[tot].net=head[v];
head[v]=tot;
}
int bfs() //bfs寻找增广路
{
for(register int i=1; i<=n; i++) vis[i]=0;
queue<int> q;
q.push(s);
vis[s]=1;
dis[s]=2005020600;
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=head[x]; i; i=e[i].net)
{
if(e[i].val==0) continue; //我们只关心剩余流量>0的边
int v=e[i].to;
if(vis[v]==1) continue; //这一条增广路没有访问过
dis[v]=min(dis[x],e[i].val);
pre[v]=i; //记录前驱,方便修改边权
q.push(v);
vis[v]=1;
if(v==t) return 1; //找到了一条增广路
}
}
return 0;
}
void update() //更新所经过边的正向边权以及反向边权
{
int x=t;
while(x!=s)
{
int v=pre[x];
e[v].val-=dis[t];
e[v^1].val+=dis[t];
x=e[v^1].to;
}
ans+=dis[t]; //累加每一条增广路经的最小流量值
}
int main()
{
scanf("%d%d%d%d",&n,&m,&s,&t);
for(register int i=1; i<=m; i++)
{
scanf("%d%d%lld",&u,&v,&w);
if(flag[u][v]==0) //处理重边的操作(加上这个模板题就可以用Ek算法过了)
{
add(u,v,w);
flag[u][v]=tot;
}
else
{
e[flag[u][v]-1].val+=w;
}
}
while(bfs()!=0) //直到网络中不存在增广路
{
update();
}
printf("%lld",ans);
return 0;
}
Dinic算法
EK每次遍历只找到一条增广路,Dinic可以遍历一次找到多条
思想:分层图+DFS
- 分层
使用bfs,用 l e v [ m a x x ] lev[maxx] lev[maxx]数组从原点开始标记每个结点的层次 - dfs
比如:
就可以从S开始,每次我们向下一层次随便找一个点,直到到达T,然后从T一层一层往上返回,每返回一层就遍历这一层的所有点
路径:
① s->1->4->t,此时回溯的时候发现1还有路可以走
② s->1->5->t ,③ s->1->3->t
然后回溯到s,发现还有路
④ s->2->3->t
优化
1.定义一个变量vis,表示是否能到达t,若能到达t则可以再次dfs,否则重新bfs
2.定义一个变量used,用来表示这个点的流量用了多少
这样,在dfs时,在找到一条增广路时,不直接返回,而是改变used的值,如果used还没达到该点流量上限fow,可以在这个点继续找别的增广路
当前边优化
代码:
#include <bits/stdc++.h>
#define maxx 10010
#define inf 0x3f3f3f3f
typedef long long ll;
using namespace std;
int n,m,s,t,u,v;
ll w,ans;
int cnt=1;
int head[maxx],lev[maxx],cur[maxx];
struct node
{
int to,next;
ll w;
} e[maxx*3];
void add(int from,int to,int w)
{
e[++cnt].to=to;
e[cnt].w=w;
e[cnt].next=head[from];
head[from]=cnt;
e[++cnt].to=from;
e[cnt].w=0;
e[cnt].next=head[to];
head[to]=cnt;
}
//在残量网络中构造分层图
int bfs()
{
memset(lev,-1,sizeof lev);
lev[s]=0;
memcpy(cur,head,sizeof head);
queue<int> q;
q.push(s);
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=head[x]; i; i=e[i].next)
{
int to=e[i].to;
int ww=e[i].w;
if(ww>0 && lev[to]==-1) //残量>0,且没有被遍历过
{
lev[to]=lev[x]+1; //层次+1
q.push(to);
}
}
}
return lev[t]!=-1; //汇点一定遍历到,遍历不到说明搜索结束
}
int dfs(int x,int flow)
{
if(x==t)
return flow;
int rmn=flow;
for(int i=cur[x]; i&&rmn; i=e[i].next)
{
cur[x]=i; //优化****************************************************
int to=e[i].to;
int w=e[i].w;
if(w>0 && lev[to]==lev[x]+1) //往上一层增广
{
int c=dfs(to,min(w,rmn));// 尽可能多地传递流量,但必须在范围之内
rmn-=c;
e[i].w-=c; //更新残余容量
e[i^1].w+=c; //最后一位取反
}
}
return flow-rmn;
}
int main()
{
cin>>n>>m>>s>>t;
for(int i=0; i<m; i++)
{
cin>>u>>v>>w;
add(u,v,w);
}
while(bfs())
{
ans+=dfs(s,inf);
}
cout<<ans<<endl;
return 0;
}
(n为点数,m为边数)
ISAP算法
优化dinic,只跑一次bfs
参考这里
二分图匹配
最大流最小割
割:去掉一些边,把本来的图分为不连通两部分,代价最小,割的大小是这些边的容量之和
最大流等于最小割
最小费用最大流
Minimum Cost Maximum Flow,MCMF
现在的网络上增加了一个属性:单位费用,一条边上的费用=流量×单位费用。
我们知道,网络最大流往往可以用多种不同的方式达到,所以现在要求:在保持流最大的同时,找到总费用最少的一种。
思路:每次增广费用最少的一条路径,即,把EK算法里的BFS换成SPFA
SPFA:
- 只让当前点能到达的点入队
- 如果一个点已经在队列里,便不重复入队
- 如果一条边未被更新,那么它的终点不入队