1. 动态规划(Dynamic Programming,简称DP)是一种解决多阶段最优化问题的方法,它将问题分解成多个子问题,通过逐个求解子问题来求解原问题。
在动态规划中,每个子问题只需要求解一次,并将其结果保存起来,以便于下次需要时直接使用,避免重复计算。这种将问题分解成多个子问题并保存子问题的求解结果的方法,可以有效地提高求解问题的效率。
2. 动态规划通常包含以下几个步骤:
-
定义状态:根据问题的特点,定义状态表示问题的子问题的解。状态可以是一个或多个变量的组合,用来表示问题的不同特征或属性。状态可以是一个维度或多个维度的数组或矩阵。
-
划分阶段:将问题划分成多个阶段,每个阶段对应一个子问题。每个阶段的求解都依赖于上一个阶段的求解结果。
-
确定状态转移方程:根据子问题之间的关系,确定状态转移方程,即子问题的解与其他子问题的解之间的关系。状态转移方程描述了问题的递推关系,通常可以通过递归或迭代的方式求解。
-
确定初始状态:确定最小子问题的解,作为初始状态。初始状态是问题求解的起点,它是问题规模最小的情况下的解。
-
自底向上求解:根据状态转移方程,从最小子问题的解开始逐步求解更大规模的子问题的解。可以使用迭代的方式,从初始状态开始依次求解每个阶段的子问题,直到求解出原问题的最优解。
-
求解原问题:根据最大规模的子问题的解,求解原问题。最大规模的子问题的解即为原问题的最优解。
动态规划通常适用于具有最优子结构和重叠子问题性质的问题。最优子结构性质意味着问题的最优解可以由子问题的最优解推导出来。重叠子问题性质意味着在计算问题的最优解时,需要多次计算相同的子问题。
下面是一个使用动态规划求解斐波那契数列的例子,使用Java代码示范:
public class FibonacciDP {public static int fibonacci(int n) {// 创建一个数组用来保存子问题的解int[] dp = new int[n+1];// 初始化初始状态dp[0] = 0;dp[1] = 1;// 自底向上求解每个阶段的子问题的解for (int i = 2; i <= n; i++) {// 使用状态转移方程求解当前阶段的子问题的解dp[i] = dp[i-1] + dp[i-2];}// 返回原问题的最优解return dp[n];}public static void main(String[] args) {int n = 10;int result = fibonacci(n);System.out.println("斐波那契数列第 " + n + " 项的值为:" + result);}
}
运行上述代码,将输出斐波那契数列第 10 项的值为:55。这个例子中,我们使用动态规划的思想,将斐波那契数列的问题划分成多个子问题,并使用一个数组 dp 来保存子问题的解。通过状态转移方程 dp[i] = dp[i-1] + dp[i-2],我们逐步求解每个阶段的子问题的解,最终得到了斐波那契数列的最优解。
3. 下面介绍几个最常用的动态规划问题:
-
最长递增子序列(Longest Increasing Subsequence, LIS): 在一个给定的序列中,找到一个最长的子序列,使得子序列中的元素按照从小到大的顺序排列,并且子序列的长度最大化。
动态规划是一种解决问题的思想,它通常用于解决具有重叠子问题和最优子结构性质的问题。使用动态规划解决LIS问题的思路是,通过构建一个辅助数组dp,dp[i]表示以第i个元素结尾的最长递增子序列长度。
以下是在Java中使用动态规划解决LIS问题的示例代码:
public class LIS {public static int lengthOfLIS(int[] nums) {int[] dp = new int[nums.length]; // 创建一个dp数组,用于存储以每个元素为结尾的最长递增子序列的长度Arrays.fill(dp, 1); // 初始化dp数组为1,每个元素默认自己就是一个递增子序列int maxLen = 1; // 用于存储最长递增子序列的长度// 从索引 1 开始遍历数组for (int i = 1; i < nums.length; i++) {// 遍历索引 i 之前的所有元素for (int j = 0; j < i; j++) {// 如果当前元素 nums[i] 大于 nums[j],说明可以将 nums[i] 添加到以 nums[j] 结尾的递增子序列中if (nums[i] > nums[j]) {// 更新当前最长递增子序列的长度dp[i] = Math.max(dp[i], dp[j] + 1);}}// 更新最长递增子序列的长度maxLen = Math.max(maxLen, dp[i]);}return maxLen;}public static void main(String[] args) {int[] nums = {10, 9, 2, 5, 3, 7, 101, 18};int lisLength = lengthOfLIS(nums);System.out.println("最长递增子序列的长度: " + lisLength);} }
-
0-1背包问题(0-1 Knapsack Problem):假设有一个背包,它能够容纳一定的重量(W)和价值(V)。现在有一组物品,每个物品有不同的重量(w)和价值(v),我们需要选择一些物品放入背包中,使得放入的物品总重量不超过背包的重量限制,并且使得放入的物品总价值最大化。
动态规划是解决0-1背包问题的常用方法。它的基本思想是利用一个二维数组(dp数组)来记录在不同的背包容量和物品个数下的最大总价值。数组的行表示不同的物品个数,列表示不同的背包容量。通过填充dp数组,我们可以逐步求解出最优解。
以下是使用动态规划解决0-1背包问题的Java代码:
public class KnapsackProblem {public static int knapsack(int[] weights, int[] values, int maxWeight) {// 创建一个二维数组来存储最大总价值int[][] dp = new int[weights.length + 1][maxWeight + 1];// 填充dp数组for (int i = 1; i <= weights.length; i++) {for (int j = 1; j <= maxWeight; j++) {// 如果当前物品的重量大于当前背包容量,则不能放入背包,总价值等于上一个物品的价值if (weights[i - 1] > j) {dp[i][j] = dp[i - 1][j];}// 如果当前物品的重量小于等于当前背包容量,则可以选择放入或不放入else {// 不放入当前物品,总价值等于上一个物品的价值int notPut = dp[i - 1][j];// 放入当前物品,总价值等于上一个物品的价值加上当前物品的价值int put = dp[i - 1][j - weights[i - 1]] + values[i - 1];// 取较大值作为当前背包容量下的最大总价值dp[i][j] = Math.max(notPut, put);}}}// 返回最大总价值return dp[weights.length][maxWeight];}public static void main(String[] args) {int[] weights = {2, 3, 4, 5};int[] values = {3, 4, 5, 6};int maxWeight = 8;int maxTotalValue = knapsack(weights, values, maxWeight);System.out.println("最大总价值为:" + maxTotalValue);} }
-
最长公共子序列(Longest Common Subsequence,LCS):给定两个或多个序列中最长的公共子序列。子序列是从给定序列中删除一些元素(可能为零),并保持相对顺序的序列。
动态规划是解决LCS问题的常用方法。基本思想是:设有两个序列A和B,分别以A[i]和B[j]结尾的LCS长度为dp[i][j]。通过迭代计算dp[i][j],可以得到最长公共子序列的长度。
在Java中,我们可以使用二维数组来存储dp的值。代码如下所示:
public class LCS {public static int getLCSLength(String s1, String s2) {int m = s1.length();int n = s2.length();// 创建一个二维数组来存储dp的值int[][] dp = new int[m + 1][n + 1];// 初始化第一行和第一列为0,表示空序列for (int i = 0; i <= m; i++) {dp[i][0] = 0;}for (int j = 0; j <= n; j++) {dp[0][j] = 0;}// 根据状态转移方程计算dp的值for (int i = 1; i <= m; i++) {for (int j = 1; j <= n; j++) {if (s1.charAt(i - 1) == s2.charAt(j - 1)) {dp[i][j] = dp[i - 1][j - 1] + 1;} else {dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);}}}// 返回dp的最后一个元素,即最长公共子序列的长度return dp[m][n];}public static void main(String[] args) {String s1 = "ABCD";String s2 = "BCDE";int lcsLength = getLCSLength(s1, s2);System.out.println("最长公共子序列的长度为:" + lcsLength);} }
通过动态规划,可以将原问题划分成多个子问题,并通过保存子问题的解来避免重复计算,从而有效地求解问题。