网络和流
有一个有许多单向线路的铁路系统,每条线路的车票数量是有限的。现在有许多人在城市 (S),问这些人中最多有多少能到达城市 (T)?
上面的问题就是网络流问题。在解决该问题之前,我们先来了解网络和流的严格定义。
问题中有许多单向线路的铁路系统就是一张网络:一个带权有向图 (G(V,E))。
问题中每条线路的车票数量是有限的,于是 (G) 中的边带边权,对于一条边 ((u,v,w) in E),记边 ((u,v,w)) 的容量为 (c(u,v) = w)。
在问题中,人们要从城市 (S) 到城市 (T)。一张网络中有源点和汇点,记作 (S) 和 (T),且满足 (S,T in V) 且 (S eq T)。
流(Flow)是网络中的一个二元函数 (f(a,b)),且对所有 (a,b in V) 均有定义。它表示实际流经该边的流量。在问题中,(f(a,b)) 表示 (a) 到 (b) 线路的车票实际卖出的张数。
注意到,如果有 (3) 个人要从 (a) 到 (b),有 (5) 个人要从 (b) 到 (a),那么这等价于有 (2) 个人要从 (b) 到 (a),因为他们的目的地相同。所以 (f(a,b)) 和 (f(b,a)) 不会同时大于 (0),在这个例子中有 (f(b,a)=2) 且 (f(a,b)=-2),因为「有 (2) 个人要从 (b) 到 (a)」和「有 (-2) 个人要从 (a) 到 (b) 是等价的」。
进一步可以发现 (f(a,b) =-f(b,a))。
于是我们可以得到 (f) 的完整定义:
显然地,流具有以下性质:
-
容量限制
(f(u,v) le c(u,v))
-
流量平衡
对于 (u in V),(u eq S) 且 (u eq T),(sum limits_{(u,v) in E} f(u,v)=0),因为每个人要么留在 (S) 点不走要么最终到达 (T),对于中途节点,每个人到达之后肯定会再次出发。
-
斜对称性
(f(a,b) = - f(b,a))
一条边 ((a,b)) 的残余容量为 (c_f(a,b)=c(a,b)-f(a,b))。
一个网络的最大流为从 (S) 出发能流到 (T) 的最大流量。
增广路和 Edmonds-Karp 最大流算法
定义增广路为一条 (S) 到 (T) 的路径 (P={(u_1,v_1),(u_2,v_2),...(u_k,v_k)}),满足路径上每条边的残余容量均大于 (0)。
显然,一条增广路能贡献的流量为 (t=min{c_f(u_i,v_i)})。同时路径上所有 (f(u_i,v_i)) 都增加了 (t)。
似乎,当增广路不存在时,就找到了最大流。然而并不是这样的。
上图中的最大流是 (2)。然而,如果找了另外一条增广路:
这个时候流就只有 (1) 了。这是因为第一条增广路堵塞了 ((4,T)) 这条边。
我们建立反悔机制,建一个新图,给每条边建一条反向边,初始容量为 (0)。一条边每流过 (1) 单位的流量,就给这条边的容量减少 (1),再给反向边的容量增加 (1)。
如果一条增广路可以经过反向边,那么说明之前一定有另外一条增广路经过了这条边。我们可以进行「反悔」,调整这两条增广路。
上图是两条增广路,其中绿色的增广路经过了反向边。然而这样的增广路在原图中并不存在,我们将它们调整为如下的两条增广路:
这样的增广路在原图中也是合法的,我们便找到了在原图中的一条增广路。
容易发现,在新图中,不存在增广路即代表找到了最大流。
Edmonds-Karp 算法(简称 EK 算法)每次使用 BFS 寻找增广路,直到增广路不存在为止。时间复杂度为 (O(nm^2))。证明略,感兴趣的读者可以自行查阅资料。
一个找反向边的小技巧:将边从 (2) 开始标号,每次加边是原边和反向边使用相邻的编号,例如 (2) 和 (3) 互为反向边,(4) 和 (5) 互为反向边...这样 (i) 的反向边就是 (i operatorname{xor} 1),其中 (operatorname{xor}) 是按位异或。
inline bool bfs(void){
memset(vst,false,sizeof(vst));
memset(pre,0,sizeof(pre));
while(!q.empty())
q.pop();
q.push(S);
vst[S]=true;
while(!q.empty()){
int i=q.front();
q.pop();
for(int j=head[i];j;j=edge[j].next){
int to=edge[j].to;
if(!vst[to]&&edge[j].v){
q.push(to),pre[to].drop=i,pre[to].edge=j,vst[to]=true; // 记录路径
if(to==T)
return true;
}
}
}
return false;
}
inline void E_K(void)
{
int minx=0x7fffffff;
while(bfs()){
for(int i=T;i!=S;i=pre[i].drop) // 找到增广路上的最大流量
minx=std::min(minx,edge[pre[i].edge].v);
for(int i=T;i!=S;i=pre[i].drop) // 修改容量
edge[pre[i].edge].v-=minx,edge[pre[i].edge^1].v+=minx;
maxsum+=minx;
}
return;
}
Dinic 最大流算法
在有多条相同长度相同的增广路时,EK 算法不能一次性处理它们。
Dinic 算法则对这一点做出了改进。按照到 (S) 的距离,图被分成若干层。
在增广时,每一层的节点只往下一层送流。时间复杂度 (O(n^2m)),非常松的上界,几乎跑不满。
inline bool bfs(void){
std::queue <int> q=std::queue <int> ();
memset(dis,INF,sizeof(dis));
dis[S]=0;
q.push(S);
while(!q.empty()){
int i=q.front();
q.pop();
for(int j=head[i];j;j=edge[j].next){
if(edge[j].v&&dis[edge[j].to]>dis[i]+1)
dis[edge[j].to]=dis[i]+1,q.push(edge[j].to);
}
} // 标号
return dis[T]<INF;
}
int dfs(int i,int flow){
if(i==T){
return flow;
}
int maxsum=0;
for(int j=head[i];j;j=edge[j].next){
int to=edge[j].to;
if(dis[to]!=dis[i]+1||!edge[j].v)
continue;
int res=dfs(to,std::min(flow,edge[j].v));
flow-=res,edge[j].v-=res,edge[j^1].v+=res,maxsum+=res;
if(!flow)
break;
}
if(!maxsum) // 如果现在从这个节点出发找不到增广路,在**这一轮**增广中之后也一定找不到,将 dis 改为 INF 避免重复访问
dis[i]=INF;
return maxsum;
}
最小费用最大流
给每条边加上一个费用,可以看作车票有了一个价格。要求最大流,同时要费用最小,这就是最小费用最大流问题。
把反向边的权值改为原边权值的相反数,每次找 (S) 到 (T) 的最短增广路进行增广即可。
时间复杂度 (O(nmf)),其中 (f) 是最大流。可以构造数据使得该算法被卡成指数级。
然而大部分题目中网络是自行构造的,所以不需要过于担心被卡的问题。
inline bool spfa(void){
std::queue <int> q=std::queue <int> ();
q.push(S);
vis[S]=true;
memset(dis,INF,sizeof(dis));
dis[S]=0;
while(!q.empty()){
int i=q.front();
q.pop();
vis[i]=false;
for(int j=head[i];j;j=edge[j].next){
int to=edge[j].to;
if(!edge[j].v)
continue;
if(dis[to]>dis[i]+edge[j].w){
dis[to]=dis[i]+edge[j].w;
if(!vis[to])
q.push(to),vis[to]=true;
}
}
}
return dis[T]<INF;
}
int dfs(int i,int flow){
if(i==T){
return flow;
}
vis[i]=true;
int maxsum=0;
for(int j=head[i];j;j=edge[j].next){
int to=edge[j].to;
if(!edge[j].v||vis[to]||dis[to]!=dis[i]+edge[j].w)
continue;
int res=dfs(to,std::min(edge[j].v,flow));
flow-=res,maxsum+=res;
edge[j].v-=res,edge[j^1].v+=res;
cost+=res*edge[j].w; // 费用 = 单位费用 * 流量
if(!flow){
break;
}
}
if(!maxsum){
dis[i]=INF;
}
vis[i]=false;
return maxsum;
}
网络流的当前弧优化
在单轮增广中,一旦我们经过了某一条边,那么它一定会尽可能地送流。
假设从 (S) 出发到点 (u) 处剩余 (f) 的流量。处理从点 (u) 出发的边时,如果某一条边被处理过了但 (f) 仍然不为 (0),那么说明这条边流完了所有能流的流量。
如果某一条边被处理之后 (f) 变为 (0),说明这条边之前的所有边流完了所有能流的流量,但这条边可能有剩余(也有可能流完了所有流之后 (f) 恰好为 (0)),下一次从这条边开始处理即可。
以最小费用最大流举例:
inline bool spfa(void){
std::queue <int> q=std::queue <int> ();
q.push(S);
vis[S]=true;
memset(dis,INF,sizeof(dis));
dis[S]=0;
for(int i=1;i<=n;++i)
cur[i]=head[i]; // 复原 cur
while(!q.empty()){
int i=q.front();
q.pop();
vis[i]=false;
for(int j=head[i];j;j=edge[j].next){
int to=edge[j].to;
if(!edge[j].v)
continue;
if(dis[to]>dis[i]+edge[j].w){
dis[to]=dis[i]+edge[j].w;
if(!vis[to])
q.push(to),vis[to]=true;
}
}
}
return dis[T]<INF;
}
int dfs(int i,int flow){
if(i==T){
return flow;
}
vis[i]=true;
int maxsum=0;
for(int j=cur[i];j;j=edge[j].next){
cur[i]=j; // 更新 cur,表示现在处理过的边
int to=edge[j].to;
if(!edge[j].v||vis[to]||dis[to]!=dis[i]+edge[j].w)
continue;
int res=dfs(to,std::min(edge[j].v,flow));
flow-=res,maxsum+=res;
edge[j].v-=res,edge[j^1].v+=res;
cost+=res*edge[j].w;
if(!flow){
break;
}
}
if(!maxsum){
dis[i]=INF;
}
vis[i]=false;
return maxsum;
}