最大流学习笔记
Part1:最大流入门
概念
源点:所有流量都从源点出来;
汇点:所有流量最终都流入汇点;
最大流:对于一张图,从源点流向汇点的最大流量
增广路:找到一条路径使得总流量增加
残余网络:每搜一次相应建立“反悔机制”,即将路径上用掉的边建一条反向边,流量和原边相同,建完后的图称为残余网络
结论
- 定义w(x,y)为从x到y的流量限制,flow(x,y)表示实际从x到y的流量
- flow(x,y)<=w(x,y)
- flow(x,y)=-flow(y,x)
- ∑ i f l o w ( x , i ) = ∑ j f l o w ( j , x ) \sum^{}_{i}{flow(x,i)} = \sum^{}_{j}{flow(j,x)} ∑iflow(x,i)=∑jflow(j,x)
Ford-Fulkerson算法
key:dfs找增广路,建立残余网络,不停地在残余网络上找增广路,直到找不到任何一条增广路为止
时间复杂度: O ( C × n 2 2 ) O(C\times \frac{n^2}{2}) O(C×2n2)
C:最大规模容量,N:点数
注:Ford-Fulkerson算法并非可实现算法
Edmond-Karp Algorithm
Key: dfs–>bfs找一条最短路径
在Ford-Fulkerson算法的基础上,将dfs找增广路径的策略替换成用bfs找一条s到t的最短路径
复杂度: O ( N M 2 ) O(NM^2) O(NM2)
代码:
#include<bits/stdc++.h>
using namespace std;
const int maxn=1000005;
const int inf=0x3f3f3f3f;
int n,m,s,t,tot=1,maxflow;
int head[maxn],pre[maxn],increase[maxn],vis[maxn];
struct node
{
int from,to,next;
int val;
}edge[maxn];
inline void add(int x,int y,int z)
{
edge[++tot].next=head[x];
edge[tot].from=x;
edge[tot].to=y;
edge[tot].val=z;
head[x]=tot;
}
inline int read()
{
int s=0,f=1;
char c=getchar();
while (c<'0'||c>'9')
{
if (c=='-')
{
f=-1;
}
c=getchar();
}
while (c>='0'&&c<='9')
{
s=s*10+c-48;
c=getchar();
}
return s*f;
}
bool bfs()//找到一条从s到达t的增广路,且增加的流量为increase[t]
{
memset(vis,0,sizeof(vis));
queue<int> q;
q.push(s);
vis[s]=true;
increase[s]=inf; //最小剩余流量,源点的流量是无限的
while (!q.empty())
{
int x=q.front();
q.pop();
for (int i=head[x];i;i=edge[i].next)
{
if (edge[i].val)
{
int y=edge[i].to;
if (vis[y]==true)
{
continue;
}
increase[y]=min(increase[x],edge[i].val);//从源点到y的最大流量限制
pre[y]=i;
q.push(y);
vis[y]=true;
if (y==t)
{
return true;
}
}
}
}
return false;
}
void update()
{
int cur=t; //反过来走到s
while (cur!=s)
{
int last=pre[cur]; //最后一条边的编号
edge[last].val-=increase[t];
edge[last^1].val+=increase[t]; //反边
cur=edge[last^1].to;//cur的上一个点
}
maxflow+=increase[t];
return ;
}
int main()
{
n=read(),m=read(),s=read(),t=read();
memset(head,0,sizeof(head));//tot=1,第一条边从2开始,对应反边需要异或
for (int i=1;i<=m;i++)
{
int x=read(),y=read(),z=read();
add(x,y,z);
add(y,x,0);
}
while (bfs()==true)
{
update();
}
cout<<maxflow<<endl;
return 0;
}
Dinic Algorithm
EK算法缺陷:
- 盲目
- 每次bfs找一条增广路径
那么我们就想要每次bfs或者dfs时找出多条增广路
我们通过建立分层图来增加效率
每次只走向下一层
这样我们走的一定是最短路
时间复杂度: O ( N 2 M ) O(N^2M) O(N2M) //绝大多数情况Dinic比EK优秀(若点数和边数接近则复杂度差不多)
代码:
#include<bits/stdc++.h>
using namespace std;
const int maxn=1000005;
const int inf=0x3f3f3f3f;
int n,m,s,t,tot=1,maxflow;
int head[maxn],pre[maxn],increase[maxn],vis[maxn],level[maxn];
struct node
{
int from,to,next;
int val;
}edge[maxn];
inline void add(int x,int y,int z)
{
edge[++tot].next=head[x];
edge[tot].from=x;
edge[tot].to=y;
edge[tot].val=z;
head[x]=tot;
}
inline int read()
{
int s=0,f=1;
char c=getchar();
while (c<'0'||c>'9')
{
if (c=='-')
{
f=-1;
}
c=getchar();
}
while (c>='0'&&c<='9')
{
s=s*10+c-48;
c=getchar();
}
return s*f;
}
bool bfs() //在残余网络生成分层图
{
memset(level,0,sizeof(level));
queue<int> q;
q.push(s);
level[s]=1;
while (!q.empty())
{
int x=q.front();
q.pop();
for (int i=head[x];i;i=edge[i].next)
{
if (edge[i].val&&!level[edge[i].to])
{
q.push(edge[i].to);
level[edge[i].to]=level[x]+1;
if (edge[i].to==t)
{
return 1;
}
}
}
}
return 0;
}
int dfs(int x,int flow) //x:当前节点,flow:当前容量限制
{
if (x==t)
{
return flow; //本次增广路找到的流量
}
int rest=flow,increase; //rest表示当前剩余的容量
for (int i=head[x];i&&rest;i=edge[i].next)
{
if (edge[i].val&&level[edge[i].to]==level[x]+1) //决定去不去邻居,邻居必须在下一层
{
increase=dfs(edge[i].to,min(rest,edge[i].val)); //以邻居为起点找到的流量
if (increase==0)
{
level[edge[i].to]=0; //increase=0,这条路没有走过去的价值,剪枝
}
edge[i].val-=increase;
edge[i^1].val+=increase;
rest-=increase;
}
}
return flow-rest; //增广的流量=容量上限-剩余的流量
}
int main()
{
n=read(),m=read(),s=read(),t=read();
memset(head,0,sizeof(head));//tot=1,第一条边从2开始,对应反边需要异或
for (int i=1;i<=m;i++)
{
int x=read(),y=read(),z=read();
add(x,y,z);
add(y,x,0);
}
int flow=0; //当前增广增加的流量
while (bfs()!=0) //对每一次残余网络找一次分层图
{
flow=dfs(s,inf);
while (flow!=0)
{
maxflow+=flow;
flow=dfs(s,inf);
}
}
cout<<maxflow<<endl;
return 0;
}
Part2:最大流进阶–建模
隐式图建图方法: 选择的次数限制---->边的容量 技巧:拆点等
1. [LGOJ]P2766 最长不下降子序列问题:
- 对于每个Xi,拆成两个点Xi,Xi次,建立一条有向边,权值为1;
- 对于满足dp[Xi]+1=dp[Xj]的点,建立Xi次指向Xj的有向边,权值为1;
- 将超级源点S和每一个dp[Xi]=1的点建立一条边,权值为1,每一个dp[Xi]=s的点建立一条Xi次到超级汇点T的有向边,权值为1
- 对于第四问,将S连向Xi的边修改为INF,将Xi连向Xi次的边修改为INF,即可选多次
- 注意特判1的情况,因为这个卡了好久的91分
#include<bits/stdc++.h>
using namespace std;
const int maxn=10005;
const int INF=0x3f3f3f3f;
int n,s,t,maxx,tot,ans;
int a[maxn],f[maxn],dep[maxn],head[maxn];
queue<int> q;
struct node
{
int from,to,next;
int val;
}edge[maxn];
inline int read()
{
int s=0,f=1;
char c=getchar();
while (c<'0'||c>'9')
{
if (c=='-')
{
f=-1;
}
c=getchar();
}
while (c>='0'&&c<='9')
{
s=s*10+c-48;
c=getchar();
}
return s*f;
}
void add(int x,int y,int z)
{
edge[++tot].next=head[x];
edge[tot].from=x;
edge[tot].to=y;
edge[tot].val=z;
head[x]=tot;
}
bool bfs()
{
memset(dep,-1,sizeof(dep));
dep[s]=1;
q.push(s);
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=head[u];i;i=edge[i].next)
{
int to=edge[i].to,w=edge[i].val;
if(dep[to]==-1&&w>0)
{
dep[to]=dep[u]+1;
q.push(to);
}
}
}
if (dep[t]==-1)
{
return 0;
}
return 1;
}
int dfs(int u,int low)
{
if(u==t)
{
return low;
}
int ret=low;
for (int i=head[u];i;i=edge[i].next)
{
int to=edge[i].to,w=edge[i].val;
if (dep[to]==dep[u]+1&&w>0)
{
int flow=dfs(to,min(ret,w));
if (flow>0)
{
edge[i].val-=flow;
edge[i^1].val+=flow;
}
ret-=flow;
if (!ret)
{
break;
}
}
}
return low-ret;
}
int dinic()
{
int res=0;
while (bfs())
{
res+=dfs(s,INF);
}
return res;
}
void find_lis()
{
for (int i=1;i<=n;i++)
{
f[i]=1;
}
maxx=1;
for (int i=2;i<=n;i++)
{
for(int j=1;j<i;j++)
{
if (a[j]<=a[i]&&f[i]<f[j]+1)
{
f[i]=max(f[i],f[j]+1);
maxx=max(f[i],maxx);
}
}
}
cout<<maxx<<endl;
}
inline void build()
{
for (int i=1;i<=n;i++)
{
add(i,i+n,1);
add(i+n,i,0);
if (f[i]==1)
{
add(s,i,1);
add(i,s,0);
}
if (f[i]==maxx)
{
add(i+n,t,1);
add(t,i+n,0);
}
for (int j=1;j<i;j++)
{
if (a[i]>=a[j]&&f[j]==f[i]-1)
{
add(j+n,i,1);
add(i,j+n,0);
}
}
}
}
inline void rebuild()
{
add(s,1,INF);
add(1,s,0);
add(n,n*2,INF);
add(n*2,n,0);
add(1,1+n,INF);
add(1+n,1,0);
if (f[n]==maxx)
{
add(n*2,t,INF);
add(t,n*2,0);
}
}
int main()
{
n=read();
if (n==1)
{
cout<<"1\n1\n1\n";
return 0;
}
s=0,t=n*2+1;
for (int i=1;i<=n;i++)
{
a[i]=read();
}
find_lis();
build();
int ans=dinic();
cout<<ans<<endl;
rebuild();
cout<<ans+dinic()<<endl;
return 0;
}
2. [LGOJ]P2065 [TJOI2011]卡片
二分图最大匹配问题,首先想到 O(NM) 的匈牙利算法,发现会TLE掉(100组,500边,500点再加上gcd),于是考虑更加优秀最大流(一般实际算法不会到达最高时间复杂度)
我们发现,数字是不超过 10^7 的,于是我们可以将其分解质因数,将数字与其质因数连边,从蓝卡连到质因数,再连到红卡,流量为MAX,再从源点连到蓝卡,红卡连到汇点,流量均为1,建图就完成了。
3. [LGOJ]P2472 [SCOI2007]蜥蜴
- 建一个超级原点,一个超级汇点。每一个石柱建两个点:s1,s2。
- 从超级原点向每一个有蜥蜴的石柱的s1连一条容量为1的边。
- 从每一个s1向他对应的s2连一条容量为那个石柱的高度的边。
- 从每一个石柱的s2向每一个从他那里可以跳到的石柱连一条容量为无限的边。
- 从每一个能跳出地图的石柱向超级汇点连一条容量为无限的边。
#include<bits/stdc++.h>
using namespace std;
const int maxn=100005;
const int maxm=1001;
const int inf=0x3f3f3f3f;
struct node
{
int from,to,next;
int val;
} edge[maxn];
int r,c,d,S,T,num,tot=1;
int head[maxn],cur[maxn],dis[maxn],q[maxn],mp[maxm][maxm];
char s[maxm];
inline int read()
{
int s=0,f=1;
char c=getchar();
while (c<'0'||c>'9')
{
if (c=='-')
{
f=-1;
}
c=getchar();
}
while (c>='0'&&c<='9')
{
s=s*10+c-48;
c=getchar();
}
return s*f;
}
void add(int x,int y,int z)
{
edge[++tot].next=head[x];
edge[tot].to=y;
edge[tot].val=z;
head[x]=tot;
edge[++tot].next=head[y];
edge[tot].to=x;
edge[tot].val=0;
head[y]=tot;
}
double dist(int x1,int y1,int x2,int y2)
{
return sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
}
int pos(int x,int y,int z)
{
return (x-1)*c+y+r*c*z;
}
bool bfs()
{
for (int i=S;i<=T;i++)
{
dis[i]=inf;
}
int h=0,t=1;
q[t]=S;
dis[S]=0;
while (h!=t)
{
int u=q[++h];
for (int i=head[u];i;i=edge[i].next)
{
int to=edge[i].to;
if (edge[i].val&&dis[to]>dis[u]+1)
{
dis[to]=dis[u]+1;
if (to==T)
{
return 1;
}
q[++t]=to;
}
}
}
if (dis[T]==inf)
{
return 0;
}
return 1;
}
int dfs(int u,int low)
{
if (u==T)
{
return low;
}
int ret=low;
for (int i=head[u];i;i=edge[i].next)
{
int to=edge[i].to;
if (edge[i].val&&dis[to]==dis[u]+1)
{
int t=dfs(to,min(ret,edge[i].val));
if (!t)
{
dis[to]=0;
}
edge[i].val-=t,edge[i^1].val+=t;
ret-=t;
}
}
return low-ret;
}
int dinic()
{
int res=0;
while (bfs())
{
for (int i=S;i<=T;i++)
{
cur[i]=head[i];
}
res+=dfs(S,inf);
}
return num-res;
}
int main()
{
r=read(),c=read(),d=read();
S=0,T=2*r*c+1;
for (int i=1;i<=r;i++)
{
scanf("%s",s+1);
for (int j=1;j<=c;j++)
{
mp[i][j]=s[j]-'0';
}
}
for (int i=1;i<=r;i++)
{
scanf("%s",s+1);
for (int j=1;j<=c;j++)
{
if (s[j]=='L')
{
num++;
add(S,pos(i,j,0),1);
}
}
}
for (int i=1;i<=r;i++)
{
for (int j=1;j<=c;j++)
{
if (mp[i][j])
{
add(pos(i,j,0),pos(i,j,1),mp[i][j]);
}
}
}
for (int i=1;i<=r;i++)
{
for (int j=1;j<=c;j++)
{
if (!mp[i][j])
{
continue;
}
for (int k=1;k<=r;k++)
{
for (int w=1;w<=c;w++)
{
if (i==k&&j==w)
{
continue;
}
if (mp[k][w]&&dist(i,j,k,w)<=d)
{
add(pos(i,j,1),pos(k,w,0),inf);
add(pos(k,w,1),pos(i,j,0),inf);
}
}
}
}
}
for (int i=1;i<=r;i++)
{
for (int j=1;j<=c;j++)
{
if (i<=d||r-i<d||j<=d||c-j<d)
{
add(pos(i,j,1),T,inf);
}
}
}
cout<<dinic()<<endl;
return 0;
}
4. [LGOJ]P2763 试题库问题
没什么好说的。。。比较简单
#include<bits/stdc++.h>
using namespace std;
const int maxn=10005;
const int inf=0x3f3f3f3f;
int n,k,tot,S,T,m,ans;
int dep[maxn],head[maxn];
queue<int> q;
bool vis[maxn];
struct node
{
int from,to,next;
int val;
}edge[maxn];
void add(int x,int y,int z)
{
edge[++tot].next=head[x];
edge[tot].from=x;
edge[tot].to=y;
edge[tot].val=z;
head[x]=tot;
}
inline int read()
{
int s=0,f=1;
char c=getchar();
while (c<'0'||c>'9')
{
if (c=='-')
{
f=-1;
}
c=getchar();
}
while (c>='0'&&c<='9')
{
s=s*10+c-48;
c=getchar();
}
return s*f;
}
inline bool bfs()
{
memset(dep,-1,sizeof(dep));
dep[S]=1;
q.push(S);
while (!q.empty())
{
int u=q.front();
q.pop();
for(int i=head[u];i;i=edge[i].next)
{
int to=edge[i].to,w=edge[i].val;
if (dep[to]==-1&&w>0)
{
dep[to]=dep[u]+1;
q.push(to);
}
}
}
if (dep[T]==-1)
{
return 0;
}
return 1;
}
int dfs(int u,int low)
{
if (u==T)
{
return low;
}
int ret=low;
for (int i=head[u];i;i=edge[i].next)
{
int to=edge[i].to,w=edge[i].val;
if (dep[to]==dep[u]+1&&w>0)
{
int flow=dfs(to,min(ret,w));
if (flow>0)
{
edge[i].val-=flow;
edge[i^1].val+=flow;
}
ret-=flow;
if (!ret)
{
break;
}
}
}
return low-ret;
}
void dinic()
{
while (bfs())
{
ans+=dfs(S,inf);
}
}
int main()
{
k=read(),n=read();
S=0,T=10000;
for (int i=1;i<=k;i++)
{
int x=read();
add(S,i,x);
add(i,S,0);
m+=x;
}
for (int i=1;i<=n;i++)
{
int p=read();
for (int j=1;j<=p;j++)
{
int num=read();
add(num,i+k,1);
add(i+k,num,0);
}
add(i+k,T,1);
add(T,i+k,0);
}
dinic();
if (ans==m)
{
for (int i=1;i<=k;i++)
{
cout<<i<<": ";
for (int j=head[i];j;j=edge[j].next)
{
if (edge[j].to>0&&!edge[j].val)
{
cout<<edge[j].to-k<<" ";
}
}
cout<<endl;
}
}
else
{
cout<<"No Solution!"<<endl;
}
return 0;
}
5. [LGOJ]P2754 [CTSC1999]家园 / 星际转移问题
我们发现每一天飞船停靠的地方是不一样的,于是我们很自然佛瑞想到我们可以对每个空间站的每个时间点进行拆点(就相当于是一个动态图,每过一个时间点都可以建一个图)
总结来说就是对于这种一个点(表面意义上的一个点,比如说一个位置)对应多种情况的(比如说随着时间的推移有着不同的状态,而且这种状态>2),我们考虑在类似于分层图上面跑网络流。
建图:
首先地球是起始站,源点肯定要向每个时刻的地球连边吧,容量INF。然后月球是终点,所以要向每个时刻的月球连边,容量INF。之后对于在空间站的人们就是两种情况:
- 人们可以选择此时在该空间站的飞船,飞向下一个空间站(/地球/月球)。
- 无法移动,所以留在此处。
显然我们把时刻拆开之后,很轻易就能计算出对于每个飞船,当前时刻的位置和下一时刻的位置,直接在分层图上连边即可,容量为最大载重。因为人有可能无法移动,而且空间站的容量为无限大,所以对于每个空间站,要向下一个时刻连一条边,容量INF。
总结来说:
- 源点向每个时刻的地球连容量为INF的边
- 同理,向每个时刻的月球连容量为INF的边
- t时刻的x空间站向t+1时刻的x空间站连一条边(人们可以留在该空间站)
- 若x空间站可以到达y空间站,则从t时刻的x空间站在向t+1时刻的y空间站连一条流量为飞船流量限制的边
最后,当当前的最大流已经超过总人数时,则此时所有人都已经被运到月球,此时输出时间就好了
注意:
- 首先需要判断是否会有答案,即地球到太阳是否连通,在这里用并查集就可以了
- 注意n要+=2,地球和月球也算空间站
#include<bits/stdc++.h>
using namespace std;
const int maxn=100005;
const int maxm=1005;
const int INF=0x3f3f3f3f;
int n,m,k,Time,Tot,sum,tot=1,S,T=10000;
int head[maxn],dep[maxn],cur[maxn],fa[maxn],num[maxn],cap[maxn],to[maxm][maxm];
struct node
{
int from,to,next;
int val;
}edge[maxn];
inline int read()
{
int s=0,f=1;
char c=getchar();
while (c<'0'||c>'9')
{
if (c=='-')
{
f=-1;
}
c=getchar();
}
while (c>='0'&&c<='9')
{
s=s*10+c-48;
c=getchar();
}
return s*f;
}
inline void add(int x,int y,int z)
{
edge[++tot].next=head[x];
edge[tot].from=x;
edge[tot].to=y;
edge[tot].val=z;
head[x]=tot;
edge[++tot].next=head[y];
edge[tot].from=y;
edge[tot].to=x;
edge[tot].val=0;
head[y]=tot;
}
inline int find(int x)
{
if (fa[x]==x)
{
return x;
}
else
{
return fa[x]=find(fa[x]);
}
}
inline void unionn(int x,int y)
{
int sx=find(x),sy=find(y);
if (sx!=sy)
{
fa[sx]=sy;
}
}
vector<int> v[maxm];
queue<int> q;
inline bool bfs()
{
memset(dep,-1,sizeof(dep));
memcpy(cur,head,sizeof(head));
q.push(S);
dep[S]=0;
while (!q.empty())
{
int u=q.front();
q.pop();
for (int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if (dep[v]==-1&&edge[i].val)
{
dep[v]=dep[u]+1;
q.push(v);
}
}
}
if (dep[T]==-1)
{
return false;
}
return true;
}
inline int dfs(int u,int low)
{
if (u==T)
{
return low;
}
int ret=low;
for (int i=head[u];i;i=edge[i].next)
{
int to=edge[i].to,w=edge[i].val;
if (dep[to]==dep[u]+1&&w>0)
{
int flow=dfs(to,min(ret,w));
if (flow>0)
{
edge[i].val-=flow;
edge[i^1].val+=flow;
}
ret-=flow;
if (!ret)
{
break;
}
}
}
return low-ret;
}
inline int dinic()
{
int ret=0;
while (bfs())
{
ret+=dfs(S,INF);
}
return ret;
}
int main()
{
n=read(),m=read(),k=read();
for (int i=1;i<=n+2;i++)
{
fa[i]=i;
}
for (int i=1;i<=m;i++)
{
cap[i]=read(),num[i]=read();
for (int j=1;j<=num[i];j++)
{
int p=read();
if (p==-1)
{
p=n+2;
}
else if (p==0)
{
p=n+1;
}
v[i].push_back(p);
if (j!=1)
{
unionn(p,v[i][v[i].size()-2]);
}
}
}
if (find(n+1)!=find(n+2))
{
cout<<"0"<<endl;
return 0;
}
n+=2;
add(S,n-1,INF);
add(n,T,INF);
while (1)
{
Time++;
for (int i=1;i<=m;i++)
{
int now=v[i][Time%num[i]],pre=v[i][(Time-1+num[i])%num[i]];
add(n*(Time-1)+pre,n*Time+now,cap[i]);
}
add(S,n*Time+n-1,INF);
add(Time*n+n,T,INF);
for (int j=1;j<=n;j++)
{
add((Time-1)*n+j,Time*n+j,INF);
}
sum+=dinic();
if (sum>=k)
{
cout<<Time<<endl;
break;
}
}
return 0;
}