网络流问题略解

 

假目录

·网络流简介

·最大流

  EK算法

·最小割

·Dinic

  当前弧优化

  最大流标程

·费用流

  正确性证明

  dijkstra费用流

  标程

·上下界最大流

 

 

 

网络流简介

有一个有向图,每条边为一条管道,有固定的容量

从一个源点出发,它可以提供无限的水流,

流到一个汇点,它可以接受无限的水流,

每条边可以有一些水流通过,水流的大小不能超过容量,

我们把一个合法的解叫做一个,一条边的水流大小叫做流量。

定义一条边的残量,是指它还能流多少流量(即容量减去当前流量)

性质:

1.容量限制:每条边的流量不超过其容量。

2.流量平衡:除源点和汇点外,对于每个点,流入它的流量和等于从它流出的流量和。

最大流:就是最大的流

最大化整个流的流量 ⇒ 最大化从源点流出的流量。

 

 

求解最大流

一个贪心的思路:

每次从残量网络中任意找一条到达汇点路径,直到没有可以到达汇点的路径

然而它是错的

 

显然如果走了红色的路径,就不能再走其他的路径了

然而最优解是走上下两条路径。。

那么我们考虑“反悔操作”

我们想让水流“回去”,从而找到更优的解

一个可行的方法是建反向边

每次进行一次增流后,将正向边残联-流量,反向边容量+流量

显然,在反向边上增流,就相当于正向边上的流量减少,而且

总的残量是在不断减小的


上面的例子在找到1->2->3->4的路径后,图变为了这样:

那么我们就又有了1->3->2->4一条路径,对它进行增广

于是就有了2的流量

这就是FF方法

1.在残量网络上找到一条从源点到汇点的道路(称为“增广路”)

2.取增广路上残量最小值v

3.将答案加上v

4.将增广路上所有边的残量减去v ,它们的反向边的残量加上 v。

由于残量网络不断在减小,它是一定会结束的

正确性之后会有证明

于是我们就可以为所欲为了

 

然而有时它会被卡

 

若走了1->2->3->4,

于是变成了这样

 

(明显抠图痕迹)

于是又有了一条1->3->2->4的路径

然后。。就被卡到了1100000000次

如何避免这种情况?

我们看一下EK算法:

——每次寻找最短增广路增广

每次增广的道路长度显然是不下降的,

所以不会被上面的极端情况卡掉

寻找最短路BFS即可 

其复杂度是 O(m^2 * n)的

 

 

最小割

选出一些边的集合,

使得删除它们之后从源点无法到达汇点,

那么这个集合就叫做一个

这些边的容量之和称作这个割的容量。

最小割就是一个容量最小的割边集合

显然 ,任取一个割,其割的容量大于最大流的流量

最小割的容量等于最大流的流量,且FF方法能够正确的求出它

考虑 FF 算法结束时,残量网络上没有了增广路。

那么我们假设这时候,从源点经过残量网络能到达的点组成的集合为 X , 不能到达的点为 Y 。

显然汇点在 Y 里,并且残量网络上没有从 X 到 Y 的边。

可以发现以下事实成立:

1.Y 到 X 的边流量为 0 。如果流量不为 0 那么应该存在一条从 X 到 Y 的反向边,于是矛盾。

2.X 到 Y 的边流量等于其容量。只有这样它才不会在残量网络中出现。

根据第1个条件,我们可以得知:没有流量从 X 到 Y 之后又回到了 X 。

所以当前流量应该等于从 X 到 Y 的边的流量之和;

根据第2个条件,它又等于从 X 到 Y 的边容量之和。

所以FF方法也证明了是正确的。。

 

既然已经证明van了,我们进入下一环节

 

 

Dinic

EK算法每增广一条道路都要进行一次bfs,显然是比较慢的

考虑EK算法的优化:若增广后源点到汇点的最短路长度不变,不需要再bfs,直到最短路的长度变大再bfs

一次bfs找多个增广路径

增广时用dfs,记录下当前路径能允许的最大的流量,到达汇点时就统计

 

 

前弧优化:

若在一次dfs中一些边已经访问过,在本次增广中就不会再产生新的贡献

于是我们直接跳过访问过的边

对于每个点维护一个“当前弧”cur[x]

表示从它开始的第一个可能可以增广的边是谁

for循环访问后将cur[x]置为最后的边i

每次for循环从cur[x]开始,而不是head[x]

bfs时将cur[x]置为head[x]

 

 模板最大流Dinic+当前弧优化完整代码:

 

 1 #include<iostream>
 2 #include<cstring>
 3 #include<cstdio>
 4 
 5 #define N 10010
 6 #define M 200010
 7 #define INF 0x3f3f3f3f
 8 #define min(a,b) (a<b?a:b)
 9 
10 const int ch_top=4e7+3;
11 char ch[ch_top],*now_r=ch-1;
12 
13 int n,m,s,t,ans;
14 
15 int Head[N],to[M],w[M],next[M],num=1;
16 
17 int deep[N],cur[N],que[N],head,tail;
18 
19 inline void add(int x,int y,int v){
20     to[++num]=y;
21     w[num]=v;
22     next[num]=Head[x];
23     Head[x]=num;
24     to[++num]=x;
25     w[num]=0;
26     next[num]=Head[y];
27     Head[y]=num;
28 }
29 
30 inline int read(){
31     while(*++now_r<'0');
32     register int x=*now_r-'0';
33     while(*++now_r>='0')x=x*10+*now_r-'0';
34     return x;
35 }
36 
37 bool bfs(){
38     memset(deep,-1,sizeof(deep));
39     head=tail=0;
40     deep[s]=0;
41     que[++tail]=s;
42     while(head<tail){
43         int u=que[++head]; cur[u]=Head[u];
44         for(int i=Head[u];i;i=next[i]){
45             int v=to[i];
46             if(deep[v]!=-1||!w[i]) continue;
47             deep[v]=deep[u]+1;
48             que[++tail]=v;
49             if(v==t) return 1;
50         }
51     }
52     return 0;
53 }
54 
55 int dfs(int now,int cap){
56     if(now==t||!cap) return cap;
57     int flow=0,f;
58     for(int i=cur[now];i;cur[now]=i=next[i]){
59         int v=to[i];
60         if(!w[i]||deep[v]!=deep[now]+1) continue;
61         if(!(f=dfs(v,min(cap,w[i])))) continue;
62         w[i]-=f;
63         cap-=f;
64         w[i^1]+=f;
65         flow+=f;
66         if(!cap) break;
67     }
68     return flow;
69 }
70 
71 void Dinic(){
72     while(bfs())
73      ans+=dfs(s,INF);
74 }
75 
76 int main()
77 {
78     fread(ch,1,ch_top,stdin);
79     n=read(); m=read(); s=read(); t=read();
80     int x,y,v;
81     for(int i=1;i<=m;i++){
82         x=read(); y=read(); v=read();
83         add(x,y,v);
84     }
85     Dinic();
86     printf("%d\n",ans);
87     return 0;
88 } 
View Code

 

 

 

 

 

 

 

费用流

现在每个管道使用要收费了,

收的费用=该管道收取的单位费用*该管道的流量

在保证最大流的情况下,求最小花费

 

解法:

建边时反向边的费用是正向边费用的相反数

每次按照费用求最短路增广

首先要保证一开始建的图没有负环

下证增广过程中没有负环:

如果增广过程中出现了负环

 

因为一开始没有负环,增广后负环一定是这样的:

图中b+(-a)<0,即出现了负环。

但是,b-a<0,b<a,那么上一次增广时的最短路应该是从u经过d的费用到v,产生矛盾

所以每次增广时选最短路是不会产生负环的。

正确性证明:

当前是最小费用流<=>当前残量网络无负环

因为残量有负环时,从负环上走一圈,可以得到更小的费用

这个东西反过来也是成立的。即如果有更小的解,一定存在一个负环来让我走一圈。

 

因为一般情况下费用相同的最短路不多,我们用EK算法进行增广

代码实现:将EK的bfs改成SPFA即可

 

最小费用最大流模板完整代码

 

 1 #include<iostream>
 2 #include<cstring>
 3 #include<cstdio>
 4 using namespace std;
 5 #define N 5010
 6 #define M 100010
 7 #define INF 0x3f3f3f3f
 8 #define reset(a,i) fill(a,a+1+n,i)
 9 const int ch_top=4e7+3;
10 char ch[ch_top],*now_r=ch-1;
11 inline int read(){
12     while(*++now_r<'0');
13     register int x=*now_r-'0';
14     while(*++now_r>='0')x=(x<<3)+(x<<1)+*now_r-'0';
15     return x;
16 }
17 int n,m,s,t,Max_flow,Min_cost;
18 int Head[N],to[M],w[M],cost[M],next[M],num=1;
19 int pre[N],path[N],dis[N],que[N*100],head,tail;
20 bool inque[N];
21 inline void add(int x,int y,int v,int c){
22     to[++num]=y;
23     w[num]=v;
24     cost[num]=c;
25     next[num]=Head[x];
26     Head[x]=num;
27     to[++num]=x;
28     cost[num]=-c;
29     next[num]=Head[y];
30     Head[y]=num;
31 }
32 bool SPFA(){
33     reset(path,0); reset(pre,0);
34     reset(dis,INF); reset(inque,0);
35     head=tail=0;dis[s]=0;que[++tail]=s;
36     while(head<tail){
37         int u=que[++head]; inque[u]=0;
38         for(int i=Head[u];i;i=next[i]){
39             int v=to[i];
40             if(!w[i]||dis[v]<=dis[u]+cost[i]) continue;
41             dis[v]=dis[u]+cost[i];
42             pre[v]=u;path[v]=i;
43             if(!inque[v]) inque[v]=1,que[++tail]=v;
44         }
45     }
46     if(pre[t]) return 1;
47     return 0;
48 }
49 void EK(){
50     while(SPFA()){
51         int f=INF;
52         for(int i=t;i!=s;i=pre[i])
53          if(w[path[i]]<f) f=w[path[i]];
54         Max_flow+=f;
55         Min_cost+=dis[t]*f;
56         for(int i=t;i!=s;i=pre[i])
57          w[path[i]]-=f,w[path[i]^1]+=f;
58     }
59 }
60 int main()
61 {
62     fread(ch,1,ch_top,stdin);
63     n=read(); m=read();
64     s=read(); t=read();
65     int x,y,v,c;
66     for(int i=1;i<=m;i++){
67         x=read(); y=read();
68         v=read(); c=read();
69         add(x,y,v,c);
70     }
71     EK();
72     printf("%d %d\n",Max_flow,Min_cost);
73     return 0;
74 }
View Code

 

 

 

 

 

 

 

然而SPFA被卡是标准操作

能不能用dijkstra求最短路呢?

dijkstra不能处理负权边,于是我们要想办法将负权边搞成非负的

考虑给每个点加一个“势”h[i],可以看做一个“高度”

在求最短路时将两点之间的费用看做cost[u][v]+h[u]-h[v]

我们可以发现,在一条从S到T的路径上,

除了S和T,其他点的“势”h[i]都在这个点的入边上减了一次,又在出边上加了一次

每个最短路都加上了h[S]-h[T],原来的最短路还是最短的

我们怎么求出这个h,是的所有的负边都成非负的呢?

设每条边的费用为ci,我们要让ci+h[u]-h[v]>=0

变形后:h[v]<=h[u]+ci

这。。貌似就是最短路。。

但是我们不能用最短路,因为我们正要求最短路。。

我们考虑用上一次的最短路作为h数组

这是可行的,下面给出证明:

若一条增广前残量不为0的边在增广后的残量仍不为零,

它仍然满足最短路的性质,dis[v]<=dis[u]+ci

若一条增广前残量为0的边在增广后残量不为零,

说明它的反向边被增广了,而且是最短路径

原先为一条u到v的边dis[v]=dis[u]+ci(因为是最短路上的边)

现在为一条v到u的边,边权为-ci

dis[u]=dis[v]-ci,dis[v]-dis[u]-ci=0,也是非负的

我们用前一次的dis作为当前的h数组时,因为dis[i]的值实际上是多了h[s]-h[i]的

所以h[i]=dis[i]-h[s]+h[i],而h[s]一直是0,所以可以写成h[i]+=dis[i]

 

dijkstra最小费用最大流模板完整代码

 

 1 #include<algorithm>
 2 #include<cstdio>
 3 #include<queue>
 4 #define N 5010
 5 #define M 100010
 6 #define INF 0x3f3f3f3f
 7 #define reset(a,i) std::fill(a,a+1+n,i)
 8 const int ch_top=4e7+3;
 9 char ch[ch_top],*now_r=ch-1;
10 inline int read(){
11     while(*++now_r<'0');
12     register int x=*now_r-'0';
13     while(*++now_r>='0')x=(x<<3)+(x<<1)+*now_r-'0';
14     return x;
15 }
16 struct HA{
17     int pos,cost;
18 };
19 struct cmp{
20     inline bool operator()(HA a,HA b){
21         return a.cost>b.cost;
22     }
23 };
24 std::priority_queue<HA,std::vector<HA>,cmp > q;
25 int n,m,s,t,Max_flow,Min_cost;
26 int Head[N],to[M],w[M],cost[M],next[M],num=1;
27 int pre[N],path[N],dis[N],h[N];
28 bool vis[N];
29 inline void add(int x,int y,int v,int c){
30     to[++num]=y;
31     w[num]=v;
32     cost[num]=c;
33     next[num]=Head[x];
34     Head[x]=num;
35     to[++num]=x;
36     cost[num]=-c;
37     next[num]=Head[y];
38     Head[y]=num;
39 }
40 bool dijkstra(){
41     for(int i=1;i<=n;i++)h[i]+=dis[i];    //将上一次的dis加到h中 
42     reset(pre,0); reset(dis,INF);
43     reset(vis,0); dis[s]=0;
44     q.push((HA){s,0});
45     while(!q.empty()){
46         int u=q.top().pos; q.pop();
47         if(vis[u]) continue; vis[u]=1;
48         for(int i=Head[u];i;i=next[i]){
49             int v=to[i];
50             if(!w[i]||vis[v]||dis[v]
51             <=dis[u]+h[u]-h[v]+cost[i]) //边权看做h[u]-h[v]+cost[i] 
52             continue;
53             dis[v]=dis[u]+h[u]-h[v]+cost[i];
54             pre[v]=u;path[v]=i;
55             q.push((HA){v,dis[v]});
56         }
57     }
58     if(pre[t]) return 1;
59     return 0;
60 }
61 void EK(){
62     while(dijkstra()){
63         int f=INF;
64         for(int i=t;i!=s;i=pre[i])
65          if(w[path[i]]<f) f=w[path[i]];
66         Max_flow+=f;
67         Min_cost+=(dis[t]-h[s]+h[t])*f;    //dis[t]多了h[u]-h[t] 
68         for(int i=t;i!=s;i=pre[i])
69          w[path[i]]-=f,w[path[i]^1]+=f;
70     }
71 }
72 int main()
73 {
74     fread(ch,1,ch_top,stdin);
75     n=read(); m=read();
76     s=read(); t=read();
77     int x,y,v,c;
78     for(int i=1;i<=m;i++){
79         x=read(); y=read();
80         v=read(); c=read();
81         add(x,y,v,c);
82     }
83     EK();
84     printf("%d %d\n",Max_flow,Min_cost);
85     return 0;
86 }
View Code

 

 

 

 

 

上下界最大流问题

每条边除了容量,还限制了最小的流量

我们考虑把一条边拆开:

不妨设ei(u,v,l,h) 表示一条边ei起点为u,终点为v,最小流量限制为l,容量为h;

 

建一个超级源S和超级汇T,

将ei拆成三条边:

一条普通边从u到v,容量为h-l,即为最小限制之外的容量;

另一条边从S到u,容量为l;

另一条边从v到T,容量为l;

 

显然这三条边是和原来等价的,

S到T跑一遍最大流

在保证有解的情况下,S连出去的边和连入T的边一定都是满的,

否则无法满足最小流量的限制

再删去S和T

从原来的s和t跑一遍最大流,两次的值相加即可。

 

posted @ 2018-08-07 11:46  yjk  阅读(408)  评论(0编辑  收藏  举报