网络流
网络流
基础知识
网络
在一个有向图里,有一个源点有一个汇点,每条边有一个容量,这种图叫做网络。
流量
容量:每条边都有一个容量(水管的最大水流容量)常用c(a,b)表示
源点:出发点
汇点:结束点
流:一个合法解称作一个流,也就是一条可以从源点到汇点的一条合法路径。
流量:每条边各自被经过的次数称作其流量,最终收集的总数为整个流的流量.常用f(a,b)表示
容量限制:每条边的流量不超过其容量(水管会爆的)。
流量平衡:对于除源点和汇点以外的点来说,其流入量一定等于流出量
即f(a,c)+f(b,c)=f(c,d);
对于任意除了源点和汇点外的点u,都有\(\sum\)f(i,u)=\(\sum\)f(u,j)
割
网络流的割:是网络中顶点的一个划分,把所有顶点划分成两个顶点集合S和T,其中源点s属于S,汇点t属于T,记作CUT(S,T)。
割的割边:如果一条弧的两个顶点一个属于顶点集S一个属于顶点集T,该弧为割CUT(S,T)的一条割边。
最大流==最小割
费用流
给定一个网络,每条边除了容量限制c(u,v),还有一个单位流量费用w(u,v).当(u,v)的流量为f(u,v)时,需要花费f(u,v)*w(u,v)的费用,其中w也满足w(u,v)=-w(v,u)。
则该网络中总花费最小的最大流称为最小费用最大流,即在最大化\(\sum\)(s,v)\(\in\)Ef(s,v)的前提下最小化\(\sum\)(u,v)\(\in\)Ef(u,v)*w(u,v)
最大流
找到一条从源点S到汇点T的路,使得流经的流量最大,这条路叫做最大流。
为了解决最大流问题,我们需要引入一个概念:增广路。
增广路
在原图G中若一条从源点S到汇点T上所有边的剩余容量都大于0(注意:要严格大于0)这条路被成为增广路(Augmenting Path)
或者说:在残存网络Gf中,有一条从源点到汇点的路径被称为增广路
反向边
在最大流算法中,我们往往引入反向流概念,使算法可以后悔。即在算法中需要回溯,但是一旦回溯,时间复杂度就会变成指数级,所以一些聪明的前辈们就建立了反向边。
反向边满足:c+(i,j) + c-(i,j) = c(i,j)
Edmond-Karp 动能算法(EK 算法)
算法步骤:
1.在图上找到一条从源点到汇点的路径(称为‘增广路’)。
2.去增广路上的残量最小值v。(也就是流过的路径中流量最小的那一个)
3.将答案加上v。
4,.将增广路上所有边的残量减去v,反向边的残量加上v。(满足反向边条件)
重复上边4个步骤直到找不到增广路为止。
例题:P3376 【模板】网络最大流
https://www.luogu.com.cn/problem/P3376
代码实现:
#include <bits/stdc++.h>
#define ll long long
#define ull unsigned long long
using namespace std;
const int N = 5007;
const int INF = 0x3f3f3f3f;
struct edge{
int from,to;
ll cap,flow;
edge(int u,int v,ll c,ll f):from(u),to(v),cap(c),flow(f){};
};
struct EK{
int n,m;//这个图有n个点,m条边
vector<edge> e;//保留所有边的信息
vector<int> G[N];//G[i][j]保留节点i的第j条边在e数组里的编号
ll a_now[N];//每个点目前的水流量
int pre[N];//节点i的前一个节点的编号
void init(int n){
for(int i = 0; i <= n; i++)G[i].clear();
e.clear();
}
void add(int u,int v,ll c){
e.push_back(edge(u,v,c,0));//正向边
e.push_back(edge(v,u,0,0));//反向边
m = e.size();
G[u].push_back(m - 2);
G[v].push_back(m - 1);
}
ll Maxflow(int s,int t){
ll flow=0;
while(1){
memset(a_now,0,sizeof(a_now));//最初每个点的水流量都是0
queue<int>Q;//BFS
Q.push(s);
a_now[s] = INF;
while(!Q.empty()){
int x = Q.front();//目前水流到的节点;
Q.pop();//出队
for(int i = 0;i < G[x].size(); i++) {
edge now=e[G[x][i]];//G[x][i]是点x到i的边的编号,now表示点x到i的所有信息
if(!a_now[now.to] && now.cap > now.flow){//点i未流到并且这条路没流满
pre[now.to] = G[x][i];
a_now[now.to] = min(a_now[x], now.cap - now.flow);
//流到下一点的水量为上一点的水量或者路径上还可以流的最大流量,这两者取最小值
Q.push(now.to);//下一个节点入队
}
}
if(a_now[t])//已经流到终点,退出寻找
break;
}
if(!a_now[t])//如果所有路都已经试过,水不能流到终点,已经是最大流
break;
for(int u = t; u != s; u = e[pre[u]].from){//反向记录路径
e[pre[u]].flow += a_now[t];//路径上所有正向边的流量增加流到终点的流量
e[pre[u] ^ 1].flow -= a_now[t];//路径上所有反向边的流量减少流到终点的流量
}
flow += a_now[t];
}
return flow;
}
};
int main(){
int n,m;
EK ek;
while(~scanf("%d%d", &n, &m)){
ek.init(n);
int s,t;
scanf("%d%d",&s,&t);
int u,v;
ll w;
while(m--){
scanf("%d%d%lld",&u,&v,&w);
ek.add(u,v,w);
}
printf("%lld\n",ek.Maxflow(s,t));
}
return 0;
}