网络流

创建时间:2025-03-03


网络的定义

设有向图 \(G=(V,E)\) 为一个网络(network),则 \(G\) 有一个源点(source)和一个汇点(sink),一般,我们将源点称为 \(s\),汇点称为 \(t\)\(G\) 的每条边有一个边权,表示这条边的容量(capacity),每条边都有一个流量,表示实际通过这条边的流(flow)的大小。

\(cap_e\) 为边 \(e\) 的容量, \(flow_e\) 为边 \(e\) 的流量,则 \(G\) 满足

  1. \[\forall e_{(x,y)} \in E, flow_e \le cap_e \]

  2. \[\forall v \in V,v \neq s,v \neq t, \sum x_{(u,v)} flow_x = \sum y_{(v,w)} flow_y \]

第1条被称为网络的“流量限制”,第2条被称为网络的“流量守恒”。

最大流

最大流的定义

对于任意网络,称其最大流(maxflow)为其源点到汇点的流量的最大值,即

\[maxflow = \sum e_{(s,x)} flow_e = \sum e_{(y,t)} flow_e \]

EK算法求解最大流

EK(Edmonds–Karp)的思想很简单,就是不断地通过BFS寻找一条长度最小的增广路\(^{\tiny{\textcircled{\scriptsize{1}}}}\),再更新增广路上所有边的边权(即还能通过的流量)和最大流的答案,直到 \(G\) 中不存在增广路。

可以发现,直接进行EK算法会遇到一个问题:之前的残余网络\(^{\tiny{\textcircled{\scriptsize{2}}}}\) 在当时是最优解,但不一定在后续也是最优解。为了解决这个问题,我们可以建立一个反悔机制:为每条边建立反向边,使正向边的边权+反向边边权=原边权。这样当我们当我们需要更改残余网络以确保答案最优性时,便可以先找到反边,再通过反边找到正边,使用如减少正边边权,增加反边边权的方式直接更改。

如何通过反边找到正边?这里需要用到一个技巧:使用链式前向星存边,将初始编号设为1(第一条边的编号为2),在建完正边后立即建反边,这样有正边编号1=反边编号,反边编号1=正边编号,如21=3,32=1。

以上便是EK算法的大体思路,具体实现如下:

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

struct Edge {
    int from, to, nxt;
    ll w;
};

const ll INF = 1e18;
const int MAX_N = 250;
const int MAX_M = 5050;

int n, m, s, t, pre[MAX_N];
int head[MAX_N], tot = 1;
Edge e[MAX_M * 2];
bool vis[MAX_N];
ll flow[MAX_N], maxflow;

void add_edge(int u, int v, ll w) {
    e[++tot] = Edge{u, v, head[u], w};
    head[u] = tot;
    e[++tot] = Edge{v, u, head[v], 0};
    head[v] = tot;
}

bool bfs() {
    fill(vis + 1, vis + n + 1, false);
    queue<int> q;
    flow[s] = INF;
    vis[s] = true;
    q.push(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;
            if (e[i].w && !vis[v]) {
                vis[v] = true;
                q.push(v);
                pre[v] = i;
                flow[v] = min(flow[u], e[i].w);
                if (v == t)
                    return true;
            }
        }
    }
    return false;
}

void update() {
    int cur = t;
    while (cur != s) {
        e[pre[cur]].w -= flow[t];
        e[pre[cur] ^ 1].w += flow[t];
        cur = e[pre[cur]].from;
    }
    maxflow += flow[t];
}

int main() {
    cin >> n >> m >> s >> t;
    for (int u, v, w; m--; ) {
        cin >> u >> v >> w;
        add_edge(u, v, w);
    }
    while (bfs())
        update();
    cout << maxflow;
    return 0;
}

注:

  1. 增广路,即一条从 \(s\)\(t\) 的路径,该路径上的所有边均还有边权,跑通增广路可以使最大流答案增加。
  2. 残余网络,指在经过一段操作后剩余的网络,其中每条边的边权表示还能通过它的流量大小。
  3. EK的时间复杂度为 \(O(nm^2)\),虽劣于Dinic的 \(O(n^2m)\),但也已经相当优秀了,且思维和代码难度远低于Dinic。

Dinic算法求解最大流

Dinic的思路与EK相似,但EK一次BFS只能找到一条增广路,Dinic一次能找到多条增广路。

Dinic和EK一样,都是通过BFS判断残余网络中是否还有增广路,但Dinic再BFS的同时对网络进行了分层,并使用DFS一次找到多条增广路。

具体实现如下:

const int INF = 0x3f3f3f3f;
const int MAX_N = 1000;
const int MAX_M = MAX_N * MAX_N;

int n, m, s, t, sum;
vector<int> p[MAX_N];
int sc[MAX_N], indg[MAX_N];
int head[MAX_N], tot = 1;
struct edge {
    int to, nxt, w;
} e[MAX_M * 2];
int dep[MAX_N], now[MAX_N];

void add_edge(int u, int v, int w) {
    e[++tot] = edge{v, head[u], w};
    head[u] = tot;
    e[++tot] = edge{u, head[v], 0};
    head[v] = tot;
}

bool bfs() {
    memset(dep, 0, sizeof(dep));
    memcpy(now, head, sizeof(now));
    queue<int> q;
    dep[s] = 1;
    q.push(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;
            if (!dep[v] && e[i].w) {
                dep[v] = dep[u] + 1;
                q.push(v);
                if (v == t)
                    return true;
            }
        }
    }
    return false;
}

int dfs(int u, int sum) {
    if (u == t)
        return sum;
    int flow = 0;
    for (int i = now[u]; i && sum; i = e[i].nxt) {
        now[u] = i;
        int v = e[i].to, &w = e[i].w;
        if (dep[v] == dep[u] + 1 && w) {
            int tmp = dfs(v, min(sum, w));
            if (tmp == 0) {
                dep[v] = 0;
                continue;
            }
            w -= tmp, e[i ^ 1].w += tmp;
            sum -= tmp, flow += tmp;
        }
    }
    return flow;
}

int Dinic() {
    int maxflow = 0;
    while (bfs())
        maxflow += dfs(s, INF);
    return maxflow;
}

注:

  • 不管是EK还是Dinic,最大流算法的时间复杂度一般都跑不满,Dinic在处理层数少的网络时复杂度甚至接近 \(O(n^2)\)

最大流的应用

最大流建模的题,其每条边通常都有一个限制,且限制一般是流量或选择次数,又或是类似二分图最大匹配的问题;当限制在点上时,可以使用拆点的技巧,例:
洛谷P2065(边数过多时的最大流)
洛谷P2763(最大流建模+拆点)
洛谷P2472(最大流建模+拆点)
洛谷P2766(DP+最大流建模+拆点)

最小割

最小割的定义

对于有向图 \(G=(V,E)\),割掉边权总和最少的边,使 \(G\) 中点 \(s\) 无法到达点 \(t\)

或者说,令 \(w_i\) 为边 \(i\) 的边权,找到集合 \(S \subseteq E\),满足删去 \(S\) 中所有边后\(s\) 无法到达 \(t\),且 \(\sum_{e \in S},w_e\) 最小。

最小割的实现

对于有向图 \(G\),我们可以先将 \(G\) 转化为一个网络,当 \(G\) 中存在增广路时,我们必须将这条路径割断,而割掉的那条边肯定是边权最小的那条边,不难发现,这条边权最小的边的边权恰好是这条增广路的流量!

故有 \(G\) 的最小割等于 \(G\) 的最大流。

具体实现同最大流,这里不再展示。

最小割的应用

最小割建模中 \(s\)\(t\) 的一条路径通常表示一种矛盾,而最小割正是要让每个人做出“二选一”或“选不选”的决定,将矛盾“割掉”,例:
洛谷P2057(最小割建模)
洛谷P2774(最小割建模)
洛谷P4313(最小割建模+虚拟节点)
洛谷P1646(最小割建模+虚拟节点)

最大权闭合子图

最大权闭合子图的定义

对于带点权的有向图 \(G\)(点权可以为负),找到点权总和最大的子图,满足该子图中所有节点的出边均在该子图中。

最大权闭合子图的实现

对于点权为正的点,将超级源点 \(s\) 向其连边,边权为其点权,表示在最大权闭合子图中选择这个点;其他的点,向超级汇点 \(t\) 连边,边权为其点权的绝对值,表示在最大权闭合子图中不选这个点;原图中的边,在网络中正常连边即可,边权为 \(\infty\)

这样,当 \(s\)\(t\) 存在路径时,一定有节点 \(x\) 在最大权闭合子图中,且存在 \(e_{(x,y)}\) 满足 \(y\) 不在最大权闭合子图中,这与最大权闭合子图的定义相矛盾!故要么从最大权闭合子图中将 \(x\) 踢出,即隔断 \(s\)\(x\) 的边,或将 \(y\) 加入最大权闭合子图,即隔断 \(y\)\(t\) 的边。

所以,我们可以通过最小割实现最大闭合子权图,其答案=正点权总和-最小割答案。

最大权闭合子图的应用

最大权闭合子图的建模常常表现在依赖关系,即选择某个点必须选择其他的几个点,例:
洛谷P3410(最大权闭合子图建模)
CF1082G(最大权闭合子图建模)
洛谷P2762(最大权闭合子图建模+输出方案)
洛谷P2805(最大权闭合子图建模+拓扑排序)
洛谷P3749(最大权闭合子图建模)

费用流

费用流的定义

顾名思义,对于网络 \(G\),其每条边都有一个费用(cost) \(c\),当大小为 \(f\) 的流流过这条边时会产生 \(f \cdot c\) 的费用。

最小费用最大流的实现

最小费用最大流,即在满足流量最大的前提下最小化费用。

有费用流的定义得,一条增广路上产生的费用为流量 \(f * \sum e_{(u,v,w,c)} c\),所以我们可以贪心的每次寻找 \(\sum e_{(u,v,w,c)} c\) 最小的增广路,即 \(s\)\(t\) 的最短路,这与先前最大流BFS的过程极为相像,故只需要将BFS替换成最短路算法并修改update函数即可。

那么应该选用怎样的最短路算法呢?考虑之前的反悔机制,显然,在费用流中,我们应该将反边的边权设为 \(-c\),所以我们便需要可以处理负权值的最短路算法(SPFA、bellman-ford等)。

这样便得到了最小费用最大流的解法,代码如下。

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

const ll INF = 1e18;
const int MAX_N = 5e3 + 50;
const int MAX_M = 5e4 + 50;

int n, m, s, t;
int head[MAX_N], tot = 1;
struct Edge {
    int from, to, nxt;
    ll w, c;
} e[MAX_M * 2];
int pre[MAX_N];
ll dis[MAX_N], flow[MAX_N];
ll maxflow, mincost;
bool inq[MAX_N];

void add_edge(int u, int v, ll w, ll c) {
    e[++tot] = Edge{u, v, head[u], w, c};
    head[u] = tot;
    e[++tot] = Edge{v, u, head[v], 0, -c};
    head[v] = tot;
}

bool SPFA() {
    fill(dis + 1, dis + n + 1, INF);
    dis[s] = 0, flow[s] = INF;
    queue<int> q;
    q.push(s);
    while (!q.empty()) {
        int u = q.front();
        q.pop(), inq[u] = false;
        for (int i = head[u]; i && flow[u]; i = e[i].nxt) {
            int v = e[i].to;
            if (e[i].w && dis[v] > dis[u] + e[i].c) {
                dis[v] = dis[u] + e[i].c;
                flow[v] = min(flow[u], e[i].w);
                pre[v] = i;
                if (!inq[v])
                    q.push(v), inq[v] = true;
            }
        }
    }
    return dis[t] != INF;
}

void update() {
    int cur = t;
    while (cur != s) {
        e[pre[cur]].w -= flow[t];
        e[pre[cur] ^ 1].w += flow[t];
        mincost += flow[t] * e[pre[cur]].c;
        cur = e[pre[cur]].from;
    }
    maxflow += flow[t];
}

int main() {
    cin >> n >> m >> s >> t;
    for (int u, v, w, c; m--; ) {
        cin >> u >> v >> w >> c;
        add_edge(u, v, w, c);
    }
    while (SPFA())
        update();
    cout << maxflow << ' ' << mincost;
    return 0;
}

注:

  • 在求解最小费用最大流时,EK和Dinic的时间复杂度差距较小,但EK代码短,思路更简单,所以一般情况下用EK求解最小费用最大流。

费用流的应用

费用流通常是要求最小费用最大流,一般在满足某种边的限制且某个量取到极值的前提下求最小/最大费用,例:
洛谷P4452(最小费用最大流建模+拆点)
洛谷P2517(最小费用最大流建模)
洛谷P2053(最小费用最大流建模+拆点)
洛谷P4553(最小费用最大流建模+拆点+强行满流处理)
洛谷P3159(最小费用最大流建模+拆点+贪心最优策略)

posted @ 2026-05-19 12:02  xubaichuan  阅读(15)  评论(0)    收藏  举报