网络流最大流,费用流

网络流:

定义:

网络流,指的是在一个每条边都有容量的有向图分配流,使得一条边的流量不会超过他的容量

有向图称为网络,顶点称为结点,边称为弧

源点:只出不进的顶点

汇点:只进不出的顶点

容量和流量:每条有向边上有两个两,容量和流量

相关性质:

容量限制:对于所有顶点u,v,满足f(u,v)<=c(u,v)  ( f:流量,c:容量)

流守恒性:对于所有顶点u,满足流入等于流出

反对称性:对所有顶点u,v满足f(u,v)=-f(v,u)

最大流:

可行流:当前有一条路,所有边上的流量没有超过其容量,那那么我们将这个流称为一个可行流

增广路:假设我们找到这样一条路,从源点连向汇点,且每条边的流量都严格小于其容量,那么我们一定可以给这条路增加一个delta流量,并使其增加以后仍然为可行流

基本思路:我们在网络中不断的寻找增广路,并对其进行流量增量增广delta,(也就是使这条路上的每条边的容量减少delta,并在总流量中增加delta),直到找不到增广路为止,我们就可以找到最大流

反悔机制:

我们在找寻增广路增广的同时,有可能会堵死别的更好的增广路,这个时候怎么办呢?

我们在建立网络时,给每条边(u,v)都建立一条反向边(v,u),且c(v,u)=0,我们在每次增广的时候,把路上的每一条边的容量减少delta时,同时也把其反向边的容量增加delta,这样一来,当我们下次找寻增广路时,就可以通过反向边退流,从而达到反悔的效果

对于一个n节点m边的网络求最大流:

题目链接:P3376 【模板】网络最大流 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

两种算法:ek算法和dinic算法

ek算法:

时间复杂度:O(nm^2)

思路:基于bfs找寻增广路,找到增广路后进行增广delta,直到源点与汇点不连通

实现代码如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
#define ll long long
const int N = 210, M = 5010 << 1;
int n, m, s, t;
int h[N], tto[M], nxt[M], tot, pre[N];
ll ww[M], mf[N];
//h,tto,nxt:链式前向星
//ww[i]:边i的容量
//mf[v]:节点v的当前流量
//pre[v]:结点v的前驱边(指向增广路起点方向的)
void add(int a, int b, int c) {
    tto[++tot] = b;
    nxt[tot] = h[a];
    ww[tot] = c;
    h[a] = tot;
}


bool bfs() {
    memset(mf, 0, sizeof(mf));
    queue<int> q;
    q.push(s); mf[s] = 1ll << 33;
    int u;
    while (!q.empty()) {
        u = q.front(); q.pop();
        //求增广路
        for (int i = h[u], v; v = tto[i]; i = nxt[i]) {
            //mf[v]==0,意味着没有被访问过,ww[i]大于0,意味着可流
            if (mf[v] == 0 && ww[i]) {
                mf[v] = min(mf[u], ww[i]);
                pre[v] = i;
                q.push(v);
                if (v == t) return true;
            }
        }
    }
    return false;
}

ll EK() {
    ll flow = 0;
    while (bfs()) {
        int v = t;
        //从t开始,给正向边减流,反向边加流
        while (v != s) {
            //i为v的前驱边
            int i = pre[v];
            //给前驱边减流
            ww[i] -= mf[t];
            //给反向边加流
            ww[i ^ 1] += mf[t];
            //迭代到前驱变指向的结点
            v = tto[i ^ 1];
        }
        flow += mf[t];
    }
    return flow;
}

int main() {
    tot = 1;
    scanf("%d%d%d%d", &n, &m, &s, &t);
    for (int i = 1, a, b, c; i <= m; i++) {
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
        add(b, a, 0);//反向边
    }
    printf("%lld\n", EK());
    return 0;
}

dinic算法:

时间复杂度:O(n^2m)

基于bfs分层优化的dfs:

先用bfs对网络进行分层,找到多条增广路

再用dfs走每条增广路,进行流量增广

其中bfs的分层限制了搜索深度

当前弧优化剪枝:(走完当前弧后,下一个dfs遍历到该结点一定不用再走这条弧,因此可以直接舍去剪枝)

剩余流量优化剪枝:当该条增广路可流流量为0时,不再搜索

残枝优化剪枝:当这个节点指向的所有增广路都不可流时,将该节点踢出分层图,以防被下一个dfs访问

最后dinic进行多次bfs,每次bfs完后都进行dfs增广,直到找不到增广路为止

实现代码如下:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <queue>
#include <cstring>
using namespace std;
const int N=10010,M=200010;
int n,m,s,t;
#define LL long long
int h[N],nxt[M],tto[M],tot,d[N],cur[N];
LL ww[M];
//d[v]:结点v在分层图的深度
//cur[u]:当前弧
void add(int a,int b,int c){
    tto[++tot]=b;
    ww[tot]=c;
    nxt[tot]=h[a];
    h[a]=tot;
}

bool bfs(){
    memset(d,0,sizeof(d));
    queue<int> q;
    q.push(s);d[s]=1;
    int u;
    while(!q.empty()){
        u=q.front();q.pop();
        for(int i=h[u],v;v=tto[i];i=nxt[i]){
            //当该节点未分层且可流时,其有可能是增广路
            if(!d[v]&&ww[i]){
                d[v]=d[u]+1;
                q.push(v);
                if(v==t) return true;
            }
        }
    }
    return false;
}

//u:当前访问节点
//mf:结点u的剩余流量
//函数返回值:结点u可以流出去的流量
LL dfs(int u,LL mf){
    //已经搜索到了汇点
    if(u==t) return mf;
    LL sum=0;
    for(int i=cur[u],v;v=tto[i];i=nxt[i]){
        //当前弧优化
        cur[u]=i;
        //如果是下一层的,且可流
        if(d[v]==d[u]+1&&ww[i]){
            //可流向v的流量
            LL f=dfs(v,min(mf,ww[i]));
            ww[i]-=f;
            ww[i^1]+=f;
            //累加可流出流量
            sum+=f;
            //减少u的剩余流量
            mf-=f;
            //剩余流量优化
            if(mf==0) break;
        }
    }
    //残枝优化
    if(!sum) d[u]=0;
    return sum;
}

LL dinic(){
    LL flow=0;
    while(bfs()){
        memcpy(cur,h,sizeof(h));
        flow+=dfs(s,1ll<<33);
    }
    return flow;
}

int main(){
    tot=1;
    scanf("%d%d%d%d",&n,&m,&s,&t);
    for(int i=1,a,b,c;i<=m;i++){
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c); add(b,a,0);
    }
    printf("%lld",dinic());
    return 0;
}

最小割问题:

定义割掉每条边的代价等于该条边的容量大小,要求割掉任意多条边,使得源点s流向汇点t的流量为0的情况下,使代价总和最小,这被称为最小割问题。

题目本质:

当一个图被分割为两个部分时,不再存在源点s到汇点t的通路,那么我们就要割掉原本每条通路上容量最小的边,而在最大流问题中,每条通路的流量恰好又是取决于其容量最小的边,因此,最小割的代价总和一定等于最大流,即最小割==最大流,此为最大流最小割定理

三种基本问题:

1.求最小割:即求最大流

2.求最小割的划分:

我们先求出最大流,然后从源点开始对残留网进行bfs,只有容量大于0的边可以通过,那么我们标记能够到达的点,则标记的点构成s集合,未标记的点构成t集合,此划分就是最小割的其中一个划分

3.求最小割的最少边数:

题目链接:P1344 [USACO4.4] 追查坏牛奶 Pollutant Control - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

基本思路:我们求出最小割的时候,由最小割等于最大流,最小割一定等于\sum c_i,其中ci为被割掉的边的容量,假设有m条边,我们只需将每条边的容量修改为c*k+1,其中k>m,那么这样新跑出来的最小割res就会等于k\sum c_i+n,其中n<=m,因此n<k,则n就代表了我们割的边数,且是最少边数(因为我们每次都选取了每条通路容量最小的边来割),因此答案就是res mod k

实现代码:

#include <cstdio>
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
#define LL long long
const int N = 35, M = 1010 << 1,mod=1001;
int h[N], nxt[M], tto[M], tot, d[N], cur[N];
LL ww[M];
void add(int a, int b, LL c) {
    tto[++tot] = b;
    ww[tot] = c;
    nxt[tot] = h[a];
    h[a] = tot;
}

int n, m;

bool bfs() {
    memset(d, 0, sizeof(d));
    int u;
    queue<int> q;
    q.push(1); d[1] = 1;
    while (!q.empty()) {
        u = q.front(); q.pop();
        for (int i = h[u], v; v = tto[i]; i = nxt[i]) {
            if (!d[v] && ww[i]) {
                d[v] = d[u] + 1;
                q.push(v);
                if (v == n) return true;
            }
        }
    }
    return false;
}

LL dfs(int u, LL mf) {
    if (u == n) return mf;
    LL sum = 0;
    for (int i = cur[u], v; v = tto[i]; i = nxt[i]) {
        cur[u] = i;
        if (d[v] == d[u] + 1 && ww[i]) {
            LL f = dfs(v, min(mf, ww[i]));
            ww[i] -= f;
            ww[i ^ 1] += f;
            sum += f;
            mf -= f;
            if (mf == 0) break;
        }
    }
    if (!sum) d[u] = 0;
    return sum;
}

LL dinic() {
    LL flow = 0;
    while (bfs()) {
        memcpy(cur, h, sizeof(h));
        flow += dfs(1, 1ll << 35);
    }
    return flow;
}


int main() {
    scanf("%d%d", &n, &m);
    tot = 1;
    int a,b;LL c;
    for (int i = 1; i <= m; i++) {
        scanf("%d%d%lld", &a, &b, &c);
        c=c*mod+1;
        add(a, b, c);
        add(b, a, 0);
    }

    LL ans = dinic();
    LL res=ans/mod;
    LL res2=ans%mod;
    printf("%lld %lld", res, res2);
    return 0;
}

费用流问题:

费用流与网络流相比,每条边增加了一个单位费用cst,当该边流过流量f时,会产生f*p的费用代价

最小费最大流问题:在最大流的前提下使总费用最小

思路:

由于我们每对一条增广路进行增广delta时,delta会在这条路的每条边都产生收费,则总费用就是delta*\sum cst,因此,我们希望每次找增广路的时候,都优先找cst总和最小的路来进行增广,这样就可以贪心地使费用最小。

那么我们就明白了,这就是找最短路,因此,我们在ek算法中,用spfa来代替bfs,我们每次都找能够增广的最短路来进行增广即可

实现代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
#define LL long long
const int N = 5E3 + 10, M = 5E4 + 10;
int n, m, s, t, flow, cost;
int h[N], tto[M << 1], ww[M << 1], cst[M << 1], nxt[M << 1], tot;
int pre[N], mf[N], dis[N], vis[N];
void add(int a, int b, int c, int d) {
    tto[++tot] = b;
    nxt[tot] = h[a];
    ww[tot] = c;
    cst[tot] = d;
    h[a] = tot;
}

bool spfa() {
    memset(mf, 0, sizeof(mf));
    memset(dis, 0x3f, sizeof(dis));
    queue<int> q;
    q.push(s); mf[s] = 1 << 29; dis[s] = 0; vis[s] = true;
    int u;
    while (!q.empty()) {
        u = q.front(); q.pop(); vis[u] = false;
        for (int i = h[u], v; v = tto[i]; i = nxt[i]) {
                //松弛操作
                if (dis[v] > dis[u] + cst[i]&& ww[i]) {
                    dis[v] = dis[u] + cst[i];
                    pre[v] = i;
                    mf[v] = min(mf[u], ww[i]);
                    if(!vis[v]){
                        q.push(v); vis[v] = true;
                    }
                }
        }
    }
    return mf[t];
}

void ek() {
    while (spfa()) {
        int v = t, i;
        while (v != s) {
            i = pre[v];
            ww[i] -= mf[t];
            ww[i ^ 1] += mf[t];
            v = tto[i ^ 1];
        }
        flow += mf[t];
        cost += mf[t] * dis[t];
    }
}


int main() {
    scanf("%d%d%d%d", &n, &m, &s, &t);
    tot = 1;
    for (int i = 1, a, b, c, d; i <= m; i++) {
        scanf("%d%d%d%d", &a, &b, &c, &d);
        add(a, b, c, d);
        add(b, a, 0, -d);
    }
    ek();
    printf("%d %d", flow, cost);
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值