【学习笔记】网络流
发现自己对网络流的理解更深了,所以就写一篇学习笔记了。
网络流基础:
最大流的概念:
流网络:一个有向图(\(G = (V,E)\)),包含一个源点和一个汇点,每条边都有一个权值代表容量(\(c\))。
可行流:给每一条边指定一个流量(\(f\)),当任意一条边都满足以下条件时,这个图就叫做一个可行流:
- 容量限制:任意一条边的流量小于等于它的容量,
- 流量守恒:任意一个点的流出的流量等于流入的流量,
最大流:可行流中流量最大的可行流就叫做最大流,也叫做最大可行流
最小割的概念:
割:将所有的点分为两个集合,满足一个集合(\(S\))中含有源点(\(s\))另一个集合(\(T\))中含有汇点(\(t\)),连接这两个集合的边的容量之和就叫做这个割的大小。
最小割:割里面大小最小的一个割
残余网络的概念:
残余网络的边与原网络中的边基本一致,但是多了原图中的边的反向边,残余网络是定义在可行流上的,也就是说不同的可行流有不同的残余网络。
若在 \(G\) 中有一条可行流 \(f\),那么这种情况下的残余网络的边的容量就为:
- 原图中有的边: \(f`_{u,v} = c_{u,v} - f_{u,v}\)
- 原图中的边的反向边: \(f`_{v,u} = f_{u,v}\)
对于正向的边也就是可以理解为可以再从这条边流下去多少,对于反向边也就是可以理解为能将流量退回来多少
几个定理:
(1)若有原图上的一条可行流 \(f\),以及一条残量网络上的可行流 \(f`\),则 \(f + f`\) 依旧是原图的一条可行流,这种加是指对应边的权值相加。
- 容量限制:考虑 \(0 \le f_{u,v} \le c_{u,v}\),而 \(0 \le f`_{u,v} \le c_{u,v} - f_{u,v}\),所以 \(0 \le f_{u,v} + f`_{u,v} \le c_{u,v}\),即满足容量限制
- 流量守恒:\(\sum_{(u,x) \in E} \ f_{u,x} = \sum_{(x,u) \in E} \ f_{x,u}\),\(\sum_{(u,x) \in E} \ f`_{u,x} = \sum_{(x,u) \in E} \ f`_{x,u}\),两者相加之后仍相等
(2)若残量网络中没有可行流,则此时的原图中的可行流一定是最大流
若是残量网络中有可行流,则将这个可行流加到原图中的可行流中显然更优,如果没有则显然没有办法更优
(3)任意一个割的容量都一定大于等于任意一个可行流的流量
考虑一个任意的割,一条可行流一定是横跨了这两个点集,也就是一定是经过了这几条红边,也就是任意一个可行流的大小一定不会超过这个割的大小
(4)最大流最小割定理:最大流等于最小割
我们记 \(|f|\) 为 \(f\) 这个可行流的流量,\(C_{S,T}\) 为 \(S,T\) 这个割的容量。
- 可以证明一定会存在一个可行流的流量等于某一个割的容量,不妨记这个可行流为 \(f\),由上文可以知:\(|f_{max}| \le C_{S,T}\),因为 \(\exists \ |f| = C_{S,T}\),所以 \(|f_{max}| \le |f|\),因为 $|f| \le f_{max} $,所以可行流 \(f\) 就是最大流。
- 因为 \(C_{min} \le C_{S,T}\),而 \(C_{S,T} = |f_{max}|\),所以 \(C_{min} \le |f_{max}|\),因为 \(|f_{max}| \le C_{S,T}\),所以 \(|f_{max}| = C_{min}\)
求解最大流的算法:
\(dinic\)
基本原理:
\(dinic\) 求解最大流就是使用:残余网络中的可行流加原网络中的可行流一定是原网络的一条可行流,若残余网络中没有可行流则原网络中的可行流就是最大流
基本做法就是,每次尽可能多地找到残余网络中的可行流,然后将残余网络中的可行流合并到原网络的可行流中。
时间复杂度:
时间复杂度 \(O(n^2m)\),但是因为 \(dinic\) 有非常多的优化所以可以跑的飞快,可以过掉点数边数在 \(10^4 - 10^5\) 的数据
代码详解:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e4+5;
const int MAXM = 2e5+5;
const int INF = 1e18+5;
struct edge{
int nxt,to,val;
edge(){}
edge(int _nxt,int _to,int _val){
nxt = _nxt,to = _to,val = _val;
}
}e[2 * MAXM];
int n,m,s,t,cnt = 1,head[MAXN],dis[MAXN],cur[MAXN];
//s 源点,t 汇点,cnt 从 1 开始
void add_edge(int from,int to,int val){ //只维护残量网络
e[++cnt] = edge(head[from],to,val);
head[from] = cnt;
e[++cnt] = edge(head[to],from,0); //残量网络建双向边
head[to] = cnt;
}
bool bfs(){ //判断是否有可行流
memset(dis,-1,sizeof(dis)); //分层图
queue<int> q;
q.push(s);dis[s] = 1;cur[s] = head[s]; //cur 即当前弧优化
while(!q.empty()){
int now = q.front();q.pop();
for(int i=head[now]; i; i = e[i].nxt){
int to = e[i].to;
if(dis[to] == -1 && e[i].val){ //找到可行流,必须流量大于 0
dis[to] = dis[now] + 1;
cur[to] = head[to];
if(to == t) return true; //能到达汇点所以就有
q.push(to);
}
}
}
return false;
}
int dfs(int now,int limit){ //找到可行流的流量。
//limit 即走过的这一条路径的限制,或者理解为流到这里的流量
if(now == t) return limit; //流到了汇点所以就找到了 limit 大小的可行流
int flow = 0;
for(int i=cur[now]; i && flow < limit; i = e[i].nxt){ //flow < limit 判断条件的优化
cur[now] = i; //当前弧优化
int to = e[i].to;
if(e[i].val && dis[to] == dis[now] + 1){ //满足可行流 + 分层图
int h = dfs(to,min(e[i].val,limit - flow));
if(!h) dis[to] = -1; //-1 优化
e[i].val -= h;e[i^1].val += h;flow += h;
}
}
return flow;
}
int dinic(){
int ans = 0,flow = 0;
while(bfs()) while(flow = dfs(s,INF)) ans += flow;
return ans;
}
int main(){
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int i=1; i<=m; i++){
int from,to,val;
scanf("%d%d%d",&from,&to,&val);
add_edge(from,to,val);
}
printf("%d\n",dinic());
return 0;
}
集中解释一下几个优化:
- 分层图优化:因为原图中可以有环,所以就将图分层,即上一层只能到达下一层,这样就能保证不会一直在某一个环上转圈
- 当前弧优化:对于这一个点的前几条边,因为我们已经将它流完了,所以下一次即使再访问到当前节点也没有必要访问那些边了
- flow < limit :显然我们的流出去的流量必须小于等于流入的流量,而等于显然意味着不能流了
- -1 优化:我们从某一个点流不到汇点一点流量,那么我们下一次也没有必要再访问这个节点了,因为这个点已经满了
上下界网络流:
无源汇上下界可行流:
问题描述:
给定一个流网络,没有源点与汇点,每一条边有最小的流量限制以及最大的流量限制,请判断是否有可行流,并输出任意一组方案。
问题分析:
看到这个题我们最显然的一种想法就是:将最小限制通过减法变成 \(0\),那么就可能可以在这种图上求一遍网络流得到一些新的东西了。
我们考虑建一下两个流网络:下界网络、差网络。这两个网络中连边与原网络一致,只是边的容量不一致。
下界网络:在下界网络中边 \((u,v)\) 的容量为原网络中这条边的最小流量限制 \((low_{u,v})\)
差网络:在差网络中边 \((u,v)\) 的容量为原网络中这条边的最大流量限制 \((high_{u,v})\) 减去最小流量限制
可以发现一点:下界网络中我们必须流满,这样下界网络中的流量加差网络中的可行流的流量如果能形成可行流,那么必然是原网络的一条可行流。
考虑是不是可行流即是否满足容量限制和流量守恒:
- 容量限制:显然对于边 \((u,v)\) 它的流量大小即:\(low_{u,v} \le f_{u,v} \le high_{u,v}\),符合上下界的要求
- 流量守恒:对于差网络中一个点的流入流出流量一定守恒,但是对于下界网络却不一定,所以不一定满足。
我们为了使得下界网络加上差网络的可行流之后可以形成一条原网络的可行流,我们就要对差网络进行一些操作。
对于下界网络的一个点,我们记它的流入流量与流出流量的差为 \(A_{x}\),即 \(A_{x} = \sum f_{in} - \sum f_{out}\)
那么为了使得流量守恒也就意味着在差网络中这个点流入流量与流出流量的差就要为 \(-A_{x}\)。
- \(-A_{x} > 0\) 那么就意味着流入流量要多一些,那么就从该点向汇点连边就好了。
- \(-A_{x} < 0\) 那么就意味着流出流量要多一些,那么就从源点向该点连边就好了。
注意这里指的流入与流出流量都是指的原差网络上的边有的流量,因为只有这些边才与下界网络对应边相加,所以我们从源点连入也就意味着增加流出流量,连向汇点就意味着增加流入流量。因为我们在差网络上要满足是一条可行流即满足流量守恒,所以连入的边流量多了,即流出流量多了,连出的边流量多了,即流入流量多了。
下图即为一个例子:
(来源自知乎)
连完之后的差网络是这样的:
(来源自知乎)
通过分析我们也能发现,我们在差网络上新连的边必须流满,因为只有他们流满才能使得差网络加下届网路是原网络的一个可行流,而如果流不满即无解。
代码详解:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 210;
const int MAXM = 1e5+5;
const int INF = 1e9+7;
struct edge{
int nxt,to,val,low;
edge(){}
edge(int _nxt,int _to,int _val,int _low){
nxt = _nxt,to = _to,val = _val,low = _low;
}
}e[MAXM];
int n,m,s,t,cnt = 1,c[MAXN],cur[MAXN],head[MAXN],dis[MAXN];
void add_edge(int from,int to,int val,int low){
e[++cnt] = edge(head[from],to,val,low);
head[from] = cnt;
e[++cnt] = edge(head[to],from,0,low);
head[to] = cnt;
}
bool bfs(){
memset(dis,-1,sizeof(dis));
queue<int> q;
q.push(s);dis[s] = 1;cur[s] = head[s];
while(!q.empty()){
int now = q.front();q.pop();
for(int i = head[now]; i; i = e[i].nxt){
int to = e[i].to;
if(dis[to] == -1 && e[i].val){
dis[to] = dis[now] + 1;
cur[to] = head[to];
if(to == t) return true;
q.push(to);
}
}
}
return false;
}
int dfs(int now,int limit){
if(now == t) return limit;
int flow = 0;
for(int i = cur[now]; i && flow < limit; i = e[i].nxt){
int to = e[i].to;
cur[now] = i;
if(dis[to] == dis[now] + 1 && e[i].val){
int h = dfs(to,min(e[i].val,limit - flow));
if(!h) dis[to] = -1;
e[i].val-=h;e[i^1].val+=h;flow+=h;
}
}
return flow;
}
int dinic(){
int ans = 0,flow;
while(bfs()){
while(flow = dfs(s,INF))
ans += flow;
}
return ans;
}
int main(){
cin>>n>>m;
s = n + 1,t = n + 2;
for(int i=1; i<=m; i++){
int from,to,low,high;
cin>>from>>to>>low>>high;
add_edge(from,to,high - low,low);
c[from] -= low,c[to] += low;
}
int res = 0;
for(int i=1; i<=n; i++){
if(c[i] < 0){
add_edge(i,t,-c[i],0);
}
else if(c[i] > 0){
add_edge(s,i,c[i],0);
res += c[i];
}
}
int ans = dinic();
if(ans != res){
printf("NO");
}
else{
printf("YES\n");
for(int i=2; i<=m * 2; i+=2){
printf("%d\n",e[i].low + e[i^1].val);
//一条边的流量就是其反向边的 val
}
}
return 0;
}
有源汇上下界最大流:
问题分析:
可以考虑先连边 \((t,s,\infty)\) 根据无源汇的跑出一个可行流,然后删去这一条边,之后再跑一边 \(s \to t\) 的最大流即可。
我们在原有的基础上增广得到的也一定是合法的流,增广出来的最大流肯定就是对应的最大流。
如果要求最小流,那么最后求一遍 \(t \to s\) 的最大流即可。
可以理解为最极限的退流,也就是正向的最小流。