网络流学习笔记
1. 概述
网络指的是一类特殊的有向图G=(V,E),与一般有向图不同的是有容量和源汇点
对于网络G=(V,E),流是一个从边集E到整数集或实数集的函数,满足如下性质
-
容量限制:对于每条边,该边流经的流量不得超过该边的容量
-
流守恒性:除源汇点外,其余任何点的净流量为0,其中,我们定义节点u的净流量为:
记源点为s,汇点为t,则由流守恒性,
2. 最大流
2.1. 定义
对于网络G=(V,E),给每条边指定流量,得到合适的流f,使f流量尽可能大,此时我们称f为G的最大流,说人话,就是求s到t的最大流量
2.2. Ford–Fulkerson 增广
Ford–Fulkerson 增广是计算最大流算法的一类统称,该方法运用贪心的思想,通过寻找增广路来更新并求解最大流
给定网络G及G上的流f,我们做如下定义
对于边 (u, v),我们将其容量与流量之差称为剩余容量
我们将 G 中所有结点和剩余容量大于 0 的边构成的子图称为残量网络
我们将
对于一条增广路,我们给每一条边
由此,最大流的求解可以被视为若干次增广分别得到的流的叠加
此外,在Ford–Fulkerson增广的过程中,对于每条边(u,v),我们都新建一条反向边(v,u)。我们约定f(u,v)=-f(v,u),这一性质可以通过在每次增广时引入退流操作来保证,即f(u, v)增加时f(v, u)应当减少同等的量。
初次接触这一方法的可能察觉到一个违反直觉的情形——反向边的流量f(v,u)可能是一个负值。实际上我们可以注意到,在Ford–Fulkerson增广的过程中,真正有意义的是剩余容量
所以退流操作带来的「抵消」效果使得我们无需担心我们按照「错误」的顺序选择了增广路。
容易发现,只要
2.3. Edmonds–Karp 算法
如何在
-
如果从s出发可以BFS到t,则找到了新的增广路
-
对于新的增广路p,我们计算出p经过的边的剩余容量最小值
,我们给p的每条边都加上 的流量,并给它的反边都退掉 的流量,令最大流增加 -
接下来,重复此过程,直到不存在满足条件的增广路
2.4. Dinic算法
2.4.1. 思想
Dinic算法又称Dinic阻塞流算法,算法的关键在于阻塞流
Dinic 算法本质上采用了贪心的思想,对于所有增广路,其中一定有一条最短的
可以证明,只要我们一直选择增广路,直到选不下去为止,不管按照什么顺序来选,都能得到正确的结果
因此,我们不妨每次都找到最短的增广路,将这条路径的贡献加入到答案中,直到无法找到下一条增广路。
考虑在增广前先对
此时,我们称
如果我们在层次图
具体流程如下:
-
在
上BFS出分层图 -
在
上DFS出阻塞流 -
将
并到原来的流 中,即 -
重复此过程,直到不存在s到t的路径
2.2.2. 当前弧优化
注意到,在
为避免这一缺陷,如果某一时刻(u,v)已经增广到极限了,即边(u,v)已无容量或v后方已增广至阻塞,那么就没有必要再尝试向(u,v)流出
据此,对于每个节点u,我们维护u的出边表中第一个还有必要的出边,习惯上,我们称这个指针为当前弧,所以该优化也称当前弧优化
2.4.3. 代码
int dfs(int u,int flow)
{
if(u==t||flow==0) return flow;
int res=flow;
for(int i=cur[u];i&&res;i=e[i].nxt)
{
cur[u]=i;
int v=e[i].to,w=e[i].val;
if(w>0&&dep[v]==dep[u]+1)
{
int k=dfs(v,min(res,w));
res-=k;
e[i].val-=k;
e[i^1].val+=k;
}
}
if(res==flow) dep[u]=0;
return flow-res;
}
2.5. 例题
2.5.1. [SCOI2007]蜥蜴
https://gxyzoj.com/d/gxyznoi/p/P69
2.5.1.1. 思路
首先建立源点和汇点,将所有能直接到达外面的点和汇点连边,将所有有蜥蜴的点和源点连边,两者边权均为inf
然后,考虑高度限制,考虑拆点,将一个柱子拆成两个点,可以看做顶和底,两点间的边权就是高度
最后将彼此可达的柱子连边,边权为inf,求最大流即可
2.5.1.2. 代码
#include<cstdio>
#include<iostream>
#include<string>
#include<cstring>
#include<queue>
#include<algorithm>
using namespace std;
const int inf=1e9;
int n,m,d,h[25][25],tx[1005],ty[1005],tot,a[25][25];
int edgenum=1,head[1005],ans;
int s,t;
struct edge{
int to,nxt,val;
}e[400005];
void add_edge(int u,int v,int w)
{
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
e[edgenum].val=w;
head[u]=edgenum;
}
int dep[1005],cur[1005];
queue<int> q;
bool bfs(int x,int y)
{
memset(dep,0x7f,sizeof(dep));
while(!q.empty()) q.pop();
for(int i=0;i<=2*tot+1;i++) cur[i]=head[i];
dep[x]=0;
q.push(x);
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(dep[v]>inf&&e[i].val)
{
dep[v]=dep[u]+1;
q.push(v);
}
}
}
if(dep[y]<inf) return 1;
return 0;
}
int dfs(int u,int flow)
{
if(u==t||flow==0) return flow;
int res=flow;
for(int i=cur[u];i&&res;i=e[i].nxt)
{
cur[u]=i;
int v=e[i].to,w=e[i].val;
if(w>0&&dep[v]==dep[u]+1)
{
int k=dfs(v,min(res,w));
res-=k;
e[i].val-=k;
e[i^1].val+=k;
}
}
if(res==flow) dep[u]=0;
return flow-res;
}
int main()
{
scanf("%d%d%d",&n,&m,&d);
for(int i=1;i<=n;i++)
{
string s1;
cin>>s1;
for(int j=1;j<=m;j++)
{
h[i][j]=s1[j-1]-'0';
if(h[i][j])
{
tot++;
tx[tot]=i,ty[tot]=j;
a[i][j]=tot;
}
}
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(h[i][j])
{
if(i<=d||i+d>n||j<=d||j+d>m)
{
add_edge(a[i][j]+tot,2*tot+1,inf);
add_edge(2*tot+1,a[i][j]+tot,0);
}
}
}
}
for(int i=1;i<=n;i++)
{
string s1;
cin>>s1;
for(int j=1;j<=m;j++)
{
if(s1[j-1]=='L')
{
int tmp=a[i][j];
ans++;
add_edge(0,tmp,1);
add_edge(tmp,0,0);
}
}
}
for(int i=1;i<=tot;i++)
{
add_edge(i,i+tot,h[tx[i]][ty[i]]);
add_edge(i+tot,i,0);
}
for(int i=1;i<=tot;i++)
{
for(int j=1;j<=tot;j++)
{
if(i==j) continue;
if((tx[i]-tx[j])*(tx[i]-tx[j])+(ty[i]-ty[j])*(ty[i]-ty[j])<=d*d)
{
add_edge(i+tot,j,inf);
add_edge(j,i+tot,0);
}
}
}
s=0,t=2*tot+1;
while(bfs(s,t))
{
// printf("%d\n",ans);
ans-=dfs(s,inf);
}
printf("%d",ans);
return 0;
}
2.5.2. [SDOI2015] 星际战争
https://gxyzoj.com/d/gxyznoi/p/P70
首先,假如a可以攻击b,则a,b之间就要连一条边权为inf的边,因为没有限制a对b的伤害
接下来,考虑如何满足限制条件
可以建立超级1源点和超级汇点,将每一个机器人向汇点连边,边权为a[i],此时,当最大流等于所有a[i]的和时,必然成立
接着考虑时间和伤害值的问题,显然,如果时间为t,则若每时每刻i的攻击均有效,则最终可造成的伤害为
所以,可以从汇点向所有武器连边,边权为
考虑t的问题,若t满足条件,则t+1必然满足,故答案具有单调性
所以,可以二分t,直接判断即可
注意n,m的顺序,以及精度,判等的精度最好不要小于
2.5.3. 士兵占领
https://gxyzoj.com/d/gxyznoi/p/P71
题面要求最小值,但是最大流只能求最大值,正难则反,考虑先将所有能放的点都放上,然后看能取消多少个点
每个点都与其所在行列有关,而每行每列又有限制,不妨建立节点去代表行和列
显然,每个能放士兵的节点可以分别向其所在的行和列连边,边权为1
但是这样话的点和边会很多,因为每个节点只有一条入边和一条出边,边权相等,所以可以直接将每个点所在行和列相连,边权为1
接下来考虑每行放点个数的限制,因为是求可以删去的最大点数,所以可以在每行每列记录可删点
此时,可以建立源点和汇点
源点向每一行连边,边权为该行的可操作点数
汇点向每一列连边,边权为该列的可操作点数
此时,最大流即为可以删去的最大点数,直接用n*m-k去减即可
注意数组的大小!!!
2.5.4. [HNOI2007] 紧急疏散 evacuate
https://gxyzoj.com/d/gxyznoi/p/P72
先说一个假的,二分时间,将每一个空地向源点连边,边权为1,再将其可以到达的点与其连边,边权为inf,最后将门向汇点连边,边权为t
为什么这个思路不对呢?首先,如果a到b的边权为inf,b到c的边权也为inf,则可以将b删去,直接从a向c连边,边权为inf,不会影响结果
所以,如果按上述过程进行连边,则相当于直接将a与其可到达的点连边,这道题就变成了一道二分图的匹配,而每秒只能移动一格的条件也失效了
因此,如何考虑移动的问题,就成为了本题的难点
首先,如果l的时间可以逃出,则l+1必然可以,所以可以二分时间
因为所有人会沿着空地奔跑,所以所有人必然可以在空地的个数的时间内逃出,可以感性理解一下
既然确定了二分的上下界,接下来就要考虑如何判断了,此时既要考虑时间,又要考虑距离,所以可以拆点
将每一个门拆成t个点,表示这t个单位时间,显然这些点要向汇点连一条边权为1的边,表示会有一个人逃出
同时,t也要向t+1连一条边权为inf的边,因为可以站在原地等待,在下一秒出去
此时,只需要将每一个点到每一个门的最短路求出,然后向对应的时间连边即可
注意,如果在该时间的情况下,会有点不存在满足条件的出口,则要直接退出
连边之后,该图的最大流即为答案
2.5.5. [SCOI2012]奇怪的游戏
https://gxyzoj.com/d/gxyznoi/p/P73
题外话:数竞的染色学了跟学了一样,根本不会
切入正题
因为是二维棋盘,而且每次是选相邻的两个格子加一,为了使每次染色的格子恰好属于两个不同的类别,考虑黑白染色
设共有
此时显然无论什么时刻,黑白两色权值所增加的部分是一样的
设最后的值都变成了x,则有
所以
但当
接着考虑如何判断,首先建立源点和汇点,源点向所有白格连边,边权为x-val,所有黑格向汇点连边,边权为x-val,表示每个点还需要加上多少才能达到x
接着所有白格和相邻黑格连边,边权为inf,因为相邻的格子可以操作无数次
此时,跑最大流即可,如果最大流是
3. 最小割
3.1. 一些概念
割:对于一个网络流图
割的容量:我们定义割(S,T)的容量c(S,T)表示所有从S到T的边的容量之和,即
最小割:对于一个割{S,T},记其容量为
3.2. 最大流最小割定理
3.2.1. 定理内容
对于任意网络,其最大流f,和最小割{S,T},满足
3.2.2. 证明
在概念中,我们提到节点u的净流量为:
3.3. 例题
3.3.1. [国家集训队] happiness
https://gxyzoj.com/d/gxyznoi/p/P74
考虑将每一个人学文则向源点连边,学理则向汇点连边,忽略组合的问题,因为一个人不可能又学文又学理,所以最小割就是答案
接下来考虑组合,因为要两个人同时学一门课,所以需要将两个点合并
但是一个人涉及的关系很多,不妨建新的节点来记录
为了保证一个人只学一门课,所以可以将两个人同时学文的节点和源点连边,边权为val,同时学理的节点和汇点连边,边权为val
然后将其所关联的两个节点分别与其连边,边权为inf
此时的图中,如果与x相连的节点必然只连向s或只连向t才满足条件,求最小割即可
3.3.2. [TJOI2015] 线性代数
https://gxyzoj.com/d/gxyznoi/p/P75
暴力展开可得
展开过程自己算,我是不想再算一遍了
可以借鉴上一题的思路,因为不能同为0和1,所以必然是一边连源点表示0,一边连汇点表示0
正求图中会出现负数,所以可以先求出
为1的部分简单,直接向汇点连一条权值为
所以依然可以借鉴上一题的思路,先从源点出发与
3.3.3. [2009国家集训队]人员雇佣
https://gxyzoj.com/d/gxyznoi/p/P76
被读题杀了T_T
题面的如果j不来,则不仅本来与i合作的权值拿不到,而且还会损失
接着思考怎么做:
和上面两道题类似,依然要分成两种状态,一种是要这个人,则向汇点连一条边权为
另一种是不要,那么他能为公司带来的贡献也会消失,所以可以从源点向i建一条边权为
如果不考虑员工去竞争对手会带来的损失,显然,花费就是最小割
接下来,考虑会带来的损失,如果属于两家不同的公司,则不仅不能得到利益,而且会有损失,所有可以从i向j建一条边权为
3.3.4. 千钧一发
https://gxyzoj.com/d/gxyznoi/p/P77
再次被读题杀,注意,题面说的是满足一条即可,不是都要满足
可以按奇偶性分类讨论
对于奇数,
对于偶数,两数必然存在一个公因数2
所以,只需要考虑一个奇数和一个偶数的情况即可
对于每个奇数,可以向所有与它搭配后两个条件都不满足的偶数连边,此时,相连的两个点必然只能选一个
显然最小割,从源点向每个奇数
3.3.5. [Hnoi2013]切糕
https://gxyzoj.com/d/gxyznoi/p/P78
先不考虑d的限制,因为是要求最小的割断方式,显然最小割
对于每一列,可以增加一个新点,从源点向在一层的点连一条边权为inf的边,从第l+1层的点向汇点连一条边权为inf的边,第k层和第k+1层的点之间连一条边权为
接着考虑光滑度的限制,即在相邻的点的高度差大于d时,源点和汇点依然联通
所以,可以从每一列的一个点,向与之相邻列的高度比它小d的点连边,边权为inf,画图感性理解即可
3.3.6. [Ahoi2009] Mincut 最小割
https://gxyzoj.com/d/gxyznoi/p/P79
显然,必须边一定包含于可行边
根据最大流最小割定理,显然跑最大流时要割掉的边是满流的,所以在残量网络中,这些边的权值必然为0
接下来考虑如何找到这些边
现在考虑现有的满流边u,v会不会被替代,在残量网络中,如果存在一个环,包含边u,v
此时,让流沿环流一圈,最大流不变,但是u,v就不是唯一的选择了,当然,这个环要包含反边
既然是要求环,可以想到强联通分量,显然,如果两个顶点在同一个强联通分量中,则必然不总是最小割
所以,可以先用tarjan将原图缩成一个DAG,此时,DAG上的边就是满足条件的边
显然,连接源点所在强联通分量和汇点所在强联通分量的边是一定要割掉的
3.4. 最小割树
3.4.1. 问题的引入
给定n个点,m条边的的图,求任意两点之间的最小割
以这个图为例:
3.4.2. 问题的探究
最朴素的方法,暴力枚举每个点,然后跑最小割,时间复杂度
显然,这样的时间复杂度是无法接受的,所以要考虑优化
可以将上述的图中的所有最小割的值列在下表中
1 | 2 | 3 | 4 | |
---|---|---|---|---|
1 | 0 | 3 | 3 | 3 |
2 | 3 | 0 | 4 | 4 |
3 | 3 | 4 | 0 | 4 |
4 | 3 | 4 | 4 | 0 |
可以发现,表中的权值只有3和4,不妨猜想,在一个n个点的图中,其实最小割值的数量远不到
这就涉及到一个定理,在给定的含有n个点的无向图中,至多n-1种本质不同的最小割
所以,应该如何应用这个定理呢
3.4.3. 思想
最小割树通常用来解决点对之间的最小割问题,它利用在给定的含有n个点的无向图中,至多n-1种本质不同的最小割这一性质,将图上跑的最小割问题转化成求树上某一路径的最小值,从而降低时间复杂度
其定义是:在一棵最小割树T中,当且仅当对于树上的所有边(s,t),树上去掉(s,t)后的两个联通块中点的集合,恰好是原图上(s,t)的最小割把原图分成的两个连通块中的点的集合,且边(u,v)的权值等于原图上(u,v)的最小割
构建方法如下:
-
任选两点x,y,求出他们在原图中的最小割cut
-
在最小割树中建立边(x,y),边权为cut
-
按照最小割将原图分成两部分,然后重复此过程
按照此方法构造出来的最小割树,满足任意两点之间的最小割等价于树上这两点路径上的最小值
而构建的方法,可以采用分治的思想
3.4.4. 代码
https://gxyzoj.com/d/gxyznoi/p/P80
显然,最简单的方法是暴力求出任意两个点之间的最小割,然后统计,用最小割树可以快速解决
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<map>
using namespace std;
const int inf=1e9;
int n,m,edgenum,head[905],from[8505],to[8505],val[8505];
bool vis[905];
struct edge{
int to,val,nxt;
}e[40005],e1[40005];
int h1[905],edgenum1;
void add(int u,int v,int w)
{
e1[++edgenum1].nxt=h1[u];
e1[edgenum1].val=w;
e1[edgenum1].to=v;
h1[u]=edgenum1;
}
void add_edge(int u,int v,int w)
{
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
e[edgenum].val=w;
head[u]=edgenum;
}
void link(int u,int v,int w)
{
add_edge(u,v,w);
add_edge(v,u,0);
}
int p[905],t1[905],t2[905];
int dep[905],cur[905];
queue<int>q;
bool bfs(int x,int y)
{
memset(dep,0x7f,sizeof(dep));
while(!q.empty()) q.pop();
for(int i=1;i<=n;i++) cur[i]=head[i];
q.push(x);
dep[x]=0;
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(dep[v]>inf&&e[i].val)
{
dep[v]=dep[u]+1;
q.push(v);
}
}
}
if(dep[y]<inf) return 1;
return 0;
}
int dfs(int u,int flow,int t)
{
if(u==t||flow==0) return flow;
int res=flow;
for(int i=cur[u];i&&res;i=e[i].nxt)
{
cur[u]=i;
int v=e[i].to,w=e[i].val;
if(w>0&&dep[v]==dep[u]+1)
{
int k=dfs(v,min(res,w),t);
res-=k;
e[i].val-=k;
e[i^1].val+=k;
}
}
if(flow==res) dep[u]=0;
return flow-res;
}
void dfs1(int u)
{
vis[u]=1;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(!e[i].val||vis[v]) continue;
dfs1(v);
}
}
int dinic(int s,int t)
{
// printf("%d %d\n",s,t);
for(int i=1;i<=n;i++)
{
vis[i]=head[i]=0;
}
edgenum=1;
for(int i=1;i<=m;i++)
{
link(from[i],to[i],val[i]);
link(to[i],from[i],val[i]);
}
int ans=0;
while(bfs(s,t))
{
ans+=dfs(s,inf,t);
}
dfs1(s);
return ans;
}
void build(int l,int r)
{
if(l>=r) return;
int s=p[l],t=p[l+1];
int res=dinic(s,t);
// printf("%d %d %d\n",s,t,res);
add(s,t,res);
add(t,s,res);
int cnt1=0,cnt2=0;
for(int i=l;i<=r;i++)
{
if(vis[p[i]]) t1[++cnt1]=p[i];
else t2[++cnt2]=p[i];
}
for(int i=1;i<=cnt1;i++)
{
p[i+l-1]=t1[i];
}
for(int i=1;i<=cnt2;i++)
{
p[i+cnt1+l-1]=t2[i];
}
build(l,l+cnt1-1);
build(l+cnt1,r);
}
int f[905][11],dis[905][11];
void dfs2(int u,int fa)
{
dep[u]=dep[fa]+1;
f[u][0]=fa;
for(int i=1;i<=10;i++)
{
f[u][i]=f[f[u][i-1]][i-1];
dis[u][i]=min(dis[u][i-1],dis[f[u][i-1]][i-1]);
}
for(int i=h1[u];i;i=e1[i].nxt)
{
int v=e1[i].to;
if(v==fa) continue;
dis[v][0]=e1[i].val;
dfs2(v,u);
}
}
int lca(int x,int y)
{
int minn=inf;
if(dep[x]<dep[y]) swap(x,y);
for(int i=10;i>=0;i--)
{
if(dep[f[x][i]]>=dep[y])
{
minn=min(minn,dis[x][i]);
x=f[x][i];
}
if(x==y) return minn;
}
for(int i=10;i>=0;i--)
{
if(f[x][i]!=f[y][i])
{
minn=min(minn,min(dis[x][i],dis[y][i]));
x=f[x][i];
y=f[y][i];
}
}
minn=min(minn,min(dis[x][0],dis[y][0]));
return minn;
}
map<int,int> mp;
int main()
{
memset(dis,0x7f,sizeof(dis));
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&from[i],&to[i],&val[i]);
}
for(int i=1;i<=n;i++) p[i]=i;
build(1,n);
memset(dep,0,sizeof(dep));
dfs2(1,0);
int ans=0;
for(int i=1;i<=n;i++)
{
for(int j=i+1;j<=n;j++)
{
int tmp=lca(i,j);
if(!mp[tmp])
{
mp[tmp]=1;
ans++;
}
}
}
printf("%d",ans);
return 0;
}
但是题目中只要求求解不同的最小割的数量,所以可以不建树,在分治的过程中直接统计,代码就不放了
4. 费用流
费用流,又称最小费用最大流是指在普通的网络流图中,每条边的流量都有一个单价,求一组可行解,是它在满足最大流的情况下,费用最小
例如下面这题
有m个仓库和n个商店,第i个仓库有
个货物,第j个商店需要 个货物,从仓库i想商店j运输货物每个需要付出 的代价,货物供需平衡,即 ,求最少得运输费用
在最大流的基础上,本题又加上了费用问题,又该如何求解呢?
4.1. SPFA费用流
我们意识到一件事情,最小费用最大流,也就是在最大流的基础上找最小费用,也就是说必须先跑最大流,所以我们假装没有费用先算增广路
对于一条增广路p,假设它增广了f的流量,则总花费为
所以可以继续沿用EK或Dinic的思路,每次增广以费用为边权的最短路径即可
而关于反向边,流量还是0,与最大流一样,但是费用要去相反数,因为在流量回退时,费用也要回退
所以,在费用流中必然会出现负边权,所以可以用SPFA求最短路
4.2. 例题
4.2.1. [SDOI2016] 数字配对
https://gxyzoj.com/d/gxyznoi/p/P81
4.2.1.1. 思路
注意读题,要求的是
所以就可以按照约数个数的奇偶性进行分类,然后从每个奇数点向每个偶数点连边,流量为inf,这样就是一个二分图
然后考虑费用的问题,因为是i,j组成一对才会产生费用,所以可以在i,j之间的边上加上费用
再由源点向约数个数是奇数的点连一条流量为
费用流即是答案
4.2.1.2. 代码
#include<cstdio>
#include<cstring>
#include<queue>
#include<algorithm>
#define ll long long
using namespace std;
const ll inf=1e17;
int n,a[205],b[205],c[205],cnt[205],edgenum=1,head[205];
int s,t;
struct edge{
int to,nxt;
ll val,flow;
}e[50004];
void add_edge(int u,int v,ll f,ll w)
{
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
e[edgenum].val=w;
e[edgenum].flow=f;
head[u]=edgenum;
}
int getp(int x)
{
int res=0;
for(int i=2;i<=x/i;i++)
{
while(x%i==0)
{
res++;
x/=i;
}
}
if(x>1) res++;
return res;
}
ll dis[205],flow[205];
int pre[205],pos[205];
bool inq[205];
queue<int>q;
bool spfa(int x,int y)
{
for(int i=0;i<=t;i++)
{
dis[i]=-inf,flow[i]=inf,inq[i]=0;
}
q.push(x);
dis[x]=0;
while(!q.empty())
{
int u=q.front();
q.pop();
inq[u]=0;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
ll w=e[i].val,f=e[i].flow;
if(dis[v]<dis[u]+w&&f)
{
dis[v]=dis[u]+w;
flow[v]=min(flow[u],f);
pre[v]=u,pos[v]=i;
if(!inq[v])
{
q.push(v);
inq[v]=1;
}
}
}
}
if(dis[y]>-inf) return 1;
return 0;
}
int MCMF()
{
ll cost=0,res=0;
while(spfa(s,t))
{
ll now=dis[t]*flow[t];
if(now+cost>=0)
{
cost+=now;
res+=flow[t];
int x=t;
while(x!=s)
{
int p=pos[x];
e[p].flow-=flow[t];
e[p^1].flow+=flow[t];
x=pre[x];
}
}
else
{
res+=cost/(-dis[t]);
break;
}
}
return res;
}
int main()
{
scanf("%d",&n);
t=n+1;
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
cnt[i]=getp(a[i]);
}
for(int i=1;i<=n;i++)
{
scanf("%d",&b[i]);
if(cnt[i]%2!=0)
{
add_edge(0,i,b[i],0);
add_edge(i,0,0,0);
}
else
{
add_edge(i,t,b[i],0);
add_edge(t,i,0,0);
}
}
for(int i=1;i<=n;i++)
{
scanf("%d",&c[i]);
}
for(int i=1;i<=n;i++)
{
if(cnt[i]%2==0) continue;
for(int j=1;j<=n;j++)
{
if(cnt[j]%2||i==j) continue;
if(a[i]%a[j]==0&&cnt[i]-cnt[j]==1)
{
add_edge(i,j,inf,1ll*c[i]*c[j]);
add_edge(j,i,0,-1ll*c[i]*c[j]);
}
if(a[j]%a[i]==0&&cnt[j]-cnt[i]==1)
{
add_edge(i,j,inf,1ll*c[i]*c[j]);
add_edge(j,i,0,-1ll*c[i]*c[j]);
}
}
}
printf("%lld",MCMF());
return 0;
}
4.2.2. [SDOI2009]晨跑
https://gxyzoj.com/d/gxyznoi/p/P82
题中要求的是最长的天数和最短的距离,显然是最小费用最大流,以时间为流量,长度为费用
依题意,每个十字路口只能经过一次,将其抽象成一张图,则每个节点只能经过一次
所以难点在于如何保证每个节点只经过一次?
考虑拆点,将每个十字路口拆成两个点,一个连出边,一个连入边,从连入边的点向连出边的点连一条权值为0,流量限制为1的边
此时,当流经十字路口时,流量必然被限制为1,求费用流即可
4.2.3. [SCOI2007]修车
https://gxyzoj.com/d/gxyznoi/p/P83
一个很错的做法,将每一个厨师和菜连边,边权为所需时间,流量为1,然后跑费用流
为什么是错的呢,因为在这种方法中,没有考虑等待的时长,那么要如何解决呢?
可以将每个工人拆成n个,然后从1到n编号,编号为i表示修他自己的倒数第i辆车的工人
所以,在每辆车向工人连边时,要乘上计算的次数
4.2.4. [Noi2012] 美食节
https://gxyzoj.com/d/gxyznoi/p/P84
和上一题很像,但是注意到数据范围,显然不能暴力加边
考虑优化,因为人数有限,显然很多边是用不上的,而且用掉的边必然是连续的,所以一边跑一边加边即可
5. 最大权闭合子图
5.1. 概念
闭合图:图内任意点的任意后继也在图内
最大闭合子图:在一个图G的所有闭合子图内点权和最大的图
5.2. 求解方法
- 将所有的边的权值设为inf,所以在后面跑最小割时必然不会把这条边割掉
- 增加源点和汇点
- 将所有点权为正数的点u向源点连一条边权为
的边 - 将所有点权为负数的点v向汇点连一条边权为
的边 - 求出最小割,记为mincut,记所有权值为正数的点的点权和为sum,则答案为
5.3. 例题
5.3.1. [NOI2006] 最大获利
https://gxyzoj.com/d/gxyznoi/p/P85
5.3.1.1. 思路
因为如果要选一个人,则和他相关的两个站必须建,所以就可以从用户向站连边,然后这道题家变成了求最大闭合子图
显然,用户能带来的价值是正的,而建站需要花钱,所以带来的价值必然是负的
所以要从源点向用户连一条边权为c的边,站向汇点连一条边权为p的边
所以,答案就是边权是正数的权值和减去最小割的权值和
5.3.1.2. 代码
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int inf=1e9;
int n,m,head[100005],edgenum=1,s,t,ans;
struct edge{
int nxt,val,to;
}e[1000005];
void add_edge(int u,int v,int w)
{
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
e[edgenum].val=w;
head[u]=edgenum;
}
void link(int u,int v,int w)
{
add_edge(u,v,w);
add_edge(v,u,0);
}
int dep[100005],cur[100005];
queue<int> q;
bool bfs(int x,int y)
{
memset(dep,0x7f,sizeof(dep));
for(int i=0;i<=t;i++) cur[i]=head[i];
while(!q.empty()) q.pop();
q.push(x);
dep[x]=1;
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(e[i].val&&dep[v]>inf)
{
dep[v]=dep[u]+1;
q.push(v);
}
}
}
if(dep[y]<inf) return 1;
return 0;
}
int dfs(int u,int flow)
{
// printf("%d %d\n",u,flow);
if(flow==0||u==t) return flow;
int res=flow;
for(int i=cur[u];i&&res;i=e[i].nxt)
{
cur[u]=i;
int v=e[i].to,w=e[i].val;
if(dep[v]==dep[u]+1&&w>0)
{
int k=dfs(v,min(res,w));
res-=k;
e[i].val-=k;
e[i^1].val+=k;
}
}
if(res==flow) dep[u]=0;
return flow-res;
}
int main()
{
scanf("%d%d",&n,&m);
t=n+m+1;
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
link(m+i,t,x);
}
for(int i=1;i<=m;i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
link(i,a+m,inf);
link(i,m+b,inf);
link(s,i,c);
ans+=c;
}
while(bfs(s,t))
{
// printf("1");
ans-=dfs(s,inf);
}
printf("%d",ans);
return 0;
}
5.3.2. [NOI2009] 植物大战僵尸
https://gxyzoj.com/d/gxyznoi/p/P86
题面真的读不懂!
简单解释一下,就是如果要获得位置x的权值,则要先攻击同行列比它大的位置,以及可以保护它的位置,获得的权值有正有负,求最大值
因为攻击有顺序要求,所以所以考虑最大闭合子图
可以从点x连一条边权为inf的边到在此之前必须攻击的点,此时我们会发现一种情况,就是它会出现环
例如
所以考虑拓扑排序,标记出所有无法攻击的点不添加,然后就是暴力最大闭合子图
5.3.3. [SHOI2017]寿司餐厅
https://gxyzoj.com/d/gxyznoi/p/P87
波波的题面……
显然,当可以获得权值
所以可以从
可以发现,前面的
所以可以从编号
此时,最大闭合子图就是答案
6. 上下界网络流
6.1. 无源汇上下界可行流
给定一个无源汇流量网络G,每条边都有上下界,求是否存在一种流使得流量守恒
记上界为
我们不妨设此时每条边的下界已经满流,建立新图,则现在每条边剩余的流量为
但是此时必然存在流量不守恒的点,所以考虑在新图中增减流量来抵消下界满流后产生的流量不守恒
此时,我们可以像普通的网络流一样建立源点和汇点,让这两个点进行调节
假设流入和流出的流量相差m(流入-流出=m)
,不做调节 ,此时流入的流量过大,需要增加流出的量,显然,就要从源点取m的流量流出维持平衡 ,此时流出的量过大,需要增加流入的量,换言之可以将多余的流量直接流向汇点
易知,源点和汇点的流量和代表着全图的流量和
显然,当所有后来建的边全部满流时,才满足条件,所以在新图中跑最大流判断是否相等即可
6.2. 有源汇上下界可行流
给定有源汇的流量网络G,每条边有上下界,问是否存在一种方式,使除源点和汇点之外的所有点流量守恒
记源点为s,汇点为t,则我们可以加入一条从t向s的边,上界为inf,下界为0
此时,有源汇的问题就被转成了无源汇问题,若有解,则s到t的流量等于t到s附加边的流量
6.3. 有源汇上下界最大流
给定一个流量网络图G,每条边有上下界,询问是否存在一种标定每条边流量的方式,使得满足上下界的同时满足除源汇点外流量守恒,若存在,询问最大流量
首先在网络上找到任意一个可行流,如果找不到就直接结束,否则考虑删去所有附加边之后的残量网络上进行调整,可以在残量网络上跑从s到t的最大流,将最大流与可行流相加,就是答案
6.4. 有源汇上下界最小流
给定一个流量网络图G,每条边有上下界,询问是否存在一种标记每条边流量的方式,使得满足上下界的同时满足除源汇点外的点流量守恒,若存在,询问最小流量
其实这一题与6.3相似,只是将最大换成了最小,依然可以沿用上面的方法
在求最大流的时候是在求出可行流后尽可能加流量,而本题因为是求最小流,所以考虑在可行流的基础上去掉多余的流量
所以首先在原图上找一个可行流,找不到就直接退出,否则考虑删去所以附加边后的残量网络,在残量网络上跑从t到s的最大流,将可行流与最大流相减,就是答案
6.5. 上下界最小费用可行流
给定一个流量网络图G,每条边有上下界和费用,求是否存在一种流,使得在满足条件的同时,费用最小
它和上面方法很像,可以先转化为有源汇上下界可行流,然后在建新图时以差为边权,输入的费用为费用,附加边的费用均为0,此时跑费用流,再加上达到下界的费用就是答案
6.6. 上下界最小费用最大流
给定一个流量网络图G,每条边有上下界和费用,求一个流F,在满足上下界的情况下,流量最大且费用最小
按照有源汇最大流的做法,可以在求出上下界最小费用可行流的情况下还要继续找,所以可以在剩余的网络上跑最小费用最大流即可
6.7. 例题
6.7.1. 80人环游世界
https://gxyzoj.com/d/gxyznoi/p/P88
6.7.1.1. 思路
题意大概是要求在满足每个点的经过次数的情况下的最小费用
因为按照题面叙述,这些人的起止点均不固定,很难进行求解,考虑建立源点和汇点
因为可以从任何地方开始,而且每个点可以有任意多的人开始,所以可以从源点s向每个节点连一条容量为inf的边,同理,可以从每个点向汇点连一条容量为inf的边
接下来考虑人数限制,因为每个点只有
此时,这道题就变成了一个有源汇上下界最小费用可行流,此时,建立超级源汇点,有超级源点向源点连一条容量为m,费用为0的边限制人数
跑上下界网络流即可
6.7.1.2. 代码
#include<cstdio>
#include<queue>
#include<algorithm>
using namespace std;
const int inf=1e9;
int n,m,head[305],edgenum=1,s,t,s1,t1;
struct edge{
int to,nxt,flow,cost;
}e[300005];
void add_edge(int u,int v,int f,int w)
{
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
e[edgenum].flow=f;
e[edgenum].cost=w;
head[u]=edgenum;
}
int in[305];
int dis[305],flow[305],pre[305],pos[305];
bool inq[305];
queue<int> q;
bool spfa(int x,int y)
{
for(int i=0;i<=t1;i++)
{
dis[i]=inf,flow[i]=inf,inq[i]=0;
}
q.push(x);
dis[x]=0;
inq[x]=1;
while(!q.empty())
{
int u=q.front();
q.pop();
inq[u]=0;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to,w=e[i].cost,f=e[i].flow;
if(f&&dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
flow[v]=min(flow[u],f);
pos[v]=i,pre[v]=u;
if(!inq[v])
{
inq[v]=1;
q.push(v);
}
}
}
}
if(dis[y]<inf) return 1;
return 0;
}
int MCMF()
{
int cost=0;
while(spfa(s1,t1))
{
int x=t1;
while(x!=s1)
{
int p=pos[x];
e[p].flow-=flow[t1];
e[p^1].flow+=flow[t1];
x=pre[x];
}
cost+=flow[t1]*dis[t1];
}
return cost;
}
int main()
{
scanf("%d%d",&n,&m);
s=0,t=2*n+1;
s1=t+1,t1=t+2;
for(int i=1;i<=n;i++)//上限-下线
{
int x;
scanf("%d",&x);
add_edge(i,i+n,0,0);
add_edge(i+n,i,0,0);
in[i]-=x;
in[i+n]+=x;
}
add_edge(s1,s,m,0);//限制人数
add_edge(s,s1,0,0);
for(int i=1;i<=n;i++)
{
for(int j=i+1;j<=n;j++)
{
int x;
scanf("%d",&x);
if(x==-1) continue;
add_edge(i+n,j,inf,x);
add_edge(j,i+n,0,-x);
}
}
for(int i=1;i<n;i++)//和源汇点连边
{
add_edge(s,i,inf,0);
add_edge(i,s,0,0);
add_edge(i+n,t,inf,0);
add_edge(t,i+n,0,0);
}
for(int i=1;i<=2*n;i++)//附加边
{
if(in[i]>0)
{
add_edge(s1,i,in[i],0);
add_edge(i,s1,0,0);
}
else
{
add_edge(i,t1,-in[i],0);
add_edge(t1,i,0,0);
}
}
printf("%d",MCMF());
return 0;
}
6.7.2. [Ahoi2014] 支线剧情
https://gxyzoj.com/d/gxyznoi/p/P89
经过读题,可以发现,每条边至少要走一次,之多可以走无数次,所以图中每条边得上下界分别为1和inf
考虑到每个点都可以作为终点,而且在每个点结束得量也不一定,所以可以从每个点想汇点建一条下界为0,上界为inf的边
此时,求有源汇最小费用可行流即可
6.7.3. 清理雪道
https://gxyzoj.com/d/gxyznoi/p/P90
依然是每条边都要走一次, 因为题意与支线剧情相似,所以我直接写费用流了
但是,本题的正解并不是费用流,很明显,只有放一个人才会产生花费,所以当网络流量最小时,必然答案最小
因为可以从任意一点出发,所以本题就是一个无源汇最小流
6.7.4. 矩阵
https://gxyzoj.com/d/gxyznoi/p/P91
式子是要求所有行列的和的绝对值的最大值的最小值,显然,当这个值越大,则越容易构造,所以考虑二分
记第i行的和为
所以答案中的b每行之和的范围是
显然,可以从源点向每行连一条上界为
因为每个数字均大于L小于R,所以可以从每行向每列建一条上界为R,下界为L的边,代表每个数
所以,判断可行流是否满足条件即可
7. 平面图转对偶图
这种方法长用来解决最小割问题
7.1. 平面图
即所有的边只在顶点处相交的图
7.2. 对偶图
对于每个平面图,都有其所对应的对偶图
如下图,这是一张平面图,因为平面图的边军不在其他地方相交,所以将原图分为了一些有节点作为顶点的平面,记为f1,f2,f3
而图中的面就是对偶图中的节点,而边则是若原图的边相连接的两个平面为x和y,则在对偶图中就有一条连接x,y的边,如图:
7.3. 最小割求解
可以发现一个性质,就是在加完对偶图的边后,原图的每个点恰好对应对偶图的一个面
在加入源点和汇点后,考虑从源点和汇点将空白部分直接分成两部分,然后连边,如图:
因为点和面相互对应,边和边相互对应,所以从s走向t的路径就是一个割
显然,最小割就是最短路
7.4. 例题
7.4.1. [NOI2010] 海拔
https://gxyzoj.com/d/gxyznoi/p/P95
7.4.1.1. 思路
显然,最优的情况一定是一个全是0的联通块和一个全是1的联通块,所以要求中间的分界线,显然最小割
但是dinic会T,因为是网格,所以是平面图
考虑平面图转对偶图,手画后发现,就是经原图旋转90°
此时dijkstra求最短路即可
7.4.1.2. 代码
#include<cstdio>
#include<queue>
#include<algorithm>
#define ll long long
using namespace std;
int n,s,t,head[300005],edgenum;
struct edge{
int to,nxt,val;
}e[6000006];
int id(int x,int y)
{
return x*n-n+y;
}
void add_edge(int u,int v,int w)
{
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
e[edgenum].val=w;
head[u]=edgenum;
}
struct node{
int y;
ll val;
bool operator < (const node &tmp)const{
return val>tmp.val;
}
};
priority_queue<node> q;
bool vis[300005];
ll ans[300005];
ll dijkstra(int x,int y)
{
q.push((node){x,0});
while(!q.empty())
{
int u=q.top().y,w=q.top().val;
q.pop();
if(!vis[u])
{
vis[u]=1;
ans[u]=w;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(!vis[v])
{
q.push((node){v,w+e[i].val});
}
}
}
}
return ans[y];
}
int main()
{
scanf("%d",&n);
n++;
s=0,t=n*n+2;
for(int i=1;i<=n;i++)
{
for(int j=1;j<n;j++)
{
int x;
scanf("%d",&x);
if(i==1) add_edge(s,id(i,j),x);
else if(i==n) add_edge(id(i-1,j),t,x);
else add_edge(id(i-1,j),id(i,j),x);
}
}
for(int i=1;i<n;i++)
{
for(int j=1;j<=n;j++)
{
int x;
scanf("%d",&x);
if(j==1) add_edge(id(i,j),t,x);
else if(j==n) add_edge(s,id(i,j-1),x);
else add_edge(id(i,j),id(i,j-1),x);
}
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<n;j++)
{
int x;
scanf("%d",&x);
if(i==1) add_edge(id(i,j),s,x);
else if(i==n) add_edge(t,id(i-1,j),x);
else add_edge(id(i,j),id(i-1,j),x);
}
}
for(int i=1;i<n;i++)
{
for(int j=1;j<=n;j++)
{
int x;
scanf("%d",&x);
if(j==1) add_edge(t,id(i,j),x);
else if(j==n) add_edge(id(i,j-1),s,x);
else add_edge(id(i,j-1),id(i,j),x);
}
}
printf("%lld",dijkstra(s,t));
return 0;
}
8. 杂题
8.1. [SDOI2010] 星际竞速
https://gxyzoj.com/d/gxyznoi/p/P92
这道题和 80人环游世界 很像,因为经过每条边会产生花费,而且有经过次数的限制,所以本题是上下界费用流
题目中要求了每个点只能经过一次,所以考虑拆点来限制流量
可以将每个点拆为入点和出点,两个点之间连一条上下界均为1,费用为0的边,接着把所有直接可达的点相连
接着考虑能力爆发模式,如果暴力将每个点相连,则边数就会达到
可以发现,如果进行一次能力爆发,则相当于从源点开始重新走,所以可以从源点向每个点连相应的查找时间
此时,费用流就是答案
8.2. [BeiJing2006] 狼抓兔子
https://gxyzoj.com/d/gxyznoi/p/P94
这道题就是封锁一条路需要x的花费,问至少花费多少去封路就能让源点和汇点不连通
显然最小割,注意数组的大小!!!
8.3. [NOI2008] 志愿者招募
https://gxyzoj.com/d/gxyznoi/p/P96
开始的时候,显然会想到源点连志愿者,志愿者连对应的天,每一天连汇点
电视这个方法显然是错的,因为无法控制每个志愿者到每一天的流量
通过费用流的思想,它是要先满足流量最大,再满足费用最小
所以可以建立一个第n+1天,从第i天向第i+1天连一条流量为
此时,必然会有一些流量缺失,需要志愿者来补充,所以若志愿者的工作时间为s到t,就可以从s向t+1连一条流量为inf,费用为c的边,此时,费用流就是答案
8.4. [SDOI2017] 新生舞会
https://gxyzoj.com/d/gxyznoi/p/97
看到是求
因为是一个男生和一个女生配对,所以显然二分图
将两者结合,从每一行向每一列连的每一条边的容量均为1,而每一条边的权值为
此时,求费用流,如果大于0,就满足条件
8.5. [网络流 24 题] 运输问题
https://gxyzoj.com/d/gxyznoi/p/98
费用流板子,注意在一次计算后边的权值会被更改,要重新建图,还有求最长路的初值
8.6. [JSOI2009] 球队收益 / 球队预算
https://gxyzoj.com/d/gxyznoi/p/99
因为带有平方,且不能确定输赢的次数,不妨让所有队先全输,然后在计算赢加的支出
记赢的次数为x,输的原次数为y,则支出为
则多赢一次的支出为
展开,得:
两式相减得:
所以对于每场比赛,新建点向对应球队连边,而对于每个球队,则枚举x,向汇点连边权为
此时,费用流就是答案
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)