我们要解决网络流里的一种常见问题——最大流问题。
(这是第一天自己看了很多博客总结出来的,有时候写字比打字的思路更清晰。当然这只是一个推导过程,通常我们解决最大流问题要用到Dinic算法,要问我为啥?它的时间复杂度低,到时候提交题一遍过不好嘛,省得又T了还得改T_T)最大流的三种解题方法,FF算法,EK算法,Dinic算法,它们的时间复杂度相差十分大。第一天看这些东西的时候真的自闭,为什么大家的博客都不能写的详细一些呢,给小白一个理解的机会嘛...在这里我打算用最清晰好懂的语言总结EK算法的两种方法即邻接表邻接矩阵和Dinic算法
Edmonds-Karp EK算法
- 邻接矩阵(所占内存有些大,会造成不必要的浪费)
#include<iostream>
#include<cstdio>
#include<string.h>
#include<algorithm>
#include<queue>
#define mem(a,b) memset(a,b,sizeof(a)); //这个定义对于这种类型需要多次初始化数组的题真的太方便了
#define maxn 500
#define inf 0x3f3f3f3f
using namespace std;
int G[maxn][maxn],pre[maxn],vis[maxn],n,m; //pre标记在这条路径上当前节点的前驱,同时标记该节点是否在队列中 G记录的是两点之间的流量 vis判断是否访问过
//利用bfs广搜 目的是为了查找最短增广路
bool bfs(int s,int e) { //从源点s到汇点e的增广路 若找到返回true
mem(vis,0);
mem(pre,0);
pre[s] = s; //记录源点
vis[s] = 1; //访问过
queue<int> que;
que.push(s);
while(!que.empty()) {
int u=que.front();
que.pop();
// 访问u的所有出边 判断能否增广
for(int i=0; i<=e; i++) {
if(G[u][i] && !vis[i]) {//流量不为0且没有被访问过
pre[i] = u;
que.push(i);
vis[i] = 1;
if(i==e) //到汇点了,那么一定存在增广路
return true;
}
}
}
return 0;
}
int EK(int s,int e) {
int ans = 0;
while(bfs(s,e)){ //判断是否存在增广路
int MIN = inf;
// 遍历一条增广路上的所有边 找到流量最小的一条边
for(int i =e; i!=s; i=pre[i]) {
MIN = min(MIN,G[pre[i]][i]); //pre[i]的值是i的前驱结点
}
//更新增广路上的流量 正向减 反向加
for(int i=e; i!=s; i=pre[i]) {
G[pre[i]][i] -= MIN;
G[i][pre[i]] += MIN;
}
ans += MIN;
}
return ans;
}
- 邻接表
#include<iostream>
#include<cstdio>
#include<string.h>
#include<algorithm>
#include<queue>
#define maxn 500
#define inf 0x3f3f3f3f
using namespace std;
int cnt;
int vis[maxn], head[maxn], path[maxn]; //head标记在这条路径上当前节点的前驱,同时标记该节点是否在队列中
struct ac{
int v, c, nex; //v初边 c流量 nex最后一次出现的位置
}edge[maxn << 8]; //左移,因为边的个数一定多余点的个数
void init() { //初始化
cnt = 0;
memset(head, -1, sizeof(head));
}
void addedge(int u, int v, int c) { //建立边之间的联系
// 正向边
edge[cnt] = {v, c, head[u]};
head[u] = cnt++;
// 反向边
edge[cnt] = {u, 0, head[v]};
head[v] = cnt++;
}
bool bfs(int s, int t) {
memset(vis, 0, sizeof(vis));
memset(path, -1, sizeof(path));
queue<int> que;
que.push(s);
vis[s] = 1;
while (!que.empty()) {
int u = que.front();
que.pop();
// 遍历u的所有出边
for (int i = head[u]; i != -1; i = edge[i].nex) {
int v = edge[i].v;
int c = edge[i].c;
// 判断能否增广
if (c == 0 || vis[v]) continue;
path[v] = i;
vis[v] = 1;
que.push(v);
if (v == t) return true;
}
}
return false;
}
int EK(int s, int t) {
int ans = 0;
while (bfs(s, t)) {
int flow = inf;
for (int i = path[t]; i != -1; i = path[edge[i^1].v]) {
flow = min(flow, edge[i].c); //找最小流量
}
for (int i = path[t]; i != -1; i = path[edge[i^1].v]) {
edge[i].c -= flow; //正向边-
edge[i^1].c += flow; //反向变+
}
ans += flow;
}
return ans;
}
addedge()函数理解如图:
Dinic算法
- 邻接表版
运用到了当前弧优化,当前弧优化的意思就是说每次开始跑邻接表遍历不是从第一条边开始跑而是从上一次点i遍历跑到的点。我们用curedge[i]表示这个点,之后每次建完分层图之后都要进行初始化,且见分层图时不存在当前弧优化
#include<iostream>
#include<cstdio>
#include<string.h>
#include<algorithm>
#include<queue>
#define maxn 300
#define inf 0x3f3f3f3f
using namespace std;
int s, e, cnt=0;
int head[maxn], dis[maxn], curedge[maxn]; //dis表示当前层数
struct ac{
int v, c, nex;
}edge[maxn << 7];
void init() {
cnt = 0;
memset(head, -1, sizeof(head));
}
void addedge(int u, int v, int c) {
// 正向建边
edge[cnt] = {v, c, head[u]};
head[u] = cnt++;
// 反向建边, 流量为0
edge[cnt] = {u, 0, head[v]};
head[v] = cnt++;
//这一部分也可以为下面的写法 更好理解点
// edge[cnt].to=v;
// edge[cnt].cap=w;
// edge[cnt].next=head[u];
// head[u]=cnt++;
//
// edge[cnt].to=u;
// edge[cnt].cap=0;
// edge[cnt].next=head[v];
// head[v]=cnt++;
}
bool bfs() {
queue<int> que;
que.push(s);
memset(dis, 0, sizeof(dis)); // 对图进行分层
dis[s] = 1;
while (!que.empty()) {
int u = que.front();
que.pop();
for (int i = head[u]; i != -1; i = edge[i].nex) {
int v = edge[i].v;
int c = edge[i].c;
// 如果节点v已经分过层或者u->v流量为0, continue
if (dis[v]!=0 || c == 0) continue;
dis[v] = dis[u] + 1; // 对v进行标记并加入队列
que.push(v);
}
}
return dis[e] > 0; // 判断是否存在增广路,s是否能到达e
}
int dfs(int u, int flow) { // 增广路走到u点的最小流量为flow
if (u == e || flow == 0) return flow;
// 遍历u的所有出边
for (int &i = curedge[u]; i != -1; i = edge[i].nex) { // 当前弧优化 运用引用,在修改i的同时,将curedge[u]顺便修改
int v = edge[i].v;
int c = edge[i].c;
// 判断能否u->v增广
if (dis[v] != dis[u] + 1 || c == 0) continue;
int d = dfs(v, min(flow, c));
if (d > 0) { // 找到一条增广路,修改增广路上的正反向边
edge[i].c -= d;
edge[i^1].c += d;
return d;
}
}
dis[u] = -1; // 炸点优化
return 0;
}
int Dinic() {
int sum = 0, d;
while (bfs()) { // 判读是否存在增广路
for (int i = 0; i <= e; ++i)
curedge[i] = head[i]; // copy head数组,在dfs中可以直接得到下一条没有被增广过的边
while ((d = dfs(s, inf)) > 0)
sum += d; // 多次dfs找增广路
}
return sum;
}
ps.以上是我对它们的理解, 可能会出现纰漏,如果有任何疑问欢迎询问和指正