ACM—課程總結

    還記得是在王魯老師的學習交流羣了看到他幫費老師發的廣告才關注到這門課,後來看到學分3.5,這誘惑真心大~然後想也沒想就選上了。當我意識到我的選擇意味着什麼的時候費老師又給了我一次放棄的機會,然而我最後選擇了堅持下去,挑戰自己!一學期下來,感覺收穫是很大很大的,不僅侷限在知識方面很多方面都得到了鍛鍊。所以也是真心的覺得這課選的很值。廢話不多說~下面來總結下這學期學習的內容。

  第一專題:貪心算法

這一專題的題目做了不少,但似乎對貪心的感念依然有些模糊,下面簡單寫下我的理解。

  一.概述

  所謂貪心算法,就是用將一個大的問題細化成若干小問題,通過逐一解決這些小問題,最終求得問題解的方法。這種策略往往易於描述,易於實現—策略可行的話。

二.例題舉例

1.木棒問題。

題意:一個加工木棍的機器,如果後面加工的木棍比前面的長且重,則不需要調機器,否則需要一分鐘調機器,輸入T組測試實例,每組由N跟木棒編寫程序,計算並輸出每組測試實例所用的最短的調機器的時間。

  這個問題首先要對每組測試實例裏的木棒進行排序,按長度升序,長度一樣的輕的在前—預處理。

  接着來到整個算法的核心部分—貪心策略:將木棒進行遍歷,每一遍刪去所有可以一起加工不用調機器的木棒,調機器用時+1,直至所有木棒都刪完。

2.田忌賽馬問題。

題意:輸入田忌的馬分數(分數代表好壞),國王馬的分數,輸出田忌最多能贏幾局。

  預處理依然是排序。

  貪心策略:如果田忌最好的馬能贏國王最好的馬,讓他倆比一局;如果田忌最差的馬能贏國王最差的馬讓他倆比一局;如果上面兩個都不行讓田忌當前最差的馬與國王最好的馬比一局。

3.搬桌子問題。

題意:在400個兩兩相對房間之間搬桌子,走廊一次只能通過一張桌子,把桌子從一個房間移到另一個房間需要10分鐘。輸入T表示搬桌子的組數,輸入N表示每一組要搬的桌子數,接下來的N行輸入桌子搬出的房間和搬入的房間。輸出每一組搬桌子的最短時間。

  預處理排序。

  貪心策略:因爲走廊不可以同時搬運兩張桌子,可以將每兩個相對的門之間的走廊設爲一個參數,統計每組桌子搬完後走廊的佔用次數,最大佔用次數乘10即爲所求時間。

三.總結

  從上面幾個例題來看,要做貪心,往往要對問題進行一定的預處理,往往是排序(並不一定是)。然後進入到整個問題的核心部分貪心策略,貪心策略有的時候是實現問題的轉化(如例3),有時候不用轉化而是反覆遍歷。

  貪心算反往往配合STL內容來實現,常用有:Vector,Set;sort排序函數也是常用的。

第二專題:搜索

提起搜索,大家都不會陌生。它的應用是十分廣泛的,比如目前internet上的搜索引擎,WINDOWS XP操作系統中的文件搜索。同時,搜索是編程解題的一種重要的手段,在競賽中,我們有時會碰到一些題目,它們既不能通過建立數學模型解決,又沒有現成算法 可以套用,或者非遍歷所有狀況纔可以得出正確結果。這時,我們就必須採用搜索算法來解決問題。幾乎每次ACM競賽都要考察到這方面的內容。因此,如何更深 入地瞭解搜索,從而更爲有效地運用這個解題的有力武器,是一個值得深入研究的問題。要掌握搜索的應用技巧,就要了解它的分類及其各方面的特點。

第一部分 基本的搜索算法
一、回溯算法
回溯算法是所有搜索算法中最爲基本的一種算法,其採用了一種“走不通就掉頭”思想作爲其控制結構,其相當於採用了先根遍歷的方法來構造解答樹,可用於找解或所有解以及最優解。

評價:回溯算法對空間的消耗較少,當其與分枝定界法一起使用時,對於所求解在解答樹中層較深的問題 有較好的效果。但應避免在後繼節點可能與前繼節點相同的問題中使用,以免產生循環。

二、深度搜索與廣度搜索
深度搜索與廣度搜索的控制結構和產生系統很相似,唯一的區別在於對擴展節點選取上。由於其保留了所有的前繼節點,所以在產生後繼節點時可以去掉一部分重複 的節點,從而提高了搜索效率。這兩種算法每次都擴展一個節點的所有子節點,而不同的是,深度搜索下一次擴展的是本次擴展出來的子節點中的一個,而廣度搜索 擴展的則是本次擴展的節點的兄弟節點。在具體實現上爲了提高效率,所以採用了不同的數據結構.

評價:廣度搜索是求解最優解的一種較好的方法,在後面將會對其進行進一步的優化。而深度搜索多用於只要求解,並且解答樹中的重複節點較多並且重複較難判斷時使用,但往往可以用A*或回溯算法代替。

第二部分 搜索算法的優化(一)
一、雙向廣度搜索
廣度搜索雖然可以得到最優解,但是其空間消耗增長太快。但如果從正反兩個方向進行廣度搜索,理想情況下可以減少二分之一的搜索量,從而提高搜索速度。

二、分支定界
分支定界實際上是A*算法的一種雛形,其對於每個擴展出來的節點給出一個預期值,如果這個預期值不如當前已經搜索出來的結果好的話,則將這個節點(包括其子節點)從解答樹中刪去,從而達到加快搜索速度的目的。

三、A*算法
A*算法中更一般的引入了一個估價函數f,其定義爲f=g+h。其中g爲到達當前節點的耗費,而h表示對從當前節點到達目標節點的耗費的估計。其必須滿足兩個條件:

1. h必須小於等於實際的從當前節點到達目標節點的最小耗費h*。
2. f必須保持單調遞增。

A*算法的控制結構與廣度搜索的十分類似,只是每次擴展的都是當前待擴展節點中f值最小的一個,如果擴展出來的節點與已擴展的節點重複,則刪去這個節點。如果與待擴展節點重複,如果這個節點的估價函數值較小,則用其代替原待擴展節點。

對A*算法的改進--分階段A*. 當A*算法出現數據溢出時,從待擴展節點中取出若干個估價函數值較小的節點,然後放棄其餘的待擴展節點,從而可以使搜索進一步的進行下去。

四、A*算法與回溯的結合(IDA*)
這是A*算法的一個變形,很好綜合了A*算法的人工智能性和回溯法對空間的消耗較少的優點,在一些規模很大的搜索問題中會起意想不到的效果。它的具體名稱 是 Iterative Deepening A*, 1985年由Korf提出。該算法的最初目的是爲了利用深度搜索的優勢解決廣度A*的空間問題,其代價是會產生重複搜索。歸納一下,IDA*的基本思路 是:首先將初始狀態結點的H值設爲閾值maxH,然後進行深度優先搜索,搜索過程中忽略所有H值大於maxH的結點;如果沒有找到解,則加大閾值 maxH,再重複上述搜索,直到找到一個解。在保證H值的計算滿足A*算法的要求下,可以證明找到的這個解一定是最優解。在程序實現上,IDA* 要比 A* 方便,因爲不需要保存結點,不需要判重複,也不需要根據 H值對結點排序,佔用空間小。

下面,以一個具體的實例來分析比較上述幾種搜索算法的效率等問題。

在scu online judge(http://cs.scu.edu.cn/acm)上有這麼一道題目:這就是古老而又經典的15數碼難題:在4*4的棋盤上,擺有15個棋 子,每個棋子分別標有1-15的某一個數字。棋盤中有一個空格,空格周圍的棋子可以移到空格中。現給出初始狀態和目標狀態,要求找到一種移動步驟最少的方 法。

看到這個題目,會發覺幾乎每個搜索算法都可以解這個問題。而事實確實如此。

首先考慮深度優先搜索,它會遍歷這棵解答樹。這棵解答樹最多可達16!個節點,深度優先搜索必須全部遍歷後,才能從所有解中選出最小的一個做爲答案,其代價是非常巨大的。

其次考慮廣度深度優先搜索,這不失爲一個好辦法。因爲廣度優先搜索的層次遍歷解答樹的特點,一旦搜索到一個目標節點,那麼這時的深度一定是最優解,而不必 象深度優先搜索那樣繼續搜索目標節點,最後比較才能得出最優解。該搜索方法在這道題目上會遇見致命的問題:廣度深度優先搜索是一種盲目的搜索,深度比較大 的測試數據會產生大量的無用的節點,同時消耗很多時間在重複節點的判斷上。

爲了減少重複的節點,加入人工智能性,馬上可以想到用A*算法。經過分析發現,該方法對避免產生大量的無用的節點起到了一定的效果,但是會花97%以上的 時間去判斷新產生節點是否與已擴展的和待擴展的節點重複。看來如何提高判重的速度成爲該題目的關鍵。解決這個問題有很多辦法,比如引入哈希表,對已擴展的 和待擴展的節點採用哈希表存儲,減少判重的代價,或者對已擴展的和待擴展的節點採用桶排序,也可以減少判重的代價。我們現在來嘗試一下用 IDA*算法。該算法有個值得注意的地方:對估計函數的選取。如果選用當前狀態每個位置上與目標狀態每個位置上相同節點的數目加當前狀態的深度作爲估計函 數,由於當前狀態每個位置上與目標狀態每個位置上相同節點的數目這個值一般較小,不能明顯顯示各個狀態之間的差別,運行過程中會產生大量的無用的節點,同 樣會使效率很低,不能在60s以內完成計算。比較優化的一個辦法是選用由於當前狀態每個位置上的數字偏離目標節點該數字的位置的距離加當前狀態的深度作爲 估計函數。這個估計函數的選取沒有統一的標準,找到合適的該函數並不容易,但是可以大致按照這個原則:在一定範圍內加大各個狀態啓發函數值的差別。

實踐證明,該方法用廣泛的通用性,在很多情況下可以替換一般的深度優先搜索和廣度優先搜索。

第三部分 搜索算法的優化(二)
該部分將談到搜索與其他算法的結合。再看scu online judge的一道題目: 給定一個8 * 8的國際象棋棋盤。給出棋盤上任意兩個位置的座標,問馬最少幾步可以從一個位置跳到另外一個位置。

該題目同樣是求最優解,如果用一般的深度優先搜索是很容易超時的。如果用廣度優先搜索,會消耗大量的內存,而且效率是很低的。這裏,我們將嘗試用深度優先搜索加動態規劃的算法解決該問題。

將該棋盤做爲存儲狀態的矩陣。每個矩陣元素的值是該位置到初始位置最少需要的步數。初始位置的元素值爲0。其他位置的元素初始化爲一個很大的正整數。首先 從初始位置開始深度優先搜索,例如某次從(i1,j1)到達位置(i2,j2),如果(i2,j2)處的值大於(i1,j1)的值加1,則(i2,j2) 處的值更新爲(i1,j1)+ 1,表示從(i1,j1) 跳到(i2,j2)比從其他地方跳到(i2,j2)更優,不斷的進行這個過程,直到不能進行下去位置,那麼最後的目標位置的值就是解。這就是一個動態規劃 的思想,每個位置的最優解都是由其他能夠一次跳到這個位置的位置的值決定的,而且是它們中的最小值。同時,該動態規劃又藉助深度優先搜索這個工具,完成對 每個位置的值的刷新,可以算是一個比較經典的深度優先搜索和動態規劃的結合。該問題還需要注意一個剪枝的問題,從起始位置到目標位置的最大步數是多少?經 過計算,最大值是6。所以一旦某個位置的值是6了,就不必再將它去刷新另外的位置,從而剪去了對很多不必要子樹的搜索,大大提高了效率。

第四部分結語
本文的主要的篇幅講的都是理論,但是根本的目的還是指導實踐。搜索,據我認爲,是當今ACM競賽中最常規、也最能體現解題者水平的一類解題方法。 “紙上得來終覺淺,絕知此事要躬行。”要想真正領悟、理解各種搜索的思想,掌握搜索的解題技巧,還需要在實踐中不斷地挖掘、探索。實踐得多了,也就能體會 到漸入佳境之妙了。算法的優化是無窮盡的。

第三專題:動態規劃

一.概述

  動態規劃的基本思想:若要解一個給定問題,我們需要解其不同部分(即子問題),再合併子問題的解以得出原問題的解。 通常許多子問題非常相似,爲此動態規劃法試圖僅僅解決每個子問題一次,從而減少計算量: 一旦某個給定子問題的解已經算出,則將其記憶化存儲,以便下次需要同一個子問題解之時直接查表。 這種做法在重複子問題的數目關於輸入的規模呈指數增長時特別有用。

三大重要性質:

最優子結構性質:如果問題的最優解所包含的子問題的解也是最優的,我們就稱該問題具有最優子結構性質(即滿足最優化原理)。最優子結構性質爲動態規劃算法解決問題提供了重要線索。

子問題重疊性質:子問題重疊性質是指在用遞歸算法自頂向下對問題進行求解時,每次產生的子問題並不總是新問題,有些子問題會被重複計算多次。動態規劃算法正是利用了這種子問題的重疊性質,對每一個子問題只計算一次,然後將其計算結果保存在一個表格中,當再次需要計算已經計算過的子問題時,只是在表格中簡單地查看一下結果,從而獲得較高的效率。

無後效性將各階段按照一定的次序排列好之後,對於某個給定的階段狀態,它以前各階段的狀態無法直接影響它未來的決策,而只能通過當前的這個狀態。換句話說,每個狀態都是過去歷史的一個完整總結。這就是無後向性,又稱爲無後效性。

二.分類解析

1.最長遞增子序列.

例題:

題意:給出序列a [1], a [2], a [3] ...... a [n],計算子序列的最大總和。

思路:最大子序列是要找出由數組成的一維數組中和最大的連續子序列。方法是:只要前i項和還沒有小於0子序列就一直往後擴展,否則丟棄之前的子序列開始新的子序列,同時記錄各個子序列的和,最後取他們中的最大值。

2.最長公共子序列.

例題:

題意:求兩個字符串的最長公共子序列。

思路:動態的方程在第一個元素的相等的時,dp[0][0] = dp[-1][-1] + 1, 天哪,這肯定就會出錯了。在處理時可以選擇字符的讀取從第一個位置開始,或者把 i 號字符的狀態存儲到i+1號位置去,這樣就從1號開始處理了,判定是就是 s1[i-1] == s1[j-1] ?


3.揹包問題.

<1>01揹包.

例題:

題意:一個人收集骨頭。給出他的揹包容量和可選的骨頭的體積和價值,輸出他的揹包能裝下的骨頭的最大價值。

思路:01揹包問題,DP公式都類似:F[i;v] = maxfF[i-1;v];F[i-1;v-Ci] + Wi,由這個公式做變形就可以。下面再來分析一下這個公式:

每種骨頭僅有一件,可以選擇放或不放。用子問題定義狀態:即F[i;v] 表示前i 件物品恰放入一個容量爲v的揹包可以獲得的最大價值。“將前i 個骨頭放入容量爲v的揹包中”這個子問題,若只考慮第i 個骨頭的策略(放或不放),那麼就可以轉化爲一個只和前i-1個骨頭相關的問題。如果不放第i 個骨頭,那麼問題就轉化爲“前i-1個骨頭放入容量爲v的揹包中”,價值爲F[i-1; v];如果放第i 個骨頭,那麼問題就轉化爲“前i-1個骨頭放入剩下的容量爲v-Ci 的揹包中”,此時能獲得的最大價值就是F[i-1;v-Ci] 再加上通過放入第i 個骨頭獲得的價值Wi。

<2>多重揹包.(沒做出來題,只列下思路。)

題意:有N種物品和一個容量爲V的揹包。第i種物品最多有n[i]件可用,每件費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大。

基本算法:這題目和完全揹包問題很類似。基本的方程只需將完全揹包問題的方程略微一改即可,因爲對於第i種物品有n[i]+1種策略:取0件,取1件……取n[i]件。令f[i][v]表示前i種物品恰放入一個容量爲v的揹包的最大權值,則有狀態轉移方程:f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k<=n[i]}複雜度是O(V*Σn[i])。

<3>完全揹包.(依然是隻給出基本思路)

題意:有N種物品和一個容量爲V的揹包,每種物品都有無限件可用。第i種物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大。

基本思路:這個問題非常類似於01揹包問題,所不同的是每種物品有無限件。也就是從每種物品的角度考慮,與它相關的策略已並非取或不取兩種,而是有取0件、取1件、取2件……等很多種。如果仍然按照解01揹包時的思路,令f[i][v]表示前i種物品恰放入一個容量爲v的揹包的最大權值。仍然可以按照每種物品不同的策略寫出狀態轉移方程,像這樣:

f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<=v}

這跟01揹包問題一樣有O(VN)個狀態需要求解,但求解每個狀態的時間已經不是常數了,求解狀態f[i][v]的時間是O(v/c[i]),總的複雜度可以認爲是O(V*Σ(V/c[i])),是比較大的。

總結解題一般步驟:(1)建立模型,確認狀態。(2)找出狀態轉移方程。(3)找出初始條件。

第四專題:圖算法
圖的基本知識 :
頂點:圖中的數據元素稱爲頂點.
有向圖:有方向的圖叫有向圖.
無向圖:沒有方向的圖叫無線圖.
完全圖:有n(n-1)/2條邊的無向圖稱爲完全圖.
有向完全圖:具有n(n-1)條弧的有向圖稱爲有向完全圖.
稀疏圖:有很少條邊或弧的圖稱爲稀疏圖,反之稱爲稠密圖.
權:與圖的邊或弧相關的數叫做權(weight).


1. Relaxation(鬆弛操作): 
procedure relax(u,v,w:integer);//多數情況下不需要單獨寫成procedure。 
begin 
  if dis[u]+w<dis[v] then 
    begin 
      dis[v]:=dis[u]+w; 
      pre[v]:=u; 
    end 
end; 
2. Dijkstra 
1) 適用條件&範圍: 
a) 單源最短路徑(從源點s到其它所有頂點v); 
b) 有向圖&無向圖(無向圖可以看作(u,v),(v,u)同屬於邊集E的有向圖) 
c) 所有邊權非負(任取(i,j)∈E都有Wij≥0); 
2) 算法描述: 
a) 初始化:dis[v]=maxint(v∈V,v≠s); dis[s]=0; pre[s]=s; S={s}; 
b) For i:=1 to n 
1.取V-S中的一頂點u使得dis[u]=min{dis[v]|v∈V-S} 
2.S=S+{u} 
3.For V-S中每個頂點v do Relax(u,v,Wu,v) 
c) 算法結束:dis[i]爲s到i的最短距離;pre[i]爲i的前驅節點 
3) 算法優化: 
使用二叉堆(Binary Heap)來實現每步的DeleteMin(ExtractMin,即算法步驟b中第1步)操作,算法複雜度從O(V^2)降到O((V+E)㏒V)。推薦對稀疏圖使用。 
使用Fibonacci Heap(或其他Decrease操作O(1),DeleteMin操作O(logn)的數據結構)可以將複雜度降到O(E+V㏒V);如果邊權值均爲不大於C的正整數,則使用Radix Heap可以達到O(E+V㏒C)。但因爲它們編程複雜度太高,不推薦在信息學競賽中使用。 
3. Floyd-Warshall 
1) 適用範圍: 
a) APSP(All Pairs Shortest Paths) 
b) 稠密圖效果最佳 
c) 邊權可正可負 
2) 算法描述: 
a) 初始化:dis[u,v]=w[u,v] 
b) For k:=1 to n 
For i:=1 to n 
For j:=1 to n 
If dis[i,j]>dis[i,k]+dis[k,j] Then
Dis[I,j]:=dis[I,k]+dis[k,j]; 
c) 算法結束:dis即爲所有點對的最短路徑矩陣 
3) 算法小結: 
此算法簡單有效,由於三重循環結構緊湊,對於稠密圖,效率要高於執行|V|次Dijkstra算法。時間複雜度O(n^3)。 
考慮下列變形:如(I,j)∈E則dis[I,j]初始爲1,else初始爲0,這樣的Floyd算法最後的最短路徑矩陣即成爲一個判斷I,j是否有通路的矩陣。更簡單的,我們可以把dis設成boolean類型,則每次可以用“dis[I,j]:=dis[I,j]or(dis[I,k]and dis[k,j])”來代替算法描述中的藍色部分,可以更直觀地得到I,j的連通情況。 
與Dijkstra算法類似地,算法中藍色的部分可以加上對Pre數組的更新,不再贅述。 
4. Prim (Dijksta的推廣)
1) 適用範圍: 
a) MST(Minimum Spanning Tree,最小生成樹) 
b) 無向圖(有向圖的是最小樹形圖) 
c) 多用於稠密圖 
2) 算法描述: 
a) 初始化:dis[v]=maxint(v∈V,v≠s); dis[s]=0; pre[s]=s; S={s};tot=0 
b) For i:=1 to n 
1.取頂點v∈V-S使得W(u,v)=min{W(u,v)|u∈S,v∈V-S,(u,v)∈E} 
2.S=S+{v};tot=tot+W(u,v);輸出邊(u,v) 
3.For V-S中每個頂點v do Relax(u,v,Wu,v) 
c) 算法結束:tot爲MST的總權值 
注意:這裏的Relax不同於求最短路徑時的鬆弛操作。它的代碼如下: 
procedure relax(u,v,w:integer);        //鬆弛操作 
begin 
  if w<dis[v] then 
    begin 
      pre[v]:=u; 
      dis[v]:=w; 
    end; 
end; 
可以看到,雖然不同,卻也十分相似。 
3) 算法優化: 
使用二叉堆(Binary Heap)來實現每步的DeleteMin(ExtractMin)操作 
算法複雜度從O(V^2)降到O((V+E)㏒V)。推薦對稀疏圖使用。 
使用Fibonacci Heap可以將複雜度降到O(E+V㏒V),但因爲編程複雜度太高,不推薦在信息學競賽中使用。 
(不要問我爲什麼和Dijkstra一樣……觀察我的prim和dijkstra程序,會發現基本上只有relax和輸出不一樣……) 
5. Kruskal 
1) 適用範圍: 
a) MST(Minimum Spanning Tree,最小生成樹) 
b) 無向圖(有向圖的是最小樹形圖) 
c) 多用於稀疏圖 
d) 邊已經按權值排好序給出 
2) 算法描述: 
基本思想:每次選不屬於同一連通分量(保證無圈)且邊權值最小的2個頂點,將邊加入MST,並將所在的2個連通分量合併,直到只剩一個連通分量 
3) 算法實現: 
a) 將邊按非降序排列(Quicksort,O(E㏒E)) 
b) While 合併次數少於|V|-1 
i. 取一條邊(u,v)(因爲已經排序,所以必爲最小) 
ii. If u,v不屬於同一連通分量 then 
1) 合併u,v所在的連通分量 
2) 輸出邊(u,v) 
3) 合併次數增1;tot=tot+W(u,v) 
c) 算法結束:tot爲MST的總權值 
4) 分析總結: 
檢查2個頂點是否在同一連通分量可以使用並查集實現(連通分量看作等價類)。 
我們可以看到,算法主要耗時在將邊排序上。如果邊已經按照權值順序給出,那太棒了…… 
另外一種可以想到的實現方法爲:O(n)時間關於邊權建二叉小根堆;每次挑選符合條件的邊時使用堆的DelMin操作。這種方法比用Qsort預排序的方法稍微快一些,編程複雜度基本一樣。附程序。 
另外,如果邊權有一定限制,即<=某常數c,則可以使用線性時間排序以獲得更好的時間效率。

  最後說說我的感想:ACM全稱國際大學生算法設計大賽,是一個高挑戰性與高含金量同在的競賽。通過這一學期的學習,我對這個比賽有了初步的認識,也終於可以理解它爲何有如此高的參考價值。至於知識方面,我想我說自己學到了皮毛都有些過,因爲涉及的算法知識真的太多了,這真不是一朝一夕就能掌握的,而且學習知識的同時要配合大量的訓練。就說現在一天搞不出來一道題,總結來說真不是那塊料,僅停留在感興趣層面吧~
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章