网络流

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

选自百度百科 [1]

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

最大流

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

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

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

image

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

EK算法

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

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

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

image

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

image

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

image

于是,就可以找到另一条增广路 \(s \to 3 \to 2 \to 1 \to 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\n", 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\n", 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\n", 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\n", 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 ↩︎

posted @ 2021-08-19 07:48  ClapEcho233  阅读(439)  评论(0编辑  收藏  举报