目录
(二)最长上升子序列(Longest Increasing Subsequence)
(三)最长公共子序列(Longest Common Subsequence)
(四)最大子数组乘积(Maximum Product Subarray)
(六)最长有效括号(Longest Valid Parentheses)
(十)0-1背包问题(0/1 Knapsack Problem)
(十四)单词拆分 III(Palindrome Partitioning III)
(十八)股票买卖问题(Best Time to Buy and Sell Stock)
(十九)最佳买卖股票时机含冷冻期(Best Time to Buy and Sell Stock with Cooldown)
(二十一)从起点到终点的最小路径数(Unique Paths)
干货分享,感谢您的阅读!
一、动态规划总结
(一)基本理解
动态规划(Dynamic Programming)是一种解决多阶段决策问题的优化方法。它通常用于优化具有重叠子问题结构的问题,并通过存储子问题的解来避免重复计算,从而显著提高算法的效率。
动态规划的核心思想是将原问题分解为若干个子问题,通过解决子问题并存储子问题的解来构建原问题的解。在这个过程中,动态规划利用了子问题的重叠性质,即多个子问题可能共享相同的子问题解。通过存储子问题的解,我们可以避免重复计算,从而减少算法的时间复杂度。
下面是对动态规划思想的总结分析:
- 最优子结构:动态规划解决的问题必须具备最优子结构的性质,即问题的最优解可以通过一系列子问题的最优解来构建。这意味着原问题的解可以通过解决子问题并结合它们的解来得到。
- 子问题重叠:动态规划的关键之一是子问题重叠性质,即不同的子问题可能共享相同的子问题解。通过存储子问题的解,我们可以避免重复计算,提高算法的效率。
- 状态转移方程:动态规划的核心是建立状态转移方程,它描述了问题的当前状态与子问题之间的关系。通过定义合适的状态和状态转移方程,我们可以将原问题划分为子问题,并通过解决子问题来构建原问题的解。
- 自底向上或自顶向下:在实现动态规划算法时,可以采用自底向上(Bottom-up)或自顶向下(Top-down)的方法。自底向上方法从子问题开始逐步求解并存储子问题的解,直到求解出原问题的解。自顶向下方法则从原问题开始,通过递归的方式解决子问题,并将子问题的解存储起来,避免重复计算。
- 存储子问题解:为了避免重复计算,动态规划通常使用数据结构(如数组、哈希表)来存储子问题的解。这样可以在需要时直接查找子问题的解,而不必重新计算。
- 时间空间权衡:动态规划算法往往需要额外的存储空间来存储子问题的解,以及计算状态转移方程所需的中间结果。在实际应用中,需要根据问题的规模和要求权衡时间和空间的使用,选择合适的算法实现。
总之,动态规划是一种通过划分问题为子问题并存储子问题的解来优化求解过程的算法思想。它适用于具有最优子结构和子问题重叠性质的问题。通过定义状态和状态转移方程,动态规划能够将原问题分解为子问题,并通过解决子问题来构建原问题的解。在实现动态规划算法时,可以选择自底向上或自顶向下的方法,同时需要合理地存储子问题的解以避免重复计算。尽管动态规划算法可能需要额外的存储空间,但它能够显著提高问题的求解效率。
(二)应用分析
动态规划广泛应用于各种领域,例如:
- 最短路径和最小生成树问题:在图论中,动态规划算法被用于求解最短路径和最小生成树等问题,如Dijkstra算法和Floyd-Warshall算法。
- 背包问题:动态规划被用于解决背包问题,包括0-1背包问题、无限背包问题和多重背包问题等。
- 字符串处理:动态规划算法可以用于字符串处理问题,如最长公共子序列(LCS)和最长递增子序列(LIS)等。
- 计算机视觉:在图像处理和计算机视觉领域,动态规划算法被广泛应用于图像分割、目标跟踪和图像识别等任务。
- 组合优化问题:动态规划也可以用于解决组合优化问题,如旅行商问题(TSP)和任务分配问题等。
需要注意的是,虽然动态规划思想能够优化问题的求解过程,但并不是所有问题都适用于动态规划方法。在实际应用中,需要仔细分析问题的性质和要求,确定是否可以使用动态规划以及选择合适的动态规划策略。
二、相关高频笔试题目练习
(一)最大子序和(Maximum Subarray)
本题直接查看数组知识及编程练习总结_张彦峰ZYF的博客-CSDN博客中第十五题即可
(二)最长上升子序列(Longest Increasing Subsequence)
本题直接查看数组知识及编程练习总结_张彦峰ZYF的博客-CSDN博客中第十六题即可
(三)最长公共子序列(Longest Common Subsequence)
题目描述:给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的公共子序列是这两个字符串所共同拥有的子序列。
示例:
输入:text1 = "abcde", text2 = "ace" 输出:3
解释:最长公共子序列是 "ace",其长度为 3。
来源:力扣(LeetCode)
链接:力扣
解题思路
最优解法是使用动态规划来解决该问题。我们可以定义一个二维数组 dp,其中 dp[i][j] 表示 text1 的前 i 个字符与 text2 的前 j 个字符的最长公共子序列的长度。
- 首先,我们需要初始化 dp 数组,将第一行和第一列的值都设为 0,表示当 text1 或 text2 的长度为 0 时,最长公共子序列的长度为 0。
- 然后,我们可以使用双重循环遍历 text1 和 text2 的字符,逐个计算 dp 数组的值。对于每个字符 text1[i-1] 和 text2[j-1],有两种情况:
- 如果 text1[i-1] 等于 text2[j-1],即当前字符相等,那么 dp[i][j] = dp[i-1][j-1] + 1,表示当前字符可以加入最长公共子序列中;
- 如果 text1[i-1] 不等于 text2[j-1],即当前字符不相等,那么 dp[i][j] = max(dp[i-1][j], dp[i][j-1]),表示当前字符不能同时出现在最长公共子序列中,需要从前一个字符的最长公共子序列长度中选择最大的值。
- 最终,dp[m][n] 就是 text1 和 text2 的最长公共子序列的长度,其中 m 和 n 分别是 text1 和 text2 的长度。
代码展示
下面是使用动态规划解决最长公共子序列问题的代码示例:
- package org.zyf.javabasic.letcode.dynamicprogramming;
-
- /**
- * @author yanfengzhang
- * @description 给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
- * 一个字符串的子序列是指这样一个新的字符串:
- * 它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- * 例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
- * 两个字符串的公共子序列是这两个字符串所共同拥有的子序列。
- * 示例:
- * 输入:text1 = "abcde", text2 = "ace" 输出:3
- * 解释:最长公共子序列是 "ace",其长度为 3。
- * @date 2023/5/18 23:23
- */
- public class LongestCommonSubsequence {
- public int longestCommonSubsequence(String text1, String text2) {
- int m = text1.length();
- int n = text2.length();
- int[][] dp = new int[m + 1][n + 1];
-
- for (int i = 1; i <= m; i++) {
- for (int j = 1; j <= n; j++) {
- if (text1.charAt(i - 1) == text2.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]);
- }
- }
- }
-
- return dp[m][n];
- }
-
- public static void main(String[] args) {
- String text1 = "abcde";
- String text2 = "ace";
- int result = new LongestCommonSubsequence().longestCommonSubsequence(text1, text2);
- System.out.println("Longest Common Subsequence: " + result);
-
- text1 = "abc";
- text2 = "def";
- result = new LongestCommonSubsequence().longestCommonSubsequence(text1, text2);
- System.out.println("Longest Common Subsequence: " + result);
-
- text1 = "AGGTAB";
- text2 = "GXTXAYB";
- result = new LongestCommonSubsequence().longestCommonSubsequence(text1, text2);
- System.out.println("Longest Common Subsequence: " + result);
-
- text1 = "";
- text2 = "abcdef";
- result = new LongestCommonSubsequence().longestCommonSubsequence(text1, text2);
- System.out.println("Longest Common Subsequence: " + result);
-
- text1 = "ABC";
- text2 = "";
- result = new LongestCommonSubsequence().longestCommonSubsequence(text1, text2);
- System.out.println("Longest Common Subsequence: " + result);
-
- text1 = "";
- text2 = "";
- result = new LongestCommonSubsequence().longestCommonSubsequence(text1, text2);
- System.out.println("Longest Common Subsequence: " + result);
- }
- }
(四)最大子数组乘积(Maximum Product Subarray)
题目描述:给定一个整数数组 nums,找到数组中乘积最大的连续子数组(该子数组中至少包含一个数字),返回其乘积。
示例 1:输入: [2,3,-2,4]. 输出: 6. 解释: 子数组 [2,3] 有最大乘积 6。
示例 2:输入: [-2,0,-1] 输出: 0. 解释: 结果不能为负数,乘积为 0。
来源:力扣(LeetCode)
链接:力扣
解题思路
要找到乘积最大的连续子数组,我们需要考虑乘积的正负性质。由于乘积可能存在负数,负数乘以负数会变成正数,因此我们需要同时记录最大值和最小值。
我们可以使用动态规划的方法来解决问题。
- 定义两个变量 maxProduct 和 minProduct,分别表示当前乘积最大值和最小值。初始时,将 maxProduct 和 minProduct 都设置为数组的第一个元素 nums[0]。
- 然后,我们遍历数组,对于每个元素 nums[i],更新 maxProduct 和 minProduct 的值。如果当前元素为正数,则乘以 maxProduct 会得到更大的值;如果当前元素为负数,则乘以 minProduct 会得到更大的值。同时,还需要考虑当前元素自身是否大于 maxProduct,或者小于 minProduct,以保证连续性。
- 最后,我们遍历完整个数组后,maxProduct 中存储的就是乘积最大的连续子数组的乘积。
代码展示
下面是使用动态规划解决最大子数组乘积问题的代码示例:
- package org.zyf.javabasic.letcode.dynamicprogramming;
-
- /**
- * @author yanfengzhang
- * @description 给定一个整数数组 nums,
- * 找到数组中乘积最大的连续子数组(该子数组中至少包含一个数字),返回其乘积。
- * 示例 1:输入: [2,3,-2,4] 输出: 6 解释: 子数组 [2,3] 有最大乘积 6。
- * 示例 2:输入: [-2,0,-1] 输出: 0 解释: 结果不能为负数,乘积为 0。
- * @date 2023/5/20 23:30
- */
- public class MaximumProductSubarray {
- /**
- * 要找到乘积最大的连续子数组,我们需要考虑乘积的正负性质。
- * 由于乘积可能存在负数,负数乘以负数会变成正数,因此我们需要同时记录最大值和最小值。
- *
- * 我们可以使用动态规划的方法来解决问题。
- * 定义两个变量 maxProduct 和 minProduct,分别表示当前乘积最大值和最小值。
- * 初始时,将 maxProduct 和 minProduct 都设置为数组的第一个元素 nums[0]。
- * 然后,我们遍历数组,对于每个元素 nums[i],更新 maxProduct 和 minProduct 的值。
- * 如果当前元素为正数,则乘以 maxProduct 会得到更大的值;
- * 如果当前元素为负数,则乘以 minProduct 会得到更大的值。
- * 同时,还需要考虑当前元素自身是否大于 maxProduct,或者小于 minProduct,以保证连续性。
- * 最后,我们遍历完整个数组后,maxProduct 中存储的就是乘积最大的连续子数组的乘积。
- */
- public int maxProduct(int[] nums) {
- int maxProduct = nums[0];
- int minProduct = nums[0];
- int result = nums[0];
-
- for (int i = 1; i < nums.length; i++) {
- int tempMax = maxProduct;
- int tempMin = minProduct;
-
- maxProduct = Math.max(Math.max(tempMax * nums[i], tempMin * nums[i]), nums[i]);
- minProduct = Math.min(Math.min(tempMax * nums[i], tempMin * nums[i]), nums[i]);
-
- result = Math.max(result, maxProduct);
- }
-
- return result;
- }
-
- public static void main(String[] args) {
- int[] nums1 = {2, 3, -2, 4};
- int maxProduct1 = new MaximumProductSubarray().maxProduct(nums1);
- System.out.println("最大乘积为:" + maxProduct1);
-
- int[] nums2 = {-2, 0, -1};
- int maxProduct2 = new MaximumProductSubarray().maxProduct(nums2);
- System.out.println("最大乘积为:" + maxProduct2);
- }
- }
(五)分割整数的最大乘积(Integer Break)
题目描述:给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。返回你可以获得的最大乘积。
示例 1:输入: 2 输出: 1 解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:输入: 10 输出: 36 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
说明:你可以假设 n 不小于 2 且不大于 58。
来源:力扣(LeetCode)
链接:力扣
解题思路
最优解法采用动态规划来解决分割整数的最大乘积问题。
我们可以定义一个数组 dp,其中 dp[i] 表示将正整数 i 拆分后得到的最大乘积。
对于 i >= 2,可以将 i 拆分为两个数 j 和 i-j,然后考虑这两个数的最大乘积。
状态转移方程如下:
dp[i] = max(j * (i-j), j * dp[i-j]),其中 1 <= j < i
- 对于 j * (i-j),表示拆分为两个数,其中一个为 j,另一个为 i-j。
- 对于 j * dp[i-j],表示拆分为两个数,其中一个为 j,另一个为 i-j 的最大乘积。
最终的答案是 dp[n],其中 n 是给定的正整数。
代码展示
下面是使用动态规划解决分割整数的最大乘积问题的代码示例:
- package org.zyf.javabasic.letcode.dynamicprogramming;
-
- /**
- * @author yanfengzhang
- * @description 给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。返回你可以获得的最大乘积。
- * 示例 1:输入: 2 输出: 1 解释: 2 = 1 + 1, 1 × 1 = 1。
- * 示例 2:输入: 10 输出: 36 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
- * 说明:你可以假设 n 不小于 2 且不大于 58。
- * @date 2023/5/23 23:07
- */
- public class IntegerBreak {
- /**
- * 我们可以定义一个数组 dp,其中 dp[i] 表示将正整数 i 拆分后得到的最大乘积。对于 i >= 2,可以将 i 拆分为两个数 j 和 i-j,然后考虑这两个数的最大乘积。
- * 状态转移方程如下:dp[i] = max(j * (i-j), j * dp[i-j]),其中 1 <= j < i
- * 对于 j * (i-j),表示拆分为两个数,其中一个为 j,另一个为 i-j。
- * 对于 j * dp[i-j],表示拆分为两个数,其中一个为 j,另一个为 i-j 的最大乘积。
- * 最终的答案是 dp[n],其中 n 是给定的正整数。
- */
- public int integerBreak(int n) {
- int[] dp = new int[n + 1];
- dp[2] = 1;
-
- for (int i = 3; i <= n; i++) {
- for (int j = 1; j < i; j++) {
- // 将 i 拆分为 j 和 i-j,比较 j * (i-j) 和 j * dp[i-j] 的大小
- dp[i] = Math.max(dp[i], Math.max(j * (i - j), j * dp[i - j]));
- }
- }
-
- return dp[n];
- }
-
- public static void main(String[] args) {
- int n1 = 2;
- int maxProduct1 = new IntegerBreak().integerBreak(n1);
- // 输出:最大乘积为:1
- System.out.println("最大乘积为:" + maxProduct1);
-
- int n2 = 10;
- int maxProduct2 = new IntegerBreak().integerBreak(n2);
- // 输出:最大乘积为:36
- System.out.println("最大乘积为:" + maxProduct2);
-
- int n3 = 15;
- int maxProduct3 = new IntegerBreak().integerBreak(n3);
- // 输出:最大乘积为:243
- System.out.println("最大乘积为:" + maxProduct3);
-
- int n4 = 20;
- int maxProduct4 = new IntegerBreak().integerBreak(n4);
- // 输出:最大乘积为:1458
- System.out.println("最大乘积为:" + maxProduct4);
- }
-
- }
(六)最长有效括号(Longest Valid Parentheses)
题目描述:给定一个只包含 '(' 和 ')' 的字符串,找出最长的包含有效括号的子串的长度。
示例 1:输入: "(()" 输出: 2 解释: 最长有效括号子串为 "()"
示例 2:输入: ")()())" 输出: 4 解释: 最长有效括号子串为 "()()"
来源:力扣(LeetCode)
链接:力扣
解题思路
要找到最长的包含有效括号的子串的长度,我们可以使用栈的思想来解决。
首先,我们定义一个栈和一个变量 maxLen,栈用于记录括号的索引,初始时将 -1 放入栈中,用于处理边界情况。
然后,我们遍历字符串,对于每个字符:
- 如果是左括号 '(',将其索引入栈。
- 如果是右括号 ')',弹出栈顶元素表示当前右括号的匹配。
- 如果栈为空,表示当前右括号没有匹配的左括号,将当前右括号的索引入栈,用于处理下一个可能的有效括号子串的起始位置。
- 如果栈不为空,计算当前有效括号子串的长度,即当前右括号的索引减去栈顶元素的索引,更新 maxLen。
最后,返回 maxLen 即为最长有效括号子串的长度。
代码展示
下面是使用栈解决最长有效括号问题的代码示例:
- package org.zyf.javabasic.letcode.dynamicprogramming;
-
- import java.util.Stack;
-
- /**
- * @author yanfengzhang
- * @description 给定一个只包含 '(' 和 ')' 的字符串,找出最长的包含有效括号的子串的长度。
- * 示例 1:输入: "(()" 输出: 2 解释: 最长有效括号子串为 "()"
- * 示例 2:输入: ")()())" 输出: 4 解释: 最长有效括号子串为 "()()"
- * @date 2023/5/22 23:18
- */
- public class LongestValidParentheses {
- /**
- * 要找到最长的包含有效括号的子串的长度,我们可以使用栈的思想来解决。
- * 首先,我们定义一个栈和一个变量 maxLen,栈用于记录括号的索引,初始时将 -1 放入栈中,用于处理边界情况。
- * 然后,我们遍历字符串,对于每个字符:
- * * 如果是左括号 '(',将其索引入栈。
- * * 如果是右括号 ')',弹出栈顶元素表示当前右括号的匹配。
- * * 如果栈为空,表示当前右括号没有匹配的左括号,将当前右括号的索引入栈,用于处理下一个可能的有效括号子串的起始位置。
- * * 如果栈不为空,计算当前有效括号子串的长度,即当前右括号的索引减去栈顶元素的索引,更新 maxLen。
- * 最后,返回 maxLen 即为最长有效括号子串的长度。
- */
- public int longestValidParentheses(String s) {
- Stack
stack = new Stack<>(); - stack.push(-1);
- int maxLen = 0;
-
- for (int i = 0; i < s.length(); i++) {
- if (s.charAt(i) == '(') {
- stack.push(i);
- } else {
- stack.pop();
- if (stack.isEmpty()) {
- stack.push(i);
- } else {
- maxLen = Math.max(maxLen, i - stack.peek());
- }
- }
- }
-
- return maxLen;
- }
-
- public static void main(String[] args) {
- String s1 = "(()";
- int maxLen1 = new LongestValidParentheses().longestValidParentheses(s1);
- System.out.println("最长有效括号子串的长度为:" + maxLen1);
-
- String s2 = ")()())";
- int maxLen2 = new LongestValidParentheses().longestValidParentheses(s2);
- System.out.println("最长有效括号子串的长度为:" + maxLen2);
- }
- }
(七)不同路径(Unique Paths)
题目描述:一个机器人位于一个 m x n 网格的左上角。机器人每次只能向下或向右移动一步。机器人试图达到网格的右下角。问总共有多少条不同的路径?
例如,一个 3 x 7 的网格,机器人从左上角到右下角总共有 28 条不同的路径。
注意:m 和 n 的值均不超过 100。
示例 1:输入: m = 3, n = 2. 输出: 3
解释: 从左上角开始,总共有三条路径可以到达右下角。
- 向右 -> 向右 -> 向下
- 向右 -> 向下 -> 向右
- 向下 -> 向右 -> 向右
示例 2:输入: m = 7, n = 3 输出: 28
来源:力扣(LeetCode)
链接:力扣
解题思路
这道题可以使用动态规划来解决。我们定义一个二维数组 dp,其中 dp[i][j] 表示从起始位置到达网格 (i, j) 的不同路径数。根据题目要求,机器人每次只能向下或向右移动,因此对于网格中的每个位置 (i, j),可以从上方的位置 (i-1, j) 或左方的位置 (i, j-1) 到达。
根据上述分析,可以得到状态转移方程:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
初始条件为:当 i=0 或 j=0 时,只有一条路径可以到达,因此 dp[i][j] = 1。
最终要求的答案是 dp[m-1][n-1],其中 m 和 n 分别是网格的行数和列数。
该算法的时间复杂度为 O(m * n),空间复杂度为 O(m * n),其中 m 和 n 分别是网格的行数和列数。通过动态规划的思想,我们可以高效地计算出不同路径的数量。
代码展示
下面是使用动态规划解决不同路径问题的代码示例:
- package org.zyf.javabasic.letcode.dynamicprogramming;
-
- /**
- * @author yanfengzhang
- * @description 一个机器人位于一个 m x n 网格的左上角。机器人每次只能向下或向右移动一步。
- * 机器人试图达到网格的右下角。问总共有多少条不同的路径?
- * 例如,一个 3 x 7 的网格,机器人从左上角到右下角总共有 28 条不同的路径。
- * 注意:m 和 n 的值均不超过 100。
- * 示例 1:输入: m = 3, n = 2. 输出: 3
- * 解释: 从左上角开始,总共有三条路径可以到达右下角。
- * 1. 向右 -> 向右 -> 向下
- * 2. 向右 -> 向下 -> 向右
- * 3. 向下 -> 向右 -> 向右
- * 示例 2:输入: m = 7, n = 3 输出: 28
- * @date 2023/5/12 23:24
- */
- public class UniquePaths {
- /**
- * 这道题可以使用动态规划来解决。
- * 我们定义一个二维数组 dp,其中 dp[i][j] 表示从起始位置到达网格 (i, j) 的不同路径数。
- * 根据题目要求,机器人每次只能向下或向右移动,
- * 因此对于网格中的每个位置 (i, j),可以从上方的位置 (i-1, j) 或左方的位置 (i, j-1) 到达。
- *
- * 根据上述分析,可以得到状态转移方程:
- * dp[i][j] = dp[i-1][j] + dp[i][j-1]
- * 初始条件为:当 i=0 或 j=0 时,只有一条路径可以到达,因此 dp[i][j] = 1。
- * 最终要求的答案是 dp[m-1][n-1],其中 m 和 n 分别是网格的行数和列数。
- *
- * 该算法的时间复杂度为 O(m * n),空间复杂度为 O(m * n),其中 m 和 n 分别是网格的行数和列数。
- * 通过动态规划的思想,我们可以高效地计算出不同路径的数量。
- */
- public int uniquePaths(int m, int n) {
- int[][] dp = new int[m][n];
-
- // 初始化第一行和第一列的路径数为1
- for (int i = 0; i < m; i++) {
- dp[i][0] = 1;
- }
- for (int j = 0; j < n; j++) {
- dp[0][j] = 1;
- }
-
- // 计算其他位置的路径数
- for (int i = 1; i < m; i++) {
- for (int j = 1; j < n; j++) {
- dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
- }
- }
-
- return dp[m - 1][n - 1];
- }
-
- public static void main(String[] args) {
- int m1 = 3, n1 = 2;
- int paths1 = new UniquePaths().uniquePaths(m1, n1);
- // 输出:不同路径的数量:3
- System.out.println("不同路径的数量:" + paths1);
-
- int m2 = 7, n2 = 3;
- int paths2 = new UniquePaths().uniquePaths(m2, n2);
- // 输出:不同路径的数量:28
- System.out.println("不同路径的数量:" + paths2);
- }
-
- }
(八)最小路径和(Minimum Path Sum)
题目描述:给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和最小。
每次只能向下或向右移动一步。
示例 1:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 路径 1→3→1→1→1 的总和最小,为 7。
来源:力扣(LeetCode)
链接:力扣
解题思路
最优解法采用动态规划的思路来解决最小路径和问题。
定义一个二维数组 dp,其中 dp[i][j] 表示从起点到网格中 (i, j) 位置的最小路径和。
根据题目要求,每次只能向下或向右移动一步,因此对于位置 (i, j),可以从上方 (i-1, j) 或左边 (i, j-1) 这两个位置移动过来。因此,可以得到状态转移方程:
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
其中 grid[i][j] 表示网格中 (i, j) 位置的数字。
接下来,可以使用动态规划的方式计算出 dp 数组的值。首先初始化边界条件,即起点 (0, 0) 的最小路径和为 grid[0][0],然后从左上角开始遍历每个位置 (i, j),根据状态转移方程更新 dp[i][j] 的值。
最后,返回右下角 (m-1, n-1) 位置的值 dp[m-1][n-1],即为从左上角到右下角的最小路径和。
代码展示
下面是使用动态规划解决最小路径和问题的代码示例:
- package org.zyf.javabasic.letcode.dynamicprogramming;
-
- /**
- * @author yanfengzhang
- * @description 给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和最小。
- * 每次只能向下或向右移动一步。
- * 示例 1:
- * 输入:
- * [
- * [1,3,1],
- * [1,5,1],
- * [4,2,1]
- * ]
- * 输出: 7
- * 解释: 路径 1→3→1→1→1 的总和最小,为 7。
- * @date 2023/5/12 23:31
- */
- public class MinimumPathSum {
- /**
- * 最优解法采用动态规划的思路来解决最小路径和问题。
- * 定义一个二维数组 dp,其中 dp[i][j] 表示从起点到网格中 (i, j) 位置的最小路径和。
- * 根据题目要求,每次只能向下或向右移动一步,
- * 因此对于位置 (i, j),可以从上方 (i-1, j) 或左边 (i, j-1) 这两个位置移动过来。
- * 因此,可以得到状态转移方程:
- * dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
- * 其中 grid[i][j] 表示网格中 (i, j) 位置的数字。
- * 接下来,可以使用动态规划的方式计算出 dp 数组的值。
- * 首先初始化边界条件,即起点 (0, 0) 的最小路径和为 grid[0][0],
- * 然后从左上角开始遍历每个位置 (i, j),根据状态转移方程更新 dp[i][j] 的值。
- * 最后,返回右下角 (m-1, n-1) 位置的值 dp[m-1][n-1],即为从左上角到右下角的最小路径和。
- */
- public int minPathSum(int[][] grid) {
- int m = grid.length;
- int n = grid[0].length;
-
- int[][] dp = new int[m][n];
-
- // 初始化边界条件
- dp[0][0] = grid[0][0];
- for (int i = 1; i < m; i++) {
- dp[i][0] = dp[i - 1][0] + grid[i][0];
- }
- for (int j = 1; j < n; j++) {
- dp[0][j] = dp[0][j - 1] + grid[0][j];
- }
-
- // 计算dp数组的值
- for (int i = 1; i < m; i++) {
- for (int j = 1; j < n; j++) {
- dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
- }
- }
-
- return dp[m - 1][n - 1];
- }
-
- public static void main(String[] args) {
- int[][] grid1 = {
- {1, 3, 1},
- {1, 5, 1},
- {4, 2, 1}
- };
- int minSum1 = new MinimumPathSum().minPathSum(grid1);
- // 输出:最小路径和为:7
- System.out.println("最小路径和为:" + minSum1);
-
- int[][] grid2 = {
- {1, 2, 3},
- {4, 5, 6},
- {7, 8, 9}
- };
- int minSum2 = new MinimumPathSum().minPathSum(grid2);
- // 输出:最小路径和为:21
- System.out.println("最小路径和为:" + minSum2);
-
- int[][] grid3 = {
- {1, 2},
- {3, 4},
- {5, 6}
- };
- int minSum3 = new MinimumPathSum().minPathSum(grid3);
- // 输出:最小路径和为:13
- System.out.println("最小路径和为:" + minSum3);
- }
- }
(九)最大矩形(Maximal Rectangle)
题目描述:给定一个仅包含 0 和 1 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。
示例 1:
输入:
[
["1","0","1","0","0"],
["1","0","1","1","1"],
["1","1","1","1","1"],
["1","0","0","1","0"]
]
输出: 6
来源:力扣(LeetCode)
链接:力扣
解题思路
最优解法采用动态规划和柱状图的思想来解决最大矩形问题。
将问题转化为柱状图中的最大矩形问题,可以通过遍历每一行,将每一行作为底部,统计每一行上方连续的1的个数,然后计算以当前行为底部的最大矩形面积。
假设我们有一个一维数组 heights,其中的每个元素表示该列上方连续的1的个数。我们需要计算以当前行为底部的最大矩形面积。
- 首先,初始化一个同样大小的一维数组 left,其中的元素表示当前位置左边界的索引。对于某个位置 (i, j),如果 matrix[i][j] 是1,则 left[j] 的值为 max(left[j], curLeft),其中 curLeft 是当前行中从左边界开始连续的1的起始位置。
- 然后,初始化一个同样大小的一维数组 right,其中的元素表示当前位置右边界的索引。对于某个位置 (i, j),如果 matrix[i][j] 是1,则 right[j] 的值为 min(right[j], curRight),其中 curRight 是当前行中从右边界开始连续的1的结束位置。
- 接下来,遍历每一行,对于每个位置 (i, j),计算以当前位置为底部的矩形的面积为 (right[j] - left[j]) * heights[j],并更新最大矩形面积。
- 最后,返回最大矩形面积即可。
代码展示
下面是使用动态规划和柱状图解决最大矩形问题的代码示例:
- package org.zyf.javabasic.letcode.dynamicprogramming;
-
- import java.util.Arrays;
-
- /**
- * @author yanfengzhang
- * @description 给定一个仅包含 0 和 1 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。
- * 示例 1:
- * 输入:
- * [
- * ["1","0","1","0","0"],
- * ["1","0","1","1","1"],
- * ["1","1","1","1","1"],
- * ["1","0","0","1","0"]
- * ]
- * 输出: 6
- * @date 2023/5/21 23:37
- */
- public class MaximalRectangle {
- /**
- * 最优解法采用动态规划和柱状图的思想来解决最大矩形问题。
- * 将问题转化为柱状图中的最大矩形问题,可以通过遍历每一行,将每一行作为底部,统计每一行上方连续的1的个数,然后计算以当前行为底部的最大矩形面积。
- * 假设我们有一个一维数组 heights,其中的每个元素表示该列上方连续的1的个数。我们需要计算以当前行为底部的最大矩形面积。
- * 首先,初始化一个同样大小的一维数组 left,其中的元素表示当前位置左边界的索引。
- * 对于某个位置 (i, j),如果 matrix[i][j] 是1,则 left[j] 的值为 max(left[j], curLeft),
- * 其中 curLeft 是当前行中从左边界开始连续的1的起始位置。
- * 然后,初始化一个同样大小的一维数组 right,其中的元素表示当前位置右边界的索引。
- * 对于某个位置 (i, j),如果 matrix[i][j] 是1,则 right[j] 的值为 min(right[j], curRight),
- * 其中 curRight 是当前行中从右边界开始连续的1的结束位置。
- * 接下来,遍历每一行,对于每个位置 (i, j),
- * 计算以当前位置为底部的矩形的面积为 (right[j] - left[j]) * heights[j],并更新最大矩形面积。
- * 最后,返回最大矩形面积即可。
- */
- public int maximalRectangle(char[][] matrix) {
- if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
- return 0;
- }
-
- int m = matrix.length;
- int n = matrix[0].length;
-
- int[] heights = new int[n];
- int[] left = new int[n];
- int[] right = new int[n];
- // 初始化 right 数组为 n
- Arrays.fill(right, n);
-
- int maxArea = 0;
-
- for (int i = 0; i < m; i++) {
- int curLeft = 0, curRight = n;
-
- // 更新 heights 数组和 left 数组
- for (int j = 0; j < n; j++) {
- if (matrix[i][j] == '1') {
- heights[j]++;
- left[j] = Math.max(left[j], curLeft);
- } else {
- heights[j] = 0;
- left[j] = 0;
- curLeft = j + 1;
- }
- }
-
- // 更新 right 数组和计算最大矩形面积
- for (int j = n - 1; j >= 0; j--) {
- if (matrix[i][j] == '1') {
- right[j] = Math.min(right[j], curRight);
- maxArea = Math.max(maxArea, (right[j] - left[j]) * heights[j]);
- } else {
- right[j] = n;
- curRight = j;
- }
- }
- }
-
- return maxArea;
- }
-
- public static void main(String[] args) {
- char[][] matrix1 = {
- {'1', '0', '1', '0', '0'},
- {'1', '0', '1', '1', '1'},
- {'1', '1', '1', '1', '1'},
- {'1', '0', '0', '1', '0'}
- };
- int maxArea1 = new MaximalRectangle().maximalRectangle(matrix1);
- // 输出:最大矩形面积为:6
- System.out.println("最大矩形面积为:" + maxArea1);
-
- char[][] matrix2 = {
- {'1', '1', '1', '1'},
- {'1', '1', '1', '1'},
- {'1', '1', '1', '1'}
- };
- int maxArea2 = new MaximalRectangle().maximalRectangle(matrix2);
- // 输出:最大矩形面积为:12
- System.out.println("最大矩形面积为:" + maxArea2);
-
- char[][] matrix3 = {
- {'0', '0', '0', '0'},
- {'0', '0', '0', '0'},
- {'0', '0', '0', '0'}
- };
- int maxArea3 = new MaximalRectangle().maximalRectangle(matrix3);
- // 输出:最大矩形面积为:0
- System.out.println("最大矩形面积为:" + maxArea3);
- }
-
- }
(十)0-1背包问题(0/1 Knapsack Problem)
题目描述:给定一个背包的容量 capacity和一组物品,每个物品有对应的重量 weights 和价值 values,要求选择一些物品放入背包中,使得放入的物品总重量不超过背包容量,并且物品的总价值最大。
注意:每个物品只能选择放入背包一次(0-1属性)。
示例:
输入:
capacity = 10
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
输出:10
解释:选择重量为 3 和 5 的物品,它们的总重量为 8,总价值为 10,符合背包容量限制且价值最大。
来源:力扣(LeetCode)
链接:力扣
解题思路
0-1背包问题可以使用动态规划来解决。我们可以使用一个二维数组 dp,其中 dp[i][j] 表示在前 i 个物品中选择,且背包容量为 j 时的最大总价值。
对于每个物品,我们有两种选择:
- 不选择该物品,即背包容量保持不变,总价值也不变,即 dp[i][j] = dp[i-1][j]。
- 选择该物品,即背包容量减去当前物品的重量,并将当前物品的价值加到总价值中,即 dp[i][j] = dp[i-1][j-weights[i]] + values[i]。
综合考虑这两种选择,可以得到状态转移方程:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weights[i]] + values[i])
初始条件为当物品数量为 0 或背包容量为 0 时,总价值为 0。
最终要求的答案是 dp[n][capacity],其中 n 是物品的数量。
代码展示
下面是使用动态规划解决0-1背包问题的代码示例:
- package org.zyf.javabasic.letcode.dynamicprogramming;
-
- /**
- * @author yanfengzhang
- * @description 给定一个背包的容量 capacity和一组物品,
- * 每个物品有对应的重量 weights 和价值 values,
- * 要求选择一些物品放入背包中,使得放入的物品总重量不超过背包容量,并且物品的总价值最大。
- * 注意:每个物品只能选择放入背包一次(0-1属性)。
- * 示例:
- * 输入:
- * capacity = 10
- * weights = [2, 3, 4, 5]
- * values = [3, 4, 5, 6]
- * 输出:13
- * 解释:选择重量为 2, 3 , 5 的物品,它们的总重量为 10,总价值为 13,符合背包容量限制且价值最大。
- * @date 2023/5/24 23:44
- */
- public class KnapsackProblem {
- /**
- * 0-1背包问题可以使用动态规划来解决。我们可以使用一个二维数组 dp,其中 dp[i][j] 表示在前 i 个物品中选择,且背包容量为 j 时的最大总价值。
- * 对于每个物品,我们有两种选择:
- * 1. 不选择该物品,即背包容量保持不变,总价值也不变,即 dp[i][j] = dp[i-1][j]。
- * 2. 选择该物品,即背包容量减去当前物品的重量,并将当前物品的价值加到总价值中,即 dp[i][j] = dp[i-1][j-weights[i]] + values[i]。
- * 综合考虑这两种选择,可以得到状态转移方程:
- * dp[i][j] = max(dp[i-1][j], dp[i-1][j-weights[i]] + values[i])
- * 初始条件为当物品数量为 0 或背包容量为 0 时,总价值为 0。
- * 最终要求的答案是 dp[n][capacity],其中 n 是物品的数量。
- */
- public int knapsack(int capacity, int[] weights, int[] values) {
- int n = weights.length;
- int[][] dp = new int[n + 1][capacity + 1];
-
- // 初始化第一行和第一列为0
- for (int i = 0; i <= n; i++) {
- dp[i][0] = 0;
- }
- for (int j = 0; j <= capacity; j++) {
- dp[0][j] = 0;
- }
-
- // 计算其他位置的最大总价值
- for (int i = 1; i <= n; i++) {
- for (int j = 1; j <= capacity; j++) {
- if (weights[i - 1] <= j) {
- // 当前物品可以放入背包
- dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1]);
- } else {
- // 当前物品放不进背包
- dp[i][j] = dp[i - 1][j];
- }
- }
- }
-
- return dp[n][capacity];
- }
-
- public static void main(String[] args) {
- int capacity = 10;
- int[] weights = {2, 3, 4, 5};
- int[] values = {3, 4, 5, 6};
-
- int maxTotalValue = new KnapsackProblem().knapsack(capacity, weights, values);
- // 输出:最大总价值:13
- System.out.println("最大总价值:" + maxTotalValue);
- }
- }
(十一)编辑距离(Edit Distance)
题目描述:给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数。
可以对一个单词执行以下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例 1:输入: word1 = "horse", word2 = "ros”. 输出: 3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例 2:输入: word1 = "intention", word2 = "execution”. 输出: 5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
来源:力扣(LeetCode)
链接:力扣
解题思路
这道题可以使用动态规划来解决。我们定义一个二维数组 dp,其中 dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需要的最少操作数。
考虑到转换过程中的三种操作:插入、删除和替换。我们可以分别考虑这三种操作对应的子问题:
- 插入操作:将 word1 的前 i 个字符转换为 word2 的前 j-1 个字符,然后插入一个字符得到 word2 的前 j 个字符。
- 删除操作:将 word1 的前 i-1 个字符转换为 word2 的前 j 个字符,然后删除 word1 的第 i 个字符。
- 替换操作:将 word1 的前 i-1 个字符转换为 word2 的前 j-1 个字符,然后将 word1 的第 i 个字符替换为 word2 的第 j 个字符。
根据上述分析,可以得到状态转移方程:
- dp[i][j] = min(dp[i][j-1] + 1, // 插入操作
-
- dp[i-1][j] + 1, // 删除操作
-
- dp[i-1][j-1] + cost) // 替换操作
其中,cost 表示 word1 的第 i 个字符和 word2 的第 j 个字符是否相等。如果相等,则 cost 为 0,否则为 1。
初始条件为:当其中一个字符串为空时,转换的操作数为另一个字符串的长度。
最终要求的答案是 dp[m][n],其中 m 和 n 分别是 word1 和 word2 的长度。
代码展示
下面是使用动态规划解决编辑距离问题的代码示例:
- package org.zyf.javabasic.letcode.dynamicprogramming;
-
- /**
- * @author yanfengzhang
- * @description 给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数。
- * 可以对一个单词执行以下三种操作:
- * 1. 插入一个字符
- * 2. 删除一个字符
- * 3. 替换一个字符
- * 示例 1:输入: word1 = "horse", word2 = "ros”. 输出: 3
- * 解释:
- * horse -> rorse (将 'h' 替换为 'r')
- * rorse -> rose (删除 'r')
- * rose -> ros (删除 'e')
- * 示例 2:输入: word1 = "intention", word2 = "execution”. 输出: 5
- * 解释:
- * intention -> inention (删除 't')
- * inention -> enention (将 'i' 替换为 'e')
- * enention -> exention (将 'n' 替换为 'x')
- * exention -> exection (将 'n' 替换为 'c')
- * exection -> execution (插入 'u')
- * @date 2023/5/26 23:50
- */
- public class EditDistance {
- /**
- * 这道题可以使用动态规划来解决。我们定义一个二维数组 dp,其中 dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需要的最少操作数。
- * 考虑到转换过程中的三种操作:插入、删除和替换。我们可以分别考虑这三种操作对应的子问题:
- * 1. 插入操作:将 word1 的前 i 个字符转换为 word2 的前 j-1 个字符,然后插入一个字符得到 word2 的前 j 个字符。
- * 2. 删除操作:将 word1 的前 i-1 个字符转换为 word2 的前 j 个字符,然后删除 word1 的第 i 个字符。
- * 3. 替换操作:将 word1 的前 i-1 个字符转换为 word2 的前 j-1 个字符,然后将 word1 的第 i 个字符替换为 word2 的第 j 个字符。
- * 根据上述分析,可以得到状态转移方程:
- * dp[i][j] = min(dp[i][j-1] + 1, // 插入操作
- * dp[i-1][j] + 1, // 删除操作
- * dp[i-1][j-1] + cost) // 替换操作
- * 其中,cost 表示 word1 的第 i 个字符和 word2 的第 j 个字符是否相等。如果相等,则 cost 为 0,否则为 1。
- * 初始条件为:当其中一个字符串为空时,转换的操作数为另一个字符串的长度。
- * 最终要求的答案是 dp[m][n],其中 m 和 n 分别是 word1 和 word2 的长度。
- */
- public int minDistance(String word1, String word2) {
- int m = word1.length();
- int n = word2.length();
-
- int[][] dp = new int[m + 1][n + 1];
-
- // 初始化第一行和第一列
- for (int i = 0; i <= m; i++) {
- dp[i][0] = i;
- }
- for (int j = 0; j <= n; j++) {
- dp[0][j] = j;
- }
-
- // 计算其他位置的最小操作数
- for (int i = 1; i <= m; i++) {
- for (int j = 1; j <= n; j++) {
- int cost = (word1.charAt(i - 1) == word2.charAt(j - 1)) ? 0 : 1;
- dp[i][j] = Math.min(dp[i][j - 1] + 1, Math.min(dp[i - 1][j] + 1, dp[i - 1][j - 1] + cost));
- }
- }
-
- return dp[m][n];
- }
-
- public static void main(String[] args) {
- String word1 = "horse";
- String word2 = "ros";
- int minOps1 = new EditDistance().minDistance(word1, word2);
- // 输出:将 "horse" 转换为 "ros" 的最少操作数:3
- System.out.println("将 \"" + word1 + "\" 转换为 \"" + word2 + "\" 的最少操作数:" + minOps1);
-
- String word3 = "intention";
- String word4 = "execution";
- int minOps2 = new EditDistance().minDistance(word3, word4);
- // 输出:将 "intention" 转换为 "execution" 的最少操作数:5
- System.out.println("将 \"" + word3 + "\" 转换为 \"" + word4 + "\" 的最少操作数:" + minOps2);
- }
- }
(十二)单词拆分(Word Break)
题目描述:给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
- 字典中的单词可以重复使用。
- 你可以假设字典中没有重复的单词。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"] 输出: true
解释: "leetcode" 可以被拆分为 "leet" 和 "code" 这两个单词。
示例 2:输入: s = "applepenapple", wordDict = ["apple", "pen”]. 输出: true
解释: "applepenapple" 可以被拆分为 "apple", "pen" 和 "apple" 这三个单词。
注意你可以重复使用字典中的单词。
来源:力扣(LeetCode)
链接:力扣
解题思路
单词拆分问题可以使用动态规划来解决。
- 我们可以使用一个布尔数组 dp,其中 dp[i] 表示字符串 s 的前 i 个字符是否可以被拆分为字典中的单词。
- 初始条件为 dp[0] = true,表示空字符串可以被拆分。
- 然后,我们可以通过遍历字符串的每个位置 i,并在每个位置检查是否存在可拆分的位置 j(j 小于 i),使得字符串从位置 j 到位置 i 的子串在字典中存在。如果存在这样的 j,且 dp[j] 为 true,则说明从位置 j 到位置 i 的子串可以被拆分为字典中的单词,因此将 dp[i] 设置为 true。
- 最终,dp[s.length()] 即为判断字符串 s 是否可以被拆分的结果。
该算法的时间复杂度为 O(n^2),其中 n 是字符串 s 的长度。通过动态规划的思想,我们可以在一次遍历中计算出是否可以将字符串拆分为字典中的单词。
代码展示
下面是使用动态规划解决单词拆分问题的代码示例:
- package org.zyf.javabasic.letcode.dynamicprogramming;
-
- import java.util.Arrays;
- import java.util.List;
-
- /**
- * @author yanfengzhang
- * @description 给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
- * 说明:
- * * 字典中的单词可以重复使用。
- * * 你可以假设字典中没有重复的单词。
- * 示例 1:
- * 输入: s = "leetcode", wordDict = ["leet", "code"] 输出: true
- * 解释: "leetcode" 可以被拆分为 "leet" 和 "code" 这两个单词。
- * 示例 2:输入: s = "applepenapple", wordDict = ["apple", "pen”]. 输出: true
- * 解释: "applepenapple" 可以被拆分为 "apple", "pen" 和 "apple" 这三个单词。
- * 注意你可以重复使用字典中的单词。
- * @date 2023/5/11 23:55
- */
- public class WordBreak {
- /**
- * 单词拆分问题可以使用动态规划来解决。我们可以使用一个布尔数组 dp,其中 dp[i] 表示字符串 s 的前 i 个字符是否可以被拆分为字典中的单词。
- * 初始条件为 dp[0] = true,表示空字符串可以被拆分。
- * 然后,我们可以通过遍历字符串的每个位置 i,并在每个位置检查是否存在可拆分的位置 j(j 小于 i),使得字符串从位置 j 到位置 i 的子串在字典中存在。
- * 如果存在这样的 j,且 dp[j] 为 true,则说明从位置 j 到位置 i 的子串可以被拆分为字典中的单词,因此将 dp[i] 设置为 true。
- * 最终,dp[s.length()] 即为判断字符串 s 是否可以被拆分的结果。
- *
- * 该算法的时间复杂度为 O(n^2),其中 n 是字符串 s 的长度。
- * 通过动态规划的思想,我们可以在一次遍历中计算出是否可以将字符串拆分为字典中的单词。
- */
- public boolean wordBreak(String s, List
wordDict) { - int n = s.length();
- boolean[] dp = new boolean[n + 1];
- dp[0] = true;
-
- for (int i = 1; i <= n; i++) {
- for (int j = 0; j < i; j++) {
- // 检查位置 j 到位置 i 的子串是否在字典中
- if (dp[j] && wordDict.contains(s.substring(j, i))) {
- dp[i] = true;
- break;
- }
- }
- }
-
- return dp[n];
- }
-
- public static void main(String[] args) {
- String s1 = "leetcode";
- List
wordDict1 = Arrays.asList("leet", "code"); - boolean canBreak1 = new WordBreak().wordBreak(s1, wordDict1);
- // 输出:是否可以拆分:true
- System.out.println("是否可以拆分:" + canBreak1);
-
- String s2 = "applepenapple";
- List
wordDict2 = Arrays.asList("apple", "pen"); - boolean canBreak2 = new WordBreak().wordBreak(s2, wordDict2);
- // 输出:是否可以拆分:true
- System.out.println("是否可以拆分:" + canBreak2);
-
- String s3 = "catsandog";
- List
wordDict3 = Arrays.asList("cats", "dog", "sand", "and", "cat"); - boolean canBreak3 = new WordBreak().wordBreak(s3, wordDict3);
- // 输出:是否可以拆分:false
- System.out.println("是否可以拆分:" + canBreak3);
- }
-
- }
(十三)单词拆分 II(Word Break II)
题目描述:给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,为 s 添加空格,使其成为一个句子中的单词拼接,返回所有可能的句子。
说明:
- 拆分时可以重复使用字典中的单词。
- 你可以假设字典中没有重复的单词。
示例 1:
输入:
s = "catsanddog"
wordDict = ["cat", "cats", "and", "sand", "dog"]
输出:
[
"cats and dog",
"cat sand dog"
]
示例 2:
输入:
s = "pineapplepenapple"
wordDict = ["apple", "pen", "applepen", "pine", "pineapple"]
输出:
[
"pine apple pen apple",
"pineapple pen apple",
"pine applepen apple"
]
解释: 注意你可以重复使用字典中的单词。
示例 3:
输入:
s = "catsandog"
wordDict = ["cats", "dog", "sand", "and", "cat"]
输出:
[]
来源:力扣(LeetCode)
链接:力扣
解题思路
最优解法采用动态规划和回溯的结合来解决单词拆分 II 问题。
- 首先使用动态规划判断给定的字符串 s 是否可以被拆分成字典中的单词。可以使用一个布尔数组 dp,其中 dp[i] 表示从字符串的开头到第 i 个字符(不包含第 i 个字符)的子串是否可以被拆分成字典中的单词。初始化时,将 dp[0] 设置为 true,表示空字符串可以被拆分。
- 然后,使用回溯算法生成所有可能的句子。对于给定的字符串 s,从开头开始逐个字符进行遍历。对于每个遍历到的位置 i,如果 dp[i] 为 true,则说明从开头到第 i 个字符的子串可以被拆分成字典中的单词。接下来,需要递归生成剩余部分的句子,并将其与当前单词拼接起来形成完整的句子。
为了避免重复计算,可以使用一个哈希表 memo 来存储已经计算过的位置,以及对应的生成的句子列表。
代码展示
下面是使用动态规划和回溯解决单词拆分 II 问题的代码示例:
- package org.zyf.javabasic.letcode.dynamicprogramming;
-
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.HashMap;
- import java.util.HashSet;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
-
- /**
- * @author yanfengzhang
- * @description 给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,为 s 添加空格,使其成为一个句子中的单词拼接,返回所有可能的句子。
- * 说明:
- * * 拆分时可以重复使用字典中的单词。
- * * 你可以假设字典中没有重复的单词。
- * 示例 1:
- * 输入:
- * s = "catsanddog"
- * wordDict = ["cat", "cats", "and", "sand", "dog"]
- * 输出:
- * [
- * "cats and dog",
- * "cat sand dog"
- * ]
- * 示例 2:
- * 输入:
- * s = "pineapplepenapple"
- * wordDict = ["apple", "pen", "applepen", "pine", "pineapple"]
- * 输出:
- * [
- * "pine apple pen apple",
- * "pineapple pen apple",
- * "pine applepen apple"
- * ]
- * 解释: 注意你可以重复使用字典中的单词。
- * 示例 3:
- * 输入:
- * s = "catsandog"
- * wordDict = ["cats", "dog", "sand", "and", "cat"]
- * 输出:
- * []
- * @date 2023/5/11 23:08
- */
- public class WordBreakII {
- /**
- * 最优解法采用动态规划和回溯的结合来解决单词拆分 II 问题。
- * 首先使用动态规划判断给定的字符串 s 是否可以被拆分成字典中的单词。
- * 可以使用一个布尔数组 dp,其中 dp[i] 表示从字符串的开头到第 i 个字符(不包含第 i 个字符)的子串是否可以被拆分成字典中的单词。
- * 初始化时,将 dp[0] 设置为 true,表示空字符串可以被拆分。
- * 然后,使用回溯算法生成所有可能的句子。
- * 对于给定的字符串 s,从开头开始逐个字符进行遍历。
- * 对于每个遍历到的位置 i,如果 dp[i] 为 true,则说明从开头到第 i 个字符的子串可以被拆分成字典中的单词。
- * 接下来,需要递归生成剩余部分的句子,并将其与当前单词拼接起来形成完整的句子。
- * 为了避免重复计算,可以使用一个哈希表 memo 来存储已经计算过的位置,以及对应的生成的句子列表。
- */
- public List
wordBreak(String s, List wordDict) { - Set
wordSet = new HashSet<>(wordDict); - Map
> memo = new HashMap<>(); -
- return backtrack(s, 0, wordSet, memo);
- }
-
- private List
backtrack(String s, int start, Set wordSet, Map> memo) { - if (memo.containsKey(start)) {
- return memo.get(start);
- }
-
- List
sentences = new ArrayList<>(); - if (start == s.length()) {
- sentences.add("");
- }
-
- for (int end = start + 1; end <= s.length(); end++) {
- String word = s.substring(start, end);
- if (wordSet.contains(word)) {
- List
nextSentences = backtrack(s, end, wordSet, memo); - for (String nextSentence : nextSentences) {
- sentences.add(word + (nextSentence.isEmpty() ? "" : " ") + nextSentence);
- }
- }
- }
-
- memo.put(start, sentences);
- return sentences;
- }
-
- public static void main(String[] args) {
- String s1 = "catsanddog";
- List
wordDict1 = Arrays.asList("cat", "cats", "and", "sand", "dog"); - List
sentences1 = new WordBreakII().wordBreak(s1, wordDict1); - System.out.println("所有可能的句子:");
- for (String sentence : sentences1) {
- System.out.println(sentence);
- }
- System.out.println();
-
- String s2 = "pineapplepenapple";
- List
wordDict2 = Arrays.asList("apple", "pen", "applepen", "pine", "pineapple"); - List
sentences2 = new WordBreakII().wordBreak(s2, wordDict2); - System.out.println("所有可能的句子:");
- for (String sentence : sentences2) {
- System.out.println(sentence);
- }
- System.out.println();
-
- String s3 = "catsandog";
- List
wordDict3 = Arrays.asList("cats", "dog", "sand", "and", "cat"); - List
sentences3 = new WordBreakII().wordBreak(s3, wordDict3); - System.out.println("所有可能的句子:");
- for (String sentence : sentences3) {
- System.out.println(sentence);
- }
- }
- }
(十四)单词拆分 III(Palindrome Partitioning III)
题目描述:给定一个字符串 s,将 s 分割成若干个非空子串,使得每个子串都是回文串。计算将字符串 s 分割成回文分割所需的最小分割次数。
示例 1:输入: s = "aab”. 输出: 1
解释: 将字符串 "aab" 分割成 ["aa","b"],其中 "aa" 是回文串,所以只需要进行一次分割。
示例 2:输入: s = "leetcode" 输出: 2
解释: 将字符串 "leetcode" 分割成 ["leet","code"],其中 "leet" 和 "code" 都是回文串,所以需要进行两次分割。
来源:力扣(LeetCode)
链接:力扣
解题思路
这个问题可以使用动态规划来解决。首先,我们可以使用动态规划来计算任意子串是否是回文串,以便我们可以在后续的计算中使用。
- 我们定义一个二维数组 dp,其中 dp[i][j] 表示将子串 s[i:j] 分割成回文子串所需的最小分割次数。
- 初始化数组 dp 为无穷大,除了 dp[i][i] = 0,表示单个字符是回文串,不需要进行分割。
- 然后,我们从长度为 2 的子串开始,逐步增加子串的长度,进行动态规划计算。对于每个子串 s[i:j],我们枚举一个分割点 k,将子串分割成两部分 s[i:k] 和 s[k+1:j],如果 s[i:k] 是回文串,则可以将其分割为一个回文子串,然后在 dp[i][j] 的基础上加 1,得到 dp[i][k] + dp[k+1][j] + 1。
- 最后,dp[0][n-1] 就是将整个字符串分割成回文子串所需的最小分割次数,其中 n 是字符串的长度。
代码展示
下面是使用动态规划解决单词拆分 III问题的代码示例:
- package org.zyf.javabasic.letcode.dynamicprogramming;
-
- import java.util.Arrays;
-
- /**
- * @author yanfengzhang
- * @description 给定一个字符串 s,将 s 分割成若干个非空子串,使得每个子串都是回文串。
- * 计算将字符串 s 分割成回文分割所需的最小分割次数。
- * 示例 1:输入: s = "aab”. 输出: 1
- * 解释: 将字符串 "aab" 分割成 ["aa","b"],其中 "aa" 是回文串,所以只需要进行一次分割。
- * 示例 2:输入: s = "leetcode" 输出: 2
- * 解释: 将字符串 "leetcode" 分割成 ["leet","code"],其中 "leet" 和 "code" 都是回文串,所以需要进行两次分割。
- * @date 2023/5/17 23:17
- */
- public class PalindromePartitioningIII {
- /**
- * 这个问题可以使用动态规划来解决。首先,我们可以使用动态规划来计算任意子串是否是回文串,以便我们可以在后续的计算中使用。
- * 我们定义一个二维数组 dp,其中 dp[i][j] 表示将子串 s[i:j] 分割成回文子串所需的最小分割次数。
- * 初始化数组 dp 为无穷大,除了 dp[i][i] = 0,表示单个字符是回文串,不需要进行分割。
- * 然后,我们从长度为 2 的子串开始,逐步增加子串的长度,进行动态规划计算。
- * 对于每个子串 s[i:j],我们枚举一个分割点 k,将子串分割成两部分 s[i:k] 和 s[k+1:j],如果 s[i:k] 是回文串,则可以将其分割为一个回文子串,
- * 然后在 dp[i][j] 的基础上加 1,得到 dp[i][k] + dp[k+1][j] + 1。
- * 最后,dp[0][n-1] 就是将整个字符串分割成回文子串所需的最小分割次数,其中 n 是字符串的长度。
- */
- public int palindromePartition(String s, int k) {
- int n = s.length();
- int[][] dp = new int[n][n];
-
- for (int len = 2; len <= n; len++) {
- for (int i = 0; i + len <= n; i++) {
- int j = i + len - 1;
- dp[i][j] = getPalindromeCount(s, i, j);
- }
- }
-
- int[][] dp2 = new int[n + 1][k + 1];
- for (int i = 0; i <= n; i++) {
- Arrays.fill(dp2[i], Integer.MAX_VALUE / 2);
- }
- dp2[0][0] = 0;
-
- for (int i = 1; i <= n; i++) {
- for (int j = 1; j <= Math.min(i, k); j++) {
- for (int p = 0; p < i; p++) {
- dp2[i][j] = Math.min(dp2[i][j], dp2[p][j - 1] + dp[p][i - 1]);
- }
- }
- }
-
- return dp2[n][k];
- }
-
- private int getPalindromeCount(String s, int i, int j) {
- int count = 0;
- while (i < j) {
- if (s.charAt(i) != s.charAt(j)) {
- count++;
- }
- i++;
- j--;
- }
- return count;
- }
-
- public static void main(String[] args) {
- String s1 = "aab";
- int k1 = 1;
- int minCuts1 = new PalindromePartitioningIII().palindromePartition(s1, k1);
- System.out.println("将字符串 " + s1 + " 分割成回文子串所需的最小分割次数为:" + minCuts1);
-
- String s2 = "leetcode";
- int k2 = 2;
- int minCuts2 = new PalindromePartitioningIII().palindromePartition(s2, k2);
- System.out.println("将字符串 " + s2 + " 分割成回文子串所需的最小分割次数为:" + minCuts2);
- }
- }
(十五)爬楼梯(Climbing Stairs)
题目描述:假设你正在爬楼梯。需要 n 阶才能到达楼顶。每次你可以爬 1 或 2 个台阶。编写一个函数,计算你到达楼顶的不同方式的数量。
注意:给定的 n 是一个正整数。
示例 1:输入: 2 输出: 2
解释: 有两种方式可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
示例 2:输入: 3 输出: 3
解释: 有三种方式可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
来源:力扣(LeetCode)
链接:力扣
解题思路
这道题可以使用动态规划来解决。我们定义一个数组 dp,其中 dp[i] 表示爬到第 i 阶楼梯的不同方式的数量。根据题目要求,爬到第 i 阶楼梯的方式只有两种:
- 从第 i-1 阶楼梯爬一步到达:dp[i-1]
- 从第 i-2 阶楼梯爬两步到达:dp[i-2]
所以,dp[i] = dp[i-1] + dp[i-2],即爬到第 i 阶楼梯的不同方式数量等于爬到第 i-1 阶和第 i-2 阶楼梯方式数量之和。
初始条件是 dp[0] = 1(表示不爬任何台阶)和 dp[1] = 1(表示爬一阶台阶)。
最终要求的答案是 dp[n],其中 n 表示楼梯的总阶数。
该算法的时间复杂度为 O(n),空间复杂度为 O(n),其中 n 是楼梯的总阶数。通过动态规划的思想,我们可以高效地计算出爬楼梯的不同方式的数量。
代码展示
下面是使用动态规划解决爬楼梯问题的代码示例:
- package org.zyf.javabasic.letcode.dynamicprogramming;
-
- /**
- * @author yanfengzhang
- * @description 假设你正在爬楼梯。需要 n 阶才能到达楼顶。每次你可以爬 1 或 2 个台阶。
- * 编写一个函数,计算你到达楼顶的不同方式的数量。
- * 注意:给定的 n 是一个正整数。
- * 示例 1:输入: 2 输出: 2
- * 解释: 有两种方式可以爬到楼顶。
- * 1. 1 阶 + 1 阶
- * 2. 2 阶
- * 示例 2:输入: 3 输出: 3
- * 解释: 有三种方式可以爬到楼顶。
- * 1. 1 阶 + 1 阶 + 1 阶
- * 2. 1 阶 + 2 阶
- * 3. 2 阶 + 1 阶
- * @date 2023/5/27 23:36
- */
- public class ClimbingStairs {
- /**
- * 这道题可以使用动态规划来解决。我们定义一个数组 dp,其中 dp[i] 表示爬到第 i 阶楼梯的不同方式的数量。
- * 根据题目要求,爬到第 i 阶楼梯的方式只有两种:
- * 1. 从第 i-1 阶楼梯爬一步到达:dp[i-1]
- * 2. 从第 i-2 阶楼梯爬两步到达:dp[i-2]
- * 所以,dp[i] = dp[i-1] + dp[i-2],即爬到第 i 阶楼梯的不同方式数量等于爬到第 i-1 阶和第 i-2 阶楼梯方式数量之和。
- * 初始条件是 dp[0] = 1(表示不爬任何台阶)和 dp[1] = 1(表示爬一阶台阶)。
- * 最终要求的答案是 dp[n],其中 n 表示楼梯的总阶数。
- */
- public int climbStairs(int n) {
- if (n <= 1) {
- return 1;
- }
-
- // 创建一个数组来保存每个楼梯阶数对应的不同爬法数量
- int[] dp = new int[n + 1];
-
- // 初始条件:第0阶和第1阶都只有一种爬法
- dp[0] = 1;
- dp[1] = 1;
-
- // 通过动态规划计算每个楼梯阶数对应的不同爬法数量
- for (int i = 2; i <= n; i++) {
- dp[i] = dp[i - 1] + dp[i - 2];
- }
-
- // 返回爬到第n阶楼梯的不同爬法数量作为结果
- return dp[n];
- }
-
- public static void main(String[] args) {
- System.out.println("楼顶数为2" + ",你到达楼顶的不同方式的数量为" + new ClimbingStairs().climbStairs(2));
- System.out.println("楼顶数为3" + ",你到达楼顶的不同方式的数量为" + new ClimbingStairs().climbStairs(3));
- System.out.println("楼顶数为4" + ",你到达楼顶的不同方式的数量为" + new ClimbingStairs().climbStairs(4));
- System.out.println("楼顶数为5" + ",你到达楼顶的不同方式的数量为" + new ClimbingStairs().climbStairs(5));
- System.out.println("楼顶数为6" + ",你到达楼顶的不同方式的数量为" + new ClimbingStairs().climbStairs(6));
- System.out.println("楼顶数为34" + ",你到达楼顶的不同方式的数量为" + new ClimbingStairs().climbStairs(34));
- }
- }
(十六)打家劫舍(House Robber)
题目描述:你是一个专业的小偷,计划偷窃一条街道上的房屋。每间房屋都存放着特定金额的财物,但相邻的房屋装有相互连通的防盗系统,如果同时选中了相邻的两间房屋进行偷窃,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,你需要计算在不触发警报的情况下,能够偷窃到的最高金额。
示例 1:输入: [1,2,3,1] 输出: 4
解释: 偷窃第1间和第3间房屋,金额总计为 1 + 3 = 4。
示例 2:输入: [2,7,9,3,1] 输出: 12
解释: 偷窃第1间、第3间和第5间房屋,金额总计为 2 + 9 + 1 = 12。
来源:力扣(LeetCode)
链接:力扣
解题思路
这道题可以使用动态规划来解决。我们定义一个数组 dp,其中 dp[i] 表示偷窃到第 i 间房屋时能够获得的最高金额。根据题目要求,我们不能选择相邻的房屋进行偷窃,因此对于第 i 间房屋,我们有两种选择:
- 偷窃第 i 间房屋:那么最高金额为 dp[i-2] + nums[i],即第 i-2 间房屋的最高金额加上当前房屋的金额。
- 不偷窃第 i 间房屋:那么最高金额为 dp[i-1],即前一间房屋的最高金额。
综上,我们可以得到状态转移方程:
dp[i] = max(dp[i-2] + nums[i], dp[i-1])
初始条件是 dp[0] = nums[0] 和 dp[1] = max(nums[0], nums[1])。
最终要求的答案是 dp[n-1],其中 n 是房屋的数量。
该算法的时间复杂度为 O(n),空间复杂度为 O(n),其中 n 是房屋的数量。通过动态规划的思想,我们可以高效地计算出能够偷窃到的最高金额。
代码展示
下面是使用动态规划解决打家劫舍问题的代码示例:
- package org.zyf.javabasic.letcode.dynamicprogramming;
-
- /**
- * @author yanfengzhang
- * @description 你是一个专业的小偷,计划偷窃一条街道上的房屋。
- * 每间房屋都存放着特定金额的财物,但相邻的房屋装有相互连通的防盗系统,
- * 如果同时选中了相邻的两间房屋进行偷窃,系统会自动报警。
- * 给定一个代表每个房屋存放金额的非负整数数组,你需要计算在不触发警报的情况下,能够偷窃到的最高金额。
- * 示例 1:输入: [1,2,3,1] 输出: 4
- * 解释: 偷窃第1间和第3间房屋,金额总计为 1 + 3 = 4。
- * 示例 2:输入: [2,7,9,3,1] 输出: 12
- * 解释: 偷窃第1间、第3间和第5间房屋,金额总计为 2 + 9 + 1 = 12。
- * @date 2023/5/27 23:17
- */
- public class HouseRobber {
- /**
- * 这道题可以使用动态规划来解决。
- * 我们定义一个数组 dp,其中 dp[i] 表示偷窃到第 i 间房屋时能够获得的最高金额。
- * 根据题目要求,我们不能选择相邻的房屋进行偷窃,因此对于第 i 间房屋,我们有两种选择:
- * 1. 偷窃第 i 间房屋:那么最高金额为 dp[i-2] + nums[i],即第 i-2 间房屋的最高金额加上当前房屋的金额。
- * 2. 不偷窃第 i 间房屋:那么最高金额为 dp[i-1],即前一间房屋的最高金额。
- * 综上,我们可以得到状态转移方程:
- * dp[i] = max(dp[i-2] + nums[i], dp[i-1])
- * 初始条件是 dp[0] = nums[0] 和 dp[1] = max(nums[0], nums[1])。
- * 最终要求的答案是 dp[n-1],其中 n 是房屋的数量。
- *
- * 该算法的时间复杂度为 O(n),空间复杂度为 O(n),其中 n 是房屋的数量。
- * 通过动态规划的思想,我们可以高效地计算出能够偷窃到的最高金额。
- */
- public int rob(int[] nums) {
- if (nums == null || nums.length == 0) {
- return 0;
- }
-
- int n = nums.length;
- if (n == 1) {
- return nums[0];
- }
-
- // 创建一个数组来保存偷窃到每个房屋时能够获得的最高金额
- int[] dp = new int[n];
-
- // 初始条件:只有一间房屋时,能够偷窃的最高金额为该房屋的金额
- dp[0] = nums[0];
-
- // 当有两间房屋时,偷窃其中金额较大的房屋
- dp[1] = Math.max(nums[0], nums[1]);
-
- // 通过动态规划计算每个房屋对应的最高金额
- for (int i = 2; i < n; i++) {
- // 在偷窃第 i 间房屋时,有两种选择:偷窃第 i-2 间房屋+当前房屋金额 或 不偷窃第 i-1 间房屋,取较大值
- dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
- }
-
- // 返回偷窃到最后一间房屋时能够获得的最高金额
- return dp[n - 1];
- }
-
- public static void main(String[] args) {
- int[] nums1 = new int[]{1, 2, 3, 1};
- System.out.println(new HouseRobber().rob(nums1));
-
- int[] nums2 = new int[]{2, 7, 9, 3, 1};
- System.out.println(new HouseRobber().rob(nums2));
- }
- }
(十七)强盗抢劫环形街区(House Robber II)
题目描述:你是一个专业的强盗,计划抢劫街区的房屋。每个房屋都存放着特定金额的钱。你面临的唯一约束条件是,由于安全系统的存在,相邻的房屋装有连接的警报系统。换句话说,如果两个相邻的房屋在同一晚上被打劫,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报系统的情况下,能够抢劫到的最高金额。
注意:这是一个环形街区,意味着第一个房屋和最后一个房屋是相邻的。
示例 1:输入: [2,3,2]. 输出: 3
解释: 你可以选择从第一间房屋抢劫 2 元,然后跳过第二间房屋,接着抢劫第三间房屋 1 元。因此,抢劫到的最高金额为 3 元。
示例 2:输入: [1,2,3,1] 输出: 4
解释: 你可以选择抢劫第一间房屋 1 元和第三间房屋 3 元,抢劫到的最高金额为 4 元。注意,你不能选择抢劫第一间房屋和最后一间房屋,因为它们是相邻的。
来源:力扣(LeetCode)
链接:力扣
解题思路
解决强盗抢劫环形街区问题可以采用动态规划的思路。由于首尾房屋相邻,因此最后一间房屋只能选择抢劫或不抢劫。这样,原问题可以分解为两个子问题:
- 不抢劫最后一间房屋,即考虑抢劫第一间房屋到倒数第二间房屋的情况。
- 不抢劫第一间房屋,即考虑抢劫第二间房屋到最后一间房屋的情况。
对于这两个子问题,可以使用类似于"打家劫舍"问题的动态规划方法来求解。
具体步骤如下:
- 对于子问题1,创建一个动态规划数组 dp1,其中 dp1[i] 表示从第一间房屋到第 i 间房屋(不包括最后一间房屋)能够抢劫到的最高金额。初始化 dp1[0] = nums[0],dp1[1] = max(nums[0], nums[1])。
- 对于子问题2,创建一个动态规划数组 dp2,其中 dp2[i] 表示从第二间房屋到第 i 间房屋能够抢劫到的最高金额。初始化 dp2[0] = 0,dp2[1] = nums[1]。
- 对于子问题1,从第三间房屋开始遍历,计算每个位置的最高金额:dp1[i] = max(dp1[i-2] + nums[i], dp1[i-1])。
- 对于子问题2,从第三间房屋开始遍历,计算每个位置的最高金额:dp2[i] = max(dp2[i-2] + nums[i+1], dp2[i-1])。
- 最后的结果为 max(dp1[n-2], dp2[n-1]),其中 n 为房屋数量。
代码展示
下面是使用动态规划解决强盗抢劫环形街区问题的代码示例:
- package org.zyf.javabasic.letcode.dynamicprogramming;
-
- /**
- * @author yanfengzhang
- * @description 你是一个专业的强盗,计划抢劫街区的房屋。每个房屋都存放着特定金额的钱。
- * 你面临的唯一约束条件是,由于安全系统的存在,相邻的房屋装有连接的警报系统。
- * 换句话说,如果两个相邻的房屋在同一晚上被打劫,系统会自动报警。
- * 给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报系统的情况下,能够抢劫到的最高金额。
- * 注意:这是一个环形街区,意味着第一个房屋和最后一个房屋是相邻的。
- * 示例 1:输入: [2,3,2]. 输出: 3
- * 解释: 你可以选择从第一间房屋抢劫 2 元,然后跳过第二间房屋,接着抢劫第三间房屋 1 元。因此,抢劫到的最高金额为 3 元。
- * 示例 2:输入: [1,2,3,1] 输出: 4
- * 解释: 你可以选择抢劫第一间房屋 1 元和第三间房屋 3 元,抢劫到的最高金额为 4 元。
- * 注意,你不能选择抢劫第一间房屋和最后一间房屋,因为它们是相邻的。
- * @date 2023/5/27 23:25
- */
- public class HouseRobberII {
- /**
- * 对于这两个子问题,可以使用类似于"打家劫舍"问题的动态规划方法来求解。
- * 具体步骤如下:
- * 1. 对于子问题1,创建一个动态规划数组 dp1,其中 dp1[i] 表示从第一间房屋到第 i 间房屋(不包括最后一间房屋)能够抢劫到的最高金额。
- * 初始化 dp1[0] = nums[0],dp1[1] = max(nums[0], nums[1])。
- * 2. 对于子问题2,创建一个动态规划数组 dp2,其中 dp2[i] 表示从第二间房屋到第 i 间房屋能够抢劫到的最高金额。
- * 初始化 dp2[0] = 0,dp2[1] = nums[1]。
- * 3. 对于子问题1,从第三间房屋开始遍历,计算每个位置的最高金额:dp1[i] = max(dp1[i-2] + nums[i], dp1[i-1])。
- * 4. 对于子问题2,从第三间房屋开始遍历,计算每个位置的最高金额:dp2[i] = max(dp2[i-2] + nums[i+1], dp2[i-1])。
- * 5. 最后的结果为 max(dp1[n-2], dp2[n-1]),其中 n 为房屋数量。
- */
- public int rob(int[] nums) {
- int n = nums.length;
- if (n == 1) {
- return nums[0];
- }
-
- // 分为两个子问题:不抢劫最后一间房屋和不抢劫第一间房屋
- int max1 = robHelper(nums, 0, n - 2);
- int max2 = robHelper(nums, 1, n - 1);
-
- // 返回两个子问题的较大值
- return Math.max(max1, max2);
- }
-
- // 求解单个子问题的最大金额
- private int robHelper(int[] nums, int start, int end) {
- int n = end - start + 1;
- if (n == 0) {
- return 0;
- }
- if (n == 1) {
- return nums[start];
- }
-
- int[] dp = new int[n];
- dp[0] = nums[start];
- dp[1] = Math.max(nums[start], nums[start + 1]);
-
- for (int i = 2; i < n; i++) {
- dp[i] = Math.max(dp[i - 2] + nums[start + i], dp[i - 1]);
- }
-
- return dp[n - 1];
- }
-
- public static void main(String[] args) {
- int[] nums1 = {2, 3, 2};
- int maxAmount1 = new HouseRobberII().rob(nums1);
- // 输出:最大金额为:3
- System.out.println("最大金额为:" + maxAmount1);
-
- int[] nums2 = {1, 2, 3, 1};
- int maxAmount2 = new HouseRobberII().rob(nums2);
- // 输出:最大金额为:4
- System.out.println("最大金额为:" + maxAmount2);
-
- int[] nums3 = {1, 2, 3, 4, 5, 1, 2, 3, 4, 5};
- int maxAmount3 = new HouseRobberII().rob(nums3);
- // 输出:最大金额为:16
- System.out.println("最大金额为:" + maxAmount3);
- }
-
- }
(十八)股票买卖问题(Best Time to Buy and Sell Stock)
题目描述:给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。
注意:你不能在买入股票前卖出股票。
示例 1:输入: prices = [7,1,5,3,6,4]. 输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
示例 2:输入: prices = [7,6,4,3,1] 输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
来源:力扣(LeetCode)
链接:力扣
解题思路
这道题可以使用动态规划来解决。
- 我们可以定义两个变量 minPrice 和 maxProfit,分别表示当前遍历过的最低价格和当前的最大利润。
- 在遍历股票价格数组时,我们可以更新 minPrice 为当前价格和 minPrice 中的较小值。然后,我们可以计算当前价格与 minPrice 的差值,并将其与 maxProfit 进行比较,更新 maxProfit 为较大值。
- 最终,maxProfit 即为所能获取的最大利润。
该算法的时间复杂度为 O(n),其中 n 是股票价格数组的长度。通过动态规划的思想,我们可以在一次遍历中找到最低价格和最大利润,从而解决股票买卖问题。
代码展示
下面是使用动态规划解决股票买卖问题的代码示例:
- package org.zyf.javabasic.letcode.dynamicprogramming;
-
- /**
- * @author yanfengzhang
- * @description 给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
- * 你只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。
- * 注意:你不能在买入股票前卖出股票。
- * 示例 1:输入: prices = [7,1,5,3,6,4]. 输出: 5
- * 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5。
- * 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
- * 示例 2:输入: prices = [7,6,4,3,1] 输出: 0
- * 解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
- * @date 2023/5/28 23:31
- */
- public class BestTimeBuyAndSellStock {
- /**
- * 这道题可以使用动态规划来解决。
- * 我们可以定义两个变量 minPrice 和 maxProfit,分别表示当前遍历过的最低价格和当前的最大利润。
- * 在遍历股票价格数组时,我们可以更新 minPrice 为当前价格和 minPrice 中的较小值。
- * 然后,我们可以计算当前价格与 minPrice 的差值,并将其与 maxProfit 进行比较,更新 maxProfit 为较大值。
- * 最终,maxProfit 即为所能获取的最大利润。
- *
- * 该算法的时间复杂度为 O(n),其中 n 是股票价格数组的长度。
- * 通过动态规划的思想,我们可以在一次遍历中找到最低价格和最大利润,从而解决股票买卖问题。
- */
- public int maxProfit(int[] prices) {
- // 初始化最低价格为正无穷大
- int minPrice = Integer.MAX_VALUE;
- // 初始化最大利润为0
- int maxProfit = 0;
-
- for (int i = 0; i < prices.length; i++) {
- if (prices[i] < minPrice) {
- minPrice = prices[i];
- } else if (prices[i] - minPrice > maxProfit) {
- maxProfit = prices[i] - minPrice;
- }
- }
-
- return maxProfit;
- }
-
- public static void main(String[] args) {
- int[] prices1 = {7, 1, 5, 3, 6, 4};
- int maxProfit1 = new BestTimeBuyAndSellStock().maxProfit(prices1);
- // 输出:最大利润:5
- System.out.println("最大利润:" + maxProfit1);
-
- int[] prices2 = {7, 6, 4, 3, 1};
- int maxProfit2 = new BestTimeBuyAndSellStock().maxProfit(prices2);
- // 输出:最大利润:0
- System.out.println("最大利润:" + maxProfit2);
- }
- }
(十九)最佳买卖股票时机含冷冻期(Best Time to Buy and Sell Stock with Cooldown)
题目描述:给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:输入: [1,2,3,0,2] 输出: 3
解释: 在第 2 天(股票价格 = 2)的时候买入,在第 3 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-2 = 1 。
随后,在第 4 天(股票价格 = 0)的时候买入,在第 5 天(股票价格 = 2)的时候卖出,这笔交易所能获得利润 = 2-0 = 2 。
来源:力扣(LeetCode)
链接:力扣
解题思路
最优解法采用动态规划来解决含有冷冻期的买卖股票问题。
假设有三个状态:
- hold[i]:表示第 i 天持有股票时的最大利润。
- notHold[i]:表示第 i 天不持有股票时的最大利润。
- cooldown[i]:表示第 i 天处于冷冻期时的最大利润。
对于第 i 天的状态转移,可以根据前一天的状态计算得出:
- 如果第 i 天持有股票,则可能是前一天已经持有股票,或者是前一天不持有股票但在冷冻期之前买入股票。
- 如果第 i 天不持有股票,则可能是前一天已经不持有股票,或者是前一天在冷冻期之前卖出了股票。
- 如果第 i 天处于冷冻期,则前一天必须卖出股票。
具体状态转移方程如下:
- hold[i] = max(hold[i-1], notHold[i-1] - prices[i])
- notHold[i] = max(notHold[i-1], cooldown[i-1])
- cooldown[i] = hold[i-1] + prices[i]
最终的答案是 max(notHold[n-1], cooldown[n-1]),其中 n 是数组 prices 的长度。
代码展示
下面是使用动态规划解决含有冷冻期的买卖股票问题的代码示例:
- package org.zyf.javabasic.letcode.dynamicprogramming;
-
- /**
- * @author yanfengzhang
- * @description 给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格。
- * 设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
- * 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
- * 示例 1:输入: [1,2,3,0,2] 输出: 3
- * 解释: 在第 2 天(股票价格 = 2)的时候买入,在第 3 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-2 = 1 。
- * 随后,在第 4 天(股票价格 = 0)的时候买入,在第 5 天(股票价格 = 2)的时候卖出,这笔交易所能获得利润 = 2-0 = 2 。
- * @date 2023/5/29 23:37
- */
- public class BestTimeBuyAndSellStockWithCooldown {
- /**
- * 最优解法采用动态规划来解决含有冷冻期的买卖股票问题。
- * 假设有三个状态:
- * 1. hold[i]:表示第 i 天持有股票时的最大利润。
- * 2. notHold[i]:表示第 i 天不持有股票时的最大利润。
- * 3. cooldown[i]:表示第 i 天处于冷冻期时的最大利润。
- * 对于第 i 天的状态转移,可以根据前一天的状态计算得出:
- * * 如果第 i 天持有股票,则可能是前一天已经持有股票,或者是前一天不持有股票但在冷冻期之前买入股票。
- * * 如果第 i 天不持有股票,则可能是前一天已经不持有股票,或者是前一天在冷冻期之前卖出了股票。
- * * 如果第 i 天处于冷冻期,则前一天必须卖出股票。
- * 具体状态转移方程如下:
- * hold[i] = max(hold[i-1], notHold[i-1] - prices[i])
- * notHold[i] = max(notHold[i-1], cooldown[i-1])
- * cooldown[i] = hold[i-1] + prices[i]
- * 最终的答案是 max(notHold[n-1], cooldown[n-1]),其中 n 是数组 prices 的长度。
- */
- public int maxProfit(int[] prices) {
- if (prices == null || prices.length <= 1) {
- return 0;
- }
-
- int n = prices.length;
- int[] hold = new int[n];
- int[] notHold = new int[n];
- int[] cooldown = new int[n];
-
- hold[0] = -prices[0];
-
- for (int i = 1; i < n; i++) {
- hold[i] = Math.max(hold[i - 1], notHold[i - 1] - prices[i]);
- notHold[i] = Math.max(notHold[i - 1], cooldown[i - 1]);
- cooldown[i] = hold[i - 1] + prices[i];
- }
-
- return Math.max(notHold[n - 1], cooldown[n - 1]);
- }
-
- public static void main(String[] args) {
- int[] prices1 = {1, 2, 3, 0, 2};
- int maxProfit1 = new BestTimeBuyAndSellStockWithCooldown().maxProfit(prices1);
- // 输出:最大利润为:3
- System.out.println("最大利润为:" + maxProfit1);
-
- int[] prices2 = {7, 1, 5, 3, 6, 4};
- int maxProfit2 = new BestTimeBuyAndSellStockWithCooldown().maxProfit(prices2);
- // 输出:最大利润为:5
- System.out.println("最大利润为:" + maxProfit2);
-
- int[] prices3 = {1, 2, 3, 4, 5};
- int maxProfit3 = new BestTimeBuyAndSellStockWithCooldown().maxProfit(prices3);
- // 输出:最大利润为:4
- System.out.println("最大利润为:" + maxProfit3);
-
- int[] prices4 = {7, 6, 4, 3, 1};
- int maxProfit4 = new BestTimeBuyAndSellStockWithCooldown().maxProfit(prices4);
- // 输出:最大利润为:0
- System.out.println("最大利润为:" + maxProfit4);
- }
- }
(二十)找零钱的最少硬币数(Coin Change)
题目描述:给定不同面额的硬币 coins 和一个总金额 amount,编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
示例 1:输入: coins = [1, 2, 5], amount = 11. 输出: 3
解释: 11 = 5 + 5 + 1
示例 2:输入: coins = [2], amount = 3 输出: -1
来源:力扣(LeetCode)
链接:力扣
解题思路
该问题可以使用动态规划来解决。
- 我们定义一个一维数组 dp,其中 dp[i] 表示凑成金额 i 所需的最少硬币个数。
- 初始化数组 dp 为无穷大,除了 dp[0] = 0,表示凑成金额为 0 不需要任何硬币。
- 接下来,我们遍历金额从 1 到 amount,对于每个金额 i,我们枚举所有的硬币面额 coin,如果 coin 小于等于 i,则说明可以使用该硬币,更新 dp[i] 的值为 dp[i - coin] + 1 和当前 dp[i] 的较小值。
- 最终,dp[amount] 就是凑成总金额所需的最少硬币个数。
代码展示
下面是使用动态规划解决找零钱的最少硬币数问题的代码示例(使用Java):
- package org.zyf.javabasic.letcode.dynamicprogramming;
-
- import java.util.Arrays;
-
- /**
- * @author yanfengzhang
- * @description 给定不同面额的硬币 coins 和一个总金额 amount,
- * 编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
- * 你可以认为每种硬币的数量是无限的。
- * 示例 1:输入: coins = [1, 2, 5], amount = 11. 输出: 3
- * 解释: 11 = 5 + 5 + 1
- * 示例 2:输入: coins = [2], amount = 3 输出: -1
- * @date 2023/5/26 23:41
- */
- public class CoinChange {
- /**
- * 我们定义一个一维数组 dp,其中 dp[i] 表示凑成金额 i 所需的最少硬币个数。
- * 初始化数组 dp 为无穷大,除了 dp[0] = 0,表示凑成金额为 0 不需要任何硬币。
- * 接下来,我们遍历金额从 1 到 amount,对于每个金额 i,
- * 我们枚举所有的硬币面额 coin,如果 coin 小于等于 i,则说明可以使用该硬币,
- * 更新 dp[i] 的值为 dp[i - coin] + 1 和当前 dp[i] 的较小值。
- * 最终,dp[amount] 就是凑成总金额所需的最少硬币个数。
- */
- public int coinChange(int[] coins, int amount) {
- int[] dp = new int[amount + 1];
- Arrays.fill(dp, Integer.MAX_VALUE);
- dp[0] = 0;
-
- for (int i = 1; i <= amount; i++) {
- for (int coin : coins) {
- if (coin <= i && dp[i - coin] != Integer.MAX_VALUE) {
- dp[i] = Math.min(dp[i], dp[i - coin] + 1);
- }
- }
- }
-
- return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
- }
-
- public static void main(String[] args) {
- int[] coins1 = {1, 2, 5};
- int amount1 = 11;
- int minCoins1 = new CoinChange().coinChange(coins1, amount1);
- System.out.println("凑成总金额 " + amount1 + " 所需的最少硬币个数为:" + minCoins1);
-
- int[] coins2 = {2};
- int amount2 = 3;
- int minCoins2 = new CoinChange().coinChange(coins2, amount2);
- System.out.println("凑成总金额 " + amount2 + " 所需的最少硬币个数为:" + minCoins2);
- }
- }
(二十一)从起点到终点的最小路径数(Unique Paths)
题目描述:给定一个二维网格,大小为 m x n,网格中的每个格子都代表一个空地。起始点位于左上角,即网格的左上角格子,终点位于右下角,即网格的右下角格子。在每个格子中,你只能向下或向右移动。请计算从起始点到终点的不同路径的数目。
例如,给定一个 3 x 3 的网格,起始点为左上角格子 (0, 0),终点为右下角格子 (2, 2)。在这个网格中,总共有 3 条不同的路径。
1 1 1
1 1 1
1 1 1
提示:每次只能向下或向右移动一步。
来源:力扣(LeetCode)
链接:力扣
解法分析
最优解法是使用动态规划来解决该问题。我们可以定义一个二维数组 dp,其中 dp[i][j] 表示从起始点到达格子 (i, j) 的不同路径数目。
具体步骤如下:
- 初始化边界条件:将 dp[0][j] 和 dp[i][0](即第一行和第一列)的值都设置为 1,因为只能向下或向右移动,所以第一行和第一列的格子只有一种路径到达。
- 遍历网格的每个格子,计算不同路径数目:对于格子 (i, j),可以从上方的格子 (i-1, j) 或左侧的格子 (i, j-1) 到达。因此,dp[i][j] = dp[i-1][j] + dp[i][j-1]。
- 最后返回 dp[m-1][n-1],即终点的不同路径数目。
该算法的时间复杂度是 O(m * n),其中 m 和 n 分别是网格的行数和列数,因为需要遍历所有格子。空间复杂度是 O(m * n),需要使用一个二维数组来存储中间结果。
代码展示
- package org.zyf.javabasic.letcode.dynamicprogramming;
-
- /**
- * @author yanfengzhang
- * @description 一个机器人位于一个 m x n 网格的左上角。机器人每次只能向下或向右移动一步。
- * 机器人试图达到网格的右下角。问总共有多少条不同的路径?
- * 例如,一个 3 x 7 的网格,机器人从左上角到右下角总共有 28 条不同的路径。
- * 注意:m 和 n 的值均不超过 100。
- * 示例 1:输入: m = 3, n = 2. 输出: 3
- * 解释: 从左上角开始,总共有三条路径可以到达右下角。
- * 1. 向右 -> 向右 -> 向下
- * 2. 向右 -> 向下 -> 向右
- * 3. 向下 -> 向右 -> 向右
- * 示例 2:输入: m = 7, n = 3 输出: 28
- * @date 2023/5/12 23:24
- */
- public class UniquePaths {
- /**
- * 这道题可以使用动态规划来解决。
- * 我们定义一个二维数组 dp,其中 dp[i][j] 表示从起始位置到达网格 (i, j) 的不同路径数。
- * 根据题目要求,机器人每次只能向下或向右移动,
- * 因此对于网格中的每个位置 (i, j),可以从上方的位置 (i-1, j) 或左方的位置 (i, j-1) 到达。
- *
- * 根据上述分析,可以得到状态转移方程:
- * dp[i][j] = dp[i-1][j] + dp[i][j-1]
- * 初始条件为:当 i=0 或 j=0 时,只有一条路径可以到达,因此 dp[i][j] = 1。
- * 最终要求的答案是 dp[m-1][n-1],其中 m 和 n 分别是网格的行数和列数。
- *
- * 该算法的时间复杂度为 O(m * n),空间复杂度为 O(m * n),其中 m 和 n 分别是网格的行数和列数。
- * 通过动态规划的思想,我们可以高效地计算出不同路径的数量。
- */
- public int uniquePaths(int m, int n) {
- int[][] dp = new int[m][n];
-
- // 初始化第一行和第一列的路径数为1
- for (int i = 0; i < m; i++) {
- dp[i][0] = 1;
- }
- for (int j = 0; j < n; j++) {
- dp[0][j] = 1;
- }
-
- // 计算其他位置的路径数
- for (int i = 1; i < m; i++) {
- for (int j = 1; j < n; j++) {
- dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
- }
- }
-
- return dp[m - 1][n - 1];
- }
-
- public static void main(String[] args) {
- int m1 = 3, n1 = 2;
- int paths1 = new UniquePaths().uniquePaths(m1, n1);
- // 输出:不同路径的数量:3
- System.out.println("不同路径的数量:" + paths1);
-
- int m2 = 7, n2 = 3;
- int paths2 = new UniquePaths().uniquePaths(m2, n2);
- // 输出:不同路径的数量:28
- System.out.println("不同路径的数量:" + paths2);
- }
-
- }
评论记录:
回复评论: