网络流总结

网络流

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、考试题

  • [x] 2018.1.3 团圆(费用流)
  • [x] 2018.1.5 变量(最小割)
  • [x] 2018.3.16 铁轨建设
  • [ ] 3.18.2.22 (san)(有关拓扑)

三、做题经验

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
",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
",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

原文地址:https://www.cnblogs.com/xzyxzy/p/8410853.html