循环流网络的费用问题
- 费用为负的网络流
- 循环流网络的费用
- 一些题目
- [UVa1659 Help Little Laura](https://onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&category=24&page=show_problem&problem=4534)
- [Aizu-2230 How to Create a Good Game](https://vjudge.net/problem/Aizu-2230#author=GPT_zh)
- 参考博客
费用为负的网络流
对于源点为s汇点为t并且流量F确定的含负权边的网络,可以通过变形消去负权边:
新增源点 S和汇点T,从S向s连一条容量为F费用为0的边,从t向T连一条容量为F费用为0的边;对于负权边e=(u,v)可以让其一开始就已经满流(即从v向u连一条容量为
c
e
c_e
ce费用为
−
d
e
-d_e
−de的边),再从S向v连一条容量为
c
e
c_e
ce费用为0的边,从u向T连一条容量为
c
e
c_e
ce费用为0的边;对正权边则同原始网络一样连边。
求新图流量为
F
+
∑
负权边
e
c
e
{\textstyle F+\sum_{负权边e}c_e}
F+∑负权边ece的最小费用,再加上
∑
负权边
e
c
e
×
d
e
{\textstyle \sum_{负权边e}c_e\times d_e}
∑负权边ece×de,就是原图流量为F的最小费用。
消除负权边后,可以用Dijkstra算法求最小费用,效率比BellmanFord算法高。当然,求最小费用流,有专门的PrimalDual原始对偶算法,不用在建图时消除负权边。
循环流网络的费用
有一些网络流其有向图是强连通的但没有源点也没有汇点,而且每个结点都要满足流量平衡,所以也没有“最大流”这种说法,称为循环流(circulation)。对于要求最大费用的循环流网络,将边权取相反数,可变成求最小费用循环流。最小费用循环流问题也可能出现负权边,同样可以套用上面处理负权边的思想来处理。
若负权边e=(u,v)可以取或不取,则预先使之满流(即反向连边,从v向u连一条容量为
c
e
c_e
ce费用为
−
d
e
-d_e
−de的边)。**实际上,满流意味着这条负权边的流量可以回退,即此负权边可以取或不取。**若回退流量,意味着此负权边组成的最小权值圈为正圈,又由于求最小费用流,不取正圈,故将流退回。这种情况可以像上面费用为负的网络流那样画图帮助理解,还是比较好理解的。
若负权边e=(u,v)必须取到,则不预先使之满流(即按原图连边,从u向v连一条容量为
c
e
c_e
ce费用为
d
e
d_e
de的边)。这种情况不那么直观,需要画图帮助理解。
一些题目
UVa1659 Help Little Laura
平面上有m条有向线段连接了n个点。你从某个点出发顺着有向线段行走,给沿途经过的每条线段涂一种不同的颜色,最后回到起点。你可以多次行走,给多个回路涂色。可以重复经过一个点,但不能重复经过一条有向线段。如下图所示是一种涂色方法(虚线表示未涂色)。
每涂一个单位长度将得到x分,但每使用一种颜料将扣掉y分。假定颜料有无限多种,如何涂色才能使得分最大?输入保证若存在有向线段u->v,则不会出现有向线段v->u。n≤100,m≤500,1≤x,y≤1000。
这是典型的负权边可以取或不取的题目,AC代码如下:
#include <iostream>
#include <iomanip>
#include <cstring>
#include <cmath>
using namespace std;
#define N 102
#define M 1780
struct edge {int u, v, cap, flow; double cost;} e[M];
int g0[N][N], cnt0[N], x[N], y[N], n, s, t, kase = 0;
int g[N][N], q[M*N], cnt[N], a[N], p[N], c; double d[N], cc; bool vis[N];
void add_edge(int u, int v, int cap, double cc) {
e[c] = {u, v, cap, 0, cc}; g[u][cnt[u]++] = c++; e[c] = {v, u, 0, 0, -cc}; g[v][cnt[v]++] = c++;
}
double solve() {
cin >> s >> t;
memset(cnt0, 0, sizeof(cnt0)); memset(a, cc = 0, sizeof(a)); memset(cnt, c=0, sizeof(cnt));
for (int i=1; i<=n; ++i) {
cin >> x[i] >> y[i];
int v; while (cin>>v && v) g0[i][cnt0[i]++] = v;
}
for (int u=1; u<=n; ++u) for (int i=0; i<cnt0[u]; ++i) {
int v = g0[u][i]; double d = t-s*sqrt((x[u]-x[v])*(x[u]-x[v])+(y[u]-y[v])*(y[u]-y[v]));
if (d < 0) {
cc -= d; ++a[v]; --a[u]; add_edge(v, u, 1, -d);
} else add_edge(u, v, 1, d);
}
s = 0; t = n+1;
for (int u=1; u<=n; ++u) if (a[u] != 0) a[u] > 0 ? add_edge(s, u, a[u], 0.) : add_edge(u, t, -a[u], 0.);
while (true) {
for (int i=0; i<=t; ++i) d[i] = 1e39;
memset(vis, 0, sizeof(vis)); d[s] = 0.; q[0] = s; a[s] = 1;
int head = 0, tail = 1;
while (head < tail) {
int u = q[head++]; vis[u] = false;
for (int i=0; i<cnt[u]; ++i) {
const edge& ee = e[g[u][i]];
if (ee.cap > ee.flow && d[ee.v] > d[u]+ee.cost) {
d[ee.v] = d[u]+ee.cost;
p[ee.v] = g[u][i];
a[ee.v] = min(a[u], ee.cap-ee.flow);
if (!vis[ee.v]) vis[q[tail++] = ee.v] = true;
}
}
}
if (d[t] >= 1e39) break;
cc -= d[t];
for (int u=t; u!=s; u=e[p[u]].u) e[p[u]].flow += a[t], e[p[u]^1].flow -= a[t];
}
return cc;
}
int main() {
cout << fixed << setprecision(2);
while (cin>>n && n) cout << "Case " << ++kase << ": " << max(solve(), 0.) << endl;
return 0;
}
Aizu-2230 How to Create a Good Game
国际游戏公司要加工一个游戏,该游戏的关卡是一个有向无环图,每两个关卡间都有若干任务。在通关所需最多任务不变的情况下,还能最多增添多少任务?
这是典型的负权边必须取到的题目。不过这个题先要做一些分析处理才能转化成最小费用循环流:从终点关卡向起点关卡连一条边权为最长路长度的正权边再将原DAG权值取反,那么那些正权圈中的负权边就是应该增加权值的边,具体应该加多少,就是正权圈的权值。
结合下面两个测试数据画图帮助理解。
样例1输入 | 样例1输出 | 样例2输入 | 样例2输出 |
---|---|---|---|
3 3 0 1 5 1 2 3 0 2 2 | 6 | 4 6 0 1 5 0 2 5 0 3 5 1 2 5 1 3 5 2 3 5 | 20 |
画出图后就能理解负权边必须取到时,其处理方式的正确性了。样例1有一条流量为2的增广路
S
→
2
→
0
→
T
S\rightarrow2\rightarrow0\rightarrow T
S→2→0→T,最小费用为
2
×
8
=
16
2\times8=16
2×8=16,负权和为
−
(
5
+
3
+
2
)
=
−
10
-(5+3+2)=-10
−(5+3+2)=−10,所以答案为6。样例2有一条流量为1的增广路
S
→
2
→
3
→
0
→
T
S\rightarrow2\rightarrow3\rightarrow0\rightarrow T
S→2→3→0→T、一条流量为1的增广路
S
→
3
→
0
→
1
→
T
S\rightarrow3\rightarrow0\rightarrow1\rightarrow T
S→3→0→1→T、一条流量为2的增广路
S
→
3
→
0
→
T
S\rightarrow3\rightarrow0\rightarrow T
S→3→0→T,最小费用为
(
−
5
+
15
)
+
(
15
−
5
)
+
2
×
15
=
50
(-5+15)+(15-5)+2\times15=50
(−5+15)+(15−5)+2×15=50,负权和为
−
5
×
6
=
−
30
-5\times 6=-30
−5×6=−30,所以答案为20。
#include <iostream>
#include <cstring>
using namespace std;
#define INF 0x7f7f7f7f
#define M 1002
#define N 102
struct edge {int u, v, cap, flow, cost;} e[(M+N)<<1];
int g[N][N], q[N*(M+N)<<1], cnt[N], a[N], d[N], p[N], c, m, n; bool vis[N];
void add_edge(int u, int v, int cap, int cc) {
e[c] = {u, v, cap, 0, cc}; g[u][cnt[u]++] = c++; e[c] = {v, u, 0, 0, -cc}; g[v][cnt[v]++] = c++;
}
int mcmf(int s, int t, int f) {
int cc = 0;
while (f > 0) {
memset(d, 127, sizeof(d)); memset(vis, 0, sizeof(vis)); d[s] = 0; q[0] = s; a[s] = f;
int head = 0, tail = 1;
while (head < tail) {
int u = q[head++]; vis[u] = false;
for (int i=0; i<cnt[u]; ++i) {
const edge& ee = e[g[u][i]];
if (ee.cap > ee.flow && d[ee.v] > d[u]+ee.cost) {
d[ee.v] = d[u]+ee.cost;
p[ee.v] = g[u][i];
a[ee.v] = min(a[u], ee.cap-ee.flow);
if (!vis[ee.v]) vis[q[tail++] = ee.v] = true;
}
}
}
if (d[t] >= INF) break;
f -= a[t]; cc += d[t]*a[t];
for (int u=t; u!=s; u=e[p[u]].u) e[p[u]].flow += a[t], e[p[u]^1].flow -= a[t];
}
return cc;
}
int solve() {
int cc = 0, s = n, t = n+1, f = 0;
memset(a, 0, sizeof(a)); memset(cnt, c=0, sizeof(cnt));
while (m--) {
int u, v, w; cin >> u >> v >> w; cc -= w; --a[u]; ++a[v]; add_edge(u, v, INF, -w);
}
for (int i=0; i<n; ++i) {
if (a[i] > 0) f += a[i], add_edge(s, i, a[i], 0);
else if (a[i] < 0) add_edge(i, t, -a[i], 0);
}
add_edge(n-1, 0, INF, -mcmf(0, n-1, 1));
return cc + mcmf(s, t, f);
}
int main() {
while (cin >> n >> m) cout << solve() << endl;
return 0;
}
参考博客
最小费用循环流