网络流-最大流
定义
给定一个网络,最大流就是求出在这个网络的所有流中流量最大的流。
Frod-Fulkerson
增广路
如果一个路径满足以下要求,那么它就是一个增广路:
- 其起点与终点分别是源点
和汇点 。 - 对于这个路径中所有边
的流量剩余容量 。
求解方法
对于一种暴力的方法,我们可以一直寻找增广路知道寻找不到为止。在找到一条增广路之后,计算出路径上剩余容量的最小值
但是有一个问题,这样在某些情况下只能找到一些较优解:
显而易见的,如果使用
可是在使用直接 dfs 进行搜索之后,可能会得到一个为
为了解决这个问题,我们可以考虑一种类似反悔贪心的手段避免上面情况发生。在每一次找到增广路时,在将这面走的剩余流量减少
同样是上面的问题,在建出给定的网络之后我们又为每一条边建一个反边:
接着,在直接 dfs 得到一条增广路
继续寻找新的增广路,得到
因为两次遍历及经过了
在使用 dfs 不断寻找增广路知道不存在是,Frod-Fulkerson 算法就完成了,这个网络的最大流就是增广路的贡献。
假设流量为
实现
bool bfs(int rGraph[V][V], int s, int t, int parent[])
{
bool visited[V];
memset(visited, 0, sizeof(visited))
queue<int> q;
q.push(s);
visited[s] = true;
parent[s] = -1;
// Standard BFS Loop
int u;
while (!q.empty())
{
// edge: u -> v
u = q.front(); // head point u
q.pop();
for (int v = 0; v < V; ++v) // tail point v
{
if (!visited[v] && rGraph[u][v] > 0) // find one linked vertex
{
q.push(v);
parent[v] = u; // find pre point
visited[v] = true;
}
}
}
return visited[t] == true;
}
int fordFulkerson(int graph[V][V], int s, int t)
{
int u, v,rGraph[V][V];
for (u = 0; u < V; ++u)
{
for (v = 0; v < V; ++v)
{
rGraph[u][v] = graph[u][v];
}
}
int parent[V],max_flow = 0;
while (bfs(rGraph, s, t, parent))
{
// edge: u -> v
int path_flow = INT_MAX;
for (v = t; v != s; v = parent[v])
{
// find the minimum flow
u = parent[v];
path_flow = min(path_flow, rGraph[u][v]);
}
// update residual capacities of the edges and reverse edges along the path
for (v = t; v != s; v = parent[v])
{
u = parent[v];
rGraph[u][v] -= path_flow;
rGraph[v][u] += path_flow; // assuming v->u weight is add path_flow
}
max_flow += path_flow;
}
return max_flow;
}
Edmonds-Karp
求解方法
简单的说,Edmonds-Karp 就是在 Frod-Fulkerson 的基础上,将栈改为了队列,或者说将 dfs 实现改为了 bfs 实现。虽然其表面上只是更改了实现方式,但是这也是为 Frod-Fulkerson 添加了一个优化:每次增广最短的增广路。这个优化其实很重要,因为在优化之后其最劣复杂度就被控制在了
时间复杂度证明
在找到一条增广路之后,我们可定需要将可以压榨的全部榨干,也就是肯定有一条边被跑慢,我们定义这一条边为关键边。所以经过增广操作,这些边因为剩余流量为
因为每一次将一条增广路破坏之后都至少有
因为关键边会从残量网络中消失,直到反向边上有了流量才会恢复成为关键边的可能性,而当反向边上有流量时最短路长度一定会增加。显而易见,增广路的长度肯定位于
因为在 Edmonds-Karp 中计算残量网络的方法为 bfs 其时间复杂度为
实现
int Bfs() {
memset(pre, -1, sizeof(pre));
for(int i = 1 ; i <= n ; ++ i) flow[i] = INF;
queue <int> q;
pre[S] = 0, q.push(S);
while(!q.empty()) {
int op = q.front(); q.pop();
for(int i = 1 ; i <= n ; ++ i) {
if(i==S||pre[i]!=-1||c[op][i]==0) continue;
pre[i] = op; //找到未遍历过的点
flow[i] = min(flow[op], c[op][i]); // 更新路径上的最小值
q.push(i);
}
}
if(flow[T]==INF) return -1;
return flow[T];
}
int Solve() {
int ans = 0;
while(true) {
int k = Bfs();
if(k==-1) break;
ans += k;
int nw = T;
while(nw!=S) {//更新残余网络
c[pre[nw]][nw] -= k, c[nw][pre[nw]] += k;
nw = pre[nw];
}
}
return ans;
}
总结
虽然比起 Frod-Fulkerson 来说 Edmonds-Karp 已经优秀了许多,但是因为
经过分析容易发现,在 Edmonds-Karp 求解了一次增广路之后它都要重新计算一次残量网络,十分耗费时间。如果可以将在一个网络中所有的增广矩阵在一次中全部求解出来在再进行增广操作,那么效果可能就会得到一个优化。
Dinic
分层图
我们可以定义一个叫分层图的概念:如果
理论上的求解方法
对于下方的这个网络,在所有边的边权均为
可以得到
因为反向边都不在最短路上,所以我们可以将他们忽视。同时在一层中的路线对于最短路也是没有贡献的,所以将他们也删除。通过这些优化,得到下面的一张图。
在分层结束之后,所有的剩余流量非
阻塞流
Dinic 的关键就是阻塞流,因为每次进行增广操作我们都会将所有的增广路,所以说与 Edmonds-Karp 算法一样,在进行了一次操作时候深度一样的所有增广路都被破坏了,就像下图。
先这样的破坏增广路,我们就称之为阻塞流。
理论上的时间复杂度
因为每一次操作都会将所有的增广路全部榨干,所以增广路的长度必然是严格递增的,所以一共只会进行
理论上的实现
int Dinic(int s,int t){
int ans=0;
while(BFS(s,t))
ans+=DFS(s,INF,t);
return ans;
}
int DFS(int s,int flow,int t){
if(s==t||flow<=0)
return flow;
int rest=flow;
for(Edge* i=head[s];i!=NULL&&rest>0;i=i->next){
if(i->flow>0&&depth[i->to]==depth[s]+1){
int k=DFS(i->to,std::min(rest,i->flow),t);
rest-=k;
i->flow-=k;
i->rev->flow+=k;
}
}
return flow-rest;
}
bool BFS(int s,int t){
memset(depth,0,sizeof(depth));
std::queue<int> q;
q.push(s);
depth[s]=1;
while(!q.empty()){
s=q.front();
q.pop();
for(Edge* i=head[s];i!=NULL;i=i->next){
if(i->flow>0&&depth[i->to]==0){
depth[i->to]=depth[s]+1;
if(i->to==t)
return true;
q.push(i->to);
}
}
}
return false;
}
dfs 函数中,
问题
在增广路的同时我们还面临了一个问题没使用阻塞 dfs 求解增广路其实与使用 bfs 求解的 Edmonds-Karp 完全不同,因为使用阻塞 dfs 会导致一些点需要重复计算,所以在写 dfs 的时候不可以打
对于下图,在某些特定的流量设计下
因为 dfs 不使用
实现
#include<iostream>
#include<vector>
#include<queue>
#include<cstring>
#define int long long
using namespace std;
const int N=205,inf=0x3f3f3f3f3f3f3f3f;
struct node{int x,v,id;};
vector<node> v[N];
int n,m,s,t,dep[N],p[N];
void add(int x,int y,int val){
int sti=v[x].size(),edi=v[y].size();
v[x].push_back({y,val,edi});
v[y].push_back({x,0,sti});
}
bool bfs(){
queue<int> q;
memset(dep,-1,sizeof(dep));
memset(p,0,sizeof(p));
q.push(s),dep[s]=0;
while(!q.empty()){
int top=q.front();q.pop();
for(node i:v[top]){
if(dep[i.x]==-1&&i.v){
dep[i.x]=dep[top]+1;
q.push(i.x);
}
}
}
return dep[t]!=-1;
}
int dfs(int x,int flow){
if(x==t){
return flow;
}
for(int i=p[x];i<v[x].size();i++){
p[x]=i;
int to=v[x][i].x,len=v[x][i].v;
if(dep[x]+1==dep[to]&&len){
int t=dfs(to,min(len,flow));
if(t){
v[x][i].v-=t;
v[to][v[x][i].id].v+=t;
return t;
}
else{
dep[to]=-1;
}
}
}
return 0;
}
int dinic(){
int ans=0,flow;
while(bfs()){
while(flow=dfs(s,inf)){
ans+=flow;
}
}
return ans;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n>>m>>s>>t;
for(int i=1,x,y,val;i<=m;i++){
cin>>x>>y>>val;
add(x,y,val);
}
cout<<dinic()<<'\n';
return 0;
}
真实的求解
首先需要明确,将
然后最关键的决定时间复杂度的优化是当前弧优化,我们每次向某条边的方向推流的时候,肯定要么把推送量用完了,么是把这个方向的容量榨干了。
除了最后一条因为推送量用完而无法继续增广的边之外其他的边一定无法继续传递流量给
最后再看重新这一整个 DFS 的过程,如果当前路径的最后一个点可以继续扩展,则肯定是在层间向汇点前进了一步,最多走
于是 Dinic 算法的总时间复杂度是
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?