网络流总结
网络流
Tags:算法
一、基本内容
>Dinic基本内容
SYC:http://www.cnblogs.com/SYCstudio/p/7260613.html
朴素DFS->局限性->反悔边->低效->分层图->当前弧优化->Dinic!
>费用流基本内容
本宝宝自己写:
1.在每一条边上有单位流量的费用,你要保证流量最大的前提,费用最小
2.算法由最朴素的算法该进,便是在BFS求增广路的时候以费用为关键字跑SPFA
3.看代码
>上下界网络流
留坑
>最长反链覆盖
留坑
二、题目
1、练基础
2、刷提高
3、网络流24题
不可做题:P2775 机器人路径规划问题 https://www.luogu.org/problemnew/show/P2775
4、考试题
三、做题经验
Part 1 易打错
1、当前弧优化的循环终点别弄错
while(BFS())
{
for(int i=1;i<=cnt;i++)cur[i]=head[i];
while(int tmp=DFS(S,INF))ans+=tmp;
}
for(int i=1;i<=点数;i++)cur[i]=head[i];
题目来源:试题库问题P2763
2、源点的deep一定要是1 不然跑不出来
deep[S]=0;Q.push(S);
deep[S]=1;Q.push(S);
3、跑最大费用最大流时,dis初值为-inf
for(int i=0;i<=T;i++)dis[i]=-10000;
for(int i=0;i<=T;i++)dis[i]=-1;
因为有-cost的边所以-1会出错
题目来源:分配问题P4014
4、最大流,费用流都可以跑环
最大流分层图可以跑环,费用流SPFA也可以
Part 2 技巧
1、拆点连边技巧
蜥蜴:https://www.luogu.org/problemnew/show/P2472
为了限制一个点的流量,将它拆成两个点并连边
连有向边的时候一条边为w,cost,反过来为0,-cost
连无向边正反都是w
2、记录匹配方案
满流的边便是匹配上了
P2763 试题库问题 https://www.zybuluo.com/mdeditor#980353
P2756 飞行员配对方案问题 https://daniu.luogu.org/problemnew/show/2756
3、某边改变时
并不需要重新建图
A、判断某条边(u->v)属不属于割集,那么在残余网络中u到达不了v
B、对于一条边扩容,那么在残余网络上找增广路就好了
C、减少一条边的容量,在残余网络中找是否有该容量的路,再分情况更新
例:团圆(考试题),看某一条边是不是必须要的,重新跑SPFA即可
4、最小割问题
最小割:
割是网络中定点的一个划分,它把网络中的所有顶点划分成两个顶点集合S和T,其中源点s∈S,汇点t∈T。记为CUT(S,T),满足条件的从S到T的最小的割
百度告诉我们,最小割等于最大流
但是求的是割点的时候呢,哈哈,死掉了吧————奶牛的电信
这些最小割真的哦心【你猜呀!】【你阅读了吗?】【死掉了吧~】
5、有关术语
参考博客:https://www.cnblogs.com/jianglangcaijin/p/6035945.html
最小顶点覆盖
能覆盖所有的边的最少顶点数(或是最小点权和)
计算方法:最小顶点覆盖=最大匹配数
最大独立集
两两互不相邻的点组成的集合的最大点数(或是最大点权和)
计算方法:最大独立集=点总数-最小顶点覆盖
例题剖析——方格取数问题
和小行星有点类似,小行星是x,y,z要消掉一个使得消掉行星,便是S-x-y-z-T的流不存在,即求最小割,转化为最大流;此题把方格化为黑白点,便不存在 S->黑点->白点->T的流,顺次进行连边求最小割即最大流即可,注意边权
又用转化思想,化为黑白点后可以看做二分图,便成了最大独立集,为总点数-最小顶点覆盖(最大匹配/最小割)
最小路径覆盖
能覆盖所有点的最少边数(边权之和)
友链:http://blog.csdn.net/tramp_1/article/details/52742572
计算方法:二分图最小路径覆盖=一侧点数-最大匹配数
运用:常用来解决不重不漏经过图中所有点的问题,一般化u为u和u',对于边(u,v),link(u,v'),最大匹配数在网络流中就是最小割/最大流,看看这题
理解:最差每个点就是一条路径,每连一条边(一个匹配)就少一条路径,n-匹配数
ps:自己习惯于跑费用流,把点拆成上下两层后这样搞:
link(S,u,1,0); link(S,u',1,1); link(u',T,1,0); link(u,v',1,0);
最大权闭合子图
给定一个有向图,从中选择一些点组成一个点集V。对于V中任意一个点,其后续节点都仍然在V中
计算方法:最大权=所有正点和-最小割
或者说是最大收益问题【模板1】【模板2】【模板3】
Part 3 转化思想
1、最小割思想
很多情况下,对于网络中的每个点存在有两种状态,选或不选同意或反对等等
就将它们分成两个集合S,T表示两种对立的状态,而且点与点之间也有相互的关系
改变某点的状态需要一定代价,并因此连边。
举个例子
最大权闭合子图中,假设所有实验都选,获得收益,那么最终答案是总收益-总损失,损失表示买一个器材的代价或者放弃一个实验的损失
每个实验和器材存在有选或不选的关系,假设选为S,不选为T,那么把实验与S连边,器材与T连边后,损失成了图的最小割,最小割的割集一定由与S/T相连的边组成(中间为正无穷)。当实验不选的时候,相当于割掉其与S的边,造成其边流量的损失,计算到最小割里;当器材要选的时候,相当于割掉其与T的边,造成其边流量的代价买器材,计算到最小割里。
边容量
一般呢,哪两个点不可割就连inf,x连到S会产生w贡献那么连x到T,容量为w
2、上升序列思想
主要是这题,求某一长度的上升序列的个数,那么转化成最大流,DP求出转移后,每个点只能向该点转移到的点连边(并不是比它大就能连边),所以一条流经过的路径数就是上升序列的长度。
最大流模板
//https://www.luogu.org/problemnew/show/3376
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<queue>
using namespace std;
const int MAXN=10010;
const int MAXM=100010;
int N,M,S,T,ans,deep[MAXN];
int head[MAXN],cnt=1,cur[MAXN];
struct edge{int next,to,w;}a[MAXM<<1];
void link(int x,int y,int w){a[++cnt]=(edge){head[x],y,w};head[x]=cnt;}
int read();
queue<int>Q;
int BFS()//分好层
{
memset(deep,0,sizeof(deep));
deep[S]=1;Q.push(S);
while(!Q.empty())
{
int now=Q.front();Q.pop();
for(int i=head[now];i!=-1;i=a[i].next)
if(!deep[a[i].to]&&a[i].w)
{deep[a[i].to]=deep[now]+1;Q.push(a[i].to);}
}//给每个深度编号
return deep[T];
}
int DFS(int x,int flow)//在该层寻找所有增广路
{
if(x==T)return flow;//返回最大流
for(int &i=cur[x];i!=-1;i=a[i].next)
//&这个符号,表示i去到cur[x]的地址,那么首先i=cur[x]然后随着i的改变cur[x]跟着改变
{
if(!a[i].w||deep[a[i].to]!=deep[x]+1)continue;//要求有流且在下一层
int tmp=DFS(a[i].to,min(a[i].w,flow));
if(tmp){a[i].w-=tmp;a[i^1].w+=tmp;return tmp;}
//记得加反悔边,return tmp表示找到一条流就退出,是单路增广
}
return 0;
}
int main()
{
//Part 1 输入建图
N=read();M=read();
S=read();T=read();
memset(head,-1,sizeof(head));
for(int i=1;i<=M;i++)
{
int x=read(),y=read(),w=read();
link(x,y,w);link(y,x,0);//建边的时候要建立反悔边
}
//Part 2 逐层找增广路
while(BFS())//走的到终点即存在增广路
{
for(int i=1;i<=N;i++)cur[i]=head[i];//当前弧优化
while(int tmp=DFS(S,10000000))ans+=tmp;//加上增广的最大流
}
printf("%d\n",ans);
return 0;
}
int read()
{
char ch=getchar();
int h=0;
while(ch>'9'||ch<'0')ch=getchar();
while(ch>='0'&&ch<='9'){h=h*10+ch-'0';ch=getchar();}
return h;
}
/*
网络流Dinic
1.流程:
A.输边建图(记得加上反悔边)
B.分层跑增广路
C.重复B直到从源点走不到汇点
2.理解:
A.反悔边:详见SYC博客http://www.cnblogs.com/SYCstudio/p/7260613.html
B.如何说明分层跑是对的或者说如何跑的:
a.每一层跑完后,S一定会到不了T
b.那么在deep[T]步走不到T,那么要走到T,必须加上之前连接同一层的两个点,deep[T]至少加1
C.复杂度:
分层M,至少分N次,DFS一次是N,然后一次至少有一条边流满即剩余流量为0,视为删掉一条边
然后也许有M次所以复杂度为O(N*N*M)
然而实际上复杂度要小的多,这道题10000*10000*100000可是在0.05ms内跑过的
D.当前弧优化:
每次分好层之后,对于点P,有流进P的边和从P流出的边
如果这条边流出满了,下次就不要搜这条边,用cur记录(没流满就return了)
*/
费用流模板
//https://www.luogu.org/problemnew/show/P3381
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<queue>
using namespace std;
int read()
{
char ch=getchar();
int h=0;
while(ch>'9'||ch<'0')ch=getchar();
while(ch>='0'&&ch<='9'){h=h*10+ch-'0';ch=getchar();}
return h;
}
const int MAXN=5001;
int N,M,S,T,head[MAXN],cnt=1,anssum=0,anscost=0;
struct edge{int next,to,w,cost;}a[MAXN*20];
void link(int x,int y,int w,int cost)
{
a[++cnt]=(edge){head[x],y,w,cost};head[x]=cnt;
a[++cnt]=(edge){head[y],x,0,-cost};head[y]=cnt;
}
queue<int>Q;
int dis[MAXN],e[MAXN],pe[MAXN],px[MAXN];
//pe[i]表示到达i点的那一条边的编号
//px[i]表示到达i点的前驱节点编号
bool SPFA()
{
memset(dis,63,sizeof(dis));
dis[S]=0;Q.push(S);
while(!Q.empty())
{
int x=Q.front();
for(int i=head[x];i!=-1;i=a[i].next)
{
int R=a[i].to;
if(!a[i].w||dis[R]<=dis[x]+a[i].cost)continue;
dis[R]=dis[x]+a[i].cost;
pe[R]=i;px[R]=x;
if(!e[R]){e[R]=1;Q.push(R);}
}
e[x]=0;Q.pop();
}
return dis[T]<dis[0];
//当不存在增广路时dis[T]=dis[0]
}
int main()
{
//init
N=read();M=read();S=read();T=read();
memset(head,-1,sizeof(head));
for(int i=1;i<=M;i++)
{
int x=read(),y=read(),w=read(),cost=read();
link(x,y,w,cost);
}
//work
while(SPFA())//算出增广路上的流
{
int sum=1e9;
for(int i=T;i!=S;i=px[i])
sum=min(sum,a[pe[i]].w);
anscost+=dis[T]*sum;//记录最小费用-流量×每条边单位流量费用之和
anssum+=sum;//记录最大流
for(int i=T;i!=S;i=px[i])
a[pe[i]].w-=sum,a[pe[i]^1].w+=sum;//逐边减
}
printf("%d %d\n",anssum,anscost);
return 0;
}
参考文献:
SYC:http://www.cnblogs.com/SYCstudio/p/7260613.html
ZSY:https://www.luogu.org/blog/zhoushuyu/wang-lao-liu-zong-jie