网络流学习笔记
咕咕咕
此博客参考学长的课件,代码是直接粘过来的,所以不要觉得奇怪。
网络瘤流
前言:关于网络流有个生动的比喻,想象一个自来水厂向各处供水,自来水厂有无限多的水,但每条管子单位时间内允许的最大流量有限,现在钦定一个出水口为汇点,现在要做的就是在满足每一条管子不爆的情况下,最大化汇点流出的水量。
一、几个定义
1. 网络
对于有向图\(G=(V,E)\),其中每条边\((u,v) \in E\)都有权值\(w_{ij}\),称之为容量,图中有两个特殊的点\(s,t(s\ne t)\) ,称\(s\)为源点, \(t\)为汇点,这个图称为网络。
2. 流
对于任意的\((u,v)\in E\),称\(f(u,v)\)为\((u,v)\)边的流量,\(f(u,v)\)恒满足:
- \(f(u,v)\le w(u,v)\)即一条边的流量不能超过其容量。
- \(f(u,v)=-f(v,u)\)即一条边的流量与其反向边的流量互为相反数。
- \(\forall x\in E- \left \{s,t\right\},\sum_{(u,v)\in E}f(u,x)=\sum_{(x,v)\in E}f(x,v)\)即流入一个点的流量等于流出这个点的流量。
3. 残量网络
对于所有的\(w(u,v)-f(u,v)>0\)的边组成的网络,称其为残量网络,残量网络中的边可能不属于E,具体原因等下解释。
4. 增广路
在原图\(G\)或其某一个残量网络中,一条每条边的剩余容量都大于\(0\)的从\(s\)到\(t\)的路径,称为一条增广路。
二、最大流
这就是前言中所提到的那个问题了。
一个比较容易想到的思路是,不断地在残量网络中找寻增广路,直到没有增广路,此时
的总流量即为最大流,但这个做法有点问题,例如下面这张图:
我们假设第一次增广,找到了\(1->2->3->4\)这条边,于是残量网络变成了这样:
这里做了个近似,我们直接把边的流量改为其残余容量。
此时已经无法继续增广了,算法结束,但不难发现,其实走\(1->3->4\)和\(1->2->4\)总流量为\(2\),这更优。
那怎么办?
我们考虑给程序一个反悔的机会,也就是说,建立一种方法,使得已经流过了某条边的流量再流回去,也就是建立反向边,为了保持总容量不变,反向边初始容量为\(0\)。
那么这时如果再走\(1->2->3->4\),残量网络变成了这样:
依然是为了保持总容量不变,在扣除正向边容量的同时,要给反向边加上相等的容量。
这时还可以继续增广:走\(1->3->2->4\),惊奇的发现,\(2\)给\(3\)的流量又让\(3\)给退回去了!而此时相当于选择了两条路径:\(1->3->4\)和\(1->2->4\),总流量为\(2\),得到了正确的结果。
FF算法
最暴力的最大流算法,每次直接dfs找增广路,找不到了就完成。
#include<bits/stdc++.h>
#define ll long long
//#define int long long
#define lc(k) k<<1
#define rc(k) k<<1|1
using namespace std;
const int MAX=1e5+10;
const int MOD=1e9+7;
inline char readchar()
{
static char buf[100000], *p1 = buf, *p2 = buf;
return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000,
stdin), p1 == p2) ? EOF : *p1++;
}
inline int read()
{
#define readchar getchar
int res = 0, f = 0;
char ch = readchar();
for(; !isdigit(ch); ch = readchar()) if(ch == '-') f = 1;
for(; isdigit(ch); ch = readchar()) res = (res << 1) + (res
<< 3) + (ch ^ '0');
return f ? -res : res;
}
inline void write(int x)
{
if(x<0)
{
putchar('-');
x=-x;
}
if(x>9) write(x/10);
putchar(x%10+'0');
}
int n,m,be,en;
struct node
{
int v,w,inv;
};//由于是vector存图,所以需要整一个变量专门记录反向边
vector<node> s[MAX];
int vis[MAX];
int dfs(int k=be,int flow=1e9)
{
if(k==en) return flow;
vis[k]=1;
for(node &v:s[k])
{
int c;
if(v.w>0&&!vis[v.v]&&((c=dfs(v.v,min(v.w,flow)))!=-1))
{
v.w-=c;//本边剩余流量-c
s[v.v][v.inv].w+=c;//反边流量+c
return c;//找到增广路了
}
}
return -1;//找不到增广路了,算法结束
}
int FF()
{
int ans=0,c;
while((c=dfs())!=-1)
{
memset(vis,0,sizeof vis);
ans+=c;
}
return ans;
}
signed main()
{
n=read(),m=read(),be=read(),en=read();
for(int i=1; i<=m; i++)
{
int u=read(),v=read(),w=read();
s[u].push_back((node)
{
v,w,(int)s[v].size()
});//两边互为反向边
s[v].push_back((node)
{
u,0,(int)s[u].size()-1
});
}
cout<<FF();
return 0;
}
这个算法就是慢,板子题都过不去。
考虑这个算法为啥这么慢,主要原因还是dfs好绕远路,每次找到的不是最短的增广路,所以复杂度没有保障。
你dfs T 飞了你会想啥?
正常人应该都会想到bfs,于是就有了EK算法。
EK算法
如上所述,EK就是bfs版的FF算法。
但是由于没有了系统栈的加持,我们只能另开一个数组来存路径,具体看代码:
由于vector写EK很麻烦,于是我用了前向星。
当然这份代码也是学长的。
#include<bits/stdc++.h>
#define ll long long
#define int long long
#define lc(k) k<<1
#define rc(k) k<<1|1
using namespace std;
const int MAX=1.2e5+10;
const int MOD=1e9+7;
inline char readchar()
{
static char buf[100000], *p1 = buf, *p2 = buf;
return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000,
stdin), p1 == p2) ? EOF : *p1++;
}
inline int read()
{
#define readchar getchar
int res = 0, f = 0;
char ch = readchar();
for(; !isdigit(ch); ch = readchar()) if(ch == '-') f = 1;
for(; isdigit(ch); ch = readchar()) res = (res << 1) + (res
<< 3) + (ch ^ '0');
return f ? -res : res;
}
inline void write(int x)
{
if(x<0)
{
putchar('-');
x=-x;
}
if(x>9) write(x/10);
putchar(x%10+'0');
}
int n,m,be,en,cnt=1;
int vis[MAX],let[MAX],flow[MAX];
int head[MAX];
struct node
{
int net,to,w;
} edge[MAX<<1];
void add(int u,int v,int w)
{
edge[++cnt]=(node)
{
head[u],v,w
};
head[u]=cnt;
return ;
}
int bfs()
{
memset(let,0,sizeof let);
queue<int> q;
q.push(be);
flow[be]=1e9;
while(!q.empty())
{
int ff=q.front();
q.pop();
if(ff==en) break;
for(int i=head[ff]; i; i=edge[i].net)
{
int v=edge[i].to,w=edge[i].w;
if(w>0&&!let[v])
{
let[v]=i;
flow[v]=min(flow[ff],w);
q.push(v);
}
}
}
return let[en];
}
int EK()
{
int mx=0;
while(bfs())
{
mx+=flow[en];
for(int i=en; i!=be; i=edge[let[i]^1].to)
{
edge[let[i]].w-=flow[en];
edge[let[i]^1].w+=flow[en];
}
}
return mx;
}
signed main()
{
n=read(),m=read(),be=read(),en=read();
for(int i=1; i<=m; i++)
{
int u=read(),v=read(),w=read();
add(u,v,w);
add(v,u,0);
}
cout<<EK();
return 0;
}
但是本着精益求精防毒瘤出题人的精神,这个算法还得继续优化。
Dinic算法
然而,最常用的网络流算法是Dinic算法。作为FF/EK算法的优化,它选择了先用BFS分层,再用DFS寻找。它的时间复杂度上界是\(O(v^{2}e)\) 。所谓分层,其实就是预处理出源点到每个点的距离(注意每次循环都要预处理一次,因为有些边可能容量变为\(0\)不能再走)。我们只往层数高的方向增广,可以保证不走回头路也不绕圈子。
我们可以使用多路增广节省很多花在重复路线上的时间:在某点DFS找到一条增广路后,如果还剩下多余的流量未用,继续在该点DFS尝试找到更多增广路。
此外还有当前弧优化。因为在Dinic算法中,一条边增广一次后就不会再次增广了,所以下次增广时不需要再考虑这条边。我们把head数组复制一份,但不断更新增广的起点。
这份代码是我自己写的。
模板题
#include<bits/stdc++.h>
#define int long long
#define NN 200010
#define N 10010
using namespace std;
int n,m,s,t,ans=0,cnt=1,q[NN],l,r;//cnt存放边的数量,ans存放答案,q用于模拟队列
int head[N],nxt[NN],to[NN],val[NN],vis[N];//vis标记此点的深度,存边
inline void add(int u,int v,int w)//建边
{
to[++cnt]=v;//终点
val[cnt]=w;//当前边的最大流量
nxt[cnt]=head[u];//赋值头节点
head[u]=cnt;//更新下一个的头节点
}
int bfs()//bfs
{
memset(vis,0,sizeof(vis));//清空vis数组
q[l=r=1]=s;//赋初值
vis[s]=1;//标记不能走了
while(l<=r)//只要没到汇点就一直循环找
{
int u=q[l++];//取出队头元素
for(int p=head[u];p;p=nxt[p])//循环每一条与此点相连的点
{
int v=to[p];//取出终点
if(val[p]&&!vis[v])//如果当前点没有走过并且当此边是有剩余的流量的时候
{
vis[v]=vis[u]+1;//计算当前点的深度
q[++r]=v;//入列
}
}
}
return vis[t];//返回汇点的深度
}
int dfs(int u,int in)//u是起点,in是从上一条边流进的流量
{
if(u==t)//如果当前的u到达了汇点
return in;//就直接返回当前点的进来的流量
int out=0;//out表示从当前边流出的流量
for(int p=head[u];p&∈p=nxt[p])//一个一个遍历能够到达的点并且保证流入的流量不为0
{
int v=to[p];//取出终点
if(val[p]&&vis[v]==vis[u]+1)//如果当前点的最大流量不为0并且深度是起点加一(保证向汇点进行深搜)
{
int res=dfs(v,min(val[p],in));//res是当前点的内流向下一条边的最大流量
val[p]-=res;//正向边减去
val[p^1]+=res;//反向边加上
in-=res;//减去
out+=res;//加上
}
}
if(out==0)//如果流出的为0
vis[u]=0;//标记不能到达汇点下一次不搜了
return out;//返回到达汇点的流量
}
signed main()
{
cin>>n>>m>>s>>t;
for(int i=1;i<=m;i++)
{
int u,v,w;
cin>>u>>v>>w;
add(u,v,w);//存正向边
add(v,u,0);//存反向边
}
while(bfs())//只要有能到达汇点的路线
ans+=dfs(s,1e18);//累加答案
cout<<ans<<endl;//输出
return 0;//好习惯
}
三、最小割
给一些定义:
- 割:对于网络\(G\),其割代表一种点的划分方式,这种划分方式需要满足将\(G\)恰好分为两部分\(S,T\)且\(s\in S,t\in T\)。
- 割的容量:表示所有的从\(S\)到\(T\)的边的容量之和,即:\(w(S,T)=\sum_{u\in S,v\in T}w(u,v)\)。
- 最小割:容量最小的割即为最小割。
如何求最小割?
这里有一条定理,极其简洁的解决了这个问题:
最大流\(=\)最小割。
我们来试着证明下:
可以把最小割认为是将一些边割断,使得整个图分为\(S,T\)两部分,那么容易得到图中所有的流量必定流经这些边中的某一条(否则无法从\(s\)到达\(t\)),所以这些边的总流量\(=\)图的总流量。
而边的流量\(\le\)边的容量,
所以这些边的总流量\(\le\)这些边的总容量,
所以图的总流量\(\le\)这些边的总容量 ,
所以流\(\le\)割,
所以最大流\(=\)最小割。
那么求最小割实际上就是求最大流,这里不在赘述。
四、费用流
我们把前言里改一下,现在自来水厂想赚钱,于是每一单位的水流经某一条管时需要收取一定费用\(c(u,v)\) ,于是为了惠民,自来水厂想找到一种方法,使得流最大的同时费用最小,这就是最小费用最大流。
回想一下前面的EK算法,我们找增广路时是随机找的,现在我不随机找了,我给每个点一个花费,我想要每次都在残量网络中找到花费最小的,咋办?
最短路。
有负权咋办?
SPFA他活了。
因为有模板题所以这道题代码也是我自己写的。
#include<bits/stdc++.h>
#define INF 0x7fffffff
using namespace std;
queue<int> q;
int head[5001],cost[100001],net[100001],to[100001],val[100001];//cost为费用数组,val为容量
int cnt=1,n,m,xb[5001];//记录下标,便于修改容量
int flow[5001],pre[5001];//前驱节点
int mflow=0,mcost=0;//最大流最小费用
int dis[5001],f[5001];//记录从源点到当前节点的最小的费用值,标记是否在队列中
void add(int x,int y,int c,int z)//建边
{
to[++cnt]=y;//存终点
cost[cnt]=z;//存花费
val[cnt]=c;//存边的最大流量
net[cnt]=head[x];
head[x]=cnt;
}
int BFS(int s,int t)//bfs
{
memset(dis,127,sizeof(dis));//重置dis数组
memset(f,0,sizeof(f));//清空f数组
int inf=dis[0];//给inf赋初值
while(!q.empty())//把栈清空
q.pop();
for(int i=1; i<=n; i++)//把前驱都赋成-1
pre[i]=-1;
f[s]=1;//标记起点入列
dis[s]=0;//原点到当前节点的最小费用为0
pre[s]=0;//没有前驱
flow[s]=INF;//起点的
q.push(s);//起点入列
while(!q.empty())//只要栈不空
{
int u=q.front();//取出队头元素
q.pop();//弹出
f[u]=0;//标记出列
for(int i=head[u]; i; i=net[i])
{
int v=to[i];//取出终点
if(val[i]>0&&dis[v]>dis[u]+cost[i])//如果当前边的最大流量不为0并且加上当前点的花费比原来要小
{
dis[v]=dis[u]+cost[i];//更新花费
pre[v]=u;//更新前驱
xb[v]=i;//存下标
flow[v]=min(flow[u],val[i]);//更新最大流
if(!f[v]) f[v]=1,q.push(v);//没入列就入列
}
}
}
if(dis[t]>=inf) return 0;//如果比起点大就返回0
return 1;//否则返回1
}
void max_flow(int s,int t)//算最大值
{
while(BFS(s,t))//只要还能到汇点
{
int k=t;//存终点
while(k!=s)
{
val[xb[k]]-=flow[t];//减去
val[xb[k]^1]+=flow[t];//加上
k=pre[k];//更新k值
}
mflow+=flow[t];//最大流量
mcost+=flow[t]*dis[t];//最小花费
}
}
int main()
{
int s,t;
cin>>n>>m>>s>>t;
for(int i=1; i<=m; i++)
{
int x,y,c,d;
cin>>x>>y>>c>>d;
add(x,y,c,d);//建边
add(y,x,0,-d);
}
max_flow(s,t);
cout<<mflow<<" "<<mcost<<endl;//输出
return 0;
}
当然dijkstra也存在一种方法来处理负权图,但这超出了我们的讨论范围以及我和我的学长的认知水平。
如果你觉得你行了
代码及注释因为我太懒了就不写解析了咕咕咕:
#include<bits/stdc++.h>
#define inf 1<<30
using namespace std;
int n,p,q,s,t,vi,m_in;
struct sb{int v,val,next;} e[101101];//存放已经建好的边
int head[1010],cnt=1,vis[1010],dep[1010];//head是头节点,cnt是边的数量,vis标记此点是否入列,dep表示深度
inline void add(int u,int v,int val)//建边函数
{
e[++cnt].v=v;//存终点
e[cnt].val=val;//存每一条边的最大流量
e[cnt].next=head[u];//头节点
head[u]=cnt;//更新
}
int bfs()//bfs
{
memset(vis,0,sizeof(vis));//清空vis数组
memset(dep,0x3f,sizeof(dep));//重置dep数组
queue<int>q;//定义队列用于bfs
q.push(s);//放入队列
dep[s]=1;//标记出列
while(!q.empty())//只要队列不空
{
int u=q.front();//取出队头元素
q.pop();//弹出队头元素
vis[u]=0;//标记出列
for(int i=head[u];i;i=e[i].next)//枚举每一条与u相连的边
{
int v=e[i].v;//取出终点
if(e[i].val&&dep[v]>dep[u]+1)//如果当前边最大流量不为0并且深度比从起点到终点大
{
dep[v]=dep[u]+1;//替换
if(vis[v]==0)//如果不在队列里面
{
q.push(v);//放入队列
vis[v]=1;//标记入列
}
}
}
}
return dep[t]!=0x3f3f3f3f;//如果能到达汇点返回1,反之返回0
}
int dfs(int u,int in)//dfs函数
{
if(u==t)//到达汇点了
{
vi=1;//标记找到了
return in;//返回当前路线的流量
}
int out=0;//流出的流量大小
for(int i=head[u];i;i=e[i].next)//枚举每一条与u相连的边
{
int v=e[i].v;//取出终点
if(e[i].val&&dep[v]==dep[u]+1)//如果当前点最大流量不为0并且是向汇点流去
{
int res=dfs(v,min(e[i].val,in));//递归找此边的流量大小
e[i].val-=res;//正向边加上
e[i^1].val+=res;//反向边减去
out+=res;//累加流出的流量大小
}
if(out==in)break;//如果当前点流出和流入的量相等就直接退出
}
if(out==0)//如果当前的流出的流量等于0
vis[u]=0; //标记下一次不搜了
return out;//返回流量大小
}
int main()
{
cin>>n>>p>>q;
int f;
s=1001,t=1002;//原点,汇点
for(int i=1;i<=n;i++)add(i,i+n,1),add(i+n,i,0);//i表示顾客入点,i+n表示顾客出点,自己与自己建边
for(int i=1;i<=p;i++)add(s,200+i,1),add(200+i,s,0); //200+i表示房间,与原点相连建边
for(int i=1;i<=q;i++)add(300+i,t,1),add(t,300+i,0); //300+i表示菜,与汇点相连建边
for(int i=1;i<=n;i++)//枚举每一个顾客
for(int j=1;j<=p;j++)//枚举每一个房间
{
cin>>f;//输入
if(f==1)//喜欢此房间
add(200+j,i,1),add(i,200+j,0);//当前顾客与房间建边
}
for(int i=1;i<=n;i++)//枚举每一个顾客
for(int j=1;j<=q;j++)//枚举每一道菜
{
cin>>f;
if(f==1)//喜欢这道菜
add(i+n,300+j,1),add(300+j,i+n,0);//之前与自己建的边与菜建边
}
while(bfs())//只要还能到汇点
{
vi=1;
while(vi)vi=0,m_in+=dfs(s,inf);//累加答案
}
cout<<m_in<<endl;//输出
return 0;//好习惯
}
本文来自博客园,作者:北烛青澜,转载请注明原文链接:https://www.cnblogs.com/Multitree/p/16756262.html
The heart is higher than the sky, and life is thinner than paper.