网络流学习笔记
网络流学习笔记
引入+概念
网络
网络是指一个有向图
每条边
其中有两个特殊的点:源点
流
设
1.容量限制:对于每条边,流经该边的流量不得超过该边的容量,即
2.斜对称性:每条边的流量与其相反边的流量之和为
3.流守恒性:从源点流出的流量等于汇点流入的流量。
那么称
(以上内容摘自 OI-Wiki)
关于网络流问题,常见的有最大流,最小割,费用流等。实际上,对于网络和流的概念我们目前并不需要深入研究,理解即可。在 OI 中对网络流的考察,更偏向于抽象建模能力而非算法本身。
网络最大流
求解网络最大流的基本思路就是寻找增广路。我们将
这里有三种算法求解网络最大流—— EK(Edmond-Karp) 算法, Dinic 算法,还有 ISAP 算法。
Edmond-Karp 算法
算法思想
利用 bfs 不断寻找增广路,直到无法增广为止。
算法过程
- 从
开始 bfs,如果可以到 则我们找到了新的增广路; - 对于增广路
,我们计算出 经过的边的剩余容量的最小值 。我们给 上每条边都加上 流量,并给它们的反向边都退掉 流量,令最大流增加了 ; - 修改原图
,在新的图 上重复上述过程,直到无法找到新的增广路。
复杂度
上界
代码
#include<bits/stdc++.h> using namespace std; int n, m, s, t; const int N = 205, M = 5050; const long long inf = 1111111111111111111; int head[N], tot = 1, pre[N]; long long incf[N]; bool vis[N]; struct node { int nxt, to, w; }edge[M<<1]; void add(int u, int v, int w) { edge[++tot].nxt = head[u]; edge[tot].to = v; edge[tot].w = w; head[u] = tot; edge[++tot].nxt = head[v]; edge[tot].to = u; edge[tot].w = 0; head[v] = tot; }//利用链式前向星连续加边的性质来处理正向边与反向边,注意从 2 开始。 bool bfs() { memset(vis, 0, sizeof(vis));//标记已经经过的点。 queue<int> q; q.push(s); vis[s] = 1; incf[s] = inf; while(q.size()) { int x = q.front(); q.pop(); for(int i = head[x]; i; i = edge[i].nxt) { if(edge[i].w!=0) { int y = edge[i].to; if(vis[y]) continue; incf[y] = min(incf[x], 1ll*edge[i].w); pre[y] = i; q.push(y); vis[y] = 1; if(y == t) return 1; } } } return 0; } long long maxflow = 0; void update() { int x = t; while(x != s) { int i = pre[x]; edge[i].w -= incf[t]; edge[i^1].w+=incf[t]; x = edge[i^1].to; } maxflow += incf[t]; } int main() { scanf("%d%d%d%d", &n, &m, &s, &t); for(int i = 1; i<=m; i++) { int u, v, c; scanf("%d%d%d", &u, &v, &c); add(u, v, c); } while(bfs()) update(); printf("%lld", maxflow); return 0; }
Dinic 算法
其实我们更常用的是 Dinic 算法,它复杂度要比 EK 算法优秀(理论上界
算法思想
在增广前先对
算法过程
- 从
开始 bfs 图 ,求出每个点的层次; - dfs 全图,同时对各边的容量进行减少或退回;
- 对新的图
,重复上述过程,直到无法 bfs 到 ;
代码
注:Dinic 算法不进行当前弧优化复杂度是不对的,所以下面代码不保证复杂度正确
#include<bits/stdc++.h> #define ll long long using namespace std; const int N = 205, M = 5050; const ll INF = 0x3f3f3f3f3f3f3f3f; int s, t; int tot = 1; int head[N]; struct node{ int nxt, to; ll w; }edge[M<<1]; void add(int u, int v, ll w){ edge[++tot].nxt = head[u]; edge[tot].to = v; edge[tot].w = w; head[u] = tot; } void Add(int u, int v, int w){ add(u, v, w); add(v, u, 0); } int dep[N]; bool bfs(){ queue<int> q; memset(dep, 0, sizeof(dep)); dep[s] = 1; q.push(s); while(!q.empty()){ int u = q.front(); q.pop(); for(int i = head[u]; i; i = edge[i].nxt){ int v = edge[i].to; if((edge[i].w>0)&&(!dep[v])){ dep[v] = dep[u]+1; q.push(v); } } } if(dep[t]){ return 1; } return 0; } ll dfs(int u, ll flow){ if(u == t) return flow; for(int i = head[u]; i; i = edge[i].nxt){ int v = edge[i].to; if(dep[v] == dep[u]+1&&(edge[i].w)){ ll fl = dfs(v, min(flow, edge[i].w)); if(fl>0){ edge[i].w -=fl; edge[i^1].w+=fl; return fl; } } } return 0; } ll ans; int n, m; int main(){ scanf("%d%d%d%d", &n, &m, &s, &t); for(int i = 1; i<=m; ++i){ int u, v, w; scanf("%d%d%d", &u, &v, &w); Add(u, v, w); } while(bfs()){ ll d = dfs(s, INF); while(d){ ans+=d; d = dfs(s, INF); } } printf("%lld\n", ans); return 0; }
当前弧优化
刚才的过程中,由于我们每次都需要遍历
代码
#include<bits/stdc++.h> #define ll long long using namespace std; const int N = 205, M = 5050; const ll INF = 0x3f3f3f3f3f3f3f3f; int s, t; int tot = 1; int head[N]; struct node{ int nxt, to; ll w; }edge[M<<1]; void add(int u, int v, ll w){ edge[++tot].nxt = head[u]; edge[tot].to = v; edge[tot].w = w; head[u] = tot; } void Add(int u, int v, int w){ add(u, v, w); add(v, u, 0); } int dep[N], cur[N];//cur用来记录当前到了哪条边,防止重复遍历 bool bfs(){ queue<int> q; memset(dep, 0, sizeof(dep)); dep[s] = 1;//必须初始化为 1 !警钟长鸣! q.push(s); while(!q.empty()){ int u = q.front(); q.pop(); for(int i = head[u]; i; i = edge[i].nxt){ int v = edge[i].to; if((edge[i].w>0)&&(!dep[v])){ dep[v] = dep[u]+1; q.push(v); } } } if(dep[t]){ return 1; } return 0; } ll dfs(int u, ll flow){ if(u == t) return flow; for(int &i = cur[u]; i; i = edge[i].nxt){//利用取址符每次修改当前弧 int v = edge[i].to; if(dep[v] == dep[u]+1&&(edge[i].w)){ ll fl = dfs(v, min(flow, edge[i].w)); if(fl>0){ edge[i].w -=fl; edge[i^1].w+=fl; return fl; } } } return 0; } ll ans; int n, m; int main(){ scanf("%d%d%d%d", &n, &m, &s, &t); for(int i = 1; i<=m; ++i){ int u, v, w; scanf("%d%d%d", &u, &v, &w); Add(u, v, w); } while(bfs()){ for(int i = 1; i<=n; ++i){ cur[i] = head[i];//记得每次bfs后初始化 } ll d = dfs(s, INF); while(d){ ans+=d; d = dfs(s, INF); } } printf("%lld\n", ans); return 0; }
ISAP 算法
该算法是基于 Dinic 的。在 Dinic 算法中,我们每次求完增广路都需要跑 bfs 来分层;而 ISAP 可以通过反向分层来达到更高的效率。
ISAP 中也存在当前弧优化,同时,它也有另一个优化:GAP 优化。具体来讲,就是我们记录一个
代码
#include<bits/stdc++.h> #define ll long long using namespace std; const int N = 205, M = 5050; const ll INF = 0x3f3f3f3f3f3f3f3f; int head[N], tot = 1; struct node{ int from, nxt, to;ll w; }edge[M<<1]; int s, t; int n, m; ll ans; int path[N];//记录增广路。 int cur[N], num[N]/*每层节点数,用于GAP优化*/,dep[N]; bool vis[N]; void add(int u, int v, int w){ edge[++tot].nxt = head[u]; edge[tot].from = u; edge[tot].to = v; edge[tot].w = w; head[u] = tot; } void adde(int u, int v, int w){ add(u, v, w); add(v, u, 0); } bool bfs(){ memset(vis, 0, sizeof(vis)); queue<int> q; q.push(t);//跑反图分层 vis[t] = 1; dep[t] = 0; while(!q.empty()){ int v = q.front(); q.pop(); for(int i = head[v]; i; i = edge[i].nxt){ node e = edge[i^1];//反向边才是原图的边。 int u = e.from; if(!vis[u] && e.w){ vis[u] = 1; dep[u] = dep[v]+1; q.push(u); } } } return vis[s]; } ll augment(){ int u = t; ll imp = INF; while(u ^ s){ node e = edge[path[u]]; imp = min(imp, e.w); u = e.from; } u = t; while(u ^ s){ edge[path[u]].w -= imp; edge[path[u] ^ 1].w += imp; u = edge[path[u]].from; } return imp; } ll maxFlow(){ ll ret = 0; bfs();// ISAP 在dfs中动态修改分层,所以一边bfs预处理即可。 for(int i = 1; i<=n; ++i){ num[dep[i]]++;//统计每层节点数目 } int u = s; for(int i = 1; i<=n; ++i) cur[i] = head[i];//当前弧优化初始化 while(dep[s] < n){ if(u == t){// 如果找到汇点,开始增广。 ret += augment(); u = s; } bool ok = false; for(int &i = cur[u]; i; i = edge[i].nxt){ node e = edge[i]; int v = e.to; if(dep[u] == dep[v]+1 && e.w){//找到一条增广路 ok = true; path[v] = i; u = v; break; } } if(!ok){//如果没找到,说明正向边残量全为0, 反向边残量大于0,更新层数。 //其实这等于你把head[u]遍历完了,所以增广已经全部完成,接下来的遍历只会找到反向边。 //需要注意,这里的层数是对于反图来讲的。 //或者说,这一步动态维护了原来需要bfs求出来的信息。 int mind = n; for(int i = head[u]; i; i = edge[i].nxt){ node e = edge[i]; int v = e.to; if(e.w) mind = min(mind, dep[v]);//找到最小层数(其实就是最短路) } if(--num[dep[u]] == 0) break;//GAP优化 dep[u] = mind+1;//层数改变 num[dep[u]]++;//修改层内点数 cur[u] = head[u];//当前弧重置 if(u ^ s) u = edge[path[u]].from;//向前回溯一个节点。 } } return ret; } int main(){ scanf("%d%d%d%d", &n, &m, &s, &t); for(int i = 1; i<=m; ++i){ int u, v, w; scanf("%d%d%d", &u, &v, &w); adde(u, v, w); } printf("%lld\n", maxFlow()); return 0; }
最小费用最大流
对于一些问题,我们的网络中的每条边
其实,我们只需要对原算法进行改造即可。我们令正向边边权为正,逆向边边权为负。整个过程其实在寻找增广路的时候去找最短路,也就是,我们把 bfs 改成最短路算法即可。这里我们以 spfa 为例(用于处理负环)。需要注意,
代码:
#include<bits/stdc++.h> #define ll long long #define int long long using namespace std; const int N = 5050, M = 50050; inline int read(){ int x = 0; char ch = getchar(); while(ch<'0' || ch>'9') ch = getchar(); while(ch>='0'&&ch<='9'){ x = x*10+ch-48; ch = getchar(); } return x; } ll dst[N];int s, t; int head[N], tot = 1; struct node{ int nxt, to;ll fl, c; }edge[M<<1]; void add(int u, int v, int w, int c){ edge[++tot].nxt = head[u]; edge[tot].to = v; edge[tot].fl= w; edge[tot].c = c; head[u] = tot; } void adde(int u, int v, int w, int c){ add(u, v, w, c); add(v, u, 0, -c); } int n, m; int pre[N], last[M]; ll flow[N]; bool vis[N]; queue<int> q; bool spfa(){ memset(dst, 0x3f, sizeof(dst)); memset(flow, 0x3f, sizeof(flow)); memset(vis, 0, sizeof(vis)); q.push(s); vis[s] = 1; dst[s] = 0; pre[t] = -1; while(!q.empty()){ int u = q.front(); q.pop(); vis[u] = 0; for(int i = head[u]; i; i = edge[i].nxt){ int v = edge[i].to; if(edge[i].fl>0&&dst[v]>dst[u]+edge[i].c){ dst[v] = dst[u]+edge[i].c; pre[v] = u; last[v] = i; flow[v] = min(flow[u], 1ll*edge[i].fl); if(!vis[v]){ vis[v] = 1; q.push(v); } } } } return pre[t]!=-1; } ll maxFlow, minCost; void MCMF(){ while(spfa()){ int u = t; maxFlow+=flow[t]; minCost+=flow[t]*dst[t]; while(u^s){ edge[last[u]].fl-=flow[t]; edge[last[u]^1].fl+=flow[t]; u = pre[u]; } } } signed main(){ n = read(), m = read(); s = read(), t = read(); for(int i = 1; i<=m; ++i){ int u = read(), v = read(), w = read(), c = read(); adde(u, v, w, c); } MCMF(); printf("%d %d\n", maxFlow, minCost); return 0; }