一、网络流
1.1 概念
网络流的定义是对于一个带权的有向图集合G=(V,E),满足以下条件
- 仅有一个入度为0的源点S
- 仅有一个初度为0的汇点T
- 每条边的权值(容量)均为非负数
网络流主要有两种类型——最大流和最小费用最大流
1.2 性质
对于任意一个时刻,设f(u,v)实际流量,则整个图G的流网络满足3个性质:
- 容量限制:对任意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点本身不会"制造"和"消耗"流量。
1.3 最大流最小割定理
对于网络流中的一些边组成的集合,如果这些边被删去后让整个网络不连通,那么这些边的集合就是一个割集
最小割:容量和最小的割集
最大流最小割定理:对于一个网络,其最大流等于最小割
证明方法就不在这里赘述了,证明思路就是证明
m
a
x
f
l
o
w
≤
m
i
n
c
u
t
且
m
a
x
f
l
o
w
≥
m
i
n
c
u
t
maxflow\leq mincut 且maxflow\geq mincut
maxflow≤mincut且maxflow≥mincut
二、最大流
2.1 思想
最大流算法的核心思想都是:建立反向边
为什么呢?
因为在我们发现这条路已经行不通的时候,我们可以通过反向边来把流量退回去
即使走到了本来不存在的反向边,我们也可以理解成把之前溜流过去的给退回来
所以,建立反向边是给了算法一个反悔的机会
最大流的算法通常有两种类型的解法:增广路算法和预流推进算法
但是因为本人特别的菜,这里只介绍比较简单的增广路算法
增广路算法主要有三种:FF, E d m o n d s Edmonds Edmonds_ K a r p Karp Karp和 d i n i c dinic dinic
2.2 FF算法
FF算法主要是通过dfs来找增广路,每次dfs找,然后把正向边流量减去每次找到的,反向边加上,但是复杂度很高,所以不建议大家去写
2.3 Edmonds_Karp算法
我们考虑在FF的基础上进行优化,如果我们用bfs去找增广路经的话,我们可以在某种程度上降低复杂度,所以EK算法就是bfs找增广
可以证明EK的复杂度是 O ( n m 2 ) O(nm^2) O(nm2)
奇丑无比的代码:
int n,m,s,t,ans;
int head[N],cnt=-1,pre[N],incf[N];
bool vis[N];
struct Edge{
int to,next,w;
}e[N<<1];
inline void add(int x,int y,int c){
e[++cnt]=(Edge){y,head[x],c},head[x]=cnt;
e[++cnt]=(Edge){x,head[y],0},head[y]=cnt;
}
inline bool bfs(){
mct(vis,0);
queue<int> q;
q.push(s),vis[s]=true;
incf[s]=inf;
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=head[u];~i;i=e[i].next){
if(e[i].w){
int v=e[i].to;
if(vis[v])continue;
incf[v]=min(incf[u],e[i].w);
pre[v]=i;
q.push(v);
vis[v]=true;
if(v==t) return true;
}
}
}
return false;
}
inline void EK(){
int x=t;
while (x!=s){
int k=pre[x];
e[k].w-=incf[t];
e[k^1].w+=incf[t];
// printf("%d %d %d\n",x,k,e[k^1].to);
x=e[k^1].to;
// printf("%d %d %d\n",x,k,e[k^1].to);
}
ans+=incf[t];
}
int main()
{
mct(head,-1);
n=read(),m=read(),s=read(),t=read();
Rep(i,1,m){
int u,v,w;
u=read(),v=read(),w=read();
add(u,v,w);
}
while(bfs())EK();
printf("%d\n",ans);
return 0;
}
大家要注意网络流存图的时候要从2开始存,因为我们调用他的反向边时运用的是 x o r xor xor 1的操作
2.4 dinic算法
2.4.1 dinic
我们发现,EK每次bfs只能够寻找一条增广路,这也拖慢了时间效率,所以我们考虑使用bfs分层,然后dfs增广,这样每次分层可以找出多条增广路,于是, d i n i c dinic dinic算法诞生了
可以证明 d i n i c dinic dinic在求解一般网络时复杂度为 O ( n 2 m ) O(n^2m) O(n2m)——大部分时候比EK快
int n,m,s,t,ans;
int head[N],cnt=1;
int depth[N];
struct Edge{
int to,next,w;
}e[N];
inline void add(int x,int y,int c){
e[++cnt]=(Edge){y,head[x],c},head[x]=cnt;
e[++cnt]=(Edge){x,head[y],0},head[y]=cnt;
}
inline bool bfs(){
memset(depth,0,sizeof(depth));
queue<int> q;
depth[s]=1;
q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;
if(e[i].w&&!depth[v]){
depth[v]=depth[u]+1;
q.push(v);
if(v==t)return true;
}
}
}
return false;
}
int dfs(int u,int flow){
if(u==t)return flow;
int rest=flow,k;
for(int i=head[u];~i&&rest;i=e[i].next){
int v=e[i].to;
if(e[i].w&&depth[v]==depth[u]+1){
k=dfs(v,min(e[i].w,rest));
if(!k)depth[v]=0;
e[i].w-=k;
e[i^1].w+=k;
rest-=k;
}
}
return flow-rest;
}
inline void dinic(){
int tmp;
while(bfs())
while(tmp=dfs(s,1e9))ans+=tmp;
}
int main()
{
memset(head,-1,sizeof(head));
read(n),read(m),read(s),read(t);
Rep(i,1,m){
int x,y,c;
read(x),read(y),read(c);
add(x,y,c);
}
dinic();
printf("%d\n",ans);
return 0;
}
2.4.2 当前弧优化
对于每一个点,都记录上一次检查到哪一条边。因为我们每次增广一定是彻底增广(即这条已经被增广过的边已经发挥出了它全部的潜力,不可能再被增广了),下一次就不必再检查它,而直接看第一个未被检查的边。
优化之后渐进时间复杂度没有改变,但是实际上能快不少。 实际写代码的时候要注意,head数组初始值为-1,存储时从0开始存储,这样在后面写反向弧的时候比较方便,直接异或即可。 关于复制head的数组cur;目的是为了当前弧优化。已经增广的边就不需要再走了.
2.4.3 dinic与二分图
2.4.3.1 二分图
对于无向图G=(V,E),如果顶点V可分割为两个互不相交的子集 ( A , B ) (A,B) (A,B),并且图中的每条边 ( i , j ) (i,j) (i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集 ( i ∈ A , j ∈ B ) (i \in A,j \in B) (i∈A,j∈B),则称图G为一个二分图。
2.4.3.2 二分图的判定
二分图可以通过染色来判定,我们将一个图进行黑白染色,根据二分图的定义可知如果相邻两点同色,则该图不是二分图
在NOIP2008中出了一道题叫双栈排序,就可以用判定二分图的方法来做
2.4.3.3 二分图匹配
二分图匹配是指对于被分开的两个点集A,B,对于A中的每一个点,匹配B中的一个点并且没有矛盾,问A中最多有几个点可以匹配上
有一种非常暴力的做法叫做匈牙利算法,每次dfs寻找匹配,复杂度为 O ( n m ) O(nm) O(nm),在这里就不过多的讲了
2.4.3.4 dinic求二分图匹配
我们发现,如果我们从源点往左半边点都连一个容量为1的边,所有原图的点也都连一个容量为1的边,把右半边点都和汇点连一条边,那么整个图就变成了了一个网络,跑最大流就是答案
可以证明 d i n i c dinic dinic算法求二分图匹配的复杂度是 O ( n m ) O(n\sqrt m) O(nm)
2.5 最大流经典题型
2.5.1 拆点转化成n分图匹配
飞行员配对方案问题 裸二分图匹配+输出路径
方格取数问题 奇偶性分组转化为二分图匹配
试题库问题 稍微转化的二分图匹配+输出路径
最小路径覆盖问题 告诉你怎么做的二分图匹配+输出路径
教辅的组成 三分图匹配
[SHOI2001]小狗散步 二分图匹配+输出路径
n分图输出路径方法
在n分图中,判断一条边是否有流量即为判断其反向边流量是否不为0
2.5.2 分层图最大流问题
[CTSC1999]家园分层图最大流据说是非正解
2.5.3 利用残量网络的最大流问题
最长不下降子序列问题利用残量网络继续加边跑最大流
** 其实你会发现,通常最大流的考题都是二分图**
三、最小费用最大流
3.1 最小费用最大流
最小费用最大流,又称费用流,就是在最大流的基础上,每条边多了一个单位费用,流过该边的每单位流量都要支付相应的代价,让你求在最大流的情况下花费的最小代价
3.2 MCMF算法
MCMF(min cost max flow)算法,是在EK的基础上,每次使用spfa去增广,寻找一条最便宜的增广路,其他的和EK基本一样
个人认为费用流比最大流好理解
int n,m,s,t,maxflow,mincost;
int head[N],cnt=1;
int dis[N],flow[N],pre[N];
bool inq[N];
struct Edge{
int to,next,w,c;
}e[N<<1];
inline void add(int x,int y,int w,int c){
e[++cnt]=(Edge){y,head[x],w,c},head[x]=cnt;
e[++cnt]=(Edge){x,head[y],0,-c},head[y]=cnt;
}
inline bool spfa(){
memset(dis,0x3f,sizeof(dis));
memset(flow,0x3f,sizeof(flow));
queue<int> q;
q.push(s);
dis[s]=0;
flow[s]=1e9;
inq[s]=true;
while(!q.empty()){
int u=q.front();q.pop();
inq[u]=false;
for(int i=head[u];~i;i=e[i].next){
int v=e[i].to;
if(e[i].w&&dis[v]>dis[u]+e[i].c){
dis[v]=dis[u]+e[i].c;
pre[v]=i;
flow[v]=min(flow[u],e[i].w);
if(!inq[v])inq[v]=true,q.push(v);
}
}
}
return dis[t]<1e9;
}
inline void mcmf(){
while(spfa()){
int u=t;
while(u!=s){
e[pre[u]].w-=flow[t];
e[pre[u]^1].w+=flow[t];
u=e[pre[u]^1].to;
}
maxflow+=flow[t];
mincost+=flow[t]*dis[t];
}
}
int main()
{
memset(head,-1,sizeof(head));
read(n),read(m),read(s),read(t);
Rep(i,1,m){
int x,y,w,c;
read(x),read(y),read(w),read(c);
add(x,y,w,c);
}
mcmf();
printf("%d %d\n",maxflow,mincost);
return 0;
}
3.3 费用流经典题型
通常我们把费用流的问题转化为
源->点
f
l
o
w
=
1
,
c
o
s
t
=
0
flow=1,cost=0
flow=1,cost=0
点->点
f
l
o
w
=
x
,
c
o
s
t
=
c
(
u
,
v
)
flow=x,cost=c(u,v)
flow=x,cost=c(u,v)
点->汇
f
l
o
w
=
1
,
c
o
s
t
=
0
flow=1,cost=0
flow=1,cost=0
也是转化为二分图模型,只是多了一位费用
[SDOI2009]晨跑拆点转化为二分图
[SCOI2007]修车暴力连边转化为满二分图
[ZJOI2010]网络扩容利用跑完最大流的残量网络继续跑费用流
[NOI2008]志愿者招聘玄学连边
四、写在最后
4.1 关于时间复杂度
网络流的时间复杂度虽然看着很高,但是往往跑不到,比如按 d i n i c dinic dinic的 O ( n 2 m ) O(n^2m) O(n2m), l u o g u luogu luogu板子题都过不去
所以大家不要被复杂度吓到了——虽然可能真的过不去
4.2 关于题目来源
这里引用的是线性规划与网络流24题上的一些题目以及 l u o g u luogu luogu省选试炼场最大流,网络流,二分图三关里的一些题目,推荐大家去写一写网络流24题,质量都很高