網絡流基礎

網絡流基礎

網絡流問題

相關概念:node

  • 源點:有n個點,有m條有向邊,有一個點很特殊,只出不進,叫作源點。
  • 匯點:另外一個點也很特殊,只進不出,叫作匯點。
  • 容量和流量:每條有向邊上有兩個量,容量和流量,從i到j的容量一般用c[i,j]表示,流量則一般是f[i,j]。
  • 最大流:通俗點解釋,就比如你有不少貨物要從源點點運到匯點點,有向圖中的一條邊表明一條公路,每條公路有固定的貨物裝載限制(容量),對每條公路你只能運輸必定數量的貨物,問你每一次運輸最多運到匯點點多少貨物。

給定指定的一個有向圖,其中有兩個特殊的源點S和匯點T,每條邊有指定的容量,求知足條件的從S到T的最大流。ios

網絡流的性質

  • 容量限制:f[u,v]<=c[u,v]
  • 反對稱性:f[u,v] = - f[v,u]
  • 流量平衡:對於不是源點也不是匯點的任意結點,流入該結點的流量和等於流出該結點的流量和。

殘量網絡,容量網絡,流量網絡

殘量網絡=容量網絡-流量網絡
概念就不講了吧,顧名思義。算法

增廣路

增廣路: 設 f 是一個容量網絡 G 中的一個可行流, P 是從 Vs 到 Vt 的一條鏈, 若 P 知足下列條件:網絡

  • 在 P 的全部前向弧 <u, v> 上, , 即 P+ 中每一條弧都是非飽和弧;
  • 在 P 的全部後向弧 <u, v> 上, , 即 P– 中每一條弧是非零流弧。

則稱 P 爲關於可行流 f 的一條增廣路, 簡稱爲 增廣路(或稱爲增廣鏈、可改進路)。沿着增廣路改進可行流的操做稱爲增廣優化

最小割最大流定理

割,割集

對於一張流量圖G,斷開一些邊後,源點s和匯點t就不在連通,咱們將這樣的k條邊的權值(即最大容量)和求和,求和後的值稱爲割。顯然,對於一張流量圖G,割有不少個且不盡相同。咱們要求的就是全部割中權值最小的那一個(可能不惟一),即花最小的代價使s和t不在同一集合中。spa

最小割最大流定理

  • 任意一個流都小於等於任意一個割
  • 構造出一個流等於一個割
  • 在一張流量圖G中,最大流=最小割。

網絡流問題解決方法

FF方法(Ford-Fulkerson)code

基本思想

根據增廣路定理, 爲了獲得最大流, 能夠從任何一個可行流開始, 沿着增廣路對網絡流進行增廣, 直到網絡中不存在增廣路爲止,這樣的算法稱爲增廣路算法。問題的關鍵在於如何有效地找到增廣路, 並保證算法在有限次增廣後必定終止。
FF方法的基本流程是 :blog

  • (1) 取一個可行流 f 做爲初始流(若是沒有給定初始流,則取零流 f= { 0 }做爲初始流);
  • (2) 尋找關於 f 的增廣路 P,若是找到,則沿着這條增廣路 P 將 f 改進成一個更大的流, 並創建相應的反向弧;
  • (3) 重複第(2)步直到 f 不存在增廣路爲止。



反向弧創建的意義:爲程序提供反悔的機會

很明顯,上圖最大流應該是2,但咱們找到了一條錯誤的路徑,因而咱們就應該有返回的機會,即創建反向邊,這樣再次從反向邊流過就至關於抵消了。隊列

算法一:EK算法(EdmondsKarp)

算法思路

在EK算法中, 程序的實現過程與增廣路求最大流的過程基本一致. 即每一次更新都進行一次找增廣路而後更新路徑上的流量的過程。可是咱們能夠從上圖中發現一個問題, 就是每次找到的增廣路曲曲折折很是長, 此時咱們每每走了冤枉路(即:明明咱們能夠從源點離匯點越走越近的,但是中間的幾條邊卻向離匯點遠的方向走了), 此時更新增廣路的複雜度就會增長。EK 算法爲了規避這個問題使用了 bfs 來尋找增廣路, 而後在尋找增廣路的時候老是向離匯點愈來愈近的方向去尋找下一個結點。博客

複雜度$\varTheta(m^{2}n)$

代碼

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define INF 0x7fffffff
#define N 10010
#define M 100010
using namespace std;
int n,m,ss,tt;
struct Edge{int to;int next;int value;}e[M<<1];
struct Pre{int node;int id;}pre[M<<1];//pre[i].node表示編號爲i的點最短路的上一個點,pre[i].id表示最短路上鍊接i點的邊的編號
int head[N],cnt=-1;//編號從0開始,緣由見下
bool vis[N];
queue<int> q;
void add(int from,int to,int value)
{
    cnt++;
    e[cnt].to=to;
    e[cnt].value=value;
    e[cnt].next=head[from];
    head[from]=cnt;
}
bool bfs(int s,int t)//用來尋找s,t的最短路並記錄,若是s,t不連通則返回0
{
    q=queue<int>();//清空隊列
    memset(vis,0,sizeof(vis));
    memset(pre,-1,sizeof(pre));
    pre[s].node=s;
    vis[s]=1;
    q.push(s);
    while(!q.empty())
    {
        int x=q.front();
        q.pop();
        for(int i=head[x];i>-1;i=e[i].next)
        {
            int now=e[i].to;
            if(!vis[now]&&e[i].value)//忽略流量爲0的邊
            {
                pre[now].node=x;//用pre記錄最短路
                pre[now].id=i;
                vis[now]=1;
                if(now==t)return 1;//找到
                q.push(now);
            }
        }
    }
    return 0;
}

int EK(int s,int t)
{
    int ans=0;
    while(bfs(s,t))
    {
        int minv=INF;
        for(int i=t;i!=s;i=pre[i].node)
            minv=min(minv,e[pre[i].id].value);
        for(int i=t;i!=s;i=pre[i].node)
        {
            e[pre[i].id].value-=minv;
            e[pre[i].id^1].value+=minv;//x^1表示x邊的反向邊,此方法僅在邊的編號從0開始時有效
        }
        ans+=minv;
    }
    return ans;
}
int main()
{
    memset(head,-1,sizeof(head));
    scanf("%d%d%d%d",&n,&m,&ss,&tt);
    for(int i=1;i<=m;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
        add(b,a,0);//創建反向邊
    }
    printf("%d\n",EK(ss,tt));
    return 0;
}

算法二:Dinic算法

其實Dinic算法是EK算法的改進

算法思路

發如今EK算法中,每增廣一次都要先進行bfs尋找最短增廣路,然而bfs後,極可能不止一條路徑能夠增廣,若是仍是按照EK算法的bfs一次增廣一條路,很顯然浪費了不少時間,這樣,咱們讓bfs負責尋找增廣路徑,dfs計算可行的最大流。

下圖1點爲s點,6點爲t點,紅線表明尋找的路徑,藍線表明回溯的路徑:

  • 圖1,bfs計算dis
    • 圖2,dfs按最短路找到t點,累加路徑上的最小容量
    • 圖3,回溯,順便更新正邊和反向邊的邊權
    • 無其餘路徑,回溯到源點
  • 圖4,再次bfs更新dis
    • 圖5,dfs按最短路找到t點,累加路徑上的最小容量
    • 圖6,回溯,順便更新正邊和反向邊的邊權
    • 無符合要求的其餘路徑,回溯到源點
  • 再次bfs,發現s和t不連通,結束算法

複雜度:

在普通圖中:$\varTheta(n^{2}m)$
在二分圖中:$\varTheta(m\sqrt{n})$

代碼

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define N 10010
#define M 100010
#define INF 0x7fffffff
using namespace std;
int n,m,ss,tt;
int dis[N];
queue<int> q;

struct Edge{int to;int value;int next;}e[M<<1];
int head[N],cnt=-1;
void add(int from,int to,int value)
{
    cnt++;
    e[cnt].to=to;
    e[cnt].value=value;
    e[cnt].next=head[from];
    head[from]=cnt;
}

bool bfs(int s,int t)//bfs功能和EK算法的類似,不一樣的是Dinic中的bfs要求出全部點到源點s的最短路dis[i]
{
    q=queue<int>();//清空隊列
    memset(dis,-1,sizeof(dis));
    dis[s]=0;
    q.push(s);
    while(!q.empty())
    {
        int x=q.front();
        q.pop();
        for(int i=head[x];i>-1;i=e[i].next)
        {
            int now=e[i].to;
            if(dis[now]==-1&&e[i].value!=0)
            {
                dis[now]=dis[x]+1;
                q.push(now);
            }
        }
    }
    return dis[t]!=-1;
}
int dfs(int x,int t,int maxflow)//表示從x出發尋找到匯點T的增廣路,尋找到maxflow流量爲止,並相應的增廣。返回值爲實際增廣了多少(由於有可能找不到maxflow流量的增廣路)
{
    if(x==t)return maxflow;
    int ans=0;
    for(int i=head[x];i>-1;i=e[i].next)
    {
        int now=e[i].to;
        if(dis[now]!=dis[x]+1||e[i].value==0||ans>=maxflow)continue;
        int f=dfs(now,t,min(e[i].value,maxflow-ans));
        e[i].value-=f;
        e[i^1].value+=f;
        ans+=f;
    }
    return ans;
}
int Dinic(int s,int t)
{
    int ans=0;
    while(bfs(s,t))
        ans+=dfs(s,t,INF);
    return ans;
}
int main()
{
    memset(head,-1,sizeof(head));
    scanf("%d%d%d%d",&n,&m,&ss,&tt);
    for(int i=1;i<=m;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
        add(b,a,0);
    }
    printf("%d\n",Dinic(ss,tt));
    return 0;
}

當前弧優化

咱們知道Dinic算法中的dfs是爲了在可行增廣路中找到最小容量並進行增廣。而找增廣路須要遍歷每一個點所鏈接的邊,直至找到一條可到達終點的路。若是這一次找到了增廣路,下一次在訪問到這個點時,上一次已經檢查過的邊就不用再走一遍了,由於遍歷一個點鏈接的邊都是有必定順序的,上一次訪問到這個點已經肯定那幾條邊是不可行的。因而,咱們用cur[i]來表示下一次遍歷邊時應該從那一條開始。

雖然漸進時間複雜度沒有發生變化,但實際應用中的確大大下降了Dinic的常數

優化代碼(其餘代碼不發生變化)

int cur[N];

int dfs(int x,int t,int maxflow)
{
    if(x==t)return maxflow;
    int ans=0;
    for(int i=cur[x];i>-1;i=e[i].next)
    {
        int now=e[i].to;
        if(dis[now]!=dis[x]+1||e[i].value==0||ans>=maxflow)continue;
        cur[x]=i;//此路可行,記錄此路
        int f=dfs(now,t,min(e[i].value,maxflow-ans));
        e[i].value-=f;
        e[i^1].value+=f;
        ans+=f;
    }
    return ans;
}
int Dinic(int s,int t)
{
    int ans=0;
    while(bfs(s,t))
    {
        memcpy(cur,head,sizeof(head));//初始化
        ans+=dfs(s,t,INF);
    }
    return ans;
}

網絡流的優化算法還有ISAP(Improved Shortest Augumenting Path),最高標號預流推動(HLPP)等等,Dinic在通常狀況下已經夠用了,其餘算法自學請移步其餘大佬博客嘍。

相關文章
相關標籤/搜索