⑴ 演算法基礎
謹以此文,感謝我在這個學校最喜歡的兩個老師之一——肖my老師。本文基本為老師上課說講授內容加上一部分自己的感悟拼湊而來,寫作文本的目的是為自己的演算法課程留下一點點東西,站在老師肩膀上形成粗糙的框架,方便以後的復習以及深入。文筆有限,其中包含的錯誤還請多多包容,不吝賜教。
to do list:
時間復雜度中遞歸樹法;動規,分治新的感悟;
點覆蓋:一組點的集合,使得圖中所有邊都至少與該集合中一個點相連。
支配集:一組點的集合,使得圖中所有的點要麼屬於該集合,要麼與該集合相連。
最大團:在一個無向圖中找出點數最多的完全圖。
獨立集:一組點的集合,集合中的頂點兩兩不相鄰。(團轉過來)
SAT問題:也稱布爾可滿足性問題。給一組變
其中Ci被稱為句子。
點覆蓋<->獨立集<->最大團
最小割:割是一組邊集。如s-t割就是如果去掉這些邊,將把原圖劃分為兩個點集,其中一個點集包含s,一個點集包含t。(兩個是指不相連,而不是代表不存在邊相連,如反向邊)
decision problem: 是否存在。
search problem:找到一個解。
(這個還能擴展,比如decision problem在多項式時間內解決,所以他是P問題嗎)
漸進符號:
注意以上三種都是緊的,對應的兩個小寫的符號是不緊的,即如下圖所示:
概念:演算法的時間復雜度是一個函數,用於定性描述演算法的運行時間。注意,這個一個代表演算法輸入字元串長度的函數。
[注]輸入字元串長度是一個比較關鍵的理解,比如在背包問題中,其時間復雜度為O(nW),因為W不定,所以只能是一個偽多項式時間。
比較:c < log2N < n < n * Log2N < n^2 < n^3 < 2^n < 3^n < n! < n^n
大致:常數<對數<冪函數<指數函數<階乘
對於指數是n相關的進行比較,優先比較指數,再比較底數。
記住一個特例:n (logn)<n!<n n
計算:
一般來說,計算採用主方法和遞歸樹法,其中遞歸樹技巧性比較強,主方法其實也是遞歸樹推導歸納而來,且主方法能得到一個比較緊的結果。
主方法:
f(n) = af(n-b)+g(n) =>O( a^(n/b) *g(n) )
P:decision problems有一個多項式演算法。
NP(nondeterministic polynomial-time):decision problems能夠在多項式時間內驗證。
NPC:NP完全問題,首先這個問題是NP的,其次,其他所有問題都可以多項式時間內歸約到它。
NPH:如果所有NP問題都可以多項式時間歸約到某個問題,則稱該問題為NP困難。
因為NP困難問題未必可以在多項式時間內驗證一個解的正確性(即不一定是NP問題),因此即使NP完全問題有多項式時間的解(P=NP),NP困難問題依然可能沒有多項式時間的解。因此NP困難問題「至少與NP完全問題一樣難」。
一些NP問題能在多項式時間內解決,因為 P∈NP
NP難類型問題的證明:
先選好一個已知NP難的問題,然後將已知NP難問題多項式歸約到要證明的問題上。先給出這個歸約,然後再證明這個歸約的正確性。
NPC類型問題的證明:
證明一個問題Y是NPC問題,先說明Y是NP的,然後找到一個NPC問題X,將這個問題X歸約到問題Y上,即證明完成。
常見的NPC問題(重要,規約的時候有用!):
packing problems: set-packing,獨立集
覆蓋問題:集合覆蓋問題,頂點覆蓋問題
嚴格滿足問題(constraint satisfaction problems):SAT,3SAT
序列問題:哈密爾頓迴路,旅行商問題
劃分問題:3D-matching, 3著色問題
數字問題:子集合問題(子集元素之和為t),背包問題
其他:分團問題(是否存在一個規模為k的團)
規約的概念與理解
規約:意味著對問題進行轉換,例如將一個未知的問題轉換成我們能夠解決的問題,轉換的過程可能涉及到對問題的輸入輸出的轉換。
自歸約:search problem <=p decision problem
歸約:A歸約到B,也就是說,我們對A套一個函數f,在f函數的作用下形成一個新的問題,對這個問題運用B的黑盒解法,能夠解決問題A。
(B <=p A)一般說來,B問題如果可以歸約到A問題,也就是說,一個解決A問題的演算法可以被用做子函數(子程序)來解決B問題,也就是說,求解B問題不會比求解A問題更困難。因此,如果B問題是困難的,那麼A問題也就是困難的,因為不存在求解A問題的高效演算法。(最後一句不懂)
我簡單說一下我理解的規約,以X規約到Y為准,大概分成兩個方面:
註:在 三 的一些實例中細品。
概念:在對問題求解時,總是做出在當前看來是最好的選擇。
貪心的證明:先假設貪心演算法得到的解不是最優解,假設S1是貪心演算法得到的解,而S2是所有最優解中和S1具有最多相同元素的解,然後比較S1和S2,觀察S1和S2中第一個(最前面一個)不一樣的元素,然後在貪心解S2中將不一樣的元素換成S1中的那個元素得到另一個最優解S3,這樣S3和S1比S2和S1有更多相同元素,和假設S2是與S1有最多相同元素的最優解矛盾,這樣來推導S1是最優解。
我的理解:假設這個不是最優的,但是一定存在一個最優的解在某一個位置之前和我當前解結構是一樣的,那麼在這個位置,選最優解也可以選當前解,不影響最終答案。
[注]概念很簡單,但是實際操作的時候,貪心的角度很重要,同樣的貪心,方向對了,演算法就是對的。
例子:
給你一系列活動,每個活動有一個起始時間和一個結束時間,要求在活動不沖突的情況下找到一種有最多活動的安排。
對於這個問題,我們有一下幾種貪心的角度:
①將任務按照 開始時間 升序排列。
②將任務按照 結束時間 升序排列。
③將任務按照 任務時長 升序排列。
④對於每一個任務,都記錄與其他任務沖突的數量,按照 沖突數量 的升序排列。
其中1,3,4都是不可以的。
任務結束時間的貪心證明(反證法):
假設貪心不是最最優的,那我們在最優解中找一個與當前解有最相似的解。
由圖可以知道,貪心貪的就是最早結束,所以如果不是最優,那麼最優的結束時間一定晚於貪心的結束時間。
由上圖就可以證明。
最大流通常與最小割相聯系。
f 為任意一個流,cap為容量,對於任意的s-t割出來的點集(A,B),v( f ) <= cap(A, B)。
當流增加到與割的容量相等時候,就不可能再有增長空間了,稱為最大流。
對於割的容量來說,不同的割法會有不同流量,有些割法永遠不會有流達到,比如部分A = {s}, B = {V - s},這種把源點割出來的割法。
綜上,通過這種感性的認識,如果能找到一個最小的割,那麼這個割就一定是最大能跑到的流(如果流能更高的話在這個割上就會超過容量,反證。)
上圖為一條增廣路,一條增廣路即為一條s-t的路徑,在路徑上仍有流可以跑,其曾廣的流就是該條路徑上最小的剩餘容量。(相當於每找一條增廣路,就至少有一條邊達到滿流。)
直到在圖中找不到增廣路,此時已經達到了最大流。
找ST集合:把滿流的邊去掉,從S出發走到能到的點,遍歷的點就是S集合;剩下的點就屬於T集合。注意,如果找到了在找S集合的時候找到了T點,說明還可以繼續找增廣路。
[補]有一個很有趣的延伸,如多源點多終點問題。問:如果我有兩個源點s1,s2,兩個終點t1,t2,我想求一組流,使得s1-t1,s2-t2的流達到最大,是否可以加一個源點S,S與s1,s2相連,邊流無限大;加一個終點T,T與t1,t2相連,邊流無限大,然後這組ST的最大流即可。——答案是No,無法保證是s1-t1,s2-t2,有可能交錯。
例子講的感覺不是特別好,對理解感覺起不到很大作用,希望以後有新的想法後進行補充。
規約是一個重要的概念和思想。
一個圖的 最大獨立集 與 最小點覆蓋 是不相交的兩個點集,它們的並就是整個點集。
個人理解:獨立集和點覆蓋都是從點的角度進行劃分的,如果我們從邊的角度來看,①一個最小的點覆蓋即為我集合中的每一個點都盡可能與更多的邊相連,②同時,一條邊的兩個端點中,只能有一個端點在最小點覆蓋中[下注]
[注]我們假設有一條邊兩個端點(u,v)都在點覆蓋之中,首先顯然u,v都不是端點,因為假設u是端點的話只需要選擇v即可;
給一個集合S和一堆S的子集S1,S2,...,Sm,問是否存在存在k個子集,使它們的並集為S。
構造:
集合為點,集合中的元素為邊,有相同元素的邊相連。(注意如果某一元素只在一個子集中出現,應該怎麼處理呢!)
規約:在構造的圖中找最小的點覆蓋,選中的點能覆蓋所有的邊即為對應集合的並集能包含所有的元素。所以就完成了集合覆蓋到點覆蓋的規約。
構造:每個句子構造一個三角形,把對應變數但是相反取值的點相連。
規約:3SAT的有一個特點就是,每一個句子中至少有一個為真即可,每個句子都必須是真。將相同變數相反取值相連的目的就是,在最大獨立集中,比如選擇x為真,則剩下所有句子中x-ba一定不會被選中,同時由獨立集和構造出來三角形的性質可以知道,每一個句子,有且僅有一個會被選中(為真)。如上圖,x1-ba為真,x2-ba和x3任選一個為真即可滿足。
search problem <=p decision version
比如:如果能在多項式時間內找到一個哈密爾頓圈,那麼就能在多項式時間內找到一個哈密爾頓圈(刪邊)
在此再談P和NP:
我們知道有些問題是可以從搜索問題規約到判斷問題的,也就是所該問題如果能在多項式內判斷,那麼久能在多項式中搜索到,那麼我們只需要說,這個判斷問題能在多項式時間內求解,就叫做P問題,也就是上圖紅字的意思;那NP問題呢,必須要給出一個解的實例,判斷的是這個實例是否滿足求解問題,這個才是上圖中的紅字。比如,我如果能在多項式時間內判斷哈密爾頓圈是否(Yes/No)存在,那這個就是ploy-time algorithm,如果我給出了一系列點,能過多項式時間內判斷這些點能否構成哈密爾頓圈,那這個就是poly-time certifier。
構造:把一個點拆分成三個點。
構造:(下面兩個圖要連在一起看)
從行的角度看,一行代表一個變數;從列的角度來看,每三列代表一個句子。兩邊中一邊是兩個點,一邊是一個點,所以有k個句子的話,每一行有3k+3個節點。從哈密爾頓圈的答案轉到3SAT的答案看這個圈在每一行是從左到右還是從右到左。
子集和問題:給一個集合S,問是否能在集合中選取元素,使得總和為W。
構造:如下圖,按照前六行和前三列進行分割,可以分成4部分,其中1,3,4部分是固定的,即在第一部分,變數v列和 變數為v(包括變數及取反)的行對應的格子為0,其餘為0;第三部分全為0;第四部分按照12依次寫下來。第二部分,如果Ci句子中有變數v,則記為1,因為一個句子只有三個變數,可以簡單通過第二部分每一列和為3進行判定。此時集合已經構造出來,W為111444,與上面的規約相似,可以通過3SAT的簡單性質進行感性的認知。
近似的想法很簡單,要解決一個問題,我們希望能夠做到①求解結果是最優的 ②在多項式時間內解決 ③對於任意的實例都能夠通過該演算法解決。現在對於部分問題,無法完全滿足以上要求,所以就犧牲了①,但是我們希望結果不是盲目的,所以就引入了近似的概念。
近似演算法。比如2-近似,認為W為近似解,W 為最優解,在求最小值的情況下W<=2W ;在求最大值的情況下,W>=1/2W*
給m個機器和n個任務,每個任務有一個ti的執行時間,我們認為完成最後一個任務所需的時間為負載時間,希望能夠讓這個負載時間最短。
第一種:將任務依次放在機器上,當某個機器空閑時立即放入新任務。此時是2近似的。
證明:
引理1.最短時間安排是大於等於任務中時間最長的任務,L* >= max tj
我們在考慮放入最後一個任務前,根據我們放置的規則,該機器是耗時最短,也就是說,該機器此時的用時是低於除掉最後一個任務後的平均時長,更低於所有任務的平均時長(引理2);再根據引理1,最後一個任務應該是小於最優解的。
補充:
在這里,我還想討論一下這個近似演算法的中等於符號,先上結論:等號不一定能夠找到一個實例,但是可以構造出一種結構,通過取極限求得,我們認為這樣 也算是緊的。
構造實例:有m個機器,其中m(m-1)個任務的用時為1,1個任務的用時為m。肯定有一種任務集合,可以按照以下方式進行安排,此時的貪心解為19。
此時最佳的解為10,如下圖:
通過推廣可以知道此時的比為(2m-1)/m,當m取極限,能夠達到2倍。
第二種:將任務從大到小排序,然後依次放在機器上,當某個機器空閑時立即放入新任務。此時是2近似的。
引理3:如果有大於m個任務,那麼L*>=2t(m-1)。證明:t(m+1)是目前最短的任務,且目前所有機器上都有任務了,所以該任務加入時最優的情況不過是加入設備的原有任務剛好和t(m+1)相等,即等號。
(2近似)在n個點中,選取k個中心點,使得這些中心點能夠以半徑R的圓包含所有的點,讓其中最大的半徑最小,如下圖所示:
基礎:距離需要滿足的三個定理①(同一性)dist(x, x) = 0 ②(自反)dist(x, y) = dist(y, x) ③(三角不等式)dist(x, y) <=dist(x, z)+dist(z, y)
r(C)為C集合中所有點的最大覆蓋半徑。(需要求min r(C))
演算法:在點集中任選一個作為中心點,然後重復以下步驟k-1次:選取距離已選點集中最遠的點,加入點集。
證明:先假設r(C )< 1/2 * r(C)以選好的點畫半徑為1/2 * r(C)的圓,顯然可知[注],這個圓里有且僅有一個r(C )中的點。那麼根據在下圖中,根據三角不等式可以得出:
[注]在每個點上r(c )一定會包含到c點,而r(C )<1/2 * r(C),相當於大圓套小圓,所以c*一定在c的圓中。
(2近似)問題還是很好理解的,在點上加權值,要找一個點覆蓋,使得權值最小。如下圖左邊就是一個帶權的最小點覆蓋。
演算法: 任選一條邊(i, j)加上代價,這個代價從零開始,且這個代價的最大值低於i和j節點的權值。顯然,這個邊權值的最大值取決於兩個端點權值的最小值,我們認為當邊權值與點權值相等時,對應的那個點是緊的。把所有緊的點找出來即為點覆蓋。
流程:
證明:
引理:邊權之和小於等於點覆蓋的點權之和。這主要是由於涉及到一條邊上兩個點都被選(緊的)的情況,感性認知可以看上圖,縮放證明如下:
w(S)是等於所選的節點的權值之和的,等於所選節點節點所對應的邊權之和,可以把它放大到所有節點對應邊權之和,這樣因為一條邊(u, v)在u上算過一次後還要在v上算一次,所以等於邊權和的兩倍。再由上面引理可得。
主要為了線性規劃和整數規劃。
(2近似)沒啥好說的,只需要把方程構造出來就行了。
由於求解出來結果不一定是整數,所以我們認為某一點的值大於1/2,就選入點集。
證明:
因為xi+xj >=1,且都是正數,那必至少一個點是大於1/2的(反證,兩個都小於1/2則和小於1)。
給你n個物品和一個背包,每個物品有一個價值v和一個大小w,背包的容量是W,要求讓背包裝下盡可能大價值。
背包的時間復雜度:O(nW)
注意其中n表示物品的個數,無論是1個還是999個,他都是多項式的,這個很好理解。但是W就不一樣了,這是一個數字。我理解的是這個數字會很奇特,比如1.00001,比如99999,這些有可能看起來不大但是實際在處理的時候很難處理的數字,統一的來說,如果我們把這些數字放在電腦上,都會以二進制的方式存儲起來,有些數字用十進製表示很小,但是放在二進制上面就會很大,由W導致不能在多項式時間內解決(找不到一個范圍/上界來框它)。
演算法: 為了處理這個問題,我們改動了dp的狀態轉移方程,要讓這個轉移方程和W無關[注]。
此時還不是多項式的,然後我們再對value進行約。[注]
[注]這兩步中,我們把w改成v,並對v進行近似處理。OPT的含義變成了,在面對是否選擇第i個物品時,要想讓價值達到當前值,最少的weight。理由是更改後的誤差是可以忍受的:對v進行近似,結果只會出現最大價值的上下誤差,如果對w進行近似,則有可能出現該物品不能放入背包中,導致整個物品直接放棄的情況。
⑵ 什麼是貪婪連接枚舉演算法
一.貪婪演算法的定義: 貪婪演算法的定義: 貪婪演算法的定義 貪婪演算法又叫登山法,它的根本思想是逐步到達山頂,即逐步獲得最優解,是解決 最優化問題時的一種簡單但適用范圍有限的策略。 二.貪婪演算法思想: 貪婪演算法思想: 貪婪演算法思想 貪婪演算法採用逐步構造最優解的方法, 即在每個階段, 都選擇一個看上去最優的策 略(在一定的標准下) 。策略一旦選擇就不可再更改,貪婪決策的依據稱為貪婪准則, 也就是從問題的某一個初始解出發並逐步逼近給定的目標, 以盡可能快的要求得到更好 的解。而且它在設計時沒有固定的框架,關鍵在於貪婪策略的選擇。但要注意的是選擇 的貪婪策略要具有無後向性, 即某階段狀態一旦確定下來後, 不受這個狀態以後的決策 的影響,也就是說某狀態以後的過程不會影響以前的狀態,只與當前狀態有關。
⑶ 幫我講一下 動態規劃
動態規劃的特點及其應用
安徽 張辰
目 錄
(點擊進入)
【關鍵詞】
【摘要】
【正文】
§1動態規劃的本質
§1.1多階段決策問題
§1.2階段與狀態
§1.3決策和策略
§1.4最優化原理與無後效性
§1.5最優指標函數和規劃方程
§2動態規劃的設計與實現
§2.1動態規劃的多樣性
§2.2動態規劃的模式性
§2.3動態規劃的技巧性
§3動態規劃與一些演算法的比較
§3.1動態規劃與遞推
§3.2動態規劃與搜索
§3.3動態規劃與網路流
§4結語
【附錄:部分試題與源程序】
1.「花店櫥窗布置問題」試題
2.「釘子與小球」試題
3.例2「花店櫥窗布置問題」方法1的源程序
4.例2「花店櫥窗布置問題」方法2的源程序
5.例3「街道問題」的擴展
6.例4「mod 4最優路徑問題」的源程序
7.例5「釘子與小球」的源程序
8.例6的源程序,「N個人的街道問題」
【參考文獻】
【關鍵詞】動態規劃 階段
【摘要】
動態規劃是信息學競賽中的常見演算法,本文的主要內容就是分析它的特點。
文章的第一部分首先探究了動態規劃的本質,因為動態規劃的特點是由它的本質所決定的。第二部分從動態規劃的設計和實現這兩個角度分析了動態規劃的多樣性、模式性、技巧性這三個特點。第三部分將動態規劃和遞推、搜索、網路流這三個相關演算法作了比較,從中探尋動態規劃的一些更深層次的特點。
文章在分析動態規劃的特點的同時,還根據這些特點分析了我們在解題中應該怎樣利用這些特點,怎樣運用動態規劃。這對我們的解題實踐有一定的指導意義。
【正文】
動態規劃是編程解題的一種重要的手段,在如今的信息學競賽中被應用得越來越普遍。最近幾年的信息學競賽,不分大小,幾乎每次都要考察到這方面的內容。因此,如何更深入地了解動態規劃,從而更為有效地運用這個解題的有力武器,是一個值得深入研究的問題。
要掌握動態規劃的應用技巧,就要了解它的各方面的特點。首要的,是要深入洞悉動態規劃的本質。
§1動態規劃的本質
動態規劃是在本世紀50年代初,為了解決一類多階段決策問題而誕生的。那麼,什麼樣的問題被稱作多階段決策問題呢?
§1.1多階段決策問題
說到多階段決策問題,人們很容易舉出下面這個例子。
[例1] 多段圖中的最短路徑問題:在下圖中找出從A1到D1的最短路徑。
仔細觀察這個圖不難發現,它有一個特點。我們將圖中的點分為四類(圖中的A、B、C、D),那麼圖中所有的邊都處於相鄰的兩類點之間,並且都從前一類點指向後一類點。這樣,圖中的邊就被分成了三類(AB、BC、CD)。我們需要從每一類中選出一條邊來,組成從A1到D1的一條路徑,並且這條路徑是所有這樣的路徑中的最短者。
從上面的這個例子中,我們可以大概地了解到什麼是多階段決策問題。更精確的定義如下:
多階段決策過程,是指這樣的一類特殊的活動過程,問題可以按時間順序分解成若干相互聯系的階段,在每一個階段都要做出決策,全部過程的決策是一個決策序列[1]。要使整個活動的總體效果達到最優的問題,稱為多階段決策問題。
從上述的定義中,我們可以明顯地看出,這類問題有兩個要素。一個是階段,一個是決策。
§1.2階段與狀態
階段:將所給問題的過程,按時間或空間特徵分解成若干相互聯系的階段,以便按次序去求每階段的解。常用字母k表示階段變數。[1]
階段是問題的屬性。多階段決策問題中通常存在著若干個階段,如上面的例子,就有A、B、C、D這四個階段。在一般情況下,階段是和時間有關的;但是在很多問題(我的感覺,特別是信息學問題)中,階段和時間是無關的。從階段的定義中,可以看出階段的兩個特點,一是「相互聯系」,二是「次序」。
階段之間是怎樣相互聯系的?就是通過狀態和狀態轉移。
狀態:各階段開始時的客觀條件叫做狀態。描述各階段狀態的變數稱為狀態變數,常用sk表示第k階段的狀態變數,狀態變數sk的取值集合稱為狀態集合,用Sk表示。[1]
狀態是階段的屬性。每個階段通常包含若干個狀態,用以描述問題發展到這個階段時所處在的一種客觀情況。在上面的例子中,行人從出發點A1走過兩個階段之後,可能出現的情況有三種,即處於C1、C2或C3點。那麼第三個階段就有三個狀態S3={C1,C2,C3}。
每個階段的狀態都是由以前階段的狀態以某種方式「變化」而來,這種「變化」稱為狀態轉移(暫不定義)。上例中C3點可以從B1點過來,也可以從B2點過來,從階段2的B1或B2狀態走到階段3的C3狀態就是狀態轉移。狀態轉移是導出狀態的途徑,也是聯系各階段的途徑。
說到這里,可以提出應用動態規劃的一個重要條件。那就是將各階段按照一定的次序排列好之後,對於某個給定的階段狀態,它以前各階段的狀態無法直接影響它未來的發展,而只能通過當前的這個狀態。換句話說,每個狀態都是「過去歷史的一個完整總結[1]」。這就是無後效性。對這個性質,下文還將會有解釋。
§1.3決策和策略
上面的階段與狀態只是多階段決策問題的一個方面的要素,下面是另一個方面的要素——決策。
決策:當各段的狀態取定以後,就可以做出不同的決定,從而確定下一階段的狀態,這種決定稱為決策。表示決策的變數,稱為決策變數,常用uk(sk)表示第k階段當狀態為sk時的決策變數。在實際問題中,決策變數的取值往往限制在一定范圍內,我們稱此范圍為允許決策集合。常用Dk(sk)表示第k階段從狀態sk出發的允許決策集合。顯然有uk(sk) Dk(sk)。[1]
決策是問題的解的屬性。決策的目的就是「確定下一階段的狀態」,還是回到上例,從階段2的B1狀態出發有三條路,也就是三個決策,分別導向階段3的C1、C2、C3三個狀態,即D2(B1)={C1,C2,C3}。
有了決策,我們可以定義狀態轉移:動態規劃中本階段的狀態往往是上一階段和上一階段的決策結果,由第k段的狀態sk和本階段的決策uk確定第k+1段的狀態sk+1的過程叫狀態轉移。狀態轉移規律的形式化表示sk+1=Tk(sk,uk)稱為狀態轉移方程。
這樣看來,似乎決策和狀態轉移有著某種聯系。我的理解,狀態轉移是決策的目的,決策是狀態轉移的途徑。
各段決策確定後,整個問題的決策序列就構成一個策略,用p1,n={u1(s1),u2(s2),…, un(sn)}表示。對每個實際問題,可供選擇的策略有一定范圍,稱為允許策略集合,記作P1,n,使整個問題達到最有效果的策略就是最優策略。[1]
說到這里,又可以提出運用動態規劃的一個前提。即這個過程的最優策略應具有這樣的性質:無論初始狀態及初始決策如何,對於先前決策所形成的狀態而言,其以後的所有決策應構成最優策略[1]。這就是最優化原理。簡言之,就是「最優策略的子策略也是最優策略」。
§1.4最優化原理與無後效性
這里,我把最優化原理定位在「運用動態規劃的前提」。這是因為,是否符合最優化原理是一個問題的本質特徵。對於不滿足最優化原理的一個多階段決策問題,整體上的最優策略p1,n同任何一個階段k上的決策uk或任何一組階段k1…k2上的子策略pk1,k2都不存在任何關系。如果要對這樣的問題動態規劃的話,我們從一開始所作的劃分階段等努力都將是徒勞的。
而我把無後效性定位在「應用動態規劃的條件」,是因為動態規劃是按次序去求每階段的解,如果一個問題有後效性,那麼這樣的次序便是不合理的。但是,我們可以通過重新劃分階段,重新選定狀態,或者增加狀態變數的個數等手段,來是問題滿足無後效性這個條件。說到底,還是要確定一個「序」。
在信息學的多階段決策問題中,絕大部分都是能夠滿足最優化原理的,但它們往往會在後效性這一點上來設置障礙。所以在解題過程中,我們會特別關心「序」。對於有序的問題,就會考慮到動態規劃;對於無序的問題,也會想方設法來使其有序。
§1.5最優指標函數和規劃方程
最優指標函數:用於衡量所選定策略優劣的數量指標稱為指標函數,最優指標函數記為fk(sk),它表示從第k段狀態sk採用最優策略p*k,n到過程終止時的最佳效益值[1]。
最優指標函數其實就是我們真正關心的問題的解。在上面的例子中,f2(B1)就表示從B1點到終點D1點的最短路徑長度。我們求解的最終目標就是f1(A1)。
最優指標函數的求法一般是一個從目標狀態出發的遞推公式,稱為規劃方程:
其中sk是第k段的某個狀態,uk是從sk出發的允許決策集合Dk(sk)中的一個決策,Tk(sk,uk)是由sk和uk所導出的第k+1段的某個狀態sk+1,g(x,uk)是定義在數值x和決策uk上的一個函數,而函數opt表示最優化,根據具體問題分別表為max或min。
,稱為邊界條件。
上例中的規劃方程就是:
邊界條件為
這里是一種從目標狀態往回推的逆序求法,適用於目標狀態確定的問題。在我們的信息學問題中,也有很多有著確定的初始狀態。當然,對於初始狀態確定的問題,我們也可以採用從初始狀態出發往前推的順序求法。事實上,這種方法對我們來說要更為直觀、更易設計一些,從而更多地出現在我們的解題過程中。
我們本節所討論的這些理論雖然不是本文的主旨,但是卻對下面要說的動態規劃的特點起著基礎性的作用。
§2動態規劃的設計與實現
上面我們討論了動態規劃的一些理論,本節我們將通過幾個例子中,動態規劃的設計與實現,來了解動態規劃的一些特點。
§2.1動態規劃的多樣性
[例2] 花店櫥窗布置問題(IOI99)試題見附錄
本題雖然是本屆IOI中較為簡單的一題,但其中大有文章可作。說它簡單,是因為它有序,因此我們一眼便可看出這題應該用動態規劃來解決。但是,如何動態規劃呢?如何劃分階段,又如何選擇狀態呢?
<方法1>以花束的數目來劃分階段。在這里,階段變數k表示的就是要布置的花束數目(前k束花),狀態變數sk表示第k束花所在的花瓶。而對於每一個狀態sk,決策就是第k-1束花應該放在哪個花瓶,用uk表示。最優指標函數fk(sk)表示前k束花,其中第k束插在第sk個花瓶中,所能取得的最大美學值。
狀態轉移方程為
規劃方程為
(其中A(i,j)是花束i插在花瓶j中的美學值)
邊界條件 (V是花瓶總數,事實上這是一個虛擬的邊界)
<方法2>以花瓶的數目來劃分階段。在這里階段變數k表示的是要佔用的花瓶數目(前k個花瓶),狀態變數sk表示前k個花瓶中放了多少花。而對於任意一個狀態sk,決策就是第sk束花是否放在第k個花瓶中,用變數uk=1或0來表示。最優指標函數fk(sk)表示前k個花瓶中插了sk束花,所能取得的最大美學值。
狀態轉移方程為
規劃方程為
邊界條件為
兩種劃分階段的方法,引出了兩種狀態表示法,兩種規劃方式,但是卻都成功地解決了問題。只不過因為決策的選擇有多有少,所以演算法的時間復雜度也就不同。[2]
這個例子具有很大的普遍性。有很多的多階段決策問題都有著不止一種的階段劃分方法,因而往往就有不止一種的規劃方法。有時各種方法所產生的效果是差不多的,但更多的時候,就像我們的例子一樣,兩種方法會在某個方面有些區別。
所以,在用動態規劃解題的時候,可以多想一想是否有其它的解法。對於不同的解法,要注意比較,好的演算法好在哪裡,差一點的演算法差在哪裡。從各種不同演算法的比較中,我們可以更深刻地領會動態規劃的構思技巧。
§2.2動態規劃的模式性
這個可能做過動態規劃的人都有體會,從我們上面對動態規劃的分析也可以看出來。動態規劃的設計都有著一定的模式,一般要經歷以下幾個步驟。
劃分階段:按照問題的時間或空間特徵,把問題分為若干個階段。注意這若干個階段一定要是有序的或者是可排序的,否則問題就無法求解。
選擇狀態:將問題發展到各個階段時所處於的各種客觀情況用不同的狀態表示出來。當然,狀態的選擇要滿足無後效性。
確定決策並寫出狀態轉移方程:之所以把這兩步放在一起,是因為決策和狀態轉移有著天然的聯系,狀態轉移就是根據上一階段的狀態和決策來導出本階段的狀態。所以,如果我們確定了決策,狀態轉移方程也就寫出來了。但事實上,我們常常是反過來做,根據相鄰兩段的各狀態之間的關系來確定決策。
寫出規劃方程(包括邊界條件):在第一部分中,我們已經給出了規劃方程的通用形式化表達式。一般說來,只要階段、狀態、決策和狀態轉移確定了,這一步還是比較簡單的。
動態規劃的主要難點在於理論上的設計,一旦設計完成,實現部分就會非常簡單。大體上的框架如下:
對f1(s1)初始化(邊界條件)
for k2 to n(這里以順序求解為例)
對每一個skSk
fk(sk)一個極值(∞或-∞)
對每一個uk(sk)Dk(sk)
sk-1Tk(sk,uk)
tg(fk-1(sk-1),uk)
y t比fk(sk)更優 n
fk(sk)t
輸出fn(sn)
這個N-S圖雖然不能代表全部,但足可以概括大多數。少數的一些特殊的動態規劃,其實現的原理也是類似,可以類比出來。我們到現在對動態規劃的分析,主要是在理論上、設計上,原因也就在此。
掌握了動態規劃的模式性,我們在用動態規劃解題時就可以把主要的精力放在理論上的設計。一旦設計成熟,問題也就基本上解決了。而且在設計演算法時也可以按部就班地來。
但是「物極必反」,太過拘泥於模式就會限制我們的思維,扼殺優良演算法思想的產生。我們在解題時,不妨發揮一下創造性,去突破動態規劃的實現模式,這樣往往會收到意想不到的效果。[3]
§2.3動態規劃的技巧性
上面我們所說的動態規劃的模式性,主要指的是實現方面。而在設計方面,雖然它較為嚴格的步驟性,但是它的設計思想卻是沒有一定的規律可循的。這就需要我們不斷地在實踐當中去掌握動態規劃的技巧,下面僅就一個例子談一點我自己的體會。
[例3] 街道問題:在下圖中找出從左下角到右上角的最短路徑,每步只能向右方或上方走。
這是一道簡單而又典型的動態規劃題,許多介紹動態規劃的書與文章中都拿它來做例子。通常,書上的解答是這樣的:
按照圖中的虛線來劃分階段,即階段變數k表示走過的步數,而狀態變數sk表示當前處於這一階段上的哪一點(各點所對應的階段和狀態已經用ks在地圖上標明)。這時的模型實際上已經轉化成了一個特殊的多段圖。用決策變數uk=0表示向右走,uk=1表示向上走,則狀態轉移方程如下:
(這里的row是地圖豎直方向的行數)
我們看到,這個狀態轉移方程需要根據k的取值分兩種情況討論,顯得非常麻煩。相應的,把它代入規劃方程而付諸實現時,演算法也很繁。因而我們在實現時,一般是不會這么做的,而代之以下面方法:
將地圖中的點規則地編號如上,得到的規劃方程如下:
(這里Distance表示相鄰兩點間的邊長)
這樣做確實要比上面的方法簡單多了,但是它已經破壞了動態規劃的本來面目,而不存在明確的階段特徵了。如果說這種方法是以地圖中的行(A、B、C、D)來劃分階段的話,那麼它的「狀態轉移」就不全是在兩個階段之間進行的了。
也許這沒什麼大不了的,因為實踐比理論更有說服力。但是,如果我們把題目擴展一下:在地圖中找出從左下角到右上角的兩條路徑,兩條路徑中的任何一條邊都不能重疊,並且要求兩條路徑的總長度最短。這時,再用這種「簡單」的方法就不太好辦了。
如果非得套用這種方法的話,則最優指標函數就需要有四維的下標,並且難以處理兩條路徑「不能重疊」的問題。
而我們回到原先「標准」的動態規劃法,就會發現這個問題很好解決,只需要加一維狀態變數就成了。即用sk=(ak,bk)分別表示兩條路徑走到階段k時所處的位置,相應的,決策變數也增加一維,用uk=(xk,yk)分別表示兩條路徑的行走方向。狀態轉移時將兩條路徑分別考慮:
在寫規劃方程時,只要對兩條路徑走到同一個點的情況稍微處理一下,減少可選的決策個數:
從這個例子中可以總結出設計動態規劃演算法的一個技巧:狀態轉移一般是在相鄰的兩個階段之間(有時也可以在不相鄰的兩個階段間),但是盡量不要在同一個階段內進行。
動態規劃是一種很靈活的解題方法,在動態規劃演算法的設計中,類似的技巧還有很多。要掌握動態規劃的技巧,有兩條途徑:一是要深刻理解動態規劃的本質,這也是我們為什麼一開始就探討它的本質的原因;二是要多實踐,不但要多解題,還要學會從解題中探尋規律,總結技巧。
§3動態規劃與一些演算法的比較
動態規劃作為諸多解題方法中的一種,必然和其他一些演算法有著諸多聯系。從這些聯系中,我們也可以看出動態規劃的一些特點。
§3.1動態規劃與遞推
——動態規劃是最優化演算法
由於動態規劃的「名氣」如此之大,以至於很多人甚至一些資料書上都往往把一種與動態規劃十分相似的演算法,當作是動態規劃。這種演算法就是遞推。實際上,這兩種演算法還是很容易區分的。
按解題的目標來分,信息學試題主要分四類:判定性問題、構造性問題、計數問題和最優化問題。我們在競賽中碰到的大多是最優化問題,而動態規劃正是解決最優化問題的有力武器,因此動態規劃在競賽中的地位日益提高。而遞推法在處理判定性問題和計數問題方面也是一把利器。下面分別就兩個例子,談一下遞推法和動態規劃在這兩個方面的聯系。
[例4] mod 4 最優路徑問題:在下圖中找出從第1點到第4點的一條路徑,要求路徑長度mod 4的余數最小。
這個圖是一個多段圖,而且是一個特殊的多段圖。雖然這個圖的形式比一般的多段圖要簡單,但是這個最優路徑問題卻不能用動態規劃來做。因為一條從第1點到第4點的最優路徑,在它走到第2點、第3點時,路徑長度mod 4的余數不一定是最小,也就是說最優策略的子策略不一定最優——這個問題不滿足最優化原理。
但是我們可以把它轉換成判定性問題,用遞推法來解決。判斷從第1點到第k點的長度mod 4為sk的路徑是否存在,用fk(sk)來表示,則遞推公式如下:
(邊界條件)
(這里lenk,i表示從第k-1點到第k點之間的第i條邊的長度,方括弧表示「或(or)」運算)
最後的結果就是可以使f4(s4)值為真的最小的s4值。
這個遞推法的遞推公式和動態規劃的規劃方程非常相似,我們在這里借用了動態規劃的符號也就是為了更清楚地顯示這一點。其實它們的思想也是非常相像的,可以說是遞推法借用了動態規劃的思想解決了動態規劃不能解決的問題。
有的多階段決策問題(像這一題的階段特徵就很明顯),由於不能滿足最優化原理等使用動態規劃的先決條件,而無法應用動態規劃。在這時可以將最優指標函數的值當作「狀態」放到下標中去,從而變最優化問題為判定性問題,再借用動態規劃的思想,用遞推法來解決問題。
[例5] 釘子與小球(NOI99)試題見附錄
這個題目一看就不覺讓人想起一道經典的動態規劃題。下面先讓我們回顧一下這個問題。
數字三角形(IOI94)在下圖中求從頂至低某處的一條路徑,使該路徑所經過的數字的總和最大,每一步只能向左下或右下走。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
在這個問題中,我們按走過的行數來劃分階段,以走到每一行時所在的位置來作為狀態,決策就是向左下走(用0表示)或向右下走(用1表示)。
狀態轉移方程:
規劃方程:
邊界條件:
這是一個比較簡單的最優化問題,我們還可以把這個問題改成一個更加簡單的整數統計問題:求頂點到每一點的路徑總數。把這個總數用fk(sk)表示,那麼遞推公式就是:
在這里,雖然求和公式只有兩項,但我們仍然用∑的形式表示,就是為了突出這個遞推公式和上面的規劃方程的相似之處。這兩個公式的邊界條件都是一模一樣的。
再回到我們上面的「釘子與小球」問題,這是一個概率統計問題。我們繼續沿用上面的思想,用fk(sk)表示小球落到第k行第sk個釘子上的概率,則遞推公式如下:
(這里函數Existk(sk)表示第k行第sk個釘子是否存在,存在則取1,不存在則取0)
邊界條件
可以看出這個公式較之上面的兩個式子雖然略有變化,但是其基本思想還是類似的。在解這個問題的過程中,我們再次運用了動態規劃的思想。
一般說來,很多最優化問題都有著對應的計數問題;反過來,很多計數問題也有著對應的最優化問題。因此,我們在遇到這兩類問題時,不妨多聯系、多發展,舉一反三,從比較中更深入地理解動態規劃的思想。
其實遞推和動態規劃這兩種方法的思想本來就很相似,也不必說是誰借用了誰的思想。關鍵在於我們要掌握這種思想,這樣我們無論在用動態規劃法解最優化問題,或是在用遞推法解判定型、計數問題時,都能得心應手、游刃有餘了。
§3.2動態規劃與搜索
——動態規劃是高效率、高消費演算法
同樣是解決最優化問題,有的題目我們採用動態規劃,而有的題目我們則需要用搜索。這其中有沒有什麼規則呢?
我們知道,撇開時空效率的因素不談,在解決最優化問題的演算法中,搜索可以說是「萬能」的。所以動態規劃可以解決的問題,搜索也一定可以解決。
把一個動態規劃演算法改寫成搜索是非常方便的,狀態轉移方程、規劃方程以及邊界條件都可以直接「移植」,所不同的只是求解順序。動態規劃是自底向上的遞推求解,而搜索則是自頂向下的遞歸求解(這里指深度搜索,寬度搜索類似)。
反過來,我們也可以把搜索演算法改寫成動態規劃。狀態空間搜索實際上是對隱式圖中的點進行枚舉,這種枚舉是自頂向下的。如果把枚舉的順序反過來,變成自底向上,那麼就成了動態規劃。(當然這里有個條件,即隱式圖中的點是可排序的,詳見下一節。)
正因為動態規劃和搜索有著求解順序上的不同,這也造成了它們時間效率上的差別。在搜索中,往往會出現下面的情況:
對於上圖(a)這樣幾個狀態構成的一個隱式圖,用搜索演算法就會出現重復,如上圖(b)所示,狀態C2被搜索了兩次。在深度搜索中,這樣的重復會引起以C2為根整個的整個子搜索樹的重復搜索;在寬度搜索中,雖然這樣的重復可以立即被排除,但是其時間代價也是不小的。而動態規劃就沒有這個問題,如上圖(c)所示。
一般說來,動態規劃演算法在時間效率上的優勢是搜索無法比擬的。(當然對於某些題目,根本不會出現狀態的重復,這樣搜索和動態規劃的速度就沒有差別了。)而從理論上講,任何拓撲有序(現實中這個條件常常可以滿足)的隱式圖中的搜索演算法都可以改寫成動態規劃。但事實上,在很多情況下我們仍然不得不採用搜索演算法。那麼,動態規劃演算法在實現上還有什麼障礙嗎?
考慮上圖(a)所示的隱式圖,其中存在兩個從初始狀態無法達到的狀態。在搜索演算法中,這樣的兩個狀態就不被考慮了,如上圖(b)所示。但是動態規劃由於是自底向上求解,所以就無法估計到這一點,因而遍歷了全部的狀態,如上圖(c)所示。
一般說來,動態規劃總要遍歷所有的狀態,而搜索可以排除一些無效狀態。更重要的事搜索還可以剪枝,可能剪去大量不必要的狀態,因此在空間開銷上往往比動態規劃要低很多。
如何協調好動態規劃的高效率與高消費之間的矛盾呢?有一種折衷的辦法就是記憶化演算法。記憶化演算法在求解的時候還是按著自頂向下的順序,但是每求解一個狀態,就將它的解保存下來,以後再次遇到這個狀態的時候,就不必重新求解了。這種方法綜合了搜索和動態規劃兩方面的優點,因而還是很有實用價值的。
§3.3動態規劃與網路流
——動態規劃是易設計易實現演算法
由於圖的關系復雜而無序,一般難以呈現階段特徵(除了特殊的圖如多段圖,或特殊的分段方法如Floyd),因此動態規劃在圖論中的應用不多。但有一類圖,它的點卻是有序的,這就是有向無環圖。
在有向無環圖中,我們可以對點進行拓撲排序,使其體現出有序的特徵,從而據此劃分階段。在有向無還圖中求最短路徑的演算法[4],已經體現出了簡單的動態規劃思想。但動態規劃在圖論中還有更有價值的應用。下面先看一個例子。
[例6] N個人的街道問題:在街道問題(參見例3)中,若有N個人要從左下角走向右上角,要求他們走過的邊的總長度最大。當然,這里每個人也只能向右或向上走。下面是一個樣例,左圖是從出發地到目的地的三條路徑,右圖是他們所走過的邊,這些邊的總長度為5 + 4 + 3 + 6 + 3 + 3 + 5 + 8 + 8 + 7 + 4 + 5 + 9 + 5 + 3 = 78(不一定是最大)。
這個題目是對街道問題的又一次擴展。仿照街道問題的解題方法,我們仍然可以用動態規劃來解決本題。不過這一次是N個人同時走,狀態變數也就需要用N維來表示,。相應的,決策變數也要變成N維,uk=(uk,1,uk,2,…,uk,N)。狀態轉移方程不需要做什麼改動:
在寫規劃方程時,需要注意在第k階段,N條路徑所走過的邊的總長度的計算,在這里我就用gk(sk,uk)來表示了:
邊界條件為
可見將原來的動態規劃演算法移植到這個問題上來,在理論上還是完全可行的。但是,現在的這個動態規劃演算法的時空復雜度已經是關於N的指數函數,只要N稍微大一點,這個演算法就不可能實現了。
下面我們換一個思路,將N條路徑看成是網路中一個流量為N的流,這樣求解的目標就是使這個流的費用最大。但是本題又不同於一般的費用流問題,在每一條邊e上的流費用並不是流量和邊權的乘積 ,而是用下式計算:
為了使經典的費用流演算法適用於本題,我們需要將模型稍微轉化一下:
如圖,將每條邊拆成兩條。拆開後一條邊上有權,但是容量限制為1;另一條邊沒有容量限制,但是流過這條邊就不能計算費用了。這樣我們就把問題轉化成了一個標準的最大費用固定流問題。
這個演算法可以套用經典的最小費用最大流演算法,在此就不細說了。(參見附錄中的源程序)
這個例題是我仿照IOI97的「障礙物探測器」一題[6]編出來的。「障礙物探
⑷ 迭代法是什麼
迭代演算法是用計算機解決問題的一種基本方法。它利用計算機運算速度快、適合做重復性操作的特點,讓計算機對一組指令(或一定步驟)進行重復執行,在每次執行這組指令(或這些步驟)時,都從變數的原值推出它的一個新值。
利用迭代演算法解決問題,需要做好以下三個方面的工作:
一、確定迭代變數。在可以用迭代演算法解決的問題中,至少存在一個直接或間接地不斷由舊值遞推出新值的變數,這個變數就是迭代變數。
二、建立迭代關系式。所謂迭代關系式,指如何從變數的前一個值推出其下一個值的公式(或關系)。迭代關系式的建立是解決迭代問題的關鍵,通常可以使用遞推或倒推的方法來完成。
三、對迭代過程進行控制。在什麼時候結束迭代過程?這是編寫迭代程序必須考慮的問題。不能讓迭代過程無休止地重復執行下去。迭代過程的控制通常可分為兩種情況:一種是所需的迭代次數是個確定的值,可以計算出來;另一種是所需的迭代次數無法確定。對於前一種情況,可以構建一個固定次數的循環來實現對迭代過程的控制;對於後一種情況,需要進一步分析出用來結束迭代過程的條件。
例 1 : 一個飼養場引進一隻剛出生的新品種兔子,這種兔子從出生的下一個月開始,每月新生一隻兔子,新生的兔子也如此繁殖。如果所有的兔子都不死去,問到第 12 個月時,該飼養場共有兔子多少只?
分析: 這是一個典型的遞推問題。我們不妨假設第 1 個月時兔子的只數為 u 1 ,第 2 個月時兔子的只數為 u 2 ,第 3 個月時兔子的只數為 u 3 ,……根據題意,「這種兔子從出生的下一個月開始,每月新生一隻兔子」,則有
u 1 = 1 , u 2 = u 1 + u 1 × 1 = 2 , u 3 = u 2 + u 2 × 1 = 4 ,……
根據這個規律,可以歸納出下面的遞推公式:
u n = u n - 1 × 2 (n ≥ 2)
對應 u n 和 u n - 1 ,定義兩個迭代變數 y 和 x ,可將上面的遞推公式轉換成如下迭代關系:
y=x*2
x=y
讓計算機對這個迭代關系重復執行 11 次,就可以算出第 12 個月時的兔子數。參考程序如下:
cls
x=1
for i=2 to 12
y=x*2
x=y
next i
print y
end
例 2 : 阿米巴用簡單分裂的方式繁殖,它每分裂一次要用 3 分鍾。將若干個阿米巴放在一個盛滿營養參液的容器內, 45 分鍾後容器內充滿了阿米巴。已知容器最多可以裝阿米巴 2 20 個。試問,開始的時候往容器內放了多少個阿米巴?請編程序算出。
分析: 根據題意,阿米巴每 3 分鍾分裂一次,那麼從開始的時候將阿米巴放入容器裡面,到 45 分鍾後充滿容器,需要分裂 45/3=15 次。而「容器最多可以裝阿米巴 2 20 個」,即阿米巴分裂 15 次以後得到的個數是 2 20 。題目要求我們計算分裂之前的阿米巴數,不妨使用倒推的方法,從第 15 次分裂之後的 2 20 個,倒推出第 15 次分裂之前(即第 14 次分裂之後)的個數,再進一步倒推出第 13 次分裂之後、第 12 次分裂之後、……第 1 次分裂之前的個數。
設第 1 次分裂之前的個數為 x 0 、第 1 次分裂之後的個數為 x 1 、第 2 次分裂之後的個數為 x 2 、……第 15 次分裂之後的個數為 x 15 ,則有
x 14 =x 15 /2 、 x 13 =x 14 /2 、…… x n-1 =x n /2 (n ≥ 1)
因為第 15 次分裂之後的個數 x 15 是已知的,如果定義迭代變數為 x ,則可以將上面的倒推公式轉換成如下的迭代公式:
x=x/2 ( x 的初值為第 15 次分裂之後的個數 2 20 )
讓這個迭代公式重復執行 15 次,就可以倒推出第 1 次分裂之前的阿米巴個數。因為所需的迭代次數是個確定的值,我們可以使用一個固定次數的循環來實現對迭代過程的控制。參考程序如下:
cls
x=2^20
for i=1 to 15
x=x/2
next i
print x
end
例 3 : 驗證谷角猜想。日本數學家谷角靜夫在研究自然數時發現了一個奇怪現象:對於任意一個自然數 n ,若 n 為偶數,則將其除以 2 ;若 n 為奇數,則將其乘以 3 ,然後再加 1 。如此經過有限次運算後,總可以得到自然數 1 。人們把谷角靜夫的這一發現叫做「谷角猜想」。
要求:編寫一個程序,由鍵盤輸入一個自然數 n ,把 n 經過有限次運算後,最終變成自然數 1 的全過程列印出來。
分析: 定義迭代變數為 n ,按照谷角猜想的內容,可以得到兩種情況下的迭代關系式:當 n 為偶數時, n=n/2 ;當 n 為奇數時, n=n*3+1 。用 QBASIC 語言把它描述出來就是:
if n 為偶數 then
n=n/2
else
n=n*3+1
end if
這就是需要計算機重復執行的迭代過程。這個迭代過程需要重復執行多少次,才能使迭代變數 n 最終變成自然數 1 ,這是我們無法計算出來的。因此,還需進一步確定用來結束迭代過程的條件。仔細分析題目要求,不難看出,對任意給定的一個自然數 n ,只要經過有限次運算後,能夠得到自然數 1 ,就已經完成了驗證工作。因此,用來結束迭代過程的條件可以定義為: n=1 。參考程序如下:
cls
input "Please input n=";n
do until n=1
if n mod 2=0 then
rem 如果 n 為偶數,則調用迭代公式 n=n/2
n=n/2
print "—";n;
else
n=n*3+1
print "—";n;
end if
loop
end
迭代法
迭代法是用於求方程或方程組近似根的一種常用的演算法設計方法。設方程為f(x)=0,用某種數學方法導出等價的形式x=g(x),然後按以下步驟執行:
(1) 選一個方程的近似根,賦給變數x0;
(2) 將x0的值保存於變數x1,然後計算g(x1),並將結果存於變數x0;
(3) 當x0與x1的差的絕對值還小於指定的精度要求時,重復步驟(2)的計算。
若方程有根,並且用上述方法計算出來的近似根序列收斂,則按上述方法求得的x0就認為是方程的根。上述演算法用C程序的形式表示為:
【演算法】迭代法求方程的根
{ x0=初始近似根;
do {
x1=x0;
x0=g(x1); /*按特定的方程計算新的近似根*/
} while ( fabs(x0-x1)>Epsilon);
printf(「方程的近似根是%f\n」,x0);
}
迭代演算法也常用於求方程組的根,令
X=(x0,x1,…,xn-1)
設方程組為:
xi=gi(X) (I=0,1,…,n-1)
則求方程組根的迭代演算法可描述如下:
【演算法】迭代法求方程組的根
{ for (i=0;i
x=初始近似根;
do {
for (i=0;i
y=x;
for (i=0;i
x=gi(X);
for (delta=0.0,i=0;i
if (fabs(y-x)>delta) delta=fabs(y-x);
} while (delta>Epsilon);
for (i=0;i
printf(「變數x[%d]的近似根是 %f」,I,x);
printf(「\n」);
}
具體使用迭代法求根時應注意以下兩種可能發生的情況:
(1) 如果方程無解,演算法求出的近似根序列就不會收斂,迭代過程會變成死循環,因此在使用迭代演算法前應先考察方程是否有解,並在程序中對迭代的次數給予限制;
(2) 方程雖然有解,但迭代公式選擇不當,或迭代的初始近似根選擇不合理,也會導致迭代失敗。
遞歸
遞歸是設計和描述演算法的一種有力的工具,由於它在復雜演算法的描述中被經常採用,為此在進一步介紹其他演算法設計方法之前先討論它。
能採用遞歸描述的演算法通常有這樣的特徵:為求解規模為N的問題,設法將它分解成規模較小的問題,然後從這些小問題的解方便地構造出大問題的解,並且這些規模較小的問題也能採用同樣的分解和綜合方法,分解成規模更小的問題,並從這些更小問題的解構造出規模較大問題的解。特別地,當規模N=1時,能直接得解。
【問題】 編寫計算斐波那契(Fibonacci)數列的第n項函數fib(n)。
斐波那契數列為:0、1、1、2、3、……,即:
fib(0)=0;
fib(1)=1;
fib(n)=fib(n-1)+fib(n-2) (當n>1時)。
寫成遞歸函數有:
int fib(int n)
{ if (n==0) return 0;
if (n==1) return 1;
if (n>1) return fib(n-1)+fib(n-2);
}
遞歸演算法的執行過程分遞推和回歸兩個階段。在遞推階段,把較復雜的問題(規模為n)的求解推到比原問題簡單一些的問題(規模小於n)的求解。例如上例中,求解fib(n),把它推到求解fib(n-1)和fib(n-2)。也就是說,為計算fib(n),必須先計算fib(n-1)和fib(n- 2),而計算fib(n-1)和fib(n-2),又必須先計算fib(n-3)和fib(n-4)。依次類推,直至計算fib(1)和fib(0),分別能立即得到結果1和0。在遞推階段,必須要有終止遞歸的情況。例如在函數fib中,當n為1和0的情況。
在回歸階段,當獲得最簡單情況的解後,逐級返回,依次得到稍復雜問題的解,例如得到fib(1)和fib(0)後,返回得到fib(2)的結果,……,在得到了fib(n-1)和fib(n-2)的結果後,返回得到fib(n)的結果。
在編寫遞歸函數時要注意,函數中的局部變數和參數知識局限於當前調用層,當遞推進入「簡單問題」層時,原來層次上的參數和局部變數便被隱蔽起來。在一系列「簡單問題」層,它們各有自己的參數和局部變數。
由於遞歸引起一系列的函數調用,並且可能會有一系列的重復計算,遞歸演算法的執行效率相對較低。當某個遞歸演算法能較方便地轉換成遞推演算法時,通常按遞推演算法編寫程序。例如上例計算斐波那契數列的第n項的函數fib(n)應採用遞推演算法,即從斐波那契數列的前兩項出發,逐次由前兩項計算出下一項,直至計算出要求的第n項。
【問題】 組合問題
問題描述:找出從自然數1、2、……、n中任取r個數的所有組合。例如n=5,r=3的所有組合為: (1)5、4、3 (2)5、4、2 (3)5、4、1
(4)5、3、2 (5)5、3、1 (6)5、2、1
(7)4、3、2 (8)4、3、1 (9)4、2、1
(10)3、2、1
分析所列的10個組合,可以採用這樣的遞歸思想來考慮求組合函數的演算法。設函數為void comb(int m,int k)為找出從自然數1、2、……、m中任取k個數的所有組合。當組合的第一個數字選定時,其後的數字是從餘下的m-1個數中取k-1數的組合。這就將求m 個數中取k個數的組合問題轉化成求m-1個數中取k-1個數的組合問題。設函數引入工作數組a[ ]存放求出的組合的數字,約定函數將確定的k個數字組合的第一個數字放在a[k]中,當一個組合求出後,才將a[ ]中的一個組合輸出。第一個數可以是m、m-1、……、k,函數將確定組合的第一個數字放入數組後,有兩種可能的選擇,因還未去頂組合的其餘元素,繼續遞歸去確定;或因已確定了組合的全部元素,輸出這個組合。細節見以下程序中的函數comb。
【程序】
# include
# define MAXN 100
int a[MAXN];
void comb(int m,int k)
{ int i,j;
for (i=m;i>=k;i--)
{ a[k]=i;
if (k>1)
comb(i-1,k-1);
else
{ for (j=a[0];j>0;j--)
printf(「%4d」,a[j]);
printf(「\n」);
}
}
}
void main()
{ a[0]=3;
comb(5,3);
}
【問題】 背包問題
問題描述:有不同價值、不同重量的物品n件,求從這n件物品中選取一部分物品的選擇方案,使選中物品的總重量不超過指定的限制重量,但選中物品的價值之和最大。
設n 件物品的重量分別為w0、w1、…、wn-1,物品的價值分別為v0、v1、…、vn-1。採用遞歸尋找物品的選擇方案。設前面已有了多種選擇的方案,並保留了其中總價值最大的方案於數組option[ ],該方案的總價值存於變數maxv。當前正在考察新方案,其物品選擇情況保存於數組cop[ ]。假定當前方案已考慮了前i-1件物品,現在要考慮第i件物品;當前方案已包含的物品的重量之和為tw;至此,若其餘物品都選擇是可能的話,本方案能達到的總價值的期望值為tv。演算法引入tv是當一旦當前方案的總價值的期望值也小於前面方案的總價值maxv時,繼續考察當前方案變成無意義的工作,應終止當前方案,立即去考察下一個方案。因為當方案的總價值不比maxv大時,該方案不會被再考察,這同時保證函數後找到的方案一定會比前面的方案更好。
對於第i件物品的選擇考慮有兩種可能:
(1) 考慮物品i被選擇,這種可能性僅當包含它不會超過方案總重量限制時才是可行的。選中後,繼續遞歸去考慮其餘物品的選擇。
(2) 考慮物品i不被選擇,這種可能性僅當不包含物品i也有可能會找到價值更大的方案的情況。
按以上思想寫出遞歸演算法如下:
try(物品i,當前選擇已達到的重量和,本方案可能達到的總價值tv)
{ /*考慮物品i包含在當前方案中的可能性*/
if(包含物品i是可以接受的)
{ 將物品i包含在當前方案中;
if (i
try(i+1,tw+物品i的重量,tv);
else
/*又一個完整方案,因為它比前面的方案好,以它作為最佳方案*/
以當前方案作為臨時最佳方案保存;
恢復物品i不包含狀態;
}
/*考慮物品i不包含在當前方案中的可能性*/
if (不包含物品i僅是可男考慮的)
if (i
try(i+1,tw,tv-物品i的價值);
else
/*又一個完整方案,因它比前面的方案好,以它作為最佳方案*/
以當前方案作為臨時最佳方案保存;
}
為了理解上述演算法,特舉以下實例。設有4件物品,它們的重量和價值見表:
物品 0 1 2 3
重量 5 3 2 1
價值 4 4 3 1
並設限制重量為7。則按以上演算法,下圖表示找解過程。由圖知,一旦找到一個解,演算法就進一步找更好的佳。如能判定某個查找分支不會找到更好的解,演算法不會在該分支繼續查找,而是立即終止該分支,並去考察下一個分支。
按上述演算法編寫函數和程序如下:
【程序】
# include
# define N 100
double limitW,totV,maxV;
int option[N],cop[N];
struct { double weight;
double value;
}a[N];
int n;
void find(int i,double tw,double tv)
{ int k;
/*考慮物品i包含在當前方案中的可能性*/
if (tw+a.weight<=limitW)
{ cop=1;
if (i
else
{ for (k=0;k
option[k]=cop[k];
maxv=tv;
}
cop=0;
}
/*考慮物品i不包含在當前方案中的可能性*/
if (tv-a.value>maxV)
if (i
else
{ for (k=0;k
option[k]=cop[k];
maxv=tv-a.value;
}
}
void main()
{ int k;
double w,v;
printf(「輸入物品種數\n」);
scanf((「%d」,&n);
printf(「輸入各物品的重量和價值\n」);
for (totv=0.0,k=0;k
{ scanf(「%1f%1f」,&w,&v);
a[k].weight=w;
a[k].value=v;
totV+=V;
}
printf(「輸入限制重量\n」);
scanf(「%1f」,&limitV);
maxv=0.0;
for (k=0;k find(0,0.0,totV);
for (k=0;k
if (option[k]) printf(「%4d」,k+1);
printf(「\n總價值為%.2f\n」,maxv);
}
作為對比,下面以同樣的解題思想,考慮非遞歸的程序解。為了提高找解速度,程序不是簡單地逐一生成所有候選解,而是從每個物品對候選解的影響來形成值得進一步考慮的候選解,一個候選解是通過依次考察每個物品形成的。對物品i的考察有這樣幾種情況:當該物品被包含在候選解中依舊滿足解的總重量的限制,該物品被包含在候選解中是應該繼續考慮的;反之,該物品不應該包括在當前正在形成的候選解中。同樣地,僅當物品不被包括在候選解中,還是有可能找到比目前臨時最佳解更好的候選解時,才去考慮該物品不被包括在候選解中;反之,該物品不包括在當前候選解中的方案也不應繼續考慮。對於任一值得繼續考慮的方案,程序就去進一步考慮下一個物品。
【程序】
# include
# define N 100
double limitW;
int cop[N];
struct ele { double weight;
double value;
} a[N];
int k,n;
struct { int ;
double tw;
double tv;
}twv[N];
void next(int i,double tw,double tv)
{ twv.=1;
twv.tw=tw;
twv.tv=tv;
}
double find(struct ele *a,int n)
{ int i,k,f;
double maxv,tw,tv,totv;
maxv=0;
for (totv=0.0,k=0;k
totv+=a[k].value;
next(0,0.0,totv);
i=0;
While (i>=0)
{ f=twv.;
tw=twv.tw;
tv=twv.tv;
switch(f)
{ case 1: twv.++;
if (tw+a.weight<=limitW)
if (i
{ next(i+1,tw+a.weight,tv);
i++;
}
else
{ maxv=tv;
for (k=0;k
cop[k]=twv[k].!=0;
}
break;
case 0: i--;
break;
default: twv.=0;
if (tv-a.value>maxv)
if (i
{ next(i+1,tw,tv-a.value);
i++;
}
else
{ maxv=tv-a.value;
for (k=0;k
cop[k]=twv[k].!=0;
}
break;
}
}
return maxv;
}
void main()
{ double maxv;
printf(「輸入物品種數\n」);
scanf((「%d」,&n);
printf(「輸入限制重量\n」);
scanf(「%1f」,&limitW);
printf(「輸入各物品的重量和價值\n」);
for (k=0;k
scanf(「%1f%1f」,&a[k].weight,&a[k].value);
maxv=find(a,n);
printf(「\n選中的物品為\n」);
for (k=0;k
if (option[k]) printf(「%4d」,k+1);
printf(「\n總價值為%.2f\n」,maxv);
}
遞歸的基本概念和特點
程序調用自身的編程技巧稱為遞歸( recursion)。
一個過程或函數在其定義或說明中又直接或間接調用自身的一種方法,它通常把一個大型復雜的問題層層轉化為一個與原問題相似的規模較小的問題來求解,遞歸策略只需少量的程序就可描述出解題過程所需要的多次重復計算,大大地減少了程序的代碼量。遞歸的能力在於用有限的語句來定義對象的無限集合。用遞歸思想寫出的程序往往十分簡潔易懂。
一般來說,遞歸需要有邊界條件、遞歸前進段和遞歸返回段。當邊界條件不滿足時,遞歸前進;當邊界條件滿足時,遞歸返回。
注意:
(1) 遞歸就是在過程或函數里調用自身;
(2) 在使用遞增歸策略時,必須有一個明確的遞歸結束條件,稱為遞歸出口。
⑸ 什麼叫結構化演算法,為什麼要提倡結構化演算法
結構化演算法是由一些基本結構順序組成的.在基本結構之間不存在向前或向後的跳轉,流程的轉移只存在於一個基本的結構范圍內.一個非結構化的演算法可以用一個等價的結構化演算法代替,其功能不變.
跟結構化演算法比較起來,非結構化演算法有以下缺點.
流程不受限制的隨意轉來轉去,使流程圖豪無規律.使人在閱讀的時候難以理解演算法的邏輯.難以閱讀,也難以修改.從而使演算法的可靠性和可維護性難以保證.