1. 程式人生 > >動態規劃典型例題解析

動態規劃典型例題解析

什麼是動態規劃

動態規劃的主要思想是把問題劃分為一個個子狀態,一個狀態的最優解往往是基於其前一個狀態的最優解。兩個狀態之間的關係,我們就稱之為狀態轉移方程。這裡引出了狀態和狀態轉移方程的概念:狀態是一個當前的值,這個值是通過前一個值以及狀態轉移方程推得的。在解決動態規劃問題的時候,我們往往會把問題建模為一個一維陣列或是二維陣列,處理完邊界值之後,就可以通過前一個狀態和後一個狀態的遞推關係迴圈解出軸上的一個個狀態值。如果說貪心演算法是為了達到目的追求區域性最優,簡單粗暴,那麼動態規劃就是一種相對嚴密,統籌全域性的演算法。

動態規劃的特點

1、空間換時間:動態規劃把最終問題的求解分解為一個個子結構,我們可以稱之為最優子結構,在求解出最終結果前,我們把之前的每一個狀態的最優解儲存在了陣列中,後面的狀態的求解基於之前的結果,這樣可以節省時間,但需要一些儲存空間來放這些值,這就是空間換時間。
2、題目提供的往往是一些具有一定屬性的物件,然後用他們去達成某種總體最優的目標。
3、動態規劃問題的求解陣列可以是一維的也可以是二維的。
4、重疊子結構:在求解一個大問題的過程中需要多次求解某個小問題,而小問題的解我們已經得到,直接取出來使用即可。
5、無後效性:某個狀態的求解只與它之前的狀態有關,而與它後面的狀態沒有關係。

動態規劃解題步驟

1、確定狀態:首先找到題目中變化的量,和求解目標對應的變數,一般我們可以基於這些量構建一個一維或者二維陣列。
2、確定遞推方向:分析題目要求,確定求解陣列狀態的迴圈初始位置和遞推方向。
3、處理陣列邊緣值:定義陣列後,往往我們會把邊緣值置0。若目標值為0,對應的狀態的解自然也是0,邊界的0的部分需要我們主動加上去,大家也要格外注意一下dp下標和陣列下標因為引入0導致的偏移。
4、在迴圈中求解陣列:即通過迴圈,去遞推一個個子狀態的值。這個時候需要注意狀態轉移方程,狀態之間的關係變化。
5、選出需要的最優解。

動態規劃題型1:錢幣選擇問題

問題描述:給你若干面值為1,3,5,數量不限的錢幣,給一個目標值,需要用最少的錢幣數量湊齊目標值。
解題思路:使用動態規劃,先確定湊0元需要0個錢幣,湊1元至少需要一個1元錢幣;湊2元至少兩個1元錢幣;湊3元時選擇一個3元硬幣比選擇3個1元硬幣更優,故選擇一個3元硬幣;湊四元的時候我們選擇用到前面的結果,只要再加上一元硬幣即可,當然在選擇這個方案前我們比對過(4-1)元、(4-3)元、(4-5)元的幾種情況。由於之前的計算結果都已經最優,所以我們只要考慮最後一張錢幣拿的是一元、三元、還是五元即可。這樣一來,我們可以遞推得到任意狀態的解。
演算法實現:

	/**
	 * 動態規劃1:錢幣選擇問題
	 */
	public void dp1()
	{
		int[] money={1,3,5,7};
		int sum=24;
		int result = DP1(money,sum);
		System.out.println(result);
	}
	/**
	 * 動態規劃1:錢幣選擇問題
	 * @return
	 */
	private int DP1(int[] money,int sum)
	{
       int[] number = new int[sum+1];
       for(int m =0;m<number.length;m++)//初始化一個較大的值
       {
    	   number[m]=m;
       }
       
       for(int i=1;i<=sum;i++) //迴圈獲取不同面值對應的最小數量
       {
    	   for(int j=0;j<money.length;j++) //迴圈拿可能情況進行比對
    	   {
    		   if(i>=money[j]&&number[i-money[j]]+1<number[i]) //滿足更小條件則賦值
    		       number[i]=number[i-money[j]]+1;  //如果發現有更小的情況就更新數值		
    	   }
       }
       return number[sum];
	}

動態規劃題型2:求三角形路徑數字最大和(自頂至底)

題目描述:給你一個類似二叉樹的結構,每個節點都有相應的值,現求自頂至底的路徑的最大數字和。
解題思路:其實這題自頂至底或者自低至頂都是可以的,我們這裡選擇前者來求解。我們只要把資料存入一個二維陣列,然後從頂端往下依次推得到達每個節點的最大數字和。
演算法實現:

	/**
	 * 動態規劃2:求三角形路徑數字最大和(自頂至底)
	 */
	public void dp2()
	{
		int[][] a = new int[5][5];
		Scanner sc = new Scanner(System.in);
		System.out.println("請輸入陣列的每一個數:");
		for(int i=0;i<5;i++)
		{
			for(int j=0;j<i+1;j++)
			{
				a[i][j]=sc.nextInt();
			}
		}
		int sum = DP2(a);
		System.out.println("最大和為:"+sum);
	}
	/**
	 * 動態規劃2:求三角形路徑最大數字和(自頂至底)
	 * @param a
	 * @param i
	 * @param j
	 * @return
	 */
	private int DP2(int[][] a) {
		int[][] sum = new int[a.length][a.length];
		sum[0][0]=a[0][0];
		int max=0;
		for(int i=1;i<a.length;i++)
		{
			for(int j=0;j<i+1;j++)
			{
				if(j>0&&j<i&&sum[i-1][j-1]+a[i][j]>sum[i-1][j]+a[i][j])
					sum[i][j]=sum[i-1][j-1]+a[i][j];
				else if(j>0&&j<i&&sum[i-1][j-1]+a[i][j]<=sum[i-1][j]+a[i][j])
					sum[i][j]=sum[i-1][j]+a[i][j];
				else if(j==0)
					sum[i][j]=sum[i-1][j]+a[i][j];
				else if(j==i)
					sum[i][j]=sum[i-1][j-1]+a[i][j];
				System.out.println(i+","+j+"最大和:"+sum[i][j]);
				if(i==a.length-1&&max<sum[i][j])
					max=sum[i][j];
			}
		}		
		return max;
	}

動態規劃題型3:求最長非降子序列長度

題目描述:求某序列的最長非降子序列長度。
解題思路:設陣列上每個值為一條非降子序列的末端,這條子序列的長度就是陣列的狀態。每個狀態都與前一個狀態有關。
演算法實現:

    /**
     * 動態規劃3:求最長非降子序列長度
     */
	public void dp3()
	{
		int[] a= {5,3,4,3,9,11,7};
		int length = DP3(a);
		System.out.println(length);
	}
	/**
	 * 動態規劃3:求最長非降子序列長度(LCS)
	 * @param a
	 * @return
	 */
	public int DP3(int[] a)
	{
		int length=1;
		int[] len = new int[a.length];
		len[0]=1;
		for(int i=1;i<a.length;i++)
		{
			if(a[i]>a[i-1])
				len[i]=len[i-1]+1;
			else
				len[i]=1;
			if(length<len[i])
		       length=len[i];
		}						
		return length;
	}

動態規劃題型4:求最長公共子序列

題目描述:給定兩個序列,求它們的最長公共子序列,子序列不要求連續(這裡的子序列可以理解為刪除序列上任意節點後餘下的部分組成的序列)
解題思路:這題也可以用動態規劃來解,這裡涉及到兩條序列,我們使用二維陣列,一個角標來表示第一個序列的某字元位置,另一個角標來表示另一個序列的某字元位置。狀態值的表示即兩個角標左側的兩個序列段的最長公共子序列長度。角標左移後得到的是當前狀態的前一個狀態,比如當前a[i]=b[j],那麼如果令i和j都減一,那麼最長公共子序列長度減一;若a[i]!=b[j],且陣列a的角標左移後最長子序列會減小,那麼不能移動這個角標,而應該移動j,若兩段序列還有相同部分,那麼a[i]還會再次等於b[j]。因此,在這個題型裡我們依然可以找到子狀態之間的遞推關係。
演算法實現:

    /**
     * 動態規劃3:求最長非降子序列長度
     */
	public void dp3()
	{
		int[] a= {5,3,4,3,9,11,7};
		int length = DP3(a);
		System.out.println(length);
	}
	/**
	 * 動態規劃3:求最長非降子序列長度(LCS)
	 * @param a
	 * @return
	 */
	public int DP3(int[] a)
	{
		int length=1;
		int[] len = new int[a.length];
		len[0]=1;
		for(int i=1;i<a.length;i++)
		{
			if(a[i]>a[i-1])
				len[i]=len[i-1]+1;
			else
				len[i]=1;
			if(length<len[i])
		       length=len[i];
		}						
		return length;
	}

動態規劃題型5: 01揹包問題

題目描述:給定若干物品,它們有一定的重量和價值,以及一個有一定容量上限的揹包,物品每種型別只有一個且在裝包時只可以選擇裝或者不裝,不能裝一部分。求揹包所能容納的最大價值。
解題思路:因為是物品只能選擇裝或者不裝,那麼就不適合採用貪心演算法了,此題我們用動態規劃來解。首先我們來找我們需要求解的狀態,這裡涉及到兩個變化的量,一個是選擇物品的種類,一個是揹包的容量(揹包的容量類似於我們之前求湊錢幣的題的那個總金額)。這題和例題1的區別是這題揹包可能會有一些空間剩餘,而例題一中必須要湊齊指定金額。在這題中,我們要求出在不同揹包容量下的最大價值,在某一定的容量下,還需要去考慮不同物品的選擇。假定我們只選擇物品A,然後求出在不同揹包容量下的最大價值,這顯然都是相同的。這時我們再加上物品B,再去推導不同揹包容量下的最大價值,這是受到物品A的影響的,我們要選擇是否放物品B。以此類推,我們可以得到在選擇任意物品和任意揹包容量下的最大價值。所以這題我們選擇二維陣列,表示不同種類物品數量的選擇和揹包的容量。
演算法實現:

	/**
	 * 動態規劃5:01揹包問題(物品有重量和價值,且只有一個,去放置到定容的包中)
	 */
	public void dp5()
	{
		int[] w = {3,4,5}; //重量
	    int[] v = {4,5,6}; //價值
	    int m = 8; //揹包最大總容量
	    int n = 3;  //物品種類數量
	    int value = DP5(w,v,m,n);
	    System.out.println(value);
	}
	/**
	 * 
	 * 動態規劃5:01揹包問題
	 * @param w:物品重量
	 * @param v:物品價值
	 * @param m:揹包容量
	 * @param n: 物品種類數量
	 */
	public int DP5(int[] w , int[] v , int m ,int n)
	{
		int max=0;
		int[][] dp = new int[n+1][m+1];
		for(int i = 0 ;i<n+1;i++)
		    dp[i][0]=0;
		for(int i = 0 ;i<m+1;i++)
			dp[0][i]=0;		
		for(int i = 1;i<n+1;i++)   //以放的物品數量為尺度
		{
			for(int j=1;j<m+1;j++) 
			{
				if(w[i-1]>j) //不放
					dp[i][j]=dp[i-1][j]; //總價值和之前的情況相同,少了這個物品,用之前的物品去填充這個包
				else //放,繼續分類
				{
					if(dp[i-1][j-w[i-1]]+v[i-1]>dp[i-1][j])//假設這個物品已經放了,然後推其總價值和不放這個物品的最優解比較,選大的
						dp[i][j]=dp[i-1][j-w[i-1]]+v[i-1]; 
					else
						dp[i][j]=dp[i-1][j];
				}
				if(dp[i][j]>max) //篩選最大值
				{
					max=dp[i][j];
				}
			}
		}
		return max;		
	}