最大网络流的多种解法(洛谷P3376 网络最大流 为例)

P3376 【模板】网络最大流 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)icon-default.png?t=M276https://www.luogu.com.cn/problem/P3376

本题是一个最大网络流的模版题,我也是刚刚学完最大网络流,所以就借此机会,练练手。

关于最大网络流的定义这里就不多做介绍(口齿不清)。如果不知道的可以去先看一下大佬的文章

                                                        这里给出一个链接(8条消息) 网络流之最大流算法(EdmondsKarp)_Yoangh的博客-CSDN博客_最大流算法icon-default.png?t=M276https://blog.csdn.net/y990041769/article/details/21026445?ops_request_misc=&request_id=&biz_id=102&utm_term=%E6%9C%80%E5%A4%A7%E7%BD%91%E7%BB%9C%E6%B5%81&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-3-21026445.142^v2^pc_search_result_control_group,143^v4^control&spm=1018.2226.3001.4187

那么回归到这题。

下面将用4种解法来对这题进行解答。

Edmons_Karps算法 (采用的是Ford_Fulkerson方法)        O(VE^{2})

使用BFS来计算增广路径,由于这种算法复杂度高,只能用于小图,所以邻接矩阵存图即可

Ford_Fulkerson方法的思路大致是

        1. 初始所有边的流量为0

        2.找到一条s -> t的路径按三大性质得到该路径的最大流

        3.更新这次搜索结束后的残流网络。

        不断重复2,3过程直到找不到路径

 第一次提交代码(73分 Wa了3个点)

        

#include<bits/stdc++.h>
using namespace std;

//一些习惯的简化
#define el '\n'
#define rep(i, a, b) for(int i = (a); i <= (b); i++)
#define lop(i, a, b) for(int i = (a); i <  (b); i++)
#define dwn(i, a, b) for(int i = (a); i >= (b); i--)
#define ms(a, b) memset(a, b, sizeof(a))

typedef long long LL;
const int MAXN = 250;
const LL INF = 9223372036854775807;
int n, m;
int edge[MAXN][MAXN];
int pre[MAXN];          
LL bfs(int s, int t){          //增广路搜索
    LL flow[MAXN];
    memset(pre, -1, sizeof(pre));
    flow[s] = INF,  pre[s] = 0;
    queue<int> Q;
    Q.push(s);
    while(!Q.empty()){
        int u = Q.front();
        Q.pop();
        if(u == t)
            break;
        rep(i, 1, n){
            if(i != s && edge[u][i] && pre[i] == -1){
                pre[i] = u;
                Q.push(i);
                flow[i] = min(flow[u], (LL)edge[u][i]);
            }
        }
    }
    if(pre[t] == -1)//已无路径
        return -1;
    return flow[t];
}
LL Edmonds_Karp(int s, int t){ //使用Ford_Fulkerson方法bfs + 更新残流网络
    LL maxflow = 0;
    while(1){
        LL nowflow = bfs(s, t);
        if(nowflow == -1)
            break;
        int cur = t;
        while(cur != s){    //更新残流网络
            int fa = pre[cur];
            edge[cur][fa] += nowflow;
            edge[fa][cur] -= nowflow;
            cur = fa;
        }
        maxflow += nowflow;
    }
    return maxflow;
}
int main(){
    int s, t;
    cin >> n >> m >> s >> t;
    rep(i, 1, m){
        int u, v, w;
        cin >> u >> v >> w;
        edge[u][v] = w;
    }
    cout << Edmonds_Karp(s, t);
    return 0;
}

过了8个点可以说整体代码思路是没问题的,那么问题出在哪呢?看了一下,感觉应该是被卡long long等等数据出现操作问题了所以应该只需要修改一下细节就可以了。 然后把longlong改完发现还是不行,那是什么问题呢?????

下载了测试样例8,发现,居然有很多重边。。。(我还是太水了)

调整了一下输入,重边的叠加,然后改long long果然就过了

#include<bits/stdc++.h>
using namespace std;

//一些习惯的简化
#define el '\n'
#define rep(i, a, b) for(int i = (a); i <= (b); i++)
#define lop(i, a, b) for(int i = (a); i <  (b); i++)
#define dwn(i, a, b) for(int i = (a); i >= (b); i--)
#define ms(a, b) memset(a, b, sizeof(a))

typedef long long LL;
const int MAXN = 250;
const int INF = 0x3f3f3f3f;
const LL LNF = 9223372036854775807;
int n, m;
LL edge[MAXN][MAXN];
int pre[MAXN];          
LL bfs(int s, int t){          //增广路搜索
    LL flow[MAXN];
    memset(pre, -1, sizeof(pre));
    flow[s] = LNF,  pre[s] = 0;
    queue<int> Q;
    Q.push(s);
    while(!Q.empty()){
        int u = Q.front();
        Q.pop();
        if(u == t)
            break;
        rep(i, 1, n){
            if(i != s && edge[u][i] && pre[i] == -1){
                pre[i] = u;
                Q.push(i);
                flow[i] = min(flow[u], edge[u][i]);
            }
        }
    }
    if(pre[t] == -1)//已无路径
        return -1;
    return flow[t];
}
LL Edmonds_Karp(int s, int t){ //使用Ford_Fulkerson方法bfs + 更新残流网络
    LL maxflow = 0;
    while(1){
        LL nowflow = bfs(s, t);
        if(nowflow == -1)
            break;
        int cur = t;
        while(cur != s){    //更新残流网络
            int fa = pre[cur];
            edge[cur][fa] += nowflow;
            edge[fa][cur] -= nowflow;
            cur = fa;
        }
        maxflow += nowflow;
    }
    return maxflow;
}
int main(){
    int s, t;
    cin >> n >> m >> s >> t;
    rep(i, 1, m){
        int u, v, w;
        cin >> u >> v >> w;
        if(!edge[u][v])
            edge[u][v] = w;//反边其实已经初始化为0
        else
            edge[u][v] += w;
    }
    cout << Edmonds_Karp(s, t);
    return 0;
}

 

Dinic算法

下面的 Dinic 可解决 FF 效率低的问题。

  • 每次多路增广:u 点通过一条边,向 v 输出流量以后,v 会尝试到达汇点(到达汇点才真正增广),然后 v 返回实际增广量。这时,如果 u 还有没用完的供给,就继续尝试输出到其它边。

    但是要警惕绕远路、甚至绕回的情况,不加管制的话极易发生。怎么管?

  • 源点顺着残量网络想要到达其它点,需要经过一些边对吧?按照经过的边数(即源点出发以后的距离)把图分层,即用 bfs 分层。 每次尝试给予时,只考虑给予自己下一层的点,就可以防止混乱。

  • 综合上面两条。每回合也是从源点出发,先按照当前残量网络分一次层,随后多路增广,尽可能增加流量。增广过程中,会加入一些反向边,这些反向边逆着层次图,本回合并不会走。所以还需要进入下一回合。一直到 bfs 分层时搜不到汇点(即残量网络断了)为止。

上诉引用大佬思路        https://www.luogu.com.cn/blog/cicos/Dinic

然后就是代码段还是一样要注意开long long,然后我们这里用前向星的储存方法写,代码中有很多细节比如cnt 必须从 1开始否者会错误等等都在代码中有详细注释

#include<bits/stdc++.h>
using namespace std;

//一些习惯的简化
#define el '\n'
#define rep(i, a, b) for(int i = (a); i <= (b); i++)
#define lop(i, a, b) for(int i = (a); i <  (b); i++)
#define dwn(i, a, b) for(int i = (a); i >= (b); i--)
#define ms(a, b) memset(a, b, sizeof(a))

typedef long long LL;
const int MAXN = 200010;
const int INF = 0x3f3f3f3f;
const LL LNF = 9223372036854775807;
int n, m, s, t;
struct edge{
    LL w;
    int to, next;
}e[MAXN];//前向星法储存
int head[10001];
int cnt;
LL ans = 0;

void init(){//初始化
    rep(i, 0, n){
        head[i] = -1;
        e[i].next = -1;
    }
    cnt = 1;//必须从1开始,如果从0开始会有错误???
}

void addage(int u, int v, int w){//前向星加边
    e[++cnt].to = v;
    e[cnt].w = w;
    e[cnt].next = head[u];
    head[u] = cnt;
}

int dep[10001];
int l, r, que[10001];//手写队列
bool fc_bfs(){//bfs分层记录
    memset(dep, 0, sizeof(dep));
    dep[s] = 1;
    // que[l = r = 1] = s;
    queue<int> Q;
    Q.push(s);
    //while(l <= r){
    while(!Q.empty()){
        //int u = que[l++];
        int u = Q.front();
        Q.pop();
        for(int i = head[u]; i != -1; i = e[i].next){
            int v = e[i].to;
            if(e[i].w && !dep[v]){
                dep[v] = dep[u] + 1;
                //que[++r] = v;
                Q.push(v);
            }
        }
    }
    return dep[t];//如果dep[t] = 0 说明已经无增广路可搜到汇点
}
LL Dinic(int u, LL in){//u代表出发点,in代表的是当前u可供给的水量(in不一定会用完)
    if(u == t)//已经到达汇点
        return in;
    LL flow = 0;//记录从u到达终点的最大流量
    for(int i = head[u]; i != -1 && in; i = e[i].next){
        int v = e[i].to;
        if(e[i].w && dep[v] == dep[u] + 1){//只遍历下一层
            LL res = Dinic(v, min(in, e[i].w));
            e[i].w -= res;
            e[i ^ 1].w += res;//反边残流
            in -= res;
            flow += res;
        }
    }
    if(flow == 0)//剪枝
        dep[u] = 0;//代表这个点无法达到汇点可以去除,以后不再遍历
    return flow;
}
int main(){
    cin >> n >> m >> s >> t;
    init();
    rep(i, 1, m){
        int u, v, w;
        cin >> u >> v >> w;
        addage(u, v, w);
        addage(v, u, 0);//反边先加上
    }
    // rep(u, 1, n){ //判断前向星准确性
    //     for(int i = head[u]; i != -1 ; i = e[i].next){
    //         cout << e[i].to << " " << e[i].w << el;
    //     }
    //     cout << el;
    // }
    while(fc_bfs())//如果能找到增广路
        ans += Dinic(s, LNF);//初始点会水量无限大
    cout << ans;
    return 0;
}

 

最后写2种最高深也是最快的算法ISAP与HLPP

        如果想要了解更多的可以去看看这个大佬的文章https://www.luogu.com.cn/blog/ONE-PIECE/jiu-ji-di-zui-tai-liu-suan-fa-isap-yu-hlpp

ISAP(Improved Shortest Augumenting Path)

在 dinic 中,我们要跑许多遍 bfs ,这就有可能导致算法效率不高。

于是 ISAP 就这样出现了,它只需要跑一遍 bfs !

大体运行过程如下:

        1.从汇点t开始反向跑回s进行一次bfs,层数从t为0层开始叠加

        2.从源点到t再进行dfs,这里末尾需要维护2个数组dep[]和gap[]

                        dep[]还是记录层数

                                gap[]记录处于某层的点数数量

        3.重复(2)操作直到出现断层(gap[u] == 0)退出

其他细节就看代码里的吧。

#include<bits/stdc++.h>
using namespace std;

//一些习惯的简化
#define el '\n'
#define rep(i, a, b) for(int i = (a); i <= (b); i++)
#define lop(i, a, b) for(int i = (a); i <  (b); i++)
#define dwn(i, a, b) for(int i = (a); i >= (b); i--)
#define ms(a, b) memset(a, b, sizeof(a))

typedef long long LL;
const int MAXN = 200010;
const int INF = 0x3f3f3f3f;
const LL LNF = 9223372036854775807;
int n, m, s, t;
struct edge{
    LL w;
    int to, next;
}e[MAXN];//前向星法储存
int head[10001];
int cur[10001];
int cnt;
LL maxflow = 0;

void init(){//初始化
    rep(i, 0, n){
        head[i] = -1;
        e[i].next = -1;
    }
    cnt = 1;//必须从1开始,如果从0开始会有错误???
}

void addage(int u, int v, int w){//前向星加边
    e[++cnt].to = v;
    e[cnt].w = w;
    e[cnt].next = head[u];
    head[u] = cnt;
}

int dep[10001], gap[10001];
int l, r, que[10001];//也可以手写队列
void fc_bfs(){//bfs分层记录
    memset(dep, -1, sizeof(dep));
    memset(gap, 0, sizeof(gap));
    gap[0] = 1; //汇点t为第0层
    dep[t] = 0;
    // que[l = r = 1] = t;
    queue<int> Q;
    Q.push(t);
    //while(l <= r){
    while(!Q.empty()){
        //int u = que[l++];
        int u = Q.front();
        Q.pop();
        for(int i = head[u]; i != -1; i = e[i].next){
            int v = e[i].to;
            if(dep[v] == -1){//这里注意反跑判断不考虑边权值
                dep[v] = dep[u] + 1;
                gap[dep[v]]++;
                //que[++r] = v;
                Q.push(v);
            }
        }
    }
    return;
}
LL dfs(int u, LL in){//u代表出发点,in代表的是当前u可供给的水量(in不一定会用完)
    if(u == t){//已经到达汇点
        maxflow += in;
        return in;
    }
    LL flow = 0;//记录从u到达终点的最大流量
    for(int i = cur[u]; i != -1; i = e[i].next){
        cur[u] = i;     //当前弧优化
        int v = e[i].to;
        if(e[i].w && dep[v] + 1 == dep[u]){//只遍历下一层
            LL res = dfs(v, min(in, e[i].w));
            e[i].w -= res;
            e[i ^ 1].w += res;//反边残流
            in -= res;
            flow += res;
            if(in == 0)//流已经全部流完了
                return flow;
        }
    }
    //能到这里说明in还有剩余但是不会再使用了隔开点u和后续的所有v
    if(--gap[dep[u]] == 0) dep[s] = n + 1;//断层退出
    dep[u]++;
    gap[dep[u]]++;
    return flow;
}

LL ISAP(){
    maxflow = 0;
    fc_bfs();
    while(dep[s] < n){
        memcpy(cur, head, sizeof(head));    //初始当前弧优化
        dfs(s, LNF);
    }
    return maxflow;
}
int main(){
    cin >> n >> m >> s >> t;
    init();
    rep(i, 1, m){
        int u, v, w;
        cin >> u >> v >> w;
        addage(u, v, w);
        addage(v, u, 0);//反边先加上
    }
    // rep(u, 1, n){ //判断前向星准确性
    //     for(int i = head[u]; i != -1 ; i = e[i].next){
    //         cout << e[i].to << " " << e[i].w << el;
    //     }
    //     cout << el;
    // }
    cout << ISAP();
    return 0;
}

 到这里我们已经可以发现从开始的代码到现在,时间复制度不断优化,越来越快了,但还没完,还有更快的那就是最终HLPP

最终HLPP(这种方法我还没有写,所以思路可参见前面引用的大佬文章%%%)

最终附上4种算法的时间测试

序号DinicFFEKEK终极 HLPPISAP
10.625sTLE0.171s0.125s0.265s
20.562sTLE0.156s0.093s0.265s
30.828sTLE0.625s0.093s0.390s
40.578sTLE0.312s0.093s0.328s
52.468s24.000s0.046s0.078s0.218s
65.546sTLE0.078s0.140s0.203s
75.218s10.984s0.109s0.125s0.328s
87.812s49.953s0.218s0.109s0.265s
91.281sTLE0.375s0.078s0.375s
100.781sTLE0.156s0.062s0.187s
110.312sTLE0.046s0.093s0.203s
120.875sTLE2.703s0.078s0.328s
130.703sTLE0.156s0.156s0.203s
140.500sTLE0.328s0.109s0.218s
150.296sTLE0.171s0.109s0.296s
160.562sTLE0.234s0.125s0.296s
174.687sTLE0.140s0.093s0.343s
182.921sTLE0.031s0.156s0.296s
192.359sTLE0.040s0.078s0.312s
204.656sTLE0.078s0.062s0.390s
210.500sTLE0.312s0.093s0.218s
221.000sTLE0.203s0.109s0.234s
230.343sTLE0.062s0.156s0.265s
241.015sTLE0.281s0.140s0.328s
总用时46.428s-7.037s2.553s6.754s

上面测试转自https://www.luogu.com.cn/blog/181775/solution-p3376

如果你看到了这里,感谢你看完了我的(废话)。

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值