网络流
网络流(network-flows)是一种类比水流的解决问题方法,与线性规划密切相关。网络流的理论和应用在不断发展,出现了具有增益的流、多终端流、多商品流以及网络流的分解与合成等新课题。网络流的应用已遍及通讯、运输、电力、工程规划、任务分派、设备更新以及计算机辅助设计等众多领域。
选自百度百科 [1]
在网络流中,有一个源点s,还有一个汇点t(看下图),边的权值称为流量,且边为有向边
最大流
最大流问题(maximum flow problem),一种组合最优化问题,就是要讨论如何充分利用装置的能力,使得运输的流量最大,以取得最好的效果。
一种通俗的理解就是:把源点看成是自来水场,汇点看成你家,边就是水管,流量就是水管最多能流多少单位的水。自来水厂源源不断的放水,问你家最多能收到几个单位的水。
首先,我们从一条路径来考虑(如下图):
显然,最大流为2。我们发现,一条路径的流量是由这条路径的最小值决定的。
EK算法
首先,引入增广路的概念(于二分图的增广路不同):一条从s到t的路径,水流流过这条路,使得当前可以到达t的流量可以增加。
知道了增广路的概念,就可以很显然的想出一种做法,不断寻找增广路并处理和累加答案,直到找不到增广路,答案就是最大流。那如何寻找增广路呢?从s开始bfs,条件是边权不为0(不为0才能增加流量),当搜到t时,就找到了一条增广路。然后,将答案加上这条增广路的流量的最小值,将这条增广路上所有边的流量减掉最小值(因为已经使用了),直到找不到增广路。
这个做法看上去很对,但是他有缺陷,看下图:
显然,有两条增广路,最大流为2。但是,如果找到了这条增广路: \(s \to 1 \to 2 \to 3 \to t\) \(s \to 1 \to 2 \to 3 \to t\) ,这幅图就会变成这样子:
最大流就变成了1!对于这个问题,可以通过建立反向边来解决。在建图时加入反向边,流量为0,在流量减去最小值的时候,将反向边加上最小值。那么,上面那幅图就变成了这样:
于是,就可以找到另一条增广路 \(s \to 3 \to 2 \to 1 \to t\) ,图变成了:
发现成功求出了正确的解。可见,反向边的用处就是标记处理过的边,在有更好情况下把原来的操作给撤销。
小细节:用邻接表存图时,要使第一条边编号为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将图分层。为什么要分层呢?因为这样可以保持找到的多条增广路是最短的。如下图,一次找出了两条增广路。
找到增广路后,用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不能处理负权),就可以了。
点击查看代码
#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; }