前述:这段时间一直在忙数论,图论这块因为知识点都看得差不多了(按dalao的博客还差几小块知识,大头已经差不多了),周六写完作业想忙里偷闲弄会图论(复习完板子就该从博客做题入手了),结果晚上没弄完那一点板子,还是拖到了周末。不仅仅是因为有一些东西遗忘了,更是第二遍看的时候更需要把里面一些比较细致的东西看明白,然后弄懂,不能留下像第一遍看的时候的那样的漏洞。
网络流的重要概念(按自己理解写的):
1.网络:就是一个由点的集合,有向边的集合,权值的集合构成的集合,可以记为D=(V,E,C),V是点的集合,E是边的集合,C是边权的集合。连在一起就是在一个点集当中,某些点之间有有向边,称为有向边的集合,在这些有向边上面又有权值,也就是可以流过这个边的最大流量,这就是权值的集合,加在一起构成了网络。
2.网络流:在这些点当中,有两个特殊的点,称作源和汇。同时,除源点和汇点外,所有点的入流和出流都相等,而源点只有流出的流,汇点只有汇入的流(定义很重要,网络流其实比较难的地方在构图,判断图是否有效的一个最low的办法就是定义)。这样的网络就是网络流。
3.残留网络:对于一个u-v的边,定义cf<u,v>=c<u,v>-f<u,v>,也就是这个边的容量和流量的差值为额外可通过的流量,所有c<u,v>中大于0的边构成的集合就是残留网络。
4.增广路径:残留网络中一条从源到汇的路径。(在最大流算法中,比较重要的就是找这个增广路径。)
最大流:
最大流是所有网络流里面情况最简单的一种,就是求从源到汇的一种方案,使得这种方案在所有方案当中流值最大,再难一些就是在边的容量的前提下加上一个最小容量,或者再加上一个费用,这就是有下界的最大流和费用流。
三种算法的公共思想:
1.在残留网络中寻找增广路径
2.若没有增广路径,那么算法结束,得到最大流。
3.找到路径上容量最小的边(网络流里面的规定就是每条路上的最大流量不超过路径上边的容量的最小值)
4.用上述容量进行更新路径,并加反向边(这里,加反向边的原因通俗的将就是可以反悔,万一有更好的选法,从那条路径上走就相当于沿着反向边走回去)
5.转到1继续
而最大流的三个算法的最大区别就是在找增广路径的方法上,更新的方法是一致的。
(以下代码均基于HDU 3549 flow problem,一道最基本的网络流的题目,题意就是让求从1到n的最大流,学这个的思路是基于饶齐的博客,没有老师指导只能跟随dalao的步伐)
算法1:Edmond-Karp算法
算法找最短路径的方法: 最朴素的BFS
AC代码:(没用类进行封装,等熟练了在进行更正)
#include<iostream>
#include<cstring>
#include<cmath>
#include<cstdio>
#include<algorithm>
#include<queue>
#define INF 1e9
#define maxn 20
using namespace std;
int n,m;
int map[maxn][maxn];
int flow[maxn][maxn];
int a[maxn];
int pre[maxn];
int EK(int s,int t){
int i,j,ans=0;
queue<int>qq;
memset (flow,0,sizeof(flow));
while (1){
memset (a,0,sizeof(a));
qq.push(s);
a[s]=INF;
while (!qq.empty()){
int u=qq.front();
qq.pop();
for (i=1;i<=n;i++){
if (!a[i]&&map[u][i]>flow[u][i]){
pre[i]=u;
qq.push(i);
a[i]=min(a[u],map[u][i]-flow[u][i]);
}
}
}
if (a[t]==0)break;
for (int i=t;i!=s;i=pre[i]){
flow[pre[i]][i]+=a[t];
flow[i][pre[i]]-=a[t];
}
ans+=a[t];
}
return ans;
}
int main(){
int i,j,k,l,x,y,z,cas=0,t;
scanf("%d",&t);
while (t--){
scanf("%d%d",&n,&m);
memset (map,0,sizeof(map));
for (i=1;i<=m;i++){
scanf("%d%d%d",&x,&y,&z);
map[x][y]+=z;
}
printf("Case %d: %d\n",++cas,EK(1,n));
}
}
算法2:Dinic算法
算法找增广路径的方法:
首先,有缺陷才能说是优化,第一个EK算法虽然十分的好理解,但是他有一个非常致命的缺陷,就是有可能路径会在同一层之间沿着回路反复,这样是非常浪费时间的。因此有了如下的优化:
在Dinic算法中,对朴素的找增广路径的算法进行了一定程度上的改进,改进方法有点类似于拓扑排序里面的入度,这里是统计和s的最短距离作为一个层次的标号,每次找增广路径的时候都沿着层次增加的方向找,这样找到的都是有用路径,可以大大增加算法的效率。
AC代码:(同样是HDU-3549)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cmath>
#include<vector>
#define maxn 20
#define INF 1e9
using namespace std;
struct edge{
edge(){};
edge(int from,int to,int val,int flow):from(from),to(to),val(val),flow(flow){}
int from,to,val,flow;
};
int n,m;
vector<edge>p;
vector<int>num[maxn];
bool visit[maxn];
int dis[maxn];
int cur[maxn];
void add(int x,int y,int z){
p.push_back(edge(x,y,z,0));
p.push_back(edge(y,x,0,0));
int xx=p.size();
num[x].push_back(xx-2);
num[y].push_back(xx-1);
}
bool bfs(int s,int t){
int i,j;
memset (visit,0,sizeof(visit));
queue<int>qq;
qq.push(s);
dis[s]=0;
visit[s]=1;
while (!qq.empty()){
int u=qq.front();
qq.pop();
for (i=0;i<num[u].size();i++){
edge& e=p[num[u][i]];
if (!visit[e.to]&&e.val>e.flow){
visit[e.to]=1;
dis[e.to]=dis[u]+1;
qq.push(e.to);
}
}
}
return visit[t];
}
int dfs(int x,int t,int val){
if (x==t||val==0){
return val;
}
int flow=0,f;
for (int& i=cur[x];i<num[x].size();i++){
edge& e=p[num[x][i]];
if (dis[x]+1==dis[e.to]&&(f=dfs(e.to,t,min(val,e.val-e.flow)))>0){
e.flow+=f;
p[num[x][i]^1].flow-=f;
flow+=f;
val-=f;
if (val==0)break;
}
}
return flow;
}
int dinic(int s,int t){
int flow=0;
while (bfs(s,t)){
memset (cur,0,sizeof(cur));
flow+=dfs(s,t,INF);
}
return flow;
}
int main(){
int i,j,k,l,x,y,z,cas=0,t;
scanf("%d",&t);
while (t--){
scanf("%d%d",&n,&m);
p.clear();
for (i=1;i<=n;i++)num[i].clear();
for (i=1;i<=m;i++){
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
printf("Case %d: %d\n",++cas,dinic(1,n));
}
}