动态规划系列
2023.10.19 :今天是第一天,决定先从动态规划开始,计划每天都要坚持刷几道吧,刷到春招
leetcode509:斐波那契数
思路:就是简单的递归斐波那契,也可以用 dp 来做。
class Solution {
public int fib(int n) {
if(n <= 1) return n;
return fib(n-1) + fib(n-2);
}
}
leetcode70:爬楼梯
思路:和 Fibonacci 类似,dp[i] = dp[i-2] + dp[i-1],到达台阶 n,要么是从 n-2 走两步上来,要么就是从 n-1 走一步上来的。
class Solution {
public int climbStairs(int n) {
if(n <= 2) return n;
int[] dp = new int[n+1];
dp[1] = 1;
dp[2] = 2;
for(int i = 3; i <= n; i++) {
dp[i] = dp[i-2] + dp[i-1];
}
return dp[n];
}
}
leetcode746:使用最小花费爬楼梯
思路:到达第 0 阶和第 1 阶都需要花费 0,即 dp 初始值,此后每到达一个位置,最小花费就是 dp[i-2]+cost[i-2] 和 dp[i-1]+cost[i-1] 中最小的,这里指的是到 dp[i-2] 的花费 + 从 dp[i-2] 出发到达该台阶的花费。我们要求的最终结果就是 dp[n],也就是到达最终楼梯顶部。
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
int[] dp = new int[n+1];
dp[0] = 0;
dp[1] = 0;
for(int i = 2; i <= n; i++) {
dp[i] = Math.min(dp[i-2]+cost[i-2], dp[i-1]+cost[i-1]);
}
return dp[n];
}
}
leetcode62:不同路径
思路:关键就是到达每个位置只有两种可能,从左侧或者上方。所以 dp 式子就是 dp[i][j] = dp[i-1][j] + dp[i][j-1],设置好 dp 数组初始化即可。
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for(int i = 0; i < m; i++) {
dp[i][0] = 1;
}
for(int i = 1; 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];
}
}
leetcode63:不同路径II
思路:和 62 一样,也是到达每个位置只有两种可能,从左侧或者上方。但是要注意的是,在初始化和 dp 数组赋值时,障碍物的位置的 dp 要设置为 0.
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int[][] dp = new int[m][n];
for(int i = 0; i < m; i++) {
if(obstacleGrid[i][0] == 1) break;
dp[i][0] = 1;
}
for(int i = 0; i < n; i++) {
if(obstacleGrid[0][i] == 1) break;
dp[0][i] = 1;
}
for(int i = 1; i < m; i++) {
for(int j = 1; j < n; j++) {
if(obstacleGrid[i][j] == 1) {
dp[i][j] = 0;
continue;
}
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
2023.10.20 :第二天,坚持,坚持,坚持
leetcode343:整数拆分
思路:这里 dp[i] 的定义是:分拆数字 i,可以得到的最大乘积为 dp[i]。那么就可以得出,我们需要的递推公式为:dp[i] = Math.max(dp[i], Math.max(dp[i-j] * j, j * (i-j))),这里的意义是 dp[i] 只能是在 dp[i],dp[i-j] * j(即拆分出来 j,乘以 i-j 能拆分得到的最大乘积),j*(i-j)(直接就拆分成 j 和 i-j)三者中取最大的。
class Solution {
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/2; j++) {
dp[i] = Math.max(dp[i], Math.max(dp[i-j]*j, j*(i-j)));
}
}
return dp[n];
}
}
leetcode96:不同的二叉搜索树
思路:本题还是要动手画一下二叉搜索树前三种情况的,可以找出规律,dp 定义为:dp[i]:1 到 i 为节点组成的二叉搜索树的个数为 dp[i] 。所以可以得出 dp[1]=1, dp[2]=2, dp[3]=5,到了 n=3 的时候,可以发现,以 1 为根节点,左子树就是 dp[0],右子树就是 dp[2],所以是 dp[0] * dp[2];而以 2 为根节点,就是 dp[1] * dp[1];以 3 为根节点,就是 dp[2] * dp[0]。因此可以推出递推式子:dp[i] += dp[j-1] * dp[i-j]。
class Solution {
public int numTrees(int n) {
int[] dp = new int[n+1];
dp[0] = 1;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= i; j++) {
dp[i] += dp[j-1] * dp[i-j];
}
}
return dp[n];
}
}
leetcode416:分割等和子集
思路:本题就是经典 01 背包问题,判断能否正好装满 weight=sum/2 的背包。对于 01 背包问题,要注意的是别忘了先对第 0 件物品的 dp 数组初始化。至于背包的 dp 含义,就是如果当前物品比背包容量大,dp[i][j]=dp[i-1][j];如果当前物品可以被装进背包,就看 dp[i-1][j] 和 dp[i-1][j-nums[i]]+nums[i](上一个物品在背包容量为 j-nums[i] 时的 dp 值加上这件物品的价值)谁更大。
class Solution {
public boolean canPartition(int[] nums) {
int n = nums.length;
int sum = 0;
for(int i = 0; i < n; i++) {
sum += nums[i];
}
if(sum % 2 == 1) return false;
int weight = sum / 2;
int[][] dp = new int[n][weight+1];
for(int i = 0; i <= weight; i++) {
if(i >= nums[0]) dp[0][i] = nums[0];
}
for(int i = 1; i < n; i++) {
for(int j = 0; j <= weight; j++) {
if(nums[i] > j) dp[i][j] = dp[i-1][j];
else {
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-nums[i]]+nums[i]);
}
}
}
return dp[n-1][weight] == weight;
}
}
leetcode1049:最后一块石头的重量
思路:关键就是要想到 尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成 01 背包问题了。最后的结果其实就是 sum - dp[n-1][weight] * 2,你可能会想到奇偶的问题,但是 sum/2,就算是奇数。得出的也是小的那个,比如 31 分为 15,16,31 / 2 = 15,最后相撞结果依然是 31 - 15 * 2 = 1。
class Solution {
public int lastStoneWeightII(int[] stones) {
int n = stones.length;
int sum = 0;
for(int i = 0; i < n; i++) {
sum += stones[i];
}
int weight = sum / 2;
int[][] dp = new int[n][weight+1];
for(int i = 0; i <= weight; i++) {
dp[0][i] = (stones[0] <= i) ? stones[0] : 0;
}
for(int i = 1; i < n; i++) {
for(int j = 0; j <= weight; j++) {
if(stones[i] > j) dp[i][j] = dp[i-1][j];
else {
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-stones[i]]+stones[i]);
}
}
}
return sum - dp[n-1][weight] * 2;
}
}
leetcode494:目标和
思路:本题第一眼看以为是回溯,但其实是一道背包问题。我们设定将这些数分为两部分,则有 left - right = target;而 left + right = sum;所以可推出 left = (target + sum) / 2,这样就变成了选定一些数装进大小为 left 的背包有几种方式。
定义二维数组 dp[i][j],其中 dp[i][j] 表示在数组 nums 的前 i 个数中选取元素,使得这些元素之和等于 j 的方案数。这里有一个取巧的点,物品的数量我们设置为 n+1,这样在 dp 数组初始化时,就不用考虑第一个物品装进所有重量背包和前 i 个物品凑进 0 的情况了,直接初始化 dp[0][0] = 1 即可。而 dp 递推式子两种情况分别是如果装不下就 dp[i][j] = dp[i-1][j],否则装得下,那就是 dp[i][j] = dp[i-1][j](不装这个正好能装满的方案数)+ dp[i-1][j-nums[i-1]](去掉当前物品重量时的对应背包方案数,就是意味着装这个物品)
需要注意的是,别忘了 sum < Math.abs(target) 和 (sum + target) % 2 != 0 这两种情况,是不存在能正好装进指定 left 重量背包的方案的。
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int n = nums.length;
int sum = 0;
for(int i = 0; i < n; i++) {
sum += nums[i];
}
if(sum < Math.abs(target) || (sum + target) % 2 != 0) return 0;
int left = (target + sum) / 2;
int[][] dp = new int[n+1][left+1];
dp[0][0] = 1;
for(int i = 1; i <= n; i++) {
for(int j = 0; j <= left; j++) {
if(nums[i-1] > j) dp[i][j] = dp[i-1][j];
else {
dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]];
}
}
}
return dp[n][left];
}
}
leetcode474:一和零
思路:本题第一眼看,确实能看出是背包,但是可能会误认为是多重背包,实际上是一个 01 背包,只不过背包有两个罢了。这里为了避免开三维数组过于混乱,采用的是滚动数组的 01 背包。
外层依旧是遍历物品,然后我们统计当前物品 0 1 的个数,内层遍历背包,要注意的是滚动数组优化的 01 背包,背包的遍历是倒着遍历的,是为了保证物品只被放入一次。
举个例子:物品 0 的重量 weight[0] = 1,价值 value[0] = 15 。如果正序遍历:dp[1] = dp[1 - weight[0]] + value[0] = 15;dp[2] = dp[2 - weight[0]] + value[0] = 30 ,此时 dp[2] 就已经是 30 了,意味着物品 0 被放入了两次,所以不能正序遍历。
倒序遍历就是先算 dp[2],dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0);dp[1] = dp[1 - weight[0]] + value[0] = 15 。所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
为什么二维数组的时候不会,因为那个时候 dp[i][j] 是通过 dp[i-1][j] 得来的,肯定不会重复计算!
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
int[][] dp = new int[m+1][n+1];
for(int i = 0; i < strs.length; i++) {
int zero = 0;
int one = 0;
String str = strs[i];
for(int j = 0; j < str.length(); j++) {
if(str.charAt(j) == '0') zero++;
else one++;
}
for(int k = m; k >= zero; k--) {
for(int l = n; l >= one; l--) {
dp[k][l] = Math.max(dp[k][l], dp[k-zero][l-one]+1);
}
}
}
return dp[m][n];
}
}
2023.10.21:昨天晚上打了游戏,早上没起来,只能下午写写算法题了,再写几天准备找找实习
leetcode518:零钱兑换II
思路:本题就是一个典型的完全背包的使用,完全背包和 01 背包的区别是,01 背包为了防止物品被重复计算,背包遍历是倒着遍历的,遍历到物品重量;而完全背包允许物品重复使用,背包遍历是正向的,从物品重量大小开始正向遍历。除此之外要注意的是:统计有多少种方案与正常的找背包最大价值不同,dp 的式子一般为:dp[j] += dp[j-nums[i]]。
class Solution {
public int change(int amount, int[] coins) {
int[] dp = new int[amount+1];
dp[0] = 1;
for(int i = 0; i < coins.length; i++) {
for(int j = coins[i]; j <= amount; j++) {
dp[j] += dp[j-coins[i]];
}
}
return dp[amount];
}
}
leetcode377:组合总和IV
思路:本题和 518 类似,也是一个完全背包,同样都是求凑够 target 的方案数,区别是 518 求的是组合,本题求的是排列。如果求组合数就是外层for循环遍历物品,内层for遍历背包;如果求排列数就是外层for遍历背包,内层for循环遍历物品。举个例子证明一下:
class Solution {
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target+1];
dp[0] = 1;
for(int i = 1; i <= target; i++) {
for(int j = 0; j < nums.length; j++) {
if(nums[j] <= i) dp[i] += dp[i-nums[j]];
}
}
return dp[target];
}
}
leetcode322:零钱兑换
思路:本题也是完全背包,但是是求装满背包的最小物品数量,所以我们初始化的时候就要把 dp 数组中的每一位初始化到最大值,然后 dp[0] = 0 代表装满 0 有最小物品数为 0。
dp 的递推式子就是:当 dp[j-coins[i]] != Integer.MAX_VALUE 时,即不是初始化的值,代表可能可以凑出方案时,dp[j] = Math.min(dp[j], dp[j-coins[i]]+1)。即在不用这个硬币,或者用这个硬币之中,选择硬币数量使用少的方案。
class Solution {
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 = 0; i < coins.length; i++) {
for(int j = coins[i]; j <= amount; j++) {
if(dp[j-coins[i]] != Integer.MAX_VALUE)
dp[j] = Math.min(dp[j], dp[j-coins[i]]+1);
}
}
return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
}
}
leetcode279:完全平方数
思路:本题也是完全背包,但是我们要求的是凑成 target 的完全平方数的最小数量,因为每个数都可以由无数个 1 凑成,所以就不用加 if(dp[j-values[i]] != max) 判断条件了。
class Solution {
public int numSquares(int n) {
int[] dp = new int[n+1];
Arrays.fill(dp, n+1);
dp[0] = 0;
for(int i = 1; i*i <= n; i++) {
for(int j = i*i; j <= n; j++) {
dp[j] = Math.min(dp[j], dp[j-i*i]+1);
}
}
return dp[n];
}
}
leetcode139:单词拆分
思路:本题我们求的是排列,所以是外层遍历背包,但本题又不是完全的像完全背包,dp 的定义是:字符串长度为 i 的话,dp[i]=true,表示可以拆分为一个或多个在字典中出现的单词。所以我们先把字典收集起来,外层遍历背包,即遍历字符串的每一位,内层我们遍历从 0 到 i,截取 j 到 i 的字符串,如果 dp[j]=true 且 set 种包含截取的 str,说明可以由字典组成,dp[i]=true。
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
boolean[] dp = new boolean[s.length()+1];
Set<String> set = new HashSet<>(wordDict);
dp[0] = true;
for(int i = 1; i <= s.length(); i++) {
for(int j = 0; j < i; j++) {
String str = s.substring(j, i);
if(dp[j] && set.contains(str)) dp[i] = true;
}
}
return dp[s.length()];
}
}
2023.10.23:昨天出去玩了一天,忘写力扣了,今天继续
leetcode198:打家劫舍
思路:经典打家劫舍系列,dp 式子就是:要么偷这家,对应 dp[i-2]+nums[i-1],要么不偷,说明到上一家时拿到的钱比偷这家得到的多,对应 dp[i-1],选两者较大的即可。dp[i] 的含义是,到达第 i 家时偷到的钱最多是多少。
class Solution {
public int rob(int[] nums) {
int n = nums.length;
int[] dp = new int[n+1];
dp[0] = 0;
dp[1] = nums[0];
for(int i = 2; i <= n; i++) {
dp[i] = Math.max(dp[i-1], dp[i-2]+nums[i-1]);
}
return dp[n];
}
}
leetcode213:打家劫舍II
思路:本题又额外限定了规则,不能同时偷第一家和最后一家,那我们就分两种情况讨论,一种是不算最后一家去偷,另一种是不偷第一家,然后取到两个 dp 数组的最后一位比较,取最大的即可。
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if(n == 1) return nums[0];
int[] dp0 = new int[n];
int[] dp1 = new int[n];
dp0[1] = nums[0];
dp1[1] = nums[1];
for(int i = 2; i < n; i++) {
dp0[i] = Math.max(dp0[i-1], dp0[i-2]+nums[i-1]);
}
for(int i = 2; i < n; i++) {
dp1[i] = Math.max(dp1[i-1], dp1[i-2]+nums[i]);
}
return Math.max(dp0[n-1], dp1[n-1]);
}
}
leetcode337:打家劫舍III
思路:本题是打家劫舍系列最后一道,基本思路是,深度遍历,遍历到当前节点时有两种选择:要么偷当前节点不偷子节点,要么偷子节点不偷当前节点。这里我们定义的 dp 数组为一个滚动数组,dp[0] 代表不考虑当前节点,dp[1] 代表偷当前节点,不偷子节点。
class Solution {
public int rob(TreeNode root) {
int[] dp = helper(root);
return Math.max(dp[0], dp[1]);
}
public int[] helper(TreeNode node) {
int[] res = new int[2];
if(node == null) return res;
int[] left = helper(node.left);
int[] right = helper(node.right);
res[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
res[1] = node.val + left[0] + right[0];
return res;
}
}
leetcode121:买卖股票的最佳时机
思路:本题是股票系列问题第一题,对于股票问题,它们设置 dp 数组的含义大都相同,对于本题:dp[i][0] 表示下标为 i 的那天持有股票时手里的最多现金数;dp[i][1] 表示下标为 i 的那天不持有股票时手里最大现金数,即要么保持前一天不持有时手里金额,要么就是按照昨天持有股票时的现金数,今天全部抛出得到的金额。
对于本题,是只允许交易一次,所以 dp[i][0] = Math.max(dp[i-1][0], -prices[i])
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][2];
dp[0][0] = -prices[0];
dp[0][1] = 0;
for(int i = 1; i < n; i++) {
dp[i][0] = Math.max(dp[i-1][0], -prices[i]);
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i]);
}
return dp[n-1][1];
}
}