動態規劃(Dynamic Programming),簡稱DP,是算法設計技巧的一種(還有貪婪算法、分治算法、回溯算法、隨機化算法等)。其主要思想,就是通過把原問題分解爲相對簡單的子問題的方式求解複雜問題的算法思想,當然,動態規劃只能應用於有最優子結構的問題。最優子結構的意思是局部最優解能決定全局最優解(對有些問題這個要求並不能完全滿足,故有時需要引入一定的近似)。
動態規劃的使用規則:
- 最優化原理(最優子結構性質):如果一個問題的最優策略它的子問題的策略也是最優的,則稱該問題具有 “最優子結構性質”
- 無後效性:當一個問題被劃分爲多個決策階段,那麼前一個階段的策略不會受到後一個階段所做出策略的影響
- 子問題的重疊性:這個性質揭露了動態規劃的本質,解決冗餘問題,重複的子問題我們可以記錄下來供後階段決策時直接使用,從而降低算法複雜度
動態規劃的求解步驟:
- 描述最優解的模型
- 遞歸地定義最優解,也就是構造動態規劃方程,也叫狀態轉移方程。就是每一步下的子問題的最優解,以及和上一步最優解的關係
- 將自頂向下的遞歸,轉化爲自底向上地計算最優解。現在逆轉爲每一步下的子問題的最優解,以及和下一步最優解的關係
- 最後根據計算的最優值得出問題的最佳策略
以下是我給出的動態規劃的五個經典算法案例,以及算法計算過程(附詳細註釋):
動態規劃經典算法題1:爬樓梯
/**
* @author LiYang
* @ClassName ClimbStairs
* @Description 動態規劃:爬樓梯
* @date 2019/12/10 13:53
*/
public class ClimbStairs {
/**
* 動態規劃:爬樓梯(自頂向下遞歸實現)
* 有N階樓梯,可以一次爬一步,也可以一次爬兩步
* 請問有多少種爬法
* @param stair 樓梯階數
* @return 爬法數量
*/
public static int climbStairsRecursive(int stair) {
//如果只有一階
if (stair == 1) {
//就只有一種爬法
return 1;
}
//如果有兩階
if (stair == 2) {
//有兩種爬法
return 2;
}
//如果有stair階,則stair階的爬法,就是stair-1階和stair-2階
//爬法的總和。因爲stair階可以通過stair-1階再爬一階,或者
//stair-2階再爬兩階達到,也就是stair-1和stair-2爬法總和
return climbStairsRecursive(stair - 1) + climbStairsRecursive(stair - 2);
}
/**
* 動態規劃:爬樓梯(自底向上實現)
* 有N階樓梯,可以一次爬一步,也可以一次爬兩步
* 請問有多少種爬法
* @param stair 樓梯階數
* @return 爬法數量
*/
public static int climbStairsForward(int stair) {
//如果只有一階
if (stair == 1) {
//直接返回一種爬法
return 1;
}
//如果有兩階
if (stair == 2) {
//直接返回兩種爬法
return 2;
}
//答案記錄的數組
//也就是如果有N階,則爬法數就是solution[N-1]
int[] solution = new int[stair];
//如果只有一階,就只有一種爬法
solution[0] = 1;
//如果有兩階,就有兩種爬法
solution[1] = 2;
//依次計算3-N階的爬法
for (int i = 2; i < solution.length; i++) {
//第i階的爬法,就是i-1階和i-2階的爬法的總和
//因爲第i階,可以通過i-1階爬一步和i-2階爬兩步達到
solution[i] = solution[i-1] + solution[i-2];
}
//返回stair階最終的爬法,也就是答案數組最後一個
return solution[stair - 1];
}
/**
* 驗證爬樓梯算法
* @param args
*/
public static void main(String[] args) {
//爬樓梯(自頂向下遞歸實現):climbStairsRecursive(12): 233
System.out.println("climbStairsRecursive(12): " + climbStairsRecursive(12));
//爬樓梯(自底向上實現):climbStairsForward(12): 233
System.out.println("climbStairsForward(12): " + climbStairsForward(12));
}
}
動態規劃經典算法題2:打家劫舍
/**
* @author LiYang
* @ClassName Rob
* @Description 動態規劃:打家劫舍
* @date 2019/12/10 14:15
*/
public class Rob {
/**
* 題目描述:
*
* 你是一個專業的小偷,計劃偷竊沿街的房屋。每間房內都藏有一定的現金,
* 影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統,
* 如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。
* 給定一個代表每個房屋存放金額的非負整數數組,計算你在不觸動
* 警報裝置的情況下,能夠偷竊到的最高金額。
*
* 入參:[2,7,9,3,1]
* 答案:12
* 解答:偷竊 1 號房屋 (金額 = 2), 偷竊 3 號房屋 (金額 = 9),接着偷竊 5 號房屋
* (金額 = 1)。偷竊到的最高金額 = 2 + 9 + 1 = 12 。
*/
/**
* 動態規劃:打家劫舍(自頂向下遞歸實現)
* @param money 相連房屋金額的數組
* @param index 當下準備打劫房屋的序號
* @return 當下能打劫到的最大錢數
*/
public static int robRecursive(int[] money, int index) {
//如果是第一間房屋,就下手
if (index == 0) {
//返回第一間房屋的錢
return money[0];
}
//如果是第二間房屋
if (index == 1) {
//返回前兩間房屋中較多的錢,因爲相連會報警
//所以二者只能擇其一
return Math.max(money[0], money[1]);
}
//如果是index間房屋,則有兩種做法:下手或不下手
//如果下手,則總錢數就是當前房屋的錢數money[index]加上前index-2房間的最大錢數
//如果不下手,則總錢數就是前index-1房屋的最大錢數
//我們返回二者中較大的值,作爲前index房間的最大錢數
return Math.max((money[index] + robRecursive(money, index - 2)),
robRecursive(money, index - 1));
}
/**
* 動態規劃:打家劫舍(驅動程序)
* @param money 相連房屋金額的數組
* @return 能打劫到的最大錢數
*/
public static int robRecursive(int[] money) {
//調用遞歸方法,求出最大的錢數
return robRecursive(money, money.length - 1);
}
/**
* 動態規劃:打家劫舍(自底向上實現)
* @param money 相連房屋金額的數組
* @return 能打劫到的最大錢數
*/
public static int robForward(int[] money) {
//如果是第一間房屋,就下手
if (money.length == 1) {
//返回第一間房屋的錢
return money[0];
}
//如果是第二間房屋
if (money.length == 2) {
//返回前兩間房屋中較多的錢,因爲相連會報警
//所以二者只能擇其一
return Math.max(money[0], money[1]);
}
//答案記錄的數組
//solution[i-1],就是記錄前i個房間可偷到的最大錢數
int[] solution = new int[money.length];
//如果是第一間房屋,就下手,獲得第一間房屋的錢
solution[0] = money[0];
//如果是第二間房屋,就只能在前兩間裏面取最大值
//不可能都下手,因爲相連會報警,只能二選一
solution[1] = Math.max(money[0], money[1]);
//依次計算前第三間到所有房屋的最大錢數
for (int i = 2; i < money.length; i++) {
//如果是i間房屋,則有兩種做法:下手或不下手
//如果下手,則總錢數就是當前房屋的錢數money[i]加上前i-2房間的最大錢數
//如果不下手,則總錢數就是前i-1房屋的最大錢數
//我們返回二者中較大的值,作爲前i房間的最大錢數
solution[i] = Math.max((money[i] + solution[i-2]), (money[i-1]));
}
//返回所有房間的最大錢數,也就是答案數組最後一個
return solution[money.length - 1];
}
/**
* 驗證打家劫舍算法
* @param args
*/
public static void main(String[] args) {
//測試用例:五間房屋,各有一定數目的錢
int[] money = new int[]{2,7,9,3,1};
//打家劫舍(自頂向下遞歸實現):robRecursive: 12
System.out.println("robRecursive: " + robRecursive(money));
//打家劫舍(自底向上實現):robForward: 12
System.out.println("robForward: " + robForward(money));
}
}
動態規劃經典算法題3:三角形最長路徑
/**
* @author LiYang
* @ClassName TriangleMaxPath
* @Description 動態規劃:三角形最長路徑
* @date 2019/12/10 15:08
*/
public class TriangleMaxPath {
/**
* 7
* 3 8
* 8 1 0
* 2 7 4 4
* 4 5 2 6 5
*
* 題目描述:在上面的數字三角形中尋找一條從頂部到底部
* 的路徑,使得路徑上所經過的數字之和最大。路徑上的每
* 一步都只能往左下或右下走。只需要求出這個最大和即可,
* 不必給出具體路徑。
*
* 注意,這道題跟迪傑斯特拉(Dijkstra)算法很像(當然,
* 迪傑斯塔拉算法也是動態規劃,大家可以從我之前寫的
* 圖論算法博客中找到迪傑斯特拉算法的相關內容)。依次從
* 上往下算,如果算到還有更大的值,則更新,如果算到
* 的是相同或更小的值,則不作處理。這道題就不寫遞歸了,
* 我直接給出自底向上的算法實現
*/
/**
* 返回示例三角形二維數組,讀者可以
* 重新自定義下面的三角形二維數組
* @return 示例三角形二維數組
*/
private static int[][] triangle() {
//直接返回示例三角形二維數組
return new int[][] {
{7},
{3, 8},
{8, 1, 0},
{2, 7, 4, 4},
{4, 5, 2, 6, 5}
};
}
/**
* 返回用於計算示例三角形走到每一個點對應的最大路徑值的記錄型二維數組
* @return 對應的用於計算最大路徑值的二維數組
*/
private static int[][] maxRecord(int[][] triangle) {
//用於計算最大路徑值的二維數組
//注意,這裏二維數組第二個長度爲0,
//可以避免現在JVM就分配內存空間,
//因爲之後裏面還要重新定義一系列數組
int[][] maxRecord = new int[triangle.length][0];
//遍歷三角形二維數組
for (int i = 0; i < triangle.length; i++) {
//根據三角形每層長度,重新定義最大路徑二維數組的每一層
maxRecord[i] = new int[triangle[i].length];
}
//返回記錄型最大路徑二維數組
return maxRecord;
}
/**
* 以二維數組三角形爲依據,計算所有點的路徑最大值
* @param triangle 用於計算的三角形二維數組源數據
* @param maxRecord 返回動態規劃後的所有點的路徑最大值二維數組
*/
private static void calculateMaxPathMatrix(int[][] triangle, int[][] maxRecord) {
//第一層,最大路徑和就是本身
maxRecord[0][0] = triangle[0][0];
//從第二層開始遍歷
for (int i = 1; i < triangle.length; i++) {
//從第二層開始,頭尾的最大路徑,就是上一層的頭尾加上自身
maxRecord[i][0] = maxRecord[i-1][0] + triangle[i][0];
maxRecord[i][i] = maxRecord[i-1][i-1] + triangle[i][i];
//中間的值,就是對應上一層左上和右下的較大的值加上自身
for (int j = 1; j <= i-1; j++) {
//此處Math.max(maxRecord[i-1][j-1], maxRecord[i-1][j]),就是動態規劃
maxRecord[i][j] = Math.max(maxRecord[i-1][j-1], maxRecord[i-1][j]) + triangle[i][j];
}
}
}
/**
* 通過計算後的三角形最大路徑二維數組,計算整個三角形的最大路徑和
* @param maxRecord 計算後的三角形最大路徑二維數組
* @return 整個三角形的最大路徑和
*/
public static int calculateMaxPath(int[][] maxRecord) {
//最大路徑值
int maxPath = Integer.MIN_VALUE;
//遍歷最底層的最大路徑數組
for (int i = 0; i < maxRecord.length; i++) {
//動態記錄最大值
maxPath = Math.max(maxRecord[maxRecord.length - 1][i], maxPath);
}
//返回最終的答案,也就是示例三角形從上到下路徑的最大值
return maxPath;
}
/**
* 運行算法,求出示例三角形從上到下路徑的最大值
* @param args
*/
public static void main(String[] args) {
//得到示例三角形的二維數組
int[][] triangle = triangle();
//初始化記錄用的最大路徑二維數組
int[][] maxRecord = maxRecord(triangle);
//動態規劃算法思想求示例三角形各個點的最大路徑
calculateMaxPathMatrix(triangle, maxRecord);
//根據算出的最大路徑二維數組,求出示例三角形從上到下路徑的最大值
int maxPath = calculateMaxPath(maxRecord);
//輸出結果:示例三角形從上到下路徑的最大值:30
System.out.println("示例三角形從上到下路徑的最大值:" + maxPath);
}
}
動態規劃經典算法題4:求子數組的最大和
import java.util.Arrays;
/**
* @author LiYang
* @ClassName MaxSubArray
* @Description 動態規劃:求子數組的最大和
* @date 2019/12/10 16:19
*/
public class MaxSubArray {
/**
* 題目描述:給定一個整數數組,找出和最大的子數組,返回其和
* 例如,[1, -2, -3, 1, -5, 3, 5, -1, 2, -2, -1] 最大子數組[3, 5, -1, 2],和爲9
*/
/**
* 返回示例原數組
* @return 示例原數組
*/
public static int[] getOriginArray() {
//直接返回示例原數組
return new int[]{1, -2, -3, 1, -5, 3, 5, -1, 2, -2, -1};
}
/**
* 動態規劃:尋找和最大的子數組的和
* @param array 源數組
* @return 源數組和最大的子數組的和
*/
public static int maxSumSubArray(int[] array) {
//第一個元素,子數組和就是本身
int nStart = array[0];
//第一個元素,最大子數組和也是本身
int nAll = array[0];
//從數組第二個開始往後遍歷
for (int i = 1; i < array.length; i++) {
//在array[i]的時候,如果前面的子數組和是正數
if (nStart > 0) {
//加上就有增益,則nStart[i] = nStart[i-1] + array[i]
nStart = nStart + array[i];
//如果前面的子數組是負數或者0
} else {
//加上nStart[i-1]沒什麼用,就不加了
//nStart[i] = array[i],重新開始
nStart = array[i];
}
//在所有子數組長度中,尋找曾經出現的最大值
nAll = Math.max(nStart, nAll);
}
//返回動態規劃的最大子數組之和
return nAll;
}
/**
* 驗證尋找和最大的子數組和的算法
* @param args
*/
public static void main(String[] args) {
//得到示例的數組
int[] originArray = getOriginArray();
//根據算法,求得最大和子數組的和
int maxSum = maxSumSubArray(originArray);
//輸出結果:
//[1, -2, -3, 1, -5, 3, 5, -1, 2, -2, -1]
//最大和子數組的和是:9
System.out.println(Arrays.toString(originArray)
+ "\n最大和子數組的和是:" + maxSum);
}
}
動態規劃經典算法題5:最長公共子串長度問題
/**
* @author LiYang
* @ClassName MaxEqualsSubString
* @Description 動態規劃:最長公共子串長度問題
* @date 2019/12/11 11:37
*/
public class MaxEqualsSubString {
/**
* 題目描述:給定兩個字符串query和text,均由小寫字母組成。要求在text
* 中找出以同樣的順序連續出現在query中的最長連續字母序列的長度。
* 例如,query爲"acbac”,text爲"acaccbabb”,那麼text中的"cba”
* 爲最長的連續出現在query中的字母序列,因此,返回結果應該爲其長度3
*/
/**
* 動態規劃:求最長公共子串長度
* @param text 字符串text
* @param query 字符串query
* @return text和query的最長公共子串的長度
*/
public static int maxEqualsSubStringLength(String text, String query) {
//先獲取text和query的字符數組
char[] textChars = text.toCharArray();
char[] queryChars = query.toCharArray();
//初始化記錄二維數組,記錄從兩個字符串各自的某字符開始,
//可以形成的最長的子串的長度(動態規劃)
int[][] maxRecord = new int[textChars.length][queryChars.length];
//雙重遍歷,遍歷兩個字符串的起點字符的所有組合
for (int iText = 0; iText < textChars.length; iText++) {
for (int iQuery = 0; iQuery < queryChars.length; iQuery++) {
//如果兩個字符串在某一字符上是相等的
if (textChars[iText] == queryChars[iQuery]) {
//這個相同字符的maxRecord的長度自加1
maxRecord[iText][iQuery] ++;
//獲得兩個字符共同的上一個字符的下標
int pText = iText - 1;
int pQuery = iQuery - 1;
//如果相等的兩個字符之前都還有字符,且曾經有被記錄爲相等
while (pText >= 0 && pQuery >= 0 && maxRecord[pText][pQuery] > 0) {
//之前的字符累加1,然後下標再往前移,直到達到
//一方的開始字符,或者字符不一樣的地方,所至
//之處,都累加1,這裏也就是動態規劃的地方!
maxRecord[pText --][pQuery --] ++;
}
}
}
}
//最長公共子串的長度
int maxLength = 0;
//雙重遍歷,尋找最長公共子串的長度
for (int iText = 0; iText < textChars.length; iText++) {
for (int iQuery = 0; iQuery < queryChars.length; iQuery++) {
//動態更新最長公共子串的長度
maxLength = Math.max(maxRecord[iText][iQuery], maxLength);
}
}
//返回求出的text和query的最長公共子串的長度
return maxLength;
}
/**
* 驗證尋找最長公共子串長度的算法
* @param args
*/
public static void main(String[] args) {
//按照示例,定義text和query
String text = "acaccbabb";
String query = "acbac";
//根據算法,求得text和query的最長公共子串長度
int maxLength = maxEqualsSubStringLength(text, query);
//輸出結果:maxLength: 3
System.out.println("maxLength: " + maxLength);
}
}
動態規劃經典算法題6:最長上升子序列長度問題
import java.util.Arrays;
/**
* @author LiYang
* @ClassName LIS
* @Description 最長上升子序列的長度
* @date 2019/12/16 15:28
*/
public class LIS {
/**
* 本題旨在求解一個數組裏面的最長上升子序列的長度,不需要連續
* 如果有多個子序列長度相同且最長,則返回共同長度
* 比如:{5, 2, 8, 6, 3, 6, 9, 7}
* 最長上升子序列爲{2, 3, 6, 7} 和 {2, 3, 6, 9},長度都是4
* 則返回最長上升子序列長度4即可
*/
/**
* 求指定數組的最長增長子序列長度
* @param nums 入參的數組
* @return nums最長增長子序列長度
*/
public static int longestIncreaseSubsequenceLength(int[] nums) {
//如果數組長度爲0/1,直接返回其長度0/1
if (nums.length < 2) {
return nums.length;
}
//用於動態計算子序列長度的數組
int[] count = new int[nums.length];
//初始化所有的子序列長度爲1
Arrays.fill(count, 1);
//最長增長子序列長度,初始化爲最小值1
int maxLength = 1;
//外層循環:數組從右到左,倒數第二個開始,直到數組的開始。
//分別求以當前元素開始到結束的子數組的最大增長序列長度,
//往前面走(i--),就把數組元素與後面的比較,如果數組元素小於
//後面的元素,則該元素的加入可以擴展子序列,然後就在後面
//找到最大的子序列長度,然後+1,就是當前元素開始的子數組的
//最大增長子序列長度,直到第一個元素。然後取這裏面最大的
//子序列長度,也就求出了最長增長子序列長度
for (int i = nums.length - 2; i >= 0; i--) {
//內層循環:從外層循環的下標的下一位開始
//直到數組的末尾
for (int j = i + 1; j < nums.length; j++) {
//如果後面的數字大於當前的數字
if (nums[j] > nums[i]) {
//當前元素開始的子數組長度動態更新
count[i] = Math.max(count[i], 1 + count[j]);
}
}
//動態更新最長增長子序列長度
maxLength = Math.max(maxLength, count[i]);
}
//返回最長增長子序列長度
return maxLength;
}
/**
* 驗證最長上升子序列的長度算法
* @param args
*/
public static void main(String[] args) {
//測試數組
int[] nums = {5, 2, 8, 6, 3, 6, 9, 7};
//打印最長上升子序列的長度:4
System.out.println(longestIncreaseSubsequenceLength(nums));
}
}
好了,六個經典的動態規劃案例和詳解已給出,大家好好研究一下吧