最长递增子序列
题目
版本1 错误
// 输入一个无序数组, 找到一个最长递增子序列的长度
public int lengthOfLIS(int [] nums) {
if (nums.length == 0) {
return 0;
}
// 明确状态
// 状态就是长度为i的子数组, i为0,1,2....n
// 明确dp[i]含义, 表示0到i, 长度为i + 1的子数组中
// 最长递增子序列是以i结尾的
// 此时的最长递增子序列的长度为x, 即dp[i] = x;
int [] dp = new int[nums.length];
// base case
// 长度为1的子数组, 最长递归子序列的长度只能为1
dp[0] = 1;
// 枚举所有状态
int max = Integer.MIN_VALUE;
for (int i = 1; i < nums.length; i ++) {
// 在当前状态下, 枚举所有可能到达此状态的情况
// 以i结尾, 那么在i之前所有的j, j < i, 如果nums[j] < nums[i], 那么都有可能参与构成递增序列
// 寻找最大的dp[j]
dp[i] = 1;
for (int j = i - 1; j >= 0; j --) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
// 每次保留最大值
max = Math.max(dp[i], max);
}
return max;
}
错误示例
错误原因
(1) 枚举所有状态时, 是从1开始的, 因此数组长度为1时, 是直接返回的max的默认值, 导致出错.
版本2 正确
// 输入一个无序数组, 找到一个最长递增子序列的长度
public int lengthOfLIS(int [] nums) {
if (nums.length == 0) {
return 0;
}
if (nums.length == 1) {
return 1;
}
// 明确状态
// 状态就是长度为i的子数组, i为0,1,2....n
// 明确dp[i]含义, 表示0到i, 长度为i + 1的子数组中
// 最长递增子序列是以i结尾的
// 此时的最长递增子序列的长度为x, 即dp[i] = x;
int [] dp = new int[nums.length];
// base case
// 长度为1的子数组, 最长递归子序列的长度只能为1
dp[0] = 1;
// 枚举所有状态
int max = Integer.MIN_VALUE;
for (int i = 1; i < nums.length; i ++) {
// 在当前状态下, 枚举所有可能到达此状态的情况
// 以i结尾, 那么在i之前所有的j, j < i, 如果nums[j] < nums[i], 那么都有可能参与构成递增序列
// 寻找最大的dp[j]
dp[i] = 1;
for (int j = i - 1; j >= 0; j --) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
// 每次保留最大值
max = Math.max(dp[i], max);
}
return max;
}
俄罗斯套娃信封问题
题目
版本1 正确
// 信封嵌套问题, 求信封最多能嵌套的次数
int maxEnvelopes(int [][] envelopes) {
// 明确状态
// 信封的宽度w 和 高度h
// 明面上的状态是2个, 但是如果我们将信封按照宽度升序排列
// 然后对应的高度会组成一个新的排列, 此时高度大的一定能够套住高度小的.
// 就变成了求高度排列的最长递增子序列的长度.
// 如果出现宽度相同的情况, 为了避免相同宽度, 但是高度不同的信封, 大的套小的
// 将宽度相同, 高度不同的信封, 在排列高度时, 按照降序排列。
int n = envelopes.length;
// 二维数组排序, 利用api方法。
// Arrays.sort()方法默认是升序排列
Arrays.sort(envelopes, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
if (o1[0] == o2[0]) {
// 宽度相等, 高度按照降序排序
return o2[1] - o1[1];
}
// 宽度大的, 排在后面, 即升序排列
return o1[0] - o2[0];
}
});
// 提取高度数组
int [] height = new int[n];
for (int i = 0; i < n; i ++) {
height[i] = envelopes[i][1];
}
// 明确dp数组的含义
// 此时的dp数组, 就变成求最长递增子序列的长度问题了, 不再赘述。
return lengthOfLIS(height);
}
public int lengthOfLIS(int [] nums) {
if (nums.length == 0) {
return 0;
}
if (nums.length == 1) {
return 1;
}
int [] dp = new int[nums.length];
dp[0] = 1;
int max = Integer.MIN_VALUE;
for (int i = 1; i < nums.length; i ++) {
dp[i] = 1;
for (int j = i - 1; j >= 0; j --) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
max = Math.max(dp[i], max);
}
return max;
}
注意点
(1) 注意问题的转化(前提是不能旋转信封)
(2) 注意二维数组排序的写法. 注意Arrays.sort方法的默认排序为升序.
连续子数组的最大和
题目
版本1 错误
// 和最大的子数组 元素有正有负
public int maxSubArray(int [] nums) {
if (nums.length == 0) {
return 0;
}
if (nums.length == 1) {
return nums[0];
}
// 明确状态
// 状态i就是元素的个数, i表示0...i子数组
// 明确dp[i]的含义
// dp[i] = x, 就是在0,1, 2....i的子数组中
// 以i结尾的连续数组的和的最大值为x
int [] dp = new int[nums.length];
// base case
dp[0] = nums[0];
// 枚举所有状态
int max = Integer.MIN_VALUE;
for(int i = 1; i < nums.length; i ++) {
// 因为是子数组, 所以必须是连续的
// 如果dp[i - 1]为正数, 表示有贡献, 就加上, 否则就是nums[i]
dp[i] = Math.max(0, dp[i - 1]) + nums[i];
max = Math.max(dp[i], max);
}
return max;
}
错误的原因
(1) 如果数组长度为2, 同时最大值就是数组的第一个元素, 这种写法, max不会去取值dp[0], 因此出错.
原因是因为比较的时候使用的是Math.max(0, dp[i - 1])
版本2 正确
// 和最大的子数组 元素有正有负
public int maxSubArray(int [] nums) {
if (nums.length == 0) {
return 0;
}
if (nums.length == 1) {
return nums[0];
}
// 明确状态
// 状态i就是元素的个数, i表示0...i子数组
// 明确dp[i]的含义
// dp[i] = x, 就是在0,1, 2....i的子数组中
// 以i结尾的连续数组的和的最大值为x
int [] dp = new int[nums.length];
// base case
dp[0] = nums[0];
// 枚举所有状态
int max = dp[0];
for(int i = 1; i < nums.length; i ++) {
// 因为是子数组, 所以必须是连续的
// 如果dp[i - 1]为正数, 表示有贡献, 就加上, 否则就是nums[i]
dp[i] = Math.max(0, dp[i - 1]) + nums[i];
max = Math.max(dp[i], max);
}
return max;
}
正确的原因
(1) 将max的初始值设为nums[0], 避免出现最大值为负数的情况出错
最长公共序列
题目
版本1 错误
// 最长公共子序列
public int longestCommonSubsequence(String text1, String text2) {
if (text1.length() == 0 || text2.length() == 0) {
return text1.length() + text2.length();
}
// 明确状态
// i和j 分别表示在text1中取前i个字符, 即0,1,2..i的字符串
// j表示在text2中取前j个字符, 即0,1,2..j的字符串
// 明确dp数组的含义
// dp[i][j] = x表示 在text1中前i个字符, 和text2中前j个字符, 构成的最长公共子序列长度为x
int [][] dp = new int[text1.length()][text2.length()];
// base case
// dp[0][j] = 0; dp[i][0] = 0; 无需额外设置
// 枚举所有状态
for (int i = 1; i < text1.length(); i ++) {
for (int j = 1; j < text2.length(); j ++) {
if (text1.charAt(i) == text2.charAt(j)) {
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[text1.length() - 1][text2.length() - 1];
}
错误的原因
(1) 如果dp数组的长度和字符串长度一致, 那么这里的base case写的就不对. 因为dp[0][j]表示的其实是text的第一个字符和text所有字符的关系, 并不是想象的那样.
(2) 因此dp数组和原始条件长度一致时, 一定要注意Base case到底是什么, 这里的base case 应该是dp[0][0], dp[0][1], dp[1][0], 这三个初识值作为条件, 必须手动算出来
版本2 正确
// 最长公共子序列
public int longestCommonSubsequence(String text1, String text2) {
if (text1.length() == 0 || text2.length() == 0) {
return text1.length() + text2.length();
}
// 明确状态
// i和j 分别表示在text1中取前i个字符, 即0,1,2..i的字符串
// j表示在text2中取前j个字符, 即0,1,2..j的字符串
// 明确dp数组的含义
// dp[i][j] = x表示 在text1中前i个字符, 和text2中前j个字符, 构成的最长公共子序列长度为x
// dp数组在构造时多增加一位长度, base case就会变的很简单
int [][] dp = new int[text1.length() + 1][text2.length() + 1];
// base case
// dp[0][j] = 0; dp[i][0] = 0; 无需额外设置
// 枚举所有状态
for (int i = 1; i <= text1.length(); i ++) {
for (int j = 1; j <= text2.length(); 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[text1.length()][text2.length()];
}
编辑距离
题目
版本1 错误
public int minDistance(String word1, String word2) {
if (word1.length() == 0 || word2.length() == 0) {
return word1.length() + word2.length();
}
// 明确状态
// 两个字符串, 就有两个状态i和j
// 分为表示在word1, 和 word2中指的位置
// 明确dp数组含义
// dp[i][j] = x表示word1前i个字符, 变成word2前j个字符所需要的最少操作步骤.
int [][] dp = new int[word1.length() + 1][word2.length() + 1];
// base case
// dp[0][j] 如果word1中没有字符, 变成word2的任意j, 就需要插入j次
for(int j = 0; j < word2.length(); j ++) {
dp[0][j] = j;
}
// 同理dp[i][0] 需要删除i次
for(int i = 0; i < word1.length(); i ++) {
dp[i][0] = i;
}
// 枚举所有状态
// 将word1变成word2
for(int i = 1; i <= word1.length(); i ++) {
for (int j = 1; j <= word2.length(); j ++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
// 啥也不用做
dp[i][j] = dp[i - 1][j - 1];
} else {
// 因为已经计算到i和j了, 所以此时word1和word2前i - 1个和前j - 1个一定是相同的了。
// 假设现在word1是abce, i指向e
// word2是abcd, j指向d
// 现在必须执行插入操作
// 那么word1变成abcde, 发现word1的长度不再是i了, 不符合dp的定义
// 因此word1要想执行插入操作, 必须先删除了i元素, 才可以。
// 那么wod1删除i元素就是子问题当word1 = abce和word2 = abc的时候, dp的结果
// 就是dp[i][j - 1];
// 因此如果必须执行插入操作的话, 就必须由子问题dp[i][j - 1]的答案得到
int insert = dp[i][j - 1] + 1;
// 选择替换
int replace = dp[i - 1][j - 1] + 1;
// 选择删除 同选择查找的逻辑
int del = dp[i - 1][j] + 1;
dp[i][j] = Math.min(insert, Math.min(replace, del));
}
}
}
return dp[word1.length()][word2.length()];
}
错误的原因
(1) 注意如果要执行插入操作, 必须要先执行一次删除, 才能执行插入. 同理, 如果要删除, 必须先执行一次插入. 因此会转化成相应的子问题.
(2) base case那里写的有问题, 当i == word1.length()和 j = word2.length()的时候都没包含上, 到底有些结果是错误的. 淦
版本2 正确
public int minDistance(String word1, String word2) {
if (word1.length() == 0 || word2.length() == 0) {
return word1.length() + word2.length();
}
// 明确状态
// 两个字符串, 就有两个状态i和j
// 分为表示在word1, 和 word2中指的位置
// 明确dp数组含义
// dp[i][j] = x表示word1前i个字符, 变成word2前j个字符所需要的最少操作步骤.
int [][] dp = new int[word1.length() + 1][word2.length() + 1];
// base case
// dp[0][j] 如果word1中没有字符, 变成word2的任意j, 就需要插入j次
// 注意j可以取word2.length()
for(int j = 0; j <= word2.length(); j ++) {
dp[0][j] = j;
}
// 同理dp[i][0] 需要删除i次
// 注意i可以取word1.length()
for(int i = 0; i <= word1.length(); i ++) {
dp[i][0] = i;
}
// 枚举所有状态
// 将word1变成word2
for(int i = 1; i <= word1.length(); i ++) {
for (int j = 1; j <= word2.length(); j ++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
// 啥也不用做
dp[i][j] = dp[i - 1][j - 1];
} else {
// 因为已经计算到i和j了, 所以此时word1和word2前i - 1个和前j - 1个一定是相同的了。
// 假设现在word1是abce, i指向e
// word2是abcd, j指向d
// 现在必须执行插入操作
// 那么word1变成abcde, 发现word1的长度不再是i了, 不符合dp的定义
// 因此word1要想执行插入操作, 必须先删除了i元素, 才可以。
// 那么wod1删除i元素就是子问题当word1 = abce和word2 = abc的时候, dp的结果
// 就是dp[i][j - 1];
// 因此如果必须执行插入操作的话, 就必须由子问题dp[i][j - 1]的答案得到
int insert = dp[i][j - 1] + 1;
// 选择替换
int replace = dp[i - 1][j - 1] + 1;
// 选择删除 同选择查找的逻辑
int del = dp[i - 1][j] + 1;
dp[i][j] = Math.min(insert, Math.min(replace, del));
}
}
}
return dp[word1.length()][word2.length()];
}
最长回文子序列
题目
版本1 错误
// 最长回文子序列
public int longestPalindromeSubseq(String s) {
// 明确状态
// i和j表示字符串s, i到j这一段子串
// 明确dp数组含义
// dp[i][j] = x 表示字符串s中, 索引下标在i....j 这段子串的最长回文序列的长度x
int [][] dp = new int[s.length()][s.length()];
// base case
for (int i = 0; i < s.length(); i ++) {
for (int j = 0; j < s.length(); j ++) {
dp[i][j] = 1;
}
}
// 枚举所有状态.
// 因为base是对角线上, 因此枚举的时候, 也需要斜着枚举
// 这样子问题的答案才有值
for(int l = 1; l < s.length(); l ++) {
// l表示两个索引之间的距离
// base case设置了l = 0的情况
for (int i = 0; i < s.length() - 1; i ++) {
int j = i + l;
if (j > s.length() - 1) {
break;
}
if (s.charAt(i) == s.charAt(j)) {
// 当l = 1的时候, 虽然此时的j > i了, 但是因为此时的dp[i][j]=0, 所以没关系
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
// 如果元素不想等, 因为求的是回文子序列
// 因此不像子数组那样会直接失败, 而是有值的
dp[i][j] = dp[i + 1][j - 1];
}
}
}
return dp[0][s.length() - 1];
}
错误的原因
(1) 当元素不相同的时候, 不应该直接舍弃两步的元素dp[i][j] = dp[i + 1][j - 1];, 而是应该在两边分别舍弃元素, 取最大值
版本2 错误
// 最长回文子序列
public int longestPalindromeSubseq(String s) {
// 明确状态
// i和j表示字符串s, i到j这一段子串
// 明确dp数组含义
// dp[i][j] = x 表示字符串s中, 索引下标在i....j 这段子串的最长回文序列的长度x
int [][] dp = new int[s.length()][s.length()];
// base case
for (int i = 0; i < s.length(); i ++) {
for (int j = 0; j < s.length(); j ++) {
dp[i][j] = 1;
}
}
// 枚举所有状态.
// 因为base是对角线上, 因此枚举的时候, 也需要斜着枚举
// 这样子问题的答案才有值
for(int l = 1; l < s.length(); l ++) {
// l表示两个索引之间的距离
// base case设置了l = 0的情况
for (int i = 0; i < s.length() - 1; i ++) {
int j = i + l;
if (j > s.length() - 1) {
break;
}
if (s.charAt(i) == s.charAt(j)) {
// 当l = 1的时候, 虽然此时的j > i了, 但是因为此时的dp[i][j]=0, 所以没关系
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
// 如果元素不想等, 因为求的是回文子序列
// 因此不像子数组那样会直接失败, 而是有值的
// 需要在两边舍弃元素
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return dp[0][s.length() - 1];
}
错误的原因
(1) base case那里写错了, 这种写法是将整个数组全都赋值成1了, 而我们期望的是i==j的时候才是1
版本3
// 最长回文子序列
public static int longestPalindromeSubseq(String s) {
// 明确状态
// i和j表示字符串s, i到j这一段子串
// 明确dp数组含义
// dp[i][j] = x 表示字符串s中, 索引下标在i....j 这段子串的最长回文序列的长度x
int [][] dp = new int[s.length()][s.length()];
// base case
for (int i = 0; i < s.length(); i ++) {
for (int j = 0; j < s.length(); j ++) {
if (i == j) {
dp[i][j] = 1;
}
}
}
// 枚举所有状态.
// 因为base是对角线上, 因此枚举的时候, 也需要斜着枚举
// 这样子问题的答案才有值
for(int l = 1; l < s.length(); l ++) {
// l表示两个索引之间的距离
// base case设置了l = 0的情况
for (int i = 0; i < s.length() - 1; i ++) {
int j = i + l;
if (j > s.length() - 1) {
break;
}
if (s.charAt(i) == s.charAt(j)) {
// 当l = 1的时候, 虽然此时的j > i了, 但是因为此时的dp[i][j]=0, 所以没关系
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
// 如果元素不想等, 因为求的是回文子序列
// 因此不像子数组那样会直接失败, 而是有值的
// 需要在两边舍弃元素
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return dp[0][s.length() - 1];
}
让字符串成为回文串的最少插入次数
题目
版本1 正确
// 让字符串成为回文串的最小插入次数
public int minInsertions(String s) {
if (s.length() == 0 || s.length() == 1) {
return 0;
}
// 明确状态 i和j表示字符串s中的索引
// i, i + 1, ...., j -1, j就表示s的一个子串
// 明确dp数组的含义
// dp[i][j] = x, 就表示使得i到j的子串构成回文串需要的最少插入次数是x
int [][] dp = new int[s.length()][s.length()];
// base case
// 当i == j 的时候, dp[i][j] = 0; 已经满足
// 枚举状态
// 需要歇着遍历
for (int l = 1; l < s.length(); l ++) {
for (int i = 0; i < s.length(); i ++) {
int j = i + l;
if (j >= s.length()) {
break;
}
if (s.charAt(i) == s.charAt(j)) {
// 无需插入操作
dp[i][j] = dp[i + 1][j - 1];
} else {
// 如果想让i....j是回文串, 并且i + 1, ...., j - 1已经是回文串了
// 那么只需要在i + 1,...j的基础上插入一个字符即可
// 或者在i,...j - 1的基础上插入一个字符
// 最差情况是在i + 1,...,j - 1的基础上插入两个字符, 但是已经包含在上述情况中了
dp[i][j] = Math.min(dp[i + 1][j], dp[i][j - 1]) + 1;
}
}
}
return dp[0][s.length() - 1];
}
正则表达式
题目
版本1 错误
public boolean isMatch(String s, String p) {
// 明确状态
// 两个指针, 一个i指向s中的字符, 一个j指向p中的字符
// 明确dp数组的含义
// dp[i][j] = x, 表示s中的前i个字符, 能否被p中的前j个字符匹配, 匹配x为true
boolean [][] dp = new boolean[s.length()][p.length()];
// base case
// 枚举状态
for(int i = 0; i < s.length(); i ++) {
for (int j = 0; j < p.length(); j ++) {
if (p.charAt(j) == '.' || p.charAt(j) == s.charAt(i)) {
dp[i][j] = Boolean.TRUE;
} else {
}
}
}
}
错误的原因
(1) 这里, 当前的问题, 无法由已经解决了的子问题得到, 因此自底向上的方式是不行的, 只能通过递归 + 备忘录的方式解决.
版本2 错误
Map<String, Boolean> visited = new HashMap<>();
public boolean isMatch(String s, String p) {
// 明确状态
// 两个指针, 一个i指向s中的字符, 一个j指向p中的字符
// 明确递归函数的含义
// digui(s, i, p, j) 表示s中的从i开始的所有字符, 能否被p中从j开始的所有字符匹配, 匹配x为true
return digui(s, 0, p, 0);
}
public boolean digui(String s, int i, String p, int j) {
if (visited.containsKey(i + ":" + j)) {
return visited.get(i + ":" + j);
}
// base case
if (j == p.length()) {
// 匹配字符串走到末尾的时候, 待匹配字符串是否匹配完毕了
return i == s.length();
}
boolean ans;
if (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') {
// p的下一个字符是不是*
if (j + 1 < p.length() && p.charAt(j + 1) == '*') {
// 如果是* 就可以匹配0次或者多次
ans = digui(s, i + 1, p, j) || digui(s, i, p, j + 2);
} else {
// 当前匹配成功, 继续匹配下一个
ans = digui(s, i + 1, p, j + 1);
}
} else {
// p的下一个字符是不是*
if (j + 1 < p.length() && p.charAt(j + 1) == '*') {
// 如果是* 就可以是0次
ans = digui(s, i , p, j + 2);
} else {
// 匹配失败
ans = Boolean.FALSE;
}
}
visited.put(i + ":" + j, ans);
return ans;
}
错误的原因
(1) i一定是最多加1, 但是关于i没有定义base case, 就会导致i数组越界
(2) j有+2的情况, 但是都是j的下一个是*的情况, 也就是说j + 2最多也就是等于p.length(), 不会超出p.length()的数值
版本3 正确
class Solution {
Map<String, Boolean> visited = new HashMap<>();
public boolean isMatch(String s, String p) {
// 明确状态
// 两个指针, 一个i指向s中的字符, 一个j指向p中的字符
// 明确递归函数的含义
// digui(s, i, p, j) 表示s中的从i开始的所有字符, 能否被p中从j开始的所有字符匹配, 匹配x为true
return digui(s, 0, p, 0);
}
public boolean digui(String s, int i, String p, int j) {
if (visited.containsKey(i + ":" + j)) {
return visited.get(i + ":" + j);
}
// base case
// base case
if (i == s.length()) {
// p中如果还剩下字符, 那么必须是和*成对出现的才可以
while (j < p.length()) {
if (j + 1 < p.length() && p.charAt(j + 1) == '*') {
j += 2;
} else {
return Boolean.FALSE;
}
}
return Boolean.TRUE;
}
if (j == p.length()) {
// 匹配字符串走到末尾的时候, 待匹配字符串是否匹配完毕了
return i == s.length();
}
boolean ans;
if (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.') {
// p的下一个字符是不是*
if (j + 1 < p.length() && p.charAt(j + 1) == '*') {
// 如果是* 就可以匹配0次或者多次
ans = digui(s, i + 1, p, j) || digui(s, i, p, j + 2);
} else {
// 当前匹配成功, 继续匹配下一个
ans = digui(s, i + 1, p, j + 1);
}
} else {
// p的下一个字符是不是*
if (j + 1 < p.length() && p.charAt(j + 1) == '*') {
// 如果是* 就可以是0次
ans = digui(s, i , p, j + 2);
} else {
// 匹配失败
ans = Boolean.FALSE;
}
}
visited.put(i + ":" + j, ans);
return ans;
}
}
正确的原因
(1) 一定要注意base case 不能想当然.
高楼扔鸡蛋
题目
版本1 错误
public int superEggDrop(int k, int n) {
// 明确状态
// k个鸡蛋, n层楼, 状态就是楼层数和鸡蛋数量
// 明确递归的含义
// 这里的当前问题不能由已经解决的子问题得到答案, 因此更像是加了备忘录的回溯算法
// digui(k, n) 的含义就是, 有k枚鸡蛋, 在n层楼的时候, 最坏情况下需要扔几次
return digui(k, n);
}
public int digui(int k, int n) {
// base case
if (k == 1) {
// 如果只有一枚鸡蛋, 那么一定是在最后一层楼扔鸡蛋会破
return n;
}
if (n == 0) {
return 0;
}
// 枚举从哪一层楼开始扔
int min = Integer.MAX_VALUE;
for(int i = 0; i < n; i ++) {
// 最差情况
int max = Math.max(digui(k - 1, i), digui(k, n - i));
// 最少的次数
min = Math.min(min, max);
}
return min;
}
错误的原因
(1) 在枚举从哪一层开始的时候, 从0开始枚举的, 应该从1开始枚举, 不然n - 0 一直是n就死循环了
(2) 当鸡蛋碎了的时候, 下一个楼层应该是i - 1, 而不是i
(3) 在选择了第i层扔的时候, max的结果要 + 1, 表示这一层扔的次数
(4) 没有采用备忘录
版本2 错误
Map<String, Integer> visited = new HashMap<>();
public int superEggDrop(int k, int n) {
// 明确状态
// k个鸡蛋, n层楼, 状态就是楼层数和鸡蛋数量
// 明确递归的含义
// 这里的当前问题不能由已经解决的子问题得到答案, 因此更像是加了备忘录的回溯算法
// digui(k, n) 的含义就是, 有k枚鸡蛋, 在n层楼的时候, 最坏情况下需要扔几次
return digui(k, n);
}
public int digui(int k, int n) {
// base case
if (k == 1) {
// 如果只有一枚鸡蛋, 那么一定是在最后一层楼扔鸡蛋会破
return n;
}
if (n == 0) {
return 0;
}
if (visited.containsKey(k + ":" + n)) {
return visited.get(k + ":" + n);
}
// 枚举从哪一层楼开始扔
int min = Integer.MAX_VALUE;
for(int i = 1; i <= n; i ++) {
// 最差情况
int max = Math.max(digui(k - 1, i - 1), digui(k, n - i)) + 1;
// 最少的次数
min = Math.min(min, max);
}
visited.put(k + ":" + n, min);
return min;
}
错误的原因
(1) 当楼层数过多的时候, 还是会超时
版本3 错误
static Map<String, Integer> visited = new HashMap<>();
public static int superEggDrop(int k, int n) {
// 明确状态
// k个鸡蛋, n层楼, 状态就是楼层数和鸡蛋数量
// 明确递归的含义
// 这里的当前问题不能由已经解决的子问题得到答案, 因此更像是加了备忘录的回溯算法
// digui(k, n) 的含义就是, 有k枚鸡蛋, 在n层楼的时候, 最坏情况下需要扔几次
return digui(k, n);
}
public static int digui(int k, int n) {
// base case
if (k == 1) {
// 如果只有一枚鸡蛋, 那么一定是在最后一层楼扔鸡蛋会破
return n;
}
if (n == 0) {
return 0;
}
if (visited.containsKey(k + ":" + n)) {
return visited.get(k + ":" + n);
}
// 枚举从哪一层楼开始扔
// 最差情况是digui(k - 1, i - 1), digui(k, n - i)两个函数中值较大的一个
// 自变量是i, digui(k - 1, i - 1)随着i的增大而增大
// digui(k, n - i)随着i的增大而减少
// 因此两个函数会有一个交点, 此时的值就是我们要求的min
// 那么现在我们只需要找到一个i, 使得digui(k - 1, i - 1) == digui(k, n - i)即可
// 利用二分查找法, 在1....n中寻找i
// 需要注意的是, 两条直线在n比较小的时候可能并不会真正相交, 因此这个思路只是指导我们如何加快搜索
int left = 1;
int right = n;
int min = Integer.MAX_VALUE;
while (left <= right) {
int mid = left + (right - left) / 2;
if (digui(k - 1, mid - 1) == digui(k, n - mid)) {
min = Math.min(min, digui(k, n - mid));
break;
}
if (digui(k - 1, mid - 1) > digui(k, n - mid)) {
min = Math.min(min, digui(k, n - mid));
right = mid - 1;
}
if (digui(k - 1, mid - 1) < digui(k, n - mid)) {
min = Math.min(min, digui(k - 1, mid - 1));
left = mid + 1;
}
}
visited.put(k + ":" + n, min);
return min;
}
错误的原因
(1) 两个函数在给定参数的情况下, 是不一定会相交的, 因此没有相等的情况下, 也需要计算一次min
(2) 两个函数不相等的情况下, 取的是函数的最大值, 不是最小值, 就是这里min = Math.min(min, digui(k, n - mid));
(3) 千万注意, 计算出最小值了, 记得 + 1, 算上当前的次数
版本4 正确
static Map<String, Integer> visited = new HashMap<>();
public static int superEggDrop(int k, int n) {
// 明确状态
// k个鸡蛋, n层楼, 状态就是楼层数和鸡蛋数量
// 明确递归的含义
// 这里的当前问题不能由已经解决的子问题得到答案, 因此更像是加了备忘录的回溯算法
// digui(k, n) 的含义就是, 有k枚鸡蛋, 在n层楼的时候, 最坏情况下需要扔几次
return digui(k, n);
}
public static int digui(int k, int n) {
// base case
if (k == 1) {
// 如果只有一枚鸡蛋, 那么一定是在最后一层楼扔鸡蛋会破
return n;
}
if (n == 0) {
return 0;
}
if (visited.containsKey(k + ":" + n)) {
return visited.get(k + ":" + n);
}
// 枚举从哪一层楼开始扔
// 最差情况是digui(k - 1, i - 1), digui(k, n - i)两个函数中值较大的一个
// 自变量是i, digui(k - 1, i - 1)随着i的增大而增大
// digui(k, n - i)随着i的增大而减少
// 因此两个函数会有一个交点, 此时的值就是我们要求的min
// 那么现在我们只需要找到一个i, 使得digui(k - 1, i - 1) == digui(k, n - i)即可
// 利用二分查找法, 在1....n中寻找i
// 需要注意的是, 两条直线在n比较小的时候可能并不会真正相交, 因此这个思路只是指导我们如何加快搜索
int left = 1;
int right = n;
int min = Integer.MAX_VALUE;
while (left <= right) {
int mid = left + (right - left) / 2;
if (digui(k - 1, mid - 1) == digui(k, n - mid)) {
min = Math.min(min, digui(k, n - mid));
break;
}
if (digui(k - 1, mid - 1) > digui(k, n - mid)) {
// 根据思路, 是要取大的值
min = Math.min(min, digui(k - 1, mid - 1));
right = mid - 1;
}
if (digui(k - 1, mid - 1) < digui(k, n - mid)) {
// 根据思路, 是要取大的值
min = Math.min(min, digui(k, n - mid));
left = mid + 1;
}
}
visited.put(k + ":" + n, min + 1);
return min + 1;
}
戳气球
题目
版本1
// 戳气球
public int maxCoins(int[] nums) {
// 明确状态
// 状态就是气球的范围,
// 明确dp数组含义
// dp[i][j] = x; 表示在i....j 之间戳破气球能够得到的最大硬币数目x
// 不包括i气球和j气球
// 需要填充首尾两个额外的气球
int [] newNums = new int[nums.length + 2];
for (int i = 0; i < newNums.length; i ++) {
if (i == 0 || i == newNums.length - 1) {
newNums[i] = 1;
} else {
newNums[i] = nums[i - 1];
}
}
int [][] dp = new int[newNums.length][newNums.length];
// base case
// dp[i][i + 1] = 0; 已经满足
// 枚举所有状态
// 斜着遍历
for (int l = 2; l < newNums.length; l ++) {
for (int i = 0; i < newNums.length; i ++) {
int j = i + l;
if (j >= newNums.length) {
break;
}
// 枚举i...j 之间最后一个戳破的气球是哪个
for (int k = i + 1; k < j; k ++) {
dp[i][j] = Math.max(dp[i][j], dp[i][k] + dp[k][j] + newNums[i] * newNums[j] * newNums[k]);
}
}
}
return dp[0][newNums.length - 1];
}
01背包问题
题目
版本1 错误
// 背包最大价值
public int pacakageMaxValue(int w, int [] val, int [] wt) {
if (w == 0 || val.length == 0) {
return 0;
}
// w是背包重量, val是不同物品的价值, wt是物品对应的重量
// 明确状态
// 背包的容量, 选择的物品
// 明确dp数组的含义
// dp[i][w] = x, 表示选择前i种物品, 此时背包的容量是w, 里面的最大价值是x
int [][] dp = new int[val.length + 1][w + 1];
// base case
// dp[0][w] = 0; dp[i][0] = 0; 已经满足
for(int i = 1; i <= val.length; i ++) {
for (int j = 1; j <= w; j ++) {
if (j - wt[i - 1] >= 0) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - wt[i - 1]] + val[i - 1]);
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[val.length][w];
}
错误的原因
(1) 在选择将第i个物品放入背包时候, 应该用的是dp[i - 1][j - wt[i - 1]] + val[i - 1], 而不是dp[i][j - wt[i - 1]] + val[i - 1], 因为每个物品只有一个.
版本2
// 背包最大价值
public int pacakageMaxValue(int w, int [] val, int [] wt) {
if (w == 0 || val.length == 0) {
return 0;
}
// w是背包重量, val是不同物品的价值, wt是物品对应的重量
// 明确状态
// 背包的容量, 选择的物品
// 明确dp数组的含义
// dp[i][w] = x, 表示选择前i种物品, 此时背包的容量是w, 里面的最大价值是x
int [][] dp = new int[val.length + 1][w + 1];
// base case
// dp[0][w] = 0; dp[i][0] = 0; 已经满足
for(int i = 1; i <= val.length; i ++) {
for (int j = 1; j <= w; j ++) {
if (j - wt[i - 1] >= 0) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - wt[i - 1]] + val[i - 1]);
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[val.length][w];
}
分割等和子集合
题目
版本1 正确
// 将一个数组中的元素, 分割成两个子集, 两个子集和相等.
public boolean canPartition(int[] nums) {
if (nums.length == 0) {
return Boolean.TRUE;
}
// 问题转化
// 分割两个子集, 且和相等, 等同于在数组中挑选几个数字, 这几个数字的和刚好是数组和的一半
// 明确状态
// 当前挑选的数字, 以及当前的数字和
int sum = IntStream.of(nums).sum();
if (sum % 2 != 0) {
// 不可能分成两个整数集合
return Boolean.FALSE;
}
sum = sum / 2;
// 明确dp数组的含义
// dp[i][j] = x, 表示在nums中, 挑选前i个数字, 刚好构成了j, 如果构成x为true
boolean [][] dp = new boolean[nums.length + 1][sum + 1];
// base case
// dp[0][j] = false; dp[i][0] = true;
for (int i = 1; i <= nums.length; i ++) {
dp[i][0] = Boolean.TRUE;
}
// 枚举所有状态
for(int i = 1; i <= nums.length; i ++) {
for (int j = 1; j <= sum; j ++) {
if (j - nums[i - 1] >= 0) {
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[nums.length][sum];
}
零钱兑换2
题目
版本1 错误
public int change(int amount, int[] coins) {
if (amount == 0) {
return 1;
}
if (coins.length == 0) {
return 0;
}
// 不同于将数组分割成两个子集的问题
// 那里面数组中每个元素都只能选一次, 但是这里每个硬币的数量是无限的
// 明确状态
// 硬币的种类, 当前的金额
// 明确dp数组的含义
// dp[i][j] = x, 表示选择前i中硬币, 能够构成金额j的可能种数为x
int [][] dp = new int[coins.length + 1][amount + 1];
// base case
// dp[0][j] = 0; dp[i][0] = 1;
for (int i = 1; i <= coins.length; i ++) {
dp[i][0] = 1;
}
// 枚举所有状态
for (int i = 1; i <= coins.length; i ++) {
for (int j = 1; j <= amount; j ++) {
if (j - coins[i - 1] >= 0) {
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - coins[i - 1]];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[coins.length][amount];
}
错误的原因
(1) dp[i - 1][j - coins[i - 1]] 如果每样硬币只有一个, 那么应该是这用写法, 但是每样硬币如果是无限个的话, 应该写成dp[i][j - coins[i - 1]], 就表示随着j的增大, 允许使用多次i
版本2 正确
public int change(int amount, int[] coins) {
if (amount == 0) {
return 1;
}
if (coins.length == 0) {
return 0;
}
// 不同于将数组分割成两个子集的问题
// 那里面数组中每个元素都只能选一次, 但是这里每个硬币的数量是无限的
// 明确状态
// 硬币的种类, 当前的金额
// 明确dp数组的含义
// dp[i][j] = x, 表示选择前i中硬币, 能够构成金额j的可能种数为x
int [][] dp = new int[coins.length + 1][amount + 1];
// base case
// dp[0][j] = 0; dp[i][0] = 1;
for (int i = 1; i <= coins.length; i ++) {
dp[i][0] = 1;
}
// 枚举所有状态
for (int i = 1; i <= coins.length; i ++) {
for (int j = 1; j <= amount; j ++) {
if (j - coins[i - 1] >= 0) {
dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[coins.length][amount];
}
打家劫舍
题目
版本1 正确
Map<Integer, Integer> visited = new HashMap<>();
public int rob(int[] nums) {
// 明确状态
// 当前在哪间房屋
// 明确dp函数
// 当前的问题不能够由已经解决的子问题得到答案, 因此需要递归
// x = digui(nums, i), 表示从第i间房屋开始, 能够得到的最大的金钱x.
return digui(nums, 0);
}
public int digui(int [] nums, int i) {
if (i >= nums.length) {
return 0;
}
if (visited.containsKey(i)) {
return visited.get(i);
}
int ans = Integer.MIN_VALUE;
// 处于每个状态下, 都可以选择打劫还是不打劫
// 选择打劫
ans = Math.max(ans, digui(nums, i + 2) + nums[i]);
// 选择不打劫
ans = Math.max(ans, digui(nums, i + 1));
visited.put(i, ans);
return ans;
}
打家劫舍2
题目
版本1 错误
// 环形房屋, 加个标志位, 如果第一个房子打劫了, 最后一个房子就不要打劫
boolean flag = Boolean.FALSE;
Map<Integer, Integer> visited = new HashMap<>();
public int rob(int[] nums) {
return digui(nums, 0);
}
public int digui(int [] nums, int i) {
if (i >= nums.length) {
return 0;
}
if (i != nums.length - 1 && visited.containsKey(i)) {
return visited.get(i);
}
int ans = Integer.MIN_VALUE;
if(i == 0) {
// 标记第一个房屋打劫了
flag = Boolean.TRUE;
}
if (i != nums.length - 1 || !flag) {
// 选择打劫
ans = Math.max(ans, digui(nums, i + 2) + nums[i]);
}
if(i == 0) {
// 撤销第一个房屋的标记
flag = Boolean.FALSE;
}
// 选择不打劫
ans = Math.max(ans, digui(nums, i + 1));
visited.put(i, ans);
return ans;
}
错误的原因
(1) 如果nums只有一个元素的话, 这样第一个房屋和最后一个房屋就是同一个, 就没办法正确得到打劫第一个房屋的钱了, 因此要单独处理这种情况
版本2 错误
// 环形房屋, 加个标志位, 如果第一个房子打劫了, 最后一个房子就不要打劫
static boolean flag = Boolean.FALSE;
static Map<Integer, Integer> visited = new HashMap<>();
public static int rob(int[] nums) {
if (nums.length == 1) {
return nums[0];
}
return digui(nums, 0);
}
public static int digui(int [] nums, int i) {
if (i >= nums.length) {
return 0;
}
if (i != nums.length - 1 && visited.containsKey(i)) {
return visited.get(i);
}
int ans = Integer.MIN_VALUE;
if(i == 0) {
// 标记第一个房屋打劫了
flag = Boolean.TRUE;
}
if (i != nums.length - 1 || !flag) {
// 选择打劫
ans = Math.max(ans, digui(nums, i + 2) + nums[i]);
}
if(i == 0) {
// 撤销第一个房屋的标记
flag = Boolean.FALSE;
}
// 选择不打劫
ans = Math.max(ans, digui(nums, i + 1));
visited.put(i, ans);
return ans;
}
错误的原因
(1) 在标志位不同的情况, visited所有的结果都是无法复用的, 因此必须清空map, 重新计算
(2) 同时在使用map数据的时候, 也无需判断是否是最后一个了
版本3 正确
// 环形房屋, 加个标志位, 如果第一个房子打劫了, 最后一个房子就不要打劫
boolean flag = Boolean.FALSE;
Map<Integer, Integer> visited = new HashMap<>();
public int rob(int[] nums) {
if (nums.length == 1) {
return nums[0];
}
return digui(nums, 0);
}
public int digui(int [] nums, int i) {
if (i >= nums.length) {
return 0;
}
if (visited.containsKey(i)) {
return visited.get(i);
}
int ans = Integer.MIN_VALUE;
if(i == 0) {
// 标记第一个房屋打劫了
flag = Boolean.TRUE;
}
if (i != nums.length - 1 || !flag) {
// 选择打劫
ans = Math.max(ans, digui(nums, i + 2) + nums[i]);
}
if(i == 0) {
// 撤销第一个房屋的标记
flag = Boolean.FALSE;
visited = new HashMap<>();
}
// 选择不打劫
ans = Math.max(ans, digui(nums, i + 1));
visited.put(i, ans);
return ans;
}
打家劫舍3
题目
方案1 正确
// 树形打家劫舍
Map<TreeNode, Integer> visited = new HashMap<>();
public int rob(TreeNode root) {
return digui(root);
}
public int digui(TreeNode root) {
if (root == null) {
return 0;
}
if (visited.containsKey(root)) {
return visited.get(root);
}
int ans = 0;
// 选择打劫
int left = 0;
if (root.left != null) {
left = digui(root.left.left) + digui(root.left.right);
}
int right = 0;
if (root.right != null) {
right = digui(root.right.left) + digui(root.right.right);
}
ans = Math.max(ans, root.val + left + right);
// 不选择打劫
ans = Math.max(ans, digui(root.left) + digui(root.right));
visited.put(root, ans);
return ans;
}