1. 程式人生 > >poj-1459-最大流dinic+鏈式前向星

poj-1459-最大流dinic+鏈式前向星


title: poj-1459-最大流dinic+鏈式前向星
date: 2018-11-22 20:57:54
tags:

  • acm
  • 刷題
    categories:
  • ACM-網路流-最大流

概述

這道是一道網路流裡最大流的板子題,,,

暑期集訓網路流草草水過,,連基本的演算法都不知道有哪些,,,更別提怎麼實現了,,,只知道網路流的大致的概念,,

今天花了一天的時間重新學習了一波,,,本以為這東西很簡單,,,沒想到不僅演算法的實現一大堆的東西,,就連題目都有時候看不懂,,,,感受就是網路流的題不僅演算法實現起來不好懂,,,每一道題的建圖也很關鍵,,,幾乎看到的每一道題的圖都是得自己去建,,完全不像最短路那些題花裡胡哨的東西都只改一改貪心時的方程就行,,,

分析思路

最短路的一些基本概念

這一段 演算法導論 上講的很好,,,不過我感覺還是在基本弄懂那幾個演算法再看一遍比較好QAQ

這裡就直接摘抄別人寫過的東西了

容量網路和網路最大流

容量網路:

\(設 G(V, E)是一個有向網路, 在 V 中指定了一個頂點, 稱為源點(記為 Vs ), 以及另一個頂點, 稱為匯點(記為 Vt); 對於每一條弧 <u, v>∈E, 對應有一個權值 c(u, v)>0, 稱為弧的容量, 通常把這樣的有向網路 G 稱為容量網路。\)

把它想象成 自來水廠自來水管網使用者 那種圖就行了,,,

弧的流量:

通過容量網路 G 中每條弧 <u, v> 上的實際流量(簡稱流量), 記為 \(f(u, v)\)

網路流: 所有弧上流量的集合 f = { f(u, v) },稱為該容量網路 G 的一個網路流。

可行流: 在容量網路 G(V, E) 中, 滿足以下條件的網路流 f, 稱為可行流:

弧流量限制條件: \(0≤f(u,v)≤c(u,v)\)

平衡條件:

除了 Vs, Vt 外, 其餘的點流入的流量總和等於流出的流量總和, 其中 Vs 流出的流量總和 - 流出的流量總和 = f, Vt 流入的流量總和 - 流出的流量總和 = f, 並且稱 f 為可性流的流量

也就是指: \(在圖中有一條從 Vs 到 Vt 的路徑, 這條路徑上起點 fo−fi=f, 終點 fi−fo=f, 其他的點 fi==fo, 並且所有的邊的當前流量小於等於最大流量.(其中 fi 代表流入流量, fo 代表流出流量)\)

偽流:

如果一個網路流只滿足弧流量限制條件, 不滿足平衡條件, 則這種網路流稱為偽流, 或稱為容量可行流。

最大流:

在容量網路 G(V, E) 中, 滿足弧流量限制條件和平衡條件、且具有最大流量的可行流, 稱為網路最大流, 簡稱最大流。

鏈與增廣路

在容量網路 G(V, E) 中, 設有一可行流 f = { f(u, v) }, 根據每條弧上流量的多少、以及流量和容量的關係,可將弧分四種類型:

飽和弧, 即 f(u,v)=c(u,v);
非飽和弧,即 f(u,v)<c(u,v);
零流弧, 即 f(u,v)=0;
非零流弧, 即 f(u,v)>0。

:

在容量網路中,稱頂點序列(u,u1,u2,…,un,v)為一條鏈,要求相鄰兩個頂點之間有一條弧, 如 <u, u1> 或 <u1, u> 為容量網路中一條弧。沿著 Vs 到 Vt 的一條鏈, 各弧可分為兩類:

  • 前向弧: 方向與鏈的正方向一致的弧, 其集合記為 P+;
  • 後向弧: 方向與鏈的正方向相反的弧, 其集合記為 P-;

    增廣路:

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

在 P 的所有前向弧 <u, v> 上, 0≤f(u,v)<c(u,v), 即 P+ 中每一條弧都是非飽和弧;
在 P 的所有後向弧 <u, v> 上, 0<f(u,v)≤c(u,v), 即 P– 中每一條弧是非零流弧。
則稱 P 為關於可行流 f 的一條增廣路, 簡稱為 增廣路(或稱為增廣鏈、可改進路)沿著增廣路改進可行流的操作稱為增廣.

殘留容量與殘留網路

殘留容量:

給定容量網路 G(V, E) 及可行流 f, 弧 <u, v> 上的殘留容量記為 c′(u,v)=c(u,v)–f(u,v)。每條弧的殘留容量表示該弧上可以增加的流量。因為從頂點 u 到頂點 v 流量的減少, 等效於頂點 v 到頂點 u 流量增加, 所以每條弧 <u, v> 上還有一個反方向的殘留容量 c′(v,u)=–f(u,v)。

  • 一個容量網路中還可以壓入的流量稱為殘留容量

殘留網路:

\(設有容量網路 G(V, E) 及其上的網路流 f,G 關於 f 的殘留網路(簡稱殘留網路)記為 G'(V', E'), 其中 G’的頂點集 V’和 G 的頂點集 V 相同,即 V’=V, 對於 G 中的任何一條弧 <u, v>, 如果 f(u,v)<c(u,v), 那麼在 G’中有一條弧 <u, v>∈E', 其容量為 c′(u,v)=c(u,v)–f(u,v), 如果 f(u,v)>0,則在 G’中有一條弧 <v, u>∈E', 其容量為 c′(v,u)=f(u,v), 殘留網路也稱為剩餘網路.\)

  • 由殘留的容量以及源點匯點構成的網路。

割與最小割

: \(在容量網路 G(V, E) 中, 設 E'⊆E, 如果在 G 的基圖中刪去 E’ 後不再連通, 則稱 E’ 是 G 的割。割將 G 的頂點集 V 劃分成兩個子集 S 和 T = V - S。將割記為(S, T)。 s-t 割: 更進一步, 如果割所劃分的兩個頂點子集滿足源點 Vs ∈ S,匯點 Vt ∈ T, 則稱該割為 s-t 割。 s-t 割(S, T)中的弧 <u, v>(u∈S, v∈T) 稱為割的前向弧, 弧 <u, v>( u∈T, v∈S) 稱為割的反向弧。\)

割的容量:\(設 (S, T) 為容量網路 G(V, E) 的一個割, 其容量定義為所有前向弧的容量總和, 用 c(S, T) 表示。\)

最小割: \(容量網路 G(V, E) 的最小割是指容量最小的割。\)

相關定理

殘留網路與原網路的關係

\(設 f 是容量網路 G(V, E) 的可行流, f’ 是殘留網路 G’ 的可行流, 則 f + f’ 仍是容量網路 G 的一個可行流。(f + f’ 表示對應弧上的流量相加)\)

網路流流量與割的淨流量之間的關係

\(在一個容量網路 G(V, E) 中, 設其任意一個流為 f, 關於 f 的任意一個割為(S, T), 則有 f(S,T)=|f|,即網路流的流量等於任何割的淨流量。\)

網路流流量與割的容量之間的關係

\(在一個容量網路 G(V, E) 中, 設其任意一個流為 f, 任意一個割為(S, T), 則必有 f(S,T)≤c(S,T),即網路流的流量小於或等於任何割的容量。\)

最大流最小割定理

\(對容量網路 G(V, E), 其最大流的流量等於最小割的容量。\)

增廣路定理

\(設容量網路 G(V, E) 的一個可行流為 f, f 為最大流的充要條件是在容量網路中不存在增廣路。\)

幾個等價命題

\(設容量網路 G(V, E)的一個可行流為 f 則:\)

\(1) f 是容量網路 G 的最大流;\)

\(2) | f |等於容量網路最小割的容量;\)

\(3) 容量網路中不存在增廣路;\)

\(4) 殘留網路 G’中不存在從源點到匯點的路徑。\)

最大流

最大流相關演算法有兩種解決思想, 一種是增廣路演算法思想, 另一種是預流推進演算法思想。

增廣路演算法

基本思想

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

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

圖示如下:

增廣路演算法的關鍵是 尋找增廣路改進網路流.

建立反向弧的作用:

為程式提供一次返回的機會

在圖中如果程式找到了一條增廣路 1 -> 2 -> 4 -> 6, 此時得到一個流量為 2 的流並且無法繼續進行增廣,
但是如果在更新可行流的同時建立反向弧的話, 就可以找到 1 -> 3 -> 4 -> 2 -> 5 -> 6 的可行流, 流量為1, 這樣就可以得到最大流為 3.

dinic模板程式

因為ek演算法的效率沒有dinic的高,,所以本著先追求實用主義就先看了dinic演算法,,,演算法實現的模板時kaungbin的,,,dinic+鏈式前向星。。。

演算法思想

DINIC 在找增廣路的時候也是找的最短增廣路, 與 EK 演算法不同的是 DINIC 演算法並不是每次 bfs 只找一個增廣路, 他會首先通過一次 bfs 為所有點新增一個標號, 構成一個層次圖, 然後在層次圖中尋找增廣路進行更新。

實現流程

  • 1.利用 BFS 對原來的圖進行分層,即對每個結點進行標號,這個標號的含義是當前結點距離源點的最短距離(假設每條邊的距離都為1),注意:構建層次圖的時候所走的邊的殘餘流量必須大於0

  • 2.用 DFS 尋找一條從源點到匯點的增廣路, 注意: 此處尋找增廣路的時候要按照層次圖的順序, 即如果將邊(u, v)納入這條增廣路的話必須滿足dis[u]=dis[v]−1, 其中 dis[i]為結點 i的編號。找到一條路後要根據這條增廣路徑上的所有邊的殘餘流量的最小值l更新所有邊的殘餘流量(即正向弧 - l, 反向弧 + l).

  • 3。重複步驟 2, 當找不到一條增廣路的時候, 重複步驟 1, 重新建立層次圖, 直到從源點不能到達匯點為止。

思路

這道題的題意是給你n個源點(發電站)、np箇中間路徑點(中轉站)、nc個匯點(使用者)以及m個通路。。求最大送到使用者的效率也就是圖的最大流。。

多個源點和匯點所以要弄一個超級源點s超級匯點t,,,s,t連源點,匯點然後跑dinic就行了,,

具體的程式碼的細節都註釋在裡面了,,,都是自己的理解可能有誤,,,看的頭疼.jpg

//dinic求網路流的最大流
//bfs求一次層次圖
//dfs求源點到匯點的一條增廣路
//然後根據這條增廣路中殘餘流量的最小值tp來更新所有邊的殘餘流量
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <vector>
#include <string.h>

using namespace std;

const int maxn = 105;
const int maxm = 1e5 + 10;
const int inf = 0x3f3f3f3f;

int n , np , nc , m;
int u , v , z;
//前向星存圖
struct edge
{
    int to;
    int next;
    int cap;        //容量
    int flow;       //流量
}edge[maxm];        //注意邊數為所給邊數的兩倍多
int tol;
int head[maxn];
void init()
{
    tol = 2;        //???
    memset(head , -1 , sizeof head);
}
void addedge(int u , int v , int w , int rw = 0)
{
    //前向星加邊,反向弧容量為rw一般為0
    //正反弧相鄰儲存,直接異或就能找到
    //正向弧的編號要比反向弧的編號小
    edge[tol].to = v;edge[tol].cap = w;edge[tol].flow = 0;
    edge[tol].next = head[u];head[u] = tol++;
    edge[tol].to = u; edge[tol].cap = rw;edge[tol].flow = 0;
    edge[tol].next = head[v];head[v] = tol++;
}
int q[maxn];        //雙向佇列,bfs使用
int dep[maxn] , cur[maxn] , sta[maxn];//sta儲存增廣路的邊
bool bfs(int s , int t , int n)
{
    //bfs搜尋網路的層次
    int front = 0;
    int tail = 0;
    memset(dep , -1 , sizeof(dep[0]) * (n + 1));
    dep[s] = 0;
    q[tail++] = s;
    while(front < tail)
    {
        int u = q[front++];
        //前向星圖的遍歷
        for(int i = head[u]; ~i; i = edge[i].next)
        {
            int v = edge[i].to;
            if(edge[i].cap > edge[i].flow && !(~dep[v]))
            {
                //(u,v)這條邊的容量大於流量時即殘餘流量大於0並且這個點沒有被分層時
                dep[v] = dep[u] + 1;    //分層
                if(v == t)return true;
                q[tail++] = v;
            }
        }
    }
    return false;
}
int dinic(int s , int t , int n)
{
    int maxflow = 0;            //待求的最大流
    while(bfs(s , t , n))       //當層次圖存在時進行dfs尋找增廣路
    {
        for(int i = 0; i < n; ++i)cur[i] = head[i]; //當前所有可以利用鏈式前向星遍歷的邊的編號
        int u = s , tail = 0;   //tail表示找到的增廣路的點的數量
        while(~cur[s])          //邊合法時
        {
            if(u == t)          //找到匯點時,即找到一條增廣路時
            {
                int tp = inf;   //tp為該增廣路中最小的殘餘流量
                //找到最小值
                for(int i = tail - 1; i >= 0; --i)
                    tp = min(tp , edge[sta[i]].cap - edge[sta[i]].flow);
                maxflow += tp;  //最大流增加
                for(int i = tail - 1; i >= 0; --i)
                {
                    //用最小的殘餘流量更新參與網路
                    //這裡是倒著遍歷每一條增廣路中的邊,,
                    //所以編號是由大到小,sta[i]是(u,v)那條弧的編號,sta[i] ^ 1是其反向弧的編號
                    //正向弧的流入流量加上tp
                    //反向弧的流入流量就是減去tp
                    edge[sta[i]].flow += tp;
                    edge[sta[i] ^ 1].flow -= tp;
                    //這條路的殘餘流量為零,經過這條路徑的增廣路不再存在
                    //增廣路的尾邊縮回到這個點
                    //並嘗試尋找經過這個點的其他的增廣路
                    if(edge[sta[i]].cap - edge[sta[i]].flow == 0)
                        tail = i;
                }
                //當前增廣路的尾邊回退到上一個點,,繼續搜尋其他的增廣路
                u = edge[sta[tail] ^ 1].to;
            }
            else if(~cur[u] &&
                    edge[cur[u]].cap > edge[cur[u]].flow &&
                    dep[u] + 1 == dep[edge[cur[u]].to])
            {
                //當這條邊能到達、殘餘流量為正值並且u是v的上一層的點時
                sta[tail++] = cur[u];   //增廣路的點數tail++,並儲存這條邊到sta
                u = edge[cur[u]].to;    //更新u
            }
            else
            {
                //回退??
                //while(u != s && cur[u] == -1)
                while(u != s && !(~cur[u]))
                    u = edge[sta[--tail] ^ 1].to;
                cur[u] = edge[cur[u]].next;
            }
        }
    }
    return maxflow;
}
int main()
{
    while(scanf("%d%d%d%d " , &n , &np , &nc , &m) != EOF)
    {
        init();
        while(m--)
        {
            scanf(" (%d,%d)%d" , &u , &v , &z); //輸入前面有空格
            ++u;++v;
            addedge(u , v , z);
        }
        while(np--)
        {
            scanf(" (%d)%d" , &u , &z);
            ++u;
            addedge(0 , u , z);                 //超級源點
        }
        while(nc--)
        {
            scanf(" (%d)%d" , &u , &z);
            ++u;
            addedge(u , n + 1 , z);             //超級匯點
        }
        printf("%d\n" , dinic(0 , n + 1 , n + 1));
    }
}

下一個就是sap,isap了吧,,,頭疼ing

(end)