面試高階演算法梳理筆記

gettogetto發表於2017-03-24
作者:尤汐_Jennica
連結:https://www.nowcoder.com/discuss/21253
來源:牛客網

1.1 說明

本篇為《挑戰程式設計競賽(第2版)》讀書筆記系列,旨在:

  • 梳理演算法邏輯
  • 探索優化思路
  • 深入程式碼細節

1.2 目錄

原文首發於個人部落格Jennica.Space,按演算法難度劃分為初中高三個級別,詳細目錄及連結如下:

  • 初級篇

    1. 窮竭搜尋
    2. 貪心
    3. 動態規劃
    4. 資料結構
    5. 圖論
    6. 數論
  • 中級篇

    1. 二分搜尋
    2. 常用技巧
    3. 資料結構(二)
    4. 動態規劃(二)
    5. 網路流
    6. 計算幾何
  • 高階篇

    1. 數論(二)
    2. 博弈論
    3. 圖論(二)
    4. 常用技巧(二)
    5. 智慧搜尋
    6. 分治
    7. 字串

1.3 題解

配套習題及詳解同步釋出在GitHub倉庫acm-challenge-workbook,持續更新。預計在2017年3月完成,歡迎watch。習題難度從國內機試、國外IT名企面試到ACM地區賽不等,吃透演算法習題冊,應聘足以。

1.4 題庫

  • Google Code Jam(GCJ
  • Peking University Online Judge(POJ
  • CodeForces(CF
  • LeetCode(LC
  • Aizu Online Judge(AOJ

2.1 窮竭搜尋

2.1.1 核心思想

  1. 深度優先搜尋(DFS):從某個狀態開始,不斷轉移,直至無法轉移,回退到前一步,再繼續轉移到其他狀態,直到找到最終解。通常採用遞迴函式或者棧(Stack)來實現。
  2. 寬度優先搜尋(BFS):從初始狀態開始,總是先搜尋至距離初始狀態近的狀態。每個狀態都只經過一次,因此複雜度為O(狀態數*轉移方式數)。通常採用迴圈或佇列(Queue)實現。

2.1.2 優化細節

  1. 特殊狀態列舉:可行解空間多數可採用DFS,但當其比較特殊時,可簡短地實現。
    • 全排列使用STL中的next_permutation
    • 組合或子集使用位運算
  2. 剪枝:明確知道從當前狀態無論如何轉移都不會存在解的情況下,不再繼續搜尋而是直接跳過。
  3. 棧記憶體與堆記憶體:
    • main函式中的區域性變數儲存在棧記憶體中,統一分配後不再擴大,影響棧深度,與機器設定有關。通常,C++中執行上萬次遞迴是可行的。
    • new或malloc的分配的是堆記憶體,全域性變數儲存在堆記憶體中,使用全域性變數代替區域性變數可減少棧溢位的風險。
  4. 加深深度優先搜尋(IDDFS):初始的DFS遞迴深度限制為1,在找到解之前不斷增加遞迴深度。

2.2 貪心

2.2.1 核心思想

  1. 貪心演算法:遵循某種規律,不斷貪心選取當前最優策略。
  2. 貪心證明:
    • 與其它選擇方案相比,該演算法並不會得到更差的解(歸納法)
    • 不存在其他的解決方案(反證法)

2.3 動態規劃

2.3.1 核心思想

  1. 動態規劃(DP):通過定義某種最優子狀態,進行狀態間轉移達到最終解。
  2. 記憶化搜尋:將重複狀態通過標記降低複雜度。
  3. 多種形式的DP:搜尋的記憶化或利用遞推關係的DP,或從狀態轉移考慮的DP。狀態定義和迴圈順序都會影響複雜度。

2.3.2 優化細節

  1. 使用memset初始化
  2. 重複迴圈陣列
  3. dp僅bool是一種浪費
  4. 根據規模改變DP物件

2.3.3 經典模型

  1. 揹包問題(01揹包,完全揹包)
  2. 最長子序列(LCS,LIS)
  3. 劃分數(第二類Stirling數,Bell數)

2.4 資料結構

2.4.1 核心思想

  1. 優先佇列:包含兩類操作插入和取值。插入一個數值,獲取最小值並刪除。堆可高效實現優先佇列。
  2. 堆:兒子的值一定不小於父親的值的一種二叉樹。插入時先在堆末插入,不斷上移直至無大小顛倒。取值時,刪除最小值,將堆末節點複製到根,不斷下移直至無大小顛倒。插入和取值複雜度都為O(logn)。
  3. 二叉搜尋樹:對所有節點都滿足,左子樹上的所有節點比自己小,右子樹上的所有節點比自己大。插入與查詢類似二分,刪除時將刪除節點左子樹最大值或右子樹(無左子樹時)上移,每種操作複雜度都為O(logn)。
  4. 並查集:一種管理元素分組情況的資料結構。可以查詢兩個元素是否同組,可以合併兩組元素,但不能進行分割操作。一次操作複雜度為阿克曼函式反函式a(n),比O(logn)快。

2.4.2 優化細節

  1. 平衡二叉樹(AVL):當左右子樹深度差超過1時,將更深的子樹旋轉上移,達到整棵樹的平衡,避免二查搜尋樹退化後複雜度升至O(n)。
  2. 路徑壓縮:並查集向上的遞迴操作中,沿途所有節點一旦向上走到一次根節點,就把其到父親的邊直接連向根。
  3. 並查集的同組:廣義可表示組內所有元素代表的情況同時發生或不發生。
  4. STL標準庫:
    • 優先佇列:priority_queue(預設根為最大值)
    • 二查搜尋樹:set(集合)、map(鍵和值對應)、multiset和multimap(可存放重複鍵值)

2.5 圖論

2.5.1 核心思想

  1. 圖:頂點集合為V、邊集為E的圖記作G=(V,E),從u到v的邊記作e=(u,v)。根據邊是否有向分為有向圖和無向圖,根據是否有環分為有環圖和無環圖。圖可由鄰接表和鄰接矩陣兩種方式表示。
  2. Bellman-Ford演算法(單源最短路):記錄起點到每個點i的最短距離d[i],用所有的邊條件持續更新d[i],直到每個d[i]都已經為最小無法更新。圖可包含負權邊,包含負環的判斷方法為將所有d[i]初始化為0,第V次d[i]是否仍存在更新。複雜度為O(EV)。
  3. Dijkstra演算法(單源最短路):從起點出發出發,更新s所有可到達的邊j,若d[j]有更新,則加入最小堆,以便下次找到剩餘集合中d[i]最小的點i,再從i出發BFS,直到到達終點t。不能處理包含負權邊的圖。複雜度為O(ElogV)。
  4. Floyd-Warshall演算法(多源最短路):定義從i到j且經過k的最短路為d[i][j]用d[i][k]+d[k][j]來更新,三重迴圈直接得到任意兩點間的最短路。圖可包含負權邊,包含負環的判斷方法為是否存在頂點i使d[i][i]為負。複雜度O(V^3)。
  5. Prim演算法(最小生成樹):假設V的子集X已經構造了部分最小生成樹,那麼接下來就是選取從X到X的補集中權值最小的邊加入。可使用最小堆維護待選的邊,複雜度為O(ElogV)。
  6. Kruskal演算法(最小生成樹):將所有邊升序排列,依次取出每條最小的邊,若該邊的兩個端點不在相同並查集內,則將該邊加入最小生成樹,並將兩點用並查集連線。耗時最多的操作為邊的排序,複雜度O(ElogE)。

2.5.2 優化細節

  1. 最短路本質是動態規劃,最小生成樹本質是貪心。
  2. Bellman-Ford演算法和Floyd-Warshall演算法可處理包含負權邊的圖,並結合各自特性判斷是否存在負環。
  3. 差分約束:將不等式組轉化為包含負權邊的單源最短路問題,一般採用Bellman-Ford方法解決。若d[i]+x>=d[j],則建立有向邊e(i,j)=x。從起點s到終點t的最短路d[t]為s和t允許的最大差。若存在負環,則不等式組無解;若d[t]=INF,則s和t相差可任意。

2.6 數論

2.6.1 核心思想

  1. 輾轉相除演算法(最小公約數):gcd(a,b)=gcd(b,a%b),迴圈至b為0,此時得到最小公約數為a。
  2. 擴充套件歐幾里德演算法(解二元一次方程):求解ax+by=gcd(a,b),類似輾轉相除法。求extgcd(a,b,&x,&y)時,遞迴求得d=extgcd(b,a%b,y,x)的解存入y和x。則ax+by=gcd(a,b)的解為x和y-(a/b)*x。
  3. 素數篩法:通過已求得的素數,將所求範圍內所有該素數的倍數都標記為合數。依序遍歷空間,未被篩掉的即為新的素數。複雜度O(nloglogn),可看作線性的。
  4. 反覆平方法(快速冪):求x的n次冪,可二分遞迴求x的n/2次冪,即x^n=(x^(n/2))^2 * x^(n&1)。複雜度為O(logn)。

2.6.2 優化細節

  1. ax+by=gcd(a,b)的解大小:x的絕對值不大於b,y的絕對值不大於a。若要求得滿足某個範圍的解,可通過引數k調節,x+=k(b/d)、y-=k(a/d)為原方程的解簇。
  2. 線性素數篩法:遍歷解空間,無論當前數是否為素數,將已經求得得素數集合中的數乘以它得到合數標記篩去。並且若該數為合數,它乘以的素數為它的因子,則對該數不再繼續迴圈已有的素數集合。上述可保證,每個合數都只通過乘以它最小的因子得到,即複雜度為線性。注意,該方法使得已有的素數集合中的組合並不一定被立即篩去,在以後遍歷到特定合數時才會被標記。
  3. 模運算:用64位處理對32數的模,避免發生溢位。模運算對加減乘可以直接應用,但對同模的兩邊做除法時,若原始ac=bc(mod m),則a-b=m*k/c,則a=b(mod m/gcd(m,c))。

3.1 二分搜尋

3.1.1 核心思想

  1. 二分搜尋:對於某個有序區間,每次查詢區間中點是否滿足條件,並以此為依據,決定遞迴查詢左半區間或右半區間。反覆迴圈上述折半過程,直到條件或精度被滿足。

3.1.2 優化細節

  1. STL:以函式lower_bound()和up_bound()的形式實現了二分搜尋
  2. 結束判定:1次迴圈可將區間減半,迴圈100次可達到精度10^-30 。還可通過區間長度與EPS判斷,但要避免EPS太小因浮點數精度問題造成的死迴圈。

3.1.3 經典模型

  1. 有序陣列中查詢某值
  2. 判斷一個假定解是否可行
  3. 最大化最小值
  4. 最大化平均值

3.2 常用技巧

3.2.1 核心思想

  1. 尺取法:又稱兩點法,通過在區間上標記並順序移動頭尾兩點,將複雜度降為線性。
  2. 反轉(開關問題):若為初末態確定,則可通過貪心求得最少步驟。高斯消元法也可求得一組可行解,且自由變元有限,所以也可以求得最優解。
  3. 集合的整數表示:通過二進位制將集合狀態對映至整數。涉及到的位運算包括:與、或、非(取反)、異或、取負(取反+1)、邏輯左移右移、交、並。遍歷所有子集或找到所有大小為k的子集,都可以通過位運算操作求得字典序升序的二進位制碼。
  4. 折半列舉(雙向搜尋):當全域性列舉複雜度太大時,可將條目折半,分別列舉所有情況。複雜度降為原本平方根。
  5. 座標離散化:將數列排序並去重,將原數列離散化對映至有限可控的區間。

3.3 資料結構(二)

3.3.1 核心思想

  1. 線段樹:是一棵完美二叉樹,樹上的每個節點表示一個區間。根節點維護整個區間,其他每個節點維護父節點二分後的某個區間。查詢和更新複雜度都是O(logn)。
  2. 樹狀陣列(BIT):將線段樹每個節點的右兒子去掉,用每個節點表示區間的右邊界代表該節點的索引,這樣就可以通過一個線性陣列維護所有必要的區間。索引的二進位制最低位的1表示區間長度,值為x&-x。求和和更新複雜度都是O(logn)。
  3. 平方分割:將n個元素裝入√n個桶內,每個桶√n個元素的分桶法,每個桶分別維護自己內部的資訊。對區間的複雜度操作可降至O(√n)。

3.3.2 優化細節

  1. 懶惰標記:線段樹可以通過在父節點上維護一個懶惰標記,來表示整棵子樹的狀態。在自頂向下的查詢操作中,在遞迴該父節點時,將標記下移至兩個兒子節點,並且更新父節點真正維護的直接變數。
  2. 稀疏表:與線段樹類似的是其區間長度都是2的整數次冪,但每個長度層級的區間左端點都連續。進行RMQ查詢時,只需要找到不大於區間的最大2的整數冪,根據這個長度,待求解的左端點及右端點減去長度即為該行在稀疏表內的列的值。預處理時時間和空間複雜度都高達O(nlogn),但結合二分查詢的單次查詢比線段樹快,只需要O(loglogn)。
  3. 維護多項式:如果需要維護的變數可以表示為索引i的n次多項式,則可以用n+1個樹狀陣列來維護。或者,線段樹的每個節點維護n+1個值。
  4. 區域樹:線段樹的每個節點可以維護一個陣列或維護一棵線段樹,適合對矩形的區域進行處理。並且,和樹狀陣列一樣,多重線段樹巢狀可以實現高維度的區域樹。

3.4 動態規劃(二)

3.4.1 核心思想

  1. 狀態壓縮DP:通常結合進位制數的特點,將每個狀態壓縮表示為整數。複雜特殊狀態的轉移就可以表示為整數下標的等式。
  2. 矩陣快速冪:若動態規劃的遞推關係式可以表示為多元一次多項式,則可以通過某常係數矩陣的冪與初始向量的乘積求得最後的結果向量。其中求冪可以結合基於二分思想的快速冪演算法。用n表示冪次數,m表示向量規模,則複雜度為O(m^3logn)。

3.4.2 優化細節

  1. 結合資料結構:某些時候涉及到更新和查詢操作時,如果利用線段樹等高階資料結構處理,可以使得轉移方程中線性的遍歷轉化為對數級別的查詢。

3.5 網路流

3.5.1 核心思想

  1. 最大流:增加反向補償邊,在殘流網路上不斷尋找增廣路。常用樸素尋找增廣路的Ford-Fulkerson演算法,複雜度為O(FE)。通過最短路演算法預處理為分層圖的Dinic演算法,複雜度為O(EV^2)。
  2. 最小割:將割中的邊全部刪去後,源點無通路可再到達匯點。最小割是最大流的強對偶問題,數值相等。
  3. 二分圖匹配:匈牙利演算法遞迴每個頂點,每次遞迴,將已有匹配刪除看能否得到更優解。
  4. 一般圖匹配:常用Edmonds演算法,較為複雜,儘量轉化為二分圖求解。
  5. 最小費用流:在殘流網路上擴充套件最短增廣路時,使用邊的花費作為邊權尋找最短路。f(e)表示e中的流量,h(v)表示勢(殘流網路中s到v的最短路),d(e)表示考慮勢後e的長度。複雜度為O(FElogV)或O(FV^2)。或者通過不斷消去負圈得到最小費用流。

3.5.2 優化細節

  1. 最大流變體:
    • 多個源點匯點:構造超級源點S和匯點T,用S連至多個源點,所有匯點連至T。
    • 無向圖:構造正反方向的兩條邊,容量與無向邊相同。
    • 頂點也有流量限制:將每點拆為入點和出點,入點至出點連邊。
    • 最小流量限制:構造超級源S匯T,對於每條邊構造逆向的容量為下限的滿流圈;連線S到s及t到T之前,通過滿流判斷可行解。
    • 部分邊容量發生變化:若影響原流,則設法將殘流網路中對應邊的容量減少或通過構造逆向圈等價減少。
    • 容量為負數:求最小割時可能出現負邊,此時應通過數值變換設法消除負邊。
  2. 最小費變體:
    • 類最大流變體:構造方式相似。
    • 最小流量限制:將原邊容量減少下限,構造新邊容量為下限且費用為原費用減去一個足夠大的數。
    • 流量任意:由於點的勢會不斷增加,所以僅在勢為負數時增廣,即能保證費用不斷減小。
    • 費用為負:不能用Dijkstra演算法,要用Bellmen-Ford演算法處理負權邊。另外,可以通過對所有邊的費用和最終結果進行適當的變形避免負權邊。
    • 最小化有流邊的費用之和:無法通過最小費用流模型求解,需要建其他模。
  3. 匹配相關對偶問題:
    • 連通圖中,最大匹配+最小邊覆蓋=頂點數
    • 最大獨立集+最小頂點覆蓋=頂點數
    • 二分圖中,最大匹配=最小頂點覆蓋

3.6 計算幾何

3.6.1 核心思想

  1. 平面掃描:掃描線在平面上按既定軌跡移動時,不斷根據掃描線掃過的部分更新,從而得到整體所求結果。掃描的方法,可以從左向右平移與y軸平行的直線,也可以固定射線的端點逆時針旋轉。
  2. 凸包:包圍原點集的最小凸多邊形的點組成的集合,稱為原點集的凸包。凸包上的點不被原點集任意三點連成的三角形包含在內部。通過Graham掃描演算法,將點集按座標排序後,分為上下兩條鏈求解。每次末尾加入新頂點,如果出現了凹的部分,則把凹點刪去。Graham可以在O(nlogn)的時間內構造凸包。最遠點對一定是凸包上的對踵點,可以通過旋轉卡殼法不斷找尋對踵點,在O(n)複雜度內求解。
  3. 數值積分:通常在複雜的幾何相交或求和問題中,通過對某個方向變數的微分,將立體切片或將平面切成線段後積分得到結果。

3.6.2 優化細節

  1. 向量表示:可以使用STL的complex類表示向量,並進行相應的內積外積操作。
  2. 計算誤差:計算幾何中的浮點數大小比較採取與ESP結合的方式,如a<0等價於a<-ESP,a≤0等價於a<ESP。由於double型別的精確尾數大約是10進位制下的15位,當ESP太小時,可能造成假陰性。C++中可以採用更高精度的long double,Java可以使用BigDecimal。
  3. 極限情況:當可行解可以取連續一段值時,很多時候只要考慮邊界的極限情況。

4.1 數論(二)

4.1.1 核心思想

  1. 線性方程組:可以採用高斯消元法求解。將方程組用矩陣表示後,遍歷每列,保留該列係數最大的行(列主元高斯消元,減少誤差),並將其乘以一定倍數用於消除其他行的該列元素。
  2. 一次同餘方程:ax=b (mod m)。定義a的逆元為y滿足ay=1 (mod m),則x=yb(mod m)。逆元y可以用擴充套件歐幾里得求解。
  3. 費馬小定律:若p為素數,a與p互素,則有a^(p-1)=1 (mod p)。
  4. 尤拉定理:對於一個正整數N的素數冪分解N=P1^q1P2^q2...Pn^qn,尤拉函式φ(N)=N(1-1/P1)(1-1/P2)...*(1-1/Pn),意義是不大於N且與N互素的正數個數。此時,對於與N互素的x,有x^φ(N)=1 (mod N)。費馬小定律可以看作尤拉定理的推廣。
  5. 線性同餘方程組:若有解則一定有無數解,解的全集可寫作x=b (mod m)。初始化為x=0,m=1。逐次加入一個新的方程ax=b (mod m),將上一步的x用mt+b的形式代入,轉化為一次同餘方程。
  6. 中國剩餘定理:n=ab(a、b互素),則(x mod n)等價於(x mod a,x mod b)。所以,通過對n的質因數分解,可以通過只考慮模n的素因子的冪p^k來計算。
  7. n!模p:將階乘表示為n!=ap^e,則e=n/p+n/p^2+n/p^3+…。由於階乘中不能被p整除的項呈現週期性,乘積為(p-1)!^(n/p)*(n mod p)!。根據威爾遜定理,(p-1)!=-1(mod p)。考慮可以被p整除的部分,通過全部除以p,可以遞迴到n/p的範圍考慮。
  8. 組合數模p:將n和m用p進製法表示後,根據Lucas定理,Lucas(n,m,p)=c(n%p,m%p)*Lucas(n/p,m/p,p) ,則對於每一位,計算其組合數模p,將答案相乘即為C(n, m)模p。
  9. 容斥原理:先不考慮重疊的情況,將所有物件計算出來,再不斷遞迴把重複計算的數目排斥出去,直到結果既無遺漏又無重複。由於遞迴時排斥採取減法,從全域性來看應根據地櫃深度的奇偶性判斷符號正負。
  10. 莫比烏斯函式:在容斥定理中,每次排斥的規模d如果是n的約數,則被加減多次的總和只和n/d有關。求這個係數的函式叫莫比烏斯函式,記作µ(n/d)。若x可以被大於1的完全平方數整除,則µ(x)=0;否則計算x的質因子個數k,µ(x)=(-1)^k。莫比烏斯反演定理利用µ(x)推出,f(n)=∑g(d)等價於g(d)=∑µ(n/d)*f(d)。
  11. Polya計數定理:在組合問題中,有時要求把旋轉和翻轉之後的狀態看作相同態,計算本質不同的個數。此時應把所有方案重複計算相同次數,再把結果除以重複的次數。另外,立方體的染色、相同顏色的數量限制、相鄰狀態限制,都可以用Polya求解。

4.2 博弈論

4.2.1 核心思想

  1. 必勝策略:任意方式都無法轉移到必勝態的為必敗態N,存在一種方式可以轉移到必敗態的為必勝態P。
  2. Nim:初始有n堆石子,每堆有ai石子,遊戲規則是每次從某堆中取走至少一顆,判斷初始狀態是否必勝。若ai陣列異或結果非0則為必勝態,否則為必敗態。
  3. Grundy數:當前狀態的Grundy值是除任意一步所能轉移到的狀態的Grundy值以外的最小非負整數。所以,從Grundy為x出發,可以轉移到Grundy為0到x-1的狀態。Grundy數等價於Nim中的石子個數,再通過異或判斷狀態必勝與否。

4.2.2 優化細節

  1. 取放石子:Grundy數可以轉移到更大值,等價於Nim中放回石子。但可以通過採取對應策略再轉移到原值的狀態,所以對勝負沒有影響。但此時,狀態可能出現迴圈,所以需要注意可能會出現不分勝負、達成平局、永不結束的情況。
  2. 複合遊戲:由於在Nim中,只要異或值相同,石子堆數不影響局面性質。所以對分割後的各部分取異或,就可以用一個Grundy數來表示幾個遊戲複合而成的狀態。

4.3 圖論(二)

4.3.1 核心思想

  1. 強連通分量:圖中任意兩點可互達的子圖叫做強連通分量。任意有向圖都可以分解為若干個不相交的強連通分量。將圖中的強連通分量都縮成一個頂點,可以將原圖轉化為DAG(有項無環圖)。
  2. 強連通分量分解:通過兩次DFS實現。第一次DFS時,回溯時為頂點標號(後序遍歷)。標號越小表示離圖尾越近,即搜尋樹的葉子。第二次DFS時,將所有邊反向後,從標號大的頂點開始遍歷,每次遍歷的頂點集合組成一個強連通分量,記錄該分量的拓撲序。接著,再取未訪問節點中最大標號的頂點開始DFS,拓撲序加一。直到頂點全部遍歷,演算法結束。總的複雜度是O(V+E)。
  3. 2-SAT:對於每個子句中文字個數不超過二的合取正規化,可以將每個子句等價轉化為兩個蘊含關係,將所有蘊含關係為邊、每個變數取真取假為點,構建有向圖。圖中的每個強連通分量內,若某個點為正確,則分量中所有頂點都為真。對於每個布林變數,考慮其取真取假的兩個點,點所在的強連通分量拓撲序大的點情況為真。由此,得到一組合法的布林變數賦值。
  4. LCA(最近公共祖先):有根樹中,兩個結點的公共祖先中距離最近的那個成為LCA。高效求解LCA可以採用倍增法預處理加二分搜尋,或中序遍歷後利用線段樹或BIT做RMQ求解。

4.4 常用技巧(二)

4.4.1 核心思想

  1. 棧的應用:在棧內維護一個下標和對應值都單向遞增的序列,則可求距離自己最近的比自己大的值。
  2. 雙向佇列的應用:佇列中維護一個以某區間內最小值開始,單向遞增的序列,則可求視窗大小一定的滑動最小值。
  3. 倍增法:通過預處理,計算出距離每個點2的次冪處的狀態。由於轉移到的目的地滿足一定條件,且具有與下標單向相關性,所以可以通過二分搜尋,每次將2的冪減1,判斷是否出終極目標的位置。

4.4.2 優化細節

  1. 數量的二進位制表示:在一定數量的物品中挑選若干個,可以通過每次是否新增2的次冪個物品來決定最終結果。將原本列舉的複雜度O(n)降至二進位制位數O(logn)。
  2. 連續狀態轉移:在DP中,如果某狀態可由連續的下標的一些狀態轉移來,並要求其最值。可以試著把狀態轉移方程分為兩部分,一部分為常量,另一部分為只與前一狀態下標有關。則問題轉化為,求某個函式在某個滑動視窗內的最值。如果視窗大小固定不變,則可利用雙向佇列求解滑動最值。

4.5 智慧搜尋

4.5.1 核心思想

  1. 剪枝:調整搜尋順序,從分支少或影響大的部分開始搜尋。無任何效用或無法到達最終態的步驟可以提前剪枝。沒有最優解則剪枝,通常可以通過貪心等演算法求得最優解下界的下界。
  2. IDA:通過搜尋判斷是否有某個不超過x的解,將x從0開始每次加1,首次求到解的x就是最優解。這樣,在搜尋過程中,就不會訪問比最優解更大的狀態。迭代加深搜尋(IDDFS)類似於寬度有限搜尋,按距離初始狀態的遠近訪問各個狀態,而IDA會通過估算下屆提前剪枝優化。
  3. A*:BFS和Dijkstra利用下界優化,將優先佇列中的鍵值改為初始狀態到當前狀態的距離加上到目標狀態的距離下界。此時,優先佇列頂端元素未必是初始狀態到當前狀態的最短路。

4.5.2 優化細節

  1. IDA與A對比:
    • IDA針對DFS,A針對BFS。
    • IDA不怎麼花費記憶體,A需要關於搜尋空間的線性的記憶體。
    • 通過不同路徑到達同一狀態,IDA效率急劇下降,而A可以通過選取合適的下屆保證每個狀態至多檢查一次。
    • IDA*在不斷增加遞迴深度限制時重複搜尋了很多狀態,但總的訪問狀態數和最後一次訪問狀態數是同一數量級的。

4.6 分治

4.6.1 核心思想

  1. 分治:將問題劃分為更小規模的子問題,遞迴解決子問題,再將結果合併從而高效解決問題。
  2. 數列上的分治:每次遞迴數列長度減半,合併時除將子問題的情況合併外,還需要考慮左右兩個子數列互動問題。通常需要在遞迴時,維護數列狀態,如排序或某些統計值大小。
  3. 樹上的分治:樹的重心是指刪除該頂點後最大子樹頂點數最小的點。通過樹的重心來分解樹,可以避免分解不均勻導致的退化現象。按重心分割,可以保證,每次劃分後子樹的大小都不超過n/2,所以遞迴深度不超過O(logn)。
  4. 平面上的分治:將待求解平面按x或y座標分為兩部分,各子問題遞迴求解,再合併。對於兩子平面互動的部分,通常可以通過一些限制條件,只考慮有可能達到最優解的一些狀態,可以極大降低複雜度。

4.6.2 優化細節

  1. 樹的遞迴:遞迴分解無根樹時,可以代入兩個引數,當前節點及父節點。在更新當前節點時,其所有相連頂點中,除去父節點及已被分解出去的子樹根節點,其餘就是可以繼續遞迴的子節點。

4.7 字串

4.7.1 核心思想

  1. KMP:通過DP計算next陣列,next[i]表示以模式串的第i個字元結尾的字尾與模式串字首的最長公共子串中,公共子串結尾的位置。當模式串與母串進行匹配時,若發生字元不匹配的情況,可以將母串指標位置保持不變,將模式串的指標前移至next位置的後一個字元,若依然不等,遞迴next直到相等或者超出模式串頭。複雜度O(m+n)。
  2. Trie:樹上的邊對應字元,從根到節點的路徑上的字元序列即為該節點表示的字串。Trie是一個高效維護字串集合的資料結構,查詢長度為l的字串複雜度為O(l),同時節約空間。
  3. AC自動機:又叫Aho-Corasick演算法,將多個模式串的所有字首用Trie表示,再在Trie上進行KMP。
  4. Robin-Carp雜湊:將字串看作b進位制數,迴圈移動頭尾,可以得到每個串的雜湊結果,用來判斷兩字串是否匹配。可推廣到二維情況的雜湊演算法。
  5. 字尾陣列(SA):將字串的所有字尾按字典序排列後得到的陣列,sa[i]表示排名第i的字尾的起始位置,rank[i]表示其實位置為i的字尾的排名。利用倍增的思想,可以在log(n(logn)^2)時間內得到字尾陣列。通過計算得到的長度為k的所有字尾及其排名,利用rank[i]和rank[i+2]組合得到長度為2k的字尾及排名。
  6. 高度陣列(LCP):字尾陣列中兩個相鄰字尾的最長公共字首。由於h[i-1]≥h[i]-1,可以從左到右遍歷字尾頭的位置,通過尺取法,在O(n)時間內求得。由於高度陣列的傳遞性,結合RMQ,可以求得任意兩個字尾間的最長字首。

4.7.2 經典模型

  1. 串的狀態轉移:KMP/AC自動機
  2. 單字串匹配:KMP/Robin-Carp雜湊
  3. 多字串匹配:Robin-Carp雜湊/AC自動機/SA+二分搜尋/擴充套件KMP
  4. 最長公共子串:strcat+SA+LCP+RMQ
  5. 最長迴文子串:strcat+SA+LCP+RMQ/Manacher

相關文章