1. 程式人生 > >淺談網路流(最大流,最小割,mcmf,最大匹配)

淺談網路流(最大流,最小割,mcmf,最大匹配)

前言:

對於網路流的基礎知識,網上許多大佬解釋得很透徹了,我在這裡也不去挑戰大佬權威了!

這篇部落格記錄我一週學習網路流的學習筆記!以後還會逐漸完善!

一、最大流

最大流定理:

如果殘留網路上找不到增廣路徑,則當前流為最大 流;反之,如果當前流不為最大流,則一定有增廣路徑。

用最大流的增廣路經求二分圖匹配:

求二分圖匹配的過程就是求最大曾廣路的問題,而最大流定理就是將兩者之間聯絡起來,所以,二分圖最大匹配問題用最大流做也是一個好方法。所以,用網路流在求二分圖匹配時候,我們首先要做的就是求出最大流。

說到最大流,最大流求法呢也是有很多的!對比一下哈~

FF(Ford-Fulkerson)演算法 :

步驟:

(1)如果存在增廣路徑,就找出一條增廣路徑 DFS(EK演算法為BFS)

(2)然後沿該條增廣路徑進行更新流量 (增加流量)

While 有增廣路徑 :

       do   更新該路徑的流量

時間複雜度:

O(cn^2),c(邊的容量和C,頂點數N)

程式碼:

缺點:如果運氣不好 這種圖會讓你的程式執行200次dfs 雖然實際上最少只要2次我們就能得到最大流

EK(Edmonds-Karp)演算法:

步驟:

(1)如果存在增廣路徑,就找出一條增廣路徑 BFS

(2)然後沿該條增廣路徑進行更新流量 (增加流量)

時間複雜度:

O(nm^2)(頂點數N,邊數M)

程式碼:

以hdu3549為例的ac程式碼

//O(m^2*n)

#include<bits/stdc++.h>
using namespace std;
const int maxm=1e4+5;
const int maxn=15+5; 
const int inf=0x7f7f7f7f;

struct edge
{
    int next,v,w;
}edge[maxm];

int head[maxn];
int vis[maxn];//儲存是否被訪問
int pre[maxn];//記錄前驅
int last[maxn];//記錄前驅頂點和當前頂點的邊的編號
int cnt;

void init()
{
    cnt=0;
    memset(head,-1,sizeof(head));
}



void add_edge(int u,int v,int w)
{
    //正向邊
    edge[cnt].v=v;
    edge[cnt].w=w;
    edge[cnt].next=head[u],head[u]=cnt++;
    //反向邊
    edge[cnt].v=u,edge[cnt].w=0;
    edge[cnt].next=head[v],head[v]=cnt++;
}

bool bfs(int s,int e)
{
    queue<int > q;
    memset(vis,0,sizeof(vis));
    //memset(pre,-1,sizeof(pre));
    vis[s]=1;
    q.push(s);
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        for(int i=head[u];i!=-1;i=edge[i].next)
        {
            int v=edge[i].v,w=edge[i].w;
            if(!vis[v] && w)
            {
                last[v]=i;
                pre[v]=u;
                vis[v]=1;
                if(v==e)
                    return true;
                q.push(v);
            }   
        }
    }
    return false;
}


int EK(int s,int e)
{
    int flow=0;
    while(bfs(s,e))
    {
        int d=inf;
        
        /*for(int i=e;i!=s;i=pre[i])
            cout<<i<<" ";
         for(int i=e;i!=s;i=pre[i])
            cout<<last[i]<<" ";
*/

        for(int i=e;i!=s;i=pre[i])
        {  
            d = min(d, edge[ last[i] ].w );
        }
            
        for(int i=e;i!=s;i=pre[i])
        { 
            edge[ last[i] ].w-=d;
            edge[ last[i]^1 ].w+=d;
        }
        flow+=d;
    }
    return flow;
}


int main()
{
    int n,m;
    int t,flag=1;
    cin>>t;
    while(t--)
    {   
        cin>>n>>m;
        init();
        for(int i=0;i<m;i++){
            int u,v,w;
            cin>>u>>v>>w;
            add_edge(u,v,w);
        }
        printf("Case %d: %d\n",flag++,EK(1,n) );
    }
    return 0;
}

dinic演算法:

這個演算法是基於FF與EK的聯合提出來的演算法,同時用到BFS與DFS。

步驟:

(1):建造原網路G的一個分層網路L。

(2):用增廣路演算法計算L的最大流F,若在L中找不 到增廣路,演算法結束。

(3):根據F更新G中的流f,轉STEP1。

分層網路的構造演算法:

STEP1:標號源節點s,M[s]=0。

STEP2:呼叫廣度優先遍歷演算法,執行一步遍歷操作, 當前遍歷的弧e=v1v2,令r=G.u(e)-G.f(e)。

若r>0,則

(1) 若M[v2]還沒有遍歷,則M[v2]=M[v1]+1,且將 弧e加入到L中,容量L.u(e)=r。

(2) 若M[v2]已經遍歷且M[v2]=M[v1]+1,則將邊e 加入到L中,容量L.u(e)=r。

(3) 否則L.u(e)=0。 否則L.u(e)=0。 重複本步直至G遍歷完。其中的G.u(e)、G.f(e)、L.u(e) 分別表示圖G中弧e的容量上界和當前流量,圖L中弧e 的容量上界。

時間複雜度:O(mn^2)鄰接表表示圖,空間複雜度為O(n+m)。

又是hdu3549哈,以下是未優化以及優化過的dinic:

未優化程式碼:

//鏈式前向星dinic
//O(mn^2)
#include<bits/stdc++.h>
using namespace std;

const int maxm=1e4+5;
const int maxn=15+5;
const int inf=0x7f7f7f7f;

struct edge
{
    int next;
    int v,w;
}edge[maxm];

int m,n;
int cnt,head[maxn];
int dis[maxn];


void init()
{
    cnt=0;
    memset(head,-1,sizeof(head));
}

void add_edge(int u,int v,int w)
{
    edge[cnt].v=v,edge[cnt].w=w;
    edge[cnt].next=head[u],head[u]=cnt++;

    edge[cnt].v=u,edge[cnt].w=0;
    edge[cnt].next=head[v],head[v]=cnt++;
}
//分層
bool bfs()
{
    memset(dis,-1,sizeof(dis));
    queue<int> q;
    dis[1]=0;
    q.push(1);
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        if(u==n)
            return true;
        for(int i=head[u];~i;i=edge[i].next)
        {
            int v=edge[i].v,w=edge[i].w;
            if(dis[v]==-1 && w)
            {
                dis[v]=dis[u]+1;
                q.push(v);
            }
        }
    }
    return false;
}
//求增廣路
int dfs(int s,int t,int flow)
{
    if(s==t)
        return flow;
    int pre=0;
    for(int i=head[s];~i;i=edge[i].next)
    {
        int v=edge[i].v,w=edge[i].w;
        if(dis[s]+1==dis[v] && w>0)
        {
            int tmp=min(flow-pre,w);
            int tf=dfs(v,t,tmp);
            edge[i].w-=tf;
            edge[i^1].w+=tf;
            pre+=tf;
            if(pre==flow)
                return pre;
        }
    }
    return pre;
}

int dinic()
{
    int ret = 0;
    while(bfs())
        ret+=dfs(1,n,inf);
    return ret;
}

int main()
{
    int t;
    cin>>t;
    int flag=1;
    while(t--)
    {   
        cin>>n>>m;
        init();
        for(int i=0;i<m;i++){
            int u,v,w;
            cin>>u>>v>>w;
            add_edge(u,v,w);
        }
        cout<<"Case "<<flag++<<": ";
        cout<<dinic()<<endl;
    }
    return 0;
}

優化:

/*
HDU3549
*/

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
#include <string.h>

using namespace std;
const int MAXN = 10000 + 5;
const int MAXM = 100000 + 5;
const int INF = 1e9;

int n,m;
int s,t;//源點 匯點
int maxflow;//答案
struct Edge {
    int next;
    int to,flow;
} l[MAXM << 1];
int head[MAXN],cnt = 1;
int deep[MAXN],cur[MAXN];//deep記錄bfs分層圖每個點到源點的距離
queue <int> q;

inline void add(int x,int y,int z) {
    cnt++;
    l[cnt].next = head[x];
    l[cnt].to = y;
    l[cnt].flow = z;
    head[x] = cnt;
    return;
}

int min(int x,int y) {
    return x < y ? x : y;
}

int dfs(int now,int t,int lim) {//分別是當前點,匯點,當前邊上最小的流量
    if(!lim || now == t) return lim;//終止條件
//  cout<<"DEBUG: DFS HAS BEEN RUN!"<<endl;
    int flow = 0;
    int f;
    for(int i = cur[now]; i; i = l[i].next) {//注意!當前弧優化
        cur[now] = i;//記錄一下榨取到哪裡了,這裡也有人這麼寫,for(int &i = cur[now]; i; i = l[i].next),並且不要cur[now] = i;
        if(deep[l[i].to] == deep[now] + 1 //誰叫你是分層圖
            && (f = dfs(l[i].to,t,min(lim,l[i].flow)))) {//如果還能找到增廣路
        flow += f;
            lim -= f;
            l[i].flow -= f;
            l[i ^ 1].flow += f;//記得處理反向邊
            if(!lim) break;//沒有殘量就意味著不存在增廣路
        }
    }
    return flow;
}

bool bfs(int s,int t) {
    for(int i = 1; i <= n; i++) {
        cur[i] = head[i];//拷貝一份head,畢竟我們還要用head
        deep[i] = 0x7f7f7f7f;
    }
    while(!q.empty()) q.pop();//清空佇列 其實沒有必要了
    deep[s] = 0;
    q.push(s);
    while(!q.empty()) {
        int tmp = q.front();
        q.pop();
        for(int i = head[tmp]; i; i = l[i].next) {
            if(deep[l[i].to] > INF && l[i].flow) {//有流量就增廣
            //deep我賦的初值是0x7f7f7f7f 大於 INF = 1e9)
                deep[l[i].to] = deep[tmp] + 1;
                q.push(l[i].to);
            }
        }
    }
    if(deep[t] < INF) return true;
    else return false;
}

void dinic(int s,int t) {
    while(bfs(s,t)) {
        maxflow += dfs(s,t,INF);
//      cout<<"DEBUG: BFS HAS BEEN RUN!"<<endl;
    }
}

void init()
{
    cnt=1;
    memset(head,0,sizeof(head));
    maxflow=0;
}

int main() {
    int t,flag=1;
    cin>>t;
    while(t--)
    {
        init();
        cin>>n>>m;//點數邊數
        int x,y,z;
        for(int i = 1; i <= m; i++) {
            scanf("%d%d%d",&x,&y,&z);
            add(x,y,z);
            add(y,x,0);
        }
    //  cout<<"DEBUG: ADD FININSHED!"<<endl;
        dinic(1,n);
        printf("Case %d: %d\n",flag++,maxflow);
    }
    return 0;
}

話說這到底優化了什麼呢,我看到一篇部落格說的很好哈:

每次增廣一條路後可以看做“榨乾”了這條路,既然榨乾了就沒有再增廣的可能了。但如果每次都掃描這些“枯萎的”邊是很浪費時間的。那我們就記錄一下“榨取”到那條邊了,然後下一次直接從這條邊開始增廣,就可以節省大量的時間。這就是 當前弧優化 。

在DFS中用cur[x]表示當前應該從x的編號為cur[x]的邊開始訪問,也就是說從0到cur[x]-1的這些邊都不用再訪問了,相當於刪掉了(上面說的榨乾),達到了滿流。DFS(x,a)表示當前在x節點,有流量a,到終點t的最大流。當前弧優化在DFS裡的關鍵點在if(a==0) break;也就是說對於結點x,如果x連線的前面一些弧已經能把a這麼多的流量都送到終點,就不需要再去訪問後面的一些弧了,當前未滿的弧和後面未訪問的弧等到下次再訪問結點x的時候再去增廣。

以上就是計算最大流的一些簡單演算法,再回到最開始的那個問題,怎麼求最大匹配?

最大匹配:

匈牙利演算法em演算法也是處理最大匹配的一個好演算法,非常好用,但是呢,網路流更全面,除了最大匹配,還可以用在其他許許多多方面,比較萬能吧!下面就最大流來求最大匹配:

超級原點與超級匯點:

想想就知道哈,二分圖就是兩個集合之間有聯絡,然而,就知道,他們有無數個源點與無數個匯點,所以呢,就要把它變換成只有一個源點與一個匯點的網路,這就需要超級原點與超級匯點!之後呢,就把每條邊的流量改成1就迎刃而解了。

求最大匹配步驟:

(1)增加一個源點s和一個匯點t;

(2)從s向集合X的每一個頂點引一條有向邊,從集合Y的每一個頂點向t引一條有向邊;

(3)將原圖的每條邊改為從集合X向集合Y的有向邊;

(4)置每條邊的容量為1;

程式碼以後附上吧。。。

MCMF(最小費用最大流):

顧名思義,就是求最大流花費的最小費用,對於費用(就是最短路啊)可以用SPFA來解(不能用dij,因為反向邊是負值),所以,就是在以上求最大流的基礎上加上SPFA及妥妥的!

程式碼:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;


const int maxn=100010;

bool vis[maxn];
int n,m,s,t,x,y,z,f,dis[maxn],pre[maxn],last[maxn],flow[maxn],maxflow,mincost;
struct Edge{
    int to,next,flow,dis;
}edge[maxn];
int head[maxn],num_edge; 
queue <int> q;

void add_edge(int from,int to,int flow,int dis)
{
    edge[++num_edge].next=head[from];
    edge[num_edge].to=to;
    edge[num_edge].flow=flow;
    edge[num_edge].dis=dis;
    head[from]=num_edge;
}

bool spfa(int s,int t)
{
    memset(dis,0x7f,sizeof(dis));
    memset(flow,0x7f,sizeof(flow));
    memset(vis,0,sizeof(vis));
    q.push(s); vis[s]=1; dis[s]=0; pre[t]=-1;

    while (!q.empty())
    {
        int now=q.front();
        q.pop();
        vis[now]=0;
        for (int i=head[now]; i!=-1; i=edge[i].next)
        {
            if (edge[i].flow>0 && dis[edge[i].to]>dis[now]+edge[i].dis)
            {
                dis[edge[i].to]=dis[now]+edge[i].dis;
                pre[edge[i].to]=now;
                last[edge[i].to]=i;
                flow[edge[i].to]=min(flow[now],edge[i].flow);
                if (!vis[edge[i].to])
                {
                    vis[edge[i].to]=1;
                    q.push(edge[i].to);
                }
            }
        }
    }
    return pre[t]!=-1;
}

void MCMF()
{
    while (spfa(s,t))
    {
        int now=t;
        maxflow+=flow[t];
        mincost+=flow[t]*dis[t];
        while (now!=s)
        {
            edge[last[now]].flow-=flow[t];
            edge[last[now]^1].flow+=flow[t];
            now=pre[now];
        }
    }
}

int main()
{
    memset(head,-1,sizeof(head)); num_edge=-1;
    scanf("%d%d%d%d",&n,&m,&s,&t);
    for (int i=1; i<=m; i++)
    {
        scanf("%d%d%d%d",&x,&y,&z,&f);
        add_edge(x,y,z,f); add_edge(y,x,0,-f);
    }
    MCMF();
    printf("%d %d",maxflow,mincost);
    return 0;
}

上下限有待更新。。。