网络流初步浅谈:EK与Dinic

  • 网络流这么美妙的东西

  • 肯定是要好好学它的

主要还是为了搞这个二分最大匹配去学的
匈牙利算法不太适合我,就来进阶一下(


  • 先来看最大流

最大流是解决从源点到汇点的最大流量,这是个网络流中几乎最基础的问题。
就像图论里面的最短路一样。
而我们的目的就是从
V个点,
E条有向边,
每个边有c(u,v)的容量,
解决这个抽水马桶最多能泵出多少水的问题。
这里有几个名词要解释一下:

  • 网络:一个有向图G=(V,E),有两个特殊节点:源点汇点
  • 边的流量:每条有向边(x,y)\(\in\)E都有一个权值c(x,y),称为边的流量。若(x,y)\(\notin\)E,则c(x,y)=0.
  • 最大流:从源点到汇点的最大流量。
  • 增广路:一条源点到汇点的所有边的剩余容量$\ge$0的路径
  • 残留网:在找到一条增广路之后,对这条增广路的所有边建立反向边。这样的话,在正向边被前面的增广路堵死的情况下,可以通过反向边“TD退流”。
  • 可行流应满足:
    • 1.容量限制\(f(x,y)\le c(x,y\)
    • 2.流量限制\(\sum_{(u,x)\in E} f(u,x)=\sum_{(x,v)\in E}f(x,v),x\ne S,x\ne T\)
    • \(\sum_{(S,v)\in E}f(S,v)\)称为整个网络的流量

我们很快就能想象到:
数据像洪水一样从S(源点)填充进边里
然后最后又汇聚到T(汇点)
被我们查询到最大流是多少
然后结束
这样,我们就想到了解决最大流的第一个最简单易懂的算法


  • EK算法

这是一个基于BFS实现的,使用DFS反复优化路径的算法。
听起来很难?但是实际非常轮椅简单
我们直接不解释,来实现一个这个程序的伪代码和代码模板:

e[i]存储第i条边的相关信息

其中v是e[i].to, c是e[i].capacity(容量),ne是e[i].nxt
总的来说是个链式前向星

h[u]存储u的第一条出边,使我们开始链式前向星枚举边的起始地址,也就是head[]

mf[v]存的是S~v这条路上的流量上限,也就是max flow

pre[v]存的是前驱边,我们能从e[i]找到v,也需要从pre[v]找到这第i条边

BFS()找增广路(最短路思想)

  • 1.初始化,mf[]=0,mf[S]=\(\infty\),S入队。
  • 2.只要队不空,u=q.front()出队,广搜基本操作
    • (1)枚举u的所有出边,更新u的最小容量,记录前驱边,扩展它的儿子入队
    • (2)若能走到T,则返回true,确认这是一条可行的增广路
    • (3)若走不到T,则返回false,我们求完最大流了

EK()求最大流(类似挤牙膏)
循环找增广路,每找到一条,

  • (1)逆序更新残留网,去维护这个图让BFS可以跑下一次,容量“你死我活此消彼长”
  • (2)累加可行流,最后返回最大值
struct edge {
    LL v, c, ne;
}e[M];//存储E条边
int h[N], idx = 1;
//idx要从2、3开始配对,因为0是边的终点,不能用0、1

void add (int a, int b, int c) {
    e[++idx] = { b, c, h[a]};//存入去往的点、容量、链式存储结构中的id
    h[a] = idx;
}
add(a, b, c);add(b, a, 0);
//建立反向边的时候容量从0开始,和正向边互补一下
bool BFS() {
    memset(mf, 0, sizeof(mf));//别忘记初始化mf
    queue < int > q;
    q.push(S);//源点入队
    mf[S] = 1e9;//因为是超级源点的说,后面一遍一遍去min它
    while(!q.empty()){//这一层如果还有下一层就继续搜下去
        int u = q.front();
        q.pop();//队头出队,搜下一层
        for(int i = h[u]; i; i = e[i].ne){
            LL v = e[i].v;
            if(mf[v] == 0 && e[i].c ){//如果没有被搜到过而且是有容量的
                md[v] = min(mf[u], e[i].c);//能不能塞下祖先的流量,如果不行就把整个流都变成自己的形状
                pre[v] = i;//记录自己前一条边的编号为i
                q.push(v);//入队继续BFS
                if(v == T)return true;//终于找到终点了,我要好好地return true
            }
        }
    }
    return false;//不存在增广路,我们已经贪完了,直接回dfs输出sum(max_flow)每条增广路最大流量的总和,就是最大流
}
//我们在拥有反向边的情况下,更新完残留网后的每一遍BFS,都拥有通过反向边与增广路的相互优化比较来确保贪心的后效性被补上,让原来的增广路顺利的 飞 起 来 。
LL EK(){
    LL flow = 0;//即ans
    while(bfs()){//有增广路被找到就while
        int v = T;//要从汇点逆着找上去,因为我们造了一个pre数组,已经通过BFS找到了这条增广路的路径了
        while(v != S){//开始回溯
            int i = pre[v]//找到回边
            e[i].c -= mf[T];//更新每个点的剩余流量
            e[i^1].c += mf[T]//i^1就是我们的反向边,因为和正向边互补,所以要加上mf,至于为什么是i^1,是因为我们存边的时候是两个两个存的,你要是愿意用i+1也行
            v = e[i^1].v;//通过反向边继续换在路径上的上一个的下一个找,也就是继续找回去
        }
        flow += mf[T];//总量叠加上这一条的量
    }
    return flow;
}

程序时间复杂度\(O\)(\(V^2E\))
时间复杂度烂完了


  • Dinic算法——EK的更优解

很明显,我们总感觉这么多遍BFS有点浪费
所以有神牛就想出算法来优化了。
所以我们预先将这张图按照节点和源点的距离,对每个点分层,在选择路径的时候按照这个层号递增地走。
好熟悉啊,这是什么……哦……是topological排序!是拓扑。
我们预先构造分层图,在同一分层图上多次寻找增广路,虽然好像最严格的的时间复杂度上界也是\(O\)(\(V^2E\)),但是平常的时候还是\(O\)(\(VE\))的,甚至对于二分图可以\(O\)(\(\sqrt{V}E\))

在实际应用中,Dinic 算法通常表现得非常优秀,尤其是在随机图或一般实际问题中,由于网络结构的稀疏性和增广路径分布较为均匀,使得分层和增广过程远低于最坏情况所描述的复杂度。事实上,仅有一些特意构造的数据或特殊的网络结构才能使得 Dinic 算法达到理论上的最差性能。
——知乎

这边有过EK的算法逻辑铺垫,直接给出代码模板和注释也没什么问题:
(所谓 弧 便是分的层)

#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;

const int INF = 0x3f3f3f3f;
const int MAXN = 1e4 + 5;
const int MAXM = 2e5 + 5;

struct Edge {
    int to, next, flow;
} edges[MAXM << 1];
int head[MAXN], cnt = 1;  // 从1开始存边

int level[MAXN], cur[MAXN];  // 当前弧优化
int n, m, s, t;  // 点数、边数、源点、汇点

inline void add_edge(int u, int v, int f) {
    edges[++cnt] = {v, head[u], f};
    head[u] = cnt;
    edges[++cnt] = {u, head[v], 0};  // 反向边初始为0
    head[v] = cnt;
}

bool bfs() {
    memset(level, 0, sizeof(level));
    queue<int> q;
    q.push(s);
    level[s] = 1;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int i = head[u]; i; i = edges[i].next) {
            int v = edges[i].to;
            if (edges[i].flow && !level[v]) {
                level[v] = level[u] + 1;
                q.push(v);
            }
        }
    }
    return level[t];
}

int dfs(int u, int flow) {
    if (u == t) return flow;
    int res = 0;
    for (int &i = cur[u]; i; i = edges[i].next) {  // 当前弧优化
        int v = edges[i].to;
        if (edges[i].flow && level[v] == level[u] + 1) {
            int f = dfs(v, min(flow, edges[i].flow));
            edges[i].flow -= f;
            edges[i ^ 1].flow += f;
            res += f;
            flow -= f;
            if (!flow) break;
        }
    }
    return res;
}

int dinic() {
    int maxflow = 0;
    while (bfs()) {
        memcpy(cur, head, sizeof(head));  // 重置当前弧
        maxflow += dfs(s, INF);
    }
    return maxflow;
}

int main() {
    scanf("%d%d%d%d", &n, &m, &s, &t);
    for (int i = 1; i <= m; ++i) {
        int u, v, f;
        scanf("%d%d%d", &u, &v, &f);
        add_edge(u, v, f);
    }
    printf("%d\n", dinic());
    return 0;
}

日拱一卒,功不唐捐——Noivelist

posted @ 2025-09-19 21:03  Noivelist  阅读(51)  评论(0)    收藏  举报