今天重温了网络流,感觉收获颇丰
网络流其实可以抽象成流水问题,有\(n\)个点\(m\)条边,每条边有最大容量,有一个出水的源点\(S\)和进水的汇点\(T\),问你最后汇点的水最多能有多少。
- 增广路:从\(S\)到\(T\)的一条路径中流的值都大于零的一条路就叫增广路
讲解法之前先介绍一下建反边的操作的意义:因为每次我们都会去寻找增广路但是我们并不知道当前这条是不是最优方案,因此我们需要一个反悔操作来帮我们找到最终答案,因此反边就出现了,反边的意思就是把原本是\((u->v)\)的路径反建一条,使每次在流到这条路径的时候在反边上减去在正边中流的流量,这样就可以做到下次再寻找增广路的时候反悔了。
EK
EK算法的核心就是每次在图中寻找一条增广路并更新流量,直到找不到新的增广路为止。时间复杂度是\(O(nm^2)\)
代码:
点击查看代码
#include <iostream>
#include <queue>
using namespace std;
typedef long long ll;
const int N = 5e3 + 10;
int n, m, s, t, tot = 1;
ll ans;
int pre[N], head[N];
ll dis[N];
bool vis[N];
struct zx{int to, nxt; ll w;} e[N * 2];
void add(int u, int v, ll w) {
e[++tot] = {v, head[u], w};
head[u] = tot;
}
bool bfs() {
for(int i = 1; i <= n; ++i) vis[i] = 0;
queue < int > q;
q.push(s), vis[s] = 1, dis[s] = 2147483647;
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to; ll w = e[i].w;
if(!w || vis[v]) continue;
pre[v] = i, vis[v] = 1;
dis[v] = min(dis[u], w);
q.push(v);
if(v == t) return 1;
}
}
return 0;
}
void work() {
int x = t;
while(x != s) {
int v = pre[x];
e[v].w -= dis[t];
e[v ^ 1].w += dis[t];
x = e[v ^ 1].to;
}
ans += dis[t];
}
int main() {
cin >> n >> m >> s >> t;
for(int i = 1, u, v, w; i <= m; ++i)
cin >> u >> v >> w, add(u, v, w), add(v, u, 0);
while(bfs()) work();
cout << ans << endl;
return 0;
}
Dinic
EK算法因为每次只寻找一条增广路所以时间复杂度比较高,如果在稠密图中就显得不优秀,所以每次寻找多个增广路的Dinic算法就出现了。
Dinic算法的思路是对于离源点距离一样的点建分层图,然后在回溯时更新边权。
但是这样还是略慢,怎么办?别急,我们有当前弧优化。
当前弧优化的意思就是一条路已经被增广过一次了,那么它就不会再被增广了,那其实在增广的时候完全不需要考虑它了,那我们就另开一个数组记录当前点需要从那个点向后遍历就行了。
时间复杂度是\(O(n^2m)\),在稠密图中明显快于EK
代码:
点击查看代码
#include <iostream>
#include <queue>
using namespace std;
typedef long long ll;
const int N = 5e3 + 10, inf = 2147483647;
int n, m, s, t, tot = 1;
ll ans;
int dfn[N], cur[N], head[N];
struct zx{int to, nxt, w;} e[N * 2];
void add(int u, int v, int w) {
e[++tot] = {v, head[u], w};
head[u] = tot;
}
bool bfs() {
for(int i = 1; i <= n; ++i) dfn[i] = inf;
queue < int > q;
q.push(s), dfn[s] = 0, cur[s] = head[s];
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to, w = e[i].w;
if(dfn[v] != inf || !w) continue;
q.push(v), dfn[v] = dfn[u] + 1, cur[v] = head[v];
if(v == t) return 1;
}
}
return 0;
}
int dfs(int u, int k) {
if(u == t) return k;
int res = 0;
for(int i = cur[u]; i && k; i = e[i].nxt) {
int v = e[i].to, w = e[i].w;
cur[u] = i;
if(!w || (dfn[v] != dfn[u] + 1)) continue;
int x = dfs(v, min(k, w));
if(!x) dfn[v] = inf;
res += x, k -= x;
e[i].w -= x, e[i ^ 1].w += x;
}
return res;
}
int main() {
cin >> n >> m >> s >> t;
for(int i = 1, u, v, w; i <= m; ++i)
cin >> u >> v >> w, add(u, v, w), add(v, u, 0);
while(bfs()) ans += dfs(s, inf);
cout << ans << endl;
return 0;
}
以上是最大流的讲解。
费用流:最小费用最大流
在考虑要最大流的同时需要使花费最小,显然普通的最大流完成不了这个,但是想要完成也很简单,只需要用SPFA或者dijkstra维护一下找增广路的过程就行了。
代码:
点击查看代码
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
typedef long long ll;
const int N = 5e5;
int n, m, s, t, tot = 1;
ll maxg, minc;
int pre[N], dis[N], mf[N], head[N];
bool vis[N];
struct zx{int to, nxt, w, dis;} e[N * 2];
void add(int u, int v, int w, int dis) {
e[++tot] = {v, head[u], w, dis};
head[u] = tot;
}
bool spfa() {
queue < int > q;
memset(dis, 0x3f, sizeof(dis));
memset(vis, 0, sizeof(vis));
q.push(s), dis[s] = 0, vis[s] = 1, mf[s] = 1 << 30;
while(!q.empty()) {
int u = q.front(); q.pop(); vis[u] = 0;
for(int i = head[u]; i; i = e[i].nxt) {
if(!e[i].w) continue;
int v = e[i].to;
if(dis[v] > dis[u] + e[i].dis) {
dis[v] = dis[u] + e[i].dis;
mf[v] = min(mf[u], e[i].w);
pre[v] = i;
if(!vis[v]) vis[v] = 1, q.push(v);
}
}
}
if(dis[t] == 1061109567) return 0;
return 1;
}
void work() {
maxg += mf[t], minc += mf[t] * dis[t];
int u = t;
while(u != s) {
int i = pre[u];
e[i].w -= mf[t], e[i ^ 1].w += mf[t];
u = e[i ^ 1].to;
}
}
int main() {
cin >> n >> m >> s >> t;
for(int i = 1, u, v, w, dis; i <= m; ++i)
cin >> u >> v >> w >> dis, add(u, v, w, dis), add(v, u, 0, -dis);
while(spfa()) work();
cout << maxg << ' ' << minc << '\n';
return 0;
}