動態規劃算法

 

動態規劃 算法是經過拆分問題,定義問題狀態和狀態之間的關係,使得問題可以以遞推(或者說分治)的方式去解決。 [1]  

動態規劃算法的基本思想與分治法相似,也是將待求解的問題分解爲若干個子問題(階段),按順序求解子階段,前一子問題的解,爲後一子問題的求解提供了有用的信息。在求解任一子問題時,列出各類可能的局部解,經過決策保留那些有可能達到最優的局部解,丟棄其餘局部解。依次解決各子問題,最後一個子問題就是初始問題的解。面試

 

基本思想與策略

編輯算法

動態規劃算法的基本思想與分治法相似,也是將待求解的問題分解爲若干個子問題(階段),按順序求解子階段,前一子問題的解,爲後一子問題的求解提供了有用的信息。在求解任一子問題時,列出各類可能的局部解,經過決策保留那些有可能達到最優的局部解,丟棄其餘局部解。依次解決各子問題,最後一個子問題就是初始問題的解。
因爲動態規劃解決的問題多數有重疊子問題這個特色,爲減小重複計算,對每個子問題只解一次,將其不一樣階段的不一樣狀態保存在一個二維數組中。

適用狀況

編輯
能採用動態規劃求解的問題的通常要具備3個性質:
(1)最優化原理:若是問題的最優解所包含的子問題的解也是最優的,就稱該問題具備最優子結構,即知足最優化原理。
(2)無後效性:即某階段狀態一旦肯定,就不受這個狀態之後決策的影響。也就是說,某狀態之後的過程不會影響之前的狀態,只與當前狀態有關。
(3)有重疊子問題:即子問題之間是不獨立的,一個子問題在下一階段決策中可能被屢次使用到。(該性質並非動態規劃適用的必要條件,可是若是沒有這條性質,動態規劃算法同其餘算法相比就不具有優點)

求解的基本步驟

編輯
動態規劃所處理的問題是一個多階段決策問題,通常由初始狀態開始,經過對中間階段決策的選擇,達到結束狀態。這些決策造成了一個決策序列,同時肯定了完成整個過程的一條活動路線(一般是求最優的活動路線)。如圖所示。動態規劃的設計都有着必定的模式,通常要經歷如下幾個步驟,以下圖所示:
初始狀態→│決策1│→│決策2│→…→│決策n│→結束狀態
(1)劃分階段:按照問題的時間或空間特徵,把問題分爲若干個階段。在劃分階段時,注意劃分後的階段必定要是有序的或者是可排序的,不然問題就沒法求解。
(2)肯定狀態和狀態變量:將問題發展到各個階段時所處於的各類客觀狀況用不一樣的狀態表示出來。固然,狀態的選擇要知足無後效性。
(3)肯定決策並寫出狀態轉移方程:由於決策和狀態轉移有着自然的聯繫,狀態轉移就是根據上一階段的狀態和決策來導出本階段的狀態。因此若是肯定了決策,狀態轉移方程也就可寫出。但事實上經常是反過來作,根據相鄰兩個階段的狀態之間的關係來肯定決策方法和狀態轉移方程。
(4)尋找邊界條件:給出的狀態轉移方程是一個遞推式,須要一個遞推的終止條件或邊界條件。
通常,只要解決問題的階段、狀態和狀態轉移決策肯定了,就能夠寫出狀態轉移方程(包括邊界條件)。實際應用中能夠按如下幾個簡化的步驟進行設計:
(1)分析最優解的性質,並刻畫其結構特徵。
(2)遞歸的定義最優解。
(3)以自底向上或自頂向下的記憶化方式(備忘錄法)計算出最優值
(4)根據計算最優值時獲得的信息,構造問題的最優解

算法實現

編輯
動態規劃的主要難點在於理論上的設計,也就是上面4個步驟的肯定,一旦設計完成,實現部分就會很是簡單。使用動態規劃求解問題,最重要的就是肯定動態規劃三要素:
(1)問題的階段
(2)每一個階段的狀態
(3)從前一個階段轉化到後一個階段之間的遞推關係。
遞推關係必須是從次小的問題開始到較大的問題之間的轉化,從這個角度來講,動態規劃每每能夠用遞歸程序來實現,不過由於遞推能夠充分利用前面保存的子問題的解來減小重複計算,因此對於大規模問題來講,有遞歸不可比擬的優點,這也是動態規劃算法的核心之處。
肯定了動態規劃的這三要素,整個求解過程就能夠用一個最優決策表來描述,最優決策表是一個二維表,其中行表示決策的階段,列表示問題狀態,表格須要填寫的數據通常對應此問題的在某個階段某個狀態下的最優值(如最短路徑,最長公共子序列,最大價值等),填表的過程就是根據遞推關係,從1行1列開始,以行或者列優先的順序,依次填寫表格,最後根據整個表格的數據經過簡單的取捨或者運算求得問題的最優解。
f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}
 
 
for(j=1; j<=m; j=j+1) // 第一個階段
   xn[j] = 初始值;
 
 for(i=n-1; i>=1; i=i-1)// 其餘n-1個階段
   for(j=1; j>=f(i); j=j+1)//f(i)與i有關的表達式
     xi[j]=j=max(或min){g(xi-1[j1:j2]), ......, g(xi-1[jk:jk+1])};
 
t = g(x1[j1:j2]); // 由子問題的最優解求解整個問題的最優解的方案
 
print(x1[j1]);
 
for(i=2; i<=n-1; i=i+1)
{  
     t = t-xi-1[ji];
 
     for(j=1; j>=f(i); j=j+1)
        if(t=xi[ji])
             break;
}

參考 :https://baike.baidu.com/item/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E7%AE%97%E6%B3%95/15742703?fr=aladdin數組

 

前言

最近在牛客網上作了幾套公司的真題,發現有關動態規劃(Dynamic Programming)算法的題目不少。相對於我來講,算法裏面遇到的問題裏面感受最難的也就是動態規劃(Dynamic Programming)算法了,因而花了好長時間,查找了相關的文獻和資料準備完全的理解動態規劃(Dynamic Programming)算法。一是幫助本身總結知識點,二是也可以幫助他人更好的理解這個算法。後面的參考文獻只是我看到的文獻的一部分。
動態規劃算法的核心

理解一個算法就要理解一個算法的核心,動態規劃算法的核心是下面的一張圖片和一個小故事。app

 



這裏寫圖片描述

A * "1+1+1+1+1+1+1+1 =?" *

A : "上面等式的值是多少"
B : *計算* "8!"

A *在上面等式的左邊寫上 "1+" *
A : "此時等式的值爲多少"
B : *quickly* "9!"
A : "你怎麼這麼快就知道答案了"
A : "只要在8的基礎上加1就好了"
A : "因此你不用從新計算由於你記住了第一個等式的值爲8!動態規劃算法也能夠說是 '記住求過的解來節省時間'"

   

由上面的圖片和小故事能夠知道動態規劃算法的核心就是記住已經解決過的子問題的解。
動態規劃算法的兩種形式

上面已經知道動態規劃算法的核心是記住已經求過的解,記住求解的方式有兩種:①自頂向下的備忘錄法 ②自底向上。
爲了說明動態規劃的這兩種方法,舉一個最簡單的例子:求斐波拉契數列Fibonacci 。先看一下這個問題:

Fibonacci (n) = 1;   n = 0

Fibonacci (n) = 1;   n = 1

Fibonacci (n) = Fibonacci(n-1) + Fibonacci(n-2)



之前學c語言的時候寫過這個算法使用遞歸十分的簡單。先使用遞歸版原本實現這個算法:

public int fib(int n)
{
    if(n<=0)
        return 0;
    if(n==1)
        return 1;
    return fib( n-1)+fib(n-2);
}
//輸入6
//輸出:8



先來分析一下遞歸算法的執行流程,假如輸入6,那麼執行的遞歸樹以下:
dom

 


這裏寫圖片描述
上面的遞歸樹中的每個子節點都會執行一次,不少重複的節點被執行,fib(2)被重複執行了5次。因爲調用每個函數的時候都要保留上下文,因此空間上開銷也不小。這麼多的子節點被重複執行,若是在執行的時候把執行過的子節點保存起來,後面要用到的時候直接查表調用的話能夠節約大量的時間。下面就看看動態規劃的兩種方法怎樣來解決斐波拉契數列Fibonacci 數列問題。函數


①自頂向下的備忘錄法

public static int Fibonacci(int n)
{
        if(n<=0)
            return n;
        int []Memo=new int[n+1];        
        for(int i=0;i<=n;i++)
            Memo[i]=-1;
        return fib(n, Memo);
    }
    public static int fib(int n,int []Memo)
    {

        if(Memo[n]!=-1)
            return Memo[n];
    //若是已經求出了fib(n)的值直接返回,不然將求出的值保存在Memo備忘錄中。               
        if(n<=2)
            Memo[n]=1;

        else Memo[n]=fib( n-1,Memo)+fib(n-2,Memo);  

        return Memo[n];
    }

 

備忘錄法也是比較好理解的,建立了一個n+1大小的數組來保存求出的斐波拉契數列中的每個值,在遞歸的時候若是發現前面fib(n)的值計算出來了就再也不計算,若是未計算出來,則計算出來後保存在Memo數組中,下次在調用fib(n)的時候就不會從新遞歸了。好比上面的遞歸樹中在計算fib(6)的時候先計算fib(5),調用fib(5)算出了fib(4)後,fib(6)再調用fib(4)就不會在遞歸fib(4)的子樹了,由於fib(4)的值已經保存在Memo[4]中。
②自底向上的動態規劃

備忘錄法仍是利用了遞歸,上面算法無論怎樣,計算fib(6)的時候最後仍是要計算出fib(1),fib(2),fib(3)……,那麼何不先計算出fib(1),fib(2),fib(3)……,呢?這也就是動態規劃的核心,先計算子問題,再由子問題計算父問題。

public static int fib(int n)
{
        if(n<=0)
            return n;
        int []Memo=new int[n+1];
        Memo[0]=0;
        Memo[1]=1;
        for(int i=2;i<=n;i++)
        {
            Memo[i]=Memo[i-1]+Memo[i-2];
        }       
        return Memo[n];
}



自底向上方法也是利用數組保存了先計算的值,爲後面的調用服務。觀察參與循環的只有 i,i-1 , i-2三項,所以該方法的空間能夠進一步的壓縮以下。
post

 


public static int fib(int n)
    {
        if(n<=1)
            return n;

        int Memo_i_2=0;
        int Memo_i_1=1;
        int Memo_i=1;
        for(int i=2;i<=n;i++)
        {
            Memo_i=Memo_i_2+Memo_i_1;
            Memo_i_2=Memo_i_1;
            Memo_i_1=Memo_i;
        }       
        return Memo_i;
    }


通常來講因爲備忘錄方式的動態規劃方法使用了遞歸,遞歸的時候會產生額外的開銷,使用自底向上的動態規劃方法要比備忘錄方法好。
你覺得看懂了上面的例子就懂得了動態規劃嗎?那就too young too simple了。動態規劃遠遠不止如此簡單,下面先給出一個例子看看可否獨立完成。而後再對動態規劃的其餘特性進行分析。
動態規劃小試牛刀

例題:鋼條切割

這裏寫圖片描述

這裏寫圖片描述
這裏寫圖片描述
這裏寫圖片描述
上面的例題來自於算法導論
關於題目的講解就直接截圖算法導論書上了這裏就不展開講。如今使用一下前面講到三種方法來來實現一下。
①遞歸版本

public static int cut(int []p,int n)
    {
        if(n==0)
            return 0;
        int q=Integer.MIN_VALUE;
        for(int i=1;i<=n;i++)
        {
            q=Math.max(q, p[i-1]+cut(p, n-i));  
        }
        return q;
    }

 

遞歸很好理解,若是不懂能夠看上面的講解,遞歸的思路其實和回溯法是同樣的,遍歷全部解空間但這裏和上面斐波拉契數列的不一樣之處在於,在每一層上都進行了一次最優解的選擇,q=Math.max(q, p[i-1]+cut(p, n-i));這個段語句就是最優解選擇,這裏上一層的最優解與下一層的最優解相關。

②備忘錄版本

public static int cutMemo(int []p)
    {
        int []r=new int[p.length+1];
        for(int i=0;i<=p.length;i++)
            r[i]=-1;                        
        return cut(p, p.length, r);
    }
    public static int cut(int []p,int n,int []r)
    {
        int q=-1;
        if(r[n]>=0)
            return r[n];
        if(n==0)
            q=0;
        else {
            for(int i=1;i<=n;i++)
                q=Math.max(q, cut(p, n-i,r)+p[i-1]);
        }
        r[n]=q;

        return q;
    }




有了上面求斐波拉契數列的基礎,理解備忘錄方法也就不難了。備忘錄方法無非是在遞歸的時候記錄下已經調用過的子函數的值。這道鋼條切割問題的經典之處在於自底向上的動態規劃問題的處理,理解了這個也就理解了動態規劃的精髓。

③自底向上的動態規劃

public static int buttom_up_cut(int []p)
    {
        int []r=new int[p.length+1];
        for(int i=1;i<=p.length;i++)
        {
            int q=-1;
            //①
            for(int j=1;j<=i;j++)
                q=Math.max(q, p[j-1]+r[i-j]);
            r[i]=q;
        }
        return r[p.length];
    }


自底向上的動態規劃問題中最重要的是理解註釋①處的循環,這裏外面的循環是求r[1],r[2]……,裏面的循環是求出r[1],r[2]……的最優解,也就是說r[i]中保存的是鋼條長度爲i時劃分的最優解,這裏面涉及到了最優子結構問題,也就是一個問題取最優解的時候,它的子問題也必定要取得最優解。下面是長度爲4的鋼條劃分的結構圖。我就偷懶截了個圖。
優化

 


這裏寫圖片描述
動態規劃原理

雖然已經用動態規劃方法解決了上面兩個問題,可是你們可能還跟我同樣並不知道何時要用到動態規劃。總結一下上面的斐波拉契數列和鋼條切割問題,發現兩個問題都涉及到了重疊子問題,和最優子結構。

①最優子結構

用動態規劃求解最優化問題的第一步就是刻畫最優解的結構,若是一個問題的解結構包含其子問題的最優解,就稱此問題具備最優子結構性質。所以,某個問題是否適合應用動態規劃算法,它是否具備最優子結構性質是一個很好的線索。使用動態規劃算法時,用子問題的最優解來構造原問題的最優解。所以必須考查最優解中用到的全部子問題。

②重疊子問題

在斐波拉契數列和鋼條切割結構圖中,能夠看到大量的重疊子問題,好比說在求fib(6)的時候,fib(2)被調用了5次,在求cut(4)的時候cut(0)被調用了4次。若是使用遞歸算法的時候會反覆的求解相同的子問題,不停的調用函數,而不是生成新的子問題。若是遞歸算法反覆求解相同的子問題,就稱爲具備重疊子問題(overlapping subproblems)性質。在動態規劃算法中使用數組來保存子問題的解,這樣子問題屢次求解的時候能夠直接查表不用調用函數遞歸。
動態規劃的經典模型
線性模型

線性模型的是動態規劃中最經常使用的模型,上文講到的鋼條切割問題就是經典的線性模型,這裏的線性指的是狀態的排布是呈線性的。【例題1】是一個經典的面試題,咱們將它做爲線性模型的敲門磚。

【例題1】在一個夜黑風高的晚上,有n(n <= 50)個小朋友在橋的這邊,如今他們須要過橋,可是因爲橋很窄,每次只容許不大於兩人經過,他們只有一個手電筒,因此每次過橋的兩我的須要把手電筒帶回來,i號小朋友過橋的時間爲T[i],兩我的過橋的總時間爲兩者中時間長者。問全部小朋友過橋的總時間最短是多少。
ui

 


這裏寫圖片描述

每次過橋的時候最多兩我的,若是橋這邊還有人,那麼還得回來一我的(送手電筒),也就是說N我的過橋的次數爲2*N-3(倒推,當橋這邊只剩兩我的時只須要一次,三我的的狀況爲來回一次後加上兩我的的狀況…)。有一我的須要來回跑,將手電筒送回來(也許不是同一我的,realy?!)這個回來的時間是沒辦法省去的,而且回來的次數也是肯定的,爲N-2,若是是我,我會選擇讓跑的最快的人來幹這件事情,可是我錯了…若是老是跑得最快的人跑回來的話,那麼他在每次別人過橋的時候必定得跟過去,因而就變成就是很簡單的問題了,花費的總時間:

T = minPTime * (N-2) + (totalSum-minPTime)

來看一組數據 四我的過橋花費的時間分別爲 1 2 5 10,按照上面的公式答案是19,可是實際答案應該是17。

具體步驟是這樣的:

第一步:1和2過去,花費時間2,而後1回來(花費時間1);

第二歩:3和4過去,花費時間10,而後2回來(花費時間2);

第三部:1和2過去,花費時間2,總耗時17。

因此以前的貪心想法是不對的。咱們先將全部人按花費時間遞增進行排序,假設前i我的過河花費的最少時間爲opt[i],那麼考慮前i-1我的過河的狀況,即河這邊還有1我的,河那邊有i-1我的,而且這時候手電筒確定在對岸,因此opt[i] = opt[i-1] + a[1] + a[i] (讓花費時間最少的人把手電筒送過來,而後和第i我的一塊兒過河)若是河這邊還有兩我的,一個是第i號,另一個無所謂,河那邊有i-2我的,而且手電筒確定在對岸,因此opt[i] = opt[i-2] + a[1] + a[i] + 2*a[2] (讓花費時間最少的人把電筒送過來,而後第i我的和另一我的一塊兒過河,因爲花費時間最少的人在這邊,因此下一次送手電筒過來的必定是花費次少的,送過來後花費最少的和花費次少的一塊兒過河,解決問題)
因此 opt[i] = min{opt[i-1] + a[1] + a[i] , opt[i-2] + a[1] + a[i] + 2*a[2] }
區間模型

區間模型的狀態表示通常爲d[i][j],表示區間[i, j]上的最優解,而後經過狀態轉移計算出[i+1, j]或者[i, j+1]上的最優解,逐步擴大區間的範圍,最終求得[1, len]的最優解。

【例題2】給定一個長度爲n(n <= 1000)的字符串A,求插入最少多少個字符使得它變成一個迴文串。
典型的區間模型,迴文串擁有很明顯的子結構特徵,即當字符串X是一個迴文串時,在X兩邊各添加一個字符’a’後,aXa仍然是一個迴文串,咱們用d[i][j]來表示A[i…j]這個子串變成迴文串所須要添加的最少的字符數,那麼對於A[i] == A[j]的狀況,很明顯有 d[i][j] = d[i+1][j-1] (這裏須要明確一點,當i+1 > j-1時也是有意義的,它表明的是空串,空串也是一個迴文串,因此這種狀況下d[i+1][j-1] = 0);當A[i] != A[j]時,咱們將它變成更小的子問題求解,咱們有兩種決策:

一、在A[j]後面添加一個字符A[i];

二、在A[i]前面添加一個字符A[j];

根據兩種決策列出狀態轉移方程爲:

d[i][j] = min{ d[i+1][j], d[i][j-1] } + 1; (每次狀態轉移,區間長度增長1)

空間複雜度O(n^2),時間複雜度O(n^2), 下文會提到將空間複雜度降爲O(n)的優化算法。
揹包模型

揹包問題是動態規劃中一個最典型的問題之一。因爲網上有很是詳盡的揹包講解,這裏只將經常使用部分抽出來。

【例題3】有N種物品(每種物品1件)和一個容量爲V的揹包。放入第 i 種物品耗費的空間是Ci,獲得的價值是Wi。求解將哪些物品裝入揹包可以使價值總和最大。f[i][v]表示前i種物品剛好放入一個容量爲v的揹包能夠得到的最大價值。決策爲第i個物品在前i-1個物品放置完畢後,是選擇放仍是不放,狀態轉移方程爲:

f[i][v] = max{ f[i-1][v], f[i-1][v – Ci] +Wi }

時間複雜度O(VN),空間複雜度O(VN) (空間複雜度可利用滾動數組進行優化達到O(V) )。
動態規劃題集整理

一、最長單調子序列
Constructing Roads In JG Kingdom★★☆☆☆
Stock Exchange ★★☆☆☆

二、最大M子段和
Max Sum ★☆☆☆☆
最長公共子串 ★★☆☆☆

三、線性模型
Skiing ★☆☆☆☆
總結

弄懂動態規劃問題的基本原理和動態規劃問題的幾個常見的模型,對於解決大部分的問題已經足夠了。但願能對你們有所幫助,轉載請標明出處http://write.blog.csdn.net/mdeditor#!postId=75193592,創做實在不容易,這篇博客花了我將近一個星期的時間。
參考文獻

1.算法導論
---------------------
做者:HankingHu
來源:CSDN
原文:https://blog.csdn.net/u013309870/article/details/75193592
版權聲明:本文爲博主原創文章,轉載請附上博文連接!spa

相關文章
相關標籤/搜索