零、前言
昨天刚刚被神秘建图方式打败了。今天又发现连知识点都没学完……
学习知料:“人菜无水平” 国集学长太阳神、看上去像法语的家伙、网络流建模基础(只是挂个链接)、“简单的网络流”(又是 “人菜无水平” 学长的)。
壹、无源汇可行流
1. 问题
每条边有 l o w ( u , v ) low(u,v) low(u,v) 需要满足 l o w ( u , v ) ⩽ f ( u , v ) ⩽ c ( u , v ) low(u,v)\leqslant f(u,v)\leqslant c(u,v) low(u,v)⩽f(u,v)⩽c(u,v),并且每个点都流量守恒(即,流量构成了很多环)。
2. 解法
首先加入源点 S S S 和汇点 T T T 。这是两个新加入的点,不需要流量守恒啥的。
按照 T r y M y E d g e \sf TryMyEdge TryMyEdge 的讲法,最小流量可以看做传送门, l o w ( u , v ) low(u,v) low(u,v) 变成 u → T u\rightarrow T u→T 而后 S → v S\rightarrow v S→v,容量均为 l o w ( u , v ) low(u,v) low(u,v) 即可。
按照 太阳神 的讲法, u → T u\rightarrow T u→T 和 S → v S\rightarrow v S→v 就是为了补充他们的流量出度、入度,剩下的 ⟨ u , v ⟩ \lang u,v\rang ⟨u,v⟩ 容量为 c ( u , v ) − l o w ( u , v ) c(u,v)-low(u,v) c(u,v)−low(u,v) 可以随便流。
做完了这个图的转换,就成了经典的网络流,直接跑最大流即可。——只是要记得此时图中的流量并非原图的流量。
我觉得这就讲的够清楚了。如果你非要再想想,那就是,如果有可行流,新图中 S S S 的出边可以满流。把原本的流量拆解到 u → T u\rightarrow T u→T 和 S → v S\rightarrow v S→v 即可。反之亦然。故,这是 等价关系。
3. 优化
由于一个点有出边也有入边,很可能会连出
S
→
u
→
T
S\rightarrow u\rightarrow T
S→u→T 的情况。生活经验 网络流基本知识告诉我们,这样的边会直接流满一条。而我们只求
S
S
S 的出边是否能满流,所以可以换一种建边的方式:对于每个
u
u
u,直接统计
∑
l
o
w
(
v
,
u
)
\sum low(v,u)
∑low(v,u) 与
∑
l
o
w
(
u
,
v
)
\sum low(u,v)
∑low(u,v),根据大小关系选择连
S
S
S 还是连
T
T
T,容量为差值。
贰、有源汇可行流
1. 问题
每条边的流量仍然有最小和最大,除了源点和汇点之外的点仍然流量守恒。源点可以流出多于流入,汇点可以流入多于流出。(不难发现,源点的净流出等于汇点净流入。)
2. 解法
这个我就不剽窃别人的讲法了。我试着自己讲一讲。讲的不好 活该你看我博客 就去康 学习知料
里的吧。(主要是文风很大程度上受到了史铁生的影响……)
模仿上面的做法。新建超级源点 S S S 和超级汇点 T T T,记原图中的源汇为 s , t s,t s,t 。之前用传送门,现在咱们用黑洞! s s s 流出多?不要紧! S → s S\rightarrow s S→s 帮你填坑! t t t 流入多?没问题! t → T t\rightarrow T t→T 人在塔在!
你满心欢喜,认为这就可以了。然而你没想到的是,现在怎么判断 S S S 是否满流?如果你想要 s s s 任意流出,不等不令 S → s S\rightarrow s S→s 容量为 + ∞ +\infty +∞ 啊。
好吧,咱们修改一下。这条边不满流不要紧,其他边满流就行!好主意。我斗胆再提醒一句,我们 求的是最大流,如果能满流,那么最大流肯定是它。现在有一条边不是满流,我们的最大流能够确保别的边满流吗?别的边不满流,也可能成为最大流嘛!没准儿存在一种其他边满流的情况,只是总流量较小呢?
行吧,看来 “黑洞” 计划泡汤了。仔细想想,为啥这样会出问题?因为我们不知道
s
,
t
s,t
s,t 之间会转移多少流量,所以
S
S
S 到
s
s
s 的连边必须是
+
∞
+\infty
+∞ 。所以 我们把流量求出来 我们让网络流内部 自行调节。我们连边
t
→
s
t\rightarrow s
t→s 容量为
+
∞
+\infty
+∞ 下界为
0
0
0 。此时再做 无源汇可行流,这条边会自发的调整为
s
,
t
s,t
s,t 之间转移的流量。
或者还用 T r y M y E d g e \sf TryMyEdge TryMyEdge 的讲法,之前我要用传送门,就是为了让资金流转有一个中介。(我们)作为中介,掌握资金的流转就是容易的事情,然后就掌握了下界的满足情况啊。可是 s , t s,t s,t 之间的流量,不需要我们去知道,只需要让它们(保持原样)存在,就像水在高处就流向低处,鸟扑打翅膀就会腾空一样自然啊。直接连接 s , t s,t s,t(而不是 S , T S,T S,T 参与其中),难道不是应当的吗?
回到理性的这一方来。类似的分析不难发现,可行流存在,必然有满流。反之亦然。
叁、上下界最大流
1. 问题
最大流当然是有源汇的啦。基本要求同 有源汇可行流
,不过要求源点的净流出最大。
2. 解法
显然要先做 有源汇可行流
,至少让它合法了再说。
然后呢?我好像不是很会最大流,除了 找增广路 啥也不会。行吧,那就暴力找。把
S
,
T
S,T
S,T 抛开不要,
s
,
t
s,t
s,t 不就是源点和汇点吗?然后就能过题。信竞结论都要证明的话,跟数竞有什么区别?
事实上我们早已埋下伏笔。无源汇可行流
云:
S
S
S 的出边满流,与存在可行流,这是等价关系。放在这里也一样。
S
S
S 的出边一直保持满流(我们没有去动它),增广时保持了其他点的流量守恒,所以 当前流永远是可行流。
所以就直接增广就行啦!并不是很难吧?——更严谨一点:对于除了我以外的人,这难道是困难的吗?
3. 优化
由于
S
S
S 出边满流,只剩下了反向边,增广路本来就不会经过它的。
T
T
T 也是一样。所以不用特别地删除二者。包括
t
→
s
t\rightarrow s
t→s 这条边,也会自动进入增广路径(其反向边),并且权值恰为可行流中的流量(在 有源汇可行流
中有提到),不删掉它反而比较方便——不需要加上可行流的流量了。
In a word, no delete.
\text{In a word, no delete.}
In a word, no delete. 我的天好押韵啊。
肆、上下界最小流
1. 问题
这个标题很难理解吗?
2. 解法
从 t t t 向 s s s 跑增广路,求出的 “最大流” 就是在可行流的基础上可以退掉的流量。只是请别忘了,现在 t → s t\rightarrow s t→s 不得不被删掉,不然跑出来就是个 + ∞ +\infty +∞ 。
不过这里有一个有趣的事情:什么是最小流?比如说 t → s t\rightarrow s t→s 的流量为 10 10 10 是否应当看做 s → t s\rightarrow t s→t 流量为 − 10 -10 −10 呢?应当是不可以的。所以净流出为负数时,应当判定为 0 0 0 。
伍、无负环上下界费用流
其实多数情况都只是略作修改而已。核心都是利用 f ′ = f − l o w f'=f-low f′=f−low,所以 ∑ f × c o s t = ∑ l o w × c o s t + ∑ f ′ × c o s t \sum f\times cost=\sum low\times cost+\sum f'\times cost ∑f×cost=∑low×cost+∑f′×cost ,左边 ∑ l o w × c o s t \sum low\times cost ∑low×cost 为定值,最小化右边 ⇔ \Leftrightarrow ⇔ 新图中的最小费用流(不管要求是什么)。
1. 无源汇最小费用可行流
然鹅跟 无源汇可行流
没啥区别。将
S
→
u
S\rightarrow u
S→u 和
u
→
T
u\rightarrow T
u→T 费用都设置为
0
0
0,直接
S
S
S 到
T
T
T 跑最小费用最大流即可。(别忘了我们是把可行流转化成了最大流。)
2. 有源汇最小费用可行流
然鹅跟 有源汇可行流
没啥区别。把
t
→
s
t\rightarrow s
t→s 的费用设置为
0
0
0,转化为了 无源汇最小费用可行流
。
3. 上下界最小费用最大流
然鹅跟 上下界最大流
没啥区别。也是先跑 有源汇最小费用可行流
,然后增广最小费用最大流。
4. 上下界最小费用最小流
不想写了,因为我 “费” 字看的太多已经看不出它是个字了。
5.5. ? ? ? \bm{\;?\;??\;} ???最大费用 ? ? \bm{\;??\;} ??流
边权取负,变为最小费用。没戳,就是介么暴力。
陆、 ∗ \color{red}^* ∗有负环费用流
并不是上下界了……但是由于 思路上有相似性 学长的博客里写了,我也只好写一写。
1. 无源汇最小费用流
考虑把负边权改为正边权。费用流中我们知道,反向边的费用是相反数。所以我们预先把负边流满,反向边则获得了我们拥有的流量。
此时这条边像不像 无源汇可行流
中的 必须流过的下界?毕竟我们规定它是流满了的!只是我们用反向边给了它 反悔 的机会罢了!
所以 ⟨ u , v ⟩ \langle u,v\rangle ⟨u,v⟩ 的连边方式仍然是 u → T , S → v u\rightarrow T,\;S\rightarrow v u→T,S→v 。而后仍然跑最小费用最大流。(这里不用判断满流了,因为肯定会满流 ⇔ \Leftrightarrow ⇔ 存在可行流。)
2. 有源汇最小费用流
类似 无源汇最小费用流
,只是额外加入
t
→
s
t\rightarrow s
t→s 罢了。如 太阳神 之语:“这是常见套路。”
柒、例题与代码
“第一次学网络流,模板题得 9 9 9 分就算成功,找什么例题啊?”—— T r y M y E d g e \sf TryMyEdge TryMyEdge
无源汇有上下界可行流
#include <cstdio>
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
inline int readint(){
int a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
const int MaxN = 205;
const int MaxM = 10202;
struct Edge{
int to, nxt, val;
Edge(){ /* nothing */; }
Edge(int T,int N,int V){
to = T, nxt = N, val = V;
}
};
Edge e[MaxM<<2];
int head[MaxN], cntEdge;
void addEdge(int a,int b,int c){
e[cntEdge] = Edge(b,head[a],c);
head[a] = cntEdge ++;
e[cntEdge] = Edge(a,head[b],0);
head[b] = cntEdge ++;
}
int dis[MaxN]; queue< int > q;
bool bfs(int x,const int &t,int n){
memset(dis,-1,n<<2);
dis[x] = 0; q.push(x);
while(!q.empty()){
x = q.front(); q.pop();
// printf("x = %d with dis = %d\n",x,dis[x]);
for(int i=head[x]; ~i; i=e[i].nxt)
if(!(~dis[e[i].to]) && e[i].val){
dis[e[i].to] = dis[x]+1;
q.push(e[i].to);
}
}
return dis[t] != -1;
}
int cur[MaxN]; // curly optimization
int dfs(int x,int inFlow,const int &t){
int sum = 0; if(x == t) return inFlow;
for(int &i=cur[x]; ~i; i=e[i].nxt){
if(dis[e[i].to] == dis[x]+1 && e[i].val){
int d = min(inFlow-sum,e[i].val);
e[i].val -= (d = dfs(e[i].to,d,t));
e[i^1].val += d; sum += d;
if(sum == inFlow) break; // gone
}
}
if(!sum) dis[x] = -1;
// printf("%lld return %lld\n",x,sum);
return sum;
}
const int infty = (1<<30)-1;
int dinic(int s,int t,int n){
int tiny_sy = 0;
while(bfs(s,t,n)){
memcpy(cur,head,n<<2);
tiny_sy += dfs(s,infty,t);
}
return tiny_sy;
}
int low[MaxM<<2], deg[MaxN];
signed main(){
int n = readint(), m = readint();
memset(head,-1,MaxN<<2);
for(int u,v,l,c,i=0; i<m; ++i){
u = readint(), v = readint();
l = readint(), c = readint();
low[cntEdge] = l;
addEdge(u,v,c-l);
deg[u] -= l, deg[v] += l;
}
int check = 0;
for(int i=1; i<=n; ++i)
if(deg[i] > 0){
addEdge(0,i,deg[i]);
check += deg[i];
}
else if(deg[i] < 0)
addEdge(i,n+1,-deg[i]);
if(dinic(0,n+1,n+2) != check){
puts("NO"); return 0;
} else puts("YES");
for(int i=0; i<m; ++i)
printf("%d\n",e[i<<1|1].val+low[i<<1]);
return 0;
}
有源汇有上下界最大流
#include <cstdio>
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
inline int readint(){
int a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
const int MaxN = 205;
const int MaxM = 10202;
struct Edge{
int to, nxt, val;
Edge(){ /* nothing */; }
Edge(int T,int N,int V){
to = T, nxt = N, val = V;
}
};
Edge e[MaxM<<2];
int head[MaxN], cntEdge;
void addEdge(int a,int b,int c){
e[cntEdge] = Edge(b,head[a],c);
head[a] = cntEdge ++;
e[cntEdge] = Edge(a,head[b],0);
head[b] = cntEdge ++;
}
int dis[MaxN]; queue< int > q;
bool bfs(int x,const int &t,int n){
memset(dis,-1,n<<2);
dis[x] = 0; q.push(x);
while(!q.empty()){
x = q.front(); q.pop();
// printf("x = %d with dis = %d\n",x,dis[x]);
for(int i=head[x]; ~i; i=e[i].nxt)
if(!(~dis[e[i].to]) && e[i].val){
dis[e[i].to] = dis[x]+1;
q.push(e[i].to);
}
}
return dis[t] != -1;
}
int cur[MaxN]; // curly optimization
int dfs(int x,int inFlow,const int &t){
int sum = 0; if(x == t) return inFlow;
for(int &i=cur[x]; ~i; i=e[i].nxt){
if(dis[e[i].to] == dis[x]+1 && e[i].val){
int d = min(inFlow-sum,e[i].val);
e[i].val -= (d = dfs(e[i].to,d,t));
e[i^1].val += d; sum += d;
if(sum == inFlow) break;
}
}
if(!sum) dis[x] = -1; return sum;
}
const int infty = (1<<30)-1;
int dinic(int s,int t,int n){
int tiny_sy = 0;
while(bfs(s,t,n)){
memcpy(cur,head,n<<2);
tiny_sy += dfs(s,infty,t);
}
return tiny_sy;
}
int low[MaxM<<2], deg[MaxN];
signed main(){
int n = readint(), m = readint();
int s = readint(), t = readint();
memset(head,-1,MaxN<<2);
for(int u,v,l,c,i=0; i<m; ++i){
u = readint(), v = readint();
l = readint(), c = readint();
low[cntEdge] = l;
addEdge(u,v,c-l);
deg[u] -= l, deg[v] += l;
}
int check = 0;
for(int i=1; i<=n; ++i)
if(deg[i] > 0){
addEdge(0,i,deg[i]);
check += deg[i];
}
else if(deg[i] < 0)
addEdge(i,n+1,-deg[i]);
addEdge(t,s,infty); // to return
if(dinic(0,n+1,n+2) != check){
puts("please go home to sleep");
return 0; // take a shower
}
else printf("%d\n",dinic(s,t,n+2));
return 0;
}
有源汇有上下界最小流
#include <cstdio>
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
typedef long long int_;
inline int readint(){
int a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
const int MaxN = 50010;
const int MaxM = 125010;
struct Edge{
int to, nxt; int_ val;
Edge(){ /* nothing */; }
Edge(int T,int N,int_ V){
to = T, nxt = N, val = V;
}
};
Edge e[MaxM<<2];
int head[MaxN], cntEdge;
void addEdge(int a,int b,int_ c){
e[cntEdge] = Edge(b,head[a],c);
head[a] = cntEdge ++;
e[cntEdge] = Edge(a,head[b],0);
head[b] = cntEdge ++;
}
int dis[MaxN]; queue< int > q;
bool bfs(int x,const int &t,int n){
memset(dis,-1,n<<2);
dis[x] = 0; q.push(x);
while(!q.empty()){
x = q.front(); q.pop();
// printf("x = %d with dis = %d\n",x,dis[x]);
for(int i=head[x]; ~i; i=e[i].nxt)
if(!(~dis[e[i].to]) && e[i].val){
dis[e[i].to] = dis[x]+1;
q.push(e[i].to);
}
}
return dis[t] != -1;
}
int cur[MaxN]; // curly optimization
int_ dfs(int x,int_ inFlow,const int &t){
int_ sum = 0; if(x == t) return inFlow;
for(int &i=cur[x]; ~i; i=e[i].nxt){
if(dis[e[i].to] == dis[x]+1 && e[i].val){
int_ d = min(inFlow-sum,0ll+e[i].val);
e[i].val -= (d = dfs(e[i].to,d,t));
e[i^1].val += d; sum += d;
if(sum == inFlow) break;
}
}
if(!sum) dis[x] = -1; return sum;
}
const int_ infty = (1ll<<60)-1;
int_ dinic(int s,int t,int n){
int_ tiny_sy = 0;
while(bfs(s,t,n)){
memcpy(cur,head,n<<2);
tiny_sy += dfs(s,infty,t);
}
return tiny_sy;
}
int low[MaxM<<2]; int_ deg[MaxN];
signed main(){
int n = readint(), m = readint();
int s = readint(), t = readint();
memset(head,-1,MaxN<<2);
for(int u,v,l,c,i=0; i<m; ++i){
u = readint(), v = readint();
l = readint(), c = readint();
low[cntEdge] = l;
addEdge(u,v,c-l);
deg[u] -= l, deg[v] += l;
}
int_ check = 0;
for(int i=1; i<=n; ++i)
if(deg[i] > 0){
addEdge(0,i,deg[i]);
check += deg[i];
}
else if(deg[i] < 0)
addEdge(i,n+1,-deg[i]);
addEdge(t,s,infty); // to return
if(dinic(0,n+1,n+2) != check){
puts("please go home to sleep");
return 0; // take a shower
}
check = e[head[s]].val; // current flow
head[s] = e[head[s]].nxt; // delete
head[t] = e[head[t]].nxt; // delete
printf("%lld\n",max(0ll,check-dinic(t,s,n+2)));
return 0;
}
网络流二十四题
别指望这里会有二十四份代码。