【8】网络流学习笔记
前言
网络流是图论中博大精深的一个分支,我自己没有学得很精通,所以这篇博客只能讲一部分内容。
网络流算法本身不会被太多考察,重点还是在于建图的思维能力。
最大流
给出一个包含 个点和 条边的有向图(下面称其为网络) ,该网络上所有点分别编号为 ,所有边分别编号为 ,其中该网络的源点为 ,汇点为 ,网络上的每条边 都有一个流量限制 。
你需要给每条边 确定一个流量 ,要求:
- (每条边的流量不超过其流量限制);
- ,(除了源点和汇点外,其他各点流入的流量和流出的流量相等);
- (源点流出的流量等于汇点流入的流量)。
定义网络 的流量 。
你需要求出该网络的最大流,即使 最大。
FF 算法
首先,我们考虑一种朴素的思路。使用 DFS 算法找到一条不包含边权为 的边的 的通路,根据每条边的流量不超过其流量限制,这条通路的流量不超过最小边的流量限制,为各边上边权的最小值。然后,将路径上的边边权减去这个这条通路的流量,表示用这条路流了这些流量。边权为 的边表示流满的边。重复这个过程,知道找不出符合条件的通路。
这个算法看起来没有什么问题,但是它是错的。在下面这个例子中,如果第一次的通路为 ,最后求出的流量只有 ,但是这个图的最大流量显然为 。
解决这个问题的方法,是在流完之后路径上的边增加反向边,反向边边权为这次流的流量。感性理解一下,因为走反向边就相当于反悔贪心,能考虑到所有情况。如果情况不好,可以接着反悔回来,结果依旧是对的。
基于此,我们给出一些常用的定义:
残余网络:在一个网络流图上,找到一条源到汇的路径(即找到了一个流量)后,对路径上所有的边,其容量都减去此次找到的流量,对路径上所有的边,都添加一条反向边,其容量也等于此次找到的流量,这样得到的新图,就称为原图的残余网络。
增广路径: 每次寻找新流量并构造新残余网络的过程,就叫做寻找流量的增广路径,也叫增广。
当然,这也带来了一些副作用,比如极高的时间复杂度。因为我们平时不会把这个算法写出来,所以代码和时间复杂度就不管了。
Edmonds-Karp算法(EK算法)
在寻找增广路径时,不一定要使用 DFS 算法,还可以使用BFS 算法。把每一次增广路的 DFS 改为 BFS,我们就得到了 Edmonds-Karp算法。
这个算法在时间复杂度上有很大的进步,为 ,其中 为节点数, 为边数,一般可以处理 的网络。
Dinic算法
DFS 算法有一个巨大的优势,就是可以回溯,借助回溯,就有可能实现一次寻找多条增广路。Dinic 算法就是使用了 BFS+DFS 达到了以上的思路,完成了算法复杂度的进一步下降。
每一次求增广路的过程分为以下 步:
:使用 BFS 算法,对原图进行分层。一个点的层数便是它离源点的最近距离。
:通过 DFS 算法寻找增广路,但是每一步必须走向下一层的节点。
:一次增广路求完之后,如果某条边尚未流满,则回溯到这条边的终点,向另一个下一层的节点继续搜索,直到这条边流满。
重复这个求增广路的过程,直到找不到增广路,也就是从源点无法到达汇点。
这样使时间复杂度得到了进一步优化,为 ,其中 为节点数, 为边数,一般可以处理 的网络。
Dinic 算法还可以进行当前弧优化。对于一个节点,当我们在 DFS 中遍历到第 条弧时,前 条弧必定流满,下一次访问时可以直接跳过,对应到代码中,就是 数组。
另外还有两个小剪枝,如果当前边已经流满,直接返回,因为没办法增加流量了,继续遍历没有意义。
如果当前边没有任何流出流量,证明该节点增广完毕,显然不会对结果再有贡献,不用再次遍历。将层次标记为 ,下一次就不会搜索到这个点了。
在代码中使用了一种成对存储技术来存储每条边以及其反向边。第一条边的编号为 ,且每条边的反向边的编号比该边编号大 。这样,无论是正向边 还是反向边 , 异或 就是其反向边存储的位置。
代码中 表示当前点的编号, 表示剩余流量, 表示流出流量。
bool bfs()
{
memset(dis,0,sizeof(dis));
l=1,r=0,q[++r]=s,dis[s]=1,pre[s]=h[s];
while(l<=r)
{
long long now=q[l];
for(int i=h[now];i;i=e[i].next)
{
if(!dis[e[i].v]&&e[i].dis>0)
{
dis[e[i].v]=dis[now]+1,pre[e[i].v]=h[e[i].v],q[++r]=e[i].v;
if(e[i].v==t)return 1;
}
}
l++;
}
return 0;
}
long long dinic(long long now,long long flow)
{
if(now==t)return flow;
long long rest=flow,out=0;
for(int i=pre[now];i;i=e[i].next)
{
pre[now]=i;
if(dis[e[i].v]==dis[now]+1&&e[i].dis>0)
{
long long k=dinic(e[i].v,min(rest,e[i].dis));
flow-=k,rest-=k,e[i].dis-=k,e[i^1].dis+=k,out+=k;
if(!flow)break;
}
}
if(out==0)dis[now]=0;
return out;
}
while(bfs())ans+=dinic(s,9999999999);
最小割
割:对于一个网络流图 ,一种点的划分方式把整个图划分为了 和 两个集合,且源点 ,汇点 ,这种划分方式称作这个图的割。
割的容量:割 的容量 表示离开 的边的权重之和。
最小割:就是求得一个割 ,使得割的容量 最小。
最大流最小割定理
在任何网络中,最大流的值等于最小割的容量。
这里给出一个比较感性的证明:
考虑求最大流的过程,本质上就是寻找了多条 的通路并删去最小边的边权。由一个显然易见的贪心,最小割也是对于每一个 的通路,找到通路上最小边删去。可见,这两个求法本身就是类似的。
对于有多条通路经过同一条边的情况,求最小割时有两种选择方式:割掉公共边和分别割掉每一条路径上的最小边,当然是哪种容量小选择哪种。最大流的求解恰好有两种对应的情况:公共边流满了和每一条路径上的最小边流满了,根据最大流的定义,这两种肯定是取较小的,恰好符合求最小割的两种选择方式取较小值。
综上所述,经过感性理解,最大流的值等于最小割的容量。所以,以后碰到需要求最小割的题目时,直接求最大流即可。
最小费用最大流
给出一个包含 个点和 条边的有向图(下面称其为网络) ,该网络上所有点分别编号为 ,所有边分别编号为 ,其中该网络的源点为 ,汇点为 ,网络上的每条边 都有一个流量限制 和单位流量的费用 。
你需要给每条边 确定一个流量 ,要求:
- (每条边的流量不超过其流量限制);
- ,(除了源点和汇点外,其他各点流入的流量和流出的流量相等);
- (源点流出的流量等于汇点流入的流量)。
定义网络 的流量 ,网络 的费用 。
你需要求出该网络的最小费用最大流,即在 最大的前提下,使 最小。
首先,既然要求出最大流,那么肯定考虑增广路。只要我们找到一条通路,就可以流一些流量(要建立反向边)。根据最大流算法,每次随便找一条通路,增加一些流量,直到没有通路为止。
我们可以考虑每一次找一条特殊的通路,来达到费用最小。很显然,这条通路就是每次 的最短路。因为最后无论怎么流,总是可以流到最大流。那我们优先寻找费用低的通路,因为这会占用一些流量,就可以避免走一些费用高的通路。
每一次寻找增广路的时候,使用 SPFA 算法,把每条边的费用当作边权。记录每一个节点最短路上的前驱,求解完最短路后直接倒推处理流量。注意剩余流量为 边依旧不能走。
不能使用 Dijistra,因为费用可能为负数。
bool spfa(long long s,long long t)
{
for(int i=1;i<=n;i++)d[i]=(long long)9999999999,pre[i]=f[i]=vis[i]=0;
hp=1,tp=0,q[++tp]=s,d[s]=0,f[s]=(long long)9999999999;
while(hp<=tp)
{
long long now=q[hp];
vis[now]=0;
for(int i=h[now];i;i=e[i].nxt)
{
if(e[i].d==0)continue;
if(d[e[i].v]>d[now]+e[i].x)
{
d[e[i].v]=d[now]+e[i].x,f[e[i].v]=min(f[now],e[i].d),pre[e[i].v]=i;
if(vis[e[i].v]==0)q[++tp]=e[i].v,vis[e[i].v]=1;
}
}
hp++;
}
if(d[t]==(long long)9999999999)return 0;
else return 1;
}
void mcmf(long long s,long long t)
{
while(spfa(s,t))
{
mf+=f[t],mc+=d[t]*f[t];
long long now=t;
while(now!=s)
{
e[pre[now]].d-=f[t],e[pre[now]^1].d+=f[t];
now=e[pre[now]^1].v;
}
}
}
例题
例题 :
几乎是双倍经验(我也不知道为什么我写的两份代码不一样)
P2740 [USACO4.2] 草地排水Drainage Ditches
最大流模板题,不多赘述。
P3376
#include <bits/stdc++.h>
using namespace std;
struct edge
{
long long v,next,dis;
}e[12000];
long long n,m,s,t,u,v,d,h[300],q[300],dis[300],pre[300],l,r,flow=0,ans=0,cnt=1;
void add_edge(long long u,long long v,long long dis)
{
e[++cnt].next=h[u];
e[cnt].v=v;
e[cnt].dis=dis;
h[u]=cnt;
}
bool bfs()
{
memset(dis,0,sizeof(dis));
l=1,r=0,q[++r]=s,dis[s]=1,pre[s]=h[s];
while(l<=r)
{
long long now=q[l];
for(int i=h[now];i;i=e[i].next)
{
if(!dis[e[i].v]&&e[i].dis>0)
{
dis[e[i].v]=dis[now]+1,pre[e[i].v]=h[e[i].v],q[++r]=e[i].v;
if(e[i].v==t)return 1;
}
}
l++;
}
return 0;
}
long long dinic(long long now,long long flow)
{
if(now==t)return flow;
long long rest=flow,out=0;
for(int i=pre[now];i;i=e[i].next)
{
pre[now]=i;
if(dis[e[i].v]==dis[now]+1&&e[i].dis>0)
{
long long k=dinic(e[i].v,min(rest,e[i].dis));
flow-=k,rest-=k,e[i].dis-=k,e[i^1].dis+=k,out+=k;
if(!flow)break;
}
}
if(out==0)dis[now]=0;
return out;
}
int main()
{
scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
for(int i=1;i<=m;i++)
{
scanf("%lld%lld%lld",&u,&v,&d);
add_edge(u,v,d);add_edge(v,u,0);
}
while(bfs())ans+=dinic(s,9999999999);
printf("%lld",ans);
return 0;
}
P2740
#include <bits/stdc++.h>
using namespace std;
struct edge
{
long long v,next,dis;
}e[12000];
long long n,m,s,t,u,v,d,h[300],q[300],dis[300],pre[300],l,r,flow=0,ans=0,cnt=1;
void add_edge(long long u,long long v,long long dis)
{
e[++cnt].next=h[u];
e[cnt].v=v;
e[cnt].dis=dis;
h[u]=cnt;
}
bool bfs()
{
memset(dis,0,sizeof(dis));
l=1,r=0,q[++r]=s,dis[s]=1,pre[s]=h[s];
while(l<=r)
{
long long now=q[l];
if(now==t)return 1;
for(int i=h[now];i;i=e[i].next)
if(!dis[e[i].v]&&e[i].dis>0)dis[e[i].v]=dis[now]+1,pre[e[i].v]=h[e[i].v],q[++r]=e[i].v;
l++;
}
return 0;
}
long long dinic(long long now,long long flow)
{
if(now==t)return flow;
long long rest=flow;
for(int i=pre[now];i;i=e[i].next)
{
pre[now]=i;
if(dis[e[i].v]==dis[now]+1&&e[i].dis>0)
{
long long k=dinic(e[i].v,min(rest,e[i].dis));
rest-=k,e[i].dis-=k,e[i^1].dis+=k;
}
}
return flow-rest;
}
int main()
{
scanf("%lld%lld",&m,&n);
s=1,t=n;
for(int i=1;i<=m;i++)
{
scanf("%lld%lld%lld",&u,&v,&d);
add_edge(u,v,d);add_edge(v,u,0);
}
while(bfs())
{
flow=-1;
while(flow!=0)
{
flow=dinic(s,99999999);
ans+=flow;
}
}
printf("%lld",ans);
return 0;
}
例题 :
P1345 [USACO5.4] 奶牛的电信Telecowmunication
很显然的最小割,但是我们之前讲的最小割是割边,这个题是割点。
考虑把点拆成边,对于一个点 ,拆成入点 和出点 ,连边 ,边权为 。断掉这条边,就相当于在原图中删掉了这个点。注意 两点连接 的边边权为正无穷,因为题目中说 不能被删掉。
考虑将原图中的边转化到图中,对于一条有向边 ,建边 ,这样就保证从 还是可以走到 。由于题目中给的是无向边,所以 还要再这样建一次。这个建边的边权为正无穷,因为不能删除原图中的边。
最后,建立 号超级源点 和 号超级汇点 。连边 ,通过 的最小割来表示切断 之间的通路。显然,为了不影响结果,这两条边的边权应该是正无穷。
#include <bits/stdc++.h>
using namespace std;
struct edge
{
long long v,next,dis;
}e[80000];
long long n,m,s,t,u,v,h[1000],q[1000],dis[1000],pre[1000],l,r,flow=0,ans=0,cnt=1;
void add_edge(long long u,long long v,long long dis)
{
e[++cnt].next=h[u];
e[cnt].v=v;
e[cnt].dis=dis;
h[u]=cnt;
}
bool bfs()
{
memset(dis,0,sizeof(dis));
l=1,r=0,q[++r]=s,dis[s]=1,pre[s]=h[s];
while(l<=r)
{
long long now=q[l];
for(int i=h[now];i;i=e[i].next)
{
if(!dis[e[i].v]&&e[i].dis>0)
{
dis[e[i].v]=dis[now]+1,pre[e[i].v]=h[e[i].v],q[++r]=e[i].v;
if(e[i].v==t)return 1;
}
}
l++;
}
return 0;
}
long long dinic(long long now,long long flow)
{
if(now==t)return flow;
long long rest=flow,out=0;
for(int i=pre[now];i;i=e[i].next)
{
pre[now]=i;
if(dis[e[i].v]==dis[now]+1&&e[i].dis>0)
{
long long k=dinic(e[i].v,min(rest,e[i].dis));
flow-=k,rest-=k,e[i].dis-=k,e[i^1].dis+=k,out+=k;
if(!flow)break;
}
}
if(out==0)dis[now]=0;
return out;
}
int main()
{
scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
add_edge(0,s,9999999999),add_edge(s,0,0);
add_edge(t+n,n*2+1,9999999999),add_edge(n*2+1,t+n,0);
for(int i=1;i<=n;i++)
if(i!=s&&i!=t)add_edge(i,i+n,1),add_edge(i+n,i,0);
else add_edge(i,i+n,9999999999),add_edge(i+n,i,0);
for(int i=1;i<=m;i++)
{
scanf("%lld%lld",&u,&v);
add_edge(u+n,v,9999999999);add_edge(v,u+n,0);
add_edge(v+n,u,9999999999);add_edge(u,v+n,0);
}
s=0,t=n*2+1;
while(bfs())ans+=dinic(s,9999999999);
printf("%lld",ans);
return 0;
}
例题 :
最小费用最大流模板题,不多赘述。
#include <bits/stdc++.h>
using namespace std;
struct edge
{
long long v,d,x,nxt;
}e[100010];
long long n,m,s,t,u,v,x,y,h[100010],d[100010],f[100010],vis[100010],pre[100010],q[500010],hp,tp,cnt=1,mc=0,mf=0;
void add_edge(long long u,long long v,long long d,long long x)
{
e[++cnt].nxt=h[u];
e[cnt].v=v;
e[cnt].d=d;
e[cnt].x=x;
h[u]=cnt;
}
bool spfa(long long s,long long t)
{
for(int i=1;i<=n;i++)d[i]=(long long)9999999999,pre[i]=f[i]=vis[i]=0;
hp=1,tp=0,q[++tp]=s,d[s]=0,f[s]=(long long)9999999999;
while(hp<=tp)
{
long long now=q[hp];
vis[now]=0;
for(int i=h[now];i;i=e[i].nxt)
{
if(e[i].d==0)continue;
if(d[e[i].v]>d[now]+e[i].x)
{
d[e[i].v]=d[now]+e[i].x,f[e[i].v]=min(f[now],e[i].d),pre[e[i].v]=i;
if(vis[e[i].v]==0)q[++tp]=e[i].v,vis[e[i].v]=1;
}
}
hp++;
}
if(d[t]==(long long)9999999999)return 0;
else return 1;
}
void mcmf(long long s,long long t)
{
while(spfa(s,t))
{
mf+=f[t],mc+=d[t]*f[t];
long long now=t;
while(now!=s)
{
e[pre[now]].d-=f[t],e[pre[now]^1].d+=f[t];
now=e[pre[now]^1].v;
}
}
}
int main()
{
scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
for(int i=1;i<=m;i++)
{
scanf("%lld%lld%lld%lld",&u,&v,&x,&y);
add_edge(u,v,x,y);add_edge(v,u,0,-y);
}
mcmf(s,t);
printf("%lld %lld",mf,mc);
return 0;
}
例题 :
本质上就是要求最少删除多少个点可以使得图不连通。
首先,如果把这个图每个点都删掉,这个图一定不连通,故可以把答案的初始值赋值为点数。
由于数据范围很小,考虑枚举任意两个点,求在不删除这两个点的情况下,最少删除多少点可以使这两个点不连通。因为如果这两个点不连通,整个图一定不连通。仔细看看,这就是例题 。
所以,我们只需要求 次例题 ,每一次的结果和答案取最小值即可。
#include <bits/stdc++.h>
using namespace std;
struct edge
{
long long v,next,dis;
}e[80000];
long long n,m,s,t,u[1000],v[1000],h[1000],q[1000],dis[1000],pre[1000],l,r,flow=0,ans=0,cnt=1;
void add_edge(long long u,long long v,long long dis)
{
e[++cnt].next=h[u];
e[cnt].v=v;
e[cnt].dis=dis;
h[u]=cnt;
}
void init()
{
memset(h,0,sizeof(h));
cnt=1,ans=0;
}
bool bfs()
{
memset(dis,0,sizeof(dis));
l=1,r=0,q[++r]=s,dis[s]=1,pre[s]=h[s];
while(l<=r)
{
long long now=q[l];
for(int i=h[now];i;i=e[i].next)
{
if(!dis[e[i].v]&&e[i].dis>0)
{
dis[e[i].v]=dis[now]+1,pre[e[i].v]=h[e[i].v],q[++r]=e[i].v;
if(e[i].v==t)return 1;
}
}
l++;
}
return 0;
}
long long dinic(long long now,long long flow)
{
if(now==t)return flow;
long long rest=flow,out=0;
for(int i=pre[now];i;i=e[i].next)
{
pre[now]=i;
if(dis[e[i].v]==dis[now]+1&&e[i].dis>0)
{
long long k=dinic(e[i].v,min(rest,e[i].dis));
flow-=k,rest-=k,e[i].dis-=k,e[i^1].dis+=k,out+=k;
if(!flow)break;
}
}
if(out==0)dis[now]=0;
return out;
}
int main()
{
while(scanf("%lld%lld",&n,&m)!=-1)
{
long long mi=9999999999;
for(int i=1;i<=m;i++)scanf(" (%lld,%lld)",&u[i],&v[i]),u[i]++,v[i]++;
mi=n;
s=0,t=n*2+1;
for(int s=1;s<=n;s++)
for(int t=s+1;t<=n;t++)
{
init();
for(int i=1;i<=m;i++)
{
add_edge(u[i]+n,v[i],9999999999);add_edge(v[i],u[i]+n,0);
add_edge(v[i]+n,u[i],9999999999);add_edge(u[i],v[i]+n,0);
}
add_edge(0,s,9999999999),add_edge(s,0,0);
add_edge(t+n,n*2+1,9999999999),add_edge(n*2+1,t+n,0);
for(int i=1;i<=n;i++)
if(i!=s&&i!=t)add_edge(i,i+n,1),add_edge(i+n,i,0);
else add_edge(i,i+n,9999999999),add_edge(i+n,i,0);
while(bfs())ans+=dinic(s,9999999999);
mi=min(mi,ans);
}
printf("%lld\n",mi);
}
return 0;
}
例题 :
由于本题拥有较多的限制,考虑图论建模跑网络流来满足这些性质。
首先,建立超级源点,并将超级源点连向每一种食物所代表的点。建立超级汇点,将每一种饮料代表的点连向超级汇点。以上边权均为 ,因为每种食物或饮料只能供一头牛享用。
这样,当我们从超级源点向超级汇点跑网络流时,就可以经过每一种食品或饮料表示使用,且满足了每种食物或饮料的数量限制。接下来,需要满足牛的限制。
把每一头牛拆成两个点,牛头和牛尾,牛头以边权 连向牛尾。这里不能不拆,因为需要这个 来限制每头牛只享用一种食物和一种饮料。然后,根据网络流向在食物和牛头,饮料和牛尾之间连边,使网络流可以正常流通。接下来,跑一遍最大流即可求出答案。
#include <bits/stdc++.h>
using namespace std;
struct edge
{
long long v,next,dis;
}e[60000];
long long n,f,d,nf,nd,t,u,h[600],q[600],dis[600],pre[600],l,r,flow=0,ans=0,cnt=1;
void add_edge(long long u,long long v,long long dis)
{
e[++cnt].next=h[u];
e[cnt].v=v;
e[cnt].dis=dis;
h[u]=cnt;
}
bool bfs()
{
memset(dis,0,sizeof(dis));
l=1,r=0,q[++r]=0,dis[0]=1,pre[0]=h[0];
while(l<=r)
{
long long now=q[l];
for(int i=h[now];i;i=e[i].next)
{
if(!dis[e[i].v]&&e[i].dis>0)
{
dis[e[i].v]=dis[now]+1,pre[e[i].v]=h[e[i].v],q[++r]=e[i].v;
if(e[i].v==t)return 1;
}
}
l++;
}
return 0;
}
long long dinic(long long now,long long flow)
{
if(now==t)return flow;
long long rest=flow,out=0;
for(int i=pre[now];i;i=e[i].next)
{
pre[now]=i;
if(dis[e[i].v]==dis[now]+1&&e[i].dis>0)
{
long long k=dinic(e[i].v,min(rest,e[i].dis));
flow-=k,rest-=k,e[i].dis-=k,e[i^1].dis+=k,out+=k;
if(!flow)break;
}
}
if(out==0)dis[now]=0;
return out;
}
int main()
{
scanf("%lld%lld%lld",&n,&f,&d);
t=n*2+f+d+1;
for(int i=1;i<=n;i++)
{
scanf("%lld%lld",&nf,&nd);
for(int j=1;j<=nf;j++)
{
scanf("%lld",&u);
add_edge(u,i+f,9999999999);add_edge(i+f,u,0);
}
for(int j=1;j<=nd;j++)
{
scanf("%lld",&u);
add_edge(i+f+n,u+2*n+f,9999999999);add_edge(u+2*n+f,i+f+n,0);
}
add_edge(f+i,f+i+n,1);add_edge(f+i+n,f+i,0);
}
for(int i=1;i<=f;i++)add_edge(0,i,1),add_edge(i,0,0);
for(int i=1;i<=d;i++)add_edge(i+n*2+f,n*2+f+d+1,1),add_edge(n*2+f+d+1,i+n*2+f,0);
while(bfs())ans+=dinic(0,9999999999);
printf("%lld",ans);
return 0;
}
例题 :
开始上难度了。
由于限制依旧很多,考虑网络流。首先考虑从正面解决这个问题,发现无法通过图论来约束租和买两种操作,无法解决。考虑反向思考。
可以先假设所有任务全部都做,不计算机器的费用,然后就是要求一个最小的值来减少机器的费用。对于求最小值,我们考虑最小割。
那么,我们建立超级源点 和超级汇点 ,用每条 的路径的割断表示选择何种方式。对于每一个任务,我们有两种方式对待它:做或者不做。如果不做,我们会失去这个任务带来的收益,但是不用考虑其他的费用。我们对于每一个任务 ,建立 ,边权为完成 带来的收益,然后通过某种办法做到 ,这样割断 就可以表示不做任务 。根据最小割的性质,就不用继续考虑 的费用了,因为不做这个任务,自然不用考虑做这个任务的费用。
考虑租用只能用于一个任务,而购买可以用于所有任务,把需要用到机器 的任务直接连向机器 ,边权为租用这台机器的费用,使机器 直接连向 ,边权为购买这台机器的费用。这样,割掉这条边时,就相当于买了这台机器,经过这些任务的边就已经断了,不会再求其他边的影响了,就相当于是不用考虑租机器。
当割掉机器与任务之间的边时,就相当于租用机器。
由于最小割会自动选择最小的情况,所有求出来的就是费用的最小值。用所有任务的总收益减去,就是最终答案。
#include <bits/stdc++.h>
using namespace std;
struct edge
{
long long v,next,dis;
}e[3000000];
long long n,m,s,t,x,v,a,b,h[30000],q[30000],dis[30000],pre[30000],l,r,sum=0,flow=0,ans=0,cnt=1;
void add_edge(long long u,long long v,long long dis)
{
e[++cnt].next=h[u];
e[cnt].v=v;
e[cnt].dis=dis;
h[u]=cnt;
}
bool bfs()
{
memset(dis,0,sizeof(dis));
l=1,r=0,q[++r]=s,dis[s]=1,pre[s]=h[s];
while(l<=r)
{
long long now=q[l];
for(int i=h[now];i;i=e[i].next)
{
if(!dis[e[i].v]&&e[i].dis>0)
{
dis[e[i].v]=dis[now]+1,pre[e[i].v]=h[e[i].v],q[++r]=e[i].v;
if(e[i].v==t)return 1;
}
}
l++;
}
return 0;
}
long long dinic(long long now,long long flow)
{
if(now==t)return flow;
long long rest=flow,out=0;
for(int i=pre[now];i;i=e[i].next)
{
pre[now]=i;
if(dis[e[i].v]==dis[now]+1&&e[i].dis>0)
{
long long k=dinic(e[i].v,min(rest,e[i].dis));
flow-=k,rest-=k,e[i].dis-=k,e[i^1].dis+=k,out+=k;
if(!flow)break;
}
}
if(out==0)dis[now]=0;
return out;
}
int main()
{
scanf("%lld%lld",&n,&m);
s=0,t=n+m+1;
for(int i=1;i<=n;i++)
{
scanf("%lld%lld",&x,&v);
sum+=x,add_edge(s,i,x),add_edge(i,s,0);
for(int j=1;j<=v;j++)
{
scanf("%lld%lld",&a,&b);
add_edge(i,n+a,b),add_edge(n+a,i,0);
}
}
for(int i=1;i<=m;i++)
{
scanf("%lld",&x);
add_edge(n+i,t,x),add_edge(t,n+i,0);
}
while(bfs())ans+=dinic(s,9999999999);
printf("%lld",sum-ans);
return 0;
}
例题 :
如果要取某一个方格,那么周围四个方格都不能取,不限制其他方格。这个正向也不太好维护,考虑先选出所有方格,然后减去一批方格,使剩余的方格没有公共边。
考虑相邻的两个点的横纵坐标之和的奇偶性不同,把图中的点划分为两类:横纵坐标之和为奇数的和横纵坐标之和为偶数的。
考虑最小割,并将割定义为删去的方格,这样我们就可以用最小割表示删去方格数的和的最小值。由于每个点只能删一次,建立超级源点 ,对于每一个横纵坐标之和为偶数的方格连边,边权为这个方格的正整数。在最小割中割掉这条边,相当于删去这个方格。建立超级汇点 ,对于每一个横纵坐标之和为奇数的方格连边,边权为这个方格的正整数。与上面同理,这样就能使每个方格最多被删除一次。
相邻的方格不能选,所有相邻的方格应该有连边,这样跑最小割的时候就会把两边的方格割断一个。对于每一个横纵坐标之和为偶数的方格,连边向与其四周相邻的点,边权为正无穷,因为这个约束关系不能被删除。
最小割会自动选择割掉哪些边,也就是在原方格图中删掉哪些点使费用最小,并根据方格之间的连边,满足相邻的方格不能选的约束关系。求出最小割之后,用方格图中数的总和减去最小割就是最后答案。
#include <bits/stdc++.h>
using namespace std;
struct edge
{
long long v,next,dis;
}e[4000000];
long long n,m,s,t,h[40000],q[40000],dis[40000],pre[40000],a[200][200],l,r,sum=0,flow=0,ans=0,cnt=1,inf=1e15;
long long dx[4]={0,0,1,-1};
long long dy[4]={1,-1,0,0};
void add_edge(long long u,long long v,long long dis)
{
e[++cnt].next=h[u];
e[cnt].v=v;
e[cnt].dis=dis;
h[u]=cnt;
}
bool bfs()
{
memset(dis,0,sizeof(dis));
l=1,r=0,q[++r]=s,dis[s]=1,pre[s]=h[s];
while(l<=r)
{
long long now=q[l];
for(int i=h[now];i;i=e[i].next)
{
if(!dis[e[i].v]&&e[i].dis>0)
{
dis[e[i].v]=dis[now]+1,pre[e[i].v]=h[e[i].v],q[++r]=e[i].v;
if(e[i].v==t)return 1;
}
}
l++;
}
return 0;
}
long long dinic(long long now,long long flow)
{
if(now==t)return flow;
long long rest=flow,out=0;
for(int i=pre[now];i;i=e[i].next)
{
pre[now]=i;
if(dis[e[i].v]==dis[now]+1&&e[i].dis>0)
{
long long k=dinic(e[i].v,min(rest,e[i].dis));
flow-=k,rest-=k,e[i].dis-=k,e[i^1].dis+=k,out+=k;
if(!flow)break;
}
}
if(out==0)dis[now]=0;
return out;
}
int main()
{
scanf("%lld%lld",&n,&m);
s=0,t=n*m+1;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
scanf("%lld",&a[i][j]);
sum+=a[i][j];
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
if((i+j)%2==0)
{
add_edge(s,m*(i-1)+j,a[i][j]),add_edge(m*(i-1)+j,s,0);
for(int k=0;k<4;k++)
{
int tx=i+dx[k],ty=j+dy[k];
if(tx>n||ty>m||tx<=0||ty<=0)continue;
add_edge(m*(i-1)+j,m*(tx-1)+ty,inf),add_edge(m*(tx-1)+ty,m*(i-1)+j,0);
}
}
else if((i+j)%2==1)
add_edge(m*(i-1)+j,t,a[i][j]),add_edge(t,m*(i-1)+j,0);
while(bfs())ans+=dinic(s,inf);
printf("%lld",sum-ans);
return 0;
}
后记
相关法律规定,网络流题目不能卡 Dinic 算法。
网络流一堆神仙思维题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探