[网络流]最大流算法 Dinic

最近做了几道题,发现用Ek算法会超时,而事实上,Ek算法使用的机会并不多,更多的是用Dinic和ISAP算法。所以特地找了一段时间来学习、理解和编Dinic算法。

类似之前的储存方法,但稍作修改,代码如下:

struct Edge {
    int from, to, cap, flow;
};
这样储存的是一条边。在做题的过程中,发现一个技巧(也算是技巧吧),出现无向边时,不需要加两条边,而是直接将反向边的容量也改为cap,会减少很多多余的时间和空间。

插入边的过程与之前相同:

void AddEdge(int from, int to, int cap) {
    edges.push_back((Edge) {
        from, to, cap, 0
    });
    edges.push_back((Edge) {
        to, from, 0, 0
    });
    m = edges.size();
    G[from].push_back(m - 2);
    G[to].push_back(m - 1);
}

Dinic算法中,主要的操作过程如下:通过BFS构造层次图,然后DFS一次增广。

首先先来看看层次图。

在残量网络中,s到节点v的距离为d,那么d就是节点v的层次。只保留每个点到下一个层次的弧,也就是d(u) + 1 = d(v),的图,就是层次图。

层次图上的任意一条路径都是从s到层次1到层次2……直到t,可以发现,每一条这样的路径都是一条s-t最短路,这样求,不会出现走多余的边的情况。

如图,这样的一个图就是一个层次图。


从1开始,2和3都是层次1,4和5是层次2,6是层次3

在层次图上DFS求最大流自然很简单,就是不考虑反向边时能得到的最大流,多次增广后,重新计算层次图,发现s和t不连通,就退出。

它的时间复杂度不会很高,相对Ek会快很多,最多只会跑n-1次DFS,因为每次DFS之后s到t的路径至少会少一条,也就是最大距离至少会增加1,而每一次跑DFS最多会用nm的时间,每一次最多遍历每一个节点,每一个节点遍历它所链接的所有边,所以最终时间复杂度为O(N^2*M),相对Ek来说会快很多。

实际上,理论数值所对应的数据几乎不会出现,通常并不会想理论值说的这么慢,只会更快。

而且,对于它,还有一个很重要的优化,对于DFS来说,要保存一个“当前所有弧的最小残量”,如果等于0,必然是不能增广的,找到一条路径直接返回它的值即可,不然会出现多路增广不会退出的情况,还需要多加一次再算增广量,必然会很慢。

还有,当前弧优化,对于一次DFS中,可能会重复访问一个节点,虽然可能性比较小,这时候,已经走过的边自然是不需要再走,那么用一个数组记录上一次这个点走了哪些连接它的边,再次访问时不再重复走,那么就会更快。

下面为代码:

struct Dinic {
    int n, m, s, t;
    vector < Edge > edges;
    vector < int > G[MAXN];
    bool vis[MAXN];
    int d[MAXN];
    int cur[MAXN];

    void AddEdge(int from, int to, int cap) {
        edges.push_back((Edge) {
            from, to, cap, 0
        });
        edges.push_back((Edge) {
            to, from, 0, 0
        });
        m = edges.size();
        G[from].push_back(m - 2);
        G[to].push_back(m - 1);
    }

    bool BFS() {
        memset(vis, 0, sizeof vis);
        queue < int > Q;
        Q.push(s);
        d[s] = 0;
        vis[s] = 1;

        while(!Q.empty()) {
            int x = Q.front();
            Q.pop();

            for(int i = 0; i < G[x].size(); ++i) {
                Edge& e = edges[G[x][i]];

                if(!vis[e.to] && e.cap > e.flow) { //如果未曾访问过,而且这一条边处于残量网络中,那么计算层次
                    vis[e.to] = 1;
                    d[e.to] = d[x] + 1;
                    Q.push(e.to);
                }
            }
        }

        return vis[t];
    }

    int DFS(int x, int a) {
        if(x == t || a == 0) { // 如果找到一条增广的路或无法继续增广,返回当前流量
            return a;
        }

        int flow = 0, f;

        for(int& i = cur[x]; i < G[x].size(); ++i) {
            Edge& e = edges[G[x][i]];

            if(d[x] + 1 == d[e.to] && (f = DFS(e.to, min(a, e.cap - e.flow))) > 0) { // 如果处于同一个层次且这一条路径还能增广
                e.flow += f;
                edges[G[x][i] ^ 1].flow -= f;
                flow += f;
                a -= f;//增广 注意当前流量a要减f

                if(a == 0) {//如果a=0,那么不能继续增广,返回上一条路
                    break;
                }
            }
        }

        return flow;
    }

    int Maxflow(int s, int t) {
        this -> s = s;
        this -> t = t;

        int flow = 0;

        while(BFS()) { // 计算层次图 如果s-t不连通那么退出
            memset(cur, 0, sizeof cur); // 初始化当前弧
            flow += DFS(s, INF); // 计算最大流
        }

        return flow;
    }
} dinic;

当然,这里的DFS用的是递归实现的,但是如果Dinic依然会超时,那么可以尝试将DFS写成迭代的过程,会提高一些速度,但是也会相应地提高代码量,不如直接使用ISAP,不仅更快,而且代码稍微少一些。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值