最大流算法之EK(最短路径增广算法)

这是网络流最基础的部分——求出源点到汇点的最大流(Max-Flow)。
最大流的算法有比较多,本次介绍的是其中复杂度较高,但是比较好写的EK算法。(不涉及分层,纯粹靠BFS找汇点及回溯找最小流量得到最终的答案)

EK算法,全名Edmonds-Karp算法(最短路径增广算法)。

首先简单介绍一下网络流的基本术语
源点:起点。所有流量皆从此点流出。只出不进
汇点:终点。所有流量最后汇集于此。只进不出
流量上限:有向边(u,v)(及弧)允许通过的最大流量
增广路:一条合法的从源点流向汇点的路径
计算最大流就是不停寻找增广路找到最大值的过程。

合法的网络流具有以下性质
1.f(i,j) <= c(i,j);//任意有向边的流量一定小于等于该边的流量限制
2.f(i,j) = -f(j,i);//从i流向j的流量与j流向i的流量互为相反数反向边
3.out(i) = in(i);//除源点、汇点外,任意一点i流入的流量与流出的相等(只是路过

这里写图片描述(截自洛谷)

EK算法思路
1.通过BFS拓展合法节点(每个节点在本次BFS中仅遍历一次),找到汇点,并记录每个节点的前面节点(pre)(若找不到增广路,算法结束)
2.通过BFS的记录,从汇点回溯回源点,记录下每条弧流量的**最小值**minn, ans += minn(否则就会超出某条边的限制流量)
3.将所有经过的边的流量减去minn,反向边加上minn
4.重复上述步骤,直到找不到增广路,算法结束。

朴素版EK
最为简单的写法,通过邻接矩阵存储。
优点:代码简单,一目了然。
缺点:轻易爆内存,(N^2)的空间太大,N >10000基本就废了(MLE)。
源代码:

#include <bits/stdc++.h> 
using namespace std;
#define INF 0x3f3f3f
#define maxn 10005

int n, m, st, en, flow[maxn][maxn], pre[maxn];
int q[maxn], curr_pos, st_pos, end_pos;
bool wh[maxn];
int max_flow;

void Init()//初始化
{
    int i, a, b, c;
    scanf("%d%d%d%d", &n, &m, &st, &en);
    for(i = 0; i != m; ++i)
    {
        scanf("%d%d%d", &a, &b, &c);
        flow[a][b] += c;
    }
    return ;
}

bool Bfs(int st, int en)//广搜找源点
{
    st_pos = -1, end_pos = 0;
    memset(wh, 0, sizeof wh);
    wh[st] = 1;
    q[0] = st;
    while(st_pos != end_pos)
    {
        curr_pos = q[++st_pos];
        for(int i = 1; i != n+1; ++i)
        {
            if(!wh[i] && flow[curr_pos][i] > 0)
            {
                wh[i] = 1;
                pre[i] = curr_pos;
                if(i == en)
                {
                    return true;
                }
                q[++end_pos] = i;
            }
        }
    }
    return false;
} 

int EK(int start_pos, int end_pos)
{
    int i, minn;
    while(Bfs(start_pos, end_pos))//回溯
    {
        minn = INF;

        for(i = end_pos; i != start_pos; i = pre[i])
        {
            minn = min(minn, flow[pre[i]][i]);
        } 

        for(i = end_pos; i != start_pos; i = pre[i])
        {
            flow[pre[i]][i] -= minn;
            flow[i][pre[i]] += minn;//反向弧加上该值(具体原因下文详解)
        } 
        max_flow += minn;
    }
    return max_flow;
}

int main()
{
    //freopen("test.in", "r", stdin);
    //freopen("test.out", "w", stdout);

    Init();

    printf("%d", EK(st, en));

    //fclose(stdin);
    //fclose(stdout);
}

经过洛谷测评(P3376 【模板】网络最大流),50分,MLE五个点

关于反向边加上最小流值的原因:
如果存在a->b有流,那么如果某一点c->b有流,则流量实际上可以再从b流到a,可以理解为把原本a->b的流量怼了回去
BFS遍历得到的剩下部分的增广路的流量实际上是原本a->b的流,而原本a->b之后流向汇点的增广路的部分流量变为由c->b的流量
增广路依旧合法

这证明了邻接矩阵是不可行的,于是我们想到了麻烦一点但是省空间的存储方法——边表至少从m*m优化到n*m,实则通常n*(m/10)便足够。

定义一个结构体

struct qi{
    int st, en, num;//弧的始点,终点,权值
}flow[maxm];

相对应的广搜也要稍作调整,注意一下pre的存储方法:
pre[i]=k,i是点,k是弧的编号

bool Bfs(int st, int en)
{
    st_pos = -1, end_pos = 0;
    memset(wh, 0, sizeof wh);
    wh[st] = 1;
    q[0] = st;
    while(st_pos != end_pos)
    {
        curr_pos = q[++st_pos];
        for(int i = 0; i < 2*m; ++i)
        {
            if(flow[i].st == curr_pos && flow[i].num > 0 && !wh[flow[i].en])
            {
                ne = flow[i].en;
                wh[ne] = 1;
                pre[ne] = i;
                if(ne == en)
                {
                    return true;
                }
                q[++end_pos] = flow[i].en;
            }
        }
    }
    return false;
} 

所以Tracback的遍历循环也要改变。

for(i = end_pos; i != st; i = flow[pre[i]].st)//注意一下i如何取下一个的
{
    minn = min(minn, flow[pre[i]].num);
} 

事实证明:MLE的问题完美解决,测试结果:94ms , 18785kb,TLE三个点

那么还需要优化一下时间
注意BFS拓展时的循环:i:=0->2*m//m是弧的个数(因为有反向边,故是2*m)
由于题目范围是m <= 100000,每次最大循环达到2e5,太浪费了。
于是我们在边表再做一点手脚:开二维数组对每个始点所有的弧分别记录
x[i][m]指以i为始点的第m条弧(再开一个num[i]记录每个始点弧的个数)。
那么代码就更加繁琐了。

首先是读入优化:

int read()
{
    input = 0;
    a = getchar();
    while(a < '0' || a > '9')
        a = getchar();
    while(a >= '0' && a <= '9')
    {
        input = input*10+a-'0';
        a = getchar();
    }
    return input;
}

然后是初始化:

void Init()
{
    int i, a, b, c;
    memset(num, -1, sizeof num);
    n = read(), m = read(), st = read(), en = read();
    for(i = 0; i != m; ++i)
    {
        flow[i].st = read();
        flow[i].en = read();
        flow[i].num = read();
        re[flow[i].st][++num[flow[i].st]] = i;
        re[flow[i].en][++num[flow[i].en]] = i+m;//初始化时将第i条弧的反向边编号为i+m
    }
    for(int i = m; i != 2*m; ++i)
    {
        flow[i].st = flow[i-m].en;
        flow[i].en = flow[i-m].st;
        flow[i].num = 0;
    }
    pre[st] = -1;
    return ;
}

BFS部分:

bool Bfs(int st, int en)
{
    int i, j;
    st_pos = -1, end_pos = 0;
    memset(wh, 0, sizeof wh);
    wh[st] = 1;
    q[0] = st;
    while(st_pos != end_pos)
    {
        curr_pos = q[++st_pos];
        for(i = 0; i < num[curr_pos]+1; ++i)
        {
            j = re[curr_pos][i];//当前可拓展节点的候选节点,即当前节点的第i条弧的终点
            if(flow[j].st == curr_pos && flow[j].num > 0 && !wh[flow[j].en])
            {
                ne = flow[j].en;
                wh[ne] = 1;
                pre[ne] = j;
                if(ne == en)
                {
                    return true;
                }
                q[++end_pos] = flow[j].en;
            }
        }
    }

    return false;
} 

Traceback只需改变反向边部分:

flow[pre[i]+m].num += minn;

主函数部分不需要调整。

最终测评结果:392ms , 56988kb,AC。

通过上述过程,我们得知网络流的存储结构应该为边表而非邻接矩阵。当然,使用vector来存储弧也是可以的,这样会更大程度上节省空间,但会对时间造成一定影响,根据实际情况自行取舍。


最后附上完整代码:

/*
Max_flow- EK
2017/04/07
While(BFS can find the end)
for i,i->j:=st -> end: 
f[i][j] -= minn;f[j][i] += minn; ans += minn;//so we can delete at least one edge(the one which satisfies f[i][j] == minn) 
*/
#include <bits/stdc++.h> 
using namespace std;
#define INF 0x3f3f3f
#define maxm 200005
#define maxn 10005

struct qi{int st, en, num;}flow[maxm];
int n, m, st, en, pre[maxn], re[maxn][maxn/10], num[maxn];
int q[maxn], curr_pos, st_pos, end_pos, ne, max_flow;
bool wh[maxn];
int input;
char a;

int read()
{
    input = 0;
    a = getchar();  
    while(a < '0' || a > '9')
        a = getchar();
    while(a >= '0' && a <= '9')
    {
        input = input*10+a-'0';
        a = getchar();
    }
    return input;
}

void Init()
{
    int i, a, b, c;
    memset(num, -1, sizeof num);

    n = read(), m = read(), st = read(), en = read();
    for(i = 0; i != m; ++i)
    {
        flow[i].st = read();
        flow[i].en = read();
        flow[i].num = read();
        re[flow[i].st][++num[flow[i].st]] = i;
        re[flow[i].en][++num[flow[i].en]] = i+m;
    }
    for(i = m; i != 2*m; ++i)
    {
        flow[i].st = flow[i-m].en;
        flow[i].en = flow[i-m].st;
    }
    return ;
}

bool Bfs(int st, int en)
{
    int i, j;
    st_pos = -1, end_pos = 0;
    memset(wh, 0, sizeof wh);
    wh[st] = 1;
    q[0] = st;

    while(st_pos != end_pos)
    {
        curr_pos = q[++st_pos];
        for(i = 0; i < num[curr_pos]+1; ++i)
        {
            j = re[curr_pos][i];
            if(flow[j].st == curr_pos && flow[j].num > 0 && !wh[flow[j].en])
            {
                ne = flow[j].en;
                wh[ne] = 1;
                pre[ne] = j;
                q[++end_pos] = flow[j].en;
                if(ne == en)
                    return true;         
            }
        }
    }

    return false;
} 

int EK(int start_pos, int end_pos)
{
    int i, minn;
    while(Bfs(start_pos, end_pos))
    {
        minn = INF;

        for(i = end_pos; i != st; i = flow[pre[i]].st)
        {
            minn = min(minn, flow[pre[i]].num);
        } 

        for(i = end_pos; i != st; i = flow[pre[i]].st)
        {
            flow[pre[i]].num -= minn;
            flow[pre[i]+m].num += minn;
        } 

        max_flow += minn;
    }
    return max_flow;
}

int main()
{
    //freopen("test.in", "r", stdin);
    //freopen("test.out", "w", stdout);

    Init();

    printf("%d", EK(st, en));

    //fclose(stdin);
    //fclose(stdout);
}

自此,EK算法的讲解便结束了。
箜瑟_qi 2017.04.07 2:32


2017.04.21略加修改:

/*
Max_flow- EK
2017/04/07
*/
#include <bits/stdc++.h> 
using namespace std;
#define INF 0x3f3f3f
#define maxm 200005
#define maxn 10005

struct Edge{
    int st, en, num;
    Edge(){}
    Edge(int s, int e, int n):
        st(s), en(e), num(n){}
}flow[maxm];

int n, m, st, en, pre[maxn], re[maxn][maxn/10], num[maxn];
int q[maxn], curr_pos, st_pos, end_pos, ne, max_flow;
bool wh[maxn];

int input;
char a;

static int read()
{
    a = getchar();
    input = 0;
    while(a < '0' || a > '9')   a = getchar();
    while(a >= '0' && a <= '9')
    {
        input = input*10+a-'0';
        a = getchar();
    }
    return input;
}

static void Init()
{
    int i, a, b, c, cur;
    memset(num, -1, sizeof num);
    n = read(), m = read(), st = read(), en = read();
    for(i = 0; i != m; ++i)
    {
        cur = 2*i;
        a = read(), b = read(), c = read();
        flow[cur] = Edge(a, b, c);
        flow[cur^1] = Edge(b, a, 0);
        re[flow[cur].st][++num[flow[cur].st]] = cur;
        re[flow[cur].en][++num[flow[cur].en]] = cur^1;
    }
    return ;
}

static bool Bfs(int st, int en)
{
    int i, j;
    st_pos = -1, end_pos = 0;
    memset(wh, 0, sizeof wh);
    wh[st] = 1;
    q[0] = st;
    while(st_pos != end_pos)
    {
        curr_pos = q[++st_pos];
        for(i = 0; i < num[curr_pos]+1; ++i)
        {
            j = re[curr_pos][i];
            if(flow[j].st == curr_pos && flow[j].num > 0 && !wh[flow[j].en])
            {
                ne = flow[j].en;
                wh[ne] = 1;
                pre[ne] = j;
                q[++end_pos] = flow[j].en;
                if(ne == en)
                    return true;         
            }
        }
    }
    return false;
} 

static int EK(int start_pos, int end_pos)
{
    int i, minn;
    while(Bfs(start_pos, end_pos))
    {
        minn = INF;

        for(i = end_pos; i != st; i = flow[pre[i]].st)
        {
            minn = min(minn, flow[pre[i]].num);
        } 

        for(i = end_pos; i != st; i = flow[pre[i]].st)
        {
            flow[pre[i]].num -= minn;
            flow[pre[i]^1].num += minn;
        } 
        max_flow += minn;
    }
    return max_flow;
}

int main()
{
    //freopen("test.in", "r", stdin);
    //freopen("test.out", "w", stdout);

    Init();
    printf("%d", EK(st, en));

    //fclose(stdin);
    //fclose(stdout);
}
  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值