2024 年春节集训 _ 第四课 - 网络流

\(EK\) 算法

考虑 \(EK\) 算法求解最大流。

每次找一条最小剩余流量 \(d>0\)\(s\rightsquigarrow t\) 的路径。然后对之流下 \(d\) 的水。这个操作我们称之为增广,所找到的这条路径叫做增广路。

一直增广到不存在任何增广路为止。

发现这样的贪心策略有时是错误的。

考虑反悔。连一条反边,反边最开始容量为 \(0\),在每次正边流过的时候反边剩余流量增加。增广时可以考虑反边的流。但是会有很多反边的起点是没办法到达的。

所以增广依然会结束。

这样连反边然后不断增广的算法被称作 \(EK\) 算法。

最大流最小割定理

定义 \(s-t\) 割为将 \(s,t\) 不连通所要切断的边边权之和。

最大流 \(=\) 最小割。

证明略。

程式
#include <bits/stdc++.h>
#define ll long long

#define fr(x) for (int e = fir[x]; e; e = nxt[e])

using namespace std;
const int N = 1e5 + 10, inf = 1e9 + 7;
int m, n, s, t, top;
int fir[N], nxt[N], c[N], to[N];
int prv[N];
inline void add(int u, int v, int w) {
    nxt[++top] = fir[u], fir[u] = top, to[top] = v, c[top] = w;
    nxt[++top] = fir[v], fir[v] = top, to[top] = u, c[top] = 0;
}
bool vis[N];
inline int up(int x) {
    if (x & 1)
        return x + 1;
    else
        return x - 1;
}
inline bool bfs(void) {
    queue<int> q;
    q.push(s);
    int v;
    memset(vis, 0, sizeof vis);
    memset(prv, 0, sizeof prv);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        fr(u) {
            if (c[e] > 0 && !vis[v = to[e]]) {
                prv[v] = e, vis[v] = 1;
                q.push(v);
                if (v == t)
                    return true;
            }
        }
    }
    return false;
}
inline int augment(void) {
    int d = inf;
    for (int u = t, e; u != s; u = to[up(e)]) e = prv[u], d = min(d, c[e]);
    for (int u = t, e; u != s; u = to[up(e)]) e = prv[u], c[e] -= d, c[up(e)] += d;
    return d;
}
int ans;
inline void maxflow(void) {
    ans = 0;
    while (bfs()) ans += augment();
    return;
}
int main() {
    scanf("%d%d", &m, &n);
    s = 1, t = n;
    for (int i = 1; i <= m; ++i) {
        int o1, o2, o3;
        scanf("%d%d%d", &o1, &o2, &o3);
        add(o1, o2, o3);
    }
    maxflow();
    printf("%d\n", ans);
}

但是这样做的复杂度是 \(\mathcal{O(nm^2)}\) 的,不算优秀。

所以你需要一些优化剪枝。也就是 \(Dinic.\)

\(Dinic\) 算法

分层图优化

考虑到 \(EK\) 算法需要每次增广都跑一次 \(bfs.\) 太过麻烦。所以使用分层图优化。

\(d[u]\) 表示 \(s\rightsquigarrow t\) 的最短距离。

在后面的 \(dfs\) 中仅需 \(dfs d[v]=d[u]+1\)\(v\) 就好了。不为什么。

当前弧优化

考虑到之前 \(dfs\) 满的边就没必要再 \(dfs\) 了。记 \(cur[i]\)\(i\) 接下来要 \(dfs\) 的边。

在一次 \(dfs\) 过后下次就没必要再 \(dfs\) 这里了。

依然是 \(bfs \rightarrow dfs \rightarrow bfs \cdots\) 这样进行的。

多路增广

一次 \(dfs\) 你需要解决很多个增广路。

所以在 \(dfs\) 一个点的同时增广多路。

程式
#include <bits/stdc++.h>
#define int long long
#define fr(x) for(int e=fir[x];e;e=nxt[e])
using namespace std;
const int N=5005,inf=0x3f3f3f3f;
int fir[N],nxt[N<<2],c[N<<2],to[N<<2],now[N],d[N];
int tot=1,n,m,s,t;
bool vis[N];
inline void add(int u,int v,int w){
	nxt[++tot]=fir[u], fir[u]=tot, c[tot]=w, to[tot]=v;
	nxt[++tot]=fir[v], fir[v]=tot, c[tot]=0, to[tot]=u;
}
bool bfs(){
	queue<int> q; q.push(s);
	for(int i=1;i<=n;++i) d[i]=-1;
	d[s]=0;
	while(!q.empty()){
		int u=q.front(); q.pop();
		now[u]=fir[u];
		fr(u){
			int v=to[e];
			if(c[e]==0||d[v]!=-1) continue;
			d[v]=d[u]+1, q.push(v); 
		}
	}
	return (bool)(~d[t]);
}
int dfs(int u,int in){
	int sum=0;
	if(u==t) return in; 
	for(int e=now[u];e;e=nxt[e]){
		int v=to[e];
		now[u]=e;
		if(d[u]+1!=d[v]||!c[e]) continue;
		int r=dfs(v,min(in-sum,c[e]));
		sum+=r, c[e]-=r, c[e^1]+=r;
		if(in==sum) break;
	}
	return sum;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin>>n>>m;
	s=1, t=n;
	for(int i=1;i<=m;++i){
		int x,y,z;
		cin>>x>>y>>z, add(x,y,z);
	}
	int ans=0;
	while(bfs()) ans+=dfs(s,inf);
	return cout<<ans<<"\n",0;
}

例题

P1345 奶牛的电信 这个其实很有意思啊,考虑最小割点集。

需要把每个点拆分,这样就能实现所谓“打碎点”了。

为了保证这个点在被打碎后无效,把这个点 \(u\) 的所有入边插到 \(u\) 上,剩余的插到 \(copy(u)\) 上。

然后因为不能打碎源点汇点,所以源点放在出点的 \(copy(s)\) 上,但是 \(t\) 原封不动。给个主函数。

注意 \(n\) 的大小,不然初始化有可能会漏一些东西。

程式
#include <bits/stdc++.h>
#define int long long
#define fr(x) for(int e=fir[x];e;e=nxt[e])
using namespace std;
const int N=50005,inf=0x3f3f3f3f;
int fir[N],nxt[N<<2],c[N<<2],to[N<<2],now[N],d[N];
int tot=1,n,m,s,t;
bool vis[N];
inline void add(int u,int v,int w){
	nxt[++tot]=fir[u], fir[u]=tot, c[tot]=w, to[tot]=v;
	nxt[++tot]=fir[v], fir[v]=tot, c[tot]=0, to[tot]=u;
}
bool bfs(){
	queue<int> q; q.push(s);
	for(int i=1;i<=n;++i) d[i]=-1;
	d[s]=0;
	while(!q.empty()){
		int u=q.front(); q.pop();
//		cout<<u<<" "<<d[u]<<"\n";
		now[u]=fir[u];
		fr(u){
			int v=to[e];
//			cout<<"try "<<v<<" c d "<<c[e]<<"  "<<d[v]<<"\n";
			if(c[e]==0||d[v]!=-1) continue;
			d[v]=d[u]+1, q.push(v); 
		}
	}
	return (bool)(~d[t]);
}
int dfs(int u,int in){
	int sum=0;
	if(u==t) return in; 
	for(int e=now[u];e;e=nxt[e]){
		int v=to[e];
		now[u]=e;
		if(d[u]+1!=d[v]||!c[e]) continue;
		int r=dfs(v,min(in-sum,c[e]));
		sum+=r, c[e]-=r, c[e^1]+=r;
		if(in==sum) break;
	}
	return sum;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin>>n>>m>>s>>t, s+=n;
	for(int i=1;i<=n;++i) add(i,i+n,1);
	for(int i=1;i<=m;++i){
		int x,y;
		cin>>x>>y, add(x+n,y,inf),add(y+n,x,inf);
	}
	int ans=0;
	n*=2;
	while(bfs()) ans+=dfs(s,inf);
	return cout<<ans<<"\n",0;
}
posted @ 2024-05-17 18:21  ChihiroFujisaki  阅读(14)  评论(0)    收藏  举报