网络流
网络流
最大流
网络中有两台计算机s和t,现在想从s传输数据到t。称使得传输量最大的f为最大流,而求解最大流的问题为最大流问题。此外,我们称c为边的容量,f为边的流量,s为源点(source),t为汇点(sink)。
Ford-Fulkerson算法
- 1️⃣只利用满足f(e)<c(e)的e或者2️⃣满足f(e)>0的e对应的反向边rev(e),寻找一条s到t的路径。
- 如果不存在满足条件的路径,则结束。否则,沿着该路径尽可能地增加流,返回第1步
称1中所考虑的满足f(e)<c(e)的e和满足f(e)>0的 e对应的反向边rev(e)所组成的图为残余网络,并称残余网络上的 s-t 路径为增广路。
当然,看完这个肯定是一脸懵逼,不要放弃,懵懵懂懂总比完全不懂好😊
(2019.7.16号:再看感觉有点懂了,理解书上P211上方的贪心算法和正确算法的流量的差,其核心是每次找s到t的路径时,s到t的总路径都是增加的,所以无论是通过1️⃣方式还是2️⃣方式找到)
- 前向弧:弧的方向与路的方向一致,前向弧的全体记为P+
- 后项弧:弧的反向与路的方向相反,后项弧的全体记为P-
- 设f是一个可行流,P是从s到t的一条路,若P满足一下条件:
- P中所有的前向弧都是非饱和弧
- P中所有的后向弧都是非零弧
则称P为关于可行流f的一条增广路
- 残余网络:有增广路的网络
- 增广路定理:当且仅当残余网络中没有增广路时,网络达到最大流
形象图解
- 初始网络(求 A\to C的最大流)
- 若从若从A\to B\to D\to C开始贪心,最终结果为4
- 每增广一次,就建立一条反向边,在对A\to B\to D\to C增广后:
注意这里的A′B′C′D并不是虚点,而是ABCD本身,这里只是为了方便画图和观察所以分开画了。
我们发现A\to D\to B\to C变得可增广了:
继续扫一遍,发现A\to D\to C还可以增广,于是最终结果为6:
Ford-Fulkerson算法
下面是一个Ford-Fulkerson算法的邻接表实现的例子。这里没有保存f(e)的值,取而代之的是直接改变c(e)的值。
记最大流的流量为F,那么Ford-Fulkerson算法最多进行F次深度优先搜索,所以其复杂度为O(F|E|)。不过这是一个很松的上界,达到这种最坏复杂度的情况几乎不存在。所以在多数情况下,即便通过估算得到的复杂度偏高,实际运用当中也是比较快的。
//用于表示边的结构体(终点、容量、反向边)
struct edge
{
int to,cap,rev;//这里的rev表示它对应的反向边的标号
};
vector<edge> G[MAX_V];//图的邻接表表示
bool used[MAX_V];//DFS中用到的访问标记
//向图中增加一条从s到t容量为cap的边
void add_edge(int from ,int to,int cap){
G[from].push_back((edge){to,cap,G[to].size()});
/*
即G[from][j]对应的反向边在G[to]中是第当前的G[to].size()个
如当前G[from]增加了一个边,记录它的反向边为G[to]中的第G[to].size(),、
然后在下一条语句中,就会将该边加入到G[to]当中
*/
G[to].push_back((edge){from,0,G[from].size()-1});
}
//通过DFS寻找增广路
int dfs(int v,int t,int f){
if(v==t) return f;
used[v]=true;//v为起点,已使用过
for(int i=0;i<G[v].size();i++){
edge &e=G[v][i];
if(!used[e.to] && e.cap>0){
int d=dfs(e.to,t,min(f,e.cap));
if(d>0){
e.cap-=d;
G[e.to][e.rev].cap+=d;
return d;
}
}
}
return 0;
}
//求解从s到t的最大流
int max_flow(int s,int t){
int flow=0;
for(;;){
memset(used,0,sizeof(used));
int f=dfs(s,t,INF);
if(f==0) return flow;
flow+=f;
}
}Dinic算法
Ford-Fulkerson算法是通过深度深度优先搜索寻找增广路,并沿它着它增广。与之相对,Dinic算法总是寻找最短的增广路,并沿着它增广。因为最短增广路的长度在增广过程中始终不会变短,所以无需每次都通过宽度预先搜索来寻找最短增广路。我们可以先进行一次宽度优先搜索,然后考虑由近距离顶点指向远距离顶点的边所组成的分层图,在上面进行深度优先搜索寻找最短增广路。如果在分层图上找不到新的增广路了,则说明最短增广路的长度确实变长了,或不存在增广路了,于是重新通过宽度优先搜索构造新的分层图。每一步构造分层图的复杂度为O(|E|),而每一步完成之后最短路的长度都会至少增加1,由于增广路的长度不会超过|V|-1,因此最多重复O(|V|)步就可以了。
struct edge{
int to,cap,rev;
};
vector<edge> G[MAX_V];
int level[MAX_V];//顶点到源点的距离标号
int iter[MAX_V];//当前弧,在其之前的边已经没有用了
//向图中增加一条从from到to的容量为cap的边
void add_edge(int from,int to,int cap){
G[from].push_back(edge{to,cap,G[to].size()});
G[to].push_back(edge{from,0,G[from].size()-1});
}
//通过BFS计算从源点出发的距离编号(这样可以在DFS跑最短路时指明方向)
void bfs(int s){
memset(level,-1,sizeof(level));
queue<int> que;
level[s]=0;
que.push(s);
while(!que.empty()){
int v=que.front();que.pop();
for(int i=0;i<G[v].size();i++){
edge &e=G[v][i];
if(e.cap>0&&level[e.to]<0){//加入最短路中的条件:1.没有访问过的 2.边还可增广的
level[e.to]=level[v]+1;
que.push(e.to);
}
}
}
}
//通过DFS寻找增广路
int dfs(int v,int t,int f){
if(v==t) return f;
for(int &i=iter[v];i<G[v].size();i++){//也会发生iter[v]++,这样不会每次都从0开始,是弧优化
edge &e=G[v][i];
if(e.cap>0&&level[v]<level[e.to]){
int d=dfs(e.to,t,min(f,e.cap));//边e能通过的最大流
if(d>0){
e.cap-=d;
G[e.to][e.rev].cap+=d;
return d;
}
}
}
return 0;
}
//求解从s到t的最大流
int max_flow(int s,int t){
int flow=0;
for(;;){//每次最短增广路的长度都会增加1,由于增广路的长度不会超过|V|-1,循环次数为O(|V|)
bfs(s);//即BFS的复杂度为O(|E|)
if(level[t]<0) return flow;//说明没有可到达t的增广路
memset(iter,0,sizeof(iter));
while((f=dfs(s,t,INF))>0) flow+=f;//复杂度为O(|E||V|)
}
}最大流的各种变体
- 多个源点和汇点的情况
如果有多个源点和汇点,并且它们都有对应的最大流出容量和流入容量限制时,增加一个超级源点s和超级汇点t,从s向每一个源点连一条容量为对应最大流出容量的边,从每一个汇点向t连一条容量为对应最大流入容量的边。 - 无向图的情况
考虑无向图的情况,此时容量表示的是两个方向流量之和的上界。最大流中没必要在两个方向都有流量,因此把无向图中容量为c的一条边当作有向图中两个方向各有一条容量为c的两条边,就能够得到同样的效果。 - 顶点上也有容量限制的情况
将每个顶点拆成两个。拆点之后得到入顶点和出顶点,再从入顶点向出顶点连容量为原先顶点限制的边,就可以把顶点上的容量限制转为边上的容量限制了。
最小割
所谓图的割,指的是对于某个顶点集合S\subset V,从S出发指向S外部的那些边的集合,记为割(S,V\backslash S)。这些边的容量之和被称为割的容量。如果有s\in S,而t\in V\backslash S,那么这时的割又称为s-t割。如果将网络中s-t割所包含的边都删去,也就不会有从s到t的路径了。因此,可以考虑一下如下问题。
- 对于给定网络,为了保证没有从s到t的路径,需要删除的边的总容量的最小值是多少?
该问题有被称为最小割问题。
首先,让我们考虑一下任意的s-t流f和任意的s-t流(S,V\S)。因为有(f的流量)=(s的出边的总流量),而对v\subset S\backslash {s}又有(v的出边的总流量)=(v的入边的总流量),所以有(f的流量)=(S的出边的总流量)-(S的入边的总流量)。由此可知(f的流量)\leq(割的流量)。
二分图匹配
指派问题
有N台计算机和K个任务。我们可以给每台计算机分配一个任务,每台计算机能够处理的任务种类各不相同。求最多能够处理的任务的个数。
二分图匹配常常在指派问题的模型中出现。
实际上,可以将二分图匹配问题看成是最大流问题的一种特殊情况,将无向边改为有向边,方向从计算机顶点集合U指向任务集合V,容量为1,并增加源点s和汇点t。
#include<bits/stdc++.h>
using namespace std;
int N,K;
bool can[MAXN][MAXK];//can[i][j]:=计算机i能处理任务j
void solve(){
//0-N-1:计算机对应的顶点
//N-N+K-1:任务对应的顶点
int s=N+K,t=s+1;
//在源点和计算机之间连边
for(int i=0;i<N;i++){
add_edge(s,i,1);
}
//在任务和汇点之间连边
for(int i=0;i<K;i++){
add_edge(N+i,t,1);
}
//在计算机和任务之间连边
for(int i=0;i<N;i++){
for(int j=0;j<K;j++){
if(can[i][j]) add_edge(i,N+j,1);
}
}
printf("%d\n",max_flow(s,t));
}最小费用流
最小传输费用
网络中有两台计算机s和t,现在每秒钟要从s传输大小为F的数据到t。该网络中一共有N台计算机,其中一些计算机之间连有一条单向的通信电缆,每条通信电缆都有对应的1秒钟内所能传输的最大数据量x和单位传输费用d,需要花费的费用为dx。问传输数据所需的最小费用。
#include<iostream>
#include<vector>
#include<cstring>
using namespace std;
#define INF 0x3f3f3f3f
#define MAXV 1000
//表示边的结构体(终点、容量、费用、反向边)
struct edge
{
int to,cap,cost,rev;
};
int V;//顶点数
vector<edge> G[MAXV];//图的邻接表表示
int dist[MAXV];//最短距离
int prevv[MAXV],preve[MAXV];//最短路中的前驱节点和对应的边
//向图中增加一条从from到to容量为cap费用为cost的边
void add_edge(int from,int to,int cap,int cost){
G[from].push_back((edge){to,cap,cost,G[to].size()});
G[to].push_back((edge){from,0,-cost,G[from].size()-1});
}
//求解从s到t流量为f的最小费用流
//如果不能再增广则返回-1
int min_cost_flow(int s,int t,int f){
int res=0;
while(f>0){
//利用Bellman-Ford算法求从s到t的最短路
fill(dist,dist+V,INF);
dist[s]=0;
bool update=true;
while(update){
update=false;
for(int v=0;v<V;v++){
if(dist[v]==INF) continue;
for(int i=0;i<G[v].size();i++){
edge &e=G[v][i];
if(e.cap>0&&dist[e.to]>dist[v]+e.cost){
dist[e.to]=dist[v]+e.cost;
prevv[e.to]=v;
preve[e.to]=i;
update=true;
}
}
}
}
if(dist[t]==INF){
///不能再增广
return -1;
}
//沿s到t的最短路尽量增广
int d=f;
for(int v=t;v!=s;v=prevv[v]){
d=min(d,G[prevv[v]][preve[v]].cap);
}
f-=d;
res+=d*dist[t];
for(int v=t;v!=s;v=prevv[v]){
edge &e=G[prevv[v]][preve[v]];
e.cap-=d;
G[v][e.rev].cap+=d;
}
}
return res;
}版子题: POJ-3068 "Shortest" pair of paths
{% fold 查看代码%}
#include<iostream>
#include<vector>
#include<cstring>
using namespace std;
#define INF 0x3f3f3f3f
#define MAXV 200
//表示边的结构体(终点、容量、费用、反向边)
struct edge
{
int to,cap,cost,rev;
};
int V;//顶点数
vector<edge> G[MAXV];//图的邻接表表示
int dist[MAXV];//最短距离
int prevv[MAXV],preve[MAXV];//最短路中的前驱节点和对应的边
//向图中增加一条从from到to容量为cap费用为cost的边
void add_edge(int from,int to,int cap,int cost){
G[from].push_back((edge){to,cap,cost,G[to].size()});
G[to].push_back((edge){from,0,-cost,G[from].size()-1});
}
//求解从s到t流量为f的最小费用流
//如果不能再增广则返回-1
int min_cost_flow(int s,int t,int f){
int res=0;
while(f>0){
//利用Bellman-Ford算法求从s到t的最短路
fill(dist+1,dist+2*V,INF);
dist[s]=0;
bool update=true;
while(update){
update=false;
for(int v=1;v<2*V;v++){
if(dist[v]==INF) continue;
for(int i=0;i<G[v].size();i++){
edge &e=G[v][i];
if(e.cap>0&&dist[e.to]>dist[v]+e.cost){
dist[e.to]=dist[v]+e.cost;
prevv[e.to]=v;
preve[e.to]=i;
update=true;
}
}
}
}
if(dist[t]==INF){
///不能再增广
return -1;
}
//沿s到t的最短路尽量增广
int d=f;
for(int v=t;v!=s;v=prevv[v]){
d=min(d,G[prevv[v]][preve[v]].cap);
}
f-=d;
res+=d*dist[t];
for(int v=t;v!=s;v=prevv[v]){
edge &e=G[prevv[v]][preve[v]];
e.cap-=d;
G[v][e.rev].cap+=d;
}
}
return res;
}
int main(){
int m;
int t=1;
while(cin>>V>>m&&V+m!=0){
memset(G,0,sizeof(G));
memset(dist,0,sizeof(dist));
memset(prevv,0,sizeof(prevv));
memset(preve,0,sizeof(preve));
int from,to,cap,cost;
for(int i=0;i<m;i++){
cin>>from>>to>>cost;
add_edge(from*2+1,to*2,1,cost);
add_edge(to*2,to*2+1,1,0);
}
cout<<"Instance #"<<t++<<": ";
int res=min_cost_flow(1,2*V-1,2);
if(res==-1) cout<<"Not possible"<<endl;
else cout<<res<<endl;
}
return 0;
}{% endfold %}
网络流24题
P2756 飞行员配对方案问题
经典二分图匹配问题:
解题报告1:Ford-Fulkerson算法+增加超级源点超级汇点
解题报告2:匈牙利算法
查看24道真题和解析