zoukankan      html  css  js  c++  java
  • 网络流

    网络流(network-flows)是一种类比水流的解决问题方法,与线性规划密切相关。网络流的理论和应用在不断发展,出现了具有增益的流、多终端流、多商品流以及网络流的分解与合成等新课题。网络流的应用已遍及通讯、运输、电力、工程规划、任务分派、设备更新以及计算机辅助设计等众多领域。

    选自百度百科 [1]

    在网络流中,有一个源点s,还有一个汇点t(看下图),边的权值称为流量,且边为有向边

    最大流

    最大流问题(maximum flow problem),一种组合最优化问题,就是要讨论如何充分利用装置的能力,使得运输的流量最大,以取得最好的效果。

    一种通俗的理解就是:把源点看成是自来水场,汇点看成你家,边就是水管,流量就是水管最多能流多少单位的水。自来水厂源源不断的放水,问你家最多能收到几个单位的水。

    首先,我们从一条路径来考虑(如下图):

    image

    显然,最大流为2。我们发现,一条路径的流量是由这条路径的最小值决定的。

    EK算法

    首先,引入增广路的概念(于二分图的增广路不同):一条从s到t的路径,水流流过这条路,使得当前可以到达t的流量可以增加。

    知道了增广路的概念,就可以很显然的想出一种做法,不断寻找增广路并处理和累加答案,直到找不到增广路,答案就是最大流。那如何寻找增广路呢?从s开始bfs,条件是边权不为0(不为0才能增加流量),当搜到t时,就找到了一条增广路。然后,将答案加上这条增广路的流量的最小值,将这条增广路上所有边的流量减掉最小值(因为已经使用了),直到找不到增广路。

    这个做法看上去很对,但是他有缺陷,看下图:

    image

    显然,有两条增广路,最大流为2。但是,如果找到了这条增广路: (s o 1 o 2 o 3 o t) (s o 1 o 2 o 3 o t) ,这幅图就会变成这样子:

    image

    最大流就变成了1!对于这个问题,可以通过建立反向边来解决。在建图时加入反向边,流量为0,在流量减去最小值的时候,将反向边加上最小值。那么,上面那幅图就变成了这样:

    image

    于是,就可以找到另一条增广路 (s o 3 o 2 o 1 o t) ,图变成了:

    image

    发现成功求出了正确的解。可见,反向边的用处就是标记处理过的边,在有更好情况下把原来的操作给撤销。

    小细节:用邻接表存图时,要使第一条边编号为2,且反向边一定要在正向边建完以后就建,这样可以很方便的通过异或1来得到反向边。

    【模板】网络最大流 - 洛谷

    点击查看代码
    #include <bits/stdc++.h>
    #define Tp template <typename Ty>
    #define I inline
    #define LL long long
    #define Con const
    #define Reg register
    #define CI Con int
    #define CLL Con LL
    #define RI Reg int
    #define RLL Reg LL
    #define W while
    #define max(x, y) ((x) > (y) ? (x) : (y))
    #define min(x, y) ((x) < (y) ? (x) : (y))
    #define Gmax(x, y) (x < (y) && (x = (y)))
    #define Gmin(x, y) (x > (y) && (x = (y)))
    struct FastIO
    {
    	Tp FastIO &operator>>(Ty &in)
    	{
    		in = 0;
    		char ch = getchar();
    		bool flag = 0;
    		for (; !isdigit(ch); ch = getchar())
    			(ch == '-' && (flag = 1));
    		for (; isdigit(ch); ch = getchar())
    			in = (in * 10) + (ch ^ 48);
    		in = (flag ? -in : in);
    		return *this;
    	}
    } fin;
    CI MaxN = 210, MaxM = 5e3 + 100;
    int nxt[MaxM << 1], to[MaxM << 1], pre[MaxM << 1], edge[MaxM << 1], cnt = 1 /*cnt要设为1*/, head[MaxN], w[MaxM << 1], n, m, s, t, vis[MaxN];
    void add(int u, int v, int ww)
    {
    	++cnt;
    	w[cnt] = ww;
    	to[cnt] = v;
    	nxt[cnt] = head[u];
    	head[u] = cnt;
    }
    bool bfs() // bfs寻找增广路
    {
    	memset(vis, 0, sizeof(vis));
    	std ::queue<int> q;
    	vis[s] = 1;
    	q.push(s);
    	W(!q.empty())
    	{
    		int p = q.front();
    		q.pop();
    		for (int i = head[p]; i; i = nxt[i])
    		{
    			if (!vis[to[i]] && w[i])
    			{
    				vis[to[i]] = 1;
    				pre[to[i]] = p; // 记录上一个点
    				edge[to[i]] = i; // 记录边的编号
    				if (to[i] == t)
    					return 1; // 有增广路
    				q.push(to[i]);
    			}
    		}
    	}
    	return 0; // 无增广路
    }
    void dfs()
    {
    	LL ans = 0;
    	W(bfs())
    	{
    		int minn = 0x7fffffff;
    		for (int i = t; i != s; i = pre[i])
    			Gmin(minn, w[edge[i]]);
    		for (int i = t; i != s; i = pre[i])
    			w[edge[i]] -= minn, w[edge[i] ^ 1] += minn; // 流量处理
    		ans += minn;
    	}
    	printf("%lld
    ", ans);
    }
    int get()
    {
    	fin >> n >> m >> s >> t;
    	for (int i = 1; i <= m; ++i)
    	{
    		int u, v, ww;
    		fin >> u >> v >> ww;
    		add(u, v, ww);
    		add(v, u, 0);
    	}
    	dfs();
    	return 0;
    }
    int main() { return get() && 0; }
    

    Dinic算法

    在EK算法中,我们发现每次dfs只能找到一条最短路。那能不能一次dfs就找到多条增广路呢?答案是可以。首先,用bfs将图分层。为什么要分层呢?因为这样可以保持找到的多条增广路是最短的。如下图,一次找出了两条增广路。

    image

    找到增广路后,用dfs遍历多条增广路更新答案,dfs看上去慢,其实它能一次性处理多条增广路,增加了效率。

    还是上面的板子题。

    点击查看代码
    #include <bits/stdc++.h>
    #define Tp template <typename Ty>
    #define I inline
    #define LL long long
    #define Con const
    #define Reg register
    #define CI Con int
    #define CLL Con LL
    #define RI Reg int
    #define RLL Reg LL
    #define W while
    #define max(x, y) ((x) > (y) ? (x) : (y))
    #define min(x, y) ((x) < (y) ? (x) : (y))
    #define Gmax(x, y) (x < (y) && (x = (y)))
    #define Gmin(x, y) (x > (y) && (x = (y)))
    struct FastIO
    {
        Tp FastIO &operator>>(Ty &in)
        {
            in = 0;
            char ch = getchar();
            bool flag = 0;
            for (; !isdigit(ch); ch = getchar())
                (ch == '-' && (flag = 1));
            for (; isdigit(ch); ch = getchar())
                in = (in * 10) + (ch ^ 48);
            in = (flag ? -in : in);
            return *this;
        }
    } fin;
    CI MaxN = 210, MaxM = 5e3 + 100, inf = 0x7fffffff;
    int n, m, s, t, to[MaxM << 1], nxt[MaxM << 1], w[MaxM << 1], dep[MaxN], head[MaxN], cnt = 1;
    LL ans = 0;
    bool vis[MaxN];
    void add(int u, int v, int ww)
    {
        ++cnt;
        w[cnt] = ww;
        to[cnt] = v;
        nxt[cnt] = head[u];
        head[u] = cnt;
    }
    bool bfs()
    { // bfs找增广路
        memset(dep, 0x3f, sizeof(dep));
        memset(vis, 0, sizeof(vis));
        vis[s] = 1;
        std ::queue<int> q;
        q.push(s);
        dep[s] = 0;
        W(!q.empty())
        {
            int p = q.front();
            q.pop();
            for (int i = head[p]; i; i = nxt[i])
            {
                if (dep[to[i]] > dep[p] + 1 && w[i])
                { // 分层
                    dep[to[i]] = dep[p] + 1;
                    if (!vis[to[i]])
                        vis[to[i]] = 1, q.push(to[i]);
                }
            }
        }
        if (dep[t] == dep[0])
            return false;
        return true;
    }
    LL dfs(int x, int minn)
    {
        if (x == t)
        {
            ans += minn;
            return minn;
        }
        LL use = 0;
        for (int i = head[x]; i; i = nxt[i])
            if (w[i] && dep[to[i]] == dep[x] + 1)
            { // 搜索增广路
                LL nex = dfs(to[i], min(minn - use, w[i]));
                if (nex > 0)
                {
                    use += nex;
                    w[i] -= nex; //更新答案
                    w[i ^ 1] += nex;
                    if (use == minn)
                        break;
                }
            }
        return use;
    }
    void Dinic()
    {
        while (bfs())
            dfs(s, inf); // 因为代码都在bfs和dfs中,所以这里代码很简洁。
        printf("%lld
    ", ans);
    }
    int get()
    {
        fin >> n >> m >> s >> t;
        for (int i = 1; i <= m; ++i)
        {
            int u, v, ww;
            fin >> u >> v >> ww;
            add(u, v, ww);
            add(v, u, 0);
        }
        Dinic();
        return 0;
    }
    int main()
    {
        return get() && 0;
    }
    

    但是,有一个点TLE了。于是,当前弧优化出现了(解释在代码里)!

    点击查看代码
    #include <bits/stdc++.h>
    #define Tp template <typename Ty>
    #define I inline
    #define LL long long
    #define Con const
    #define Reg register
    #define CI Con int
    #define CLL Con LL
    #define RI Reg int
    #define RLL Reg LL
    #define W while
    #define max(x, y) ((x) > (y) ? (x) : (y))
    #define min(x, y) ((x) < (y) ? (x) : (y))
    #define Gmax(x, y) (x < (y) && (x = (y)))
    #define Gmin(x, y) (x > (y) && (x = (y)))
    struct FastIO
    {
        Tp FastIO &operator>>(Ty &in)
        {
            in = 0;
            char ch = getchar();
            bool flag = 0;
            for (; !isdigit(ch); ch = getchar())
                (ch == '-' && (flag = 1));
            for (; isdigit(ch); ch = getchar())
                in = (in * 10) + (ch ^ 48);
            in = (flag ? -in : in);
            return *this;
        }
    } fin;
    CI MaxN = 210, MaxM = 5e3 + 100, inf = 0x3fffffff;
    int n, m, s, t, to[MaxM << 1], nxt[MaxM << 1], w[MaxM << 1], dep[MaxN], head[MaxN], cnt = 1, cur[MaxN];
    LL ans = 0;
    bool vis[MaxN];
    void add(int u, int v, int ww)
    {
        ++cnt;
        w[cnt] = ww;
        to[cnt] = v;
        nxt[cnt] = head[u];
        head[u] = cnt;
    }
    bool bfs()
    {
        for (int i = 0; i <= n; ++i)
        {
            vis[i] = 0;
            dep[i] = inf;
            cur[i] = head[i]; // cur相当于head
        }
        vis[s] = 1;
        std ::queue<int> q;
        q.push(s);
        dep[s] = 0;
        W(!q.empty())
        {
            int p = q.front();
            q.pop();
            for (int i = head[p]; i; i = nxt[i])
            {
                if (dep[to[i]] > dep[p] + 1 && w[i])
                {
                    dep[to[i]] = dep[p] + 1;
                    if (!vis[to[i]])
                        vis[to[i]] = 1, q.push(to[i]);
                }
            }
        }
        if (dep[t] == dep[0])
            return false;
        return true;
    }
    LL dfs(int x, int minn)
    {
        if (x == t)
        {
            ans += minn;
            return minn;
        }
        LL use = 0;
        for (int i = cur[x]; i; i = nxt[i])
        {
            cur[x] = i; // 当我们已经搜过一条边时,一定已经让这条边无法继续增广了,所以这条边已经没什么用了,
            //             直接用cur记录下一条有用的边,搜索时就可以省时间了。
            if (w[i] && dep[to[i]] == dep[x] + 1)
            {
                LL nex = dfs(to[i], min(minn - use, w[i]));
                if (nex > 0)
                {
                    use += nex;
                    w[i] -= nex;
                    w[i ^ 1] += nex;
                    if (use == minn)
                        break;
                }
            }
        }
        return use;
    }
    void Dinic()
    {
        while (bfs())
            dfs(s, inf);
        printf("%lld
    ", ans);
    }
    int get()
    {
        fin >> n >> m >> s >> t;
        for (int i = 1; i <= m; ++i)
        {
            int u, v, ww;
            fin >> u >> v >> ww;
            add(u, v, ww);
            add(v, u, 0);
        }
        Dinic();
        return 0;
    }
    int main()
    {
        return get() && 0;
    }
    

    费用流

    指在水流过水管时,每单位水需要交纳水费(可能为负数,就是水厂要付你钱),求最大流和在流量最大的情况下最小的费用。

    显然,还是分层,遍历。那么要修改哪一步呢?显然,遍历是不用修改的,所以需要修改分层。那么要将分层的bfs修改成什么呢?想到费用,我们第一个想到的就是最短路。所以,只要将分层的bfs改为已经死的SPFA(因为Dijkstra不能处理负权),就可以了。

    P3381 【模板】最小费用最大流

    点击查看代码
    #include <bits/stdc++.h>
    #define Tp template <typename Ty>
    #define I inline
    #define LL long long
    #define Con const
    #define Reg register
    #define CI Con int
    #define CLL Con LL
    #define RI Reg int
    #define RLL Reg LL
    #define W while
    #define max(x, y) ((x) > (y) ? (x) : (y))
    #define min(x, y) ((x) < (y) ? (x) : (y))
    #define Gmax(x, y) (x < (y) && (x = (y)))
    #define Gmin(x, y) (x > (y) && (x = (y)))
    struct FastIO
    {
        Tp FastIO &operator>>(Ty &in)
        {
            in = 0;
            char ch = getchar();
            bool flag = 0;
            for (; !isdigit(ch); ch = getchar())
                (ch == '-' && (flag = 1));
            for (; isdigit(ch); ch = getchar())
                in = (in * 10) + (ch ^ 48);
            in = (flag ? -in : in);
            return *this;
        }
    } fin;
    CI MaxN = 5e3 + 100, MaxM = 5e4 + 100, inf = 0x3fffffff;
    int nxt[MaxM << 1], to[MaxM << 1], w[MaxM << 1], c[MaxM << 1], head[MaxN], cnt = 1, cost[MaxN << 1];
    bool vis[MaxN];
    int n, m, s, t;
    LL ans = 0, anscost = 0;
    void add(int u, int v, int ww, int cc)
    {
        ++cnt;
        to[cnt] = v;
        w[cnt] = ww;
        c[cnt] = cc;
        nxt[cnt] = head[u];
        head[u] = cnt;
    }
    bool SPFA() // 使用最短路算法分层
    {
        memset(vis, 0, sizeof(vis));
        memset(cost, 0x3f, sizeof(cost));
        std::queue<int> q;
        vis[s] = 1;
        cost[s] = 0;
        q.push(s);
        W(!q.empty())
        {
            int p = q.front();
            q.pop();
            vis[p] = 0;
            for (int i = head[p]; i; i = nxt[i])
            {
                if (cost[p] + c[i] < cost[to[i]] && w[i])
                {
                    cost[to[i]] = cost[p] + c[i];
                    if (!vis[to[i]])
                        vis[to[i]] = 1, q.push(to[i]);
                }
            }
        }
        if (cost[t] == cost[0])
            return false;
        return true;
    }
    int dfs(int x, int minn)
    {
        if (x == t)
        {
            vis[t] = 1;
            ans += minn;
            return minn;
        }
        int use = 0;
        vis[x] = 1;
        for (int i = head[x]; i; i = nxt[i])
        {
            if ((!vis[to[i]] || to[i] == t) && cost[to[i]] == cost[x] + c[i] && w[i])
            {
                int search = dfs(to[i], min(minn - use, w[i]));
                if (search > 0)
                {
                    use += search;
                    anscost += (search * c[i]); // 统计答案时乘上费用
                    w[i] -= search;
                    w[i ^ 1] += search;
                    if (use == minn)
                        break;
                }
            }
        }
        return use;
    }
    void Dinic()
    {
        while (SPFA())
        {
            do
            {
                memset(vis, 0, sizeof(vis));
                dfs(s, inf);
            } while (vis[t]);
        }
        printf("%lld %lld
    ", ans, anscost);
    }
    int get()
    {
        fin >> n >> m >> s >> t;
        for (int i = 1; i <= m; ++i)
        {
            int u, v, ww, cc;
            fin >> u >> v >> ww >> cc;
            add(u, v, ww, cc);
            add(v, u, 0, -cc); // 反向边费用是正向边费用的负数
        }
        Dinic();
        return 0;
    }
    int main() { return get() && 0; }
    

    参考


    1. https://baike.baidu.com/item/网络流/2987528?fr=aladdin ↩︎

  • 相关阅读:
    冒泡排序
    MySql
    利用mybatis-generator自动生成代码
    SQL,HQL,CQL,JPQL了解
    mysql中的约束
    详解CRUD?
    什么是数据库ACID?
    建造者模式(Builder Pattern)
    Java中的设计模式
    java中的排序面试题
  • 原文地址:https://www.cnblogs.com/binghun/p/15159690.html
Copyright © 2011-2022 走看看