网络流的基础内容就不详细发了,网上到处都是,可自学。 总版点这里

ps:以下有些链接是hihocoder的题目(题面有详细讲解),请确保先登录hihocoder,再点击进入相应题目网页。

最大流

基础知识

EK算法

Dinic算法(接弧优化)

EK与Dinic算法的对比讲解

如果实在看不懂,这里简短地跟你说明白(本人已经好长一段时间没钻网络流了,这段是靠记忆手打的,如有误可以指正):

增广路径:从起点到终点的一条路径,该路径上所有边都没有满流。当残余网络中再没有增广路径时,也就是说原网络已经无法再往任何一条从起点到终点的路径中加流了,此时增广过的所有路径的流的总和就是最大流

普通dfs:dfs找到一条增广路径,增广这条路径后回溯,找新的增广路。(这个算法效率低是因为增广一条路径后,回溯时要从这条路径上的某个地方跳出该路径,改走残余网络中其它子路径到终点,从而形成新的增广路。但这条增广路前面一段已经被增广过,也就是说边的剩余容量普遍较低,从而导致回溯出新的增广路能新加的流较少,增广一次只能加少量流,因此所需的增广次数较多,增广效率低)

Ford-Fulkerson:这个方法跟普通dfs没多少区别,区别就是该算法增广一条路径后从起点重新找增广路。(但实际上它下一次找的增广路正常情况下还是按照上一次增广的路径走,只是找到第一条满流的边后改道,这跟普通dfs回溯到的位置是一模一样的……所以说这个方法效率也不高,也没什么人用)

EK:从起点开始bfs找增广路,当某条路径延伸到终点时,增广这条路径并从起点重新开始bfs找增广路。(bfs的优势就在于从每个点向所有边扩张,并更新从起点到每个点当前在一条路径中能通过的最大流量,这样从起点延伸到终点的流量是该残余网络中能通过流很大的增广路(目前还未想通是不是最大),这样增广一次就能加很多流,因此所需的增广次数较少,增广效率高。这就是为什么EK(bfs)面对10000个点的网络跑得飞快而dfs直接卡爆)

Dinic:用bfs给残余网络分层(建议从终点开始分层,原因在下文有补充),这里假设大家按照我的建议从终点开始分层,那么分层就可以理解为算出每个点到终点的最短距离。每次从起点开始,只能走邻层的点,这样可以确保增广路是残余网络的最短路径。

   Q:为什么要确保增广路是最短路径? A:上面我发的讲Dinic算法的那篇文章里有一个栏目叫“朴素算法的低效之处”,分层图就是解决那种例子的——优先让入终点的边满流(很显然从起点到终点的流必须得经过入终点的边),这样就不用担心像那个坑爹的例子一样,被多余的小流边把增广路的流卡得特别小,然后所需的增光次数就很多,拉低增广效率。

   按照走邻层的规则,用dfs找到一条增广路后,更新这条路径上的流,然后重新做残余网络的分层图,并回到起点重新用dfs找增广路。

   Q:那这里又为什么要用dfs找增广路? A:由于增广路上的流会被更新,因此至少会有一条边满流,不能出现在分层图中,因此每次增广一条路后要重新做分层图。如果只找一条不要求流量的增广路的话(要求邻层即可),用dfs一般情况下能直接搜出这条路径,效率较高。bfs的队列扩展就显得多余了。

模板题:luogu3376

2018.3.21补充:第一份30分普通dfs代码有问题(没有回溯到满流边之前,有可能把满流边再加流至溢出),但不知道为什么能过数据啊,难道是数据太水了?

 #include<bits/stdc++.h>
using namespace std;
inline int read(){
int x=,f=; char c=getchar();
for(;!isdigit(c);c=getchar()) if(c=='-') f=-;
for(;isdigit(c);c=getchar()) x=(x<<)+(x<<)+c-'';
return x*f;
}
int n,m,s,t,fa[],faedge[],ans;
bool vis[];
struct edge{
int v,w,next;
}e[];
int head[],cnt;
inline void add(int w,int v,int u){
e[++cnt]=(edge){v,w,head[u]};
head[u]=cnt;
}
void dfs(int cur){
int i;
if(cur==t){
i=t;
int flow=;
while(i!=s) flow=min(flow,e[faedge[i]].w), i=fa[i];
i=t;
while(i!=s) e[faedge[i]].w-=flow, i=fa[i];
ans+=flow;
return;
}
vis[cur]=;
for(i=head[cur];i;i=e[i].next){
if(vis[e[i].v]) continue;
fa[e[i].v]=cur;
faedge[e[i].v]=i;
dfs(e[i].v);
}
vis[cur]=;
}
int main(){
n=read(),m=read(),s=read(),t=read();
for(int i=;i<=m;i++){
add(read(),read(),read());
}
dfs(s);
printf("%d\n",ans);
return ;
}

30分普通dfs(有问题)

30分普通dfs(已改正)
 #include<bits/stdc++.h>
#define maxn 10001
#define maxm 100001
#define inf 0x7f7f7f7f
using namespace std;
inline int read(){
int x=,f=; char c=getchar();
for(;!isdigit(c);c=getchar()) if(c=='-') f=-;
for(;isdigit(c);c=getchar()) x=(x<<)+(x<<)+c-'';
return x*f;
}
int n,m;
struct edge{
int to,cap,flow,next;
}e[maxm<<];
int head[maxn],cnt,rhead[maxn];
int fa[maxn],faedge[maxn],flow[maxn];
struct EdmondsKarp{
inline void init(){
memset(head,-,sizeof head);
memset(rhead,-,sizeof rhead);
}
inline void addedge(int from,int to,int cap){
e[cnt]=(edge){to,cap,,head[from]};
head[from]=cnt++;
e[cnt]=(edge){from,,,rhead[from]};
rhead[from]=cnt++;
}
int maxflow(int s,int t){
int ans=,u,v,i;
while(){
memset(flow,,sizeof flow);
queue<int>q;
q.push(s);
flow[s]=inf;
while(!q.empty()){
u=q.front(); q.pop();
for(i=head[u];i!=-;i=e[i].next){
v=e[i].to;
if(!flow[v] && e[i].cap>e[i].flow){
fa[v]=u;
faedge[v]=i;
flow[v]=min(flow[u],e[i].cap-e[i].flow);
q.push(v);
}
}
if(flow[t]) break;
}
if(!flow[t]) break; i=t;
while(i!=s){
e[faedge[i]].flow+=flow[t];
e[faedge[i]^].flow-=flow[t];
i=fa[i];
}
ans+=flow[t];
}
return ans;
}
}aug; int main(){
int s,t,u,v,w;
n=read(),m=read(),s=read(),t=read();
aug.init();
for(int i=;i<m;i++){
u=read(),v=read(),w=read();
aug.addedge(u,v,w);
}
printf("%d\n",aug.maxflow(s,t));
return ;
}

100分EK(最慢点128ms)

 #include<bits/stdc++.h>
#define inf 0x7f7f7f7f
#define maxn 10001
#define maxm 100001
using namespace std;
inline int read(){
int x=,f=; char c=getchar();
for(;!isdigit(c);c=getchar()) if(c=='-') f=-;
for(;isdigit(c);c=getchar()) x=(x<<)+(x<<)+c-'';
return x*f;
}
class Dinic_Enhancer
{
private:
struct edge{
int v,w,next;
}e[maxm<<];
int cnt,head[maxn];
int depth[maxn],cur[maxn];//cur就是记录当前点u循环到了哪一条边(弧优化)
public:
int n,m,s,t;
void init()
{
cnt=;
memset(head,-,sizeof(head)); n=read(),m=read(),s=read(),t=read();
int u,v,w;
for(int i=;i<m;i++){
u=read(),v=read(),w=read();
add_edge(u,v,w);
}
}
void add(int u,int v,int w)
{
e[cnt].v=v;
e[cnt].w=w;
e[cnt].next=head[u];
head[u]=cnt++;
}
void add_edge(int u,int v,int w)
{
add(u,v,w);
add(v,u,);
}
int dfs(int u,int flow)
{
if(u==t) return flow;
for(int& i=cur[u];i!=-;i=e[i].next){ //注意这里的&符号,这样i增加的同时也能改变cur[u]的值,达到记录当前弧的目的
if(depth[e[i].v]==depth[u]+ && e[i].w)
{
int di=dfs(e[i].v,min(flow,e[i].w));
if(di>)
{
e[i].w-=di;
e[i^].w+=di;
return di;
}
}
}
return ;
}
int bfs()
{
queue<int> Q;
memset(depth,,sizeof(depth));
depth[s]=;
Q.push(s);
int i,u;
while(!Q.empty())
{
u=Q.front(); Q.pop();
for(i=head[u];i!=-;i=e[i].next)
if(depth[e[i].v]== && e[i].w>)
{
depth[e[i].v]=depth[u]+;
if(e[i].v==t) return ;
Q.push(e[i].v);
}
}
return ;
}
int dinic()
{
int ans=,i,flow;
while(bfs())
{
for(i=;i<=n;i++) cur[i]=head[i];//每一次建立完分层图后都要把cur置为每一个点的第一条边 感谢@青衫白叙指出这里之前的一个疏漏
while(flow = dfs(s,inf)) ans+=flow;
}
return ans;
}
}de; int main(){
de.init();
printf("%d\n",de.dinic());
return ;
}

100分Dinic(最慢点44ms)

考虑到ISAP算法网上没几个说得清楚的,我在这里单独说一下吧。

  ISAP的算法究竟在哪里做了优化呢?其实Dinic算法的弧优化给了我们很好的启示。既然能从上一次走过的边开始走,那我们就可以考虑一下是否可以每次dfs出一条增广路径后只更改单个点的层数(深度),而不用每次都bfs更新分层图。Dinic算法中,当我们沿着残量网络找到一条增广路增广后,残量网络肯定会变化(至少有一条边的流量满了,因此这条边相当于不在图中了),因此决定允许弧的d数组要进行相应的更新(Dinic 的做法就是每次增广后 都重新计算整个残量网络的d数组)。而ISAP改进的地方之一就是,其实没有必要马上更新d数组。这是因为,去掉一条边只可能令最短路径变得更长,而如果增广之前的残量网络存在另一条最短路,并且在增广后的残量网络中仍存在,那么这条路径毫无疑问是最短的。

  当残量网络中的一个点 与分层图中的所有邻层点连的边都满流 时,这时说明这个点到终点的最短路径都增广过了,要增广更长的路径了。因此要把它的弧指向剩下的未满流的边。但指向哪条呢?剩下的未满流的边指向的点都与这个点不是邻层的,由于当前走的一定是上一次残量网络的最短路径,因此 沿着这些不是邻层的点到终点的距离 一定是更长的,且连向这些不是邻层的点的边一定都未满流(可自行思考)。为了确保下一次仍然走的是残量网络中的最短路径,只要把弧指向 剩下的未满流的边指向的点 中 距离终点最近的一个,这样就走的是残量网络中的最短路径了。(具体证明可以自己接着意会一下)

  这样ISAP算法就只需要在最开始bfs一次,也就是给图分一次层就可以了。实际写代码的时候可以从终点开始分层(即终点深度为0),这样一个点的深度就是它到终点的最短距离了。当这个点所在的最短路都被增广过,也就是它 与分层图中的所有邻层点连的边都满流 时,把它的深度改为 所有与它相连的非邻层的点 中深度最低(即到终点距离最近)的加1 就可以了。其实从终点分层还有一个好处,见下面附录。

gap优化:如果你听说过ISAP算法也就应该听过gap优化。gap优化是个什么东西?可以这么说:

  gap优化可以提前结束程序,很多时候提速非常明显(高达 100 倍以上)。通过上面描述的ISAP算法,我们知道了可以通过不断更改满流边所连点的层数 来寻找新的增广路径。gap优化就是说,更改一个点u的层数时(设它的弧原来指向t,它原来的层数depth为d[u],点t原来的层数depth为d[t]),u,t之间的连通性会消失,但如果在这之前u是残量网络最后一个和t距离d[u](更新前)的点,那么此时源点和汇点也不连通了。从分层图的角度来说,两点之前连通时是邻层的,即d[u]-1=d[t],而如果残量网络中连接d[u]和d[t]两层的边只有这一条,那么当这条边的流量流完时(即需要更改点u的层数),整个网络的分层图就没有连接d[u]和d[t]两层的边了,而我们又知道只能走邻层点,且源点层数最大、汇点层数最小,因此此时源点和汇点就不连通了。gap优化的实现非常简单,用一个数组记录并在适当的时候判断要更改层数的点是否满足gap优化,若满足就结束程序即可。

 //ISAP
#include<bits/stdc++.h>
#define inf 0x7f7f7f7f
#define maxn 10001
#define maxm 100001
using namespace std;
inline int read(){
int x=,f=; char c=getchar();
for(;!isdigit(c);c=getchar()) if(c=='-') f=-;
for(;isdigit(c);c=getchar()) x=(x<<)+(x<<)+c-'';
return x*f;
}
class ISAP
{
private:
struct edge{
int v,w,next;
}e[maxm<<];
int cnt,head[maxn];
int fa[maxn],faedge[maxn],num[maxn];
int depth[maxn],cur[maxn];//cur就是记录当前点u循环到了哪一条边(弧优化)
public:
int n,m,s,t;
void init()
{
cnt=;
memset(head,-,sizeof(head)); n=read(),m=read(),s=read(),t=read();
int u,v,w;
for(int i=;i<m;i++){
u=read(),v=read(),w=read();
add_edge(u,v,w);
}
}
void add(int u,int v,int w)
{
e[cnt].v=v;
e[cnt].w=w;
e[cnt].next=head[u];
head[u]=cnt++;
}
void add_edge(int u,int v,int w)
{
add(u,v,w);
add(v,u,);
}
int bfs()
{
int i,u; queue<int> Q;
for(i=;i<=n;i++) depth[i]=n;
depth[t]=;
Q.push(t);
while(!Q.empty())
{
u=Q.front(); Q.pop();
for(i=head[u];i!=-;i=e[i].next){
if(depth[e[i].v]>depth[u]+ && e[i^].w) //注意ISAP中只做一次bfs,而Dinic要做若干次分层图。ISAP做bfs的目的不是做分层图,而是为了在主程序中从每个点到终点的最短距离开始计算路径,求的是每个点到终点的最短距离,因此可以不断更新;而Dinic的分层图是为了找到从起点到终点的最短路径,使在dfs过程中按照这条最短路径走,而不拐到最短路径外的点,所以分层图中每个点的层必须是第一次广搜遍历到的,不能做更改。这就是为啥这里判的是depth[e[i].v]>depth[u]+1
{
depth[e[i].v]=depth[u]+;
Q.push(e[i].v);
}
}
}
if(depth[s]>) return ;
return ;
}
int augment(){
int u=t,flow=inf;
while(u!=s) u=fa[u], flow=min(flow,e[cur[u]].w);
u=t;
while(u!=s) u=fa[u], e[cur[u]].w-=flow, e[cur[u]^].w+=flow;
return flow;
}
int isap()
{
int ans=,flow,u=s,i;
bfs();
for(i=;i<=n;i++) num[depth[i]]++, cur[i]=head[i];//每一次建立完分层图后都要把cur置为每一个点的第一条边 感谢@青衫白叙指出这里之前的一个疏漏
while(depth[s]<=n){
if(u==t){
ans+=augment();
u=s;
}
bool done=;
for(i=cur[u];i!=-;i=e[i].next){
if(depth[u]==depth[e[i].v]+ && e[i].w){
fa[e[i].v]=u;
cur[u]=i; //记录当前弧!在回溯边时也可以用!
done=;
u=e[i].v;
break;
}
}
if(!done){
int mn=n;
for(i=head[u];i!=-;i=e[i].next)
if(e[i].w) mn=min(mn,depth[e[i].v]);
if(--num[depth[u]]==) return ans;
num[depth[u]=mn+]++;
cur[u]=head[u];
if(u!=s) u=fa[u]; //一定要返回!因为这个点之前和前一个点在一条增广路上,两点深度相邻,而修改了这个点的深度后这个点和前一个点的深度就不相邻了(前一个点到起点的路径深度依然连续相邻),为了维护路径深度的连续性,要回到前一个点去找其他路径(第一次交这个代码没写这行,改完这个点的深度就接着从这条边往下走了,白搜了无数次)
}
}
return ans;
}
}ISAP; int main(){
ISAP.init();
printf("%d\n",ISAP.isap());
return ;
}

100分ISAP(最慢点20ms)

补充:Dinic和ISAP算法中 从终点开始分层的好处(有人可能不知道,不知道的话一定要看看)

作为一个资深OIer的你应该知道为什么做分层图时,要从终点开始分层。如果不知道,你画个图yy一下就明白了。

从起点开始分层标号后的分层图如上。那么你的程序从起点开始搜索增广路时,就可能先搜索长的那条路,即1-2-3-4-3。此时得走到4-3边时,程序才发现这条路径上有两个相邻点不是邻层(这里的邻层:后一个点的深度是前一个点的+1。不能逆过来),从而返回。但是这条更长的子路都走完了才发现它不是最短(即路径的最后两个点不满足深度递增1),假如有一组黑数据,把上面这张图中的长路径改得更长(比如经过十万八万个点的),那把这条路再走一遍,增广过程就退化成dfs寻找从起点到终点了,效率还不低死?有没有什么办法能让它在进入路径的地方就判断这条子路是不是最短(即是不是当前图的增广路的一部分)呢?

当然有了。从终点开始分层标号就好了。

此时,从起点出发的话,上面那条更长路径刚一进去就发现两个点不邻层了(这里的邻层:后一个点的深度是前一个点的-1。不能逆过来),从而立刻判断这条路不是最短,也就不是增广路中的一部分,从而退回去寻找其他路径。这样整个图每次成功前进时(即不退回)都走的是最短路,那么就能一次沿最短路走到终点,这条最短路也就是增广路了。

相信大家都已经理解了吧!

最小割最大流

讲解

【2018.2.8-】网络流学习笔记(含ISAP!)的更多相关文章

  1. 2018/2/14 设计模式学习笔记(一) 自己实现ArrayList,LinkedList和Iterator,以及在此过程中对于面向对象,面向接口,还有抽象类的一些思考感悟

    因为本人目前为止学习编程不过七个月,所以后面的感悟对于一些大神来说可能嗤之以鼻,但对于一些刚刚入门的萌新来说在理解面向对象的思想上,以及抽象类和接口应该怎么设计等方面应该还是会有所帮助的 首先我们定义 ...

  2. 2018/2/13 ElasticSearch学习笔记三 自动映射以及创建自动映射模版,ElasticSearch聚合查询

    终于把这些命令全敲了一遍,话说ELK技术栈L和K我今天花了一下午全部搞定,学完后还都是花式玩那种...E却学了四天(当然主要是因为之前上班一直没时间学,还有安装服务时出现的各种error真是让我扎心了 ...

  3. 2018/1/19 Netty学习笔记(一)

    这段时间学了好多好多东西,不过更多是细节和思想上的,比如分布式事物,二次提交,改善代码质量,还有一些看了一些源码什么的; 记录一下真正的技术学习,关于Netty的学习过程; 首先说Netty之前先说一 ...

  4. 2018/1/28 RocketMq学习笔记

    RocketMq是支持Topic模式的MQ中间件,它的传输格式为topic(主题,一个product对应一个主题,),Tag(标签,其实就是副标题,是为了更好的支持集群模式而出现的,这样客户端可以指定 ...

  5. 2018/1/9 redis学习笔记(一)

    本文不涉及redis基本命令以及javaapi的解释操作; 首先介绍下redis,一个nosql非关系型数据库,运行在缓存中,特点就是可存储的数据结构类型很多,做为KEY-VALUE数据库,它的键只能 ...

  6. $2018/8/19 = Day5$学习笔记 + 杂题整理

    \(\mathcal{Morning}\) \(Task \ \ 1\) 容斥原理 大概这玩意儿就是来用交集大小求并集大小或者用并集大小求交集大小的\(2333\)? 那窝萌思考已知\(A_1,A_2 ...

  7. 2018/3/14 Hadoop学习笔记(一)

    首先,什么是Hadoop?为什么它是现在大数据处理最热门的框架呢?(正确来说,现在Hadoop是一个生态圈) Hadoop是apache下一套开源的服务框架,它主要的作用就是利用服务器集群,来对海量数 ...

  8. RxJava2.0学习笔记2 2018年7月3日 周二

    摘记: 1.map -- 转换  有些服务端的接口设计,会在返回的数据外层包裹一些额外信息,这些信息对于调试很有用,但本地显示是用不到的.使用 map() 可以把外层的格式剥掉,只留下本地会用到的核心 ...

  9. 深度学习中的序列模型演变及学习笔记(含RNN/LSTM/GRU/Seq2Seq/Attention机制)

    [说在前面]本人博客新手一枚,象牙塔的老白,职业场的小白.以下内容仅为个人见解,欢迎批评指正,不喜勿喷![认真看图][认真看图] [补充说明]深度学习中的序列模型已经广泛应用于自然语言处理(例如机器翻 ...

随机推荐

  1. Azure 进阶攻略 | 关于Java 和事件中心的那不得不说的事

    物联网技术辣么火,虽然之前有说过不少,但今天,仍有一个憋在我心里已久,不得不说的话题:基于Azure 的物联网平台必不可少,你可能已经在使用,但也许并没有意识到的服务:Azure 事件中心. 啊?事件 ...

  2. URAL 1519 Formula 1 (插头DP,常规)

    题意:给一个n*m的矩阵,格子中是'*'则是障碍格子,不允许进入,其他格子都是必走的格子,所走格子形成一条哈密顿回路,问有多少种走法? 思路: 本来是很基础的题,顿时不知道进入了哪个坑.这篇插头DP的 ...

  3. Gym 100425A Luggage Distribution (组合数学,二分)

    一开始想着球盒模型,数据范围大,递推会GG. 用凑的方法来算方案.往n个小球之间插两个隔板,方案是(n-1)*(n-2)/2,不区分盒子,三个盒子小球数各不相同的方案数被算了6次(做排列), 两个相同 ...

  4. 图像处理框架 Core Image 介绍

    这篇文章会为初学者介绍一下 Core Image,一个 OS X 和 iOS 的图像处理框架. 如果你想跟着本文中的代码学习,你可以在 GitHub 上下载示例工程.示例工程是一个 iOS 应用程序, ...

  5. Linux之Nginx服务 nfs文件存储 负载均衡

    一.搭建Nginx服务 Nginx 是俄罗斯人编写的十分轻量级的HTTP 服务器,Nginx,它的发音为"engine X",是一个高性能的HTTP和反向代理服务器,同时也是一个I ...

  6. python基础面试题整理---从零开始 每天十题(01)

    最近在弄flask的东西,好久没写博客的,感觉少了点什么,感觉被别人落下好多,可能渐渐的养成了写博客的习惯吧.也是自己想学的东西太多了(说白了就是基础太差了,只是know how,不能做到konw w ...

  7. mysql启动提示mysql.host 不存在,启动失败的解决方法

    图示: 日志: 190625 10:48:42 InnoDB: Started; log sequence number 0 130207190625 10:48:42 [ERROR] Fatal e ...

  8. HibernateDaoSupport类的底层中hql操作使用

    spring的ApplicationContex.xml 中配置 sql 查询方法: 加载数据源的两种方式: <!--方式一:使用 c3p0 连接池 加载数据源 --> <bean ...

  9. NSLocale

      1.创建本地化对象 // 根据本地标识符创建本地化对象 NSLocale *usLocale = [[NSLocale alloc] initWithLocaleIdentifier"e ...

  10. 初涉树形dp

    算是一个……复习以及进阶? 什么是树形dp 树形dp是一种奇妙的dp…… 它的一个重要拓展是和各种树形的数据结构结合,比如说在trie上.自动机上的dp. 而且有些时候还可以拓展到环加外向树.仙人掌上 ...