笨辦法理解動態規劃算法

動態規劃在編程中有着普遍的應用,對於某些問題咱們能夠經過動態規劃顯著的下降程序的時間複雜度。本質上動態規劃並非一種算法,而是解決一類問題的思想。本篇博客經過一些很是簡單而又經典的問題(好比數塔、0-1揹包、徹底揹包、走樓梯問題、最長公共子序列等)來幫助你們理解動態規劃的通常套路。html

歡迎探討,若有錯誤敬請指正 java

如需轉載,請註明出處 http://www.cnblogs.com/nullzx/算法

 


 

1. 動態規劃的基本思想

若是咱們解決一個問題的時候能將一個大問題轉換成一個或者若干個規模較小的同等性質的問題,當咱們求解出這些小問題的答案後,大問題的答案很容易解決,對於這樣的狀況,顯然咱們能夠遞歸(或者說分治)的方式解決問題。若是在求解這些小問題的過程當中發現有些小問題咱們須要重複計算屢次,那麼咱們就乾脆把已經求解過的小問題的答案記錄下來放在一張表中,這樣下次遇到這個小問題,咱們只須要查表就能夠直接獲得結果,這個就是動態規劃的白話講解。動態規劃的難點在於如何定義問題及子問題。編程

2. 笨辦法的套路

1)若是能夠將一個規模較大的問題轉換成一個或若干個規模較小的子問題,也就是能找到遞推關係,這個時候咱們不妨先將程序寫成遞歸的形式。數組

2)若是使用遞歸求解規模較小的問題上存在子問題重複求解的現象,那麼咱們就創建一張表(有可能這個表只有一行)記錄須要重複求解的子問題。填表的過程和將大問題劃分爲子問題的方式相反,咱們會從最簡單的子問題開始填表。如今咱們就利用這個套路解決下面這些經典的問題。app

3. 利用套路解題

3.1 菲波那切數列

問題描述:菲波那契數列的定義f(n) = f(n-1) + f(n-2), 且f(1)=1, f(2) = 1,求f(n)的值。斐波那契數列的定義自己就是將大問題轉換成兩個同性質的子問題,因此咱們能夠直接根據定義寫成遞歸形式。iphone

 

	public static int recursion(int n) {
		
		if (n < 0) {
			return 0;
		}
		
		if (n == 1 || n == 2) {
			return 1;
		}
		
		return recursion(n-1) + recursion(n-2);
	}

咱們以f(6)爲例如今把遞歸的過程畫出來優化

clip_image002

咱們發如今求解F(6)時,須要求解F(2)四次,求解F(1)三次,求解F(3)三次,F(4)兩次,因此說咱們的算法的效率是很低的。提升效率的辦法就是將F(1),F(2),F(3) ….的結果放在表中,下次要計算這些問題的時候咱們直接從表中獲取就行了,這就是一個最簡單的動態規劃的例子。如今咱們按照套路,從最小的子問開始填表就行了。ui

	public static int dynamic(int n) {
		
		int[] table = new int[n+1];
		
		table[1] = 1;
		table[2] = 1;
		
		/*從小到大填表*/
		for (int i = 3; i < table.length; i++) {
			table[i] = table[i-1] + table[i-2];
		}
		
		return table[n];
	}

須要說明的是,這個例子只是一個入門的例子,實際上它不存在最優子結構的問題,並且也不須要長度爲n+1的table數組,只須要兩個變量便可(能夠理解爲動態規劃的優化版本),而咱們之因此這樣講解只是爲了讓你們從動態規劃的角度去理解問題。this

3.2 走樓梯問題

問題描述:總共有n個樓梯,每次能夠走2個臺階,或者每次走3個臺階,或者每次走5個臺階,請問這n個樓梯總共有幾種走法。

n個階梯的問題,能夠分解成三個小問題,即n-2個階梯有幾種走法,n-3個階梯有幾種走法,n-5個階梯有幾種走法,而n個階梯的走法就是這三種走法的和。或者能夠反過來思考,你已經站在最後一個臺階上了,那麼到達最後一個臺階的狀況只能是三種狀況,最後一步剛好走2個臺階剛好到達,最後一步剛好走3個臺階剛好到達,最後一步剛好走5個臺階剛好到達。經過這個思想,咱們就能夠寫出遞歸形式的代碼。

	public static int recursion(int n) { 
		
		if (n < 0) {
			return 0;
		}
		
		if (n == 0) {
			return 1;
		}
		
		return recursion(n - 5) + recursion(n - 3) + recursion(n - 2);
	}

顯然上面遞歸的處理方式須要重複計算不少子問題,畫出遞歸調用的圖就一目瞭然,因爲該圖和上一個問題的圖很相似,這裏就省略了。所以就建立一張表,把子問題的結果都記錄下來,dp[i]表示走到第i個階梯有多少種走法。按照套路,咱們應該從小的階梯數開始填表。

	public static int dynamic(int n) {
		
		int[] record = new int[n+1];
		
		record[0] = 1;
		
		for (int i = 0; i < record.length; i++) {
			
			int n2 = i - 2 >= 0 ? record[i-2] : 0;
			int n3 = i - 3 >= 0 ? record[i-3] : 0;
			int n5 = i - 5 >= 0 ? record[i-5] : 0;
			
			record[i] = n2 + n3 + n5;
		}
		
		return record[n];
	}

一樣,這裏例子中也不存在最優問題。

3.3 數塔問題

問題描述:從頂部出發在每個節點能夠選擇向下或者向右下走,一直走到底層,要求找出一條路徑,使得路徑上的數字之和最大。

clip_image002[6]

對於上圖所示的數塔:最大值爲379, 綠色的的數字就是被選擇的節點。

這個問題不能使用貪心算法,請你們本身用三層的階梯列舉出反例。咱們如今試着將這個問題分解成子問題,以下圖所示。想求得最大值,咱們只要選擇的紅色邊框數塔最大值和藍色邊框數塔的最大值中更大的那個,而後加上32,就整個數塔的最大值。這樣咱們就將一個大的問題轉化成了兩個規小的問題,而後這兩個規模較小的問題還能夠繼續分解成更小的子問題。根據這個思路,咱們能夠獲得以下遞歸形式的代碼。

clip_image004

	/*咱們用一個二維數組的左下半數組表示數塔*/
	public static int recursion(int[][] a){
		return recursion(a, 0, 0);
	}
	
	/*參數i表示所在的行,j表示所在的列*/
	private static int recursion(int[][] a, int i, int j){
		
		/*
		 * 當分解問題到最下一層時,
		 * (a.length - 1, j)位置爲頂點的數塔實際上數塔只有一個元素,
		 * 直接返回
		*/
		if (i == a.length - 1){
			return a[i][j];
		}
		
		/*求(i+1, j)位置爲頂點的數塔最大值*/
		int r1 = recursion(a, i+1, j);
		
		/*求(i+1, j+1)位置爲頂點的數塔最大值*/
		int r2 = recursion(a, i+1, j+1);
		
		/*返回(i,j)爲頂點位置的數塔的最大值*/
		return Math.max(r1, r2) + a[i][j];
	}

上述代碼可以獲得正確的結果,可是咱們發現計算大一點的數塔計算會很費時間,這主要是重複計算的問題,咱們如今來分析一下爲何會出現重複計算的問題。clip_image002[8]

上圖中的紫色邊框數塔既存在於紅色邊框數塔中,也存在於藍色邊框數塔中,會重複計算兩次。實際上,咱們使用遞歸時重複計算的問題顯然不止這一個,因此效率不高。爲此咱們應該建立一張和數塔形狀同樣的三角形表用來記錄更小的數塔的最大值。咱們table表示這個表,表中table[i][j]位置的值表示以(i,j)爲頂點的數塔的最大值。咱們用a[i][j]表示數塔中第i行,第j列的值。那麼table[i][j] = a[i][j] + Math.max(table[i-1][j], table[i-1][j-1])。按照套路,咱們應該從最小的數塔開始填表。按照table[i][j]的定義,table表的最下面一行就應該等於數塔表中的最下面一行。

clip_image004[4]

按照定義,咱們就能夠填倒數第二行的dp[i][j]。

table[4][0] = 79 + Math.max(0, 71) = 150
table[4][1] = 69 + Math.max(71, 51) = 140
table[4][2] = 78 + Math.max(51, 82) = 160
table[4][3] = 29 + Math.max(82, 91) = 120
table[4][4] = 63 + Math.max(91, 64) = 154

填入到table表的倒數第二行,以下圖所示

clip_image002[10]

有了倒數第二行,咱們就能夠推出倒數第三行,依次類推,咱們就能夠獲得最上面table [0][0]的數值,它就表示了整個數塔的最大值。除了最大值,若是咱們還須要知道走了哪些路徑,咱們還應該定義一個path表,在填table[i][j]時,同時填寫path[i][j]。path[i][j]表示了以(i, j)爲頂點的數塔的最大值是由兩個子數塔(table[i-1][j]爲頂點的數塔和table[i-1][j+1]爲頂點的數塔)中的哪個獲得的。

public class NumbericalTower {

	/*最大值對應的各個頂點位置*/
	private LinkedList<Map.Entry<Integer, Integer>> pathList;
		
	/*存儲整個數塔的最大值*/
	private int result;

	public NumbericalTower(int[][] a) {

		pathList = new LinkedList<Map.Entry<Integer, Integer>>();
		dynamic(a);
	}

	
	private void dynamic(int[][] a){

		final int N = a.length;
		
		/*path[i][j] 表示(i+1, j)爲頂點的數塔和(i+1,j+1)爲頂點的數塔
		 *中較大的那個*/
		int[][] path = new int[N][N];
		
		/*動態規劃對應的表*/
		int[][] table = new int[N][N];

		/*從最小的數塔開始填表*/
		for (int i = N - 1; i >= 0; i--) {
			
			/*根據下層數塔的最大值計算上層的數塔的最大值*/
			for (int j = 0; j <= i; j++) {
				
				if (i == N - 1) {
					table[i][j] = a[i][j];
					path[i][j] = -1;
					
				}else if (table[i+1][j] > table[i+1][j+1]) {
					table[i][j] = table[i+1][j] + a[i][j];
					path[i][j] = j;
				}else{
					table[i][j] = table[i+1][j+1] + a[i][j];
					path[i][j] = j+1;
				}
			}
		}
		
		result = table[0][0];
		
		/*記錄最大值對應的頂點*/
		int i = 0, j = 0;
		pathList.add(new SimpleEntry<Integer, Integer>(0, 0));
		
		while (true) {
			j = path[i][j];
			i = i + 1;
			pathList.add(new SimpleEntry<Integer, Integer>(i, j));
			
			if (path[i][j] == -1) {
				break;
			}
		}
	}
	
	int max(){
		return result;
	}
	
	List<Map.Entry<Integer, Integer>> path(){
		return pathList;
	}
	
	public static void main(String[] args) {
		int[][] a = {
			{32},
			{83, 68},
			{40, 37, 47},
			{ 5,  4, 67, 22},
			{79, 69, 78, 29, 63},
			{ 0, 71, 51, 82, 91, 64}
		};

		NumbericalTower nt = new NumbericalTower(a);
		int max = nt.max();
		List<Map.Entry<Integer, Integer>> path = nt.path();
		System.out.println("最大值:" + max);
		System.out.println("\n\n路徑爲:");
		for (Map.Entry<Integer, Integer> entry : path) {
			int r = entry.getKey();
			int c = entry.getValue();
			System.out.println("行 : " + r + ", 列:"+ c);
		}
	}
}

運行結果

最大值:379

路徑爲:
行 : 0, 列:0
行 : 1, 列:0
行 : 2, 列:1
行 : 3, 列:2
行 : 4, 列:2
行 : 5, 列:3
3.4 零-壹揹包問題

問題描述:有n 個物品,它們有各自的重量(weight)和價值(value),現有給定容量的揹包,如何讓揹包裏裝入的物品具備最大的價值總和此時揹包中的物品?一個物品只有不拿和拿兩種狀況,能夠用0和1表示,因此稱之爲0-1揹包問題。

咱們來看一個具體的例子。假設有以下物品:

clip_image002

求揹包容量在10的時候的能裝物品的最大價值,以及裝了哪些物品?

3.4.1 解決揹包的最大價值

咱們可能首先想到的是貪心算法,咱們算出每種物品的單位重量價值(weight/value),而後按照單位重量價值排序。咱們放入物品時首先選擇單位重量價值高的物品,直到放不下爲止。可是很遺憾,這樣得不到最優解。咱們不妨列舉一個極端的例子,假設只有兩個物品,A的value = 2.9, weight = 2.1;B的value = 3, weight = 3,顯然物品A的單位重量價值要大於B的單位重量價值,但對於容量爲3的揹包,咱們應該選擇物品B,因此貪心算法失效。對於0-1揹包問題,貪心選擇之因此不能獲得最優解是由於:它沒法保證最終能將揹包裝滿,而部分閒置的揹包空間使每公斤揹包空間的價值下降了。

回到上面具體的這個問題,它能夠表述爲

maxValue{寶石、剃鬚刀、ipad、充電寶、iphone | 揹包容量10},

每一個物品只有選和不選兩種結果,咱們不妨從第一個物品開始。若是選了寶石,那麼問題轉化爲當前揹包已有價值爲50,並在剩下的揹包容量(10 - 4)的前提下,再剩下的物品中(即剃鬚刀、ipad、充電寶、iphone)選取出最大的價值;若是不選寶石,那麼問題轉化爲當前揹包價值爲0,並在剩下的揹包容量10的前提下,在剩下的物品中(即剃鬚刀、ipad、充電寶、iphone)選取出最大的價值。咱們只須要選擇:

50 + maxValue{剃鬚刀、ipad、充電寶、iphone | 揹包容量6}

0 + maxValue{剃鬚刀、ipad、充電寶、iphone | 揹包容量10}

中較大的那個。而這就直接轉化成兩個子問題的求解,顯然咱們已經能夠用分治的方式解決這個問題了。咱們不妨把遞歸樹(或者說分治樹)畫出來。

clip_image004[6]

上圖就是0-1揹包問題的遞歸樹,圖左文字邊表示當前可選的物品,節點中的值表示揹包的容量。咱們沒有把整個遞歸樹所有都畫出來,由於圖中咱們就已經發現了須要重複計算的子問題。若是揹包容量變大,物品種類變多,那麼須要重複計算的子問題就越多。須要說明的是上圖中有三個揹包容量爲7的子問題,可是隻有被標記的兩個子問題纔是重複的子問題,由於這兩個子問題的揹包容量同樣,可選物品同樣。爲了不子問題的重複求解,咱們就創建一張動態規劃表,下次遇到重複的子問題,咱們就直接查表。下圖表示了動態規劃表和遞歸樹之間的關係。

clip_image006

那咱們如今的主要問題就變成了如何填這樣一張表。咱們用一個名爲dp的二維數組表示這張表,dp[0]行須要單獨初始化,從dp[1]行開始填表,規則:從左到右,從上到下。

       clip_image008

dp[i][j]表示前i個物品(包括物品i),在揹包容量爲j時能裝的最大價值。

dp[i][j]爲下面二者的最大值:

1)物品i不放入揹包中:揹包容量爲j時,前i-1個物品組合出的最大價值

2)物品i放入揹包中:物品i的價值 + 除去物品i佔用的重量後,剩餘揹包容量j-weight(i)由前i-1個物品組合出的最大價值

用公式表示爲

clip_image010

3.4.2 解決揹包有哪些物品
經過dp表,咱們還能夠知道哪些物品放入了揹包中。從表格的右下角開始(第0個物品要單獨處理):

1)若是dp[i][j] > dp[i-1][j],說明該物品i被放入到了揹包中,令i = i – 1, j = j – weight[i],而後重複步驟1。

2)若是dp[i][j] == dp[i-1][j],且只想獲得揹包最大價值的其中一種的物品一種組合方式,不妨認爲該物品i沒有被放入到了揹包中,令i = i – 1, 重複步驟1)。

clip_image012

對於步驟2),若是

dp[i][j] == dp[i-1][j] && dp[i][j – weight(i)] + value(i) == dp[i][j]

說明物品i能夠放入揹包中(令i = i – 1, j = j – weight[i]),也能夠不用放入揹包中(令i = i - 1)。這裏就產生分支,說明放入揹包中的物品組合方式不惟一,爲了簡單起見,咱們找到一種物品的組合方式便可。

package demo;

import java.util.LinkedList;
import java.util.List;


public class KnapsacProblem {
	/*動態規劃表*/
	private int[][] dp;
	
	/*揹包裝的最大價值*/
	private int maxVal;
	
	/*揹包最大價值時對應的商品編號*/
	private List<Integer> goodsNumber; 
	
	public KnapsacProblem(int[] weight, int[] values, int capacity){
		
		if ( weight.length != values.length ){
			throw new IllegalArgumentException();
		}
		
		int goodsLen = weight.length;
		
		/*第0列不使用*/
		this.dp = new int[goodsLen][capacity + 1];
		
		goodsNumber = new LinkedList<Integer>();
		
		
		/*單獨初始化第0行*/
		for ( int j = 1; j < capacity + 1; j++){
			if (j >= weight[0]){
				dp[0][j] = values[0];
			}
		}
		
		/*填dp表*/
		for ( int i = 1; i < goodsLen; i++ ) {
			for ( int j = 1; j < capacity + 1; j++ ) {
				if ( weight[i] <= j ) {
					dp[i][j] = Math.max(dp[i-1][j], values[i] + dp[i-1][j - weight[i]]);
				} else {
					dp[i][j] = dp[i-1][j];
				}
			}
		}
		
		maxVal = dp[goodsLen - 1][capacity - 1];
		
		/*找出使用了哪些物品*/
		int j = capacity;
		for (int i = goodsLen - 1; i > 0; i-- ) {
			if ( dp[i][j] > dp[i-1][j] ) {
				goodsNumber.add(i);
				j = j - weight[i];
			}
		}
		
		/*單獨處理第0行,回退到第0行時發現揹包中還有物品,說明物品0在揹包中*/
		if (j > 0){
			goodsNumber.add(0);
		}
	}
	
	public int  getPackageMaxValue(){
		return this.maxVal;
	}
	
	public List<Integer> getGoodsNumber(){
		return this.goodsNumber;
	}
	
	public static void main(String[] args){
		
		int[] weight = {4, 5, 2, 1, 2};
		int[] values = {50, 40, 60, 20, 30};
		int capacity = 10;
		
		KnapsacProblem kp = new KnapsacProblem(weight, values, capacity);
		
		System.out.println(kp.getPackageMaxValue());
		System.out.println(kp.getGoodsNumber());
	}

}

運行結果

160
[4, 3, 2, 0]

若是咱們僅僅須要知道最大的價值,不須要知道裝了哪些物品,咱們就能夠對空間複雜度進行優化,動態規劃表只須要一維,由於dp[i][?]僅和dp[i-1][?]有關。

3.5 切分「和相等」的子集

Given a non-empty array containing only positive integers, find if the array can be partitioned into two subsets such that the sum of elements in both subsets is equal.

Note:

1. Each of the array element will not exceed 100.

2. The array size will not exceed 200.

Example 1:

Input: [1, 5, 11, 5]

Output: true

Explanation: The array can be partitioned as [1, 5, 5] and [11].

Example 2:

Input: [1, 2, 3, 5]

Output: false

Explanation: The array cannot be partitioned into equal sum subsets.

這是LeetCode的原題。這個問題本質上仍是0-1揹包問題,揹包容量是數組之和的一半,物品的價值和體積是1比1的關係,額外條件是須要把揹包裝滿。

3.6 徹底揹包問題

問題描述:有n 種物品,它們有各自的重量(weight)和價值(value),現有給定容量的揹包,每種物品能夠拿任意多個,如何讓揹包裏裝入的物品具備最大的價值,以及每種物品裝了幾個?

clip_image002

假設,咱們仍是利用0-1揹包中的物品,揹包容量爲11。

徹底揹包問題也能夠轉化成0-1揹包問題。由於第i個物品最多拿「揹包重量/(物品i的重量)」個,也就是說在0-1揹包問題中每一個物品i佔一行,徹底揹包問題中,每一個物品佔「揹包重量/(物品i的重量)」 個行,按照這個思路顯然已經可以解決這個問題。如今咱們不把這個問題轉化爲0-1揹包問題,而從這個問題的根源直接思考。

3.6.1  解決揹包的最大價值

徹底揹包問題能夠表述爲

maxValue{寶石、剃鬚刀、ipad、充電寶、iphone | 揹包容量10}

每一個物品只有選和不選兩種結果,咱們不妨從第一個物品開始。若是選了寶石,那麼問題轉化爲當前揹包已有價值爲50,並在剩下的揹包容量(10 - 4)的前提下,繼續在{寶石、剃鬚刀、ipad、充電寶、iphone}選取出最大的價值;若是不選寶石,那麼咱們就在{剃鬚刀、ipad、充電寶、iphone}中選擇一種,那麼問題轉化爲當前揹包價值爲0,並在剩下的揹包容量10的前提下,再剩下的物品中即{剃鬚刀、ipad、充電寶、iphone }選取出最大的價值。

所以咱們只須要選擇:

50 + maxValue{寶石、剃鬚刀、ipad、充電寶、iphone | 揹包容量6}

0 + maxValue{剃鬚刀、ipad、充電寶、iphone | 揹包容量10}

中較大的那個。

而這就直接轉化成兩個子問題的求解,顯然咱們已經能夠用分治的方式解決這個問題了。咱們一樣能夠把遞歸樹畫出來,一樣還會發現存在須要重複求解的子問題,爲了不子問題的重複求解,咱們仍是創建一張動態規劃表,下次遇到重複的子問題,咱們就直接查表。這裏咱們直接給出動態規劃表,咱們用一個名爲dp的二維數組表示這張表,dp[0]行單獨初始化,從dp[1]行開始填表,規則:從左到右,從上到下。

clip_image004

dp[i][j]表示前i個物品(包括物品i),在揹包容量爲j時能裝的最大價值。

dp[i][j]爲下面兩者的最大值:

clip_image006[4]

clip_image008[5]

3.6.2 解決揹包中物品的種類和個數

一樣,從dp表中咱們還能夠知道哪些物品被選擇了,選擇多少次。咱們仍是從右下角開始回溯。

1)dp[i][j] > dp[i-1][j] 說明i號物品被選擇了,j = j – weight[i]

2)dp[i][j] == dp[i-1][j] 爲了簡單起見,咱們認爲i號物品沒有被選擇,令i = i -1(實際上這裏一樣可能存在分支,即最大價值時物品的組合方式和數量並不惟一,咱們這裏爲了簡單處理,就不考慮這個問題了)。

clip_image009

package demo;

import java.util.AbstractMap.SimpleEntry;
import java.util.LinkedList;

public class AllKnapsacProblem {
	
	private int maxVal;
	
	private LinkedList<SimpleEntry<Integer, Integer>> goodsIdCount;
	
	public int getPackageMaxValue(){
		return maxVal;
	}
	
	public LinkedList<SimpleEntry<Integer, Integer>> getGoodsCount(){
		return goodsIdCount;
	}
	
	public AllKnapsacProblem(int[] weight, int[] values, int capacity){
		/*處理最大價值問題============================================*/

		if ( weight.length != values.length ){
			throw new IllegalArgumentException();
		}
		
		int goodsLen = weight.length;
		
		/*第0列不使用*/
		int[][] dp = new int[goodsLen][capacity + 1];
		
		/*第0行單獨處理*/
		for (int j = weight[0]; j <= capacity; j++){
			dp[0][j] = dp[0][j - weight[0]] + values[0];
		}
		
		for (int i = 1; i < goodsLen; i++){
			
			for (int j = 1; j <= capacity; j++){
				
				int max1 = dp[i-1][j];
                int max2 = j - weight[i] >= 0 ? values[i] + dp[i][j - weight[i]] : 0;
                
                dp[i][j] = Math.max(max1, max2);
			}
		}
		
		maxVal = dp[goodsLen-1][capacity];
		
		/*處理物品種類和個數問題問題============================================*/
		
		/*SimpleEntry<Integer, Integer>:key表示物品編號,value表示物品個數*/
		goodsIdCount = new LinkedList<SimpleEntry<Integer, Integer>>();
		
		int i = goodsLen - 1;
		int j = capacity;
		
		SimpleEntry<Integer, Integer> entry = new SimpleEntry<Integer, Integer>(i, 0);
		while (i > 0){
			
			if (dp[i][j] > dp[i-1][j]){
				int n = entry.getValue();
				entry.setValue(n+1);
				j = j - weight[i];
			}
			
			if (dp[i][j] == dp[i-1][j]){
				if (entry.getValue() > 0) {					
					goodsIdCount.add(entry);
				}
				i--;
				entry = new SimpleEntry<Integer, Integer>(i, 0);
			}
		}
				
		/*單獨處理第0行*/
		if (j > 0) {
			goodsIdCount.add(new SimpleEntry<Integer, Integer>(0, j/weight[0]));
		}
	}
	
	public static void main(String[] args){
		
		int[] values = {50, 40, 60, 20, 30};
		int[] weight = {4,   5,  2,  1, 2};
		int capacity = 11;
		
		AllKnapsacProblem ap = new AllKnapsacProblem(weight, values, capacity);
		
		System.out.println("揹包價值" + ap.getPackageMaxValue());
		for (SimpleEntry<Integer, Integer> entry : ap.goodsIdCount) {
			System.out.printf("物品%d : %d個\n", entry.getKey(), entry.getValue());
		}
		
	}
	
}
運行結果
320
物品3 : 1個
物品2 : 5個

 

3.7 找零錢問題

You are given coins of different denominations ([dɪˌnɑ:mɪˈneɪʃn] 面額) and a total amount of money amount. Write a function to compute the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1。

Example 1:

Input: coins = [1, 2, 5], amount = 11

Output: 3

Explanation: 11 = 5 + 5 + 1

Example 2:

Input: coins = [2], amount = 3

Output: -1

這道題目是LeetCode上面的原題。假設在一堆面值爲 1,2,5,11面值的硬幣,問最少須要多少個硬幣才能找出總值爲以兌換15元。面對這個問題咱們也會首先想到貪心算法,可是貪心算法給出的組合方案爲{11,1,1,1,1},但其實最優方案爲{5,5,5}。若是使用枚舉算法,每種硬幣都有選0個,選1個,選2個,選…,這樣時間複雜度過高。這個問題本質上仍是徹底揹包問題,物品的價值和重量比是1比1,額外條件是須要把揹包裝滿,因此咱們可使用動態規劃算法去解決它,代碼這裏就不給出了。

3.8 最長公共子序列

咱們首先看一會兒序列的定義。假設給定一個字符串,咱們抽取任意多個不超過字符串長度的字符,並保持它們的先後關係,這樣的字符咱們稱之爲子序列。對於字符串ABCDEFG而言, BEF、C、AG等等都是它的一個子序列。

Longest common sequence問題:給定兩個字符串s1和s2,求這兩個字符串中最長的公共子序列。好比給定兩個字符串s1:bdcaba和s2:abcbdab,它們的公共子序列

長度爲4,最長公共子序列是:bcba。

字符串s1的長度用n表示,字符產s2的長度用m表示,字符串s1和s2的最長公共字串用lcs(n,m)。那麼這個問題能夠轉化爲三個子問題

1)求lcs(n-1, m-1)

2)求lcs(n-1, m)

3)求lcs(n, m-1)

當咱們求的上述三個子問題的答案,那麼lcs(n, m)的結果就能夠經過以下方式獲得:

若是s1[n] == s2[m]

    lcs(n, m) = lcs(n-1, m-1)+1

若是s1[n] != s2[m] :

    lcs(n, m) = max{ lcs(n-1, m-1), lcs(n-1, m), lcs(n, m-1) }

可是實際上lcs(n,m)只要轉化成兩個子問題lcs(n-1, m)和lcs(n, m-1)就行了。

而子問題lcs(n-1, m-1)是沒有必要的,由於lcs(n-1, m-1)一定小於等於lcs(n-1, m)和lcs(n, m-1)中的en任意一個。從常理上來講很好理解,不可能兩個字符串中的任意一個變長了,公共子序列反而減小了。而本質上是因爲lcs(n-1, m-1)也是lcs(n-1, m)和lcs(n, m-1)這兩個問題的子問題。

經過上面的分析,咱們把大的問題轉化成小的問題,就能夠經過遞歸(或者說分治)的方式把問題解決了,下面就是遞歸對應的代碼。

	public static void recursion (char[] s1, char[] s2) {
		maxLen = recursion0 (s1, s1.length-1, s2, s2.length-1);
	}
	
	private static int recursion0 (char[] s1, int idx1, char[] s2, int idx2){
		
		if(idx1 < 0 || idx2 < 0){
			return 0;
		}
		
		int max1, max2;
		
		max1 = recursion0 (s1, idx1, s2, idx2 - 1);
		max2 = recursion0 (s1, idx1 - 1, s2,  idx2);
		
		if (s1[idx1] == s2[idx2]){
			return Math.max(max1, max2) + 1;
		}else{
			return Math.max(max1, max2);
		}
	}

顯然上述也一樣存在不少重複計算的子問題,爲了下降時間複雜度,要一張二維表記錄重複計算的子問題的結果,這張表咱們用dp表示, dp[i][j]就表示以s1[i]和s2[j]結尾的字符串最長公共子序列。按照套路填表規則要從最小的子問題開始,

clip_image002[3]

第0行,表示「b」和「bdcaba」的公共子序列,能夠單獨處理,同理第0列也能夠單獨處理,填表完成後如上圖所示。從第二行開始,dp表按照從上到下,從左到右的填表順序填表。根據子遞歸中子問題的定義,dp[i][j]的取值以下:

clip_image002[1]

clip_image006[4]

當填完整張表時,右下角的值就是公共子序列的最大長度。若是咱們還須要知道公共子序列是什麼,那麼咱們能夠從右下角開始回溯,若是dp[i][j] > dp[i-1][j] 且 dp[i][j] > dp[i][j-1], 說明s1[i]或者s2[j]是公共子序列,不然選擇走dp[i-1][j]和dp[i][j-1]中較大的那個,一樣第0行要單獨處理。

package demo;

public class LongestCommonSequence {
	
	private int[][] dp;
	private int maxLen;
	private String lcs;
	
	private char[] s1, s2;
		
	public int maxLen(){
		return maxLen;
	}
	
	public String getLCS() {
		return lcs;
	}
	
	public LongestCommonSequence(String str1, String str2) {
		s1 = str1.toCharArray();
		s2 = str2.toCharArray();
		dynamic();
		getString();
	}
	
	/*動態規劃算法*/
	private void dynamic(){
		
		dp = new int[s1.length][s2.length];
		
		/*單獨處理第0行*/
		for(int j = 0, x = 0; j < s2.length; j++){
			if (s1[0] == s2[j]){
				x = 1;
			}
			dp[0][j] = x;
		}
		
		/*單獨處理第0列*/
		for (int i = 0, x = 0; i < s1.length; i++) {
			if (s2[0] == s1[i]){
				x = 1;
			}
			dp[i][0] = x;
		}
		
		for (int i = 1; i < s1.length; i++) {
			
			for(int j = 1; j < s2.length; j++){
				
				if(s1[i] == s2[j]){
					dp[i][j] = 1 + dp[i-1][j-1];
				}else{
					dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
				}
			}
		}
		
		maxLen = dp[s1.length - 1][s2.length - 1];
		
	}
	
	/*回溯求出公共子序列*/
	private void getString(){
		
		int cnt = maxLen;
		StringBuffer sb = new StringBuffer();
		
		int i = s1.length - 1, j = s2.length - 1;
		
		while (i > 0 && j > 0){
			if (dp[i][j] > dp[i-1][j] && dp[i][j] > dp[i][j-1]){
				sb.append(s1[i]);
				i--;
				j--;
				cnt--;
			}else{
				if (dp[i-1][j] > dp[i][j-1]){
					i--;
				}else{
					j--;
				}
			}
		}
		
		/*單獨處理第0行, i和j必然有一個爲0*/
		if (cnt > 0){
			
			while (true){
				
				if (s1[i] == s2[j]){
					sb.append(s1[i]);
					break;
				}
				
				if (i > 0){
					i--;
				}
				
				if (j > 0){
					j--;
				}
			}
			
			cnt--;
		}
		
		lcs = sb.reverse().toString();
	}
	
	public static void main(String[] args){
		LongestCommonSequence lcs = new LongestCommonSequence("bcba", "bdcaba");
		System.out.println(lcs.maxLen);
		System.out.println(lcs.getLCS());
	}

}

4. 動態規劃算法總結

枚舉算法:若是爲了方便的解決這個問題,咱們須要將大問題化簡成小問題,將全部小問題中的最優解做爲咱們解決大問題的基礎。

貪心算法:若是爲了方便的解決這個問題,咱們須要將大問題化簡成小問題,在全部小問題中,僅選擇對當前最有利的小問題做爲咱們解決大問題的基礎。

動態規劃:若是爲了方便的解決這個問題,咱們須要將大問題化簡成小問題,記錄已解決過的小問題,將全部小問題中的最優解做爲咱們解決大問題的基礎。換句話說,能用貪心算法解決的,動態規劃算法也確定能解決,反之不成立。

能用動規解決的問題的特色

1) 問題具備最優子結構性質。若是問題的最優解所包含的子問題的解也是最優的,咱們就稱該問題具備最優子結構性質。

2) 無後效性。當前的若干個狀態值一旦肯定,則此後過程的演變就只和這若干個狀態的值有關和以前是採起哪一種手段或通過哪條路徑演變到當前的這若干個狀態,沒有關係。

5. 參考內容

[1]. 動態規劃:最長上升子序列(LIS)

[2]. 什麼是動態規劃?動態規劃的意義是什麼?

[3]. 漫畫:什麼是動態規劃?

相關文章
相關標籤/搜索