网络流-最大流(残量网络、增广路经、Edmonds-Karp算法、Dinic算法、最小割边集)

前言
太长时间没打acm,最近暑期开始训练赛,发现很多算法都不会了,看到题目知道用什么但是就是写不出来,再不学习就GG了。。。赶快赶快赶快补补补学学学
今天不学习,明天变辣鸡~

最大流

在一个流网络中(带权有向图),从源点(没有入度的点)到汇点(没有出度的点)的所有流中的权值最大值,就是最大流。(自认为,不一定严谨)

增广路经

增广路就是表示从源点s到汇点t 的一条简单路径,该路径上不存在边权小于等于0的边。

残量网络

残量网络是最大流中的一个重要概念,假设有一源点为s汇点为t的流网络G=(V,E),f是G中的一个流,可以定义边u,v的剩余容量Gf(u,v)=c(u,v)-f(u,v),由此定义残量网络Gf(V,Ef)。
其中c(u,v)表示流网络中u->v这条表的权值,f(u,v)表示u->v这条边目前的流。
所以,残量网络就是将原图中的每一条边的边权更新为这条边最初的边权与流经这条边的流之差。
除了修改正向的边权之外,还要讲逆向的边权改为目前流经这条边的流量。如下图所示:
如下图留个节点,七条边的有向图,红色代表一条增广路,其流量为3。/前面表示的是流量,后面表示的是这条边的容量。
网络流-最大流(残量网络、增广路经、Edmonds-Karp算法、Dinic算法、最小割边集)_第1张图片
对应的残量网络如下图所示:其中红色边是修改权值后的边,绿色边是建立的逆向边。最后一个(4,6)这条边由于红色边边权为零,所以直接删除了。
网络流-最大流(残量网络、增广路经、Edmonds-Karp算法、Dinic算法、最小割边集)_第2张图片

Edmonds-Karp算法原理及代码

算法原理
该算法每次在残量网络中从s到t寻找增广路,然后更新残量网络,并将该增广路经上的最小边权加到答案max_flow中,直到找不到增广路为止。
时间复杂度分析
该算法的时间复杂度为O(nm^2),因为每一次找增广路,至少会将正向路径上的一条边的边权变为0,将所有边的边权变为0的次数最坏情况为m次(每次变为0一条边),每次更新网络的最坏时间为n(所有点都走一遍),所以最坏情况下时间复杂度为O(nm²)。
残量网络中添加并更新逆向边的原因
原因来自:这里
在做增广路时可能会阻塞后面的增广路,或者说,做增广路本来是有个顺序才能找完最大流的。
但我们是任意找的,为了修正,就每次将流量加在了反向弧上,让后面的流能够进行自我调整。
举例:
比如说下面这个网络流模型
网络流-最大流(残量网络、增广路经、Edmonds-Karp算法、Dinic算法、最小割边集)_第3张图片
我们第一次找到了1-2-3-4这条增广路,这条路上的delta值显然是1。
于是我们修改后得到了下面这个流。(图中的数字是容量)
网络流-最大流(残量网络、增广路经、Edmonds-Karp算法、Dinic算法、最小割边集)_第4张图片
这时候(1,2)和(3,4)边上的流量都等于容量了,我们再也找不到其他的增广路了,当前的流量是1。
但是,
这个答案明显不是最大流,因为我们可以同时走1-2-4和1-3-4,这样可以得到流量为2的流。
那么我们刚刚的算法问题在哪里呢?
问题就在于我们没有给程序一个“后悔”的机会,应该有一个不走(2-3-4)而改走(2-4)的机制。
代码:

 #include
using namespace std;
// using namespace __gnu_pbds;
#define pb push_back
#define _filein freopen("C:\\Users\\zsk18\\Desktop\\in.txt","r",stdin)
#define _fileout freopen("C:\\Users\\zsk18\\Desktop\\out.txt","w",stdout)
#define ok(i) printf("ok%d\n",i)
#define mp(a,b) make_pair(a,b)
// #define gcd(a,b) __gcd(a,b) ;
typedef double db;
typedef long long ll;
// typedef __int128 ll;
typedef unsigned long long ull;
typedef pair<int,int>PII;
const double PI = acos(-1.0);
const ll MOD=998244353;
const ll NEG=1e9+6;
const ll MAXN=1e3+10;
const int INF=0x3f3f3f3f;
const ll ll_INF=1e15;
const double eps=1e-9;
ll qm(ll a,ll b){ll ret=1;while(b){if(b&1)ret=ret*a%MOD;a=a*a%MOD;b>>=1;}return ret;}
ll inv(ll x){return qm(x,MOD-2);}
ll gcd(ll a,ll b){return b==0?a:gcd(b,a%b);}
ll lcm(ll a,ll b){return a*b/gcd(a,b);}
int n,m;
int head[MAXN],nex[MAXN],st[MAXN],e[MAXN],w[MAXN];
int o;
void add(int x,int y,int val)
{
    st[++o]=x;e[o]=y;w[o]=val;
    nex[o]=head[x];head[x]=o;
}
int vis[MAXN];
int pre[MAXN]; //记录到达第i个点的之前的边的编号
int now_mi[MAXN];//记录当前路径中的流。(即路径中的边权最小值)
ll max_flow;//答案
int s,t;
bool bfs()//寻找 增广路
{
    memset(vis,0,sizeof(vis));
    now_mi[s]=INF;
    queue<int>q;
    while(!q.empty())q.pop();
    q.push(s);
    int x,y;
    while(!q.empty())
    {
        x=q.front();
        q.pop();
        // printf("x=%d\n",x);
        for(int i=head[x];i;i=nex[i])
        {
            y=e[i];
            if(vis[y]||w[i]<=0)continue;
            now_mi[y]=min(now_mi[x],w[i]);//记录到目前节点的最小边权  
            pre[y]=i;
            vis[y]=1;
            // printf("y=%d\n",y);
            q.push(y);
            if(y==t)return true;
        }
    }
    return false;
}
void update()//更新最大流与图
{
    int x=t;
    while(x!=s)
    {
        // printf("update x=%d\n",x);
        int i=pre[x];
        w[i]-=now_mi[t]; //正向边减少流的值
        w[i^1]+=now_mi[t];//反向边增加流的值
        x=e[i^1];
    }
    max_flow+=(ll)now_mi[t];
}
int main()
{  
    scanf("%d%d",&n,&m);
    o=1;s=1;t=n;
    for(int i=0;i<m;i++)
    {
        int x,y,val;
        scanf("%d%d%d",&x,&y,&val);
        add(x,y,val);
        add(y,x,0);//建立逆向边,边权初始为0.
    }
    while(bfs())update();
    printf("%I64d\n",max_flow);
    return 0;
}

Dinic算法原理及代码

算法原理
dicnic算法的步骤分为两步:
1、从源点进行bfs,更新每一个点距离源点s的深度,用d[]数组来表示。如果每次bfs能够到达汇点t,则return true 执行步骤2,否则,return false,算法结束
2、如果bfs返回为true,从源点s进行dfs搜索。每次搜索的目的是为了返回在残量网络的结构不变的前提下,从该点到汇点t的最大流量。这一步主要有以下几个步骤:
(1)、记录目前从源点s到达该点的流量flow。初始源点s的流量为INF。
(2)、记录目前的返回值ret。即目前从该点到达汇点的最大流量。并且ret不能大于flow。因为流到i点的流量为flow,从i点出去的流量绝对不可能大于flow。
(3)、如果dfs完成,若返回值不为0,则继续执行步骤1,若为0,则执行步骤1。时间复杂度分析
dicnic每次bfs更新深度的时间复杂度为m,每次dfs的时间复杂度为n。每次搜索最多执行n次。所以时间复杂度为O(n^2m)。
代码

#include
using namespace std;
// using namespace __gnu_pbds;
#define pb push_back
#define _filein freopen("C:\\Users\\zsk18\\Desktop\\in.txt","r",stdin)
#define _fileout freopen("C:\\Users\\zsk18\\Desktop\\out.txt","w",stdout)
#define ok(i) printf("ok%d\n",i)
#define mp(a,b) make_pair(a,b)
// #define gcd(a,b) __gcd(a,b) ;
typedef double db;
typedef long long ll;
// typedef __int128 ll;
typedef unsigned long long ull;
typedef pair<int,int>PII;
const double PI = acos(-1.0);
const ll MOD=998244353;
const ll NEG=1e9+6;
const ll MAXN=500+10;
const int INF=0x3f3f3f3f;
const ll ll_INF=1e15;
const double eps=1e-9;
ll qm(ll a,ll b){ll ret=1;while(b){if(b&1)ret=ret*a%MOD;a=a*a%MOD;b>>=1;}return ret;}
ll inv(ll x){return qm(x,MOD-2);}
ll gcd(ll a,ll b){return b==0?a:gcd(b,a%b);}
ll lcm(ll a,ll b){return a*b/gcd(a,b);}
int head[MAXN],nexts[MAXN];
int e[MAXN],w[MAXN];
int tot;
void add(int x,int y,int z)
{
    e[++tot]=y;w[tot]=z;nexts[tot]=head[x];head[x]=tot;
    e[++tot]=x;w[tot]=0;nexts[tot]=head[y];head[y]=tot;
}
int n,m;
int s,t;
int d[MAXN];
int max_flow;
bool bfs()//更新深度数组d
{
    memset(d,0,sizeof(d));
    int x=s;
    d[s]=1;
    queue<int>q;
    q.push(x);
    while(!q.empty())
    {
        x=q.front();q.pop();
        for(int i=head[x];i;i=nexts[i])
        {
            int y=e[i];
            if(d[y]||!w[i])continue;
            d[y]=d[x]+1;
            q.push(y);
            if(y==t)return 1;
        }
    }
    return 0;
}
int dinic(int x,int flow)
{
    // printf("flow=%d\n",flow);
    if(x==t)return flow;
    int ret=0;
    for(int i=head[x];i;i=nexts[i])
    {
        if(flow<=0)break;//其实这里不可能小于,只会等于。
        int y=e[i];
        if(w[i]&&d[y]==d[x]+1)
        {
            int mid=dinic(y,min(w[i],flow));//从下一个点到汇点t的不超过min(w[i],flow)的最大流量。
            w[i]-=mid;//更新正向点
            w[i^1]+=mid;//更新反向边
            flow-=mid;//更新目前可用的流浪
            ret+=mid;//更新可以从目前点流到汇点t的流量
        }
    }
    return ret;//返回从目前店可以流到汇点t的流量。
}
int main(void)
{
    tot=1;
    scanf("%d%d",&n,&m);
    s=1,t=n;
    for(int i=1;i<=m;i++)
    {
        int x,y,z;
        scanf("%d%d%d",&x,&y,&z);
        add(x,y,z);
    }
    int flow;
    while(bfs())
        while(flow=dinic(s,INF))
            max_flow+=flow;
    printf("%d\n",max_flow);
    return 0;
} 

最小割边集

最大流和最小割其实是一样的。
在找到最大流之后,寻找最小割边集的方法:
从源点开始进行搜索,扩展未流满的边,最后能扩展到的点与未扩展到的点之间的边即为最小割的边集,即一旦扩展到一个流满的边,这个边就加入vector< int >ans里面就好了。

你可能感兴趣的:(学习记录-图论,算法,acm竞赛,c++)