70. 爬楼梯
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
public int climbStairs(int n) { // 分别存放 f(x-2) f(x-1) f(x) int[] arr = new int[]{0,1,2}; if (n<=2) { return arr[n]; } for (int i=3;i<=n;i++) { // f(x) = f(x-1) + f(x-2) int fx = arr[2] + arr[1]; // fx 放在末尾,数组往前移 arr[0] = arr[1]; arr[1] = arr[2]; arr[2] = fx; } return arr[2]; }
53. 最大子数组和
- dp[i]=max{nums[i],dp[i−1]+nums[i]}
- dp[0]= nums[0]
class Solution { public int maxSubArray(int[] nums) { int result = nums[0]; int dp[] = new int[nums.length]; dp[0] = nums[0]; for(int i=1;i<nums.length;i++){ //递推公式 dp[i] = Math.max(dp[i-1]+nums[i],nums[i]); //result用来记dp数组里的最大数值 if(dp[i]>result) result = dp[i]; } return result; } }
96. 不同的二叉搜索树
如果整数1 ~ n中的 k 作为根节点值,则 1 ~ k-1 会去构建左子树,k+1 ~ n 会去构建右子树。
左子树出来的形态有a 种,右子树出来的形态有 b 种,则整个树的形态有 a∗b 种。
class Solution { public int numTrees(int n) { // 最后要返回 dp[n] int dp[] = new int[n+1]; // dp[0] 代表用 0 个数去构建左子二叉搜索树,dp[n-1] 代表用 n-1 个数(除掉根节点)去构建右子二叉搜索树 // dp[n] = dp[0]*dp[n-1] + dp[1]*dp[n-2] + dp[2]*dp[n-3] + ..... + dp[n-2]*dp[0] + ... dp[n-1]*dp[0] // 一共有 i 个数,左子树用 j 个,右子树用 i-j-1 个 dp[i] = dp[j]dp[i-j-1]之和 0<=j<=i-1(左子树可以用0个到i-1个(除根节点)) // 空树,只有一种 dp[0] = 1; // 一个数字的,只有一种 dp[1] = 1; // dp[0] 和 dp[1] 已知,所以从 dp[2] 开始 for (int i=2;i<=n;i++) { // 左子树可以用0个到i-1个(除根节点) for (int j=0;j<=i-1;j++) { dp[i] += dp[j]*dp[i-j-1]; } } return dp[n]; } }
198. 打家劫舍
dp[0] = nums[0];
// 注意 dp[1] 即只有两家的时候,这时候最大是偷这两家之中大的那个
dp[1] = Math.max(nums[0], nums[1]);
// dp[i-1] 是指这一间不偷,i-1间偷;dp[i-2]+nums[i] 是 i-2 间偷,i-1 间不偷,这间偷 dp[i] = max{dp[i-1], dp[i-2]+nums[i]}
dp[i] 是 0~i 的最优解
class Solution { public int rob(int[] nums) { if (nums.length==1) { return nums[0]; } if (nums.length==2) { return Math.max(nums[0], nums[1]); } int dp[] = new int[nums.length]; dp[0] = nums[0]; // 注意 dp[1] 即只有两家的时候,这时候最大是偷这两家之中大的那个 dp[1] = Math.max(nums[0], nums[1]); // dp[i-1] 是指这一间不偷,i-1间偷;dp[i-2]+nums[i] 是 i-2 间偷,i-1 间不偷,这间偷 // dp[i] = max{dp[i-1], dp[i-2]+nums[i]} for (int i=2;i<nums.length;i++) { dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]); } return dp[nums.length-1]; } }
416. 分割等和子集
官方题解就很好
计算之前就排除掉的一些情况:
1、数组元素个数 n 小于 2,即只有 0 个 1 个元素,那么不可能被划分为两部分
2、如果数组元素总和是奇数,那么划分为两部分,使得两部分的和相等是不可能的
3、令 target = sum/2 即数组所有元素和的一半。最大元素记为 maxNum。那么:如果 maxNum>target,答案显然是 false ; 如果 maxNum = target ,那么显然是 true
- i 是从 0 到 输入数组的长度
- j 是 0~数组总和/2,即为分割后成两部分后,各部分的和,以下称作 target
- dp[i][j] 是从数组的 [0,i] 下标范围内选取若干个正整数(可以是 0 个),是否存在一种选取方案使得被选取的正整数的和等于 j
- 最后的结果即为 dp[n-1][target] ,从数组下标 [0,n-1] 范围内,即数组的全部范围内,选取若干个整数,是否能使其和为 target
递推公式:
- 如果 j<nums[i],即要选取的当前数字 nums[i] 已经比目标和 j 大的话,nums[i] 这个数字一定不能选,那么 dp[i][j] = dp[i-1][j]
- 如果 j>=nums[i],即要选取的当前数字 nums[i] 不比目标和 j 大的话,nums[i] 这个数字可选可不选
- 如果选 nums[i] 这个数字,[0,i-1] 仅需完成目标和 j-nums[i],那么 dp[i][j] = dp[i-1][j-nums[i]]
- 如果不选 nums[i] 这个数字, 就要靠 [0,i-1] 去完成目标和 j,那么 dp[i][j] = dp[i-1][j]
- 总结上面两个,那么 dp[i][j] = dp[i-1][j-nums[i]] | dp[i-1][j]
边界条件:
- 对于目标和 j 是 0,那么在数组下标 [0,i] 范围内,一定存在一种选取方案使得被选取的正整数和等于 0 —— 即什么都不选
- 对于 i=0 ,那么在数组下标 [0,0] 范围内,只有一个 nums[0] 可以选,即 dp[0][nums[0]] = true,dp[0][其它大小的j] = false
class Solution { public boolean canPartition(int[] nums) { // 元素数量 1 或 2,不可以划分为两部分 if (nums.length < 2) { return false; } int sum = 0; int maxNum = Integer.MIN_VALUE; for (int num : nums) { if (num>maxNum) { maxNum = num; } sum += num; } // 如果总和是奇数,那么划分为两部分,使得两部分的和相等是不可能的 if (sum % 2 != 0) { return false; } // target 是总和的一半 int target = sum/2; // 最大的数就比 target 大了,不可能分为和相等的两部分 if (maxNum > target) { return false; } // 最大的数等于 target,那么最大的数一组,其它的数一组,就是结果 if (maxNum == target && sum - maxNum == target) { return true; } boolean[][] dp = new boolean[nums.length][target+1]; // 边界条件 for (int i=0; i<nums.length;i++) { // 对于目标和 j 是 0,那么在数组下标 [0,i] 范围内,一定存在一种选取方案使得被选取的正整数和等于 0 —— 即什么都不选 dp[i][0] = true; } for (int j=0; j<=target;j++) { // 对于 i=0 ,那么在数组下标 [0,0] 范围内,只有一个 nums[0] 可以选,即 dp[0][nums[0]] = true,dp[0][其它大小的j] = false if (nums[0] <= target) { dp[0][nums[0]] = true; } else { dp[0][j] = false; } }
for (int i=1;i<nums.length;i++) { for (int j=1;j<=target;j++) { if (nums[i] < j) { /* 如果 j>=nums[i],即要选取的当前数字 nums[i] 不必目标和 j 大的话,nums[i] 这个数字可选可不选 1.如果选 nums[i] 这个数字,[0,i-1] 仅需完成目标和 j-nums[i],那么 dp[i][j] = dp[i-1][j-nums[i]] 2.如果不选 nums[i] 这个数字, 就要靠 [0,i-1] 去完成目标和 j,那么 dp[i][j] = dp[i-1][j] 总结上面两个,那么 dp[i][j] = dp[i-1][j-nums[i]] | dp[i-1][j] */ dp[i][j] = dp[i-1][j] | dp[i-1][j-nums[i]]; } else { // 如果 j<nums[i],即要选取的当前数字 nums[i] 已经比目标和 j 大的话,nums[i] 这个数字一定不能选,那么 dp[i][j] = dp[i-1][j] dp[i][j] = dp[i-1][j]; } } } return dp[nums.length - 1][target]; } }
72. 编辑距离
官方题解:https://leetcode.cn/problems/edit-distance/solution/bian-ji-ju-chi-by-leetcode-solution/
我们可以对任意一个单词进行三种操作:
- 插入一个字符;
- 删除一个字符;
- 替换一个字符。
题目给定了两个单词,设为 A 和 B,这样我们就能够六种操作方法。
但我们可以发现,如果我们有单词 A 和单词 B:
- 对单词 A 删除一个字符和对单词 B 插入一个字符是等价的。例如当单词 A 为 doge,单词 B 为 dog 时,我们既可以删除单词 A 的最后一个字符 e,得到相同的 dog,也可以在单词 B 末尾添加一个字符 e,得到相同的 doge;
- 同理,对单词 B 删除一个字符和对单词 A 插入一个字符也是等价的;
- 对单词 A 替换一个字符和对单词 B 替换一个字符是等价的。例如当单词 A 为 bat,单词 B 为 cat 时,我们修改单词 A 的第一个字母 b -> c,和修改单词 B 的第一个字母 c -> b 是等价的。
这样以来,本质不同的操作实际上只有三种:
- 在单词 A 中插入一个字符;
- 在单词 B 中插入一个字符;
- 修改单词 A 的一个字符。
当我们获得 D[i][j-1],D[i-1][j] 和 D[i-1][j-1] 的值之后就可以计算出 D[i][j]。
- D[i][j-1] 为 A 的前 i 个字符和 B 的前 j - 1 个字符编辑距离的子问题。即对于 B 的第 j 个字符,我们在 A 的末尾添加了一个相同的字符,那么 D[i][j] 最小可以为 D[i][j-1] + 1;
- D[i-1][j] 为 A 的前 i - 1 个字符和 B 的前 j 个字符编辑距离的子问题。即对于 A 的第 i 个字符,我们在 B 的末尾添加了一个相同的字符,那么 D[i][j] 最小可以为 D[i-1][j] + 1;
- D[i-1][j-1] 为 A 前 i - 1 个字符和 B 的前 j - 1 个字符编辑距离的子问题。即对于 B 的第 j 个字符,我们修改 A 的第 i 个字符使它们相同,那么 D[i][j] 最小可以为 D[i-1][j-1] + 1。特别地,如果 A 的第 i 个字符和 B 的第 j 个字符原本就相同,那么我们实际上不需要进行修改操作。在这种情况下,D[i][j] 最小可以为 D[i-1][j-1]。
边界情况
一个空串和一个非空串的编辑距离为
- D[i][0] = i ,从 word1 到一个空串,相当于对 word1 执行 i 次删除操作
- D[0][j] = j ,从一个空串到 word1,相当于对 word1执行 j 次插入操作。
class Solution { public int minDistance(String word1, String word2) { // dp[i][j] 是 word1 的 1...i 到 word2 的 1...j 的编辑距离(0 表示空串) // 注意 dp 数组的大小是 length+1 ,而不是 length,因为 0 的位置要表示空串int[][] dp = new int[word1.length()+1][word2.length()+1]; // 边界条件 // dp[i][0]=i 从word1 的 0...i 到空串编辑距离,删除 i 次 // dp[0][j]=j 从空串到 word2 的 0...j 的编辑举例,插入 j 次 for (int i=0;i<word1.length()+1;i++) { dp[i][0] = i; } for (int j=0;j<word2.length()+1;j++) { dp[0][j] = j; } // 如果 word1[i] 和 word2[j] 相等,dp[i][j] = min{dp[i-1][j]+1, dp[i][j-1]+1, dp[i-1][j-1]+1} // = 1 + min{dp[i-1][j], dp[i][j-1], dp[i-1][j-1]} // 如果 word1[i] 和 word2[j] 相等,dp[i][j] = min{dp[i-1][j]+1, dp[i][j-1]+1, dp[i-1][j-1]} // = 1 + min{dp[i-1][j], dp[i][j-1], dp[i-1][j-1]-1} for (int i=1;i<word1.length()+1;i++) { for (int j=1;j<word2.length()+1;j++) { // 为什么是 charAt i-1 和 charAt j-1 呢? // 因为 dp 的 0 被空串占了,dp[i][j]表示 word1第i个字符,word2第j个字符,word1 和 word2 字符下标又是从0开始的,所以是 charAt(i-1) 和 charAt(j-1) if (word1.charAt(i-1) == word2.charAt(j-1)) { dp[i][j] = 1 + Math.min(Math.min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1]-1); } else { dp[i][j] = 1 + Math.min(Math.min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1]); } } } return dp[word1.length()][word2.length()]; } }
1143. 最长公共子序列
dp[i][j] 表示 text1(0:i) 和 text2(0:j) 的最长公共子序列的长度
用 dp 的 0 行 0 列表示字符串为空的场景,即 i=0 或 j=0 的 0 行 0 列,dp 值为0
因此 dp 对应的下标比字符串的下标多1
class Solution { public int longestCommonSubsequence(String text1, String text2) { // dp[i][j] 表示 text1(0...i) 与 text2(0...j) 的最长公共子序列 // dp[0] 用来表示空字符串的情况了,所以要 length+1 int[][] dp=new int[text1.length()+1][text2.length()+1]; // 要由左上角得来,所以从左上到右下 // text1[i] == text2 dp[i][j] = dp[i-1][j-1]+1 // text1[i] != text2 dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]) int maxLen = 0; for (int i=1;i<text1.length();i++) { dp[i][0] = 0; } for (int j=1;j<text2.length();j++) { dp[0][j] = 0; } for (int i=1;i<text1.length()+1;i++) { // dp[0] 用来表示空字符串的情况了,所以要 length+1 // dp[1] 才是 text.charAt(0),所以要 -1 这里 char c1=text1.charAt(i-1); for (int j=1;j<text2.length()+1;j++) { char c2=text2.charAt(j-1); if (c1 == c2) { dp[i][j] = dp[i-1][j-1] + 1; if (dp[i][j] > maxLen) { maxLen = dp[i][j]; } } else { dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]); } } } return maxLen; } }
最长公共子串
和最长公共子序列类似
dp[i][j] 表示 text1(0:i) 和 text2(0:j) 的最长公共子串的长度
用 dp 的 0 行 0 列表示字符串为空的场景,即 i=0 或 j=0 的 0 行 0 列,dp 值为0
只是状态转移方程,在比较的当前字符不等时,不一样
- dp[i][j] = dp[i-1][j-1]+1, text1(i-1)==text2(i-1)
- dp[i][j] = 0, text1(i-1) != text2(i-1)
import java.util.HashMap; import java.util.Map; public class TestLongestCommonSubStr { public static void main(String[] args) { String res = longestCommonSubsequence("rghello123wohellord66665rld", "vfhello123abc4hellord66665"); System.out.println(res); } public static String longestCommonSubsequence(String text1, String text2) { // dp[i][j] 表示 text1(0...i) 与 text2(0...j) 的最长公共子序列 // dp[0] 用来表示空字符串的情况了,所以要 length+1 int[][] dp=new int[text1.length()+1][text2.length()+1]; // 要由左上角得来,所以从左上到右下 // text1[i] == text2 dp[i][j] = dp[i-1][j-1]+1 // text1[i] != text2 dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]) // key为dp[i][j]的i,即text1的子串结束下标,value为text1(0:i)的最长公共子串起始下标 Map<Integer, Integer> map = new HashMap<>(); int maxLen = 0; int[] maxLenIndex = new int[2]; for (int i=1;i<text1.length();i++) { dp[i][0] = 0; } for (int j=1;j<text2.length();j++) { dp[0][j] = 0; } for (int i=1;i<text1.length()+1;i++) { // dp[0] 用来表示空字符串的情况了,所以要 length+1 // dp[1] 才是 text.charAt(0),所以要 -1 这里 char c1=text1.charAt(i-1); for (int j=1;j<text2.length()+1;j++) { char c2=text2.charAt(j-1); if (c1 == c2) { dp[i][j] = dp[i-1][j-1] + 1; // 起始下标从 i-1 来 int istart = put(map, i-1, i); if (dp[i][j] > maxLen) { // 最长公共子串的长度 maxLen = dp[i][j]; // 最长公共子串在 text1 的起始下标 maxLenIndex[0] = istart; // 最长公共子串在 text1 的结束下标 maxLenIndex[1] = i; } } else { // 子序列 //dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]); // 子串 dp[i][j] = 0; } } } return text1.subSequence(maxLenIndex[0], maxLenIndex[1]).toString(); } private static int put(Map<Integer, Integer> map, int istartFrom, int i) { // istartFrom 起始下标从 istartFrom 来 Integer istart = map.get(istartFrom); if (istart == null) { // 当 i=1 时会走到这里 map.put(i, i); return i; } else { // 放入 结束下标-起始下标 map.put(i, istart); return istart; } } }
718. 最长重复子数组
dp[i][j] 表示 nums1[0:i] nums2[0:j]的最长公共子数组
dp[0][0] 是两个数组为空的场景,所以 dp 初始化时要 new int[nums1.length][nums2.length]
if nums1[i]==nums2[j] dp[i][j] = dp[i-1][j-1]+1 从左上到右下
else dp[i][j]=0
class Solution { public int findLength(int[] nums1, int[] nums2) { int dp[][] = new int[nums1.length+1][nums2.length+1]; // dp[i][j] nums1[0:i] nums2[0:j]的最长公共子数组 // dp[0][0] 是两个数组为空的场景 // if nums1[i]==nums2[j] dp[i][j] = dp[i-1][j-1]+1 从左上到右下 // else dp[i][j]=0 for (int i=0;i<nums1.length+1;i++) { dp[i][0] = 0; } for (int j=0;j<nums2.length+1;j++) { dp[0][j] = 0; } int maxLen = 0; for (int i=1;i<nums1.length+1;i++) { for(int j=1;j<nums2.length+1;j++) { if (nums1[i-1] == nums2[j-1]) { dp[i][j] = dp[i-1][j-1] + 1; if (dp[i][j] > maxLen) { maxLen = dp[i][j]; } } else { dp[i][j] = 0; } } } return maxLen; } }
279. 完全平方数
完全背包问题
class Solution { public int numSquares(int n) { // 为了使数组下标和数字一一对应 int[] dp = new int[n+1]; // 题目要求 n>=1 for (int i=1;i<=n;i++) { // 最坏情况是拆解为 1+1+1+1+...... dp[i] = i; // j*j<=i 是因为要去掉 j*j 这个完全平方数,剩下的数要是一个子问题 // 而 j*j==i 就是i自己本身就是个完全平方数的情况 for (int j=1;j*j<=i;j++) { // 之前的子问题 + j*j这个完全平方数 dp[i] = Math.min(dp[i], dp[i-j*j] + 1); } } return dp[n]; } }
322. 零钱兑换
完全背包问题
一、完全背包:每种硬币可以选择多个
dp 长度 amount+1,dp[0]=0,其它初始值为 dp[i]=amount+1,可以边循环边设置
最后返回 dp[amount]>amount? -1:dp[amount];
dp[i] = Math.min(dp[i], dp[i-coins[j]]+1)
外层循环金额从小到大,内层循环硬币
1. i=2 时,只有在 coin=2 时,dp[2] = min(13, dp[0]+1) = 1,即选择 2 这一个硬币
2.i=4 时,在 coin=2,dp[4] = min(13,dp[2]+1) = 2,即选择两个面值 2 的硬币
i=4 时,在 coin=4,dp[4] = min(2, dp[0]+1) = 1,即只选一个面值为 4 的,比选两个面值 2 的硬币个数更少
3. i=6 时,在 coin=2, dp[6] = min(13, dp[4]+1) = 2,即选择面值 2,4
i=6 时,在 coin=4, dp[6] = min(2, dp[2]+1) = 2, 还是选择面值 2, 4
i=6,在 coin=6, dp[6]=min(2, dp[0]+1)=1,只选一个面值 6 的比选择 2,4 的硬币个数更少
。。。。。
i=12,在 coin=2,dp[12]=min(13, dp[10]+1) = 3,即选择 4,6,2
i=12,在 coin=4,dp[12]=min(3, dp[8]+1) = 3,即选择 4,4,4 或者说 2,6,4
i=12,在 coin=6,dp[12]=min(3, dp[6]+1) = 2,即选择 6,6,比前面两种选择硬币个数更少
二、01背包:每种硬币只可以选择一个
dp 长度 amount+1,dp[0]=0,其它初始值为 dp[i]=amount+1 ,要提前全部设置好。因为金额在内层循环不能边循环边设置。
最后返回 dp[amount]>amount? -1:dp[amount];
dp[i] = Math.min(dp[i], dp[i-coins[j]]+1)
外层循环硬币,内循环金额从大到小(大不依赖小,即每种一个)
在 coin = 2,金额从大到小全循环了一遍之后
- dp[2]=min(13, dp[0]+1)=1 (取当前的硬币 2)
在 coin = 4,金额从大到小全循环了一遍之后
- 在金额为 8 即 dp[8] 时,由于从大到小,dp[8-4]=dp[4] 还没产生,而从小达到就会先产生 dp[4]=1,就可以选 4,4 。这就是为什么从大到小可以防止重复选取。
- dp[6]=min(13, dp[2]+1)=2 (取上一轮的硬币 2,和当前的硬币 4)
- dp[4]=min(13, dp[0]+1)=1 (取当前的硬币 4)
在 coin = 6,金额从大到小全循环了一遍之后
- dp[12] = min(13, dp[6]+1) = 3 (取 dp[6] 的 2,4 ,和当前的硬币 6)
- dp[10]=min(13, dp[4]+1) = 2 (取 dp[4] 的 4,和当前的硬币 6)
- dp[8]=min(13, dp[2]+1)= 2 (取 dp[2] 的2,和当前的硬币 6)
class Solution { /* * 完全背包,每种硬币可以选择多个 */ public int coinChange(int[] coins, int amount) { // 金额 int dp[] = new int[amount + 1]; // 总金额i==conins[j],即刚好选择一个coins[j]的情况,此时希望dp[i-coins[j]]=0 dp[0] = 0; for (int i=1;i<=amount;i++) { // 因为是求最小值,初始令它等于不可能的最大值(有1的零钱最大也是amount个,所以amount+1是不可能的值) dp[i] = amount + 1; // 硬币选择 for (int j=0;j<coins.length;j++) { // 防止 i-coins[j] 小于0越界。 // 注意不能把这个条件放到 for 循环里 j<coins.length && coins[j]<=i // 因为虽然不满足 coins[j]<=i,外层循环还是要继续,放在for里,循环就暂停了 if (coins[j]<=i) { // 不选/选 coins[j] dp[i] = Math.min(dp[i], dp[i-coins[j]]+1); } } } // 最后还是初始时的不可能的值,说明没有解法,返回-1 return dp[amount]>amount? -1:dp[amount]; } /* * 如果是01背包,每种硬币只能选择一个的话 */ public int coinChange2(int[] coins, int amount) { // 金额 int dp[] = new int[amount + 1]; // 总金额i==conins[j],即刚好选择一个coins[j]的情况,此时希望dp[i-coins[j]]=0 dp[0] = 0; // 总金额从大到小,这样不依赖小的了,即每种只选择一个 // 但是小的要总有一个不可能的数字,这样 min 比小的时候,小的才不会算进去 // 由于金额是内层循环,所以不能在循环里顺便初始化,把初始化放在双层循环外 for (int i=1;i<=amount;i++) { dp[i] = amount+1; } // 0-1 背包(每种硬币只能选1个),要先遍历可选硬币,再遍历总金额 for (int j=0;j<coins.length;j++) { // 总金额从大到小,这样不依赖小的了,即每种只选择一个 // 但是小的要总有一个不可能的数字,这样 min 比小的时候,小的才不会算进去 for (int i=amount;i>=1;i--) { if (coins[j]<=i) { // 不选/选 coins[j] dp[i] = Math.min(dp[i], dp[i-coins[j]]+1); } } } // 最后还是初始时的不可能的值,说明没有解法,返回-1 return dp[amount]>amount? -1:dp[amount]; } }
139. 单词拆分
可以由动态规划,也可以由 dfs 。由此题思考动态规划和 dfs 的联系
dfs 的暴力枚举(可重复) 和 枚举所有组合(组合即不可重复01背包,dfs 时加入 startIndex)
为什么要加 !dp[i] ? 且只要一次为真就可以退出内层循环 ?
比如 Leetcode [leet, code] i=4 时的内层循环:
词为"leet" 时 s.substring(4-wordLen, 末尾)=“leet” dp[4]=dp[0]&&("leet".equals("leet"))=true
词为"code" 时 s.substring(4-wordLen, 末尾)=“code” dp[4]=dp[0]&&("code".equals("leet"))=false
本来为真的又给弄成 false 了 ,但只要一次为真就可以了,所以后面的循环没必要。而且也不能有人把 true 又给改成 false,所以判断 if(!dp[i]) 就不改了
class Solution { private boolean dfsRes = false; Solution() { dfsRes= false; } public boolean wordBreak(String s, List<String> wordDict) { // dpComplete(s, wordDict) // dp01(s, wordDict) // dfs(s, wordDict, ""); dfs2(s, wordDict, "", 0); return dfsRes; } // 完全背包,可重复选取 public boolean dpComplete(String s, List<String> wordDict) { boolean dp[] = new boolean[s.length()+1]; // 比如 Leetcode ["leet", "code"] i=4时,词为"leet" 时 dp[4]=dp[0]&&("leet".equals("leet")) // 所以 dp[0] 要为 true dp[0] = true; for (int i=1;i<=s.length();i++) { //初始化默认就是false了。dp[i] = false; for (int j=0;j<wordDict.size();j++) { String word = wordDict.get(j); int wordLen = word.length(); /* 为什么要加 !dp[i] ? * 比如 Leetcode [leet, code] i=4时,词为"leet" 时 dp[4]=dp[0]&&("leet".equals("leet"))=true * 只要一次为真就可以退出循环 * 如果下次循环 i=4时,词为"code" 时 dp[4]=dp[0]&&("code".equals("leet"))=false * 本来为真的又给弄成 false 了 */ if (!dp[i] && wordLen<=i) { dp[i] = dp[i-wordLen] && (word.equals(s.substring(i-wordLen, i))); if (dp[i]) { break; } } } } return dp[s.length()]; } // 01背包,不可重复选取 public boolean dp01(String s, List<String> wordDict) { boolean dp[] = new boolean[s.length()+1]; dp[0] = true; // 外层是可选择项 for (int j=0;j<wordDict.size();j++) { // 里层是背包容量倒序 for (int i=s.length();i>=0;i--) { String word = wordDict.get(j); int wordLen = word.length(); if (!dp[i] && wordLen<=i) { dp[i] = dp[i-wordLen] && (word.equals(s.substring(i-wordLen, i))); if (dp[i]) { break; } } } } return dp[s.length()]; } // 可重复选取。递归解法。会超时。 public void dfs(String s, List<String> wordDict, String curWord) { // 超出 s 长度,直接返回 if (curWord.length()>s.length()) { return; } if (curWord.equals(s)) { dfsRes = true; return; } // 可重复选取,就要递归所有的可能性 // 不可重复选取是属于组合, 要有 startIndex,然后 i=startIndex 下一层 i+1 for (int i=0;i<wordDict.size();i++) { String curs = curWord + wordDict.get(i); if (s.startsWith(curs)) { // curWord 是 String, 无状态变量,所以不需要恢复现场 // 因为可以重复选取,所以下一次的 startIndex 是 i 而不是 i+1 dfs(s, wordDict, curs); } } } // 不可重复选取。递归解法 public void dfs2(String s, List<String> wordDict, String curWord, int startIndex) { // 超出 s 长度,直接返回 if (curWord.length()>s.length()) { return; } if (curWord.equals(s)) { dfsRes = true; return; } // 可重复选取,就要递归所有的可能性 // 不可重复选取是属于组合, 要有 startIndex,然后 i=startIndex 下一层 i+1 for (int i=startIndex;i<wordDict.size();i++) { String curs = curWord + wordDict.get(i); if (s.startsWith(curs)) { // curWord 是 String, 无状态变量,所以不需要恢复现场 dfs2(s, wordDict, curs, i+1); } } } }
300. 最长递增子序列
dp[i] 表示 nums[0:i] 的最长递增子序列
枚举所有 <i 的 j,即在 nums[i] 之前的所有递增子序列
如果 nums[i]>nums[j],dp[j] 是 [0:j] 的最长,又加上 nums[i] 这一个比 nums[j] 大的
dp[i] = Math.max(dp[i], dp[j] + 1);
class Solution { public int lengthOfLIS(int[] nums) { int res=1; // dp[i] nums[0:i] 的最长递增子序列 int[] dp = new int[nums.length]; for (int i=0;i<dp.length;i++) { // 最短默认1 dp[i] = 1; for (int j=0;j<i;j++) { if (nums[i]>nums[j]) { // dp[j] 是 [0:j] 的最长,又加上nums[i]这一个比nums[j]大的 dp[i] = Math.max(dp[i], dp[j] + 1); } } res = Math.max(res, dp[i]); } return res; } }
152. 乘积最大子数组
一开始想的是 dp[i] 表示以 nums[i] 结尾的子数组的最大乘积和
所以 if(nums[i]*dp[i-1]>nums[i]) 就把 nums[i] 加到前面那个子数组里, dp[i]=nums[i]*dp[i-1]。否则的话重新从 nums[i] 开始,即 dp[i]=nums[i]
如果全是正数,没问题。可是 [-2,3,-4] 这个用例不通过,输出3,而正确答案是 12
dp[0]=-2 ;因为 dp[0]*-3<-3 所以 dp[1]=3;dp[2]=3
解决:
如果遇到负数,一乘就会颠倒,乘以最大的会变成最小的,乘以最小的会变成最大的
所以需要两个 dp 数组,一个存以 nums[i] 结尾的子数组的最大乘积和,一个存最小乘积和
这样,遇到负数,最大乘积要从最小乘积和 dp 数组里取前一个来乘,最小乘积反而要从最大乘积和 dp 数组里取前一个来乘
以及和重新开始,即 nums[i] 进行比较
dpMax[i] = Math.max(dpMin[i-1]*nums[i], nums[i]);
dpMin[i] = Math.min(dpMax[i-1]*nums[i], nums[i]);
class Solution { /* * [-2,3,-4] 这个用例不通过,输出3,而正确答案是 12 * 不具有最优子结构 */ public int maxProduct2(int[] nums) { int maxMulti = nums[0]; int[] dp = new int[nums.length]; dp[0] = nums[0]; // dp[i] 以 Nums[i] 结尾的最大子数组 for (int i=1;i<nums.length;i++) { int addi = nums[i]*dp[i-1]; // 把 nums[i] 加到前面 if (addi > nums[i]) { dp[i] = addi; } // 不把nums[i]加到前面。而是从nums[i]这里重新开始 else { dp[i] = nums[i]; } maxMulti = Math.max(maxMulti, dp[i]); } return maxMulti; } public int maxProduct(int[] nums) { int[] dpMax = new int[nums.length]; int[] dpMin = new int[nums.length]; int maxResult = nums[0]; dpMax[0]=nums[0]; dpMin[0]=nums[0]; for (int i=1;i<nums.length;i++) { // 小于0了,一乘就会颠倒,乘以最大的会变成最小的,乘以最小的会变成最大的 if (nums[i]<0) { dpMax[i] = Math.max(dpMin[i-1]*nums[i], nums[i]); dpMin[i] = Math.min(dpMax[i-1]*nums[i], nums[i]); } else { dpMax[i] = Math.max(dpMax[i-1]*nums[i], nums[i]); dpMin[i] = Math.min(dpMin[i-1]*nums[i], nums[i]); } maxResult = Math.max(maxResult, dpMax[i]); } return maxResult; } }
62. 不同路径
class Solution { public int uniquePaths(int m, int n) { // dp[i][j] 表示到 [i][j] 格有多少种不同粒径 int dp[][] = new int[m][n]; // 因为只能向下向右移动,所以到第一列所有格子的路径都只有一种 for (int i=0;i<m;i++) { dp[i][0] = 1; } // 因为只能向下向右移动,所以到第一行所有格子的路径都只有一种 for (int i=0;i<n;i++) { dp[0][i] = 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]; } /* 只能向左和向右走,而且循环的时候也控制了小于边界。所以这个是否在格子里的判断是没有必要的 private boolean isInGrid(int m, int n, int i, int j) { if (i>=0 && i<m && j>=0 && j<n) { return true; } return false; } */ }
64. 最小路径和
class Solution { 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 i=1;i<n;i++) { dp[0][i] += dp[0][i-1] + grid[0][i]; } for (int i=1;i<m;i++) { for (int j=1;j<n;j++) { // 比较出从 左边向右走,和从上边走下来 二者中更小的那个 // dp[i][j] = 从上边和从左边中较小的那个+当前格子的值 dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + grid[i][j]; } } return dp[m-1][n-1]; } /* 因为只能向下走,和向右走,所以不会越界,没有必要判断 private boolean isInGrid(int m,int n,int i,int j) { if (i>=0 && i<m && j>=0 && j<n) { return true; } return false; } */ }
5. 最长回文子串
class Solution { public String longestPalindrome(String s) { int maxLen = 1; int maxLeni = 0; int maxLenj = 0; int N = s.length(); boolean dp[][] = new boolean[N][N]; // dp[i][j] 表示 s[i:j] 是否是最长回文字符串 // dp[i][j] = dp[i+1][j-1],所以要保证先得出 dp[i+1][j-1] // 即 i 递减(i+1到i),j 递增(j-1到j) for (int i=N-1;i>=0;i--) { for (int j=i;j<N;j++) { if (i==j) { // 只有一个字符一定为真 dp[i][j] = true; } else if (j-i==1) { // aa 只有两个字符,这两个字符要完全相等才为真 dp[i][j] = s.charAt(i)==s.charAt(j); } else if (s.charAt(i) == s.charAt(j)) { // aba abba abcba // 依赖 dp[i+1][j-1],所以要保证先得出 dp[i+1][j-1] dp[i][j] = dp[i+1][j-1]; } else { dp[i][j] = false; } if (dp[i][j]) { int thisLen = j-i+1; if (thisLen>maxLen) { maxLen = thisLen; maxLeni = i; maxLenj = j; } } } } return s.substring(maxLeni, maxLenj+1); } }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek “源神”启动!「GitHub 热点速览」
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器