网络流

7 篇文章 0 订阅

强烈推荐:北京大学ACM暑期课讲义-网络流,讲的很精华,慢慢看比较容易看懂。

 

最大流:

最大流即有向图源点到汇点的最大流量。

各种算法的核心思想就是寻找增广路,核心思想就像是贪心,增广路的寻找是搜索(bfs,dfs),再加上各种技巧优化。

还有很出名的最大流最小割定理,st2点间最大流=st两点间的最小割。很容易接受的定理..

 

以poj1273为例:

O(m*n^2)的dinic算法:

#include <bits/stdc++.h>

using namespace std;

#define inf 11111111

const int N = 255;

struct

{

    int v, w, ne;

} ed[444];

int cnt, co, id[N], flor[N], cur[N];

//co为点数,遍历所有点时用到(函数中唯一需要改的地方)

void add(int a, int b, int x)

{

    //加边一次加4个,直接调用就好

    ed[cnt].v = b, ed[cnt].w = x;

    ed[cnt].ne = id[a], id[a] = cnt++;

    ed[cnt].v = a, ed[cnt].w = 0;

    ed[cnt].ne = id[b], id[b] = cnt++;

}

int bfs(int s, int t)

{

    queue<int> q;

    memset(flor, 0, sizeof(flor));

    flor[s] = 1, q.push(s);

    while (q.size())

    {

        int now = q.front();

        q.pop();

        if (now == t)

            return 1;

        for (int i = id[now]; ~i; i = ed[i].ne)

        {

            int to = ed[i].v;

            if (flor[to] == 0 && ed[i].w > 0)

            {

                flor[to] = flor[now] + 1;

                q.push(to);

                if (to == t)

                    return 1;

            }

        }

    }

    return flor[t] != 0;

}

int dfs(int s, int t, int value)

{

    int ret = value, a;

    if (s == t || value == 0)

        return value;

    for (int &i = cur[s]; ~i; i = ed[i].ne)

    {

        int to = ed[i].v;

        if (flor[to] == flor[s] + 1 && (a = dfs(to, t, min(ret, ed[i].w))))

        {

            ed[i].w -= a, ed[i ^ 1].w += a;

            ret -= a;

            if (!ret)

                break;

        }

    }

    if (ret == value)

        flor[s] = 0;

    return value - ret;

}

int dinic(int s, int t)

{

    int ret = 0;

    while (bfs(s, t))

    {

        for (int i = 1; i <= co; i++)//遍历所有点,这里是需要修改的!!

            cur[i] = id[i];

        ret += dfs(s, t, inf);

    }

    return ret;

}

int main()

{

    int a, b, c, n, m;

    while (~scanf("%d%d", &m, &n))

    {

        memset(id, -1, sizeof(id)), cnt = 0;

        for (int i = 0; i < m; i++)

        {

            scanf("%d%d%d", &a, &b, &c);

            add(a, b, c);

        }

        co = n;

        printf("%d\n", dinic(1, n));

    }

    return 0;

}

 

最大流几个注意点:

矩阵存边的话重边需要加上

初始化最大值时要大于最终的最大流(比如所有流量之和)

 

 

例题:

Poj 1459

多源点多汇点的最大流

有多源点多汇点的话,并不是类似最短路的让多点进queue,而是加一个超级源点和超级汇点,超级点和源/汇点之间连边为inf的有向边。

读入数据有点恶心,用dinic模板时注意dinic函数while()下循环的范围为最小下标到最大下标(包括后来加的点)

多源多汇的情况,加上超级源汇点就行。

 

 

Poj 3436

看似和图论没什么关系,但可以归于类似最大流的最优化问题。每个机器是一个节点,机器的输出符合另一个机器的输入则建立一条有向边。

首先肯定是建立超级源点汇点,源点连输入没有1的机器,汇点连输入只有1的机器。

这样相当于是求最小点割集(注意是割点不是割边)

建图策略是拆点,把每个点拆成出点和入点,入点唯一的出边指向该点的出点,权值为该点的q值,其他边权值全为最大值(比最大边大就行)(这也是求最小点割的方法)

网上有不拆点的建图方法,但好像是错的。(这题数据水..)

所以点带权值求最小点割的情况,拆一下点跑最大流就行。

求最大流的具体情况,就先把原图备份上,跑完最大流后对比这个图和原图,同一条边权值不同的就是最大流经过的点,流量就是2边的差值。

 

Poj 2112

相当于已知c个奶牛到k个挤奶机的距离,每个挤奶机最多有m个奶牛,求每个奶牛都到达一个挤奶机,走路最多的奶牛最少走多少步。

看似和图论没什么关系,其实也可以归于最大流的数学表达。

先对原图floyd求全局最短路。

发现对于每个ans,可以用最大流确定其是否可行,具体是:

对于每个ans,建立超级源点,汇点,分别连奶牛,挤奶机,边长分别是1,m,map里每个奶牛到挤奶机的距离小于等于ans的边都加入(边长为1,大一点也行,反正前面的奶牛最大流已经限定为1了),求最大流,如果最大流为c(所有奶牛都能到),则可行。

二分答案ans,找到可行下界。

二分答案+用网络流check也是一种图论解决问题的思路,试着想象一下最大流的数学本质兴许有用。(类似多条件限制下的最优化)

 

 

最好用dinic模板,几乎不会被卡时间。

 

有上下界的网络流:

就是有下界的最大流(上界本来就有)。

算法思想是通过构图把他转化为一般的最大流。(ppt上讲的很清楚)

 

具体流程是:

首先把每条边拆成必要边和不必要边(必要边是下界流量,不必要边是上界-下界)

加点x,y,将每个不必要边u->v拆成u->x和y->v,再加边原汇点->原源点,容量为无穷。此时新图为y是源点x是汇点的最大流图。

若最大流<所有流入x的边之和(即最大流小于流量下界和),则原图无解。

否则,记sum1为从s点流出的流量和。

在残余网络里,去掉st之间的所有边(就只有上一步加的无穷边和反向0边这2个边),再做一次s到t的最大流(xy点存在与否并不影响,因为这2点只有出或入,并不能传递流量),最大流为sum2。

有下界的最大流流量即sum1 + sum2。

 

要求最大流的具体信息的话,第一次跑完存图为G1,第二次跑完图为G2,原图下界为LC,

则原图i->j流量为:G1[i][j] – G2[i][j] + LC[i][j]

 

有重边的话:注意流量和下界都要累加,合并为1条边。

 

Poj 2396

非常神奇的题目了,看上去和图论没什么关系系列(但据说是比较明显的图论题目)

给一个200*20以内的矩阵,给出每行每列的和,及一些元素的大小关系(<=>),求任意一个满足所有要求的矩阵,或判断不可能。

用网络流模型,每行每列对应一个点,加超级源点汇点,各连接所有行和列,上下界均为该行(列)的和。再把每行对每列连接起来(就像完全二分图),下界为0上界为inf。对于每个点(i,j)的约束,更新i行->j列的流量上下界(相等则上下界均为该值)。

求出任意一个可行流,i->j的流量就是最终矩阵(i,j)元素的值。

 

是求可行流而不是最大流,所以只用做一次最大流(因为源点汇点的流量都满足上界=下界,第二次并不会更新最大流,sum2必定为0)

此时(一次dinic后i->j的流量)+(i->j的流量下界)即为原图该边的流量。

 

坑点是给的条件可能会出现悖论(直接输出不可能),且下界可能会为负数(此时处理为0)。

 

 

具体实现的话,在原最大流模板下加一个有下界的最大流函数,用二维矩阵存上下界,在函数内建图,跑2遍最大流记录sum1,sum2,并返回即可。

 

模板(上面和dinic一样,只多了一行数组定义):

#include <bits/stdc++.h>

using namespace std;

#define inf 11111111

const int N = 255;

struct

{

    int v, w, ne;

} ed[444], ed1[444];

int cnt, co, id[N], flor[N], cur[N];

//co为点数,遍历所有点时用到(函数中唯一需要改的地方)

int mi[N][N], ma[N][N];

void add(int a, int b, int x)

{

    //加边一次加4个,直接调用就好

    ed[cnt].v = b, ed[cnt].w = x;

    ed[cnt].ne = id[a], id[a] = cnt++;

    ed[cnt].v = a, ed[cnt].w = 0;

    ed[cnt].ne = id[b], id[b] = cnt++;

}

int bfs(int s, int t)

{

    queue<int> q;

    memset(flor, 0, sizeof(flor));

    flor[s] = 1, q.push(s);

    while (q.size())

    {

        int now = q.front();

        q.pop();

        if (now == t)

            return 1;

        for (int i = id[now]; ~i; i = ed[i].ne)

        {

            int to = ed[i].v;

            if (flor[to] == 0 && ed[i].w > 0)

            {

                flor[to] = flor[now] + 1;

                q.push(to);

                if (to == t)

                    return 1;

            }

        }

    }

    return flor[t] != 0;

}

int dfs(int s, int t, int value)

{

    int ret = value, a;

    if (s == t || value == 0)

        return value;

    for (int &i = cur[s]; ~i; i = ed[i].ne)

    {

        int to = ed[i].v;

        if (flor[to] == flor[s] + 1 && (a = dfs(to, t, min(ret, ed[i].w))))

        {

            ed[i].w -= a, ed[i ^ 1].w += a;

            ret -= a;

            if (!ret)

                break;

        }

    }

    if (ret == value)

        flor[s] = 0;

    return value - ret;

}

int dinic(int s, int t)

{

    int ret = 0;

    while (bfs(s, t))

    {

        for (int i = 1; i <= co; i++)//遍历所有点,这里是需要修改的!!

            cur[i] = id[i];

        ret += dfs(s, t, inf);

    }

    return ret;

}

int bound_flow(int s, int t)

{

    //由mi ma数组求有上界的网络流 co为遍历所有点的参数,需要提前传入

    memset(id, -1, sizeof(id));

    int i, j, x = co + 1, y = co + 2;

    int sum = 0, sum1 = 0, sum2 = 0, s1 = 0;

    for (i = 0; i <= co; i++)

        for (j = 0; j <= co; j++)

            if (mi[i][j] != 0)//拆必要边

            {

                if (i == s)

                    s1 += mi[i][j];

                add(i, x, mi[i][j]), add(y, j, mi[i][j]);

                sum += mi[i][j];//必要边之和

            }

    for (i = 0; i <= co; i++)

        for (j = 0; j <= co; j++)

            if (ma[i][j] - mi[i][j] > 0)//加不必要边

            {

                if (i == s)

                    s1 += ma[i][j] - mi[i][j];

                add(i, j, ma[i][j] - mi[i][j]);

            }

    add(t, s, inf);//加无穷边

    memcpy(ed1, ed, sizeof(ed));

    co = co + 2;//改点数

    int temp = dinic(y, x);

    if (temp != sum)

        return -1;

    i = s;

    for (j = id[i]; ~j; j = ed[j].ne)

        if (ed[j].w < ed1[j].w)

            sum1 += ed1[j].w - ed[j].w;//计算sum1

 

    for (j = id[i]; ~j; j = ed[j].ne)

        if (ed[j].v == t)

            ed[j].w = 0;

    for (j = id[t]; ~j; j = ed[j].ne)

        if (ed[j].v == s)

            ed[j].w = 0;//删st边

    sum2 = dinic(s, t);

    return sum1 + sum2;

}

 

 

费用流:

最小费用最大流:对于每条有向边i->j,都有一个单位费用和流量(流量每多1都要多一个单位费用),求s到t的最大流中,总费用最小的。

原理类似贪心,每次寻找费用最短路,并分配最大可能流量。

因为存在负边(取消流时产生的负边),不能用dij求最短路,而是用spfa。所以复杂度比较玄学,上界大概是最大流量*单次spfa。

 

以poj2135为例:

求无向图1->n->1的不重复最短路径。

叒是看似和网络流没关系的。相当于求1->n 的2条不相交边的路径的和的最小。

建立源点汇点,源点连到1一条流量为2(表示2条路)费用为0的有向边,汇点同理连到n。

对于每条费用为c的无向边i<->j,,建立i->j和j->i的流量为1费用c的边(带0边)。

这时候源点到汇点的最小费用就是所求最短路。

 

#include <bits/stdc++.h>

using namespace std;

const int inf = 1 << 28, MAXN = 1005, MAXM = 10005 << 2;

struct

{

    int s, to, ne, ca, va;//费用va 容量ca

} ed[MAXM];

int id[MAXN], pre[MAXN], dis[MAXN], cnt;

bool vis[MAXN];

void addedge(int a, int b, int v, int c)

{

    //加边和反向0边

    ed[cnt].to = b, ed[cnt].s = a, ed[cnt].va = v;

    ed[cnt].ca = c, ed[cnt].ne = id[a], id[a] = cnt++;

    ed[cnt].to = a, ed[cnt].s = b, ed[cnt].va = -v;

    ed[cnt].ca = 0, ed[cnt].ne = id[b], id[b] = cnt++;

}

bool spfa(int s, int t, int nnum)

{

    //[0,nnum]中s到t是否存在最短路

    memset(vis, 0, sizeof(vis));

    memset(pre, -1, sizeof(pre));

    for (int i = 0; i <= nnum; i++)

        dis[i] = inf;

    queue<int> que;

    que.push(s);

    dis[s] = 0, vis[s] = 1;

    while (!que.empty())

    {

        int temp = que.front();

        que.pop(), vis[temp] = 0;

        for (int i = id[temp]; ~i; i = ed[i].ne)

            if (ed[i].ca)

            {

                int ne = ed[i].to;

                if (dis[temp] + ed[i].va < dis[ne])

                {

                    dis[ne] = dis[temp] + ed[i].va;

                    pre[ne] = i;

                    if (!vis[ne])

                        vis[ne] = 1, que.push(ne);

                }

            }

 

    }

    return dis[t] != inf;

}

int getMincost(int s, int t, int nnum)

{

    //[0,nnum]中s到t的最小费用最大流的最小费用

    int ans_flow = 0, ans_cost = 0, temp, minc;

    while (spfa(s, t, nnum))

    {

        temp = t;

        minc = inf;

        while (pre[temp] != -1)

        {

            minc = min(ed[pre[temp]].ca, minc);

            temp = ed[pre[temp]].s;

        }

        temp = t;

        while (pre[temp] != -1)

        {

            ed[pre[temp]].ca -= minc;

            int ss = pre[temp] ^ 1;

            ed[ss].ca += minc;

            temp = ed[pre[temp]].s;

        }

        ans_cost += dis[t] * minc;

    }

    return ans_cost;

}

int main()

{

    int i, a, b, v, s, t, n, m;

    while (~scanf("%d%d", &n, &m))

    {

        memset(id, -1, sizeof(id)), cnt = 0;

        memset(ed, 0, sizeof(ed));

        for (i = 0; i < m; i++)

        {

            scanf("%d%d%d", &a, &b, &v);

            addedge(a, b, v, 1);

            addedge(b, a, v, 1);

        }

        s = n + 1, t = n + 2;

        addedge(s, 1, 0, 2);

        addedge(n, t, 0, 2);

        printf("%d\n", getMincost(s, t, t));

    }

    return 0;

}

 

 

费用流的常见思路是构造网络流模型,其最大流是题目已经确定的或很快能算出来的不大的数(因为复杂度里有最大流),并给每条边一个费用。其费用就是要求的最优化答案。

构图比较难...

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值