刷题记录08
卡码网52. 携带研究材料(第七期模拟笔试)
52. 携带研究材料(第七期模拟笔试)
题目描述
小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的重量,并且具有不同的价值。
小明的行李箱所能承担的总重量是有限的,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料可以选择无数次,并且可以重复选择。
输入描述
第一行包含两个整数,n,v,分别表示研究材料的种类和行李所能承担的总重量
接下来包含 n 行,每行两个整数 wi 和 vi,代表第 i 种研究材料的重量和价值
输出描述
输出一个整数,表示最大价值。
输入示例
4 5
1 2
2 4
3 4
4 5
输出示例
10
提示信息
第一种材料选择五次,可以达到最大值。
数据范围:
1 <= n <= 10000;
1 <= v <= 10000;
1 <= wi, vi <= 10^9.
解法一:二维dp数组
本题是一个完全背包问题,所有的物品可以无限选择,与0-1背包的解题思路类似,但一些关键的地方会有所不同。
动态规划五部曲:
1. dp数组定义即元素含义
一组物品,一个背包容量,定义二维dp数组,行为物品,列为背包容量,dp[i][j]表示第i个物品在第j个背包容量的最大价值。
2. dp数组推导公式(关键不同点)
对于完全背包,dp[i][j]的来源有两个方面,当背包容量为j时,如果不选择物品i,那么它的值为上一个物品在该容量下的值,即,这点与0-1背包是一样的。
当选择物品i时,因为物品可以无限选择,因此它需要考虑减去当前物品重量后的剩余背包容量在当前物品下的最大值,即,,注意这里与0-1背包不同,0-1背包是背包容量减去当前物品重量后的背包容量在上一个物品的dp值,即
,这是递推公式上的不同点。
if (weight[i] > j) { // 超过背包容量,无法放入
dp[i][j] = dp[i-1][j];
}else {// 可以放入
dp[i][j] = Math.max(dp[i][j-weight[i]] + value[i], dp[i-1][j]);
}
3.dp数组初始化(也有所不同)
我们需要对第0行和第0列进行初始化,当背包容量为0时,所有物品的最大价值都应该为0(Java不需要额外的初始化操作),对于物品0,初始化有所不同,它需要考虑物品的复用。即
// 初始化第0行,注意每个物品可以被无限次放入
for (int i = weight[0]; i <= bagSize; i++) {
dp[0][i] = dp[0][i-weight[0]] + value[0];// 完全背包,初始化与0-1背包不同
}
4. 循环遍历顺序
由于dp值只与左边和上方的值有关,因此完全背包和0-1背包的遍历顺序其实是一样的。
可以外层遍历物品,内层遍历背包容量,也可以外层遍历背包容量,内层遍历物品,两者都是正序遍历。
// 遍历物品
for (int i = 1; i < weight.length; i++) {
// 遍历背包
for (int j = 1; j <= bagSize; j++) {
if (weight[i] > j) { // 超过背包容量,无法放入
dp[i][j] = dp[i-1][j];
}else {// 可以放入
dp[i][j] = Math.max(dp[i][j-weight[i]] + value[i], dp[i-1][j]);
}
}
}
5.举例推导dp数组
以背包容量为4,物品属性如下为例
得到dp数组为:
整体代码:
/**
* 卡码网52. 携带研究材料(第七期模拟笔试) https://kamacoder.com/problempage.php?pid=1052
* 动态规划,完全背包问题
* 二维dp数组
* @param weight
* @param value
* @param bagSize
* @return
*/
public int getMaxValueBagToAll1(int[] weight, int[] value, int bagSize) {
int[][] dp = new int[weight.length][bagSize+1];
// 初始化dp数组,第0列初始化为0,不需要再写
// 初始化第0行,注意每个物品可以被无限次放入
for (int i = weight[0]; i <= bagSize; i++) {
dp[0][i] = dp[0][i-weight[0]] + value[0];// 完全背包,初始化与0-1背包不同
}
// 遍历物品
for (int i = 1; i < weight.length; i++) {
// 遍历背包
for (int j = 1; j <= bagSize; j++) {
if (weight[i] > j) { // 超过背包容量,无法放入
dp[i][j] = dp[i-1][j];
}else {// 可以放入
dp[i][j] = Math.max(dp[i][j-weight[i]] + value[i], dp[i-1][j]);
}
}
}
return dp[weight.length-1][bagSize];
}
解法二:一维dp数组
与0-1背包一样,本题也可以写成一维dp数组的形式
动态规划五部曲:
1. dp数组定义与含义
因为只有一个背包,所有定义一维dp数组,大小为背包容量(Java 里为bagSize+1)
dp[i]表示背包容量为i时的最大价值
2.dp数组推导公式
一维dp数组的更新是在上一个物品的dp数组上进行覆盖,当物品放不下时,报错dp值不变,即实现二维数组的dp[i][j] = dp[i-1][j]操作,当物品能够放入背包时,考虑本身值与扣减后的背包容量dp值中更大的一个,因此得到,其中,j表示背包容量,i表示第i个物品。
dp[j] = Math.max(dp[j-weight[i]] + value[i], dp[j]);
3.dp数组初始化
通过对二维dp数组初始化进行压缩,我们只需要初始化dp[0]即可(Java可省略)
4. 确认遍历顺序
与二维dp数组类似,外层循环遍历物品,内层循环遍历背包容量,但注意,这里遍历顺序不能交换。另外,内层是从第i个物品的重量开始遍历的,剪枝掉了背包容量放不下当前物品的情况,且因为物品可以无限选取,因此内层循环是正序遍历的,不像0-1背包 为了保证一个物品最多选择一次而使用逆序遍历。
5. 举例推导dp数组
还是以背包容量为4,下面的物品为例
得到不同物品下的dp数组:(这里情况较为特殊)
物品0时:
物品1时:
物品2时:
整体代码:
/**
* 卡码网52. 携带研究材料(第七期模拟笔试) https://kamacoder.com/problempage.php?pid=1052
* 动态规划,完全背包问题
* 一维dp数组
* @param weight
* @param value
* @param bagSize
* @return
*/
public int getMaxValueBagToAll2(int[] weight, int[] value, int bagSize) {
int[] dp = new int[bagSize+1];
// 初始化dp[0]为0
for (int i = 0; i < weight.length; i++) {// 遍历物品
for (int j = weight[i]; j <= bagSize; j++) {// 正序遍历背包
dp[j] = Math.max(dp[j-weight[i]] + value[i], dp[j]);
}
}
return dp[bagSize];
}
力扣518.零钱兑换2
给你一个整数数组 coins
表示不同面额的硬币,另给一个整数 amount
表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0
。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
解法一:二维dp数组
解题思路:总金额作为背包最大容量,coins数组即为物品,由于硬币无限,所以本题是一个完全背包。
动态规划五部曲:
1.dp数组定义和含义
物品一种,总金额背包一种,因此定义二维dp数组,行为物品,列为背包容量,dp[i][j]表示第i种硬币在背包容量为j时的组合数。
2. dp数组推导公式
对于dp[i][j],有两种情况,放当前硬币和不放当前硬币,当放当前硬币时,需要考虑剩余金额对当前硬币的dp值,因此有
当不放入硬币时,dp[i][j] = dp[i-1][j],即为上一个物品当前容量的组合个数。
if (coins[i] > j) {// 超过背包容量,无法放入
dp[i][j] = dp[i-1][j];
}else {// 可以放入
dp[i][j] = dp[i][j-coins[i]] + dp[i-1][j];
}
3.dp数组初始化
对于第0列,很好理解,总金额为0时,只有不放这一种选项,因此初始化第0列为1。而对于第0行,除了第一个元素初始化为1外,另外的元素需要考虑它能不能正好满足金额(能够重复选择),如果总金额为第0个硬币的倍数,那么有一种方式满足金额,否则无法满足总金额,初始化为0.
// 初始化第0列,背包为0时只有一种方式
for (int i = 0; i < coins.length; i++) {
dp[i][0] = 1;
}
// 初始化第0行,注意每个硬币都可以无限次放入,能满足金额才初始化为1
for (int i = coins[0]; i <= amount; i++) {
if (i % coins[0] == 0) dp[0][i] =1;
}
对于第0行的初始化,我觉得下面的方式更好理解
for (int i = coins[0]; i <= amount; i++) {// 初始化第0行
dp[0][i] += dp[0][i-coins[0]];
}
4. 确认遍历顺序
遍历顺序没有什么特殊的,正常遍历物品和背包即可。
for (int i = 1; i < coins.length; i++) {
for (int j = 1; j <= amount; j++) {
if (coins[i] > j) {// 超过背包容量,无法放入
dp[i][j] = dp[i-1][j];
}else {// 可以放入
dp[i][j] = dp[i][j-coins[i]] + dp[i-1][j];
}
}
}
5.举例推导dp数组
以amount = 5, coins = [1, 2, 5]为例,得到dp数组为:
整体代码:
class Solution {
public int change(int amount, int[] coins) {
int[][] dp = new int[coins.length][amount+1];
// 初始化第0列,背包为0时只有一种方式
for (int i = 0; i < coins.length; i++) {
dp[i][0] = 1;
}
for (int i = coins[0]; i <= amount; i++) {// 初始化第0行,注意每个硬币都可以无限次放入
if (i % coins[0] == 0) dp[0][i] =1;
}
for (int i = 1; i < coins.length; i++) {
for (int j = 1; j <= amount; j++) {
if (coins[i] > j) {// 超过背包容量,无法放入
dp[i][j] = dp[i-1][j];
}else {// 可以放入
dp[i][j] = dp[i][j-coins[i]] + dp[i-1][j];
}
}
}
return dp[coins.length-1][amount];
}
}
解法二:一维dp数组
可以写成一维dp数组的形式
动态规划五部曲:
1.dp数组定义和含义
只有一个背包需要考虑,定义一维dp数组,容量为amount,dp[j]表示背包容量为j时的组合数。
2.dp数组推导公式
对于dp[j],当选择第i个硬币时,需要考虑去掉该硬币金额后的剩余金额的组合数,因此推导公式为
dp[j] += dp[j-coins[i]];
3.dp数组初始化
当总金额为0时,只有不选择一种情况,因此初始化dp[0] = 1,其余的dp值后续都会覆盖,所以不需要初始化。
4. 确认循环遍历顺序
外层循环遍历硬币coins,内层循环正序遍历背包容量。
for (int i = 0; i < coins.length; i++) {
for (int j = coins[i]; j <= amount ; j++) {
dp[j] += dp[j-coins[i]];
}
}
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
5.举例推导dp数组
以amount = 5, coins = [1, 2, 5]为例,得到dp数组为:
coins[0]遍历:
coins[0]遍历:
coins[0]遍历:
整体代码:
class Solution {
public int change(int amount, int[] coins) {
if (amount == 0) return 1;
int[] dp = new int[amount+1];
dp[0] = 1;// 初始化,背包容量为0,只有都不选一种方法
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];
}
}
力扣377.组合总数4
给你一个由 不同 整数组成的数组 nums
,和一个目标整数 target
。请你从 nums
中找出并返回总和为 target
的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
解法一:动态规划
解题思路:
这里需要注意,虽然本题叫做组合总数,但由于顺序不同的序列视为不同的组合,因此实际求的是排列总数。
nums数值即为动规的物品,target即为背包容量,且由于每个数字可以重复利用,因次是完全背包问题。
动规五部曲:
1. dp数组定义和含义
定义一维dp数组,dp[i]表示目标为i时的排列个数
2. dp数组推导公式
完全背包的递推公式为
3. dp数组初始化
当目标为0时,初始化dp[0]=1,其余值初始化为0即可,因为都得被覆盖。
4. 遍历顺序
由于本题是排列问题,因此我们需要先遍历背包后遍历物品。
// 排列问题需要先遍历背包
for (int i = 1; i <= target; i++) {// 遍历背包
for (int j = 0; j < nums.length; j++) {// 遍历物品
if (i >= nums[j]) {// 背包容量足够
dp[i] += dp[i - nums[j]];
}
}
}
解释:
对于组合问题,我们先遍历物品后遍历背包,对于背包容量j的结果,它受到之前背包容量的dp值影响,但之前背包容量的背包值只与当前物品之前的物品有关,不会出现当前物品影响之后的物品对应的dp值的情况,因此,不会出现同样的值排列顺序不同的结果。
对于排列问题,我们先遍历背包后遍历物品,背包容量j受到之前的背包容量j-1.j-2..的影响,在内层遍历物品的过程中,j-2可能与当前物品的后面的物品有关,从而导致在背包容量为j时,物品i在物品i+k的后面,且在此之前,应该有物品i在物品i+k的前面的排列情况,从而相同的物品但不同排列的情况都被记录。
综上,对于组合问题,我们采用先遍历物品,后遍历背包的方式,对应排列问题,我们采用先遍历背包,后遍历物品的方式。(且如果是0-1背包问题,应该会逆序遍历背包,组合问题待验证)
5. 举例推导dp数组
以nums=[1,2,3], target=4为例
得到dp数组更新为:
背包为0时:
背包为1时:
背包为2时:
背包为3时:
背包为4时:
整体代码:
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 (i >= nums[j]) {// 背包容量足够
dp[i] += dp[i - nums[j]];
}
}
}
return dp[target];
}
}
另外,本题采用回溯法也能实现,只是时间较长,在力扣上超时了
代码为:(就不进行递归三部曲分析了)
class Solution {
int totalCount = 0;
List<Integer> curList = new ArrayList<>();
public int combinationSum4(int[] nums, int target) {
//Arrays.sort(nums);// 排序与否是一样的
trackBackCombinationSum4(nums, target, 0);
return totalCount;
}
public void trackBackCombinationSum4(int[] nums, int target, int startIndex) {
if (target == 0) {
// 找到一种结果
totalCount++;
return;
}
if (target < 0) return;
for (int i = 0; i < nums.length && target >= nums[i]; i++) {
target -= nums[i];
curList.add(nums[i]);
trackBackCombinationSum4(nums, target, 0);
curList.remove(curList.size()-1);
target += nums[i];
}
}
}
卡码网57. 爬楼梯(第八期模拟笔试)
题目描述
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
输入描述
输入共一行,包含两个正整数,分别表示n, m
输出描述
输出一个整数,表示爬到楼顶的方法数。
输入示例
3 2
输出示例
3
提示信息
数据范围:
1 <= m < n <= 32;
当 m = 2,n = 3 时,n = 3 这表示一共有三个台阶,m = 2 代表你每次可以爬一个台阶或者两个台阶。
此时你有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶段
- 1 阶 + 2 阶
- 2 阶 + 1 阶
解题思路:
把该题看成一个完全背包的排列问题:将楼梯阶数n作为背包容量,每次能爬的楼梯数作为物品,即0到m的物品,物品可无限选择,因此为完全背包,不同上楼梯顺序算不同的数,最排列。
动规五部曲
1.dp数组定义和含义
定义一维dp数组,大小为楼梯阶数n,dp[i]表示上到第i阶楼梯有多少种方法
2. dp数组递归公式
完全背包:,j为背包容量,i为物品
3.dp数组初始化
初始化dp[0]=1,其余为0
4.确认遍历顺序
完全背包排列问题,先正序遍历背包容量,后正序遍历物品
5.举例推导dp数组
以5层台阶,每次最多爬3层台阶为例,即n=5,m=3
得到dp数组为
第0阶:
第1阶:
第2阶:
第3阶:
第4阶:
第5阶:
整体代码:
import java.util.Scanner;
class Main{
public static void main (String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int m = in.nextInt();
System.out.println(climbStairs(n,m));
}
public static int climbStairs(int n, int m) {
int[] dp = new int[n+1];
// 初始化
dp[0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (i >= j) {// 背包容量足够
dp[i] += dp[i-j];
}
}
}
return dp[n];
}
}
力扣322.零钱兑换1
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1
。
你可以认为每种硬币的数量是无限的。
解题思路:
一个典型的完全背包问题应用,与之前不同的是,这里找的是最小值。
动规五部曲:
1. dp数组定义和含义
定义一维dp数组,容量为amount,dp[j]表示总金额为j时的最小的硬币个数
2.dp数组递推公式
当硬币i不能被选择(背包容量不够时),很好理解,dp[j] = dp[j]保持不变即可,当可以选择硬币i时,选择硬币的结果为d[j-coinis[i]]+1,不选时为dp[j],要找的是两者的最小值,因此递推公式为
3.dp数组初始化
当总金额为0时,最小的硬币数肯定是0,对于其它dp值,因为本题是求最小值,因此我们初始化为Int的最大值-1,(-1是为了防止数据内存溢出),且coins[i]最大值为,因此必须要足够大
for (int i = 1; i <= amount; i++) {
dp[i] = Integer.MAX_VALUE;
}
4.确认遍历顺序
一维数组只能先遍历物品后遍历背包
5.举例推导dp数组
以输入:coins = [1, 2, 5], amount = 5为例
得到dp数组为:
初始时:
i=0时:
i=1时:
i=2时:
整体代码:
class Solution {
public int coinChange(int[] coins, int amount) {
if (amount ==0) return 0;
int[] dp = new int[amount+1];
// 初始化,除了0以外,都最大化初始化
for (int i = 1; i <= amount; i++) {
dp[i] = Integer.MAX_VALUE - 1;
}
for (int i = 0; i < coins.length; i++) {
for (int j = coins[i]; j <= amount; j++) {
dp[j] = Math.min(dp[j], dp[j-coins[i]]+1);
}
}
// 当最后的dp值未变,说明没有结果,返回-1
return dp[amount] == Integer.MAX_VALUE - 1? -1 : dp[amount];
}
}
力扣279.完全平方数
给你一个整数 n
,返回 和为 n
的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1
、4
、9
和 16
都是完全平方数,而 3
和 11
不是。
解题思路:动态规划
需要将本题看成一个完全背包问题,背包容量为n,物品值为不大于n的完全平方数,即i*i不大于n
动规五部曲:
1.dp数组定义与含义
定义一维dp数组,dp[j]表示构成n的最小完全平方和数量
2.dp数组推导公式
完全背包,且求最小数量,则
3.dp数组初始化
定义dp[0]=0,可以理解为和为0的完全平方和最小数量为0,初始化其余dp值为最大值,因为要求最小值。
// 初始化,因为求最小值,所以需要将其余值设为最大值
for (int j = 1; j <= n; j++) {
dp[j] = Integer.MAX_VALUE;
}
4.确认遍历顺序
本题的遍历顺序可以是先遍历物品再遍历背包
for (int i = 0; i*i<= n; i++) {// 遍历物品
for (int j = i*i; j <= n; j++) {// 遍历背包
if (dp[j-i*i] != Integer.MAX_VALUE) {// 如果当前背包容量减去物品i*i的dp值未变,则跳过
dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
}
}
}
也可以是先遍历背包再遍历物品
for (int j = 1; j <= n; j++) {// 遍历背包
for (int i = 1; i*i <= j; i++) {// 遍历物品
if (dp[j-i*i] != Integer.MAX_VALUE) {// 如果当前背包容量减去物品i*i的dp值未变,则跳过
dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
}
}
}
5.举例推导dp数组
以n=5为例
初始化时,
i*i=1时,
i*i=4时,
整体代码:
class Solution {
public int numSquares(int n) {
// 注意,这里的物品是不大于n的i*i值,明白这点就很好办了
int[] dp = new int[n+1];
// 初始化,因为求最小值,所以需要将其余值设为最大值
for (int j = 1; j <= n; j++) {
dp[j] = Integer.MAX_VALUE;
}
for (int i = 0; i*i<= n; i++) {// 遍历物品
for (int j = i*i; j <= n; j++) {// 遍历背包
if (dp[j-i*i] != Integer.MAX_VALUE) {// 如果当前背包容量减去物品i*i的dp值未变,则跳过
dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
}
}
}
return dp[n];
}
}
力扣139.单词拆分
给你一个字符串 s
和一个字符串列表 wordDict
作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s
则返回 true
。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
解题思路:动态规划
要用动态规划,需要把字符串s看成背包,字符串列表wordDict作为物品,但要注意的是,后续遍历的时候,不是直接遍历这个字符串列表,而是用字符串的子串来判断,当子串在列表中时,才进行进一步的判断。
动规五部曲:
1.dp数组定义和含义
定义一维dp数组,背包容量为s的长度,dp[j]表示字符串(0,j)的子串是否能够被划分
2.dp数组推导公式
对于字符串子串j,当子串(i,j)在字符串列表中时,dp[j]的值由dp[i]决定,当dp[i]为true时,dp[j]也应该为true,此时,子串j可以被划分,反之则为false。
if (wordDict.contains(s.substring(i,j)) && dp[i]) {// substring是左闭右开的,即取i不取j
// 当子串(i,j)包含在字符串数组中,且子串起始点i的dp值为true时,到j的字符串子串才能被拆分
dp[j] = true;
}
3.dp数组初始化
初始化dp[0] = true,表示空字符串可以被划分,题目中表示没有空字符串,因此该定义更多的是为了后续递推的进行。初始化其余dp值为false,Java的boolean数组初始化默认为false
4.确认遍历顺序
这里我们首先得确认本题是排序还是组合问题。
对于该题,我们可以理解为拿无数个字符串列表中的子串取填充字符串s,看能否将字符串s全部填满,字符串s是固定的,因此子串的填充位置也是固定的,同样的子串无法随意排序,因此为排列问题,只有唯一的解法才能满足。
因此我们需要先遍历背包,再遍历wuopin
for (int j = 1; j <= s.length(); j++) {// 遍历背包
for (int i = 0; i < j; i++) {// 遍历物品,注意这里实际的物品是字符串数组中的
}
}
5.举例推导dp数组
使用用例:s = "applepenapple", wordDict = ["apple", "pen"],对应的dp数组状态如下:
1表示true,0表示false
初始为:
子串apple,i=0,j=5:
子串pen,i=5,j=8:
子串apple,i=8,j=13:
整体代码:
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
boolean[] dp = new boolean[s.length() + 1];
// dp[0]初始化为0,其余值填充为false,Java默认为false
dp[0] = true;
for (int j = 1; j <= s.length(); j++) {// 遍历背包
for (int i = 0; i < j; i++) {// 遍历物品,注意这里实际的物品是字符串数组中的
if (wordDict.contains(s.substring(i,j)) && dp[i]) {// substring是左开右闭的,即不取i取j
// 当子串(i,j)包含在字符串数组中,且子串起始点i的dp值为true时,到j的字符串子串才能被拆分
dp[j] = true;
}
}
}
return dp[s.length()];
}
}
本题还可以用回溯法,基本思路为:
通过回溯遍历完所有的子串划分顺序,判断子串是否在字符串列表中,如果遍历完子串还未false返回,则表示能够划分,反之,如果所有可能的子串划分都无法满足,则返回false。
卡码网56. 携带矿石资源(第八期模拟笔试)
题目描述
你是一名宇航员,即将前往一个遥远的行星。在这个行星上,有许多不同类型的矿石资源,每种矿石都有不同的重要性和价值。你需要选择哪些矿石带回地球,但你的宇航舱有一定的容量限制。
给定一个宇航舱,最大容量为 C。现在有 N 种不同类型的矿石,每种矿石有一个重量 w[i],一个价值 v[i],以及最多 k[i] 个可用。不同类型的矿石在地球上的市场价值不同。你需要计算如何在不超过宇航舱容量的情况下,最大化你所能获取的总价值。
输入描述
输入共包括四行,第一行包含两个整数 C 和 N,分别表示宇航舱的容量和矿石的种类数量。
接下来的三行,每行包含 N 个正整数。具体如下:
第二行包含 N 个整数,表示 N 种矿石的重量。
第三行包含 N 个整数,表示 N 种矿石的价格。
第四行包含 N 个整数,表示 N 种矿石的可用数量上限。
输出描述
输出一个整数,代表获取的最大价值。
输入示例
10 3
1 3 4
15 20 30
2 3 2
输出示例
90
提示信息
数据范围:
1 <= C <= 2000;
1 <= N <= 100;
1 <= w[i], v[i], k[i] <= 1000;
解题思路:
本题是一个多重背包问题,每个物品可以选择多个,但不像完全背包那样能够无限选择,也不像0-1背包那样只能选择一次,多重背包像是0-1背包的改版,把多重背包中的物品展开,每次看成一个物品,那么整个问题就变成了0-1背包问题。
可以直接对物品数组进行改写展开成单个物品的0-1背包方式(这里就不详细写了),也可以进行0-1背包解法的改版。
动规五部曲
1.dp数组定义和含义
dp数组定义不变,同样定义一维dp数组,dp[j]表示背包容量为j时的最大价值
4.确认dp遍历顺序
传统的0-1背包两种遍历都可以,这里将遍历顺序提前是为了说名后续的递推公式推导。
以先遍历物品后遍历背包容量为例,因为为0-1背包,因此背包容量需要逆序遍历,又因为同一个物品可以选择多次,因此在遍历背包的循环内需要对当前物品的数量进行遍历。
for (int i = 0; i < weight.length; i++) {// 遍历物品
for (int j = bagSize; j >= weight[i]; j--) {// 遍历背包
// 遍历同一种物品中的每一个,表示使用了num个物品i
for (int num = 1; num <= nums[i] && (j- num*weight[i]) >= 0; num++) {
}
}
}
2.dp数组推导公式
根据上面的遍历顺序推导,我们在考虑剩余容量时,需要考虑当前使用了多少个该物品,因此剩余容量为j-num*weight[i],保证剩余容量够时才进行判断,因此需要考虑dp[j-num*weight[i]]的值。
dp[j] = Math.max(dp[j], dp[j - num*weight[i]] + num*value[i]);
3.dp数组初始化
与0-1背包相同,初始化为0即可,如果求最小值,则除dp[0]外需要初始化为足够大的值
5.举例推导dp数组
背包容量为10,物品属性如下
初始时,
物品0时,
物品1时,
物品2时,
整体代码:
import java.util.Scanner;
public class Main{
public static void main (String[] args) {
Scanner in = new Scanner(System.in);
int c = in.nextInt();// 背包容量
int n = in.nextInt();// 物品数量
int[] weight = new int[n];
int[] value = new int[n];
int[] nums = new int[n];
for (int i = 0;i < n ;i++) {
weight[i] = in.nextInt();
}
for (int i = 0;i < n ;i++) {
value[i] = in.nextInt();
}
for (int i = 0;i < n ;i++) {
nums[i] = in.nextInt();
}
in.close();
System.out.println(carryBag(weight,value,nums,c));
}
/**
* 卡码网56. 携带矿石资源(第八期模拟笔试)https://kamacoder.com/problempage.php?pid=1066
* @param weight
* @param value
* @param nums 物品最大可选数量
* @param bagSize
* @return
*/
public static int carryBag(int[] weight, int[] value,int[] nums, int bagSize) {
int[] dp = new int[bagSize + 1];
// 初始化,所有dp值都初始化为0
for (int i = 0; i < weight.length; i++) {// 遍历物品
for (int j = bagSize; j >= weight[i]; j--) {// 遍历背包
// 遍历同一种物品中的每一个,表示使用了num个物品i
for (int num = 1; num <= nums[i] && (j- num*weight[i]) >= 0; num++) {
dp[j] = Math.max(dp[j], dp[j - num*weight[i]] + num*value[i]);
}
}
}
return dp[bagSize];
}
}
力扣198.打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额
解题思路:
因为相邻的房屋不能同时选择,所以对于第i个房间,需要考虑前两个房间的选择,当不选择当前房间时,那么它只需要考虑前一个房间的值,如果选择当前房间盗取,则前一个房间不能盗取,考虑i-2个房间的值。
动规五部曲:
1.dp数组定义和含义
定义一维dp数组,dp[i]表示第i个房间获得的最高金额。
2.dp数组递推公式
当选择盗取第i个房间时,第i-1个房间就不能盗取,只需要考虑i-2个房间即可,因此dp[i]=dp[i-2]+nums[i]。
当不选择盗取第i个房间时,第i-1个房间就可以盗取,也不以不盗取,因此dp[i]=dp[i-1]
综上,dp数组递推公式为:
dp[i] = Math.max((dp[i-2] + nums[i]), dp[i-1]);
3.dp数组初始化
由于dp[i]需要i-1和i-2的结果,因此dp数组需要对最开始两个值初始化,dp[0]=nums[0],dp[1]=max{dp[0],dp[1]},另外,因为题目给定房间个数最小为1,因此需要对只有一个房间进行单独判断。
if (nums.length == 1) return nums[0];
int[] dp = new int[nums.length];
// 初始化dp数组,
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
4.确认遍历顺序
由递推公式可以看出来需要从前往后遍历,
for (int i = 2; i < nums.length; i++) {
}
5.举例推到dp数组
以输入[2,7,9,3,1]为例
得到dp数组为:
整体代码:
class Solution {
public int rob(int[] nums) {
if (nums.length == 1) return nums[0];
int[] dp = new int[nums.length];
// 初始化dp数组,
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < nums.length; i++) {
dp[i] = Math.max((dp[i-2] + nums[i]), dp[i-1]);
}
return dp[nums.length-1];
}
}
力扣213.打家劫舍2
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
解题思路:
本题与打家劫舍基本相同,只是需要处理对开始和末尾的房屋的处理。
对于一个数组,成环的情况需要考虑的主要有三种:
1. 不考虑第一个和最后一个房间
2.考虑第一个房间,不考虑最后一个房间
3. 考虑最后一个房间,不考虑第一个房间
对于上面的情况,我们发现,只要在情况3中,不选择最后一个房间或在第3种情况中不选择第一个房间,那么就是第1种情况,注意:考虑最后一个或第一个房间的情况最后的结果不一定会选择该房间,可能不盗取该房间的总金额更高(选择了房间1和房间3)。
动规五部曲:
1.dp数组定义和含义
定义一维dp数组,然第一个和最后一个元素不是都要考虑,dp[i]表示第i个房间获得的最高金额。
2.dp数组递推公式
当选择盗取第i个房间时,第i-1个房间就不能盗取,只需要考虑i-2个房间即可,因此dp[i]=dp[i-2]+nums[i]。
当不选择盗取第i个房间时,第i-1个房间就可以盗取,也不以不盗取,因此dp[i]=dp[i-1]
综上,dp数组递推公式为:
dp[i] = Math.max((dp[i-2] + nums[i]), dp[i-1]);
3.dp数组初始化
这里在代码上有所不同,首先给出函数的不同,本题的函数需要根据考虑的数组起始和结束下标
public int robMax(int[] nums, int start, int end)
虽然初始化依旧是前两个dp值,但由于起始点可以是1,因此不是初始化dp[0]和dp[1],而是初始化start开始的前两个dp值
// start和end是考虑的房间起始下标
if (start == end) return nums[start];
int[] dp = new int[nums.length];
// 初始化dp数组,类似于dp[0]和dp[1]
dp[start] = nums[start];
dp[start+1] = Math.max(nums[start], nums[start+1]);
4.确认遍历顺序
由递推公式可以看出来需要从前往后遍历,
for (int i = 2; i < nums.length; i++) {
}
5.举例推到dp数组
以输入[1,6,1,9,1]为例
考虑第一个房间,不考虑最后一个房间得到dp数组为:,注意这里start=0,end=3,
不考虑第一个房间,考虑最后一个房间得到dp数组为:,注意这里start-1,end=4,
最后取两者间的最大值
整体代码:
class Solution {
public int rob(int[] nums) {
if (nums.length == 1) return nums[0];
int left = robMax(nums, 0, nums.length - 2);// 考虑第一个,不考虑最后一个
int right = robMax(nums, 1, nums.length - 1);// 考虑最后一个,不考虑第一个
return Math.max(left, right);
}
public int robMax(int[] nums, int start, int end) {
// start和end是考虑的房间起始下标
if (start == end) return nums[start];
int[] dp = new int[nums.length];
// 初始化dp数组,类似于dp[0]和dp[1]
dp[start] = nums[start];
dp[start+1] = Math.max(nums[start], nums[start+1]);
for (int i = start+2; i <= end; i++) {// dp数组推导
dp[i] = Math.max((dp[i-2] + nums[i]), dp[i-1]);
}
return dp[end];
}
}
力扣.337打家劫舍3
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root
。
除了 root
之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root
。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
解法一:动态规划(树形dp)
对于一个节点,有偷与不偷两种选择,当偷当前节点时,它的左右孩子节点一定不能偷,当不偷当前节点时,它的左右节点都是可偷可不偷,需要看那种情况得到的结果更大。另外,当前节点偷与不偷得到的结果与左右孩子节点的结果有关,因此我们通常使用后序遍历的方式遍历二叉树。
动规五部曲:
1. dp数组定义和含义,增加递归函数定义
定义递归函数返回值为数组,同一层次的节点的结果数组作为整体数组的同一层,每一层的结果影响上一层的结果,在每一层的数组中,0元素表示不偷当前节点的最大金额,1元素表示偷当前节点的最大金额。
public int[] treeDp(TreeNode node)
2. dp数组递推公式
在每一层递归中,通过递归得到左右子树的dp数组(伪),然后根据偷不偷当前节点得到当前节点的数组,偷当前节点时,cur[1]的值为当前节点值加左右子树节点不偷的金额,不偷当前节点时,对于左右子树,要看偷与不偷那个结果更大。从而得到当前节点的结果数组
int[] left = treeDp(node.left);// 左,左子树的偷与不偷的金额
int[] right = treeDp(node.right);// 右,右子树的偷与不偷的金额
// 中,当前节点偷与不偷的金额,cur[0]表示不偷,cur[1]表示偷
int[] cur = new int[2];
// 不偷当前节点,那么左右孩子节点可以偷也可以不偷,选择金额最大的方式
cur[0] = Math.max(left[0],left[1]) + Math.max(right[0], right[1]);
// 偷当前节点,那么左右孩子节点只能不偷
cur[1] = node.val + left[0] + right[0];
3.dp数组初始化
当节点为空时,肯定不能偷,此时初始化数组的两个元素都为0,然后返回。
// 空节点,不可能偷
if (node == null) return new int[2];
4.确认遍历顺序
采用后序遍历,保证访问当前节点时,左右孩子节点已经访问处理过了,从而保证当前节点的结果更新 。
5.举例推导dp数组
以下面的树为例 :
得到各个节点的结果数组:(空节点不考虑)
节点3(下标为4的元素):{0,3}
节点1:{0,1}
节点2:{3,2}
节点3(下标为2的元素):{1,3}
节点3(根节点):{6,7}
整体代码:
class Solution {
public int rob(TreeNode root) {
int[] dpRes = treeDp(root);
//返回两种情况下的最大值
return Math.max(dpRes[0], dpRes[1]);
}
public int[] treeDp(TreeNode node) {
// 空节点,不可能偷
if (node == null) return new int[2];
int[] left = treeDp(node.left);// 左,左子树的偷与不偷的金额
int[] right = treeDp(node.right);// 右,右子树的偷与不偷的金额
// 中,当前节点偷与不偷的金额,cur[0]表示不偷,cur[1]表示偷
int[] cur = new int[2];
// 不偷当前节点,那么左右孩子节点可以偷也可以不偷,选择金额最大的方式
cur[0] = Math.max(left[0],left[1]) + Math.max(right[0], right[1]);
// 偷当前节点,那么左右孩子节点只能不偷
cur[1] = node.val + left[0] + right[0];
return cur;
}
}
解法二:递归暴力破解+优化
对于二叉树,大多数情况都能直接进行递归,但本题单纯的递归暴力破解会超时
递归三部曲:
1.递归函数和返回值
返回当前节点下的最大金额
2. 递归结束条件
当节点为空时,结束递归,返回0(空节点无可偷的金额)
3.单层递归
对于当前节点node,当不偷该节点时,记录左孩子和右孩子递归得到的最大金额
// 不偷当前节点,当前节点的金额为左右孩子节点的金额之和
int curRes1 = trackRob(node.left) + trackRob(node.right);
当我们偷当前节点时,它的左右孩子节点都不能偷,因此我们直接记录左孩子的孩子节点和右孩子的孩子节点的最大金额,加上当前节点的金额。最终结果为这两种情况中的最大值
// 偷当前节点,当前节点的金额为左右孩子节点的金额之和加上当前节点的金额
int curRes2 = node.val;
if (node.left != null) {
// 跳过偷左孩子,直接到左孩子的孩子
curRes2 += trackRob(node.left.left) + trackRob(node.left.right);
}
if (node.right != null) {
// 跳过偷右孩子,直接到右孩子的孩子
curRes2 += trackRob(node.right.left) + trackRob(node.right.right);
}
整体代码:
class Solution {
public int rob(TreeNode root) {
return trackRob(root);
}
public int trackRob(TreeNode node){
if (node == null) return 0;// 节点为空,不可能偷,返回0
// 不偷当前节点,当前节点的金额为左右孩子节点的金额之和
int curRes1 = trackRob(node.left) + trackRob(node.right);
// 偷当前节点,当前节点的金额为左右孩子节点的金额之和加上当前节点的金额
int curRes2 = node.val;
if (node.left != null) {
// 跳过偷左孩子,直接到左孩子的孩子
curRes2 += trackRob(node.left.left) + trackRob(node.left.right);
}
if (node.right != null) {
// 跳过偷右孩子,直接到右孩子的孩子
curRes2 += trackRob(node.right.left) + trackRob(node.right.right);
}
// 返回两种情况的最大值
return Math.max(curRes1, curRes2);
}
}
上面的代码超时了,因为在计算过程中,出现了许多节点被重复计算的过程,因此我们可以进一步改进该方法,在节点值 计算出来后,存入一个全局Map中,在计算每个节点的金额时,先在Map中查看是否存在当前节点,当存在时,则直接使用存储的值即可,避免重复计算的问题。
整体代码:
class Solution {
Map<TreeNode, Integer> map = new HashMap<>();
public int rob(TreeNode root) {
return trackRob(root);
}
public int trackRob(TreeNode node){
if (node == null) return 0;// 节点为空,不可能偷,返回0
// 当前节点已经计算过,直接返回保存的结果
if (map.containsKey(node)) return map.get(node);
// 不偷当前节点,当前节点的金额为左右孩子节点的金额之和
int curRes1 = trackRob(node.left) + trackRob(node.right);
// 偷当前节点,当前节点的金额为左右孩子节点的金额之和加上当前节点的金额
int curRes2 = node.val;
if (node.left != null) {
// 跳过偷左孩子,直接到左孩子的孩子
curRes2 += trackRob(node.left.left) + trackRob(node.left.right);
}
if (node.right != null) {
// 跳过偷右孩子,直接到右孩子的孩子
curRes2 += trackRob(node.right.left) + trackRob(node.right.right);
}
// 记录当前节点的最优解,用于去除重复计算
map.put(node, Math.max(curRes1, curRes2));
// 返回两种情况的最大值
return Math.max(curRes1, curRes2);
}
}