EK算法笔记

本文详细介绍了增广路算法的思想和EK算法在求解网络流问题中的应用,包括HDU3549、HDU1532和POJ1149等实例,并探讨了其时间复杂度。通过实例演示,帮助理解算法核心与优化技巧。

算法思想

这里就不赘述网络流的相关概念,之后会出一篇网络流总结来具体阐述

增广路算法

增广路算法是对给定的网络求得最大流的一种思想,它并不是具体的算法,其余的最大流算法是增广路算法的具体实现与演变

增广路定理:设flow是网络G的一个可行流,若不存在从源点到汇点的增广路,则flow是G的一个最大流

增广路算法的基本思想:在残余网络中找增广路,然后在实流网络中沿增广路增流,在残余网络中沿增广路减流,重复直到不存在增广路为止,最后根据定理,求得的实流网络的可行流即最大流

EK

EK算法属于增广路算法的一种实现,用来求解最大流

EK算法的基本思路基于BFS,不如说只是在多次BFS上加上了回溯与更新,使用队列存放已访问但未回溯的点,使用数组标记访问与增广路的前驱

算法步骤

  1. 初始化各必要变量值
  2. 将源点入队,标记源点访问
  3. 判断队列是否为空,空则结束,根据当前实流网络返回最大流值
  4. 获取队首,在残余网络中检查队首邻接点,若邻接点 i i i未访问,则置访问标记,前驱,如果 i = = t i==t i==t,表示已经找到一条增广路,跳转5,否则 i i i入队,执行3
  5. 从汇点开始,通过前驱数组在残余网络中逆向找增广路上每条边值的最小化值
  6. 更新实流网络和残余网络(增流和减流),更新最大流,跳2

找增广路
在混合网络中BFS,从源点s开始,搜索s的邻接点v,若未被访问,则标记已访问且记录v的前驱为u,若v不为汇点,则v入队,继续BFS,当v为汇点,则认为找到一条增广路,队列空则无增广路

代码

struct node {
    int next,to,cap,flow;//链式前向星,容量和流
} e[maxn];
bool BFS(int s,int t) {
    memset(pre,-1,sizeof(pre));//初始化
    memset(vis,0,sizeof(vis));
    queue<int>q;
    q.push(s);
    vis[s]=1;
    while(!q.empty()) {
        int u=q.front();
        q.pop();
        for(int i=head[u]; ~i; i=e[i].next) {
            int v=e[i].to;
            if(!vis[v]&&e[i].cap>e[i].flow) {//容量大于流,可增流
                vis[v]=1;
                pre[v]=i;
                q.push(v);
                if(v==t)return 1;
            }
        }
    }
    return 0;
}

增流
在找到一条增广路之后,根据前驱数组从汇点向前一直到源点,找增广路边上的 m i n min min,将该值作为增广量,然后从汇点到源点逐个修改,增广路同向边增加,反向边减小
代码

int EK(int s,int t) {
    int acc=0;
    while(BFS(s,t)) {
        int d=0x3f3f3f3f,v=t;
        while(v!=s) {
            int i=pre[v];
            d=min(d,e[i].cap-e[i].flow);//可增加的流量
            v=e[i^1].to;//换点
        }
        acc+=d;
        v=t;
        while(v!=s) {
            int i=pre[v];
            e[i].flow+=d;
            e[i^1].flow-=d;//实流
            v=e[i^1].to;
        }
    }
    return acc;
}

设边数为 E E E,点数为 V V V,在EK算法过程中,找到一条增广路的时间为 O ( E ) O(E) O(E),最多执行 O ( V E ) O(VE) O(VE)次,可以计算出时间复杂度为 O ( V E 2 ) O(VE^2) O(VE2)

训练

HDU3549

题目大意:给一个有向带权图,求解最大流

思路:直接使用EK即可

代码

#include <bits/stdc++.h>
const int maxn=1e4+10;
using namespace std;
int head[maxn],cnt,pre[maxn],T,n,m;
bool vis[maxn];
struct node {
    int next,to,cap,flow;//链式前向星,容量和流
} e[maxn];
void Add(int from,int to,int cap,int flow) {
    e[cnt].next=head[from];
    e[cnt].cap=cap;
    e[cnt].flow=flow;
    e[cnt].to=to;
    head[from]=cnt++;//注意是从0开始存的
}
bool BFS(int s,int t) {
    memset(pre,-1,sizeof(pre));//初始化
    memset(vis,0,sizeof(vis));
    queue<int>q;
    q.push(s);
    vis[s]=1;
    while(!q.empty()) {
        int u=q.front();
        q.pop();
        for(int i=head[u]; ~i; i=e[i].next) {
            int v=e[i].to;
            if(!vis[v]&&e[i].cap>e[i].flow) {//容量大于流,可增流
                vis[v]=1;
                pre[v]=i;//pre记录的是前驱边
                q.push(v);
                if(v==t)return 1;
            }
        }
    }
    return 0;
}
int EK(int s,int t) {
    int acc=0;
    while(BFS(s,t)) {
        int d=0x3f3f3f3f,v=t;
        while(v!=s) {
            int i=pre[v];
            d=min(d,e[i].cap-e[i].flow);//可增加的流量
            v=e[i^1].to;//换点
        }
        acc+=d;
        v=t;
        while(v!=s) {
            int i=pre[v];
            e[i].flow+=d;
            e[i^1].flow-=d;//实流
            v=e[i^1].to;
        }
    }
    return acc;
}
int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >>T;
    for(int i=1; i<=T; i++) {
        memset(head,-1,sizeof(head));
        cnt=0;
        cin >>n>>m;
        while(m--) {
            int x,y,c;
            cin >>x>>y>>c;
            Add(x,y,c,0);
            Add(y,x,0,0);//存反边,容量和流均为0
        }
        cout <<"Case "<<i<<": ";
        cout <<EK(1,n)<<endl;
    }
    return 0;
}
/*
10
6 9
1 2 12
1 3 10
3 2 2
4 3 5
2 4 8
3 5 13
5 4 6
5 6 4
4 6 18
*/

HDU1532

题目大意:略

思路:EK模板题

代码

#include <bits/stdc++.h>
using namespace std;
const int maxn=1e3;
int head[maxn],cnt,pre[maxn],n,m;
bool vis[maxn];
struct node {
    int next,to,cap,flow;
} e[maxn];
void Add(int from,int to,int cap,int flow) {
    e[cnt].to=to;
    e[cnt].next=head[from];
    e[cnt].flow=flow;
    e[cnt].cap=cap;
    head[from]=cnt++;
}
bool BFS(int s,int t) {
    memset(pre,-1,sizeof(pre));
    memset(vis,0,sizeof(vis));
    queue<int>q;
    vis[s]=1;
    q.push(s);
    while(!q.empty()) {
        int u=q.front();
        q.pop();
        for(int i=head[u]; ~i; i=e[i].next) {
            int v=e[i].to;
            if(!vis[v]&&e[i].cap>e[i].flow) {
                vis[v]=1;
                pre[v]=i;
                q.push(v);
                if(v==t)return 1;
            }
        }
    }
    return 0;
}
int EK(int s,int t) {
    int acc=0;
    while(BFS(s,t)) {
        int v=t,d=0x3f3f3f3f;
        while(v!=s) {
            int i=pre[v];
            d=min(d,e[i].cap-e[i].flow);
            v=e[i^1].to;
        }
        acc+=d;
        v=t;
        while(v!=s) {
            int i=pre[v];
            e[i].flow+=d;
            e[i^1].flow-=d;
            v=e[i^1].to;
        }
    }
    return acc;
}
int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    while(cin >>n>>m) {
        memset(head,-1,sizeof(head));
        cnt=0;
        while(n--) {
            int s,e,c;
            cin >>s>>e>>c;
            Add(s,e,c,0);
            Add(e,s,0,0);
        }
        cout <<EK(1,m)<<endl;
    }
    return 0;
}

POJ1149

题目大意:M个猪圈,每个猪圈有初始数量,依次来N个人,每个人可以打开指定的猪圈,购买若干头猪,每个人有一个购买上限,每个人购买完后,猪圈不会立刻关上,管理员可以重新分配已打开的猪圈的猪,分配完后关闭猪圈等待下一个人,求出最多能卖出多少猪

思路:可以看到,如果没有购买次序之间的限制,每个猪圈都可以视为源点,客户设置为汇点,那么可以用一个超级汇点作为所有用户最后的汇点,容量为购买数量,在考虑购买次序之间的限制的情况下,设置一个源点,将源点和每个猪圈的第一个人连边,容量为开始时猪圈猪的数量,因为第一个人才能打开这个猪圈,使得猪圈内猪的数量能发生变化,若两个人先后打开一个相同的猪圈,则连边两人编号,容量为无穷大,因为两人能使猪圈内猪的数量变化

代码

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int maxn=1e4;
const int inf=0x3f3f3f3f;
int head[maxn],cnt,pre[maxn],n,m,init[maxn],last[maxn];
bool vis[maxn];
struct node {
    int to,next,cap,flow;
} e[maxn];
void Add(int from,int to,int cap,int flow) {
    e[cnt].to=to;
    e[cnt].next=head[from];
    e[cnt].cap=cap;
    e[cnt].flow=flow;
    head[from]=cnt++;
}
bool BFS(int s,int t) {
    memset(pre,-1,sizeof(pre));
    memset(vis,0,sizeof(vis));
    queue<int>q;
    q.push(s);
    vis[s]=1;
    while(!q.empty()) {
        int u=q.front();
        q.pop();
        for(int i=head[u]; ~i; i=e[i].next) {
            int v=e[i].to;
            if(!vis[v]&&e[i].cap>e[i].flow) {
                vis[v]=1;
                pre[v]=i;
                q.push(v);
                if(v==t)return 1;
            }
        }
    }
    return 0;
}
int EK(int s,int t) {
    int acc=0;
    while(BFS(s,t)) {
        int v=t,d=inf;
        while(v!=s) {
            int i=pre[v];
            d=min(d,e[i].cap-e[i].flow);
            v=e[i^1].to;
        }
        acc+=d;
        v=t;
        while(v!=s) {
            int i=pre[v];
            e[i].flow+=d;
            e[i^1].flow-=d;
            v=e[i^1].to;
        }
    }
    return acc;
}
int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >>m>>n;
    for(int i=1; i<=m; i++)cin >>init[i];
    memset(head,-1,sizeof(head));
    for(int i=1; i<=n; i++) {
        int a,b;
        cin >>a;
        while(a--) {
            int k;
            cin >>k;
            if(last[k]==0) {//第一次访问
                last[k]=i;//记录上一层访问的客户
                Add(0,i,init[k],0);//与源点相连
                Add(i,0,0,0);
            } else {
                Add(last[k],i,inf,0);
                Add(i,last[k],0,0);
                last[k]=i;//更新上一个访问者
            }
        }
        cin >>b;
        Add(i,n+1,b,0);//需求与汇点相连
        Add(n+1,i,0,0);
    }
    cout <<EK(0,n+1);
    return 0;
}

总结

EK算法是用基于BFS和回溯的最大流算法,它适用于稀疏图,当边较多时,时间消耗会过大,这时应该选择别的算法,对于图论相关问题,如果算法已经确定的情况下,其实求解问题更重要的是如何建图

参考文献

  1. POJ1149最大网络流
  2. 《算法训练营 海量图解+竞赛刷题》
<think>好的,我现在需要帮助用户了解网络流EK算法的实现和应用。首先,我要回顾一下用户的问题,确保自己理解正确。用户提到想了解EK算法的实现和应用,并且之前的引用材料中提到了相关的信息,比如EK算法的核心思想、步骤,以及一些应用场景,比如医生值班问题、资源分配等。 首先,我应该从算法的基础开始。EK算法是Edmonds-Karp的缩写,属于Ford-Fulkerson方法的一种实现,通过BFS寻找增广径,保证了多项式时间复杂度。需要解释清楚算法的基本步骤,比如如何用BFS找最短增广,更新残留网络,反向边的作用等。 接下来是实现的细节。用户可能需要代码示例,但根据引用5中的代码结构,可以整理出算法的框架,包括BFS部分和更新最大流的部分。需要确保代码结构清晰,可能用伪代码或某种编程语言的示例,比如Python或C++,但用户之前的问题中有Python示例,可能更适合用Python。不过要注意,最大流的实现通常涉及图的结构,比如邻接表,以及处理反向边的方式。 然后是应用部分。引用1提到了资源分配、经济学、生物学等,引用3提到了医生值班问题。需要具体说明这些应用场景是如何转化为最大流问题的,比如如何构建网络图,确定源点、汇点,边的容量等。例如,医生值班问题中,可能将医生和日期作为节点,通过边连接表示可用性,并设定合适的容量来满足需求。 还需要考虑用户可能的疑问,比如为什么使用BFS而不是DFS,时间复杂度如何,以及EK算法的优缺点。比如,BFS保证了找到的是最短增广径,从而时间复杂度为O(VE²),相比DFS更高效,避免陷入长时间循环。 另外,引用2提到残留网络和增广的重要性,需要解释残留网络的概念,以及算法如何通过不断更新残留网络来寻找最大流。同时,最小割最大流定理是算法正确性的基础,这一点也需要简要说明。 在组织回答时,应该分点清晰,先讲实现步骤,再讲应用案例,最后可能涉及优化或其他算法的比较,但用户主要关注EK本身,所以重点放在实现和应用上。同时,根据用户提供的引用,需要适当引用相关文献,比如引用1、3、5来支持应用场景和算法步骤的描述。 最后,生成相关问题需要考虑用户可能进一步想了解的内容,比如与其他算法的比较,复杂度分析,或者更复杂的应用案例。确保问题具有引导性,帮助用户深入学习。 现在需要确保回答的结构符合用户的要求:结构清晰,分步骤说明,包含实现代码和应用案例,引用正确,并且使用中文。同时,检查LaTeX格式是否正确,数学表达式使用$...$,独立公式用$$...$$,但根据用户提供的示例,可能不需要太多复杂公式,所以可能需要用行内公式,比如流量增加量$Δf$等。 总结,回答应包括: 1. EK算法基本原理和步骤 2. 实现细节,代码框架或伪代码 3. 应用案例,如医生值班问题 4. 算法特性(时间复杂度和优缺点) 5. 相关问题 需要避免过于技术化的术语,保持解释的易懂性,同时确保引用正确,如引用3提到的医生值班问题,引用5的代码结构,引用1的应用领域。</think>### 网络流EK算法实现与应用 #### 一、EK算法原理与实现 EK算法(Edmonds-Karp算法)是Ford-Fulkerson方法的一种高效实现,**核心是通过BFS寻找最短增广径**,保证时间复杂度为$O(VE^2)$,避免了DFS可能陷入的指数级时间问题[^2][^5]。 ##### 实现步骤: 1. **初始化残留网络** 残留网络初始为原图,每条边$u→v$的容量为$c(u,v)$,反向边$v→u$容量为0。 2. **BFS寻找增广径** 从源点$s$出发,用BFS找到一条到汇点$t$的径,径上所有边残留容量$>0$。若找不到径,算法终止。 3. **计算流量增量$Δf$** $$Δf = \min\{ c_f(u,v) \mid (u,v) \text{在增广径上} \}$$ 4. **更新残留网络** - 正向边容量减少$Δf$,反向边容量增加$Δf$(反向边用于“撤销”流量)[^4][^5]。 - 总流量增加$Δf$。 5. **重复步骤2-4**,直到无增广径。 ##### 代码框架(Python伪代码): ```python def edmonds_karp(graph, s, t): max_flow = 0 parent = [-1] * len(graph) while bfs(graph, s, t, parent): # BFS找增广径 path_flow = float('inf') v = t while v != s: # 计算最小残留容量 u = parent[v] path_flow = min(path_flow, graph[u][v]) v = u v = t while v != s: # 更新残留网络 u = parent[v] graph[u][v] -= path_flow graph[v][u] += path_flow # 反向边 v = u max_flow += path_flow return max_flow ``` #### 二、应用场景与建模方法 ##### 1. 医生值班问题 **问题描述**:医院需安排医生在$n$天值班,每天至少$k$名医生在岗,且每名医生连续工作不超过$m$天。 **建模方法**: - **构建网络图**: - 源点$s$连接所有医生节点,边容量为$m$(限制连续工作天数)。 - 医生节点连接其可工作的日期节点,边容量为1。 - 所有日期节点连接汇点$t$,边容量为$k$(每天最少需求)[^3]。 ##### 2. 资源分配问题 **问题描述**:将有限资源分配给多个任务,每个任务有最小资源需求和优先级。 **建模方法**: - 源点表示资源池,任务节点通过边连接汇点,边容量为任务需求。 - 通过中间节点限制资源冲突(如共享设备)。 ##### 3. 交通流量优化 **问题描述**:计算城市网中从A到B的最大车流量。 **建模方法**: - 将口作为节点,道为有向边,边容量为车道通行能力。 - 使用EK算法求解最大流,即为网最大通行量[^1]。 #### 三、算法特性 | 特性 | 说明 | |----------------|----------------------------------------------------------------------| | 时间复杂度 | $O(VE^2)$,适合中小规模网络[^5] | | 空间复杂度 | $O(V^2)$(邻接矩阵)或$O(V+E)$(邻接表) | | 优势 | 实现简单,BFS避免死循环 | | 局限性 | 大规模网络效率低,可改用Dinic算法(时间复杂度$O(V^2E)$)[^1][^5] | #### 四、与其他算法的对比 | 算法 | 核心思想 | 时间复杂度 | 适用场景 | |---------------|------------------------|---------------|------------------------| | Ford-Fulkerson| DFS随机找增广 | 无保证 | 理论分析 | | **EK算法** | **BFS找最短增广** | $O(VE^2)$ | 中小规模网络 | | Dinic算法 | 分层网络+阻塞流 | $O(V^2E)$ | 大规模稀疏图 |
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值