java動態規劃

一,基本概念

1.定義:動態規劃實際上是一類題目的總稱,並不是指某個固定的算法,其意義是通過採用遞推或分而治之的策略,通過解決大問題的子問題從而解決整體的做法,核心思想是巧妙地將問題拆分成多個子問題,通過計算子問題而得到整體問題的解,而子問題又可以拆分成更多的子問題,從而用類似遞推迭代的方法解決要求的問題

2.與分治法最大的差別是:適合於用動態規劃法求解的問題,經過分解後得到的子問題往往不是獨立的(即下一個子階段的求解是建立在上一個子階段的解的基礎上,進行進一步的求解)

3.解題的核心分爲兩步:

第一步:狀態的定義,有的問題過於抽象或者過於囉嗦干擾我們解題思路,我們要做的就是將題幹中的問題進行轉化(換一種說法,含義不變),eg,

題目:求一個數列中最大連續子序列的和

我們將這個原問題轉化爲:

Fk是第k項的最大子序列的和,求F1—Fn中最大值

第二步:狀態轉移方程的定義,即如何去求解一個問題,對於上述已經轉換成的問題,關注點在於,如何能夠用前一項或者前幾項的信息得到下一項,這種從最優子狀態轉換爲下一個最優狀態的思路就是動態規劃的核心,對於上例子,狀態轉移方程即是

Fk=max{Fk+Ak,Ak}

Fk是前k項的和,Ak是第k項的值

4.應用場景

從上述可以得出,動態規劃不是某個固定的算法,而是一種策略思路,那麼什麼時候應該去使用什麼時候不能用?

對於一個可拆分問題中存在可以由前若干項計算當前項的問題可以由動態規劃計算,能用動態規劃計算的問題存在一種遞推的連帶關係

二,常見動態規劃問題

1.國王和金礦

題目:有一個國家發現了5座金礦,每座金礦的黃金儲存量不同,需要參與與挖掘的工人數也不同,參與挖掘工人的總數是10人,每座金礦要麼全挖要麼不挖,不能派出一半人挖取一半金礦,要求獲得儘可能多的黃金,應該選擇哪幾座金礦?

金礦分佈及工人要求:400金/5人、500金/5人、200金/3人、300金/4人、350金/3人

分析:動態規劃的三個核心元素:最優子結構,邊界,狀態轉移方程式

方法一:排列組合,每一座金礦都有挖和不挖兩種選擇,如果n座金礦,就有2^n中選擇,對所有可能性做遍歷,排除那些使用工人數超過10的選擇,在剩下的選擇裏找出獲得金數量最多的,時間複雜度爲O(2^n)

動態規劃

最優子結構:10個工人4個金礦時挖出黃金最多,(10-第5金礦需工人)工人4個金礦時挖出的黃金最多

最優選擇即:10工人4金礦的數量,和,(10-第5金礦需工人)工人4個金礦時挖出的黃金+第5座數量,最大值

設金礦數量爲n,工人數量爲w,金礦黃金量爲數組g[],金礦用工量爲數組p[],則最優選擇用表達式描述爲

F(5,10)=max(F(4,10), F(4,10-p[4])+g[4])

邊界:n=1, w<p[0]時,F(n,w)=0,給的工人不夠挖一個金礦,得到爲0

          n=1, w>=p[0]時,F(n,w)=g[0],只有一個金礦且工人足夠,那麼只能挖這一個金礦

總結就是:

F(n,w)=0, (n<=1,w<p[0])
F(n,w)=g[0], (n==1,w>=p[0])
F(n,w)=F(n-1,w), (n>1,w<p[n-1])
F(n,w)=max{F(n-1,w),F(n-1,w-p[n-1]}+g[n-1], (n>1, w>=p[n-1])

方法二:簡單遞歸

將狀態轉移方程式翻譯成遞歸程序,遞歸結束的條件就是方程式當中的邊界,因爲每個狀態由兩個最優子結構,所以遞歸的執行流程類似於一棵高度爲n的二叉樹,時間複雜度爲O(2^n)

方法三:備忘錄算法

在簡單遞歸的基礎上增加一個HashMap備忘錄,存儲中間結構,HashMap的key是一個包含金礦數n個工人數w的對象,value是最優選擇獲得的黃金數,時間複雜度和的空間複雜度相同,都是備忘錄中不同key的數量

通過表格進行分析

規律:根據前一行的結果就可以推出行的一行

方法時間複雜度爲O(n*w),空間複雜度爲O(w)

2.數塔取數問題

問題:一個高度爲n的由正整數組成的三角形,從上走到下,求經過的數字和的最大值,每次只能走到下一層相鄰的數上,例如:從第三層的6向下走,只能走到第四層的2或者9上

示例:輸入爲(應該呈三角形,排版問題導致,請忽略)

5
8  4
3  6  9
7  2  9  5

輸出:28

最優方案是:5+8+6+9=28

狀態定義:Fi,j是第i行j列最大取數和,求第n行Fn,m中的最大值(0<m<n)

狀態轉移方程:

Fi,j=max{Fi-1,j-1,Fi-1,j}+Ai,j
package DTGH;

import java.util.Scanner;

public class Triangle {
	public static long triangleMax(){
		Scanner in=new Scanner(System.in);
		int n=in.nextInt();//先輸入一個數字表示測試數據爲幾行
		long max=0;
		int [][] dp=new int [n][n];
		dp[0][0]=in.nextInt();
		for(int i=1;i<n;i++){
			for(int j=0;j<=i;j++){
				int num=in.nextInt();
				if(j==0){
					dp[i][j]=dp[i-1][j]+num;
				}
				else{
					dp[i][j]=Math.max(dp[i-1][j-1], dp[i-1][j])+num;
				}
				max=Math.max(dp[i][j], max);
			}
		}
		return max;
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		System.out.println(triangleMax());
	}

}

運行結果

4
5
8 4
3 6 9
7 2 9 5
28

3.編輯距離

題目:編輯距離指兩個字串之間,由一個轉成另一個所需要的最少編輯操作次數,許可的編輯操作包括將一個字符替換成另一個字符、插入一個字符、刪除一個字符,給出兩個字符串a和b,求a和b的編輯距離

eg:將kitten轉爲sitting:

sitten(k->s), sittin(e->i), sitting(g),輸出爲3

狀態定義:Fi,j表示第一個字符串的前i個字母和第二個字符串的前j個字母需要編輯的次數,求Fn,m,n和m分別是兩個字符串的長度

狀態轉移方程:

當Fi,j-1=Fi-1,j時,Fi,j=Fi,j-1

當Fi,j-1!=Fi-1,j時,Fi,j=min{Fi-1,j-1,Fi,j-1,Fi-1,j}+1
package DTGH;

import java.util.Scanner;

public class EditDistance {
	public static int solution(){
		Scanner in=new Scanner(System.in);
		String a=in.nextLine();
		String b=in.nextLine();
		int alen=a.length();
		int blen=b.length();
		int [][]dp=new int[alen+1][blen+1];
		for(int i=0;i<alen+1;i++){
			dp[i][0]=i;
		}
		for(int j=0;j<blen+1;j++){
			dp[0][j]=j;
		}
		for(int i=1;i<alen+1;i++){
			for(int j=1;j<blen+1;j++){
				if(a.charAt(i-1)==b.charAt(j-1)){
					dp[i][j]=dp[i-1][j-1];
				}
				else{
					dp[i][j]=Math.min(dp[i-1][j-1], Math.min(dp[i-1][j], dp[i][j-1]))+1;
				}
			}
		}
		return dp[alen][blen];
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		System.out.println(solution());
	}

}
kitten
sitting
3

4.走方格問題

題目:有一個矩陣,它的每個格子有一個權值,從左上角格子開始每次只能向右或者向下走,最後到達右下角位置,路徑上所有數字累加起來就是路徑和,返回所有的路徑中最小的路徑和

分析:對於右下角的位置來說,來自於上一個向右走或者上一個向下,若設dp[n][m]爲走到n*m位置的路徑長度,

那麼dp[n][m]=min{dp[n][m-1], dp[n-1][m]}+a[n][m]

eg:{[1,2,3] [1,1,1]},2,3,輸出4

public class Rectangle {
//	n*m矩陣,從左上角走到右下角,同一時間只能向下或者向右
//	dp[n][m]爲走到n*m的路徑長度
	public static int minPathSum(){
		Scanner in=new Scanner(System.in);
		int n=in.nextInt();
		int m=in.nextInt();
		int [][]a=new int [n][m];
		for(int i=0;i<n;i++){
			for(int j=0;j<m;j++){
				a[i][j]=in.nextInt();
			}
		}
		int [][]dp=new int[n][m];
		dp[0][0]=a[0][0];
		for(int i=1;i<m;i++){//邊界,第0行,只能是前一個向右走
			dp[0][i]=a[0][i]+dp[0][i-1];
		}
		for(int i=1;i<n;i++){//邊界,第0列,即最左邊一列,只能是上面的往下走
			dp[i][0]=a[i][0]+dp[i-1][0];
		}
		for(int i=1;i<n;i++){
			for(int j=1;j<m;j++){
				dp[i][j]=Math.min(dp[i-1][j], dp[i][j-1])+a[i][j];
			}
		}
		return dp[n-1][m-1];
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		System.out.println(minPathSum());
	}

}

持續更新。。。

參考:https://blog.csdn.net/QuinnNorris/article/details/77484573

https://blog.csdn.net/p10010/article/details/50196211

https://zhuanlan.zhihu.com/p/31628866

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章