网络流算法总结

网络流算法总结

目录

  1. Edmonds-Karp 算法
  2. Dinic 算法
  3. SAP和ISAP 算法
  4. 预流推进算法和最高标号预流推进算法(鸽了,爽。ISAP够用了)
  5. 最小割问题(以Dinic为例)
  6. 最小费用路算法(Bellman-Ford/SPFA 变形)

Edmonds-Karp 算法

利用BFS找到所有的路径,只要找到一条路径没有满流量,就尽量往里面的边充流量,直到所有的路径都充满了不能再容纳新的流量为止。Edmonds-Karp算法是Ford-Fulkerson方法的一种实现,时间复杂度是O( n m 2 nm^2 nm2)。

代码

HDU 1532 AC确认,这道题裸板子,但是起点终点固定为1和n,并且有可能有重边,没有改代码

#include <iostream>
#include <queue>
#include <unordered_map>
#include <cstring>
/*
 * 潘骏同志模拟的EK算法
 */
using namespace std;
long long n,m;
struct edge
{
    long long to;
    long long cap;
    long long flow;
};
unordered_map<long long,edge> egs[100005];
//存边,兼具邻接矩阵和邻接表的优点,找负边也很方便,存取都是O(1)
bool vis[100005];
long long cap[100005];
long long pre[100005];
long long E(long long s,long long t)//BFS过程
{
    memset(vis,0,sizeof vis);
    memset(cap,0,sizeof cap);
    memset(pre,0,sizeof pre);
    queue<long long> qq;
    qq.push(s);
    vis[s]=1;
    cap[s]=0x3f3f3f3f3f3f3f3f;
    while(!qq.empty())
    {
        long long now=qq.front();
        qq.pop();
        for(auto it:egs[now])
        {
            edge nxt=it.second;
            if(!vis[nxt.to] && nxt.cap-nxt.flow>0)
            {
                cap[nxt.to]=min(cap[now],nxt.cap-nxt.flow);
                pre[nxt.to]=now;
                vis[nxt.to]=true;
                if(cap[t])
                {
                    return cap[t];
                }
                qq.push(nxt.to);
            }

        }

    }
    return 0;
}
long long K(long long s,long long t)//更新流量过程
{
    long long ans=0;
    long long greencap=E(s,t);//绿帽表示增量
    while(greencap!=0)
    {
        ans+=greencap;
        long long last=pre[t];
        long long now=t;
        while(last!=0)
        {
            egs[last][now].flow+=greencap;
            egs[now][last].flow-=greencap;//用于反悔,万一还有别的路能走更大流量
            now=last;
            last=pre[now];
        }

        greencap=E(s,t);
    }
    return ans;
}
int main()
{
    //freopen("testdata.in","r",stdin);
    while(cin>>m>>n) {
        for(int i=1;i<=n;i++) {
            egs[i].clear();
        }
        for (long long i = 0; i < m; i++) {
            long long a, b, c;
            cin >> a >> b >> c;
            edge temp;
            temp.to = b;
            temp.cap = c;
            temp.flow = 0;
            if(egs[a].find(b)==egs[a].end()){
                egs[a][b] = temp;
            } else {
                egs[a][b].cap+=c;//如果有重边就加上容量把边合并了
            }
            temp.to = a;
            temp.cap = 0;
            if(egs[b].find(a)==egs[b].end()){
                egs[b][a] = temp;
            }
        }
        cout << K(1, n) << endl;
    }
    return 0;
}

Dinic 算法

Dinic算法是利用上述Edmonds-Karp算法的最短增广路思想改进来的,我们需要找到阻塞流(即不考虑反向弧的情况下的极大流)来增广。因为最多要找n-1次(刘汝佳说的,我不知道啊)阻塞流,每次处理O( n m nm nm),所以总的复杂度为O( n 2 m n^2 m n2m).实际上一些特殊的图里面还更快一点。

代码

HDU 1532 AC确认,这道题裸板子,但是起点终点固定为1和n,并且有可能有重边,没有改代码

另外POJ 3469/百练3478 也是这套板子过的,就是时间有点丑陋8000+ms

#include <iostream>
#include <queue>
#include <unordered_map>
#include <cstring>
/*
 * 潘骏同志模拟的Dinic算法
 */
using namespace std;
long long n,m;
struct edge
{
    long long to;
    long long cap;
    long long flow;
};
unordered_map<long long,edge> egs[100005];
bool vis[100005];
long long dis[100005];
bool bfs(long long s,long long t)
{
    memset(vis,0,sizeof vis);
    memset(dis,0,sizeof dis);
    queue<long long> qq;
    qq.push(s);
    vis[s]=1;
    dis[s]=1;
    while(!qq.empty())
    {
        long long now=qq.front();
        qq.pop();
        for(auto it:egs[now])
        {
            edge nxt=it.second;
            if(!vis[nxt.to] && nxt.cap-nxt.flow>0)
            {
                dis[nxt.to]=dis[now]+1;//标记层次图
                vis[nxt.to]=true;
                qq.push(nxt.to);
            }

        }

    }
    if(dis[t]!=0)
    {
        return true;
    } else
    {
        return false;
    }
}
long long dfs(long long s,long long t,long long now,long long capper)
{
    //利用DFS找所有的阻塞流,这一过程最多执行n-1次
    if(now==t || capper==0)
    {
        //到达终点或者容量为零了就返回
        return capper;
    }
    long long flows=0;
    long long greenflow;
    for(auto it : egs[now])
    {
        if(it.second.cap>it.second.flow && dis[now]+1==dis[it.second.to]) {
            //判断是不是层次递增的允许弧,是否还有剩余流量
            greenflow = dfs(s, t, it.second.to, min(capper, it.second.cap - it.second.flow));
            if (greenflow > 0) {
                //残量网络还有可以增广的路径,增广
                egs[now][it.second.to].flow += greenflow;
                egs[it.second.to][now].flow -= greenflow;
                flows += greenflow;
                capper -= greenflow;
                if (capper == 0) {
                    return flows;
                }
            }
        }
    }
    return flows;
}
int main()
{
    //freopen("testdata.in","r",stdin);
    while(cin>>m>>n) {
        for(int i=1;i<=n;i++) {
            egs[i].clear();
        }
        for (long long i = 0; i < m; i++) {
            long long a, b, c;
            cin >> a >> b >> c;
            edge temp;
            temp.to = b;
            temp.cap = c;
            temp.flow = 0;
            if(egs[a].find(b)==egs[a].end()){
                egs[a][b] = temp;
            } else {
                egs[a][b].cap+=c;
            }
            temp.to = a;
            temp.cap = 0;
            if(egs[b].find(a)==egs[b].end()){
                egs[b][a] = temp;
            }
        }
        long long ans=0;
        while(bfs(1,n))
        {

            long long greencap=dfs(1,n,1,0x3f3f3f3f3f3f3f3f);
            while(greencap)
            {
                ans+=greencap;
                greencap=dfs(1,n,1,0x3f3f3f3f3f3f3f3f);
            }
        }
        cout<<ans<<endl;
    }
    return 0;
}

预流推进算法和最高标号预流推进算法

鸽了,爽。ISAP够用了,复杂度没啥区别的,就这样吧。。。

SAP和ISAP算法

SAP即最短增广路算法,ISAP即改进的最短增广路算法。SAP其实思想和Dinic是一样的,但是Dinic找层次图是每次都要重新进行BFS更新,SAP是直接改标号。因为本质和Dinic是一样的,所以时间复杂度上界仍然需要O( n 2 m n^2m n2m).

代码

HDU1532 AC确认,时间15ms,比dinic省时间多了,此处没有修改

#include <iostream>
#include <vector>
#include <cstring>
#include <queue>
/*
 * 潘骏同志模拟的ISAP算法
 */
using namespace std;
int n,m;
struct edge {
    int from;
    int to;
    int flow;
    int cap;
    int nxt;
    edge(int f, int t, int c, int n) : from(f), to(t), cap(c), nxt(n) {
        flow = 0;
    }
};
vector<edge> edges;
int egs[505];
void addedge(int f,int t,int c) {
    edges.emplace_back(f, t, c, egs[f]);
    egs[f] = edges.size() - 1;
    edges.emplace_back(t, f, 0, egs[t]);
    egs[t] = edges.size() - 1;
}
int dis[505];
bool vis[505];
bool bfs(int s,int t) {
    memset(vis, 0, sizeof vis);
    memset(dis, 0, sizeof dis);
    queue<int> qq;
    qq.push(t);
    vis[t] = 1;
    dis[t] = 0;
    while (!qq.empty()) {
        int now = qq.front();
        qq.pop();
        for (int i = egs[now]; i != -1; i = edges[i].nxt) {

            if (!vis[edges[i].to] && edges[i].cap - edges[i].flow > 0) {
                dis[edges[i].to] = dis[now] + 1;//标记层次图
                vis[edges[i].to] = true;
                qq.push(edges[i].to);
            }

        }

    }
    if (dis[t] != 0) {
        return true;
    } else {
        return false;
    }
}
int pre[505];
int aug(int s,int t) {
    int now = t;
    int greencap = 0x3f3f3f3f;
    while (now != s) {
        greencap = min(greencap, edges[pre[now]].cap - edges[pre[now]].flow);
        now = edges[pre[now]].from;
    }
    now = t;
    while (now != s) {
        edges[pre[now]].flow += greencap;
        edges[pre[now] ^ 1].flow -= greencap;
        now = edges[pre[now]].from;
    }
    return greencap;
}
int num[505];
int cur[505];
int isap(int s,int t) {
    bfs(s, t);
    memset(num, 0, sizeof num);
    for (int i = 1; i <= n; i++) {
        cur[i] = egs[i];//当前待增广的弧初始化为头节点
        num[dis[i]]++;//记录每一层有多少节点
    }
    int now = s;
    int maxflow = 0;
    while (dis[s] < n) {//如果源点到汇点的距离超过了n-1,说明有点经过了两次(从回边,因为总会有回边的flow<=cap),没有增广路存在了
        if (now == t) {//到达汇点,把当前的增广路流量加入最大瘤并回到源点重新开始
            maxflow += aug(s, t);
            now = s;
        }
        bool isoperated = false;
        for (int i = cur[now]; i != -1; i = edges[i].nxt) {
            if (edges[i].flow < edges[i].cap && dis[edges[i].from] == dis[edges[i].to] + 1) {//是还有容量的容许弧
                cur[now] = i;//前面的路径都增广过了,没必要增广了
                pre[edges[i].to] = i;
                now = edges[i].to;
                isoperated = true;
                break;//深度优先地进行增广
            }
        }
        if (!isoperated) {//没有允许弧能支持增广了,撤退
            int miner = n;
            for (int i = egs[now]; i != -1; i = edges[i].nxt) {
                if (edges[i].flow < edges[i].cap) {
                    miner = min(miner, dis[edges[i].to] + 1);//选取最接近汇点的点下次再来
                }
            }
            if (--num[dis[now]] < 0) {
                break;//改编号以后本层没有节点了,那么出现断层将没有增广路可以经过这里,那么就没有残量了,直接退出。
            }
            dis[now] = miner;
            num[dis[now]]++;//修改编号下次再来
            cur[now] = egs[now];//修改编号后所有的边都得重新检查,相当于dinic里重新找一条增广路
            if (now != s)now = edges[pre[now]].from;//回溯,找下一个,源点则是无路可退
        }
    }
    return maxflow;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    while(cin>>m>>n) {
        memset(egs, -1, sizeof egs);
        memset(pre,-1,sizeof pre);
        edges.clear();
        for (int i = 0; i < m; i++) {
            int a, b, c;
            cin >> a >> b >> c;
            addedge(a, b, c);
        }
        cout<<isap(1,n)<<endl;
    }
    return 0;
}

最小割问题(以Dinic为例)

s-t割定义(刘汝佳《算法竞赛入门经典》P369):

把所有顶点分成两个集合S和T=V-S,其中源点s在集合S中,汇点t在集合T中。如果把“起点在S中,重点在T中”的所有边删除,就无法从s到达t了。这样的集合划分称为一个s-t割,它的容量定义为: c ( S , T ) = ∑ u ∈ S , v ∈ T c ( u , v ) c(S,T)=\sum_{u\in S,v\in T}^{ } c(u,v) c(S,T)=uS,vTc(u,v) ,即起点在S中,终点在T中的所有边的容量和。

最小割最大流定理(刘汝佳《算法竞赛入门经典》P370):

在增广路算法结束时,f是s-t最大流,(S,T)是s-t最小割。

其实就是在Dinic最后一次BFS的时候能到达的边放进S,不能到达的边放进T,我们已经求出的最大流就是这个割的容量。

代码

此段代码插入最大流跑完以后的地方,如果只需要求容量其实根本不需要

vector<int> S;
vector<int> T;
for(int i=1;i<=n;i++)
{
    if(dis[i]==0)
    {
        T.push_back(i);
    }
    else
    {
        S.push_back(i);
    }
}

最小费用路算法 (Bellman-Ford/SPFA 变形)

在Edmonds-Karp的基础上,我们只需要做一个贪心(这里是指找最短路作为增广路的方法很贪心,SPFA/Bellman-Ford 本身其实是DP来着),就可以了。每次找增广路的时候都挑一条最短路(费用最小路)去增广,而不是像原版Edmonds-Karp一样无脑BFS。我们同样要把所有的路都增广到无法再增加流量为止,这样算出来的流量依然是最大的。因为我们需要加入负边来让最大流有退路可走,所以不能直接用Dijkstra来走最短路,最常用还是SPFA。

代码

洛谷P3381 AC确认(不开O2优化会T),该题是真的裸板子题,所以这里的版本根本没有改动

Update:改成链式前向星了,并且封装了mfmc函数,现在已经可以不开O2直接过了

#include <iostream>
#include <cstring>
#include <queue>
#include <vector>
#include <unordered_map>
#include <set>
using namespace std;
struct edge
{
    int from;
    int to;
    int flow;
    int cap;
    int cost;
    int nxt;
    edge(int f,int t,int c,int co,int n):from(f),to(t),cap(c),cost(co),nxt(n){
        flow=0;
    }
};
int egs[50005];
vector<edge> edges;
void addedge(int f,int t,int c,int co){
    edges.emplace_back(f,t,c,co,egs[f]);
    egs[f]=edges.size()-1;
    edges.emplace_back(t,f,0,-co,egs[t]);
    egs[t]=edges.size()-1;
}
int dis[50005];
bool vis[50005];
int pre[50005];
int visitor[50005];
int flower[50005];
int total;

int bellmanford(int s,int t)
{
    memset(dis,0x3f,sizeof dis);
    memset(vis,0,sizeof vis);
    memset(pre,-1,sizeof pre);//编号为0的边是存在的,所以初始化为-1
    memset(visitor,0,sizeof visitor);
    memset(flower,0,sizeof flower);
    dis[s]=0;
    flower[s]=0x3f3f3f3f;
    deque<int> qq;
    qq.push_back(s);
    vis[s]=true;
    while(!qq.empty())
    {
        int now=qq.front();
        qq.pop_front();
        visitor[now]++;
        if(visitor[now]>total)//因为一条路最多才n-1条边,如果超过n次遍历同一边肯定有负环
        {
            return -1;
        }
        for(int i=egs[now];i!=-1;i=edges[i].nxt)
        {

            int nxt=edges[i].to;
            int tempflow=min(flower[now],edges[i].cap-edges[i].flow);
            int lener=edges[i].cost;
            if(edges[i].cap-edges[i].flow>0 && dis[nxt]>dis[now]+lener)
            {
                dis[nxt]=dis[now]+lener;
                pre[nxt]=i;//松弛了的且不在队列的点才能进队列哦
                flower[nxt]=tempflow;
                if(!vis[nxt])
                {
                    if((!qq.empty()) && dis[nxt]<dis[qq.front()])//小的优先遍历
                        qq.push_front(nxt);
                    else
                        qq.push_back(nxt);
                    vis[nxt]=true;
                }
            }
        }
        vis[now]=false;
    }
    if(dis[t]<0x3f3f3f3f)
        return flower[t];//dis[t]如果是无穷大则说明到不了终点,没有增广路了
    else
        return -1;
}
pair<int,int> mfmc(int s,int t){
    long long maxflow=0;
    long long mincost=0;
    long long nowflow=0;
    while((nowflow=bellmanford(s,t))>0)//找不到增广路的时候才停止
    {
        //cout<<nowflow<<endl;
        maxflow+=nowflow;
        mincost+=dis[t]*nowflow;
        int now=t;
        while(now!=s)
        {
            edges[pre[now]].flow+=nowflow;
            edges[pre[now]^1].flow-=nowflow;//负边相应减去流量
            now=edges[pre[now]].from;
        }
    }
    return make_pair(maxflow,mincost);
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    //freopen("testdata.in","r",stdin);
    //freopen("testdata.out","w",stdout);
    int n,m;
    while(cin>>n>>m)
    {
        total=n;
        long long s,t;
        cin>>s>>t;
        memset(egs,-1,sizeof egs);
        edges.clear();
        for(long long i=0; i<m; i++)
        {
            long long ss,ee,capper,coster;
            cin>>ss>>ee>>capper>>coster;
            addedge(ss,ee,capper,coster);
        }
        pair<int,int> mfmcer=mfmc(s,t);
        cout<<mfmcer.first<<" "<<mfmcer.second<<endl;
    }
    return 0;
}

参考了以下文章,在此表示感谢:

https://zhuanlan.zhihu.com/p/46039732

https://www.cnblogs.com/owenyu/p/6852664.html

https://www.renfei.org/blog/isap.html

维基百科Edmonds-Karp算法

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值