揹包問題具體例子:假設現有容量10kg的揹包,另外有3個物品,分別爲a1,a2,a3。物品a1重量爲3kg,價值爲4;物品a2重量爲4kg,價值爲5;物品a3重量爲5kg,價值爲6。將哪些物品放入揹包可以使得揹包中的總價值最大?java
首先想到的,通常是窮舉法,一個一個地試,對於數目小的例子適用,若是容量增大,物品增多,這種方法就無用武之地了。算法
其次,能夠先把價值最大的物體放入,這已是貪婪算法的雛形了。若是不添加某些特定條件,結果未必可行。編程
最後,就是動態規劃的思路了。先將原始問題通常化,欲求揹包可以得到的總價值,即欲求前i個物體放入容量爲m(kg)揹包的最大價值c[i][m]——使用一個數組來存儲最大價值,當m取10,i取3時,即原始問題了。而前i個物體放入容量爲m(kg)的揹包,又能夠轉化成前(i-1)個物體放入揹包的問題。下面使用數學表達式描述它們二者之間的具體關係。數組
表達式中各個符號的具體含義。安全
w[i] : 第i個物體的重量;app
p[i] : 第i個物體的價值;框架
c[i][m] : 前i個物體放入容量爲m的揹包的最大價值;測試
c[i-1][m] : 前i-1個物體放入容量爲m的揹包的最大價值;優化
c[i-1][m-w[i]] : 前i-1個物體放入容量爲m-w[i]的揹包的最大價值;spa
由此可得:
c[i][m]=max{c[i-1][m-w[i]]+pi , c[i-1][m]}(下圖將給出更具體的解釋)
根據上式,對物體個數及揹包重量進行遞推,列出一個表格(見下表),表格來自(http://blog.csdn.net/fg2006/article/details/6766384?reload) ,當逐步推出表中每一個值的大小,那個最大價值就求出來了。推導過程當中,注意一點,最好逐行而非逐列開始推導,先從編號爲1的那一行,推出全部c[1][m]的值,再推編號爲2的那行c[2][m]的大小。這樣便於理解。
思路釐清後,開始編程序,Java代碼以下所示:
public class BackPack { public static void main(String[] args) { int m = 10; int n = 3; int w[] = {3, 4, 5}; int p[] = {4, 5, 6}; int c[][] = BackPack_Solution(m, n, w, p); for (int i = 1; i <=n; i++) { for (int j = 1; j <=m; j++) { System.out.print(c[i][j]+"\t"); if(j==m){ System.out.println(); } } } //printPack(c, w, m, n); } /** * @param m 表示揹包的最大容量 * @param n 表示商品個數 * @param w 表示商品重量數組 * @param p 表示商品價值數組 */ public static int[][] BackPack_Solution(int m, int n, int[] w, int[] p) { //c[i][v]表示前i件物品恰放入一個重量爲m的揹包能夠得到的最大價值 int c[][] = new int[n + 1][m + 1]; for (int i = 0; i < n + 1; i++) c[i][0] = 0; for (int j = 0; j < m + 1; j++) c[0][j] = 0; for (int i = 1; i < n + 1; i++) { for (int j = 1; j < m + 1; j++) { //當物品爲i件重量爲j時,若是第i件的重量(w[i-1])小於重量j時,c[i][j]爲下列兩種狀況之一: //(1)物品i不放入揹包中,因此c[i][j]爲c[i-1][j]的值 //(2)物品i放入揹包中,則揹包剩餘重量爲j-w[i-1],因此c[i][j]爲c[i-1][j-w[i-1]]的值加上當前物品i的價值 if (w[i - 1] <= j) { if (c[i - 1][j] < (c[i - 1][j - w[i - 1]] + p[i - 1])) c[i][j] = c[i - 1][j - w[i - 1]] + p[i - 1]; else c[i][j] = c[i - 1][j]; } else c[i][j] = c[i - 1][j]; } } return c; }
運行結果爲:
0 0 4 4 4 4 4 4 4 4 0 0 4 5 5 5 9 9 9 9 0 0 4 5 6 6 9 10 11 11 Process finished with exit code 0
1、基本概念
動態規劃過程是:每次決策依賴於當前狀態,又隨即引發狀態的轉移。一個決策序列就是在變化的狀態中產生出來的,因此,這種多階段最優化決策解決問題的過程就稱爲動態規劃。
2、基本思想與策略
基本思想與分治法相似,也是將待求解的問題分解爲若干個子問題(階段),按順序求解子階段,前一子問題的解,爲後一子問題的求解提供了有用的信息。在求解任一子問題時,列出各類可能的局部解,經過決策保留那些有可能達到最優的局部解,丟棄其餘局部解。依次解決各子問題,最後一個子問題就是初始問題的解。
因爲動態規劃解決的問題多數有重疊子問題這個特色,爲減小重複計算,對每個子問題只解一次,將其不一樣階段的不一樣狀態保存在一個二維數組中。
與分治法最大的差異是:適合於用動態規劃法求解的問題,經分解後獲得的子問題每每不是互相獨立的(即下一個子階段的求解是創建在上一個子階段的解的基礎上,進行進一步的求解)。
3、適用的狀況
能採用動態規劃求解的問題的通常要具備3個性質:
(1) 最優化原理:若是問題的最優解所包含的子問題的解也是最優的,就稱該問題具備最優子結構,即知足最優化原理。
(2) 無後效性:即某階段狀態一旦肯定,就不受這個狀態之後決策的影響。也就是說,某狀態之後的過程不會影響之前的狀態,只與當前狀態有關。
(3)有重疊子問題:即子問題之間是不獨立的,一個子問題在下一階段決策中可能被屢次使用到。(該性質並非動態規劃適用的必要條件,可是若是沒有這條性質,動態規劃算法同其餘算法相比就不具有優點)
4、求解的基本步驟
動態規劃所處理的問題是一個多階段決策問題,通常由初始狀態開始,經過對中間階段決策的選擇,達到結束狀態。這些決策造成了一個決策序列,同時肯定了完成整個過程的一條活動路線(一般是求最優的活動路線)。如圖所示。動態規劃的設計都有着必定的模式,通常要經歷如下幾個步驟。
初始狀態→│決策1│→│決策2│→…→│決策n│→結束狀態
圖1 動態規劃決策過程示意圖
(1)劃分階段:按照問題的時間或空間特徵,把問題分爲若干個階段。在劃分階段時,注意劃分後的階段必定要是有序的或者是可排序的,不然問題就沒法求解。
(2)肯定狀態和狀態變量:將問題發展到各個階段時所處於的各類客觀狀況用不一樣的狀態表示出來。固然,狀態的選擇要知足無後效性。
(3)肯定決策並寫出狀態轉移方程:由於決策和狀態轉移有着自然的聯繫,狀態轉移就是根據上一階段的狀態和決策來導出本階段的狀態。因此若是肯定了決策,狀態轉移方程也就可寫出。但事實上經常是反過來作,根據相鄰兩個階段的狀態之間的關係來肯定決策方法和狀態轉移方程。
(4)尋找邊界條件:給出的狀態轉移方程是一個遞推式,須要一個遞推的終止條件或邊界條件。
通常,只要解決問題的階段、狀態和狀態轉移決策肯定了,就能夠寫出狀態轉移方程(包括邊界條件)。
實際應用中能夠按如下幾個簡化的步驟進行設計:
(1)分析最優解的性質,並刻畫其結構特徵。
(2)遞歸的定義最優解。
(3)以自底向上或自頂向下的記憶化方式(備忘錄法)計算出最優值
(4)根據計算最優值時獲得的信息,構造問題的最優解
5、算法實現的說明
動態規劃的主要難點在於理論上的設計,也就是上面4個步驟的肯定,一旦設計完成,實現部分就會很是簡單。
使用動態規劃求解問題,最重要的就是肯定動態規劃三要素:
(1)問題的階段 (2)每一個階段的狀態
(3)從前一個階段轉化到後一個階段之間的遞推關係。
遞推關係必須是從次小的問題開始到較大的問題之間的轉化,從這個角度來講,動態規劃每每能夠用遞歸程序來實現,不過由於遞推能夠充分利用前面保存的子問題的解來減小重複計算,因此對於大規模問題來講,有遞歸不可比擬的優點,這也是動態規劃算法的核心之處。
肯定了動態規劃的這三要素,整個求解過程就能夠用一個最優決策表來描述,最優決策表是一個二維表,其中行表示決策的階段,列表示問題狀態,表格須要填寫的數據通常對應此問題的在某個階段某個狀態下的最優值(如最短路徑,最長公共子序列,最大價值等),填表的過程就是根據遞推關係,從1行1列開始,以行或者列優先的順序,依次填寫表格,最後根據整個表格的數據經過簡單的取捨或者運算求得問題的最優解。
f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}
6、動態規劃算法基本框架
[cpp] view plain copy
這道最大m子段問題我是在課本《計算機算法分析與設計》上看到,課本也給出了相應的算法,也有解這題的算法的邏輯。可是,看完以後,我知道這樣作能夠解出正確答案,可是我如何能想到要這樣作呢? 課本和網上的某些答案都講得比較晦澀,有些關鍵的步驟不是通常人能夠想獲得的。不只要知其然,還要知其因此然。不然之後咱們遇到相似的問題仍是不會解。
下面是我解這道題的思考過程。我按照本身的想法作,作到最後發現和課本的思想差很少,也有一點差異。若是對這道題有些不明白,能夠仔細看看,相信看完以後你會豁然開朗。
問題: 給定n個整數(可能爲負數)組成的序列 以及一個正整數m,要求肯定序列
的m個不相交子段,使這m個子段的總和達到最大。
0 首先舉個例子方便理解題目 若是 = {1,-2,3,4,-5,-6,7,8,-9} m=2 明顯所求兩個子段爲{3,4}{7,8} 最大m子段和爲26。
1 先想如何求得最大子段和。
1.1最容易想到的方法是窮舉法。列出全部的子段組合,求出每一個組合的子段和,全部組合中最大者即爲所求。
仔細分析後發現:計算量巨大且難以實現。果斷放棄。
1.2 分析:用數組a[1…n]來表示一個序列,用二維數組SUM[n][m]表示由數組a的前n個數字組成的子序列的最大m子段。(可知 n>=m)
SUM[n][m]即爲所求.
分析最後一個數字a[n]全部可能的狀況
1) a[n] 不屬於構成最大m子段和的一部分, SUM [n][m] = SUM [n-1][m]
2) a[n] 屬於構成最大m子段和的一部分, 且a[n]單獨做爲一個子段。
此時SUM[n][m] = SUM[n-1][m-1]+a[n];
3) a[n] 屬於構成最大m子段和的一部分, 且a[n]做爲最後一個子段的一部分。
此時比較複雜, a[n]只是做爲最後一個子段的一部分, 因此a[n-1]也必定在最後一個子段之中,不然a[n]即是一個單獨的子段,先後矛盾.
因此SUM[n][m] = (包含a[n-1]的由前n-1個數字組成的子序列的最大m子段和) + a[n]
若用 b[n][m] 表示包含a[n]的、由前n個數字組成的子序列 的最大m子段和。
則 SUM[n][m] = b[n-1][m] + a[n]
1.3 咱們仔細觀察第三種狀況裏面定義的b[n][m]: 包含a[n]的、由前n個數字組成的子序列 的最大m子段和。
假設a[k] (k∈[m,n])是 原問題的解中的最後一個子段的最後一個元素, 則b[k][m]即爲原問題的最大子段和。(若不明白再多看b的定義幾回~)
因此原問題所求的最大子段和SUM[n][m]爲
1.4 如今就好辦了, 分析b[n][m]的值如何計算。
回顧咱們剛剛對a[n]的分析的三種狀況中的後面兩種
1) a[n]單獨做爲一個子段,
則 b [n][m] = SUM[n-1][m-1] + a[n]
(而SUM[n-1][m-1]= )
2) a[n]做爲末尾子段的一部分
則 b[n][m] = b[n-1][m]+a[n]
分別計算狀況1) 和狀況2) 下的b[n][m], 比較, 取較大者.
a) 特殊狀況,
若m=n 則a[n]爲單獨子段 按狀況1)計算
若n=1 則SUM[n][m] = a[n]
1.5 到這裏很明顯能夠看出這是一個動態規劃的問題,還不太懂動態規劃也不要緊,你只要記得,要計算b[i][j], 須要有:SUM[i-1][j-1]、b[i-1][j] 。
而 SUM[i-1][j-1]由數組b算出。須要先算出 b[k][j-1] (j-1<=k <=i-1 )。參見前面SUM的推導.
因此我須要先知道 b[k][j-1] (j-1<=k <=i-1 ) 以及 b[i-1][j]
因此,數組b 如何填寫?不明白能夠畫個表看看
好比上表:在求SUM[8][4]時,咱們須要先求的爲圖中黃色區域.
黑色部分不可求(無心義), 白色部分在求解的時候不須要用到.
能夠看出 咱們只須要求 當 1<=j<=m 且 j<=i<=n-m+j 部分的b[i][j]就能夠得出解.(此處我用畫圖 有誰能夠有更方便的方法來理解歡迎討論)
至此 咱們大概知道此算法如何填表了,如下爲框架.
for(int j=1; j<=m ; j ++)
for(int i= j ;i <= n-m + i ; j++)
1.6 開始寫算法(我用java 實現)
1 package com.cpc.dp; 2 3 public class NMSum { 4 5 public static void Sum(int[] a ,int m ) { 6 7 int n = a.length; // n爲數組中的個數 8 int[][] b = new int[n+1][m+1]; 9 int[][] SUM = new int[n+1][m+1]; 10 11 for(int p=0;p<=n;p++) { // 一個子段獲數字都不取時 // 12 b[p][0] = 0; 13 SUM[p][0] = 0; 14 } 15 // for(int p=0;p<=m;p++) { // 當p > 0 時 並沒有意義, 此部分不會被用到,註釋掉 16 // b[0][p] = 0; 17 // SUM[0][p] = 0; 18 // } 19 for(int j=1;j<=m;j++){ 20 for (int i = j;i<=n-m+j;i++){ 21 22 // n=1 m=1 此時最大1子段爲 a[0] java 數組爲從0開始的 須要注意 後面全部的第i個數爲a[i-1]; 23 if(i==1){ 24 b[i][j] = a[i-1]; 25 SUM[i][j] = a[i-1]; 26 }else 27 { 28 //先假設 第i個數做爲最後一個子段的一部分 29 b[i][j] = b[i-1][j] + a[i-1]; 30 31 // 若第i個數做爲單獨子段時 b[i][j]更大 則把a[i-1] 做爲單獨子段 32 // 考慮特殊狀況 若第一個數字爲負數 b[1][1]爲負數 在求b[2][1] SUM[1][0]=0>b[1][1] 則捨去第一個數字 此處合理 33 if(SUM[i-1][j-1]+a[i-1] > b[i][j]) b[i][j] = SUM[i-1][j-1] + a[i-1]; 34 35 //填寫SUM[i][j]供之後使用 36 if(j<i){ // i 比j 大時 37 if(b[i][j]>SUM[i-1][j]){ // 用b[i][j] 與以前求的比較 40 SUM[i][j] = b[i][j]; 41 }else { 42 SUM[i][j] = SUM[i-1][j]; 43 } 44 }else // i = j 45 { 46 SUM[i][j] = SUM[i-1][j-1] + a[i-1]; 47 } 48 } 49 }//end for 50 }// end for 51 System.out.println(SUM[n][m]); // 輸出結果 52 }// end of method 53 54 public static void main(String[] args) { 55 int[] a = new int[]{1,-2,3,4,-5,-6,7,18,-9}; 56 Sum(a, 3); 57 } 58 }
output : 33
測試經過
/************** 4.22 更新***************************/
2 算法的優化
2.1 分析 算法的空間複雜度 爲O(mn).咱們觀察一下,在計算b[i][j]時 咱們用到b[i-1][j] 和 SUM[i-1][j-1],也就是說,每次運算的時候 咱們只須要用到數組b的這一行以及數組SUM的上一行.
咱們觀察一下算法的框架
for(int j=1; j<=m ; j ++)
for(int i= j ;i <= n-m + i ; j++)
// 計算b[i][j] 須要 SUM[i-1][j-1] 和 b[i-1][j]
// 計算SUM[i][j] 須要 SUM[i-1][j] b[i][j] SUM[i-1][j-1]
假設在 j=m 時(即最外面的for循環計算到最後一輪時)
要計算b[*][j] *∈[m,n]
我只須要知道 SUM[*-1][j-1] b[*-1][j] (即須要上一輪計算的數組SUM以及這一輪計算的數組b)
而以前所求的數組SUM和數組b其餘部分的信息已經無效,
咱們只關心最後一輪計算的結果,而最後一輪計算須要倒數第二輪計算的結果.
倒數第二輪計算須要再倒數第三結果.以此循環
所以咱們能夠考慮重複利用空間,
在一個位置所存儲的信息已經無效的時候,能夠覆蓋這個位置,讓它存儲新的信息.
舉個例子: 老師在黑板上推導某一個公式的時候, 黑板的面積有限,而有時候推導的過程十分長,很快黑板不夠用了,這個老師一般會擦掉前面推導的過程,留下推導下一步要用的一部分,在擦掉的地方繼續寫.
可是如何安全地覆蓋前面失效的內容而不會致使覆蓋掉仍然須要使用的內容呢?
分析後能夠得知一下約束:
1) 求b[i][j] 須要用到SUM[i-1][j-1] 因此SUM[i-1][j-1]必須在b[i][j]的值求完後才能夠被覆蓋
2) 求b[i][j] 須要用到 b[i-1][j] (j 相等)
3) 求SUM[i][j] 須要用到 SUM[i-1][j] (j 相等)
4) 求SUM[i][j] 須要用到 b[i][j] (j 相等)
5) 求SUM[i][j] 須要用到SUM[i-1][j-1] (i的位置錯開)
對於最外面的for循環
咱們只關心最後一輪(也就是第(j=m)輪)的結果,因此考慮把兩個二維數組變成一維數組b[1...n] 、SUM[1..n]
假設在第j輪計算後:
b[i] 表示的意義與原來的 b[i][j]相同 ( 也就是原來的b[i][j]會覆蓋b[i][j-1] )
SUM[i] 表示什麼呢
咱們觀察約束1)知道,在第j 輪計算 b[i] (即原來的b[i][j])時,仍然會用到原來SUM[i-1][j-1],
也就是說 , 在計算b[i]時,SUM[i-1] 須要存儲的是原來的SUM[i-1][j-1]
對於裏面的for 循環
因爲計算 b[i]須要SUM[i-1]
因此在計算完b[i] 後才計算新的SUM[i-1]
即在b[i]計算完後,能夠覆蓋掉SUM[i-1] 使之表示原來的SUM[i-1][j]
也就是說, 在第j輪計算完畢後, SUM[i] 表示的意義與原來的SUM[i][j]相同
2.2 分析得差很少了, 廢話少說,開始優化代碼
1 package com.cpc.dp; 2 3 public class NMSUM2 { 4 5 public static void Sum(int[] a ,int m ) { 6 7 int n = a.length; // n爲數組中的個數 8 int[] b = new int[n+1]; 9 int[] SUM = new int[n+1]; 10 11 b[0] = 0;// 一個子段獲或者數字都不取時 ,也能夠不設置,由於 java默認int數組中元素的初始值爲0 12 SUM[1] = a[0]; 13 14 for(int j=1;j<=m;j++){ 15 b[j] = b[j-1] + a[j-1]; // i=j 時 16 SUM[j-1] = -1; // 第j 輪 SUM[j-1]表示原來的 SUM[j-1][j] 無心義 設置爲-1 17 int temp = b[j]; 18 for (int i = j+1;i<=n-m+j;i++){ 19 20 //先假設 第i個數做爲最後一個子段的一部分 21 b[i] = b[i-1] + a[i-1]; 22 // 若第i個數做爲單獨子段時 b[i][j]更大 則把a[i-1] 做爲單獨子段 23 if(SUM[i-1]+a[i-1] > b[i]) b[i] = SUM[i-1] + a[i-1]; 24 25 //下面原來計算的是原來的SUM[i][j] ,可是如今要修改的應該是原來的SUM[i][j-1] ,如何把SUM[i][j]保存 下來? 26 // 能夠在循環外面定義一個變量temp來暫存 等下一次循環再寫入 27 SUM[i-1] = temp; 28 if(b[i]>temp){ 29 temp = b[i]; //temp 記錄SUM[i][j] 30 } 31 }//end for 32 SUM[j+n-m] = temp; 33 }// end for 34 System.out.println(SUM[n]); // 輸出結果 35 }// end of method 36 37 public static void main(String[] args) { 38 int[] a = new int[]{1,-2,3,4,-5,-6,7,18,-9}; 39 Sum(a, 1); 40 } 41 }
output: 25
算法的空間複雜度變爲o(n)~ 優化完畢!
動態規劃過程是:每次決策依賴於當前狀態,又隨即引發狀態的轉移。一個決策序列就是在變化的狀態中產生出來的,因此,這種多階段最優化決策解決問題的過程就稱爲動態規劃。
基本思想與分治法相似,也是將待求解的問題分解爲若干個子問題(階段),按順序求解子階段,前一子問題的解,爲後一子問題的求解提供了有用的信息。在求解任一子問題時,列出各類可能的局部解,經過決策保留那些有可能達到最優的局部解,丟棄其餘局部解。依次解決各子問題,最後一個子問題就是初始問題的解。
因爲動態規劃解決的問題多數有重疊子問題這個特色,爲減小重複計算,對每個子問題只解一次,將其不一樣階段的不一樣狀態保存在一個二維數組中。
與分治法最大的差異是:適合於用動態規劃法求解的問題,經分解後獲得的子問題每每不是互相獨立的(即下一個子階段的求解是創建在上一個子階段的解的基礎上,進行進一步的求解)。
以上都過於理論,仍是看看常見的動態規劃問題吧!!!
有數組penny,penny中全部的值都爲正數且不重複。每一個值表明一種面值的貨幣,每種面值的貨幣可使用任意張,再給定一個整數aim(小於等於1000)表明要找的錢數,求換錢有多少種方法。
給定數組penny及它的大小(小於等於50),同時給定一個整數aim,請返回有多少種方法能夠湊成aim。
測試樣例:
[1,2,4],3,3
返回:2
解析:設dp[n][m]爲使用前n中貨幣湊成的m的種數,那麼就會有兩種狀況:
使用第n種貨幣:dp[n-1][m]+dp[n-1][m-peney[n]]
不用第n種貨幣:dp[n-1][m],爲何不使用第n種貨幣呢,由於penney[n]>m。
這樣就能夠求出當m>=penney[n]時 dp[n][m] = dp[n-1][m]+dp[n-1][m-peney[n]],不然,dp[n][m] = dp[n-1][m]
代碼以下:
[java] view plain copy
有一個矩陣map,它每一個格子有一個權值。從左上角的格子開始每次只能向右或者向下走,最後到達右下角的位置,路徑上全部的數字累加起來就是路徑和,返回全部的路徑中最小的路徑和。
給定一個矩陣map及它的行數n和列數m,請返回最小路徑和。保證行列數均小於等於100.
測試樣例:
[[1,2,3],[1,1,1]],2,3
返回:4
解析:設dp[n][m]爲走到n*m位置的路徑長度,那麼顯而易見dp[n][m] = min(dp[n-1][m],dp[n][m-1]);
代碼以下:
[java] view plain copy
有n級臺階,一我的每次上一級或者兩級,問有多少種走完n級臺階的方法。爲了防止溢出,請將結果Mod 1000000007
給定一個正整數int n,請返回一個數,表明上樓的方式數。保證n小於等於100000。
測試樣例:
1
返回:1
解析:這是一個很是經典的爲題,設f(n)爲上n級臺階的方法,要上到n級臺階的最後一步有兩種方式:從n-1級臺階走一步;從n-1級臺階走兩步,因而就有了這個公式f(n) = f(n-1)+f(n-2);
代碼以下:
[java] view plain copy
給定兩個字符串A和B,返回兩個字符串的最長公共子序列的長度。例如,A="1A2C3D4B56」,B="B1D23CA45B6A」,」123456"或者"12C4B6"都是最長公共子序列。
給定兩個字符串A和B,同時給定兩個串的長度n和m,請返回最長公共子序列的長度。保證兩串長度均小於等於300。
測試樣例:
"1A2C3D4B56",10,"B1D23CA45B6A",12
返回:6
解析:設dp[n][m] ,爲A的前n個字符與B的前m個字符的公共序列長度,則當A[n]==B[m]的時候,dp[i][j] = max(dp[i-1][j-1]+1,dp[i-1][j],dp[i][j-1]),不然,dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
代碼以下:
[java] view plain copy