@总结 - 8@ 上下界网络流等一类网络流问题
@0 - 参考资料@
@1 - 问题引入@
我们知道,通常情况下,一个合法的流应该具有如下几个性质:
(1)(除源点汇点以外)流量守恒:∑f(i,u)=∑f(u,j)。
(2)斜对称性:f(u,v)=−f(v,u)。
(3)容量限制:f(u,v)≤c(u,v)。
但在某些问题中,我们还要求边的流量有下界,即 l(u,v)≤f(u,v)≤c(u,v)。
我们需要将这种流量带有上下界的网络流进行模型的转化,使得可以使用通常的网络流算法解决。
@2 - 上下界可行流@
什么是可行流?简单说即不包含源汇的,满足流量守恒、斜对称性、流量上下界的流。
如果不包含下界,零流就是一个可行流。
根据定义我们有 f(u,v)=l(u,v)+f′(u,v),其中 0≤f′(u,v)≤c(u,v)−l(u,v),可以发现 f′(u,v) 是一个没有下界的流。
我们将原来的边 (u,v) 的上界改成 c(u,v)−l(u,v),再进行下一步的处理。
我们考虑用一个等价的东西代替 l(u,v) 这部分流的。
考虑 l(u,v) 这部分流,它只会对 “流量守恒” 这一个条件造成影响:它贡献了点 v 流量为 l(u,v) 的入流,贡献了点 u 流量为 l(u,v) 的出流。
我们新建源点 ss,汇点 tt。对于边 (u,v),我们新建边 (ss,v),(u,tt),使得它们的容量都为 l(u,v)。
这样,假如所有的 (ss,v), (u,tt) 都满流,它们就可以起到和 l(u,v) 一样的作用。
找出 ss 到 tt 的最大流即可,如果满流说明有一个可行解。此时边 (u,v) 的流量就是 f′(u,v)(不是 f(u,v) 哦,f(u,v)=l(u,v)+f′(u,v))。
这样的确就是一个完整的算法了。但是我们还可以进行建图上的优化。
如果同时有边 (ss,u),(u,tt),可以合并成一条边。如果有多条 (ss,u)/(u,tt),也可以合并成一条边。
具体的话,可以统计 ss 到 u 的容量和 − u 到 tt 的容量和,根据其正负以及大小来建边。
@3 - 上下界最大流/最小流@
考虑求解带源汇的可行流(因为最大流/最小流必须要在有源汇的情况下才能被定义):
我们汇点 t 向源点 s 连一条容量为 inf 的边即可。
此时因为流量守恒, f′(t,s) 就等于当前可行流中 s 到 t 的流量。
考虑求解最大流:
直接源点 s 向汇点 t 增广即可。
首先,ss 没有入边,tt 没有出边,增广路不会经过它们俩。
然后,因为斜对称性, (t,s) 有一个流量为 −f′(t,s) 的反向边。增广的时候必然会经过这个反向边,就把初始可行流中 s 到 t 的流量统计进去了。
而且因此,也不需要特意去加上某些边的流量下界(因为这个是包含下界的可行流哦)。
最后,因为我们的下界是转换成等价形式了。因此如果不修改 ss,tt 的流量,就不会出现不满足下界的情况。
考虑求解最小流(没有下界的话,最小流就是零流):
先去掉 (t,s),汇点 t 向源点 s 增广。最后用 f′(t,s) − 增广得到的最大流就是答案。
t 向 s 增广即退流操作,退的越多自然流量就越小。其他的和上面差不多。
@4 - 上下界费用流@
(先不考虑负环的问题)
考虑求解无源汇的可行流。
建图部分和最大流差不多,在最后 ss 向 tt 跑最小费用最大流即可。
考虑求解有源汇的最小费用最大流。
t 向 s 连容量 inf,费用 0 的边,跑最小费用最大流,再 s 向 t 求最小费用最大流。
考虑求解有源汇的最小费用流。
t 向 s 连容量 inf,费用 0 的边,跑最小费用最大流,再 s 向 t 求最小费用流。
其实和上面的最大流问题模型差不多。
@5 - 带负环的最小费用流@
这个……尽管和上下界网络流已经没关系了,但是鉴于它们的思路有一定的相似性,我还是在这里提一下。
考虑求解最小费用循环流。
什么是最小费用循环流?实际上就是合法的,费用最小的无源汇的流。
一个算法是:找到一个负权环,将它加入答案。可以发现这个算法效率不高。
一个想法是:我们可以先贪心地选择所有的负权边,再花费最小代价将它调整为合法的流(不会证明,直观上是对的)。
调整这一步,是不是比较类似于上下界网络流呢?
但是,和上下界网络流不同的是,这里是预先流满(可以取消选择),而上下界网络流是强制流满(不能取消选择)。
怎么实现撤回呢?根据斜对称性,一条边的流的减少等价于它反向边的流的增加。
所以我们这样来建图:
对于负权边 (u,v),连 (ss,v),(u,tt),费用为 0;连 (v,u),费用为 −w(u,v)。三条边的容量都为 c(u,v)。
对于其他边,保持不变。
最后 ss 向 tt 跑最小费用最大流,答案为 最大流的费用 + 所有的负权边的权值和。
建图方面,是不是和上下界网络流也有一定的共同点?
这是一个通用套路。
带源汇的话,一样是 t 向 s 连容量 inf,费用 0 的边。
这也是一个通用套路。
@6 - 例题与参考代码实现@
上下界可行流模板题 sgu 194:Reactor Cooling
【sgu 都搬去和 cf 一个网站了……然而 vjudge 还是没有更新】
【51nod 换网址了……然而 vjudge 还是没有更新】
update in 2020/06/01:然而它更新了,而且加入了loj。
参考代码:
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXV = 200 + 5;
const int MAXE = 2*200*200 + 5;
const int INF = 1<<30;
struct flow_graph{
struct edge{
int to, flow, cap;
edge *nxt, *rev;
}e[MAXE], *adj[MAXV], *ecnt;
int d[MAXV], vd[MAXV], s, t;
void init() {
ecnt = &e[0];
for(int i=0;i<MAXV;i++)
adj[i] = NULL, d[i] = vd[i] = 0;
}
void addedge(int u, int v, int c) {
edge *p = (++ecnt), *q = (++ecnt);
p->to = v, p->cap = c, p->flow = 0;
p->nxt = adj[u], adj[u] = p;
q->to = u, q->cap = 0, q->flow = 0;
q->nxt = adj[v], adj[v] = q;
p->rev = q, q->rev = p;
}
int aug(int x, int tot) {
if( x == t ) return tot;
int mind = t + 1, sum = 0;
for(edge *p=adj[x];p;p=p->nxt) {
if( p->cap > p->flow ) {
if( d[x] == d[p->to] + 1 ) {
int del = aug(p->to, min(tot-sum, p->cap-p->flow));
p->flow += del, p->rev->flow -= del, sum += del;
if( sum == tot || d[s] > t + 1 ) return sum;
}
mind = min(mind, d[p->to]);
}
}
if( !sum ) {
vd[d[x]]--;
if( !vd[d[x]] ) {
d[s] = t + 2;
return sum;
}
d[x] = mind + 1;
vd[d[x]]++;
}
return sum;
}
int max_flow(int _s, int _t) {
int flow = 0; s = _s, t = _t;
while( d[s] <= t + 1 )
flow += aug(s, INF);
return flow;
}
}G;
struct edge{
int u, v, l, c;
}e[MAXE];
int deg[MAXV];
int main() {
int N, M; scanf("%d%d", &N, &M); G.init();
for(int i=1;i<=M;i++) {
scanf("%d%d%d%d", &e[i].u, &e[i].v, &e[i].l, &e[i].c);
deg[e[i].u] -= e[i].l, deg[e[i].v] += e[i].l; G.addedge(e[i].u, e[i].v, e[i].c - e[i].l);
}
int tot = 0;
for(int i=1;i<=N;i++)
if( deg[i] < 0 ) G.addedge(i, N + 1, -deg[i]);
else if( deg[i] > 0 ) G.addedge(0, i, deg[i]), tot += deg[i];
if( G.max_flow(0, N + 1) == tot ) {
puts("YES");
for(int i=1;i<=M;i++)
printf("%d\n", e[i].l + G.e[2*i-1].flow);
}
else puts("NO");
}
上下界最大流问题 zoj 3229:Shoot the Bullet(东方文花帖)。
建模比较简单,为了不剧透,附在代码最末。
参考代码:
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXV = 1500 + 5;
const int MAXE = MAXV*100 + 5;
const int INF = int(1E9);
struct FlowGraph{
struct edge{
int to, flow, cap;
edge *nxt, *rev;
}edges[MAXE], *adj[MAXV], *ecnt;
int s, t, n, d[MAXV], vd[MAXV];
void init() {
ecnt = &edges[0];
for(int i=0;i<MAXV;i++) adj[i] = NULL;
}
void addedge(int u, int v, int c) {
edge *p = (++ecnt), *q = (++ecnt);
p->to = v, p->flow = 0, p->cap = c;
p->nxt = adj[u], adj[u] = p;
q->to = u, q->flow = 0, q->cap = 0;
q->nxt = adj[v], adj[v] = q;
p->rev = q, q->rev = p;
}
int aug(int x, int tot) {
if( x == t ) return tot;
int sum = 0, mind = n + 1;
for(edge *p=adj[x];p;p=p->nxt) {
if( p->cap > p->flow ) {
if( d[p->to] + 1 == d[x] ) {
int del = aug(p->to, min(tot-sum, p->cap-p->flow));
p->flow += del, p->rev->flow -= del, sum += del;
if( sum == tot || d[s] > n ) return sum;
}
mind = min(mind, d[p->to]);
}
}
if( sum == 0 ) {
vd[d[x]]--;
if( vd[d[x]] == 0 )
d[s] = n + 1;
d[x] = mind + 1;
vd[d[x]]++;
}
return sum;
}
int max_flow(int _s, int _t, int _n) {
s = _s, t = _t, n = _n;
for(int i=0;i<MAXV;i++) d[i] = vd[i] = 0;
int flow = 0;
while( d[s] <= n )
flow += aug(s, INF);
return flow;
}
}G;
int deg[MAXV], C[MAXV], L[500 + 5][1000 + 5];
int main() {
int n, m;
while( scanf("%d%d", &n, &m) == 2 ) {
G.init(); int s = n + m + 1, t = n + m + 2;
for(int i=1;i<=m;i++) {
int x; scanf("%d", &x);
G.addedge(i, t, INF);
deg[t] += x, deg[i] -= x;
}
for(int i=1;i<=n;i++) {
int D; scanf("%d%d", &C[i], &D);
G.addedge(s, m + i, D);
for(int j=1;j<=C[i];j++) {
int T, R; scanf("%d%d%d", &T, &L[i][j], &R), T++;
G.addedge(m + i, T, R - L[i][j]);
deg[T] += L[i][j], deg[m + i] -= L[i][j];
}
}
int tot = 0;
for(int i=1;i<=n+m+2;i++) {
if( deg[i] > 0 ) G.addedge(0, i, deg[i]), tot += deg[i];
if( deg[i] < 0 ) G.addedge(i, n + m + 3, -deg[i]);
deg[i] = 0;
}
G.addedge(t, s, INF);
if( G.max_flow(0, n + m + 3, n + m + 3) == tot ) {
printf("%d\n", G.max_flow(s, t, n + m + 3));
for(int i=1;i<=n;i++) {
int j = C[i];
for(FlowGraph::edge *p=G.adj[m + i];p;p=p->nxt)
if( p->to <= m && p->to >= 1 ) L[i][j] += p->flow, j--;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=C[i];j++)
printf("%d\n", L[i][j]);
}
else printf("-1\n");
puts("");
}
}//MLE 报 Segmentation Fault, RE 也报 Segmentation Fault……
/*
建成二分图。
左边一排 n 个点表示 n 天,由源点连过来,容量为 D。
右边一排 m 个点表示 m 个女孩,连向汇点,下界为 G。
第 i 天向 Ci 个女孩连流量在 [Lij, Rij] 的边。
*/
上下界最小流问题 bzoj 2502:清理雪道。
一样的,建模附在最后面。
参考代码:
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXV = 100 + 5;
const int MAXE = MAXV*MAXV + 5;
const int INF = int(1E9);
struct FlowGraph{
struct edge{
int to, flow, cap;
edge *nxt, *rev;
}edges[MAXE], *adj[MAXV], *ecnt;
int s, t, n, d[MAXV], vd[MAXV];
void init() {
ecnt = &edges[0];
for(int i=0;i<MAXV;i++) adj[i] = NULL;
}
void addedge(int u, int v, int c) {
edge *p = (++ecnt), *q = (++ecnt);
p->to = v, p->flow = 0, p->cap = c;
p->nxt = adj[u], adj[u] = p;
q->to = u, q->flow = 0, q->cap = 0;
q->nxt = adj[v], adj[v] = q;
p->rev = q, q->rev = p;
}
int aug(int x, int tot) {
if( x == t ) return tot;
int sum = 0, mind = n + 1;
for(edge *p=adj[x];p;p=p->nxt) {
if( p->cap > p->flow ) {
if( d[p->to] + 1 == d[x] ) {
int del = aug(p->to, min(tot-sum, p->cap-p->flow));
p->flow += del, p->rev->flow -= del, sum += del;
if( sum == tot || d[s] > n ) return sum;
}
mind = min(mind, d[p->to]);
}
}
if( sum == 0 ) {
vd[d[x]]--;
if( vd[d[x]] == 0 )
d[s] = n + 1;
d[x] = mind + 1;
vd[d[x]]++;
}
return sum;
}
int max_flow(int _s, int _t, int _n) {
s = _s, t = _t, n = _n;
for(int i=0;i<MAXV;i++) d[i] = vd[i] = 0;
int flow = 0;
while( d[s] <= n )
flow += aug(s, INF);
return flow;
}
}G;
int deg[MAXV];
int main() {
G.init(); int N; scanf("%d", &N);
int s = N + 1, t = N + 2, ss = 0, tt = N + 3;
for(int i=1;i<=N;i++) {
int K; scanf("%d", &K);
deg[i] += K;
for(int j=1;j<=K;j++) {
int B; scanf("%d", &B);
G.addedge(i, B, INF); deg[B]--;
}
G.addedge(s, i, INF);
G.addedge(i, t, INF);
}
for(int i=1;i<=N;i++) {
if( deg[i] < 0 ) G.addedge(ss, i, -deg[i]);
if( deg[i] > 0 ) G.addedge(i, tt, deg[i]);
}
G.addedge(t, s, INF);
G.max_flow(ss, tt, tt);
int ans = G.adj[t]->flow;
G.adj[s] = G.adj[s]->nxt, G.adj[t] = G.adj[t]->nxt;
printf("%d\n", ans - G.max_flow(t, s, tt));
}
/*
每个点都可以作为起点/终点:源点连每个点/每个点连汇点。
必须经过即流量下界为 1。
这或许启发我们 DAG 中的最小路径覆盖/有向图中的最小环覆盖 也可以转换为上下界网络流的模型。
*/
上下界最小费用流问题 bzoj 3876: [Ahoi2014&Jsoi2014]支线剧情。
建模附在代码最后。
参考代码:
#include<queue>
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXV = 400 + 5;
const int MAXE = 20000 + 5;
const int INF = int(1E9);
struct FlowGraph{
struct edge{
int to, flow, cap, dis;
edge *nxt, *rev;
}edges[MAXE], *adj[MAXV], *cur[MAXV], *ecnt;
int s, t, cost, dist[MAXV];
void init() {
ecnt = &edges[0];
for(int i=0;i<MAXV;i++) adj[i] = NULL;
}
void addedge(int u, int v, int c, int w) {
edge *p = (++ecnt), *q = (++ecnt);
p->to = v, p->flow = 0, p->cap = c, p->dis = w;
p->nxt = adj[u], adj[u] = p;
q->to = u, q->flow = 0, q->cap = 0, q->dis = -w;
q->nxt = adj[v], adj[v] = q;
p->rev = q, q->rev = p;
}
bool inque[MAXV];
bool relabel() {
queue<int>que;
for(int i=0;i<MAXV;i++) dist[i] = INF, cur[i] = adj[i];
que.push(s); dist[s] = 0, inque[s] = true;
while( !que.empty() ) {
int f = que.front(); que.pop(); inque[f] = false;
for(edge *p=adj[f];p;p=p->nxt) {
if( p->cap > p->flow && dist[f] + p->dis < dist[p->to] ) {
dist[p->to] = dist[f] + p->dis;
if( !inque[p->to] ) {
que.push(p->to);
inque[p->to] = true;
}
}
}
}
return !(dist[t] == INF);
}
bool vis[MAXV];
int aug(int x, int tot) {
if( x == t ) {
cost += tot*dist[x];
return tot;
}
int sum = 0; vis[x] = true;
for(edge *&p=cur[x];p;p=p->nxt) {
if( p->cap > p->flow && !vis[p->to] && dist[p->to] == dist[x] + p->dis ) {
int del = aug(p->to, min(tot-sum, p->cap-p->flow));
p->flow += del, p->rev->flow -= del, sum += del;
if( sum == tot ) break;
}
}
vis[x] = false;
return sum;
}
int min_cost_max_flow(int _s, int _t) {
s = _s, t = _t; int flow = 0; cost = 0;
while( relabel() )
flow += aug(s, INF);
return flow;
}
}G;
int deg[MAXV];
int main() {
G.init(); int N; scanf("%d", &N);
int s = N + 1, t = N + 2, ss = 0, tt = N + 3, ans = 0;
for(int i=1;i<=N;i++) {
int K; scanf("%d", &K);
deg[i] += K;
for(int j=1;j<=K;j++) {
int B, T; scanf("%d%d", &B, &T);
G.addedge(i, B, INF, T); deg[B]--;
ans += T;
}
G.addedge(i, t, INF, 0);
}
G.addedge(s, 1, INF, 0);
for(int i=1;i<=N;i++) {
if( deg[i] < 0 ) G.addedge(ss, i, -deg[i], 0);
if( deg[i] > 0 ) G.addedge(i, tt, deg[i], 0);
}
G.addedge(t, s, INF, 0);
G.min_cost_max_flow(ss, tt);
printf("%d\n", ans + G.cost);
}
/*
和上一题比较类似,只是给定了边的费用,并且规定起点必须在 1。
注意因为原图(包含源汇 s, t 的那个图)是一个 DAG,所以从 ss 出发的流必然会经过 s 才会回到 tt。
即我们不需要最后再从 s 开始增广一遍。
*/
@7 - 一些类似的杂题@
poj 1637:Sightseeing tour(混合图欧拉回路问题)
这是一个比较经典的问题。思路是:尝试给无向边定向,使得每个点的入度等于其出度。
入度等于出度?是不是很像网络流中的流量守恒。
我们先随机给无向边定向,再进行调整(把某些无向边反向)。
考虑反向一条无向边会发生什么:
相当于这条边的流量-1,它反向边的流量+1。
相当于这条边的流量-2。
因为是欧拉回路,每个点的度数必须要为偶数。我们不妨给所有边的流量 / 2。
这样,调整一条边变成了给这条边的流量-1。
这就十分网络流了。直接按照上下界网络流的思路来使得它流量守恒。
一份参考代码。
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXV = 200 + 5;
const int MAXE = 5000 + 5;
const int INF = 1<<30;
struct FlowGraph{
struct edge{
int to, flow, cap;
edge *nxt, *rev;
}edges[MAXE], *adj[MAXV], *ecnt;
int s, t, d[MAXV], vd[MAXV];
void init() {
ecnt = &edges[0];
for(int i=0;i<MAXV;i++)
adj[i] = NULL, d[i] = vd[i] = 0;
}
void addedge(int u, int v, int c) {
edge *p = (++ecnt), *q = (++ecnt);
p->to = v, p->cap = c, p->flow = 0;
p->nxt = adj[u], adj[u] = p;
q->to = u, q->cap = 0, q->flow = 0;
q->nxt = adj[v], adj[v] = q;
p->rev = q, q->rev = p;
//printf("%d %d %d\n", u, v, c);
}
int aug(int x, int tot) {
//printf("%d %d %d %d\n", x, s, t, tot);
if( x == t ) return tot;
int mind = t, sum = 0;
for(edge *p=adj[x];p;p=p->nxt) {
if( p->cap > p->flow ) {
if( d[x] == d[p->to] + 1 ) {
int del = aug(p->to, min(tot-sum, p->cap-p->flow));
p->flow += del, p->rev->flow -= del, sum += del;
if( sum == tot || vd[s] > t ) return sum;
}
mind = min(mind, d[p->to]);
}
}
if( !sum ) {
vd[d[x]]--;
if( !vd[d[x]] ) {
d[s] = t + 1;
return sum;
}
d[x] = mind + 1;
vd[d[x]]++;
}
return sum;
}
int max_flow(int _s, int _t) {
int flow = 0; s = _s, t = _t;
while( d[s] <= t )
flow += aug(s, INF);
//printf("%d\n", flow);
return flow;
}
}G;
int deg[MAXV];
void solve() {
G.init(); int m, s, ss, tt;
scanf("%d%d", &m, &s);
for(int i=1;i<=m;i++) deg[i] = 0;
for(int i=1;i<=s;i++) {
int x, y, d; scanf("%d%d%d", &x, &y, &d);
if( d == 0 ) G.addedge(x, y, 1);
deg[x]++, deg[y]--;
}
for(int i=1;i<=m;i++) {
if( deg[i] % 2 ) {
puts("impossible");
return ;
}
deg[i] /= 2;
}
int tot = 0; ss = 0, tt = m + 1;
for(int i=1;i<=m;i++)
if( deg[i] > 0 ) G.addedge(ss, i, deg[i]), tot += deg[i];
else if( deg[i] < 0 ) G.addedge(i, tt, -deg[i]);
if( G.max_flow(ss, tt) == tot ) puts("possible");
else puts("impossible");
}
int main() {
int n; scanf("%d", &n);
for(int i=1;i<=n;i++) solve();
}
codeforces 708D:Incorrect Flow
也是一道涉及预流,然后调整流使得其满足网络流性质的题。
这里可以戳到我的题解
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 智能桌面机器人:用.NET IoT库控制舵机并多方法播放表情
· Linux glibc自带哈希表的用例及性能测试
· 深入理解 Mybatis 分库分表执行原理
· 如何打造一个高并发系统?
· .NET Core GC压缩(compact_phase)底层原理浅谈
· 手把手教你在本地部署DeepSeek R1,搭建web-ui ,建议收藏!
· 新年开篇:在本地部署DeepSeek大模型实现联网增强的AI应用
· Janus Pro:DeepSeek 开源革新,多模态 AI 的未来
· 互联网不景气了那就玩玩嵌入式吧,用纯.NET开发并制作一个智能桌面机器人(三):用.NET IoT库
· 【非技术】说说2024年我都干了些啥