日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

网络流学习笔记

發(fā)布時間:2023/12/10 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 网络流学习笔记 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

網(wǎng)絡(luò)流學(xué)習(xí)筆記:

$ by~~wch $

前言:

本文篇幅較長,結(jié)合右上角的目錄了瀏覽會方便一些

然后本文主要還是自己復(fù)習(xí)所用,會偏向講一些(博主經(jīng)常忘的)核心,有些地方講得粗略請諒解,所以大家可以對書看,書上都是大佬寫的比較全面。(額,假定大家都有認(rèn)真看書)

然后基本概念庫里的知識比較多,實在接受不了直接往后看算法,博主盡量會在算法前標(biāo)注需要的基本概念(或者直接講),大家再回來挑著反復(fù)看就好。但是盡量要明白算法核心,只要懂了最大流的原理,這些基本概念會顯得比較簡單。另外帶*號的可以忽略,博主也不會講。

然后我們要有能學(xué)好網(wǎng)絡(luò)流的信心,這個很重要!




一、基本概念庫:

本文中弧和邊是一個東西,然后大家要注意容量流量是不同的兩個概念;殘留容量和剩余流量是一個東西,但他們和實際流量要區(qū)分開來!

容量網(wǎng)絡(luò)和網(wǎng)絡(luò)最大流:

容量網(wǎng)絡(luò): 設(shè) $ G(V, E) $ 是一個有向圖, $ V $ 為點集, $ E $ 為邊集,在 $ V $ 中有兩個指定的特殊頂點:源點( $ S $ )和匯點( $ T $ )。每一條弧(邊) $ <u, v>∈E $ ,都有一個給定權(quán)值 $ c(u, v) $ ,稱為弧的容量。這樣的有向網(wǎng)絡(luò) $ G $ 被稱為容量網(wǎng)絡(luò)。

弧的流量: 通過容量網(wǎng)絡(luò) $ G $ 中每條弧 $ <u, v> $ 上的實際流量(簡稱流量),記為 $ f(u, v) $ ,其基本定律如下 :

  • 容量限制: $ 0\leq f(x,y)\leq c(x,y) ~~ $ 講人話:邊流量不超過其容量
  • 斜對稱: $ f(x,y)=-f(y,x) ~~ $ 講人話:存在流量為負(fù)的反向邊
  • 流量守恒: $ \forall x \not= S,T\quad \sum_{(u,x)\in E}f(u,x)=\sum_{(x,v)\in E}f(x,v) ~~ $ 講人話:流進這條邊的總流量等于流出這條邊的總流量
  • 注意這三個基本定律很重要,然后第二個定律不要和后文的反向邊 $ f(x,y)=c(y,x) $ 弄混!

    網(wǎng)絡(luò)流: 所有弧上流量(流量可以為零)的集合 $ f(S,T)=\sum_{(u,v)\in E} f(u, v) $ ,稱為該容量網(wǎng)絡(luò) $ G $ 的一個網(wǎng)絡(luò)流。
    可行流: 在容量網(wǎng)絡(luò) $ G(V, E) $ 中,滿足以下條件的網(wǎng)絡(luò)流 $ f $ ,稱為可行流:

  • 弧流量限制條件:可行流上所有弧流量不超過其容量,且不為負(fù)。
  • 平衡條件:除了源點和匯點,其他點和邊流入的流量等于流出的流量。
  • 其中 $ S $ 流出的流量總和 = $ T $ 流入的流量總和 = $ f $ , 并且稱 $ f $ 為可行流的流量.

    偽流: 如果一個網(wǎng)絡(luò)流只滿足弧流量限制條件,不滿足平衡條件,則這種網(wǎng)絡(luò)流稱為偽流,或稱為容量可行流。
    最大流: 在容量網(wǎng)絡(luò) $ G(V, E) $ 中,滿足弧流量限制條件和平衡條件、且具有最大流量的可行流,稱為網(wǎng)絡(luò)最大流,簡稱最大流。



    鏈與增廣路:

    在容量網(wǎng)絡(luò) $ G(V, E) $ 中,設(shè)有一可行流 $ f =\sum_{(u,v)\in E} f(u, v) $ ,根據(jù)每條弧上流量的多少、以及流量和容量的關(guān)系,可將弧分四種類型:

  • 飽和弧:即達到容量上限的弧 $ f(u,v)=c(u,v) $ ;
  • 非飽和弧:即沒有流滿的弧 $ f(u,v)<c(u,v) $ ;
  • 零流弧:即流量為零 $ f(u,v)=0 $ ;
  • 非零流弧:即流量大于零 $ f(u,v)>0 $ 。
  • 鏈:在容量網(wǎng)絡(luò)中,稱一串連續(xù)相鄰的頂點序列 $ (u,u_1,u_2,…,u_n,v) $ 為一條鏈,相鄰兩個頂點之間都有一條弧,沿著源點到匯點方向的一條鏈,各弧可分為兩類:

    前向弧:方向與鏈的正方向一致的弧,其集合記為 $ P+ $ ;
    后向弧:方向與鏈的正方向相反的弧,其集合記為 $ P- $ ;
    增廣路:設(shè) $ f $ 是一個容量網(wǎng)絡(luò) $ G $ 中的一個可行流, $ P $ 是從 $ S $ 到 $ T $ 的一條鏈,若 $ P $ 滿足下列條件:

  • 在 $ P $ 的所有前向弧 $ <u, v> $ 上, $ 0≤f(u,v)<c(u,v) $ ,即 $ P+ $ 中每一條弧都是非飽和弧
  • 在 $ P $ 的所有后向弧 $ <u, v> $ 上, $ 0<f(u,v)≤c(u,v) $ ,即 $ P– $ 中每一條弧都是非零流弧
  • 則稱 $ P $ 為關(guān)于可行流 $ f $ 的一條增廣路。沿著增廣路改進可行流(增加可行流流量)的操作稱為增廣。



    * 二分圖最大匹配:

    必須邊: 若 $ (u,v) $ 屬于任意一個最大匹配方案
    必須邊性質(zhì): $ (u,v) $ 是當(dāng)前二分圖的匹配邊,且 $ u $ 和 $ v $ 屬于兩個不同的強連通分量
    可行邊:若 $ (u,v) $ 屬于至少一個最大匹配方案
    可行邊性質(zhì): $ (u,v) $ 是當(dāng)前二分圖的匹配邊,且 $ u $ 和 $ v $ 屬于兩個不同的強連通分量



    殘留容量與殘留網(wǎng)絡(luò):

    殘留容量: 給定容量網(wǎng)絡(luò) $ G(V, E) $ 及可行流 $ f $ ,弧 $ <u, v> $ 上的殘留容量記為 $ c′(u,v)=c(u,v)–f(u,v) $ 。每條弧的殘留容量表示該弧上可以增加的流量。

    反向邊: 一條實際不存在的邊,它是正向邊的另一種表達方式(好比我的流量增加-10,反過來就是我們流量減少10),所以正向邊發(fā)生改變反向邊也要同時發(fā)生改變!

    反向邊殘留容量:因為從節(jié)點 $ u $ 到節(jié)點 $ v $ 流量的減少,等效于頂點 $ v $ 到頂點 $ u $ 流量增加,所以每條弧 $ <u, v> $ 上還有一個反方向的殘留容量 $ c′(v,u)=f(u,v) $ 。(這是一個相對概念,你給我的東西少了,等于我將你本來要多給我的返還給了你

    殘留網(wǎng)絡(luò): 設(shè)有容量網(wǎng)絡(luò) $ G(V, E) $ 及其上的網(wǎng)絡(luò)流 $ f $ , $ G $ 關(guān)于 $ f $ 的殘留網(wǎng)絡(luò)記為 $ G'(V', E') $ ,其中 $ G’ $ 的頂點集 $ V’ $ 和 $ G $ 的頂點集 $ V $ 相同,即 $ V’=V $ ,對于 $ G $ 中的任何一條弧 $ <u, v> $ ,如果 $ f(u,v)<c(u,v) $ ,那么在 $ G’ $ 中有一條對應(yīng)的弧 $ <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) $ ,殘留網(wǎng)絡(luò)也稱為剩余網(wǎng)絡(luò)。 (要注意括號內(nèi)點的順序)



    割與最小割:

    割: 在容量網(wǎng)絡(luò) $ G(V, E) $ 中, 設(shè) $ E'?E $ (注意是邊的集合), 如果在 $ G $ 的基圖中刪去 $ E’ $ 后 $ S $ 和 $ T $ 不再連通,則稱 $ E’ $ 是 $ G $ 的割。

    割的容量: 割的容量定義為割中所有前向弧(從源點到匯點為正方向)的容量總和,用 $ c(S, T) $ 表示。(注意是容量,不是流量!)

    最小割: 容量網(wǎng)絡(luò) $ G(V, E) $ 的最小割是指容量最小的割。



    相關(guān)定理:

    殘留網(wǎng)絡(luò)與原網(wǎng)絡(luò)的關(guān)系: 設(shè) $ f $ 是容量網(wǎng)絡(luò) $ G(V, E) $ 的可行流, $ f’ $ 是殘留網(wǎng)絡(luò) $ G’ $ 的可行流,則 $ f + f’ $ 仍是容量網(wǎng)絡(luò) $ G $ 的一個可行流。( $ f + f’ $ 表示對應(yīng)弧上的流量相加)(這是一個增廣操作)

    網(wǎng)絡(luò)流流量與割的容量之間的關(guān)系: 在一個容量網(wǎng)絡(luò) $ G(V, E) $ 中,設(shè)其任意一個網(wǎng)絡(luò)流為 $ f(S,T) $ ,任意一個割為 $ (S, T) $ ,則必有 $ f(S,T)≤c(S,T) $ ,即網(wǎng)絡(luò)流的流量小于或等于任何割的容量。

    最大流最小割定理: 對容量網(wǎng)絡(luò) $ G(V, E) $ ,其最大流的流量等于最小割的容量。

    增廣路定理: 設(shè)容量網(wǎng)絡(luò) $ G(V, E) $ 的一個可行流為 $ f $ , $ f $ 為最大流的充要條件是在容量網(wǎng)絡(luò)中不存在增廣路。

    幾個等價命題: 設(shè)容量網(wǎng)絡(luò) $ G(V, E) $ 的一個可行流為 $ f $ 則以下為等價命題:

  • $ f $ 是容量網(wǎng)絡(luò) $ G $ 的最大流;
  • $ f $ 等于容量網(wǎng)絡(luò)最小割的容量;
  • 容量網(wǎng)絡(luò)中不存在增廣路;
  • 殘留網(wǎng)絡(luò) $ G’ $ 中不存在從源點到匯點的路徑。



  • 二、網(wǎng)絡(luò)最大流:

    1. $ EK $ 與 $ Dinic $ 原理講解

    注:請先閱讀基本概念中的網(wǎng)絡(luò)流和增廣路的書面意義,以及殘量網(wǎng)絡(luò)和反向邊,可適當(dāng)結(jié)合書閱讀下文

    本文著重介紹 $ Edmonds-Karp $ 算法(在費用流里很重要),以及 $ Dinic $ 算法(跑最大流的常用算法,比前面那種快)。它們都是用增廣路來求的最大流,增廣路是一個核心概念算法基礎(chǔ),弄懂它學(xué)網(wǎng)絡(luò)流會更得心應(yīng)手,所以本段會花大篇幅講解。

    算法原理: $ EK $ 與 $ Dinic $ 其實都是增廣路做法。那么增廣路究竟是怎樣使我們得到最大流的呢?我們在基本概念里提到過一個叫反向邊的東西,它是增廣路的核心。我們從源點到匯點隨便一個 $ BFS $ (或 $ DFS $ )就能得到一個網(wǎng)絡(luò)流,但是這個流不一定是最大流。我們要讓它擴展成最大流,那么勢必會有一個轉(zhuǎn)向操作(就是我們將某個點流向某條邊的流量收回,然后將這些流量流向另一條邊)。這相當(dāng)于我們需要對之前的選擇“反悔”,而反向邊成全了我們。

    反向邊是一個抽象概念,我們在腦海里構(gòu)建一個模型(博主能力有限,暫難配圖,諒解一下),有一條有流量的弧 $ f $ (或者說邊),量為 $ c(u,v) $ ,量為 $ f(u,v) $ ,連接一個起點 $ u $ 和終點 $ v $ 。流過我們起點的流量可能大于 $ f(u,v) $ ,流過終點的流量也可能大于 $ f(u,v) $ (因為可能有流量從其它邊流到這個點,再從另外一些邊流走)。

    這條邊存在一條反向邊,反向邊是一個實際并不存在的抽象概念,它代表這條邊我們可以收走(轉(zhuǎn)走)的流量,所以其可用容量就是這條正向邊的量 $ f(u,v) $ (注意反向邊的剩余容量和正向邊的流量密切相關(guān),改變其中一個另一個就會改變)。我們 $ BFS $ (或 $ DFS $ )流過這條反向邊就相當(dāng)于:流過起點 $ u $ (注意字母,自己畫圖,“起點”二字是根據(jù)正向邊定義的)流過起點 $ u $ 的流量不變,而流過這條邊和終點 $ v $ 的流量減少了(抽象思考就是從終點 $ v $ 收回了一部分流量)然后在起點原本要流向這條邊的流量,被收回又可以流向其它節(jié)點(就是繼續(xù) $ DFS $ )。(整個過程就相當(dāng)于有一部分流量從終點流向了起點。)

    具體地,我們在代碼中用 $ j $ 表示當(dāng)前邊,反向邊建在存邊數(shù)組中下標(biāo)為 $ j~xor~1 $ 的位置。因為我們的這兩條邊密切關(guān)聯(lián),需要同時更改,這樣方便操作。我們的流量流過正向邊時,正向邊的剩余容量會減少,相當(dāng)于我們反向邊的剩余容量增加,這個我們要一起更改:

    b[j].v-=f; //正向邊剩余容量減少 b[j^1].v+=f; //反向邊剩余流量增加

    每一次我們增廣路時,會使原網(wǎng)絡(luò)的流量增加,增加的量就是增廣路的流量(可以畫圖理解)。而當(dāng)我們從源點到匯點,已經(jīng)沒有一條正向邊與反向邊組成的剩余容量(可用容量)均為正,就是沒有增廣路時,這個網(wǎng)絡(luò)流就被擴展成了最大流! 可以證明一個非最大流一定含有增廣路,因為我們可以通過轉(zhuǎn)向(跑反向邊)將這個非最大流變成最大流,這就說明存在一條正向邊與反向邊組成的剩余容量(可用容量)均為正的路徑,即增廣路!而尋找這個增廣路 $ DFS $ 顯然最擅長。



    2. 算法的基本實現(xiàn):

    $ EK $ 算法

    就是直接像上面講的一樣,用 $ BFS $ 不斷求增廣路,因為是 $ BFS $ ,所以每次只找一條增廣路。博主看到這個做法時在想:為什么要用 $ BFS $ 呢? $ DFS $ 不行嗎? $ DFS $ 一次或許還能找多條啊! 后來博主發(fā)現(xiàn) $ DFS $ 會有走環(huán)(往回走)的現(xiàn)象,或者說它有可能重復(fù)走以前走過的點,為了防止它重復(fù)走點而跑環(huán)超時,我們設(shè)一個訪問數(shù)組,強制每個點至多走一次。但是這樣也就只能找一條增廣路了,不過這樣 $ DFS $ 確實可以跑最大流,復(fù)雜度和 $ BFS $ 相差不多,就是好寫。

    一個常用的技巧,我們對每一條正向邊都建了一條反向邊,這條邊的容量與正向邊的流量有直接關(guān)系,所以我們在改變其中一條邊時,另一條邊也會同時發(fā)生改變。為了方便操作,我們將反向邊建到數(shù)組中以 $ j~XOR~1 $ 為下標(biāo)的位置,詳細(xì)看下面數(shù)組。自己可以理一理,然后我們的存邊要從1開始 $ top=1 $ 為初值。因為 $ 2~XOR~1=3 $ 所以2和3才是配對的!

    #include<bits/stdc++.h> #define rg register int using namespace std;int n,m,S,T,t=1; int res,ans,top=1; //注意top初值為1,才能保證j^1是反邊 int tou[10005]; //鏈?zhǔn)角跋蛐谴孢?int vis[10005]; //每個點只走一次struct su{int to,v,next; }b[200005];inline bool dfs(int i,int w){ vis[i]=t;if(i==T||!w)return ans+=w,res=w,1; //找到了就直接回溯for(rg j=tou[i];j;j=b[j].next){if(b[j].v&&vis[b[j].to]!=t&&dfs(b[j].to,min(w,b[j].v))){ //三個分別為:還有剩余容量,沒被訪問過,流向兒子的流量不超過其邊的容量b[j].v-=res; b[j^1].v+=res; return 1; //邊回溯邊更新信息}}return 0; //沒找到 }int main(){//freopen(".in","r",stdin);//freopen(".out","w",stdout);scanf("%d%d%d%d",&n,&m,&S,&T);for(rg i=1;i<=m;++i){rg x,y,v; scanf("%d%d%d",&x,&y,&v);b[++top]=su{y,v,tou[x]}; tou[x]=top;b[++top]=su{x,0,tou[y]}; tou[y]=top; //直接++top,因為起始top=1} while(dfs(S,1e9))++t;printf("%d\n",ans);return 0; }

    $ Dinic $ 算法

    上面我們說過 $ DFS $ 為了防止它跑重復(fù)的點我們讓它只找一條增廣路,有沒有辦法讓它能找多條呢?有的,我們多一個 $ BFS $ 分層的過程,這樣它每次 $ DFS $ 向下搜都只會向匯點靠近不會出現(xiàn)跑回來浪費時間的情況,更不可能跑環(huán) 。所以 $ Dinic $ 會不斷重復(fù)以下過程,直到網(wǎng)絡(luò)中沒有增廣路為止:

    1. 在殘量網(wǎng)絡(luò)上 $ BFS $ 給節(jié)點分層,構(gòu)造分層圖:

    inline bool bfs(){for(rg i=1;i<=n;++i)qi[i]=tou[i],dep[i]=0; //第一個是當(dāng)前弧優(yōu)化的初始,我們只需要看后面那個節(jié)點層數(shù)(深度)的初始化queue<int> q; q.push(S); dep[S]=1; //用隊列模擬bfswhile(!q.empty()){rg i=q.front(); q.pop(); //取出隊頭for(rg j=tou[i];j;j=b[j].next)if(b[j].v&&!dep[b[j].to]){ //這條邊必須走得通,然后被標(biāo)記過的點不再標(biāo)記dep[b[j].to]=dep[i]+1; //標(biāo)記層數(shù),為上一層+1if(b[j].to==T)return 1; //找到了就直接退(不用擔(dān)心有些點沒被標(biāo)層)// 這里不用擔(dān)心有些點沒被標(biāo)層,因為匯點的所有上一層都一定被標(biāo)記了(這是bfs!)q.push(b[j].to); //沒找到繼續(xù)找}} return 0; }

    2. 在分層圖上 $ DFS $ 找到所有增廣路,回溯時更新節(jié)點信息(剩余流量):

    inline int dfs(int i,int w){if(i==T||!w)return w;rg rest=w,f;for(rg &j=qi[i];j&&rest;j=b[j].next){ //&為當(dāng)前弧優(yōu)化,不用看,下面會講if(b[j].v&&dep[b[j].to]==dep[i]+1){f=dfs(b[j].to,min(rest,b[j].v)); //注意這個min函數(shù),它反映增廣路的容量b[j].v-=f; b[j^1].v+=f; rest-=f; //更新剩余容量,反邊要加容量} //我們還要更新剩余容量,保證流進來的等于流出去的}return w-rest; }

    因為 $ DFS $ 時,一個節(jié)點可以流向多個節(jié)點,所以可以找到多條增廣路,同時剪枝與優(yōu)化(下文會講)讓 $ Dinic $ 算法運行較快。雖然理論時間復(fù)雜度: $ O(n^2m) $ ,但實際上跑得非常快,所以做題算復(fù)雜是總比較糾結(jié),書上默認(rèn)可以跑 $ 10^4~10^5 $ 的數(shù)據(jù)范圍。不過 $ Dinic $ 跑二分圖可以說得心應(yīng)手,復(fù)雜度為 $ m\sqrt{n} $ ,還總是能超常發(fā)揮。



    3. $ Dinic $ 的當(dāng)前弧優(yōu)化和剪枝

    當(dāng)前弧優(yōu)化:

    (配合下面代碼食用更佳)我們在一般的鏈?zhǔn)角跋蛐谴孢叺?$ DFS $ 里從 $ i $ 號節(jié)點向下搜索時,是從 $ tou[i] $ (似乎很多人用 $ Head[i] $ )所記錄的邊開始搜索。我們結(jié)合代碼及算法原理發(fā)現(xiàn),在如我們當(dāng)前節(jié)點的 $ rest $ (剩余可用流量)不為零 的情況下:

  • 如果跑 $ b[j].to $ (兒子節(jié)點)所得到可用增廣流量 $ f $ 卻為0,說明這個兒子節(jié)點已經(jīng)搜索不到增廣路了,那么之后再怎么搜索都不可能找到增廣路;
  • 如果跑 $ b[j].to $ (兒子節(jié)點)所得到可用增廣流量 $ f $ 不為0 且 $ rest>f $ ,說明這個兒子節(jié)點已經(jīng)被我們跑滿了,那么以后這個父親 $ i $ 在搜索這個兒子時不可能會得到增廣路了。
  • 綜上,我們新建一個數(shù)組存鏈?zhǔn)角跋蛐堑钠鹗歼?#xff0c;并用取址符保證這一次 $ DFS $ 在跑第二次某個節(jié)點時,不會去跑那些已經(jīng)不可能找到增廣路的兒子節(jié)點:

    for(rg &j=qi[i];j/* &&rest!=0 */;j=b[j].next)

    注意,博主為什么在這個for循環(huán)里不用屏蔽的那句話呢?,其實我們要將它寫在后面(如下方代碼)。上文我們在當(dāng)前弧優(yōu)化的第二個情況里面說過一個條件 $ rest>f $ ,如果我們將屏蔽的那句話放在 $ for $ 循環(huán)里,這個節(jié)點跑的最后一個兒子并不滿足這個條件,這個兒子還可能被增廣!為了不讓當(dāng)前弧負(fù)優(yōu)化,我們必須嚴(yán)謹(jǐn)!

    for(rg &j=qi[i];j/* &&rest!=0 */;j=b[j].next){ //注意博主寫了&取址符,這是當(dāng)前弧優(yōu)化的關(guān)鍵if(b[j].v&&dep[b[j].to]==dep[i]+1){f=dfs(b[j].to,min(rest,b[j].v));b[j].v-=f; b[j^1].v+=f; rest-=f; //更新剩余容量,反邊要加容量} if(!rest)break; //注意在這里退出保證了當(dāng)前弧優(yōu)化的正確性,不然會負(fù)優(yōu)化 } // 當(dāng)前弧優(yōu)化不能在for循環(huán)的判斷里退出,因為j是取址的,會往后多改一次,將最后一條可能沒跑滿的邊判錯

    可行性剪枝:

    這個比當(dāng)前弧要簡單一些,當(dāng)前弧優(yōu)化核心是再次遍歷某一個節(jié)點時,不會訪問它沒用的子節(jié)點(就是父親訪問兒子后不可能找到增廣路)。那我們自然可以想到,如果一個節(jié)點對于所有節(jié)點都沒用(就是所有這個節(jié)點的父親訪問這個節(jié)點后都不能找到增廣路),那么這是一個廢節(jié)點,我們可以及時排除。

    原理: (請結(jié)合下面代碼)如果我們當(dāng)前節(jié)點 $ rest $ (剩余可用流量)不為零,然后在遍歷某一個兒子后得到的增廣容量也為0,那么任何其它節(jié)點在訪問這個節(jié)點后一定也不能得到增廣路。不然我們在第一次遍歷這個兒子時一定可以得到一個增廣路。但是這個剪枝有一個前提條件:這個節(jié)點通向它兒子的邊一定還有剩余容量,不然就是兒子可以找到增廣路,在這條邊回溯時也就斷掉了。

    for(rg &j=qi[i];j;j=b[j].next){if(b[j].v&&dep[b[j].to]==dep[i]+1){ //不要少了前面那一個,它會影響我們剪枝f=dfs(b[j].to,min(rest,b[j].v));if(!f){dep[b[j].to]=-2; continue;} //剪枝:這個點已經(jīng)不可能再被增廣了(滿流了)b[j].v-=f; b[j^1].v+=f; rest-=f; //更新剩余容量,反邊要加容量} if(!rest)break; }

    4. 例題: 洛谷 P3376 最大流

    沒什么好說的,就是一道板子,照著上面說的寫就好。數(shù)據(jù)還賊水。

    #include<iostream> #include<cstdio> #include<iomanip> #include<algorithm> #include<cstring> #include<cstdlib> #include<ctime> #include<cmath> #include<vector> #include<queue> #include<map> #include<set>#define ll long long #define db double #define rg register intusing namespace std;int n,m,S,T; int ans,top=1; //注意top初值為1,才能保證j^1是反邊 int dep[10005]; //節(jié)點層數(shù) int tou[10005]; //鏈?zhǔn)角跋蛐谴孢?int qi[10005]; //當(dāng)前弧優(yōu)化存邊struct su{int to,v,next; }b[200005];inline int qr(){register char ch; register bool sign=0; rg res=0;while(!isdigit(ch=getchar()))if(ch=='-')sign=1;while(isdigit(ch))res=res*10+(ch^48),ch=getchar();if(sign)return -res; else return res; }inline void add(int x,int y,int v){b[++top]=su{y,v,tou[x]}; tou[x]=top; //加邊 (c++11) }inline bool bfs(){for(rg i=1;i<=n;++i)qi[i]=tou[i],dep[i]=0; //初始化queue<int> q; q.push(S); dep[S]=1; //初始化while(!q.empty()){rg i=q.front(); q.pop();for(rg j=tou[i];j;j=b[j].next)if(b[j].v&&!dep[b[j].to]){dep[b[j].to]=dep[i]+1; //標(biāo)記層數(shù)if(b[j].to==T)return 1; //找到了就直接退(不用擔(dān)心有些點沒被標(biāo)層)q.push(b[j].to); //沒找到繼續(xù)找}} return 0; }inline int dfs(int i,int w){if(i==T||!w)return w;rg rest=w,f;for(rg &j=qi[i];j;j=b[j].next){ //注意博主寫了&取址符,這是當(dāng)前弧優(yōu)化的關(guān)鍵if(b[j].v&&dep[b[j].to]==dep[i]+1){ //不要少了前面那一個,它會影響我們剪枝f=dfs(b[j].to,min(rest,b[j].v));if(!f){dep[b[j].to]=-2; continue;} //剪枝:這個點已經(jīng)不可能再被增廣了(滿流了)b[j].v-=f; b[j^1].v+=f; rest-=f; //更新剩余容量,反邊要加容量} if(!rest)break; //注意這里退出保證了當(dāng)前弧優(yōu)化的正確性}return w-rest; }int main(){//freopen(".in","r",stdin);//freopen(".out","w",stdout);n=qr(); m=qr(); S=qr(); T=qr();for(rg i=1;i<=m;++i){rg x=qr(),y=qr(),v=qr();add(x,y,v); add(y,x,0);} while(bfs()) ans+=dfs(S,1e9); //Dinic有兩步(bfs+dfs)printf("%d\n",ans);return 0; }

    5. 其它算法:ISAP與HLPP

    這個暫時不會講,給出兩個思考,和一個鏈接

    $ ISAP $ :在 $ Dinic $ 中,我們會先 $ BFS $ 給殘量網(wǎng)絡(luò)分層,然后再 $ DFS $ 尋找增廣路。我們知道 $ BFS $ 分層是為了讓 $ DFS $ 能一次找多條增廣路,那有沒有辦法減少這個分層次數(shù)?我們可以先從匯點向源點跑一次 $ BFS $ 標(biāo)記深度,然后動態(tài)維護這個深度。這是 $ ISAP $ 的核心。

    $ HLPP $ :我們求最大流時,不難有一個腦補做法:從源點放肆灌水,每條邊能流就流;當(dāng)從一個大管子流向一個小管子,流量會減小,大管子上面的流量會因為壓強自然流向另一邊;最后在流過匯點的流量就是最大流。大家可以想一想這個怎么實現(xiàn),事實上,這一個與增廣路毫不相干的做法,稱為預(yù)留推進,跑得很快,但比較難寫。




    三、最小割與最大流

    注:需要先仔細(xì)閱讀基本概念里的殘量網(wǎng)絡(luò)和割的定義

    最大流最小割定理:

    任意一個網(wǎng)絡(luò)流的最大流等于其最小割。

    首先我們可以證明任何一個網(wǎng)絡(luò)流 $ \leq $ 任何一個最小割。反證法:如果最大流 $ > $ 最小割,那么我們將最小割所包含的邊全部割掉,這是最大流一定還有部分流量可以從源點到匯點,這與最小割的定義(源匯點不連通矛盾)。所以任何一個網(wǎng)絡(luò)流 $ \leq $ 任何一個最小割。

    接下來我們只需證明最大流可以構(gòu)造一組最小割,這里需要用到殘量網(wǎng)絡(luò),它與割有著密不可分的聯(lián)系。殘量網(wǎng)絡(luò)就是由網(wǎng)絡(luò)中所有沒流滿的邊(包括反向邊)構(gòu)成的圖。我們知道一個網(wǎng)絡(luò)的最大流,它所對應(yīng)的殘量網(wǎng)絡(luò)一定是個不連通圖(源匯點不連通),否則使它們聯(lián)通的鏈就是增廣路,與最大流矛盾。所以我們從源點出發(fā)找到所有可以標(biāo)記的點,這些點和未標(biāo)記點的連邊構(gòu)成最小割(這些邊一定滿流),而他們的流量等于最大流(這個要從定義出發(fā)思考)。



    網(wǎng)絡(luò)流例題及技巧

    $ POJ~1966~Cable~TV~Network $


    第一眼可能讓人很難下手,但本就是沖著網(wǎng)絡(luò)流來的,所以我們直接一點。這道題我們要讓這個聯(lián)通圖斷開,那么勢必會有兩個點變得不連通,這道題的數(shù)據(jù)范圍很小,所以我們試著暴力枚舉兩個點。這樣就變成了最小割。不過,嗯?割的東西怎么是點?

    為了靠近我們已經(jīng)學(xué)得知識,我們想辦法看,能不能割點變成割邊。反正網(wǎng)絡(luò)流最喜歡千變?nèi)f化、左右建模了。。。于是我們引進書上的一個東西:

  • 一個節(jié)點可以拆成兩個節(jié)點,將原節(jié)點用中間那條邊表示
  • 一條邊可以拆成兩條邊,將原邊用中間那個點表示
  • 中間的邊權(quán)為1代表這個點是否被割,旁邊的邊權(quán)為inf是為了排除其影響(因為它不可能被割掉)
  • 我們用第一條和第三條性質(zhì)可以解決這個問題。首先對于每個節(jié)點建立兩個 $ i $ 和 $ i+n $ 節(jié)點。然后這兩個節(jié)點之間用一條權(quán)值為1的有向邊(從 $ i $ 到 $ i+n $ ) ,如果這條邊在最小割中被割掉(等價于原本的點被割掉)。然后 $ i $ 節(jié)點連入邊(權(quán)值正無窮), $ i+n $ 節(jié)點連出邊(權(quán)值正無窮),連正無窮是為了讓割掉的邊只能是中間的邊。然后我們跑一遍最大流,它對應(yīng)的最小割里每條代表原來一個點,因為權(quán)值為1,所以流量就是答案。

    注意:我們的源匯點也要被分為兩個點,而網(wǎng)絡(luò)流中的實際源點是 $ S+n $ ,它連出邊。因為源匯點的性質(zhì),這兩個點不可能被割掉,所以它們中間不連邊。


    $ code: $

    #include<iostream> #include<cstdio> #include<iomanip> #include<algorithm> #include<cstring> #include<cstdlib> #include<ctime> #include<cmath> #include<vector> #include<queue> #include<map> #include<set>#define ll long long #define db double #define rg register intusing namespace std;int n,m,S,T; int ans,top=1; int dep[505]; int tou[505]; int qi[505]; int f[55][55];struct su{int to,v,next; }b[5005];inline int qr(){register char ch; register bool sign=0; rg res=0;while(!isdigit(ch=getchar()))if(ch=='-')sign=1;while(isdigit(ch))res=res*10+(ch^48),ch=getchar();if(sign)return -res; else return res; }inline void add(int x,int y,int v){ //注意博主加邊自帶反向b[++top]=su{y,v,tou[x]}; tou[x]=top;b[++top]=su{x,0,tou[y]}; tou[y]=top; }inline bool bfs(int x){for(rg i=1;i<=x;++i)qi[i]=tou[i],dep[i]=0;queue<int> q; q.push(S); dep[S]=1;while(!q.empty()){rg i=q.front(); q.pop();for(rg j=tou[i];j;j=b[j].next)if(b[j].v&&!dep[b[j].to]){dep[b[j].to]=dep[i]+1;if(b[j].to==T)return 1;q.push(b[j].to);}} return 0; }inline int dfs(int i,int w){if(i==T||!w)return w;rg rest=w,f;for(rg &j=qi[i];j;j=b[j].next){if(b[j].v&&dep[b[j].to]==dep[i]+1){f=dfs(b[j].to,min(w,b[j].v));if(!f){dep[b[j].to]=-2; continue;}b[j].v-=f; b[j^1].v+=f; w-=f;} if(!w)break;}return rest-w; }inline void solve(){rg res=0; top=1;for(rg i=1;i<=n*2+2;++i) tou[i]=0; //初始化for(rg i=1;i<=n;++i){if(i!=S&&i!=T)add(i,i+n,1); //一點拆成兩點,中間連邊f(xié)or(rg j=1;j<=n;++j)if(f[i][j])add(i+n,j,1e9); //連邊注意是否有加n操作} S=S+n;while(bfs(n*2+2)) res+=dfs(S,1e9); //DInicans=min(res,ans); }int main(){//freopen(".in","r",stdin);//freopen(".out","w",stdout);rg t=qr();while(t--){n=qr();m=qr();for(rg i=1;i<=n;++i){for(rg j=i;j<=n;++j){f[i][j]=f[j][i]=0; //初始化}}for(rg i=1;i<=m;++i){rg x=qr()+1,y=qr()+1;f[x][y]=f[y][x]=1; //鄰接矩陣讀邊}if(n==0||n==2){puts("0");continue;}if(m==0&&n&&n!=2){puts("1");continue;}//特判,這題有點卡細(xì)節(jié)ans=1e9;for(rg i=1;i<=n;++i){for(rg j=1;j<=n;++j){if(f[i][j]||i==j)continue; //注意兩個相鄰的點不可能通過割點不聯(lián)通S=i;T=j; solve(); //枚舉源匯點}} if(ans==1e9)ans=n; //無論怎么割點圖都聯(lián)通,就輸出nprintf("%d\n",ans);}return 0; }


    四、最小費用最大流

    學(xué)會網(wǎng)絡(luò)流之后,費用流就會比較好學(xué),因為費用流一定是最大流,只是它還涉及在最大流的同時保證費用最大或最小。。。

    費用流的求法

    設(shè) $ G(V, E) $ 是一個網(wǎng)絡(luò), $ V $ 為點集, $ E $ 為邊集。其中每一條弧(邊) $ <x,y>∈E $ ,都有一個給定容量 $ c(x,y) $ ,除此之外還有一個費用 $ v(x,y) $ 。當(dāng)邊 $ (x,y) $ 的流量為 $ f(x,y) $ 時,需要花費 $ f(x,y)\times v(x,y) $ ,現(xiàn)在要求網(wǎng)絡(luò)里費用最小(大)的一個最大流。

    思路1: 我們找到一個最大流,然后通過修改邊的流量優(yōu)化費用。這個不好實現(xiàn),暫時不講。相比之下,思路二會更接近我們已經(jīng)學(xué)過的東西。

    思路2: 我們考慮在跑最大流的同時優(yōu)化費用,這個我們直接貪心,在跑最大流找增廣路時去找一條費用最小的增廣路,然后一直找到?jīng)]有增廣路為止。因為我們每次只找一條,所以我們不能用 $ Dinic $ ,而且如果要跑帶負(fù)邊權(quán)的最短路我們又只好用 $ SPFA $ ,且沒有辦法邊找邊回溯更新邊的信息。只能用 $ SPFA $ 找到并記錄一條費用最小的增廣路,然后跑完再更新路徑上所有邊的信息。

    例題:洛谷 P3381 最小費用最大流

    就是一道板子:

    #include<iostream> #include<cstdio> #include<iomanip> #include<algorithm> #include<cstring> #include<cstdlib> #include<ctime> #include<cmath> #include<vector> #include<queue> #include<map> #include<set>#define ll long long #define db double #define rg register intusing namespace std;int n,m,s,t; int ans1,ans2,top=1; int fa[5005]; //記錄節(jié)點的父親,便于找到路徑更新 int eg[5005]; //記錄連接父親的邊,便于找到路徑更新 int vv[5005]; //記錄流過節(jié)點的流量 int dis[5005]; //記錄節(jié)點距離,用來SPFA int tou[5005]; //鏈?zhǔn)角跋蛐?bool vis[5005]; //SPFAqueue<int> q;struct su{int to,v1,v2,next; }b[200005];inline int qr(){register char ch; rg sign=0,res=0;while(!isdigit(ch=getchar()))if(ch=='-')sign=1;while(isdigit(ch))res=res*10+(ch^48),ch=getchar();if(sign)return -res; else return res; } int sdf; inline bool spfa(){for(rg i=1;i<=n;++i)dis[i]=1e9, vv[i]=1e9; //跑最短路的更新q.push(s); dis[s]=0; //初始while(!q.empty()){rg i=q.front(); vis[i]=0; q.pop(); //取隊頭for(rg j=tou[i];j;j=b[j].next){rg to=b[j].to;if(b[j].v2&&dis[to]>dis[i]+b[j].v1){ //跑SPFA,不要少了第一個判斷if(!vis[to])vis[to]=1,q.push(to);dis[to]=dis[i]+b[j].v1; fa[to]=i; //更新距離,更新父親eg[to]=j; vv[to]=min(vv[i],b[j].v2); //更新邊與流量}}}return dis[t]!=1e9; }inline void solve(){rg x=t,y,v=vv[t];while(x!=s){ //找到路徑并更新信息y=eg[x];b[y].v2-=v;b[y^1].v2+=v; //反向邊加權(quán)x=fa[x];}ans1+=v; ans2+=v*dis[t]; //記錄答案 }int main(){//freopen(".in","r",stdin);//freopen(".out","w",stdout);n=qr(); m=qr(); s=qr(); t=qr();for(rg i=1;i<=m;++i){rg x=qr(),y=qr(),v2=qr(),v1=qr();b[++top]=su{y,v1,v2,tou[x]}; tou[x]=top;b[++top]=su{x,-v1,0,tou[y]}; tou[y]=top;} while(spfa()) solve();printf("%d %d\n",ans1,ans2);return 0; }


    五、帶上下界的網(wǎng)絡(luò)流

    帶上下界的網(wǎng)絡(luò)流最重要的就是無源匯上下界網(wǎng)絡(luò)流,他是一個萬能基礎(chǔ)!所以本文會花大部分筆墨解讀其核心,懂了它之后的幾個上下界算法就是水到渠成。

    1. 無源匯上下界網(wǎng)絡(luò)可行流:

    模型:有一個網(wǎng)絡(luò) $ G(V, E) $ , $ V $ 為點集, $ E $ 為邊集,沒有源匯點。其中每一條邊都有一個流量限制: $ li \leq f \leq ri $ 。求一個可行流,即在這個可行流里對于所有的點:流入總量 = 流出總量。(邊一定流量守恒所以我們只考慮點) (注意沒有源匯點,所以這個流是一個循環(huán)流,無始無終!)

    我們定義:某個點的流入總量為所有入邊的實際流量總和,流出總量為所有出邊的實際流量總和。

    核心思路:

    因為我們的可行流里,每條邊的流量一定大于其下界,所以我們欽定先建立一個初始流它的每條邊的流量恰好是其流量下界。然后我們考慮建立它對應(yīng)的殘量網(wǎng)絡(luò),因為原網(wǎng)絡(luò)中每條邊的實際容量被欽定成下界,所以殘量網(wǎng)絡(luò)中每一條邊的殘留容量即為原邊的上界流量 - 下界流量 。

    這句話敲重要:我們發(fā)現(xiàn)這個初始流不一定滿足流量守恒(即對于所有的點:流入總量 = 流出總量),是個非可行流。于是我們考慮能否在殘量網(wǎng)絡(luò)中也尋找一個不滿足流量守恒的非可行流(本文暫稱為“添補流”),使得它們兩個合并后能夠變成一個我們想要的可行流!

    而要找到這個合適的添補流,我們必須尋找它與初始流之間的關(guān)系:因為兩流合并后為一個流量守恒的可行流,所以我們可以根據(jù)初始流算出殘量網(wǎng)絡(luò)的“添補流”中每個點的流入總量與流出總量應(yīng)該滿足的關(guān)系:

  • 流量已經(jīng)守恒:如果在初始流中某個點的流入總量 = 流出總量,那么殘量網(wǎng)絡(luò)的“添補流”所對應(yīng)節(jié)點應(yīng)滿足:流入總量 = 流出總量
  • 流入 > 流出:如果在初始流中某個點的流入總量 = 流出總量 ?-v? ,那么殘量網(wǎng)絡(luò)的“添補流”所對應(yīng)節(jié)點應(yīng)滿足:流入總量 ?=? 流出總量+v 。這樣當(dāng)初始流和添補流加起來時,這個點才滿足 流入總量 ?= 流出總量 (-v +v)
  • 流入 < 流出:即如果在初始流中某個點的流入總量 = 流出總量 ?+v? ,那么殘量網(wǎng)絡(luò)的添補流所對應(yīng)節(jié)點應(yīng)滿足:流入總量 ?=? 流出總量 -v 。這樣最后合并時這個點才滿足 流入總量 ?= 流出總量 (+v -v)
  • 下面代碼里:a數(shù)組表示在殘量網(wǎng)絡(luò)的添補流里某個節(jié)點應(yīng)滿足:流入總量比流出總量大多少(可以為負(fù))

    int a[]; //在殘量網(wǎng)絡(luò)的添補流里某個節(jié)點i應(yīng)滿足:流入總量-流出總量 = a[i] for(rg i=1;i<=m;++i){scanf("%d%d%d%d",&x,&y,&l,&r) //l為下界,r為上界add(x,y,r-l,r); //加一條從x到y(tǒng)的剩余容量為r-l的邊,最后一個上界用來最后求可行流流量a[x]+=l; //注意x為出發(fā)點,在初始流中流出總量增加,相應(yīng)的添補流中流入總量也應(yīng)增加a[y]-=l; //y為出發(fā)點,在初始流中流入總量增加,相應(yīng)的添補流中流入總量需要減少 }

    只要這三個條件滿足,初始流和添補流合并后,我們就能得到所有點都流量守恒的可行流,也就是我們的答案。


    如何求這個添補流:

    首先我們要明白,我們目前還沒辦法在一個無源匯的網(wǎng)絡(luò)中找一個非可行流。但是也正因為這個網(wǎng)絡(luò)無源匯,我們找的流為非可行流,再聯(lián)系到網(wǎng)絡(luò)流的千變?nèi)f化,我們想能不能通過添加源匯點和一些邊,以跑網(wǎng)絡(luò)流的形式,來實現(xiàn)對每個點流入總量和流出總量關(guān)系的限制!(也就是說在添加源匯點和一些邊的情況下得到的網(wǎng)絡(luò)流,在刪去源匯點和這些邊后,是一個我們想要的不滿足流量守恒的“添補流”) 這是可以實現(xiàn)的,具體來說:

  • 如果我們在殘量網(wǎng)絡(luò)中需要某個點的流入總量 > 流出總量(準(zhǔn)確一點:流入總量 = 流出總量 -v),那么我們可以建一條從這個點出發(fā)的容量為 v 的邊。這樣我們在加這條邊的情況下跑網(wǎng)絡(luò)流,如果這條邊能流滿,那么在這個網(wǎng)絡(luò)流里這個點:流入總量 = 流出總量 = 流向這條邊的流量(v)+ 流向其他邊的流量。這時只要我們將這條加進來的邊再刪除掉,得到的非流量守恒流里,這個點:流入總量 = 流出總量 - 流向這條邊的流量(v)。達到了我們想要的效果!
  • 如果我們在殘量網(wǎng)絡(luò)中需要某個點的流入總量 < 流出總量(準(zhǔn)確一點:流入總量 = 流出總量 +v),那么我們可以建一條流向這個點的容量為 v 的邊。這樣我們在加這條邊的情況下跑網(wǎng)絡(luò)流,如果這條邊能流滿,那么在這個網(wǎng)絡(luò)流里這個點:從這條邊流進來的流量(v)+ 從其他邊流進來的流量 = 流入總量 = 流出總量。這時只要我們將這條加進來的邊再刪除掉,得到的非流量守恒流里,這個點:流入總量 - 從這條邊流進來的流量(v)= 流出總量
  • 讓某個點流入總量 = 流出總量:這個我們什么都不做就好,因為在有源匯的網(wǎng)絡(luò)跑網(wǎng)絡(luò)流自然滿足流量守恒
  • 我們發(fā)現(xiàn)第一步里建了入邊,第二步里建了出邊,為了讓這些邊有來處有去處,我們建一個虛擬源點和虛擬匯點:所有新建的指向節(jié)點的邊從虛擬源點出發(fā),所有新建的從節(jié)點出發(fā)的邊流向虛擬匯點。
  • 然后我們發(fā)現(xiàn)我們的前兩個性質(zhì)都需要滿足新建的邊滿流,才能達到構(gòu)造一個可行的添補流,所以我們直接跑網(wǎng)絡(luò)最大流。如果所有新建的邊都能滿流就可以構(gòu)造出這樣一個添補流。否則無法構(gòu)建添補流,原問題無解。
  • //再強調(diào)一下:a[i]表示殘量網(wǎng)絡(luò)的添補流里某個點應(yīng)滿足的:流入總量比流出總量大多少! for(rg i=1;i<=n;++i){if(a[i]>0)add(i,t,0,a[i]); //流入>流出,從該節(jié)點向匯點連邊if(a[i]<0)add(s,i,0,-a[i]); //流入<流出,從源點向該節(jié)點連邊if(a[i]>0)ans+=a[i]; //最大流需要跑滿的流量 }

    最后我們只需要將最大流里所有添加的點和邊去掉,然后在得到的添補流里某條邊的實際流量加上初始流中這條邊的流量(就是其下界流量)就可得到這條邊在原網(wǎng)絡(luò)的可行流里的實際流量!


    例題:

    #include<iostream> #include<cstdio> #include<iomanip> #include<algorithm> #include<cstring> #include<cstdlib> #include<ctime> #include<cmath> #include<vector> #include<queue> #include<map> #include<set>#define ll long long #define db double #define rg register intusing namespace std;int n,m,s,t; int ans,top=1; //top從1開始 int a[205]; //添補流中某個點的應(yīng)該的流入總量-流出總量 int qi[205]; //當(dāng)前弧優(yōu)化 int dep[205]; //層深度 int tou[205]; //鏈?zhǔn)角跋蛐莝truct su{int to,v,up,next; //v為剩余容量,up為上界 }b[200005];inline int qr(){ //快讀register char ch; register bool sign=0; rg res=0;while(!isdigit(ch=getchar()))if(ch=='-')sign=1;while(isdigit(ch))res=res*10+(ch^48),ch=getchar();if(sign)return -res; else return res; }inline void add(int x,int y,int v,int up){ //加邊b[++top]=su{y,v,up,tou[x]}; tou[x]=top; //正向邊b[++top]=su{x,0,up,tou[y]}; tou[y]=top; //反向邊 }inline bool bfs(int x){for(rg i=1;i<=x;++i)qi[i]=tou[i],dep[i]=0;queue<int> q; q.push(s); dep[s]=1;while(!q.empty()){rg i=q.front(); q.pop();for(rg j=tou[i];j;j=b[j].next){if(b[j].v&&!dep[b[j].to]){dep[b[j].to]=dep[i]+1;if(b[j].to==t)return 1;q.push(b[j].to);}}}return 0; }inline int dfs(int i,int w){if(i==t)return w;rg rest=w,f;for(rg &j=qi[i];j;j=b[j].next){ //&為當(dāng)前弧優(yōu)化if(b[j].v&&dep[b[j].to]==dep[i]+1){f=dfs(b[j].to,min(rest,b[j].v));if(!f){dep[b[j].to]=-2; continue;} //剪枝b[j].v-=f; b[j^1].v+=f; rest-=f; //更改信息}if(!rest)break; //當(dāng)前弧優(yōu)化}return w-rest; //返回增廣流量 }int main(){//freopen(".in","r",stdin);//freopen(".out","w",stdout);n=qr(); m=qr(); s=n+1; t=n+2;for(rg i=1;i<=m;++i){rg x=qr(),y=qr(),l=qr(),r=qr();add(x,y,r-l,r); a[x]+=l; a[y]-=l;}for(rg i=1;i<=n;++i){if(a[i]>0)add(i,t,0,a[i]); //流入>流出if(a[i]<0)add(s,i,0,-a[i]); //流入<流出if(a[i]>0)ans+=a[i]; //我需要跑滿的流量} while(bfs(n+2)) ans-=dfs(s,1e9); //dinicif(!ans){ puts("YES"); //跑滿了for(rg i=2;i<=m*2;i+=2)printf("%d\n",b[i].up-b[i].v); //上界減去剩余容量}else puts("NO"); //沒跑滿return 0; }

    2. 有源匯上下界網(wǎng)絡(luò)可行流:

    模型:現(xiàn)在有一個網(wǎng)絡(luò),存在一個源點和一個匯點,每條邊都有一個流量上下界限制。求一個從源點到匯點的可行流,滿足邊的流量限制,且所有點流量守恒。

    思路:

    這是一個看起來很難但實際上比較傻的問題。這個網(wǎng)絡(luò)里已經(jīng)存在源匯點,看起來我們確實不能向無源匯一樣加點加邊了。但是聯(lián)想上一個內(nèi)容,無源匯的上下界網(wǎng)絡(luò)流,它的可行流是一個循環(huán)流,無始無終。本題確實制指定了源匯點,但是我們可不可以想辦法將源匯點變成一般點,所求的可行流變成一個循環(huán)流?其實到這里我們就應(yīng)該要懂了:我們直接在從匯點向源點連一條容量無限大的邊,所求的可行流所有流量在流入?yún)R點后有流向源點,變成了一個循環(huán)流。這樣我們又直接像無源匯一樣做就行了!

    關(guān)于我們最后求得的可行流:

    首先每一條邊的流量可以仿照無源匯做法得出。我們?nèi)绻焖俚贸稣麄€可行流的流量,我們發(fā)現(xiàn)這個流量就是所有從原點出發(fā),在匯點集合的流量的總和。而所有流入?yún)R點的流量會通過我們加入的那條容量無限大的邊流回源點,所以我們直接最后看看這條邊的實際流量即可。



    3. 有源匯上下界網(wǎng)絡(luò)最大流

    模型:現(xiàn)在有一個網(wǎng)絡(luò),存在一個源點和一個匯點,每條邊都有一個流量上下界限制。求一個從源點到匯點的最大可行流,需滿足邊的流量限制,且所有點流量守恒。

    思路:

    (大家可以先獨立思考一下)這道題其實可以轉(zhuǎn)化為一個很普通的求網(wǎng)絡(luò)最大流問題。只不過我們必須先按照有源匯上下界可行流的模式找到一個可行流。然后我們想辦法將這個可行流轉(zhuǎn)化成最大流,我們在求完可行流后的殘量網(wǎng)絡(luò)里去掉兩個虛擬源匯點和與它們有連接的邊,然后以題目所給的源匯點和邊的剩余容量構(gòu)成的殘量網(wǎng)絡(luò)里跑最大流,用這個最大流與初始流合并就是滿足上下界的最大可行流

    核心原理:(為什么這樣做是對的)

    首先我們是不改變初始流的,我們改變的是殘量網(wǎng)絡(luò)上的添補流(它和初始流合并變成可行流),我們可以在殘量網(wǎng)絡(luò)上對已經(jīng)求得的添補流進行增廣操作。而求添補流注重的是點的流入總量和流出總量的關(guān)系 ,我們已經(jīng)求得了一個滿足關(guān)系的添補流,而增廣操作也并不會改變點的流入總量與流出總量的差,所以增廣后的添補流和初始流合并后依舊流量守恒。然后這個殘量網(wǎng)絡(luò)上有所有的剩余容量,所以用它求出來的最大添補流再加上初始流就一定可以得到可行最大流。因為初始流包含所有邊的下界,并且最后才被加上,所以最終流的邊流量一定不會小于流量下界!



    4. 有源匯上下界網(wǎng)絡(luò)最小流

    模型:現(xiàn)在有一個網(wǎng)絡(luò),存在一個源點和一個匯點,每條邊都有一個流量上下界限制。求一個從源點到匯點的最小可行流,需滿足邊的流量限制,且所有點流量守恒。

    思路1: 二分答案

    我們之前求解有源匯的上下界可行流時,會從匯點向源點連一條容量無限的邊?,F(xiàn)在我們稍稍進行修改,我們對這條邊設(shè)置一個上限 $ f $ 。 然后跑無源匯上下界可行流,如果存在可行流,那么原網(wǎng)絡(luò)的最小可行流 $ minf \leq f $ ;如果不存在可行流,那么原網(wǎng)絡(luò)最小可行流 $ minf > f $ 。

    于是我們發(fā)現(xiàn)我們可以二分答案,并每次將這個答案作為源匯點之間邊的最大容量,判是否有可行流即可。


    思路2: 反向邊性質(zhì)

    上面那一種做法復(fù)雜度需要乘上一個二分答案產(chǎn)生的復(fù)雜度,可是我們求有源匯上下界網(wǎng)絡(luò)最大流時沒有,難道最小流特殊一些?肯定不是。有源匯上下界最小流其實和有源匯上下界最大流一個思路,原理都是一個原理。

    我們先跑一邊有源匯上下界可行流,然后考慮將這個可行流轉(zhuǎn)化成最小流。這個我們也可以變成普通最大流,我們在求完可行流后的殘量網(wǎng)絡(luò)里去掉兩個虛擬源匯點和與它們有連接的邊,然后以題目所給的源匯點和邊的剩余容量構(gòu)成的殘量網(wǎng)絡(luò)里跑從匯點流向源點的最大流,用這個最大流加上初始流合并就是滿足上下界的最小可行流。(注意是加上初始流,所以不會出現(xiàn)小于流量下界的情況)

    因為我們已經(jīng)求得了一個滿足關(guān)系的添補流,而反向增廣操作也并不會改變點的流入總量與流出總量的差,所以增廣后的添補流和初始流合并后依舊流量守恒。我們殘量網(wǎng)絡(luò)上的流量通過反向邊其實就相當(dāng)于正向邊流量減少,所以一次從匯點到源點的增廣操作,就相當(dāng)于從源點到匯點的流量減少。(然后無論怎么增廣,正向邊的流量都不會為負(fù)的)



    5. (待填)無源匯上下界最大最小流:



    6. (待填)有源匯上下界費用流:




    六、習(xí)題推薦

    轉(zhuǎn)載于:https://www.cnblogs.com/812-xiao-wen/p/11253286.html

    總結(jié)

    以上是生活随笔為你收集整理的网络流学习笔记的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。