【瞎口胡】网络流基础
网络和流
有一个有许多单向线路的铁路系统,每条线路的车票数量是有限的。现在有许多人在城市 \(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 \neq 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 \neq S\) 且 \(u \neq 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;
}