网络流算法以及24题(未完结)
继续开坑,待填坑 (19/24)
感谢WC带我复习学习了网络流算法,这篇会讲到网络流当中最大流-最小割问题的Dinic算法,以及最小费用最大流的算法,如果有机会还会介绍有上下界的最大可行流等拓展问题
网络最大流(Dinic)
网络可以理解为一个带权的有向图,最简单的图是只有一个边权,代表流量。存在源点和汇点,分别用来输出流量和接受流量。网络最大流问题通俗来讲可以理解为,一个自来水厂,就是源点,和很多个住户中间节点,和一个排污口,就是汇点,这三者之前存在管道,管道对流量是有限制的,并且自来水厂没有入水管道,排污口没有出水管道,在此条件下,求最大的流量,这就是最本质的网络最大流问题
以此图为例
我们有一个贪心的做法,就是让每条边尽可能满流,对于下面这幅图我们可能存在这样的规划
这样是正确的吗??未必是,请看图解
最大流是11,也就是说不一定满流才是最优的。
这样的话我们应该如何求解呢?引入反向边,意思就是我每次还是按照尽可能让每条边满流,如果流过去单位流量x,我们把反向边的容量调大,让它最多可以流回去x,如果存在更优解的情况,我们是可以通过反向边退回流量,也就是可以反悔。
然后接着再想流量怎么流过去,流过去的前提是容量未满,换句话说还有剩余流量。
然后介绍Dinic算法,先通过BFS构造分层图,按照可以流过去的情况确定深度deep,并且规定只有在相邻两边的deep相差为1时才可以流过去,然后通过dfs计算流量。之后重复bfs和dfs即可,直至没有可行方案
构造分层图例如
然后我们接下来按照满足尽可能满流和流量守恒的原则dfs求解,尽可能满流是指当流入量大于等于边的容量时,就让边满流过去,流量守恒是指当流入量小于边的容量时,需要流过去流入量的流量。
所以此次的最大流可能是27。这里跟dfs有关,是不确定的
此次dfs过后,我们会更新边权以及反向边的边权,此时边权代表的不是容量而是剩余容量,所以我们还可能存在可行流量,那么循环执行bfs,检查是否可行,不可行,退出输出即可
具体还是让我们看看代码吧,这次写注释了
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int maxn = 5020;
int n,m,str,end,tot = -1;
int head[maxn],deep[maxn];
LL ans;
struct node
{
int to,net,va;
}data[maxn<<1];
void add(int x,int y,int va)
{
data[++tot].net = head[x];
data[tot].to = y;
data[tot].va = va;
head[x] = tot;
}
bool bfs()
{
memset(deep,0,sizeof(deep));//deep确定层数
deep[str] = 1;
queue<int> q;
q.push(str);
while(!q.empty())
{
int from = q.front();q.pop();
for(int i=head[from];i!=-1;i=data[i].net)
{
int to = data[i].to ;
int va = data[i].va ;
if(va&&(!deep[to]))//分层依据是没有到过并且还有容量
{
deep[to] = deep[from]+1;
q.push(to);
if(to==end)
return true;//如果to==end表示可以流到汇点,直接返回即可
}
}
}
return false;
}
LL dinic(int from,LL flow)
{
if(from==end)
return flow;//终止条件判断
LL rest = flow;//rest表示剩余的流量
for(int i=head[from];i!=-1;i=data[i].net)
{
int to = data[i].to ;
LL va = data[i].va ;
if(!rest)//如果都流完了可以提前返回
return flow;
if(va&&deep[to]==deep[from]+1)//可以流过去
{
LL k = dinic(to,min(rest,va));//流量守恒和尽可能满流原则
if(k==0)//流不过去,就调整为9,减少dfs的次数
deep[to] = 0;
data[i].va -= k;
data[i^1].va += k;
rest -= k;
//正向边剩余容量减去本次流过去的,反向边容量加上本次流过去的,用去了k个流量,剩余流量减去k
}
}
return flow - rest;//流过来的 = 流出去的 + 剩余的
}
int main()
{
scanf("%d%d%d%d",&n,&m,&str,&end);
memset(head,-1,sizeof(head));
for(int i=1;i<=m;i++)
{
int a,b,va;
scanf("%d%d%d",&a,&b,&va);
add(a,b,va);
add(b,a,0);
//正向边容量为va,反向边的初始容量为0
}
while(bfs())//通过bfs检查是否可行
ans += dinic(str,2e9);//dfs统计答案
printf("%lld\n",ans);
return 0;
}
有几个注意的问题
1.加边时tot和head[]的处置要设置成-1,因为之后取反向边的时候直接异或1即可,循环也要改成i ! = -1
2.注意对容量va的判断,不要写错
3.待补充
练习题
P1343 地震逃生
P2740 [USACO4.2]草地排水Drainage Ditches
最小费用最大流(SPFA + EK)
P3381 【模板】最小费用最大流
最小费用最大流问题是在一个流网络中,边不仅有容量限制,还有价格限制,表示单位流量的费用,求最小花费下的最大流
EK是单次寻找增广路的方法,规则也和上文的Dinic相似,只需要BFS和DFS即可。
对于最小费用最大流问题,我们先保证最小费用,因为最大流是可以通过EK一点点求出来的,当前花费最小肯定是最好的。所以用到了SPFA求解一条最短可行路,然后通过EK求出流量以及修改反向边等操作,直至SPFA无解即可
总上所述,与最大流相似的,我们还是要找增广路,但是我们不能全图增广了,主要是因为要找花费最小的,所以用SPFA代替了BFS的过程,只修改最短路径,反向边等操作是不变的
代码如下,依然有注释
#include<bits/stdc++.h>
using namespace std;
const int maxn = 5020;
const int maxm = 50020;
int n,m,str,end,tot = -1,maxflow,mincost;
int head[maxn],pre[maxn],last[maxn],flow[maxn],d[maxn];
bool vis[maxn];
struct node
{
int net,va,cost,to;
}xuan[maxm<<1];
void add(int a,int b,int va,int cost)
{
xuan[++tot].cost = cost;
xuan[tot].net = head[a];
xuan[tot].to = b;
xuan[tot].va = va;
head[a] = tot;
}
bool SPFA()
{
memset(d,0x3f,sizeof(d));
memset(flow,0x3f,sizeof(flow));
memset(vis,0,sizeof(vis));
queue<int> q;
q.push(str);
vis[str] = 1;
d[str] = 0;
pre[end] = -1;
while(!q.empty())
{
int from = q.front();q.pop();
vis[from] = 0;
for(int i=head[from];i!=-1;i=xuan[i].net)
{
int to = xuan[i].to ;
int va = xuan[i].va ;
int cost = xuan[i].cost ;
if(va>0&&d[to]>d[from]+cost)
{//当前可行的要求是最短,容量也必须大于0
d[to] = d[from]+cost;
pre[to] = from;//前驱以及这个边记下来,因为要修改剩余容量
last[to] = i;
flow[to] = min(flow[from],va);//流过的容量
if(!vis[to])
{
vis[to] = 1;
q.push(to);
}
}
}
}
return pre[end] != -1;
}
void MCMF()
{
while(SPFA())
{
int now = end;
maxflow += flow[end];
mincost += flow[end]*d[end];
while(now!=str)
{
xuan[last[now]].va -= flow[end];
xuan[last[now]^1].va += flow[end];
now = pre[now];
}
//以流到end的流量为准,根据前驱依次经过来过的路,修改反向流量即可
}
}
int main()
{
memset(head,-1,sizeof(head));
scanf("%d%d%d%d",&n,&m,&str,&end);
for(int i=1;i<=m;i++)
{
int a,b,va,cost;
scanf("%d%d%d%d",&a,&b,&va,&cost);
add(a,b,va,cost);
add(b,a,0,-cost);//费用需要是负数,因为这算退费啊
}
MCMF();
printf("%d %d",maxflow,mincost);
return 0;
}
网络流24题题解
此外对于比较经典的网络流24题,我打算在今后的学习中做一份题解,为了保持一个友好的篇幅,已更新的题解我会在这里以链接的方式给出来
最大流问题
P2756 飞行员配对方案问题 二分图最大匹配
P3254 圆桌问题 多对多匹配,输出方案
P2763 试题库问题 多对多匹配,输出方案
P2766 最长不下降子序列问题 dp + 分层图构建 + 拆点
P4011 孤岛营救问题 状态压缩 + BFS
P2761 软件补丁问题 状态压缩 + SPFA
P2765 魔术球问题 拆点 + 合理利用残余网络
P2754 [CTSC1999]家园 / 星际转移问题 分层图
P2764 最小路径覆盖问题 转化问题,合并点
P2774 方格取数问题 转化为最小割
费用流问题
P4016 负载平衡问题 转运需要付费,最大流等于总和,边权限制平均值
P4014 分配问题 效率为费用,二分图最大流
P3356 火星探险问题 图上连边,要求输出路径