最詳細(也可能現在不是了)網絡流建模基礎


網絡流建模方法一覽

大鍋博主終於找回來了原稿。。。。

哈,據說加上最詳細這幾個字會比較容易吸引人閱讀。

資料來源:百度百科(笑~

​ 還有一些dalao的博客,不過也都只是因為不想打字了就copy一些很通俗的結論過來,全文幾乎都是筆者的原創。

注:此文隨着筆者的不斷做題持續更新。

已經很熟悉網絡流的dalao可以從七開始看,或許能有些收獲。

網絡流的題目,大部分都是在考建模,很少有考你板子的。。。除非你非得用EK結果被卡上天。筆者從做過的所有網絡流的題目里尋找了一些比較常用和經典的建模方式,一共大家學習。

那么就從簡單到難一步步看吧。

一、傻逼建圖法

筆者打上這兩個字是絕對沒有任何問題的,讓我們來想想網絡流的模板題的要求是什么。

(注:筆者選取了洛谷為題目來源,原因竟是視覺效果友好)

很明顯, 沒有比這種題目更簡單的網絡流了,題目給你怎么連的邊,你就照着題目連上就可以了,幾乎是把你當做一個啥都不會的人手把手教你建圖。。但是這種題目的數據范圍一般較大,建議使用較高效率的網絡流算法。

二、超級源點和匯點的設置

這應該是網絡流題目中最最最最常用的一種方法了。經常有題目,我們要求一些最小的利益什么的,但是明顯沒有可用的源點和匯點或者更多情況是題目中給出了不止一個源點和匯點,這時候就要建立超級源和超級匯,把所有的題目中的可行源點和匯點分別連接到超級源和超級匯上,而事實上,我們拿到任何一道網絡流題目,除非它的源點和匯點是嚴格給定的,我們都可以嘗試建立超級源匯從而達到減小建模難度的目的。

例題:飛行員匹配問題

題解:這個題我們先不管輸出方案,只看要輸出的那個最大值,很明顯,如果我們把英國飛行員當成源點,外國飛行員當成匯點的話,會有很多源點和匯點,本着方便至上的原則,建立超級源匯,然后板子即可。
前面的題目這么簡單筆者並不想放代碼

三、最小割類問題

這其實是一個很關鍵的定理。事實上真要細細的說這個總結里寫的肯定是不夠用的。我們在做題的時候,有時會遇到這樣的一類問題,就是給你一個圖,然后給你邊權,問你如何在割掉的邊權盡量小的情況下把指定的兩個點or兩個集合分開。然后我們就把你求得的這個最小的邊權和叫做這個圖的最小割。

那么補上之前不想證明的結論。

首先最大流==最小割

為什么呢,想象一下,我們現在有這么一個有向圖,然后,最大流是小於等於割的,那么。。。在最大流=某個割的時候這個割就是最小的。。。感性理解一下的話,一個最大流跑到那個圖上,這個圖怎么看S也沒法連到T去了。

然后最小割的唯一性?

我們考慮一條邊是否一定在最小割中。明顯,我們跑完一遍網絡流之后殘量網絡里滿流的邊都是可能在最小割里的,然后又到了跑Tarjan的時候了,如果這條邊在最小割里,那么跑完Tarjan之后它所連接的兩個端點一定是在不同的強連通分量里。如果是的話,用腦子想想,割了這條邊這個圖還是沒分開啊。

那么我們也可以由此得證,假設一條邊所連接的點一個在S所在的強連通分量里,一個在T所在的強連通分量里的話,那么這條邊一定在最小割里。

1.平面圖最小割=最大流=對偶圖最短路

然后就是轉換成對偶圖的最短路的問題。其實這已經不應該算在網絡流的范疇里了(因為它變成了最短路)。那么就不在這里詳述了,具體的可以去看看[BZOJ1001]狼抓兔子,雖然可以最大流全力優化蜜汁跑過。

我們在這里還是討論最小割的問題

2.割邊

這種就是我們所說的最平常的最小割問題,我們只需要直接求出圖的最大流就可以了。

然后是一些高深一些的建模:有很多題目都會讓你求這樣一個東西,就是怎么安排這些balabala的工作可以獲得balabala的利益,然后讓你去求這個最大利益。很多人都會正常的去想最大流的做法但是由於有限制條件,所以不能像那么求。然后我們不妨轉換一下,如果我們所有的都可以選,我們是不是可以得到一個最大利益,然后我們考慮的是放棄最小的利益和,這樣我們就把這個題轉換成了一個最小割的問題,可以利用最大流求出。

然后隨之而來的就是另一個模型:最大權閉合圖這類題目都可以轉化為最小割來求解,具體證明還是請百度吧,這篇文章並不是介紹這類東西的,並且筆者對這些模型的證明也不如網上的神犇。現在筆者又回來證這個東西了。請向后翻閱,自然能找到。。

例題:Luogu P3410 拍照

題解:這題就是一個最大權閉合圖的模型,求出總利益減去不可得的利益就可以了,連邊的話比較容易就不詳述了,不過再往后的題目應該就會有連邊的思路了。畢竟前面這些都是些入門題目。

例題:方格取數問題

題解:我們可能會有一個比較基礎的貪心思想,沒錯,就是隔一個取一個,但是這么做並不可行,具體反例很容易找。然后我們通過觀察,發現這道題和某最大權閉合子圖有些類似,如果我們全取所有點,刪去最小割說不准可行。 開始考慮建圖,首先所有的奇數格子連源點,偶數格子連匯點,邊權為點權。他們之間的邊只連奇數到偶數的,邊權為inf,這么連是為了避免重復計算。然后直接DINIC最大流就可以了。

3.割點(?)

這也是最小割的一種經典問題,具體實現方式我們等下放在拆點那個欄目里講,這里就先不說了。

四、拆點

說實話,網絡流最難思考的地方就在這里了,幾乎所有拿得出來的經典的題目都是神奇的拆點然后求出最大流。

1.基礎拆點——入點和出點

經常有這一類問題,一個圖給出了點權而不是邊權,我們在連接邊的時候就顯得十分不好操作,這個時候我們往往就會有這樣一種操作,把每個點拆成入點和出點,題目給出的連邊均由每個點的出點連向入點,然后每個點的入點和出點之間連一條流量為點權的邊,就可以滿足點權的限制了。

然后讓我把剛才的坑補一下:割點

我們在很多時候都會發現,在求最小割的時候,我們要割的不是邊,而是割掉一個點。這時候我們再用剛才的直接跑的方法很明顯就不可以了。因為割掉一條邊最多只影響兩個點之間的連邊,而割掉了一個點的話,與這個點相連的所有邊就都失去了作用。這個時候我們就可以先拆點,然后把這個點入點和出點之間的那條邊割掉,就相當於是這個點在圖中不存在了。

例題:教輔的組成

題解:本題的構圖思路可以表示為:
源點->練習冊->書(拆點)->答案->匯點
注意一定要拆點,因為一本書只能用一次。
由於題目難度不大,這個題就提點到這里了。

例題:奶牛的電信

為什么找這道奶牛題。。以為確實是割點的題目不是很好找,剩下的題目不大適合放在這個地方。如果你好好看了剛才的割點,這個題應該可以直接做出來了。不詳述了。

2.按照時間拆點

有一部分題目是這樣的,我們給出的圖的同時也給出了一個天數或者時間的限制,然后對每一天做出詢問,最后求總和,很明顯的一點是,要把每一天都連向匯點然后求出總和。這個時候我們發現,如果其他的點僅僅只是一個點的話,無法滿足求出每一天這個要求,因為會互相影響,這個時候我們就相應的把這些點拆成天數這么多個點,然后分別向向對應的天數連邊。例題的話一會會有一些筆者認為比較好的題集中放在一起,這里就先介紹建圖方法了。

3.根據時間段拆點

這不是和上面那個一樣么???實際上不是的,注意,剛才那個是時間,而這個是時間段。不過說實話這種拆點方式主要是在費用流里出現,因為有很多這種題目,就是一個人在不同的流量是費用是不一樣的,所以為了滿足這個要求,就把這個人拆成一共有多少不同費用的點。舉個例子:你們數學老師今天布置作業越多,你的不滿值越大,如果是5張以下,一張卷子增加1點不滿值,如果10張以下,一張卷子增加兩點,如果10張以上,那么一張卷子增加inf點不滿值。那么我們就把你拆成三個點,分別對應三種不滿值。

4.蛇皮拆點

有很多題目,拆點往往十分難想或者蛇皮至極有很多典型的例子就不一一詳述了,比如什么一個點拆三個點四個點之類的。直接說一些例題貌似是比較好的。

[CQOI2012]交換棋子

這題不建議剛學費用流的人做,因為確實比較惡心,容易打擊到自己。首先,我們可以發現交換這個操作是很難去用流量描述和限制的。那我們應該怎么辦?如果我們把格子上的所有黑棋子當棋子,而剩下的白子就當成空的格子,我們就把一個交換的操作當成了一個移動的操作。那么如何限制流量呢?我們這個題要求的是每個格子參與交換的次數,針對一次交換要使用兩個格子的次數,我們把格子拆成入點,原點和出點,分別計算,至於交換次數自然讓費用承擔。好了,這只是大體的思路,但是實際上這題還有很多坑點和不好理解的地方。

拆點更多的是一種工具,而且是網絡流里必要和強大的工具。能不能想出拆點的方法從而跑出網絡流,往往是解決網絡流問題的關鍵。

五、枚舉和二分

原先這個版塊叫枚舉而二分最大流量,現在想想,當時的理解還是不夠深刻,實際上,能夠枚舉和二分的遠遠不止流量。這里詳細重新講述一下。

1.枚舉和二分流量條件

當然,這個部分依然是必不可少的。

有這樣一部分題目,它給你的圖不一定是完整的,往往需要你確定一個值來確定是否能夠跑出期望的最大流。就比如說去計算一個最少的時間或者花費時,我們並沒有一個具體的數值,這個時候我們往往預先通過連接inf先跑一遍最大流,之后二分時間每次重構圖,再次跑最大流,並且通過此時的流量是否和剛才相等來調整二分的上下界。抑或跟人數有關的題目,我們二分的時候判斷最大流是否等於當前人數。

然后枚舉的建圖方法就更是簡單,每次枚舉+1時重構圖,然后一直跑到不能再跑了為止。

2.枚舉和二分費用

費用流的題目中,有些題目會給你費用的限制,或者說是間接的控制了你的流量,比如說他要一個滿足條件時可能的最小費用,你仍然可以像剛才二分流量時那么跑。但是它也有可能要一個不超過一個費用時能滿足的最大條件,這個時候去二分那個條件,然后控制費用。

3.枚舉和二分邊和點

有的題目里,各個點的添加是有順序的,每個點的邊的添加也可能是有順序的,這個時候就有一類問題會讓你確定一個值,要這個值之前的所有點可以滿足最大流量,這個時候我們就要二分加入圖中的點了,邊也是同理。不過總的來說,二分的方法萬變不離其宗,總是要通過流量或者費用調整二分上下界。

六、點的構造

大部分題目是有跡可循的,因為他們至少有點或者有明顯能夠當做點的狀態。或者題目給出的點就真的能夠當成點使用。但是也有的題目,並沒有明顯的點的提示,或者給出的你明顯的點完全不能夠當成網絡流里的點使用。對於這種題目,我們就要自己構造出來一些新的點來進行網絡流的使用。
單說可能不是很好理解,我們放上幾道題給大家看一下。

[HEOI2016/TJOI2016]游戲

這道題目大意就是讓我們在一個矩陣里放東西,如果沒有硬石頭限制,就是同行同列只能放一個。然后考慮算法,n,m<=50很明顯這題與什么數據結構啊什么的是沒有緣分了。那么就是DP或者網絡流了。。考慮DP明顯的狀態太多根本沒法轉移,那么網絡流呢?這一個一個點完全沒有什么用處連起來也沒法限制。。。這時候我們就要對這些已經有的點進行重構,根據網絡流能夠描述互斥關系的原理來搞出這道題。
那么:
這張網格圖可以抽象成一系列行塊和列塊,列塊和行塊就是說,這一個橫行塊或縱列塊里至多放一個炸彈,另外顯而易見的,我們發現選中一個點的時候會同時選中兩個塊,那么也就是說這張圖滿足兩個限制條件
1.每個點最多被選中一次
2.某些點對之間有必選關系
那么之后連邊跑網絡流即可了。

BZOJ4950:[Wf2017]Mission Improbable

先說一下題目大意,就是有一個網格方陣,每個格子里都有一個權值,題目要求你在保證每行每列最大的權值不變的情況下,取走盡量多的權值。並且本來有權的格子里不能取成0。

不取成0不用考慮,留下1就可以了。那么剩下的就是保證每行每列最大值不變了。我們可以貪心的考慮每行都把那個最大的留下,如果有那么一行一列的箱子的最大值是相同的,這個時候把最大值放在交點處很明顯就要比放在其它的地方要好的多。那么怎么處理這種情況呢?可以像上面的題目那樣把每行每列都構造成點,然后一旦出現了剛才那種點,就連起來。之后跑一遍最大流,就可以求出來我們能夠放在交點出的最大值是多少了。

七、動態思想

1.基本介紹

有一些題目是這個樣子的,他們往往有着較其他網絡流題目更大的數據,並且他們也有一個特點,就是在一部分邊連好之后,之后是先建建圖或者是跑一個點再加一個點這么建圖對最終結果並沒有影響。這樣一來我們就可以通過動態加點或者邊的方式使其滿足題目要求的復雜度。

2.限制條件

2.1數據范圍不能過大

雖說是減小了時間復雜度,但是也只是減少了部分,很多情況下復雜度的等級並沒有變化,只是因為在大部分操作里減少了一些不必要的操作使速度加快。

2.2主要應用於費用流

大多時候動態加點的題目都是費用流題目,原因就是最短路一次基本上也就只能增廣出來一條增廣路,如果是網絡流的話,當前弧優化往往能夠取到更好的效果。費用流由於大部分人都會使用EK的朴素算法,所以動態加點可以是速度提高好幾個檔次。

2.3當前最優原則

剛才說過了前面點的選擇不會影響后面的選擇,這里仔細說一下。因為在跑網絡流的時候,不可避免的后面跑的點回合前面跑的點有沖突,這個時候我們可以通過反邊來使沖突化解。那么如果我們一個一個的加點,很明顯就沒辦法滿足這個條件。但是如果我們可以確定當前這個點跑了這個流,后面的點跑這個流的一定不如這個點優,那么我們就可以無視那個沖突了。

3.經典的例子

對於這個思想有一個很經典的題目,更好的是它甚至有數據弱化版來對比。

[NOI2012]美食節&&[SCOI2007]修車

這兩個題的原理都是一樣的,就是差在了一個數據范圍,我們先說后面那個數據范圍小的。

題目描述

同一時刻有N位車主帶着他們的愛車來到了汽車維修中心。維修中心共有M位技術人員,不同的技術人員對不同的車進行維修所用的時間是不同的。現在需要安排這M位技術人員所維修的車及順序,使得顧客平均等待的時間最小。

說明:顧客的等待時間是指從他把車送至維修中心到維修完畢所用的時間。

數據范圍

(2<=M<=9,1<=N<=60), (1<=T<=1000)

Solution

要求平均時間最短,就等同於要求總時間最短

對於一個修車工先后用\(W_1-W_n\)的幾個人,花費的總時間是

\[W_n\times 1+W_{n-1} \times 2+...+W_1 \times n \]

不難發現倒數第a個修就對總時間產生a*原時間的貢獻

然后我們將每個工人划分成N個階段,(i,t)表示修車工i在倒數第t個修

可以建一個二分圖,左邊表示要修理的東西,右邊表示工人+階段

於是可以從左邊的e向右邊的(i,t)連邊,權值是\(Time[e][i]*t\),就是第e個用i這個修車工所用時間

最小權值完全匹配后,最小權值和除以N就是答案

因為權值是正的,所以一個修車工接到的連線一定是從(i,1)開始連續的,也符合現實情況

因為假設是斷續的,那后面的(i,n)改連向(i,n-k),k<n時,答案更優,違背了前面的最優性

這樣我們建立好圖以后直接跑費用流就可以了。

那么我們繼續看下一道題

題目描述 2.0

m個廚師都會制作全部的n種菜品,但對於同一菜品,不同廚師的制作時間未必相同。他將菜品用1, 2, ..., n依次編號,廚師用1, 2, ..., m依次編號,將第j個廚師制作第i種菜品的時間記為 ti,j 。小M認為:每個同學的等待時間為所有廚師開始做菜起,到自己那份菜品完成為止的時間總長度。換句話說,如果一個同學點的菜是某個廚師做的第k道菜,則他的等待時間就是這個廚師制作前k道菜的時間之和。而總等待時間為所有同學的等待時間之和。現在,小M找到了所有同學的點菜信息: 有 pi 個同學點了第i種菜品(i=1, 2, ..., n)。他想知道的是最小的總等待時間是多少。

數據范圍 2.0

對於100%的數據,n <= 40, m <= 100, p <= 800, ti,j <= 1000 (其中p = ∑pi)

Solution 2.0

連邊還是和上一個題目一樣。那么數據范圍變大了,我們現在的時間復雜度就是O(\(n^2m\times p \times \overline k\))嗯,T了。

那么怎么辦呢?通過仔細的思考可以發現,我們觀察發現,第一次spfa得出的最短路肯定是某人倒數第一個修某車某廚師倒數第一個做某菜,因為倒數第一個肯定比倒數第二個距離短

那么我們可以在一開始建圖的時候,只把所有“倒數第一個做的菜”的那些邊加上

一旦一條增廣路被用掉了(也就是一個廚師-做菜順序二元組(j,k) 被用掉了),那么我們就把所有代表二元組(j,k+1) 加上去(一共有n條),再跑spfa

這樣我們圖中的總邊數不會超過$n\ast\sum_{i=1}^n p \lbrack i\rbrack $

也就是總時間在\(O\left(np^2\ast \overline k\right)\)左右,k是spfa常數,就可以通過這道題。

八、二分圖

二分圖又稱作二部圖,是圖論中的一種特殊模型。 設G=(V,E)是一個無向圖,如果頂點V可分割為兩個互不相交的子集(A,B),並且圖中的每條邊(i,j)所關聯的兩個頂點i和j分別屬於這兩個不同的頂點集(i in A,j in B),則稱圖G為一個二分圖。

好了不介紹這東西的基本定義了隨便百度百度就應該能百度到了,至於增廣路什么的也都去看看基礎教程吧,筆者這里說的是網絡流的建模。。

1.最大匹配

眾所周知(?),二分圖的最大匹配就是在二分圖上跑出來的最大流。那么最優匹配什么的也可以網絡流搞搞搞出來,而且時間復雜度一般都要比想象的好很多,大部分比匈牙利要快。。。

2.基本定義

為了方便待會理解,這里特別放上幾個基本的定義。

最小覆蓋:即在所有頂點中選擇最少的頂點來覆蓋所有的邊。

最大匹配:二分圖左右兩個點集中,選擇有邊相連的兩個匹配成一對(每個點只能匹配一次),所能達到的最大匹配數。

獨立集:集合中的任何兩個點都不直接相連。

3.最小覆蓋數=最大匹配數

這個比較基礎,證起來主要看自己理解吧,具體的證明我也沒有特別好的思路,不過還是盡量給大家證一下吧。我們先搞一個二分圖,然后最大匹配一下,如圖,綠點就是匹配成功的點,紅點就是匹配失敗的點。那么我們可以發現,每一條邊都有兩種情況,一種是連了一個紅點一個綠點,一種是連了兩個綠點。可以發現,沒有任何一條邊會連接兩個紅點,所以我們在選擇覆蓋點的時候都選綠點。

img

然后又因為如果一個點連接了一個紅點,那么與這個點相連的其他的綠點都一定不會連接紅點,自己可以畫一畫,明顯可以發現如果連接紅點的話,那么剛才所求的就不是最大匹配了。那么對於這些點,我們肯定選擇那個連接了紅點的點作為覆蓋點,因為並不會有其他的點與那個紅點相連了。所以可以基本上的出最小覆蓋數=最大匹配。

那么我們就把這類問題轉化成了網絡流了。

4. 最大獨立集=總點數-最小覆蓋集

這個定理非常好用,不少題目都直接間接的用到了這個東西。先上圖:

img

是不是很熟悉,沒錯就是剛才那個圖,筆者實在懶得畫另一個了。我們用那兩個綠點完成了對這個圖的最小覆蓋。

然后我們來反證這個結論,即證最小覆蓋集+最大獨立集\(\neq\)總點數。

首先我們可以看出除了我們選擇的兩個用來最小覆蓋的點之外,剩下的點之間彼此之間都沒有連邊,我們可以嘗試把任意兩個紅點之間連一條邊,那么明顯,我們不滿足最小覆蓋的要求了,或者我們嘗試通過轉換使最小覆蓋更小。。當然不可行,因為我們已經求得就是最小覆蓋了,並且易證的是剩下的所有點一定構成一個獨立集。並且這個獨立集的大小不能夠更大了,然后我們就證出了題目所給的定理。

5.刪邊

題目不適合起太長,這一部分主要是說:在一個二分圖中,如果刪去一條邊能夠使這個圖的最大匹配減小1的話,那么這條邊一定在殘量網絡中滿流,並且它所連接的兩個點一定不在同一個強連通分量當中。

首先滿流的條件一定要有,不滿流那這條邊就不在最大匹配里了。然后就是不在同一個強連通分量里這個條件,我們可以發現,在同一個強連通分量里的話,那他們就可以自己在這個強連通分量的內部進行增廣,也就是說我們可以通過操作從而找到另一組最大匹配並且不需要使用當前這條邊,所以這條邊刪掉也就無可厚非了。

6.最小路徑覆蓋

說一下大意,就是現在有一個有向無環圖G,要求用盡量少的不相交的簡單路徑覆蓋所有的節點 。

那么這個也有一個結論,就是最小路徑覆蓋=原圖節點數-最大匹配

誒,等等,哪里來的最大匹配。。

我們對當前的圖拆點,把每個節點都拆成x,y兩個節點。如果\(x_1有一條指向x_2的邊,那么就把x_1和y_2之間連一條邊\),這樣我們就能得到一個二分圖了,那么為什么這么做可行呢?

一開始每個點都是獨立的為一條路徑,總共有n條不相交路徑。我們每次在二分圖里找一條匹配邊就相當於把兩條路徑合成了一條路徑,也就相當於路徑數減少了1。所以找到了幾條匹配邊,路徑數就減少了多少。所以有最小路徑覆蓋=原圖的結點數-新圖的最大匹配數。

以上就是我們常用的網絡流構圖的基本思想了,接下來筆者會從做過的題目里挑選出來一些覺得建模比較有代表性的題目分享給大家

九、經典的建模

其實你也可以看出來筆者這個地方寫的很青澀,那是早之前寫的了,可能對於剛剛入門網絡流的人比較有用,但是對於大佬們肯定用處不大了,下面的題目肯定也都有自己獨特的見解和理解方式,所以請dalao從6開始看。。。

1.方格取數加強版

注意,這道題和剛才那個不是一道題目。

記不記得我們NOIP曾經考過一道叫做方格取數題目。這道題就是那個題的加強版。本來是要走2次,現在變成了要走n次。。原來的DP方案好像一下子就垮掉了。現在我們來這么考慮,我們把這個題當成一個圖論題而不是DP題,那么我們走過的格子就要拿走相應格子的點權,剛才說過什么,如果是點權問題的話,是不是要拆點。那么我們又怎么處理每個格子可以走無數次呢?好辦,我們對於每個格子的入點和出點分別連兩條邊,一條是費用為點權流量為1的邊,表示可以取走這個格子里的點權;一條是費用為0流量為inf的邊,表示這個格子可以無數次經過。然后相應的建立超級源和超級匯,分別向左上角和右下角連接流量為k的邊來限制要走k次這個條件。這樣我們是不是就做出了這個題。

2.[CQOI2015]網絡吞吐量

這道題就開始慢慢的使建圖復雜起來了,我們根據題意可以發現這道題的網絡流是嚴格的按照最短路跑的。等等?按照最短路跑?這和樹上跑網絡流有個P區別,不直接寫出來那個最小的流量O(1)出解更好么?然后筆者就畫了畫樣例模擬了一下,發現這個樣例,並不只有一條最短路!!!所以我們先求出來一遍最短路之后,通過遍歷一個點的所有出邊的方式判定到下一個點的是不是最短路。如果$ dis[v]=dis[u]+edge[i].dis $那么我們就找到了到下一個點的一條最短路。然后連邊求最短路即可。

3.數字梯形問題

這個題有三問,雖然很相似就是了。讓我們一問一問解決這個題目。P.s.把這個題放在這里的意義就是讓大家思考一下網絡流里連接inf邊的道理。

首先先從起點到最上層的每個數字連流量為1的邊,表示從這些數字開始走,然后從最下層的每個數字連到匯點,流量為1。

①從梯形的頂至底的 m 條路徑互不相交;

這一問就是每個點和每條邊都只能用一次的意思,所以練出來流量為1的邊跑費用流就可以了。

②從梯形的頂至底的 m 條路徑僅在數字結點處相交;

那么我們現在就可以使用重復的點了對不對。。。所以入點和出點的連邊我們就可以連inf了,表示這個點可以用無數次。然后我們再跑一遍費用流。

③從梯形的頂至底的 m 條路徑允許在數字結點相交或邊相交。

話說。。邊怎么相交,這個題貌似相交不了。。實際上就是可以經過重復的邊,然后我們每個點的出點連向下個點的入點的流量也設置為inf然后我們再跑費用流。

好了,看上去這道題A了,那么交上去試試?然后你就會有這種效果:

為什么,就是因為這一句話 “然后從最下層的每個數字連到匯點,流量為1。”,如果我們這么連邊了,那么最后一層的點,就只能使用一次,然后你不就掛掉了。。。所以從第二次開始,我們就要把這條邊連inf變成1,然后就可以了。

4. 餐巾計划問題

網絡流24題里還是有很好的題目的,比如說這一道,建圖方式妥妥的一股清流。首先我們發現這個題的狀態很多,首先是兩個洗毛巾的操作,這個比較好辦,拆點了之后直接去洗就可以了,然后就是這個題目的精髓所在,對於這種直接購買進來的操作,我們往常的思路是向入點連邊,然后限制流量,然而這個題我們僅僅只在入點限制流量,而把買餐巾這個操作連到當天的出點上,這樣就可以使在洗掉的餐巾不足以支持滿流的情況下是這一天滿流。然后因為我們有攢着餐巾不使用這種操作,所以我們要把上一天的入點和下一天的入點之間連一條inf的邊表示這種情況。

5.[CTSC1999]家園

這個題具體來說就是一個拆門的操作(霧),先讓筆者來說明一下拆門這個操作是什么含義吧。。

筆者有次做了一道叫做緊急疏散的網絡流題目,一開始筆者是用時間點建分層圖,結果發現不可以這么做,整個題目會爆炸掉,但是這個題又沒有更好的方法去支持時間的限制,然后筆者發現可以有拆門這種操作,因為門,也就是指定的匯點的個數總是有限的,哪怕你把這東西拆成500個點,也不會造成有太多邊的情況。然后從每天向下一天連邊,就可以在一些題目上避免使用分層圖造成復雜度過高。

那么這個題我們也可以這么干。首先我們把每天的地球和月球都和超級源點和匯點連inf的邊,然后把太空船上一天的位置和這一天的位置連一條容量為太空船裝載人數的邊,然后再從每一天的向下一天連一條inf邊表示可以用人留在這里,我們就可以通過每一天的最大流跑出結果。然后是一個操作,大部分這種題目我們要二分答案,然而二分的代價就是你要有非常繁雜的預處理過程才能保證你建出來的圖沒有問題,然而這個題的數據范圍小到讓你可以直接枚舉,並且每次不把圖清零,而是在已有的殘量圖上繼續跑,從而達到不錯的時間效率與較低的代碼難度。


從這里開始這一個專題要變得不和諧起來了,並且可能有我自己YY的神奇的模型。

6.黑白染色

怎么說呢,黑白染色這東西,很迷。

6.1適用性

黑白染色一定是在棋盤圖上,嗯,這點絕對沒有問題。然后就是要把棋盤的格子當做點,並且經過了染色之后,你會驚奇的發現黑點只會限制白點,不會限制黑點,白點也只會限制黑點。也就是說同色的點之間並沒有直接關系。

6.2為什么使用它

當你拿到一個棋盤的題手足無措,就要被水淹沒時,黑白染色很有可能救你一命。黑白染色,一共只有兩種顏色,所以這樣建圖之后一般都會和二分圖非常像,然后就是隨便匹配的事了。另一種情況是你把這題轉化成了一個最小割的模型,發現題目中刪去某個點或者添加某個點的操作就是在刪邊。從而隨便網絡流跑出來這個題。

6.3模型建立

基本上可以確定的是你選擇一個你喜歡的顏色,把它連向你不喜歡的那個顏色,然后你不喜歡的那個顏色去連匯點,你喜歡的連源點。但是問題也就來了,這個東西適用於幾乎所有要用到黑白染色的題目,那么也就是說,它沒屁用。實際上,真正惡心的一般都在黑白點互相連接的限制上,所以具體問題只能具體分析。

6.4Difficult

然而有的時候發現黑白染色並不能完全解決問題,或者說這個題並不能符合黑白染色的定義。。那么我們就紅黃藍染色,再不行就紅黃綠藍染色。然后重新構造模型。不得不說,這東西有的時候還是能派上大用的。

6.5時間復雜度

黑白染色的題目,我們一定要相信信仰,因為建出來的圖基本上都是那種二分圖,還是邊比較稀疏的那種,所以我們可以幾乎在\(O(n^{1.5})\)的時間復雜度就解決問題,所以不要問為什么有的題1e5的數據都跑網絡流了。

7.切糕模型

事先證明,這個鬼畜的名字不是我叫出來的,是網上某個博主這么叫的,然后我一想確實是很有道理。這種模型本來應該叫距離限制模型。也就是說,每個元素選擇時,有多種選擇,並且相鄰兩個元素之間的選擇會相互限制。例題就是HNOI某一年的一道叫切糕的題目。

其實這個模型挺經典的,但是很多情況下題目不僅僅只是有這樣一個限制,然后就被其它的模型覆蓋了。

8.最大密度子圖

這個事實上都可以單獨拿來當成一個題做了。先說一下要求:

給定一個無向圖,要求從無向圖里抽出一個子圖,使得子圖中的邊數\(\mid E\mid\)與點數\(\mid V\mid\)的比值最大,即求最大的\(\frac{\mid E\mid}{\mid V\mid}\).

給出一種解法,由前面說過的最大權閉合子圖得到的。假設答案為k ,則要求解的問題是:選出一個合適的點集 V 和邊集 E,令(|E|−k∗|V|)取得最大值。所謂“合適”是指滿足如下限制:若選擇某條邊,則必選擇其兩端點。

建圖:以原圖的邊作為左側頂點,權值為1;原圖的點作為右側頂點,權值為 −k (相當於 支出 \(k\))。  若原圖中存在邊 (u,v),則新圖中添加兩條邊 ([uv]−>u), ([uv]−>v),轉換為最大權閉合子圖。 其中k可以二分得到。


鍋,突然發現這個東西沒證。。。。

然后發現第一步並不能證出來。。。

現在我又證出來了。。。

無向圖的子圖的密度:\(D=\frac{|E|}{|V|}\)

看成是分數規划問題,二分答案 λ ,需要求

\(g(\lambda)=max(|E|-\lambda|V|)\)

g(λ)=0時我們取到最優解。

喏,現在權當我這一步證出來了。

給出證明吧。

給出函數\(\lambda=f(x)=\frac{a(x)}{b(x)}(x\in S)\)

那么我們猜測一個最優值λ,重新構造一個函數:

\(g(\lambda)=max\lbrace a(x)-\lambda b(x)\rbrace\)

若我們設一個\(\lambda^*=f(x^*)\)為最優解,那么則有:

\(g(\lambda)<0⇔\lambda>\lambda^*\)

\(g(\lambda)=0⇔ \lambda=\lambda^*\)

\(g(\lambda)>0⇔ \lambda<\lambda^*\)

似乎貌似就是大於小於的情況要么不合法,要么不夠優。當g(λ)=0時正好取到最優解。

然后我們開始思考如何搞出來,可以發現我們選擇一條邊就要選擇與它相連的兩個端點。那么,隨便YY一下,搞一個如上面的建圖,就可以發現上面那個建圖滿足我們的條件。

可是有個問題,當λ取到一定值的時候,它變大並不會影響最大權閉合圖的值,統統都是0.。所以我們二分求得是在g(λ)=0時最小的λ。

9.疏散模型

這個模型就是我自己YY出來的了。前面應該提到過了,我做過一道叫緊急疏散的鬼畜題。這道題讓我生生調了半天代碼,從此對這題印象深刻。

那么這是一個什么樣的模型呢?它利用兩個特點,一個是分層圖的一些概念,一個是前后綴的思想。明顯的是,分層圖我們可能在一些題目中看出,然后坑爹的是,這個題分層的話整個題目復雜度極高,時間空間都不支持。這個時候可以嘗試只拆開匯點,也就是我之前所說的拆門。然后對於那個時間點能到匯點的點,與這個時刻的門連一條邊。然后利用前綴思想,把每一天的匯點一串inf連下去,最后就可以求出。而且時空復雜度都很小啊。

10.優化建圖

有這么些題,你一看就知道應該怎么建圖,然后仔細看看數據范圍,用常規方法不是炸你空間就是讓你T掉。。。。這個時候我們可以嘗試一下優化,比如說使用數據結構。雖然這東西放出來之后一般人不會願意打。那么是什么原理呢?如果我們某個區間里的每個點都要互相連邊,那么就可以建線段樹,然后把對應的區間連邊,然后線段樹的節點之間互相連邊,就可以優化邊數。

然而值得注意的是,我並沒有寫數據結構/優化建圖,因為更不正常的題目,我們或許會用到計算幾何。十分鬼畜,鬼畜至極。。然而這種題目。。鬼才會去寫啊。。。有興趣的去看看[Jsoi2018]絕地反擊 。話說這個優化的原理是什么呢?舉個栗子:假設一個題目有很多點,但是你經過各種手玩和證明之后,發現所有不在凸包上的點不會互相連邊,這個時候求一發凸包,就可以大大的減少邊數了。

11.最長反鏈和最小鏈覆蓋

有向無環圖中,鏈是一個點的集合,這個集合中任意兩個元素v、u,要么v能走到u,要么u能走到v。

反鏈是一個點的集合,這個集合中任意兩點誰也不能走到誰。

最長反鏈是反鏈中最長的那個。

那怎么求呢?

11.1 傳遞閉包

在開始介紹這個之前,我先簡單敘述一下傳遞閉包是個什么東西。可以發現的是百度百科里的東西都奇怪的一批根本讓人看不懂。那么我就盡量通俗的說一下。

傳遞閉包就是求一個圖里面所有滿足傳遞性的點。那么傳遞性又是什么呢?簡單來說就是:假設圖中對於圖中的節點\(i\),若果有一個j點可以到\(i\)\(i\)點又可以到\(k\),那么\(j\)就能到\(k\)。這樣的節點就可以說是有傳遞性。看上去非常的,簡單。其實就是這樣。求完傳遞閉包之后,我們能夠知道的是圖中的任意兩點是否相連。

那么,我們可以用Floyd算法搞出來,沒錯,就是Floyd。只不過我們現在是求兩點是否相連,而不是求其最短路徑。

11.2最小鏈覆蓋

這個東西就是求出最長反鏈的關鍵。因為:最小鏈覆蓋=最長反鏈長度。

那么,最小鏈覆蓋又怎么求出呢?

回想一下我們之前是不是有一個地方介紹過了最小路徑覆蓋。那個地方所求的是不能相交的最小路徑覆蓋,那么這里我們要求的就是路徑可以相交的最小路徑覆蓋。

問題又來了,這個又怎么求解呢?

我們把原來的圖做一遍傳遞閉包,然后如果任意兩點\(x,y\),滿足\(x\)可以到達\(y\),那么我們就在拆點后的二分圖里面連一條\((x_1,y_2)\)的邊。

這樣球可以繞過一些在原圖中被其他路徑占用的點,構造新的路徑從而求出最小路徑覆蓋。

那么這樣我們就把可以相交的最小路徑覆蓋轉化為了路徑不能相交的最小路徑覆蓋了。 從而我們求出了這個圖的最小鏈覆蓋。

11.3 十分感性的證明

那么我們證明一下剛才一個結論的正確性。

我們通過觀察定義可以得出,最長反鏈的點一定在不同的鏈里。所以最長反鏈長度\(\le\)最小鏈覆蓋數。

那么我們接着可以通過感性理解和數學歸納證出最長反鏈長度\(\ge\)最小鏈覆蓋數.

如此我們就能夠得證了。

11.4最長鏈長度 = 最小反鏈覆蓋數

不想證了。這個結論就是上面的反向結論,其實直接背過就可以了。。。。。

12.混合圖的歐拉回路

12.1歐拉回路

(1)給定一個圖G,假設我們能一筆畫,如果圖G中的一個路徑包括每個邊恰好一次,則該路徑稱為歐拉路徑(Euler path)。如果一個回路是歐拉路徑,則稱為歐拉回路(Euler circuit)。
具有歐拉回路的圖稱為歐拉圖(簡稱E圖)。具有歐拉路徑但不具有歐拉回路的圖稱為半歐拉圖。

(2)對於一個無向圖,如果每個點的度數均為偶數,那么這個圖是一個歐拉圖。

(3)對於一個有向圖,如果每個點的出度都等於入度,那么這個圖是一個歐拉圖。

(4)對於無向圖,如果每個點度數均為偶數,或者有且僅有兩個頂點度數為奇數,那么這個圖中存在歐拉路徑。

(5)對於有向圖,如果每個點的出度等於入度,或者有且僅有兩個點不符,且這兩個點一個入度比出度小1,一個出度比入度小1,則這個圖存在歐拉路徑。

12.2求解一般歐拉圖

直接暴力枚舉所有點的出度入度即可,用上面的關系進行判斷,可以求出。

12.3求解混合歐拉圖

明顯,不能通過剛剛的方法來套用到這個題上去。

對於這種圖,我們首先可以自定向無向邊的方向。可以rand,也可以規定一個方向從而全部指向一個方向。不過這樣無法直接得到答案,說不定臉好,但是我們可以通過這樣一個操作搞出來一個完全有向的圖,可以套用上面的性質來初步判斷這個題是否有歐拉回路。不過這樣做明顯沒有依據,只是一個預處理的過程,再通過調整判斷是否有解。

那么如何自調整呢?引用一段話:

所謂的自調整方法就是將其中的一些邊的方向調整回來,使所有的點的出度等於入度。但是有一條邊的方向改變后,可能會改變一個點的出度的同時改變另一個點的入度,相當於一條邊制約着兩個點。同時有些點的出度大於入度,迫切希望它的某些點出邊轉向;而有些點的入度大於出度,迫切希望它的某些入邊轉向。這兩條邊雖然需求不同,但是他們之間往往一條邊轉向就能同時滿足二者。

然后我們發現,網絡流貌似可以滿足這個自調整的性質。

12.4算法步驟

1.\(首先對於每個入度>出度的點,我們將其與源點S連接一條權值為\frac{入度-出度}{2}的邊;\)

\(對於每個入度<出度的點,我們將其與匯點T連接一條權值為\frac{出度-入度}{2}的邊。\)

為什么除以2,因為我們改變一條邊的方向時,會造成出度和入度的改變,所以。。。

2.然后將原圖中所有你定了向的無向邊連接的兩個點連一條邊權為1,方向為你定向方向的無向邊。

3.跑網絡流去,如果滿流自然就有解。

4.把在網絡流中那些因為原圖無向邊而建的流量為1的邊中經過流量的邊反向,就形成了一個能跑出歐拉回路的有向圖,如果要求方案,用有向圖求歐拉回路的方法求解即可 。

13.最大權閉合子圖

發現前面寫最小割的時候把這個東西跳過去了,。。。現在補上。

13.1定義

一個子圖(點集), 如果它的所有的出邊所指向的點都在這個子圖當中,那么它就是閉合子圖。  點權和最大的閉合子圖就是最大閉合子圖。

13.2構圖

設置超級源匯S,T。

然后使S和所有的正權的點連接權值為點權的邊,所有點權為負的點和T連接權值為點權絕對值的邊。然后如果選擇了某個v點才可以選u點,那么把u向v連接一條權值為\(\infty\)的邊。

然后隨便跑跑網絡流。

最大點權和=正點權和-最小割。

13.3並不感性的證明

首先說簡單割,就是我們求最小割的時候每條割邊都與S或T相連。在閉合圖中,由於我們不與S或T連接的邊的權值都是inf,所以我們不會割到他們。容易得證閉合圖中的最小割是簡單割。

接着證明簡單割和閉合圖的關系。

因為簡單割不會含有那些正無窮的邊,所以不含有連向另一個集合(除T)的點,所以其出邊的終點都在簡單割中,滿足閉合圖定義。

我們假設跑完最小割之后的圖中一共有兩部分點\(X,Y\)\(X\)表示與S相連的點,\(Y\)表示與T相連的點,那么我們可以設\(X_0,Y_0\)為負權點,\(X_1,Y_1\)為正權點。然后由於我們求了一遍最小割,根據上面簡單割的證明我們發現不會割去中間inf的邊,所以我們可以求出:

\(最小割=|X_0|+|Y_1|\)

剛才已經證明過X,Y均是閉合圖。

然后我們可以發現X的權值和可以表示為\(Sum_X=X_1-|X_0|\)

我們把上面那個式子代入到這個式子里。

\(Sum_X+最小割=X_1-|X_0|+|X_0|+Y_1\)

然后我們得出:

\(Sum_X+最小割=X_1+Y_1=原圖所有正權點權值之和\)

然后我們得證。

14.文理分科模型

本來不想整理這個,因為確實不是特別的突出,用的地方也不是很多。然后,筆者在寫這個東西的這一天連着看見兩道這樣的題。所以還是整理一下。

這里表示如果點的貢獻不僅僅是點權,而且還有附加的條件的話,比如同時選A,同時選B,選擇不相同。。。。。

那么我們就利用最小割的性質構造一下即可。用“割掉”代表選擇。

然后分情況討論所有選擇情況,使對應情況下被割掉的邊流量之和等於權值 即可。


到這里這個課件的系統知識部分就已經結束了,然而還是有不小的漏洞,比如線性規划,比如有上下界的網絡流,比如流量平衡等等。不過筆者自信自己的絕對是目前網上能找到的最全的一個了。

十、幾道腦洞大開的題目

這些題大多比較新,都是這兩年的新題,並且建模的思路極其鬼畜新穎,所以還是很值得一看的。不,是很值得一看,而且我在這里寫的題解確實,十分詳細。。。。


1.[SDOI2017]新生舞會

題面描述

學校組織了一次新生舞會,Cathy作為經驗豐富的老學姐,負責為同學們安排舞伴。

有nn 個男生和nn 個女生參加舞會買一個男生和一個女生一起跳舞,互為舞伴。

Cathy收集了這些同學之間的關系,比如兩個人之前認識沒計算得出 \(a_{i,j}\)

Cathy還需要考慮兩個人一起跳舞是否方便,比如身高體重差別會不會太大,計算得出 \(b_{i,j}\) ,表示第i個男生和第j個女生一起跳舞時的不協調程度。

當然,還需要考慮很多其他問題。

Cathy想先用一個程序通過\(a_{i,j}\)\(b_{i,j}\) 求出一種方案,再手動對方案進行微調。

Cathy找到你,希望你幫她寫那個程序。

一個方案中有n對舞伴,假設沒對舞伴的喜悅程度分別是\(a'_1,a'_2,...,a'_n\),假設每對舞伴的不協調程度分別是\(b'_1,b'_2,...,b'_n\) 。令

C=\(\frac{a'_1+a'_2+...+a'_n}{b'_1+b'_2+...+b'_n}′\)

Cathy希望C值最大。

數據范圍

對於100%的數據,\(1\le n\le 100,1\le a_{i,j},b_{i,j}<=10^4\)

Solution

這個題乍一看上去很是讓人束手無策,DP明顯被強大的后效性D掉了,據說有線性規划的做法,但是筆者並沒有想出來,搜索的話n=100讓人很是頭疼。那么現在沒有什么辦法,我們就只能再回到題目里面去看看有沒有什么沒有發現的信息。

然后我們發現了這個東西:C=\(\frac{a'_1+a'_2+...+a'_n}{b'_1+b'_2+...+b'_n}′\)

我們發現這個分式一定有意義,那么分母不可能為0,又因為給的都是正整數,所以分母大於0。

那么首先我們先去分母,這個式子就變成了:\((b'_1+b'_2+\cdots+d'_n)\times C=(a'_1+a'_2+\cdots+a'_n)\)

然后我們移項,它就成了\((a'_1+a'_2+\cdots+a'_n)-(b'_1+b'_2+\cdots+d'_n)\times C=0\)

然后我們去括號,就變成了:\(a'_1+a'_2+\cdots+a'_n-b'_1\times C-b'_2\times C-\cdots-b'_n\times C=0\)

為了方便計算,我們可以合並一下同類項:\((a'_1-b'_1\times C)+(a'_2-b'_2\times C)+\cdots+(a'_n-b'_n\times C)=0\)

我們題目中要求的式子就和現在的式子一樣了。

此時我們就可以二分C值,然后判斷tot是否大於或小於0來縮小邊界。

然后我們發現每個{a,b}數對的下標都是一一對應的,所以我們直接從第i個男生想第j個女生連接一條\(a_i-b_i\times C\)的邊就可以了。

說了從這里開始就會有代碼。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<queue>
#define inf 1000000000000001ll
#define maxxx 500000001
#define re register
#define ll long long
#define min(a,b) a<b?a:b 
using namespace std;
const long double eps=0.00000007;
struct po{
    int to,nxt,w;
    ll dis;
};
po edge[800001];
int n,m,s,t,b[205],p;
int head[205],num=-1;
ll tot,dis[205],pa[501][501],pb[501][501];
inline int read()
{
    int x=0,c=1;
    char ch=' ';
    while((ch>'9'||ch<'0')&&ch!='-')ch=getchar();
    while(ch=='-')c*=-1,ch=getchar();
    while(ch<='9'&&ch>='0')x=x*10+ch-'0',ch=getchar();
    return x*c;
}
inline void add_edge(int from,int to,int w,ll dis)
{
    edge[++num].nxt=head[from];
    edge[num].to=to;
    edge[num].w=w;
    edge[num].dis=dis;
    head[from]=num;
}
inline void add(int from,int to,int w,ll dis)
{
    add_edge(from,to,w,dis);
    add_edge(to,from,0,-dis);
}
inline bool spfa()
{
    for(re int i=s;i<=t;i++) dis[i]=inf+1;
    memset(b,0,sizeof(b));
    queue<int> q;
    q.push(t);
    dis[t]=0;
    b[t]=1;
    while(!q.empty()){
        int u=q.front();
        q.pop();
        b[u]=0;
        for(re int i=head[u];i!=-1;i=edge[i].nxt){
            int v=edge[i].to;
            if(edge[i^1].w>0&&dis[v]>dis[u]-edge[i].dis){
                dis[v]=dis[u]-edge[i].dis;
                if(!b[v]){
                    b[v]=1;
                    q.push(v);
                }
            }
        }
    }
    return dis[s]<inf;
}
inline int dfs(int u,int low)
{
    b[u]=1;
    if(u==t) return low;
    int diss=0;
    for(re int i=head[u];i!=-1;i=edge[i].nxt){
        int v=edge[i].to;
        if(edge[i].w&&!b[v]&&dis[v]==dis[u]-edge[i].dis){
            int check=dfs(v,min(edge[i].w,low));
            if(check){
                tot+=check*edge[i].dis;
                low-=check;
                diss+=check;
                edge[i].w-=check;
                edge[i^1].w+=check;
                if(low==0) break;
            }
        }
    }
    return diss;
}
inline void max_flow()
{
    int ans=0;
    while(spfa()){
        b[t]=1;
        while(b[t]){
            memset(b,0,sizeof(b));
            ans+=dfs(s,maxxx);

        }
    }
    return;
}
inline void build(ll x)
{
    memset(head,-1,sizeof(head));
    num=-1;tot=0;
    for(re int i=1;i<=n;i++)
    add(s,i,1,0),add(n+i,t,1,0);
    for(re int i=1;i<=n;i++)
     for(re int j=1;j<=n;j++)
     add(i,j+n,1,-(pa[i][j]-pb[i][j]*x));
}
inline bool check(ll x)
{
    build(x);
    max_flow();
    if(-tot<=0) return 1;
    else return 0;
}
int main()
{
    n=read();
    for(re int i=1;i<=n;i++) 
     for(re int j=1;j<=n;j++)
     pa[i][j]=read(),pa[i][j]*=5000000;
    for(re int i=1;i<=n;i++)
     for(re int j=1;j<=n;j++)
     pb[i][j]=read();
     s=0,t=n+n+1;
    ll l=1,r=50000000000ll;
    while(r>=l){
        ll mid=(l+r)/2;
        if(check(mid))
        r=mid-1; else
        l=mid+1;
    }
    printf("%.6lf",l*1.0/5000000);
}

這個題要用數組寫費用流,卡了結構體。。。。

2.[2018多省省隊聯測]劈配

這是省選考試題了,可惜當時並沒有想到最后。。。

題目描述

總共n名參賽選手(編號從1至n)每人寫出一份代碼並介紹自己的夢想。接着由所有導師對這些選手進行排名。

為了避免后續的麻煩,規定不存在排名並列的情況。

同時,每名選手都將獨立地填寫一份志願表,來對總共m位導師(編號從1至m)作出評價。

志願表上包含了共m檔志願。

對於每一檔志願,選手被允許填寫最多C位導師,每位導師最多被每位選手填寫一次(放棄某些導師也是被允許的)。

在雙方的工作都完成后,進行錄取工作。

每位導師都有自己戰隊的人數上限,這意味着可能有部分選手的較高志願、甚至是全部志願無法得到滿足。節目組對”

前i名的錄取結果最優“作出如下定義:

前1名的錄取結果最優,當且僅當第1名被其最高非空志願錄取(特別地,如果第1名沒有填寫志願表,那么該選手出局)。

前i名的錄取結果最優,當且僅當在前i-1名的錄取結果最優的情況下:第i名被其理論可能的最高志願錄取

(特別地,如果第i名沒有填寫志願表、或其所有志願中的導師戰隊均已滿員,那么該選手出局)。

如果一種方案滿足‘‘前n名的錄取結果最優’’,那么我們可以簡稱這種方案是最優的。

舉例而言,2位導師T老師、F老師的戰隊人數上限分別都是1人;2位選手Zayid、DuckD分列第1、2名。

那么下面3種志願表及其對應的最優錄取結果如表中所示:

img

可以證明,對於上面的志願表,對應的方案都是唯一的最優錄取結果。

每個人都有一個自己的理想值si,表示第i位同學希望自己被第si或更高的志願錄取,如果沒有,那么他就會非常沮喪。

現在,所有選手的志願表和排名都已公示。巧合的是,每位選手的排名都恰好與它們的編號相同。

對於每一位選手,Zayid都想知道下面兩個問題的答案:

在最優的錄取方案中,他會被第幾志願錄取。

在其他選手相對排名不變的情況下,至少上升多少名才能使得他不沮喪。

作為《中國新代碼》的實力派代碼手,Zayid當然輕松地解決了這個問題。

不過他還是想請你再算一遍,來檢驗自己計算的正確性。

數據范圍

img

Solution

這個題剛拿到題我就基本確定網絡流算法了。。你看這名字,劈配,多么的人性化,直接把算法告訴你。然后開始思考建模,最后崩潰。

那么這個題應該怎么做呢,我們一步一步的分析:

2.1面向小范圍數據

首先我們可以發現有那么一部分數據的范圍真是有夠小的,那么我們可以搜索,暴力查找出所有可能的方案,然后選取最優的一個,就可以先把1,2,3這三個數據點的分拿到手。

2.2針對數據特點

我們可以發現這些數據總共有兩個特點,一個是C的特殊性,一個是b的特殊性。

針對C的特殊性,我們發現可以有一個貪心,就是最高的開始一個一個選,如果后面有不能選的,那么他一定就沒人可選,然后暴力求解出第二問就可以很穩的拿到4,5,6,7這40分。

然后針對b的特殊性,嗯,有一個方法來着,動態加邊的二分圖匹配,這樣又可以得到第9個點的10pts。

2.3 各種偽算法

然后就是各種奇奇怪怪的算法了,比如我考場上寫的費用流,回來用mhr的思路寫的拆點的網絡流,還有什么極其暴力的復雜度很高的匈牙利什么的,它們都能過一部分點,但是全都不行。這里介紹一下mhr思路的那個做法,實際是可行的,只是我第二問做錯了,他的第二問寫的太丑T了而已。

很明顯c=1的可以隨便過去,那么C=其他 的呢?我們考慮拆點,把每一個人拆成總共志願個數個點,把所有導師向匯點連邊,所有的志願都和導師連邊。然后對每一個人動態加邊,就是這個人先和他的第一志願連邊,然后判斷是否有流量增量,如果沒有,就先把這個邊刪了,和下一個志願連邊,這樣針對任何一個人,他前面是已經連好的最優的情況,並且由於邊的限制,它不會影響前面的最優情況。這樣跑到最后我們就會得到一個最優解。

並且由於同一個時刻我們這里面最多有nm條邊,所以並不會有時間復雜度問題,這樣第一問就做出來,然后考慮第二問,我們枚舉所有不滿意的人,使用二分,二分這個人前進到多少名可以保證他能夠滿足要求。但是這樣我們每一次都要重新建圖,時間復雜度大概是\(O(nlogn\times n^2m)\),看上去問題很嚴重,當初我和mhr是這么說的,你不用跑網絡流,你看你那里有個memset,你光這么多遍memset就超時了。。。

但是我們在經過一些剪枝優化之后,實際上的復雜度遠遠沒有這么多,就可以在一個合理的時間范圍內得到答案。然后估計可以是最優解最后一名了。。。

因為我第二問求錯了,mhr寫的太丑T了,所以這個方法就不放代碼了。

2.4正解算法

在上一個mhr算法中,我們可以發現每一個人實際上只有一個利用的點,也就是說最后的圖里面我們一共有\((n-1)\times m\)這么多的點白白浪費掉了。於是可以考慮能不能不拆點,找到一些其他的方法來實現那個過程。然后發現剛才拆點的目的是為了在后面的人選的時候不讓前面的人選到更差的。最后刪邊完善了這個操作。但是實際上我們可以通過重新建圖來達到同樣的效果。可以發現的是第一問里面我們是非常嚴格的按照排名選的,一旦一個人的志願等級確定了,那么就一定不會更改。並且這個人選的志願等級我們已經保存在了輸出數組里面。每一次直接重新調用就可以了。可以發現雖然多了一個十分繁瑣的重新建圖的過程,但是並沒有將他的復雜度提升太多。

然后對於第二問,我們依然枚舉n然后二分,但是現在的是時間復雜度變成了\(O(nlogn\times nm)\)的,就可以跑過去了。

代碼如下了:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<queue>
#define re register
#define inf 50000001
#define ll long long
#define min(a,b) a<b?a:b
#define max(a,b) a>b?a:b
using namespace std;
struct po{
    int nxt,to,w;
};
po edge[6000001];
int T,C,n,m,s,t;
int head[501],dep[501],num=-1,b[205],want[202];
int a[205][205],rs[205][205],ql[202],ans,cur[501],last,tag;
int out1[201],out2[201];
inline int read()
{
    int x=0,c=1;
    char ch=' ';
    while((ch>'9'||ch<'0')&&ch!='-')ch=getchar();
    while(ch=='-')c*=-1,ch=getchar();
    while(ch<='9'&&ch>='0')x=x*10+ch-'0',ch=getchar();
    return x*c;
}
inline void add_edge(int from,int to,int w)
{
    edge[++num].nxt=head[from];
    edge[num].to=to;
    edge[num].w=w;
    head[from]=num;
}
inline void add(int from,int to,int w)
{
    add_edge(from,to,w);
    add_edge(to,from,0);
}
inline bool bfs()
{
    memset(dep,0,sizeof(dep));
    queue<int> q;
    while(!q.empty())
    q.pop();
    q.push(s);
    dep[s]=1;
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        for(re int i=head[u];i!=-1;i=edge[i].nxt)
        {
            int v=edge[i].to;
            if(dep[v]==0&&edge[i].w>0)
            {
                dep[v]=dep[u]+1;
                if(v==t)
                return 1;
                q.push(v);
            }
        }
    }
    return 0;
}
inline int dfs(int u,int dis)
{
    if(u==t) return dis;
    int diss=0;
    for(re int& i=cur[u];i!=-1;i=edge[i].nxt){
        int v=edge[i].to;
        if(edge[i].w!=0&&dep[v]==dep[u]+1){
            int check=dfs(v,min(dis,edge[i].w));
            if(check>0)
            {
                dis-=check;
                diss+=check;
                edge[i].w-=check;
                edge[i^1].w+=check;
                if(dis==0) break;
            }
        }
    }
    return diss;
}
inline void dinic()
{
    while(bfs())
    {
        for(re int i=s;i<=t;i++)
            cur[i]=head[i];
        while(int d=dfs(s,inf)) ans+=d;
    }
}
void init(){
    memset(out1,0,sizeof(out1));
    memset(out2,0,sizeof(out2));
    n=read();m=read();
    s=0;t=m+n+1;
    for(re int i=1;i<=n;i++) out1[i]=m+1;
    for(re int i=1;i<=m;i++) b[i]=read();
    for(re int i=1;i<=n;i++)
        for(re int j=1;j<=m;j++)
            a[i][j]=read();        
    for(re int i=1;i<=n;i++) want[i]=read();
}
int main()
{
    //freopen("date.in","r",stdin);
    T=read();C=read();
    while(T--){
        init();
        for(re int i=1;i<=n;i++){
            num=-1;memset(head,-1,sizeof(head));
            for(re int j=1;j<=n;j++){if(j==i) tag=num+1;add(s,j,1);}
            for(re int j=1;j<=m;j++) add(n+j,t,b[j]);
            for(re int j=1;j<i;j++)
                for(re int l=1;l<=m;l++)
                    if(a[j][l]==out1[j]) add(j,n+l,1);
            dinic();
            for(re int l=1;l<=m;l++){
                for(re int j=1;j<=m;j++)
                    if(a[i][j]==l) add(i,n+j,1);
                dinic();
                if(edge[tag].w==0){out1[i]=l;break;}
            }
        }
        for(re int i=1;i<=n;i++){
            if(out1[i]<=want[i]) continue;
            int l=1,r=i-1;out2[i]=i;
            while(l<=r){
                num=-1;memset(head,-1,sizeof(head));
                for(re int j=1;j<=n;j++){if(j==i) tag=num+1;add(s,j,1);}
                for(re int j=1;j<=m;j++) add(n+j,t,b[j]);
                int mid=l+r>>1;
                for(re int j=1;j<mid;j++)
                    for(re int l=1;l<=m;l++)
                        if(a[j][l]==out1[j]) add(j,n+l,1);
                dinic();
                for(re int j=1;j<=m;j++)
                    if(a[i][j]>0&&a[i][j]<=want[i]) add(i,n+j,1);
                dinic();
                if(edge[tag].w==0) {l=mid+1;out2[i]=i-mid;}
                else r=mid-1;
            }
        }
        for(re int i=1;i<=n;i++)
        cout<<out1[i]<<" ";
        cout<<endl;
        for(re int i=1;i<=n;i++)
        cout<<out2[i]<<" ";
        cout<<endl;    
    }
}

3.[2017國家集訓隊測試]無限之環

題目描述

曾經有一款流行的游戲,叫做InfinityLoop,先來簡單的介紹一下這個游戲:
游戲在一個n×m的網格狀棋盤上進行,其中有些小方格中會有水管,水管可能在方格某些方向的邊界的中點有接口
,所有水管的粗細都相同,所以如果兩個相鄰方格的公共邊界的中點都有接頭,那么可以看作這兩個接頭互相連接
。水管有以下15種形狀:
img

游戲開始時,棋盤中水管可能存在漏水的地方。
形式化地:如果存在某個接頭,沒有和其它接頭相連接,那么它就是一個漏水的地方。
玩家可以進行一種操作:選定一個含有非直線型水管的方格,將其中的水管繞方格中心順時針或逆時針旋轉90度。
直線型水管是指左圖里中間一行的兩種水管。
現給出一個初始局面,請問最少進行多少次操作可以使棋盤上不存在漏水的地方。

第一行兩個正整數n,m代表網格的大小。
接下來n行每行m數,每個數是[0,15]中的一個
你可以將其看作一個4位的二進制數,從低到高每一位分別代表初始局面中這個格子上、右、下、左方向上是否有水管接頭。
特別地,如果這個數是000,則意味着這個位置沒有水管。
比如3(0011(2))代表上和右有接頭,也就是一個L型,而12(1100(2))代表下和左有接頭,也就是將L型旋轉180度

數據范圍

\(n×m≤2000\)

Solution

說實話這個題真的不好做,我自己也是調了一個上午,代碼量要好調的就很大,但是縮減代碼長度的話錯了又不好調。。很惡心的一個題。

3.1確定算法

有誰可以一眼看出來這題網絡流我真的服他,牆都不服就服他。這個題筆者仔細揣摩了好久才確定是費用流無疑。

具體怎么看出來的,我把我的分析過程跟大家說一下:首先考慮這個題最可能的算法,看到這種狀態很多的題目首先就是思考能不能搜索,然后發現復雜度太高過不去。接着就是動態規划,筆者是沒想出來,但是有一個大概的動態規划的構思,就是根據水管的種類其實很少,一共就15種,可不可以搞一個狀壓DP試試,讀者有興趣的可以自己思考。然后這題不可能是數據結構,那排除了一切不可能的情況之后,剩下的再不可能也是可能的了。於是思考網絡流。

我們可以這么考慮,只要一個格子有了一個頭,那么它就必須和旁邊的一個格子里的頭對應起來,否則就不能形成通路。所有格子的任何一個頭都滿足這個道理。那么我們就需要讓他們全部匹配。

嗯,匹配,似乎和網絡流沾上點邊了。

3.2問題簡化1

可以先考慮簡單的版本,如果就是給了你這些水管的狀態,也沒讓你轉動他們,你是否能夠用網絡流算出來它能不能形成一條通路呢?答案是可以,我們發現一個地方的水管只和它上下左右的水管有着直接的聯系。這樣我們就把這個簡化過的問題精簡成了一個經典的網絡流題目的模型,沒錯,黑白染色模型。我們拆點之后求最大流是否是水管的接頭數就可以了。

3.3問題簡化2

我們把這個簡單的版本升華一下,現在你可以轉動這些水管了,我們能不能使它形成一個通路呢?答案仍然是可行的,我們只需要拆完點之后把可以轉換到的狀態之間互相連邊就可以了。仍然是求最大流是否是水管接頭數。

3.4回到題目中

現在我們回到原來的題目中,不難發現,我們只要在問題簡化2的基礎上增加一個費用就可以了。那么我們如何設置這些費用呢?

很明顯這個題有三種不同樣式的格子,一個是死胡同形,一個是L形,還有一個T形。剩下的兩種要么不能動,要么動了和沒動一樣,不需要討論。
對於死胡同形,很明顯轉到旁邊需要1的花費,而轉到對面需要2的花費。
對於L形,不大明顯的是我們轉動90度就相當於把一條邊轉到了對面,而180度就是完全旋轉過去。。。
對於T形,可以發現他就是一個反過來的死胡同形,反過來連邊就可以了。。

那么這個題就可以套用費用流的板子了:

#pragma comment(linker, "/STACK:1024000000,1024000000")
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<queue>
#define inf 1000000001
#define re register
#define ll long long
#define min(a,b) a<b?a:b
#define MAXN 20001
#define MAXM 40000005
#define id1 nm[i][j]
#define id2 (nm[i][j]+(n*m))
#define id3 (nm[i][j]+(n*m*2))
#define id4 (nm[i][j]+(n*m*3))
#define id5 (nm[i][j]+(n*m*4))
using namespace std;
int n,m,s,t;
int head[MAXN],num=-1,tot,dis[MAXN],b[MAXN],maxx;
int to[100005],nxt[100005],w[100005],edis[100005];
int a[2001][2001],nm[2001][2001];
inline int read()
{
    int x=0,c=1;
    char ch=' ';
    while((ch>'9'||ch<'0')&&ch!='-')ch=getchar();
    while(ch=='-')c*=-1,ch=getchar();
    while(ch<='9'&&ch>='0')x=x*10+ch-'0',ch=getchar();
    return x*c;
}
inline void add_edge(int from,int too,int ww,ll dis)
{
    nxt[++num]=head[from];
    to[num]=too;
    w[num]=ww;
    edis[num]=dis;
    head[from]=num;
}
inline void add(int from,int too,int ww,ll dis)
{
    add_edge(from,too,ww,dis);
    add_edge(too,from,0,-dis);
}
inline bool spfa()
{
    for(re int i=s;i<=t;i++) dis[i]=inf+1;
    memset(b,0,sizeof(b));
    deque<int> q;
    q.push_back(t);
    dis[t]=0;
    b[t]=1;
    while(!q.empty()){
        int u=q.front();
        q.pop_front();
        b[u]=0;
        for(re int i=head[u];i!=-1;i=nxt[i]){
            int v=to[i];
            if(w[i^1]>0&&dis[v]>dis[u]-edis[i]){
                dis[v]=dis[u]-edis[i];
                if(!b[v]){
                    b[v]=1;
                    if(!q.empty()&&dis[v]<dis[q.front()])
                    q.push_front(v);
                    else
                    q.push_back(v);
                }
            }
        }
    }
    return dis[s]<inf;
}
inline int dfs(int u,int low)
{
    b[u]=1;
    if(u==t) return low;
    int diss=0;
    for(re int i=head[u];i!=-1;i=nxt[i]){
        int v=to[i];
        if(w[i]&&!b[v]&&dis[v]==dis[u]-edis[i]){
            int check=dfs(v,min(w[i],low));
            if(check){
                tot+=check*edis[i];
                low-=check;
                diss+=check;
                w[i]-=check;
                w[i^1]+=check;
                if(low==0) break;
            }
        }
    }
    return diss;
}
inline int max_flow()
{
    int ans=0;
    while(spfa()){
        b[t]=1;
        while(b[t]){
            memset(b,0,sizeof(b));
            ans+=dfs(s,inf);    
        }
    }
    return ans;
}
int main() 
{
    //freopen("date.in","r",stdin);
    memset(head,-1,sizeof(head));
    n=read();m=read();
    for(re int i=1;i<=n;i++)
        for(re int j=1;j<=m;j++)
            a[i][j]=read(),nm[i][j]=(i-1)*m+j;
    s=0;t=5*n*m+1;
    for(re int i=1;i<=n;i++)
        for(re int j=1;j<=m;j++){
            if((i+j)%2==0) add(s,nm[i][j],inf,0);
            else add(nm[i][j],t,inf,0);
        }
    for(re int i=1;i<=n;i++){
        for(re int j=1;j<=m;j++)
        if((i+j)%2==0){
            if(a[i][j]==1){add(id1,id2,1,0);add(id2,id3,1,1);add(id2,id5,1,1);add(id2,id4,1,2);maxx++;}
            if(a[i][j]==2){add(id1,id3,1,0);add(id3,id2,1,1);add(id3,id4,1,1);add(id3,id5,1,2);maxx++;}
            if(a[i][j]==4){add(id1,id4,1,0);add(id4,id3,1,1);add(id4,id5,1,1);add(id4,id2,1,2);maxx++;}
            if(a[i][j]==8){add(id1,id5,1,0);add(id5,id2,1,1);add(id5,id4,1,1);add(id5,id3,1,2);maxx++;}
            if(a[i][j]==3){add(id1,id2,1,0);add(id1,id3,1,0);add(id2,id4,1,1);add(id3,id5,1,1);maxx+=2;}
            if(a[i][j]==6){add(id1,id3,1,0);add(id1,id4,1,0);add(id3,id5,1,1);add(id4,id2,1,1);maxx+=2;}
            if(a[i][j]==9){add(id1,id5,1,0);add(id1,id2,1,0);add(id5,id3,1,1);add(id2,id4,1,1);maxx+=2;}
            if(a[i][j]==12){add(id1,id4,1,0);add(id1,id5,1,0);add(id4,id2,1,1);add(id5,id3,1,1);maxx+=2;}
            if(a[i][j]==7){add(id1,id2,1,0);add(id1,id3,1,0);add(id1,id4,1,0);add(id2,id5,1,1);add(id3,id5,1,2);add(id4,id5,1,1);maxx+=3;}
            if(a[i][j]==11){add(id1,id2,1,0);add(id1,id3,1,0);add(id1,id5,1,0);add(id2,id4,1,2);add(id3,id4,1,1);add(id5,id4,1,1);maxx+=3;}
            if(a[i][j]==13){add(id1,id2,1,0);add(id1,id4,1,0);add(id1,id5,1,0);add(id2,id3,1,1);add(id4,id3,1,1);add(id5,id3,1,2);maxx+=3;}
            if(a[i][j]==14){add(id1,id3,1,0);add(id1,id4,1,0);add(id1,id5,1,0);add(id3,id2,1,1);add(id4,id2,1,2);add(id5,id2,1,1);maxx+=3;}         
            if(a[i][j]==5){add(id1,id2,1,0);add(id1,id4,1,0);maxx+=2;}
            if(a[i][j]==10){add(id1,id3,1,0);add(id1,id5,1,0);maxx+=2;}
            if(a[i][j]==15){add(id1,id2,1,0);add(id1,id3,1,0);add(id1,id4,1,0);add(id1,id5,1,0);maxx+=4;}
        } else {
            if(a[i][j]==1){add(id2,id1,1,0);add(id3,id2,1,1);add(id5,id2,1,1);add(id4,id2,1,2);maxx++;}
            if(a[i][j]==2){add(id3,id1,1,0);add(id2,id3,1,1);add(id4,id3,1,1);add(id5,id3,1,2);maxx++;}
            if(a[i][j]==4){add(id4,id1,1,0);add(id3,id4,1,1);add(id5,id4,1,1);add(id2,id4,1,2);maxx++;}
            if(a[i][j]==8){add(id5,id1,1,0);add(id2,id5,1,1);add(id4,id5,1,1);add(id3,id5,1,2);maxx++;}
            if(a[i][j]==3){add(id2,id1,1,0);add(id3,id1,1,0);add(id4,id2,1,1);add(id5,id3,1,1);maxx+=2;}
            if(a[i][j]==6){add(id3,id1,1,0);add(id4,id1,1,0);add(id5,id3,1,1);add(id2,id4,1,1);maxx+=2;}
            if(a[i][j]==9){add(id5,id1,1,0);add(id2,id1,1,0);add(id3,id5,1,1);add(id4,id2,1,1);maxx+=2;}
            if(a[i][j]==12){add(id4,id1,1,0);add(id5,id1,1,0);add(id2,id4,1,1);add(id3,id5,1,1);maxx+=2;}
            if(a[i][j]==7){add(id2,id1,1,0);add(id3,id1,1,0);add(id4,id1,1,0);add(id5,id2,1,1);add(id5,id3,1,2);add(id5,id4,1,1);maxx+=3;}
            if(a[i][j]==11){add(id2,id1,1,0);add(id3,id1,1,0);add(id5,id1,1,0);add(id4,id2,1,2);add(id4,id3,1,1);add(id4,id5,1,1);maxx+=3;}
            if(a[i][j]==13){add(id2,id1,1,0);add(id4,id1,1,0);add(id5,id1,1,0);add(id3,id2,1,1);add(id3,id4,1,1);add(id3,id5,1,2);maxx+=3;}
            if(a[i][j]==14){add(id3,id1,1,0);add(id4,id1,1,0);add(id5,id1,1,0);add(id2,id3,1,1);add(id2,id4,1,2);add(id2,id5,1,1);maxx+=3;} 
            if(a[i][j]==5){add(id2,id1,1,0);add(id4,id1,1,0);maxx+=2;}
            if(a[i][j]==10){add(id3,id1,1,0);add(id5,id1,1,0);maxx+=2;}
            if(a[i][j]==15){add(id2,id1,1,0);add(id3,id1,1,0);add(id4,id1,1,0);add(id5,id1,1,0);maxx+=4;}
        }
    }
    for(re int i=1;i<=n;i++)
        for(re int j=1;j<=m;j++){
            if((i+j)%2==0){
                if(i>1) add(id2,id4-m,1,0);
                if(j>1) add(id5,id3-1,1,0);
                if(i<n) add(id4,id2+m,1,0);
                if(j<m) add(id3,id5+1,1,0);
            }
        }
    int d=max_flow();
    cout<<((maxx==d<<1)?tot:-1);
    return 0;
}

4.[HAOI2017]新型城市化

題目描述

Anihc國有n座城市.城市之間存在若一些貿易合作關系.如果城市x與城市y之間存在貿易協定.那么城市文和城市y則是一對貿易伙伴(注意:(x,y)和(y,x))是同一對城市)。

為了實現新型城市化.實現統籌城鄉一體化以及發揮城市群輻射與帶動作用.國 決定規划新型城市關系。一些城市能夠被稱為城市群的條件是:這些城市兩兩都是貿易伙伴。 由於Anihc國之前也一直很重視城市關系建設.所以可以保證在目前已存在的貿易合作關系的情況下Anihc的n座城市可以恰好被划分為不超過兩個城市群。

為了建設新型城市關系Anihc國想要選出兩個之前並不是貿易伙伴的城市.使這兩個城市成為貿易伙伴.並且要求在這兩個城市成為貿易伙伴之后.最大城市群的大小至少比他們成為貿易伙伴之前的最大城市群的大小增加1。

Anihc國需要在下一次會議上討論擴大建設新型城市關系的問題.所以要請你求出在哪些城市之間建立貿易伙伴關系可以使得這個條件成立.即建立此關系前后的最大城市群的 大小至少相差1。

輸入輸出格式

輸入格式:

第一行2個整數n,m.表示城市的個數,目前還沒有建立貿易伙伴關系的城市的對數。

接下來m行,每行2個整數x,y表示城市x,y之間目前還沒有建立貿易伙伴關系。

輸出格式:

第一行yi個整數ans,表示符合條件的城市的對數.注意(x,y)與(y,x)算同一對城市。

接下來Ans行,每行兩個整數,表示一對可以選擇來建立貿易伙伴關系的城市。對於 一對城市x,y請先輸出編號更小的那一個。最后城市對與城市對之間順序請按照字典序從小到大輸出。

輸入輸出樣例

輸入樣例#1:

5 3
1 5
2 4
2 5

輸出樣例#1:

2
1 5
2 4

說明

數據規模與約定

數據點1: n≤16

數據點2: n≤16

數據點3~5: n≤100

數據點6: n≤500

數據點7~10: n≤10000

對於所有的數據保證:n <= 10000,0 <= m <= min (150000,n(n-1)/2).保證輸入的城市關系中不會出現(x,x)這樣的關系.同一對城市也不會出現兩次(無重邊.無自環)。

Solution

既然把題放在這里了,那么自然就不會像題解里寫的solution一樣了。

其實這個題的主體思路在前面的幾個分塊中沒有來的及體現出來,涉及到二分圖的問題。這也是筆者一個失誤,二分圖類的問題一定近期補到前面去。

那么我們繼續來看這道題。題目給出的是一些城市之間的關系。然后看那個城市群的定義,這些城市兩兩之間能夠互相到達。加上給出的城市之間的關系是之間沒有路徑能夠互相到達,那么就比較明顯了,如果我們根據題目中要求把城市連起來,那么我們一定可以得到一個二分圖。(為什么忽略了只能是一個城市群的情況??因為就一個城市群你也沒有可以建立的關系了,這么出題沒有意義。)

然后我們仔細考慮這個題讓我們干什么,在兩個本來沒有關系的城市之間建立關系,就是從我們剛剛建立的二分圖里面刪去一條邊,最大城市群的大小增加1,就是讓我們這個二分圖里的最大獨立集增加1。又因為定理:最大獨立集=總點數-最小覆蓋集=最大匹配。所以我們要求的就是刪去一條邊可以使二分圖的最大匹配減小1的總邊數。然后我們可以用網絡流來實現。

那么我們現在就有了一個初步的做法了,大體思路就是枚舉所有的邊,然后不停的網絡流,之后慢慢的計算有那些邊可以刪掉。不過這樣過5個點也就差不多快超時了,網絡上有大佬說退流或許能夠達到更好的時間效率,不過我看上去並沒有從根本上優化算法,所以過掉第6個點還是懸。那么我們有沒有更優化的方法呢?

有的,根據定理:若一條邊一定在最大匹配中,則在最終的殘量網絡中,這條邊一定滿流,且這條邊的兩個頂點一定不在同一個強連通分量中。 可得,我們只需要通過殘量網絡跑一遍Tarjan就可以了。那么這個定理又是怎么來的呢。。(話說為什么有這么多的定理。)

筆者淺顯的證明一下:首先要滿流,然后不在同一個強連通分量里是指,如果在同一個強連通分量里,那么我們在這個強連通分量內部增廣一下,整個分量里的殘量都會變化,但是網絡的最大匹配並不會變化,也就是說我們又能得到一個新的最大匹配,也就是說這條邊的存在是無可厚非的。

那么可以放上代碼了。

Code

#pragma comment(linker, "/STACK:1024000000,1024000000")
#include <iostream>
#include <cstdlib>
#include <cmath>
#include <string>
#include <cstring>
#include <algorithm>
#include <cstdio>
#include <queue>
#include <set>
#include <map>
#define MAXN 200000
#define re register
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
#define ms(arr) memset(arr, 0, sizeof(arr))
const int inf = 0x3f3f3f3f;
struct po
{
    int nxt,to,w,from;    
}edge[250001];
struct ANS
{
    int x,y;
}ans[MAXN];
int n,m,cur[MAXN],head[20002],num=-1,dep[20002],s,t,c[MAXN],vis[20002];
inline int read()
{
    int x=0,c=1;
    char ch=' ';
    while((ch>'9'||ch<'0')&&ch!='-')ch=getchar();
    while(ch=='-') c*=-1,ch=getchar();
    while(ch<='9'&&ch>='0')x=x*10+ch-'0',ch=getchar();
    return x*c;
}
inline void add_edge(int from,int to,int w)
{
    edge[++num].nxt=head[from];
    edge[num].from=from;
    edge[num].to=to;
    edge[num].w=w;
    head[from]=num;
}
inline void add(int from,int to,int w)
{
    add_edge(from,to,w);
    add_edge(to,from,0);
}
inline bool bfs()
{
    memset(dep,0,sizeof(dep));
    queue<int> q;
    while(!q.empty())
    q.pop();
    q.push(s);
    dep[s]=1;
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        for(re int i=head[u];i!=-1;i=edge[i].nxt)
        {
            int v=edge[i].to;
            if(dep[v]==0&&edge[i].w>0)
            {
                dep[v]=dep[u]+1;
                if(v==t)
                return 1;
                q.push(v);
            }
        }
    }
    return 0;
}
inline int dfs(int u,int dis)
{
    if(u==t)
    return dis;
    int diss=0;
    for(re int& i=cur[u];i!=-1;i=edge[i].nxt)
    {
        int v=edge[i].to;
        if(edge[i].w!=0&&dep[v]==dep[u]+1)
        {
            int check=dfs(v,min(dis,edge[i].w));
            if(check!=0)
            {
                dis-=check;
                diss+=check;
                edge[i].w-=check;
                edge[i^1].w+=check;
                if(dis==0) break;
            }
        }
    }
    return diss;
}
inline void dinic()
{
    while(bfs())
    {
        for(re int i=s;i<=t;i++)
        cur[i]=head[i];
        while(int d=dfs(s,inf));
    }
}
void put_color(int u,int col)
{
    c[u]=col;
    vis[u]=1;
    for(re int i=head[u];i!=-1;i=edge[i].nxt){
        int v=edge[i].to;
        if(!vis[v]) put_color(v,col^1);
    }
}
int dfn[MAXN],low[MAXN],stack[MAXN],color_num,color[MAXN],cnt,top;
inline void Tarjan(int u)
{
    dfn[u]=low[u]=++cnt;
    vis[u]=1;
    stack[++top]=u;
    for(re int i=head[u];i!=-1;i=edge[i].nxt){
        if(!edge[i].w){
            int v=edge[i].to;
            if(!dfn[v]){
                Tarjan(v);
                low[u]=min(low[u],low[v]);
            } else if(vis[v]) low[u]=min(low[u],dfn[v]);
        }
    }
    if(dfn[u]==low[u]){
        color[u]=++color_num;
        vis[u]=0;
        while(stack[top]!=u){
            color[stack[top]]=color_num;
            vis[stack[top--]]=0;
        }
        top--;
    }
}
int x[MAXN],y[MAXN],tot;
inline bool cmp(ANS a,ANS b){
    return a.x==b.x?a.y<b.y:a.x<b.x;
}
int main() 
{
    memset(head,-1,sizeof(head));
    n=read();m=read();
    for(re int i=1;i<=m;i++){
        x[i]=read();y[i]=read();
        add_edge(x[i],y[i],0);
        add_edge(y[i],x[i],1);
    }
    
    for(re int i=1;i<=n;i++)
        if(!vis[i]) put_color(i,2);
    memset(head,-1,sizeof(head));
    s=0,t=n+1;num=-1;
    for(re int i=1;i<=n;i++){
        if(c[i]==2)
            add(s,i,1);
        else add(i,t,1);
    }
    for(re int i=1;i<=m;i++){
        if(c[x[i]]==2)
            add(x[i],y[i],1);
        else add(y[i],x[i],1);
    }
    dinic();
    memset(vis,0,sizeof(0));
    for(re int i=1;i<=n;i++)
        if(!dfn[i]) Tarjan(i);
    for(re int i=0;i<=num;i+=2){
        int u=edge[i].from,v=edge[i].to;
        if(!edge[i].w&&color[u]!=color[v]&&u!=s&&v!=t&&u!=t&&v!=s){
            if(u>v) swap(u,v);
            ans[++tot].x=u;ans[tot].y=v;
        }
    }
    
    sort(ans+1,ans+tot+1,cmp);
    cout<<tot<<endl;
    for(re int i=1;i<=tot;i++)
        printf("%d %d\n", ans[i].x,ans[i].y);
    return 0;
}
/*6 5 3 7 2 4 1*/

待續。。。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM