經典演算法之動態規劃

2021-10-02 02:23:16 字數 4266 閱讀 7337

一、引例

先來看看生活中經常遇到的事吧——假設您是個土豪,身上帶了足夠的1、5、10、20、50、100元面值的鈔票。現在您的目標是湊出某個金額w,需要用到盡量少的鈔票。

依據生活經驗,我們顯然可以採取這樣的策略:能用100的就盡量用100的,否則盡量用50的……依次類推。在這種策略下,666=6×100+1×50+1×10+1×5+1×1,共使用了10張鈔票。

這種策略稱為「貪心」:假設我們面對的局面是「需要湊出w」,貪心策略會盡快讓w變得更小。能讓w少100就盡量讓它少100,這樣我們接下來面對的局面就是湊出w-100。長期的生活經驗表明,貪心策略是正確的。

但是,如果我們換一組鈔票的面值,貪心策略就也許不成立了。如果乙個奇葩國家的鈔票面額分別是1、5、11,那麼我們在湊出15的時候,貪心策略會出錯:

15=1×11+4×1 (貪心策略使用了5張鈔票)

15=3×5 (正確的策略,只用3張鈔票)

為什麼會這樣呢?貪心策略錯在了**?

在這裡我們發現,貪心是一種只考慮眼前情況的策略。

貪心策略的綱領是:「盡量使接下來面對的w更小」。這樣,貪心策略在w=15的局面時,會優先使用11來把w降到4;但是在這個問題中,湊出4的代價是很高的,必須使用4×1。如果使用了5,w會降為10,雖然沒有4那麼小,但是湊出10只需要兩張5元。

那麼,現在我們怎樣才能避免「鼠目寸光」呢?

重新分析剛剛的例子。w=15時,我們如果取11,接下來就面對w=4的情況;如果取5,則接下來面對w=10的情況。我們發現這些問題都有相同的形式:「給定w,湊出w所用的最少鈔票是多少張?」接下來,我們用f(n)來表示「湊出n所需的最少鈔票數量」。

那麼,如果我們取了11,最後的代價(用掉的鈔票總數)是多少呢?

明顯cost = f(4)+1 = 4+1 = 5,它的意義是:利用11來湊出15,付出的代價等於f(4)加上自己這一張鈔票。現在我們暫時不管f(4)怎麼求出來。

依次類推,馬上可以知道:如果我們用5來湊出15,cost就是cost = f(10)+1 = 2+1 = 3

那麼,現在w=15的時候,我們該取那種鈔票呢?當然是各種方案中,cost值最低的那乙個

- 取11:cost = f(4)+1 = 4+1 = 5

- 取5:cost = f(10)+1 = 2+1 = 3

- 取1:cost = f(14)+1 = 4+1 = 5

顯而易見,cost值最低的是取5的方案。我們通過上面三個式子,做出了正確的決策

這給了我們乙個至關重要的啟示——f(n)只與f(n-1) 、f(n-5) 、f(n-11)相關;更確切地說:

f(n) = min + 1

這樣,我們便在o(n)的複雜度下解決了這個問題。

現在我們再來看一看它的原理。

-f(n)只與f(n-1) 、f(n-5) 、f(n-11)相關。

- 我們只關心f(w),不關心是怎麼湊出w的。

這兩個事實,保證了我們做法的正確性。它比起貪心策略,會分別算出取1、5、11的代價,從而做出乙個正確決策,這樣就避免掉了「鼠目寸光」!

它與暴力的區別在**?我們的暴力列舉了「使用的硬幣」,然而這屬於冗餘資訊。我們要的是答案,根本不關心這個答案是怎麼湊出來的。譬如,要求出f(15),只需要知道f(14),f(10),f(4)的值。其他資訊並不需要。我們捨棄了冗餘資訊。我們只記錄了對解決問題有幫助的資訊——f(n).

我們能這樣幹,取決於問題的性質:求出f(n),只需要知道幾個更小的f(c)。我們將求解f(c)稱作求解f(n)的「子問題」。

這就是dp(動態規劃,dynamic programming).

將乙個問題拆成幾個子問題,分別求解這些子問題,即可推斷出大問題的解

二、分析

動態規劃與分治法相似,都是通過組合原問題的解來求解原問題。不同的是,分治法將問題劃分成互不相交的子問題,遞迴地求解子問題,再將它們的解組合起來,求出原問題的解。而動態規劃應用於子問題重疊的情況,即不同的子問題具有公共的子子問題(子問題的求解都是遞迴進行的,將其劃分成更小的子子問題)。在這種情況下,分治法會做許多不必要的工作,它會反覆求解那些公共子子問題。而動態規劃對每個子子問題只求解一次,將其儲存在乙個**中,從而無需每次求解乙個子子問題時都重新計算,避免了許多不必要的計算工作。

動態規劃問題通常用來求解最優化問題。這類問題可以有許多可行解,我們希望尋找其中的最優解(如最大或最小)。

因此,分析乙個問題是否為動態規劃問題,主要是判斷它是否具有動態規劃的兩個必要條件

(1)重疊子問題

(2)最優子結構

設計動態規劃演算法的4個步驟:

1)劃分階段:按照問題的時間或空間特徵,把問題分為若干個階段。

2)確定狀態和狀態變數:將問題發展到各個階段時所處於的各種客觀情況用不同的狀態表示出來。

3)確定決策並寫出狀態轉移方程:狀態轉移就是根據上一階段的狀態和決策來匯出本階段的狀態。如果確定了決策,狀態轉移方程也就可寫出。但事實上常常是反過來做,根據相鄰兩個階段的狀態之間的關係來確定決策方法和狀態轉移方程。

(4)尋找邊界條件:給出的狀態轉移方程是乙個遞推式,需要乙個遞推的終止條件或邊界條件。

其中,能否寫出正確的狀態轉移方程,是整個動態規劃演算法能否正確的關鍵

實際應用中可以按以下4個簡化的步驟進行設計:

1)刻畫乙個最優解的結構特徵

2)遞迴地定義最優解的值

3)計算最優解的值,通常採用自底向上的方法

4)利用計算出的資訊構造乙個最優解

三、經典問題

斐波那契數列

現在要求輸入乙個整數n,請你輸出斐波那契數列的第n項。n<=39

斐波那契數列指的是這樣乙個數列:1、1、2、3、5、8、13、21、34、……

在數學上,斐波納契數列以如下被以遞迴的方法定義:f(0)=0,f(1)=1, f(n)=f(n-1)+f(n-2)(n>=2,n∈n*)

解決這一經典問題,有三種常用的解法:

(1)迭代法

public int fibonacci(int n) 

if (n == 1)

int num1 = 0, num2 = 1;

int result = 0;

for (int i = 2; i <= n; i++)

return result;

}

(2)遞迴法

public int fibonacci(int n) 

if (n == 1)

return fibonacci(n - 1) + fibonacci(n - 2);

}

(3)動態規劃法

//遞迴寫法

public int fibonacci(int n)

if (n == 1)

f[n] = fibonacci(n - 1) + fibonacci(n - 2);

return f[n];

}//非遞迴寫法

public int fibonacci(int n)

if (n == 1)

while(k <= n)

return f[n];

當然,還有一種比較小眾的解法,一般不用,就是可以根據遞推式,來解出通項公式。然後根據通項公式直接求出結果。

f(n) = f(n-1) + f(n-2), 易得通項公式為:

public int fibonacci(int n)

演算法之動態規劃

一 思想 首先要了解 動態規劃 必須先知道什麼叫做 多階段決策 百科裡面對這個問題解釋的很全,我就load一段出來,大家得要好好品味,好好分析。上面圖中最後一句話就定義了動態規劃是要幹什麼的問題。二 使用規則 現在我們知道動態規劃要解決啥問題了,那麼什麼情況下我們該使用動態規劃呢?最優化原理 最優子...

演算法之動態規劃

鋼條切割問題 鋼條切割問題出現在 演算法導論 一書第204頁,作為動態規劃的例題出現,該題內容如下 serling公司購買長鋼條,將其切割為短鋼條 切割工序本身沒有成本支出。公司管理層希望知道最佳的切割方案。假定我們知道serling公司 一段長為i英吋的鋼條的 為pi i 1,2,單位為美元 鋼條...

五大演算法之動態規劃 (經典問題)

動態規劃 遞迴寫法 以斐波那契舉例 若直接暴力求解,時間複雜度為o 2 n n為輸入的斐波那契數列第n項 intf int n 遞迴寫法也稱為記憶化搜尋 設定dp陣列儲存 const int maxn 1000 int dp maxn intf int n 遞推寫法 int n 輸入資料 scanf...