网络流简单总结
网络流
一.最大流
1.网络流
1. 网络
网络 G=(V,E,C,s,t) 是一个连通的有向图,且满足如下性质:
对于任意一个时刻,设___f(u,v)___实际流量,则整个图G的流网络满足以下性质:
-
容量限制:对任意u,v∈V,f(u,v)≤c(u,v)。
-
反对称性:对任意u,v∈V,f(u,v)=-f(v,u)。从u到v的流量一定是从 v到u的流量的相反值。
-
流守恒性:对任意u,若u不为S或T,一定有∑f(u,v)=0,(u,v)∈E。即u
到相邻节点的流量之和为0,因为流入u的流量和u点流出的流量相等,u点本身不会"制造"和"消耗"流量。 -
val(f)=Σf(s,v)=Σf(v,t) v∈V 流 f 的值为源点流出的水流总和,也等于流入汇点的水流总和
-
对于
vX 有 f(X,X)=0 ;
对于vX,Y,Z 其中 X ∩ Y=Φ ,有\(f(X∪Y,Z)=f(X,Z)+f(Y,Z\))$
2.残余网络与增广路
- 残余网络: 在原网络上,对流量未满的边连上容量为 c(e)-f(e) 的边,表示还能推进多少流,再在原来具有流量的边上连容量为 f(e) 的反向边,表示还能回退多少流。
- 增广路:一条路径上边的残余流量均大于0的路径(可进行增广)。
3.最小割最大流定理
- 一个网络的割集为能够将原网络分为两个连通图且 S,T 分居的边集。
- 最小割为一个网络的割集中所有边和容量最小的一个割集。
- \(K(X,Y)\)是G的一个割 \(c(K)=cap(X,Y)=cap(K)\)
\(cap(X,Y)=\sum f(e)\)
可证明一个网络的最大流即为最小割的容量。
2.求解网络最大流
1.EK算法
通过反复寻找增广路来求解最大流
时间复杂度 \(O(|V||E||E|)\)
int head[N];
int cnt=0;
struct edge{
int to,next,cap;
}a[N<<1];
struct node{
int v,id;
};
node pre[N<<1];
bool vis[N<<1];
inline int BFS()
{
queue<int> Q;
Set(pre,-1);Set(vis,0);
Q.push(1);vis[1]=1;
pre[1].v=1;
while(!Q.empty()){
register int u=Q.front();Q.pop();
Start(u){
v=a[i].to;
if(!vis[v]&&a[i].cap){
pre[v].v=u;
pre[v].id=i;
vis[v]=1;
if(v==m) return 1;
Q.push(v);
}
}
}
return 0;
}
inline int EK()
{
register int max_flow=0;
while(BFS()){
register int d=INF;
for(register int i=m;i!=1;i=pre[i].v){
d=min(d,a[pre[i].id].cap);
}
for(register int i=m;i!=1;i=pre[i].v)
{
register int id=pre[i].id;
a[id].cap-=d;a[id^1].cap+=d;
}
max_flow+=d;
}
return max_flow;
}
-
FF算法
为EK算法中找增广路的方式以 dfs 的形式实现的算法
时间复杂度 \(O(f|E|)\) -
Dinic算法
通过分层标记每个节点的层次,每次只走层次递增的路,通过回溯,使得一次dfs可找到多条增广路,提高效率。
/*
最大流后,从源开始沿残量网络BFS,搜到的点打个标记。所有连接打标记和不打标记的点的边即为所求;
另有三个优化: 1. 2. 3.
*/
#define Start(x) for(register int v,i=head[x];i!=-1;i=a[i].next)
#define Set(a,b) memset(a,b,sizeof(a))
#define Copy(a,b) memcpy(a,b,sizeof(a))
int dis[N];
struct edge{
int to,next,cap;
}a[N<<1];
int head[N];
int cur[N];
int cnt=0;
int n,m;
inline int dfs(int u,int flow)
{
if(u==m) return flow;
register int delta=flow;//流入时的最大流量
for(register int v,&i=cur[u];i!=-1;i=a[i].next)
//优化1. 当前弧优化,一次dfs中一条弧不会被考虑两次(前一次已满流)
{
v=a[i].to;
if(dis[v]!=dis[u]+1||a[i].cap==0) continue;
register int d=dfs(v,min(delta,a[i].cap));//该条分流可分出多少流量;
if(d==0) dis[v]=0;//优化2. 标记已经流满的点;
a[i].cap-=d;a[i^1].cap+=d;
delta-=d;
if(!delta) break;//无更多流量可用;
}
return flow-delta;//最大能用掉的流量;
}
inline bool BFS()
{
queue<int> Q;Set(dis,-1);dis[1]=0;
Q.push(1);
while(!Q.empty()){
register int u=Q.front();Q.pop();
Start(u){
v=a[i].to;
if(!a[i].cap) continue;
if(dis[v]>dis[u]+1||dis[v]==-1){
dis[v]=dis[u]+1;
if(v==m) return 1;//优化3. 找到一条可增广路就返回
Q.push(v);
}
}
}
return (dis[m]!=-1);
}
inline void Dinic()
{
register int max_flow=0;
while(BFS()) Copy(cur,head),max_flow+=dfs(1,INF);
printf("%d\n",max_flow);
}
3.最小费用最大流
1. 定义
定义:每条边都有通过单位流量的费用,在是最大流的情况下保证费用最小。
2. 求解思路
- 先求出最大流,标记好最后残余网络的容量等,在不使流量减小的情况下改变流量去向使费用最小。
- 在找增广路进行增广时,每次找的都是从源点到汇点的最短路,这样就保证了费用最优化。
3. 实现方法
给出思路2的实现:
- EK算法 + SPFA最短路径算法 复杂度为 \(O(k*SPFA()+k*n)\).
适用于稀疏图,稠密图时 SPFA 容易被卡。
2.EK算法 + Dijkstra最短路径算法
时间复杂度 稀疏图&&使用堆优化\(O(k*mlogn)\)
稠密图 \(O(k*n^2)\)
方法: 1. 连边方法 ,除了基本的网络流连边外,还要加上每条边的费用,正向边边权为 W ,反向边为 -W 。
2. Dijkstra 求解带负边权的图的最短路径时的方法:
给每个点增加一个势函数 h(x) 表示求解新图中S到T的最短路时某点的标号值 设w(u,v)为u,v边的边权,则通过势函数给边权修正后:
\(w'(u,v)=w(u,v)+h(u)-h(v)\)
第一次求解最短路时 易知 \(h(x)=dis(x)\)
更据最短路径算法:
○1.若边(u,v)在 S到T的最短路上,则有\(h(v)=h(u)+w(u,v),w'(u,v)=0\)
○2.若边(u,v)不在 S到T 的最短路上,则\(h(v)>h(u)+w(u,v),w'(u,v)>0\)
故修改后边权均大于0
需要注意的是,若一开始图中即有容量不为0的负边,那么一开始要用SPFA求解最短路.
typedef long long ll;
typedef pair<ll,int> Pr;
#define fi first
#define se second
#define Set(a,b) memset(a,b,sizeof(a))
#define Copy(a,b) memcpy(a,b,sizeof(a))
struct edge{
int to;int next;ll cap;ll w;
}a[M<<1];
int head[N];
ll dis[N];//最短路径
ll h[N];//势函数
int pr_dot[N];//前驱点
int pr_edge[N];//前驱边
inline void Dijkstra()//EK算法求解最大流 用Dijkstra算法求解最小费用
{
Set(h,0);register ll max_flow=0;register ll min_cost=0;
while(233){
priority_queue<Pr,vector<Pr>,greater<Pr> > Q;
//尽量开在外面
Set(dis,127/3);//最好不要设成 -1 ;
Set(pr_dot,0);
Set(pr_edge,0);
dis[S]=0;Q.push(Pr(0,S));
register ll INF=dis[0];
while(!Q.empty()){
Pr u=Q.top();Q.pop();
if(u.fi!=dis[u.se]) continue;//重复进堆的删除;
if(u.se==T) break;
//搜到一条可增广路即退出(为了保证是最短路,不能在更新的时候搜到就退出)
for(register int v,i=head[u.se];i!=-1;i=a[i].next)
{
v=a[i].to;
register ll cost=a[i].w+h[u.se]-h[v];
if(a[i].cap<=0||dis[v]<=u.fi+cost)continue;
//不能直接用边权
dis[v]=dis[u.se]+cost;
pr_dot[v]=u.se;
pr_edge[v]=i;
Q.push(Pr(dis[v],v));
}
}
if(dis[T]==INF) break;
for(register int i=1;i<=n;i++) h[i]=min(h[i]+dis[i],INF);
register ll flow=INF;
for(register int u=T;u!=S;u=pr_dot[u]) flow=min(flow,a[pr_edge[u]].cap);
max_flow+=flow;
min_cost+=flow*(h[T]-h[S]);
for(register int u=T;u!=S;u=pr_dot[u]) a[pr_edge[u]].cap-=flow,a[pr_edge[u]^1].cap+=flow;
}
printf("%lld %lld\n",max_flow,min_cost);
}
int main()
{
.....
for(register int i=1;i<=m;i++){
u=read();v=read();cap=1ll*read();w=1ll*read();
add(u,v,cap,w);add(v,u,0,-w);//一开始的反向边的容量都为0 故第一次用Dijkstra 跑最短路没有关系
}
Dijkstra();
}
鉴于上述算法容易打萎,再贴一个SPFA最小费用最大流的代码
queue<int> Q;
inline void clear(queue<int> &Q) {
queue<int> P;swap(P,Q);
}
inline bool SPFA()
{
for(register int i=1;i<=T;i++) dis[i]=INF,pre_edge[i]=-1,pre_dot[i]=-1,in[i]=0;
clear(Q);
Q.push(S);
while(!Q.empty())
{
register int u=Q.front();Q.pop();
for(register int v,i=head[u];i!=-1;i=a[i].next)
{
v=a[i].to;
if(a[i].cap==0||dis[v]<=dis[u]+a[i].w) continue;
dis[v]=dis[u]+a[i].w;pre_dot[v]=u;pre_edge[v]=i;
if(!in[v]) {in[v]=1;Q.push(v);}
}
in[u]=0;
}
return (dis[T]!=INF);
}
void calc()
{
while(SPFA())//EK一遍遍跑最短路
{
register int flow=INF;
for(register int i=T;i!=S;i=pre_dot[i])
flow=min(flow,a[pre_edge[i]].cap);
ans+=flow*dis[T];
for(register int i=T;i!=S;i=pre_dot[i]) {
a[pre_edge[i]].cap-=flow;a[pre_edge[i]^1].cap+=flow;
}
}
}
4.重要(经典)模型
1. 三分图匹配
给出图一与图二,图二与图三的可能匹配关系,求可能的最大的完整匹配数
解析:和二分图匹配类似,但中间的图要拆成两个点,并连上容量为1的边,保证中间的点不被匹配多次
2.不相交最短路计数
求一张有向带权图中起点 s 到终点 t 的最多的不相交最短路
解析: 一个隐藏条件,令 \(dis_(u)\) 表示到 u 的最短路长度,一条最短路径上的边\((u,v)\)则满足 \(dis(v)=dis(u)+len(u,v)\),否则该边不可能在最短路径上,若满足上述条件,则u,v之间连一条容量为1的边(保证不相交),最大流即为答案。
3.二分图最小覆盖集
最小覆盖:每一条边至少选中一个点,要求选择的点最少
解析:
定理:二分图最大匹配数等于最小覆盖数
简要证明:匹配意味着选择了一条边连接的两个点
对于一条边,如果它没有被覆盖,说明它两端的点都没有被选择,那么就可以选择这一条边,使匹配数加一,覆盖数也会加一,因为只要选择一个点
如果一条边已经被覆盖了,说明这条边不会是匹配边(或者恰是上一种情况选择的匹配边),并且我们没有必要再选择他的一个端点覆盖,因为它已经被一个点覆盖了
所以命题成立
并且二分图最大匹配可以用网络最大流跑,于是就可以做了
4.(带权)二分图最大独立集
最大独立集:每一边只能有至多一个端点被选择,要求选择的点数尽量大
解析:
定理:最大独立集等于最小覆盖集的补集
简要证明:取反前我们选择了最少的点去覆盖边,每一条边上只选了一个点,取反之后就一定每一条边只会最多选一个点,并且点数是最多的
新建源点汇点,源点向一边连容量为权值的点,另一边向汇点连容量为权值的边,两边的点有边就连容量为INF的边
于是总权值-最大流/最小割即为答案
5.有向图最小链盖(不能相交)
最小链盖:用最少的路径覆盖有向图上的所有点(不能相交)
解析:
把点拆成两个,一个连入边,一个出边,最大匹配即为答案
原理:初始答案为点数,每有一个匹配等价于有两个点可以用一条链覆盖,需要的链数就减少1,如果有一个点有多条入边或出边,必然不能只用一条链去覆盖,而这恰好对应我们的图中一个点不能被多个点匹配
6.有向图最长反链
在有向无环图中,有如下的一些定义和性质:
链:一条链是一些点的集合,链上任意两个点x,y,满足要么x能到达y,要么y能到达x
反链:一条反链是一些点的集合,链上任意两个点x,y,满足x不能到达y,且y也不能到达x
解析:
定理:DAG中,最长反链 = 最小链覆盖(可以相交)
对偶定理:最长链长度 = 最小反链覆盖
P.S.:这个定理同样适用于求解一个序列中最少的不上升子序列个数,它等于最长上升子序列的长度
(一个数向后面不比它大的数连有向边)
路径可以相交的话,等价于每一个点和它能到达的连边,这样就不用管某条边用没用过了
用floyd传递闭包(就是求连通性),然后二分图匹配按照上一个问题搞就行了
例子:BZOJ1143祭祀\(\;\;\;\;\)
7.最大权闭合子图
在图中选取权值和尽量大的点,使得对于途中任意一条边满足,如果他的起点被选择了,那么他的终点也应该被选择
解析:
显然是有负的点权的
首先贪心的想法,随机选一个点,看满足要求之后答案是增了还是减了,减了就不选
考虑到我们决策的变化只和一次下来选的负点权的和与正点权的和的正负性有关
于是可以把负权和正权分开考虑,构建一个二分图的模型
连边就比较容易看出来了
新建源汇,源点向正权点,负权点向汇点连上容量为权值的绝对值的边,有边的点连容量为INF的边
这样我们求解最小割,如果割了一个点的边,则表示不选该点,那么答案就为所有正点权的和减去最大流/最小割了
因为割正权点代表不选该点,价值减掉权值,割负权点代表选择了该点,价值也要减掉它的绝对值
(Updated)