【算法训练营Day27】动态规划part3

文章目录

完全背包理论基础

完全背包与01背包的唯一区别就在于完全背包的每件物品有无数个,可以放入背包多次。

我们从dp四部曲来分析一下完全背包问题:

  • dp数组及其下标:dpij表示物品0 ~ i可多次放入到容量为j的背包中最大价值是多少?
  • 递推关系 :我们从01背包的递推对比来看:
    • 01背包的递推关系:关键点在于物品只能用一次,所以如果要放物品,那么肯定是没有该物品,然后把该物品的位置空出来再进行推导。所以dpij是两种情况取较大值:
      • 放物品i:dpi - 1j - weight\[i] + valuei
      • 不放物品i:dpi - 1j
    • 完全背包的递推关系:关键点在于物品可以使用多次,所以如果要放物品,此时是可以有该物品的,只需要把物品的位置空出来即可。所以dpij是两种情况取较大值:
      • 放物品i:dpij - weight\[i] + valuei
      • 不放物品i:dpi - 1j
  • 初始化:根据推导公式以及dp含义,我们可以将容量为0的那一列先初始化为0,注意到递推式中dpij依赖左边以及上边,所以我们一定要初始化第一行,也就是只放物体0的情况。因为可以多次放物品0,所以dp0j = dp0j - weight\[0] + value0
  • 遍历顺序:在01背包中是从后往前,这是因为一个物品只能取一次,而在完全背包中是从前往后,因为每个物品可以取多次。

完全背包典例

题目链接:52. 携带研究材料

java 复制代码
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        int bag = in.nextInt();
        int[] weight = new int[n];
        int[] values = new int[n];
        for(int i = 0;i < n;i++) {
            weight[i] = in.nextInt();
            values[i] = in.nextInt();
        }
        int[][] dp = new int[n][bag + 1];
        //初始化
        for(int j = 1;j <= bag;j++) if(j - weight[0] >= 0) dp[0][j] = dp[0][j - weight[0]] + values[0];
        //遍历
        for(int i = 1;i < n;i++) {
            for(int j = 1;j <= bag;j++) {
                if(j - weight[i] >= 0) dp[i][j] = Math.max(dp[i][j - weight[i]] + values[i],dp[i - 1][j]);
                else dp[i][j] = dp[i - 1][j];
            }
        }
        System.out.println(dp[n - 1][bag]);
    }
}

零钱兑换II

题目链接:518. 零钱兑换 II

解题逻辑:

这个题就类似于我们在01背包中做过的,把背包装满有多少种情况。所以他的递推式是相加,而不是取较大值。

dp的四部曲分析和上面的典例基本一样,就不在此赘述。

java 复制代码
class Solution {
    public int change(int amount, int[] coins) {
        int n = coins.length;
        int[][] dp = new int[n][amount + 1];
        //初始化
        for(int i = 0;i < n;i++) dp[i][0] = 1;
        for(int j = 1;j <= amount;j++) if(j - coins[0] >= 0) dp[0][j] = dp[0][j - coins[0]];
        //遍历
        for(int i = 1;i < n;i++) {
            for(int j = 1;j <= amount;j++) {
                if(j - coins[i] >= 0) dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]];
                else dp[i][j] = dp[i - 1][j];
            }
        }
        return dp[n - 1][amount];
    }
}

一维dp数组:

java 复制代码
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 = 1;j <= amount;j++) {
                if(j - coins[i] >= 0) dp[j] = dp[j] + dp[j - coins[i]];
            }
        }
        return dp[amount];
    }
}

组合总和 Ⅳ

题目链接:377. 组合总和 Ⅳ

这一题和上面的题很相似,也是求把背包塞满有多少种情况。区别在于上一题是求组合数(也就是不考虑顺序),而本题是求排列数(也就是要考虑顺序),两者的处理区别在于:

  • 如果求组合数就是外层for循环遍历物品,内层for遍历背包。
  • 如果求排列数就是外层for遍历背包,内层for循环遍历物品。

为什么?

  • 先遍历物品,再遍历背包。相当于固定了物品之后,对全容量进行递推,每次外循环新增一个后面的元素,所以dp数组里存的是组合的情况。
  • 先遍历背包,再遍历物品。相当于固定了容量,对物品进行递推,当前容量可以由任意物品组成,进入下一轮容量固定之后,其依赖上一轮的结果,并且此轮也可以由任意物品组成,所以dp数组里面存的是排列的情况。

解题代码如下:

java 复制代码
class Solution {
    public int combinationSum4(int[] nums, int target) {
        int n = nums.length;
        int[] dp = new int[target + 1];
        //递推式:一维dp :dp[j] = dp[j] + dp[j - nums[i]]
        //初始化
        dp[0] = 1;
        //遍历
        for(int j = 1;j <= target;j++) {
            for(int i = 0;i < n;i++) {
                if(j - nums[i] >= 0) dp[j] = dp[j] + dp[j - nums[i]];
            }
        }
        return dp[target];
    }
}

爬楼梯(进阶版)

题目链接:57. 爬楼梯

解题思路同上。

解题代码:

java 复制代码
import java.util.*;

public class Main{
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int bag = in.nextInt();
        int m = in.nextInt();
        int[] dp = new int[bag + 1];
        //递推 dp[j] = dp[j] + dp[j - value[i]]
        dp[0] = 1;
        for(int j = 1;j <= bag;j++) {
            for(int i = 0;i < m;i++) {
                if(j >= i + 1) {
                    dp[j] = dp[j] + dp[j - (i + 1)];
                }
                
            }
        }
        System.out.println(dp[bag]);
    }
}

零钱兑换

题目链接:322. 零钱兑换

解题逻辑:

从dp四部曲分析:

  • dp数组及下标含义:dpj表示装满容量为j的背包最少的硬币个数
  • 递推式:dpij = min(dpi - 1j,dpij - values\[i] + 1),简化为一维dp,则dpj = min(dpj,dpj - values\[i] + 1)
  • 初始化:dp0 = 0,其余全部初始化为Integer.MAX_VALUE - 1,减1是因为防止递推的时候 + 1导致变为负数从而取最小的时候误取。
  • 遍历顺序:先遍历物品,再遍历背包,使用物品的组合可以解决问题。

代码如下:

java 复制代码
class Solution {
    public int coinChange(int[] coins, int amount) {
        int n = coins.length;
        int[] dp = new int[amount + 1];
        //初始化
        for(int i = 1;i <= amount;i++) dp[i] = Integer.MAX_VALUE - 1;
        //遍历
        for(int i = 0;i < n;i++) {
            for(int j = coins[i];j <= amount;j++) {
                dp[j] = Math.min(dp[j],dp[j - coins[i]] + 1);
            }
        }
        return dp[amount] == Integer.MAX_VALUE - 1 || dp[amount] < 0 ? -1 : dp[amount];
    }
}

或者当遍历到Integer.MAX_VALUE直接跳过,也就是如果取当前元素,那么dpj - coins\[i]需要先被取到:

java 复制代码
class Solution {
    public int coinChange(int[] coins, int amount) {
        int n = coins.length;
        int[] dp = new int[amount + 1];
        //初始化
        for(int i = 1;i <= amount;i++) dp[i] = Integer.MAX_VALUE;
        //遍历
        for(int i = 0;i < n;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];
    }
}

完全平方数

题目链接:279. 完全平方数

解题逻辑和上一题一样,代码如下:

java 复制代码
class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n + 1];
        for(int i = 1;i <= n;i++) dp[i] = Integer.MAX_VALUE;
        for(int i = 1;i <= 100;i++) {
            for(int j = i * i;j <= n;j++) {
                if(dp[j - i * i] != Integer.MAX_VALUE) dp[j] = Math.min(dp[j],dp[j - i * i] + 1);
            }
        }
        return dp[n];
    }
}

单词拆分

题目链接:139. 单词拆分

解题逻辑:

从dp四部曲分析:

  • dp数组及下标含义:dpj表示s的前j个元素能否被拼出
  • 递推式:也是分为取和不取两种情况。dpj = dpj || (wordDict.get(i).equals(s.substring(j - wordDict.get(i).length(),j)) && dpj - wordDict.get(i).length())
  • 初始化:dp0 = true
  • 遍历顺序:先遍历背包,再遍历物品,使用物品的排列可以解决问题。

代码逻辑:

java 复制代码
class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        int n = wordDict.size();
        int bag = s.length();
        boolean[] dp = new boolean[bag + 1];
        //初始化
        dp[0] = true;
        for(int j = 1;j <= bag;j++) {
            for(int i = 0;i < n;i++) {
                if(wordDict.get(i).length() <= j) {
                    dp[j] = dp[j] || (wordDict.get(i).equals(s.substring(j - wordDict.get(i).length(),j)) && dp[j - wordDict.get(i).length()]);
                }
            }
        }
        return dp[bag];
    }
}

背包问题总结

关于(一维dp数组)常用的递推公式:

  • 问能否能装满背包(或者最多装多少):dpj = max(dpj, dpj - nums\[i] + numsi)
  • 问装满背包有几种方法:dpj += dpj - nums\[i]
  • 问背包装满最大价值:dpj = max(dpj, dpj - weight\[i] + valuei);
  • 问装满背包所有物品的最小个数:dpj = min(dpj - coins\[i] + 1, dpj)

关于遍历顺序:

  • 01背包
    • 二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
    • 一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历
  • 完全背包
    • 一维dp数组实现,先遍历物品还是先遍历背包需要分情况,且第二层for循环是从小到大遍历。
    • 如果求组合数就是外层for循环遍历物品,内层for遍历背包。
    • 如果求排列数就是外层for遍历背包,内层for循环遍历物品。
相关推荐
春日见几秒前
决策规划控制面经汇总
人工智能·深度学习·算法·机器学习·自动驾驶
Full Stack Developme1 分钟前
Java DFA算法
java·python·算法
fie888910 分钟前
LBP + HOG 特征检测与识别 MATLAB 实现
数据结构·算法·matlab
海天鹰13 分钟前
图片去黑边算法
qt·算法
wabs66615 分钟前
关于动态规划【力扣63.不同路径II与62.不同路径的区别(C++)】自我总结
动态规划
xxwl5851 小时前
一个原创题(二)
c++·算法
moeyui7051 小时前
LeetCode 380:Insert Delete GetRandom O(1) 题解和一些延伸
算法·leetcode·职场和发展
三千里1 小时前
路径规划算法-备忘
算法·自动驾驶·动态规划
退休倒计时1 小时前
【每日一题】LeetCode 15. 三数之和 TypeScript
数据结构·算法·leetcode·typescript
林爷万福1 小时前
MATLAB光谱数据分析从入门到项目实战
算法·光纤光谱仪