网络流 笔记

本文原在 2024-07-22 10:17 发布于本人洛谷博客。

一、定义与性质

1. 基本定义

从水厂出发,有很多节点和水管,节点不能存水,但容量无限,水管有容量上限,全部水管最终经过某些节点都会流向某个工厂里,问最多同时能给工厂发多少水?

流网络:这张图。

源点:水厂。

汇点:工厂。

弧:水管。

弧的流量:这条水管当前流多少水,对于弧 \((u,v)\)\(f(u,v)\) 表示。

弧的容量:这条水管最多能流多少水,用 \(c(u,v)\) 表示。

弧的残量:这条水管还能流多少水,即 \(c(u,v)-f(u,v)\)

流量网络/容量网络/残量网络:边权表示/流量/容量/残量的流网络。

(重要) 增广路:在残量网络中,还能从源点流向汇点的一条路径。

2. 性质

  • 斜对称性:\(f(u,v)=-f(v,u)\)

  • 流量守恒:

    • 对于节点,输出量和输入量相等。
    • 源点的输出量等于汇点的输入量。

二、最大流

1. EK 算法

第一章第 1 节中所提到的问题就属于最大流问题。

基本思路就是:建一张容量网络,找到一条路径就给每条边减去相应的流量,并在剩下的残量网络上,不断找增广路。

但是对于下面这个图,显然找不到增广路了,但是肉眼可见最大流是 \(1\to 2\to 4\) 加上 \(1\to 3\to 4\)

因此给程序一个反悔的机会:给每条边都建一条反向边(根据 \(0\oplus 1 =1\)\(1\oplus 0 = 0\)\(2\oplus 1 = 3\)\(3\oplus 1 = 2\)\(4\oplus 1 =5\)……的性质,可以用 \(i\)\(i\oplus 1\) 作为一组相反的边),边权表示正向边的流量,实时更新。

每次找到一条路径后,给路径上的边减去本路径的最大流,路径上的反边加上本路径的最大流。这样就会发现:如果路径中含有一条反向边,它恰好能将正向边的容量给推回去。

int bfs() {  // bfs寻找增广路
    for (int i = 1; i <= n; i++)
        vis[i]=0;
    queue<int> q;
    q.push(s);
    vis[s] = 1;
    dis[s] = oo;
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int i = head[x]; i; i = edge[i].next) {
            if (edge[i].flow == 0)
                continue;
                // 如果是正向边:没有残量了怎么流?如果是反向边:正向边没有流量怎么流?
            int v = edge[i].v;
            if (vis[v])
                continue; // 访问过了
            flow[v] = min(flow[u], edge[i].flow);
            // 显然,一条路径的最大流取决于能流最小的那条边
            pre[v] = i; // 记录前驱,方便修改边权 
            q.push(v);
            vis[v] = 1;
            if (v == t)
                return 1; // 找到了一条增广路 
        }
    }
    return 0;
}

void update() {  // 更新所经过边的正向边权以及反向边权 
    int u = t;
    while (u != s) {
        int v = pre[u];
        edge[v].flow -= flow[t];
        edge[v ^ 1].flow += flow[t];
        u = edge[v ^ 1].v;
    }
    maxflow += flow[t];   // 累加每一条增广路经的最小流量值 
}
void EK() {
    while (bfs())
        update();
}

2. Dinic 算法

EK 算法一次才找一条增广路,使用 Dinic 算法的 DFS 可以一次寻找多条增广路。

用 BFS 对图分层,作用在于一次可以得到多条长度相同的最短增广路。

每次从当前点出发,选用从当前点所在层到下一层的边,发送一定的流量,流量的大小取边残量和当前点从源点获取的剩余流中两者的最小值。

搜索完成后,返回一个流量值,即这条增广路的流量,此时就能够对边和反向边的残量进行更新了。

bool bfs() {
    fill(dis, dis + N, oo);
    queue<int> q;
    q.push(s);
    dis[s] = 0;
    now[s] = head[s];
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int i = head[u]; i; i = edge[i].next) {
            int v = edge[i].v;
            if (edge[i].w <= 0 or dis[v] != oo)
                continue;
            q.push(v);
            now[v] = head[v]; // 记录当前弧优化,可以不写,直接用 head
            dis[v] = dis[u] + 1; // 分层
            if (v == t) // 到汇点了
                return true;
        }
    }
    return false;
}
int dfs(int u, int last) {
    if (u == t) return last; // 到汇点了
    int ret = 0;
    for (int i = now[u]; i and last; i = edge[i].next) {
        now[u] = i;
        int v = edge[i].v;
        if (edge[i].w <= 0 or dis[v] != dis[u] + 1) // 没有流量或者层数对不上
            continue;
        int tmp = dfs(v, min(last, edge[i].w)); // 往后走的最大流
        if (!tmp) dis[v] = oo;
        edge[i].w -= tmp;
        edge[i ^ 1].w += tmp;
        ret += tmp;
        last -= tmp;
    }
    return ret;
}
void dinic() {
    while(bfs())
        ans += dfs(s, oo);
}

三、费用流

以最小费用最大流为例:

每条水管要收费,假设水管 \((u,v)\) 的单价是 \(cost\),那么你就要付 \(f(u,v)\times cost\)

将 EK 算法的 BFS 改成 SPFA 即可,可能有负边不能 Dijkstra。

namespace McMf {
    int cost[N], flow[N], pre[N];
    bool vis[N];
    bool spfa() {
        queue<int> q;
        fill(cost, cost + N, oo);
        memset(vis, 0, sizeof vis);
        q.push(s);
        cost[s] = 0;
        vis[s] = true;
        flow[s] = oo;
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            vis[u] = false;
            for (int i = head[u]; i; i = edge[i].next) {
                if (!edge[i].flow)
                    continue;
                int v = edge[i].v;
                if (cost[v] > cost[u] + edge[i].cost) {
                    cost[v] = cost[u] + edge[i].cost;
                    flow[v] = min(flow[u], edge[i].flow);
                    pre[v] = i;
                    if (!vis[v]) {
                        q.push(v);
                        vis[v] = true;
                    }
                }
            }
        }
        return cost[t] <= oo / 2;
    }
    void mcmf() {
        while (spfa()) {
            int u = t;
            maxflow += flow[u];
            mincost += flow[u] * cost[u];
            while (u != s) {
                int p = pre[u];
                edge[p].flow -= flow[t];
                edge[p ^ 1].flow += flow[t];
                u = edge[p ^ 1].v;
            }
        }
    }
}
using namespace McMf;

四、上下界网络流

1. 无源汇上下界可行流

假设上下界是 \([L,R]\),点 \(x\) 流入的量是 \(in_x\),流出的量是 \(out_x\)

令每条边都先流自己的 \(L\),统计到 \(in_x\)\(out_x\) 中,然后构造一个流网络,边权为 \(R-L\)

如果 \(in_x>out_x\),那么建边 \((s,x,in_x-out_x)\)

如果 \(in_x<out_x\),那么建边 \((x,t,out_x-in_x)\)

在这个新构造的流网络中,如果源点流出的每一条边都能流满,那么说明流量平衡。

2. 有源汇上下界可行流

连接题目给出的源点 \((t,s,[0,\infty])\),用 1 的方法。

3. 有源汇上下界最大流

跑完可行流后,在残量网络中找 \(s\to t\) 的最大流。

#include <bits/stdc++.h>
#define int long long
#define IOS ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
using namespace std;
const int N = 1e6 + 10, oo = 1e18;
int n, m, s, t, s1, t1, in[N], out[N], over;
int head[N], ide = 1;
struct EDGE {
	int v, next, w;
} edge[N];
void add(int u, int v, int w) {
	edge[++ide] = {v, head[u], w};
	head[u] = ide;
	edge[++ide] = {u, head[v], 0};
	head[v] = ide;
}
int dis[N], now[N];
bool bfs() {
	fill(dis, dis + N, oo);
	queue<int> q;
	q.push(s);
	dis[s] = 0;
	now[s] = head[s];
	while (!q.empty()) {
		int u = q.front();
		q.pop();
		for (int i = head[u]; i; i = edge[i].next) {
			int v = edge[i].v;
			if (edge[i].w <= 0 or dis[v] != oo)
				continue;
			q.push(v);
			now[v] = head[v];
			dis[v] = dis[u] + 1;
			if (v == t)
				return true;
		}
	}
	return false;
}
int dfs(int u, int last) {
	if (u == t)
		return last;
	int ret = 0;
	for (int i = now[u]; i and last; i = edge[i].next) {
		now[u] = i;
		int v = edge[i].v;
		if (edge[i].w <= 0 or dis[v] != dis[u] + 1)
			continue;
		int tmp = dfs(v, min(last, edge[i].w));
		if (!tmp)
			dis[v] = oo;
		edge[i].w -= tmp;
		edge[i ^ 1].w += tmp;
		ret += tmp;
		last -= tmp;
	}
	return ret;
}
int dinic() {
	int ret = 0;
	while(bfs())
		ret += dfs(s, oo);
	return ret;
}
signed main() {
	IOS;
	cin >> n >> m >> s1 >> t1;
	s = n + 1, t = n + 2;
	for (int i = 1, u, v, lb, ub; i <= m; i++) {
		cin >> u >> v >> lb >> ub;
		in[v] += lb, out[u] += lb;
		add(u, v, ub - lb);
	}
	for (int i = 1; i <= n + 2; i++)
		if (in[i] > out[i]) {
			add(s, i, in[i] - out[i]);
			over += in[i] - out[i];
		} else
			add(i, t, out[i] - in[i]);
	add(t1, s1, oo);
	if (dinic() != over) {
		cout << "please go home to sleep";
		return 0;
	}
	s = s1, t = t1;
	cout << dinic();
	return 0;
}

4. 有源汇上下界最小流

求出可行流,把 \((t,s,[0,\infty])\) 删掉,再跑一次,前后两次相减。

五、Trick

  • 拆点连边限制通过单点的流量。
posted @ 2025-02-11 16:03  Garbage_fish  阅读(23)  评论(0)    收藏  举报