leetcode算法刷题的第三十二天
1.完全背包问题---二维dp数组
题目描述
小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的重量,并且具有不同的价值。
小明的行李箱所能承担的总重量是有限的,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料可以选择无数次,并且可以重复选择。
输入描述
第一行包含两个整数,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.
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main(){int n,bagWeight;int w,v;cin>>n>>bagWeight;vector<int> weight(n);vector<int> value(n);for(int i=0;i<n;i++){cin>>weight[i]>>value[i];}vector<vector<int>> dp(n,vector<int>(bagWeight+1,0));//初始化for(int j=weight[0];j<=bagWeight;j++){dp[0][j]=dp[0][j-weight[0]]+value[0];}for(int i=1;i<n;i++){//遍历物品for(int j=0;j<=bagWeight;j++){//遍历背包容量if(j<weight[i]) dp[i][j]=dp[i-1][j];else dp[i][j]=max(dp[i-1][j],dp[i][j-weight[i]]+value[i]);}}cout<<dp[n-1][bagWeight]<<endl;return 0;
}
思路总结:
第一,确定dp数组以及下标的含义
dp[i][j] 表示从下标为[0-i]的物品,每个物品可以取无限次,放进容量为j的背包,价值总和最大是多少。
第二,确定递推公式
递推公式: dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i])
第三,
dp数组如何初始化
关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。
首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。
状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
可以看出有一个方向 i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。
dp[0][j],即:存放编号0的物品的时候,各个容量的背包所能存放的最大价值。
那么很明显当 j < weight[0]
的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。
当j >= weight[0]
时,dp[0][j] 如果能放下weight[0]的话,就一直装,每一种物品有无限个。
dp[0][j] 和 dp[i][0] 都已经初始化了,那么其他下标应该初始化多少呢?
其实从递归公式: dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); 可以看出dp[i][j] 是由上方和左方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。
但只不过一开始就统一把dp数组统一初始为0,更方便一些。
第四,确定遍历顺序
可以先遍历物品再遍历背包
也可以先遍历背包再遍历物品
第五,举例推导dp数组
2.leetcode 518.零钱兑换II
题目链接
class Solution {
public:int change(int amount, vector<int>& coins) {int bagSize=amount;vector<vector<uint64_t>> dp(coins.size(),vector<uint64_t>(bagSize+1,0));//初始化最上面那一行for(int j=0;j<=bagSize;j++){if(j%coins[0]==0) dp[0][j]=1;}//初始化最左列for(int i=0;i<coins.size();i++){dp[i][0]=1;}//以下遍历顺序行列可以颠倒for(int i=1;i<coins.size();i++){//行,遍历物品for(int j=0;j<=bagSize;j++){//列,遍历背包if(coins[i]>j) dp[i][j]=dp[i-1][j];else dp[i][j]=dp[i-1][j]+dp[i][j-coins[i]];}}return dp[coins.size()-1][bagSize];}
};
思路总结:
第一,确定dp数组以及下标的含义
定义二维dp数值 dp[i][j]:使用 下标为[0, i]的coins[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种组合方法。
第二,确定递推公式
本题递推公式:dp[i][j] = dp[i - 1][j] + dp[i][j - nums[i]]
,区别依然是 dp[i - 1][j - nums[i]]
和 dp[i][j - nums[i]]
第三,
dp数组如何初始化
那么二维数组的最上行 和 最左列一定要初始化,这是递推公式推导的基础
这里首先要关注的就是 dp[0][0] 应该是多少?
背包空间为0,装满「物品0」 的组合数有多少呢?
应该是 0 个, 但如果 「物品0」 的 数值就是0呢? 岂不是可以有无限个0 组合 和为0!
题目描述中说了1 <= coins.length <= 300
,所以不用考虑 物品数值为0的情况。
那么最上行dp[0][j] 如何初始化呢?
dp[0][j]的含义:用「物品0」(即coins[0]) 装满 背包容量为j的背包,有几种组合方法。
如果 j 可以整除 物品0,那么装满背包就有1种组合方法。
最左列如何初始化呢?
dp[i][0] 的含义:用物品i(即coins[i]) 装满容量为0的背包 有几种组合方法。
都有一种方法,即不装。
所以 dp[i][0] 都初始化为1
第四,确定遍历顺序
二维DP数组的完全背包的两个for循环先后顺序是无所谓的。
先遍历背包,还是先遍历物品都是可以的。
第五,打印dp数组
dp数组应该是这样的:
1 0 1 0 1 0
1 0 1 1 1 1
1 0 1 1 1 2
3.leetcode 377.组合总和IV
题目链接
class Solution {
public:int combinationSum4(vector<int>& nums, int target) {vector<int> dp(target + 1, 0);dp[0] = 1;for (int i = 0; i <= target; i++) { // 遍历背包for (int j = 0; j < nums.size(); j++) { // 遍历物品if (i - nums[j] >= 0 && dp[i] < INT_MAX - dp[i - nums[j]]) {dp[i] += dp[i - nums[j]];}}}return dp[target];}
};
思路总结:这道题的答案是这样的,但是力扣里面过不了,是因为C++测试用例有两个数相加超过int的数据。
第一,确定dp数组以及下标的含义
dp[i]: 凑成目标正整数为i的排列个数为dp[i]
第二,确定递推公式
dp[i](考虑nums[j])可以由 dp[i - nums[j]](不考虑nums[j]) 推导出来。
因为只要得到nums[j],排列个数dp[i - nums[j]],就是dp[i]的一部分。
求装满背包有几种方法,递推公式一般都是dp[i] += dp[i - nums[j]];
本题也一样。
第三,dp数组的初始化
因为递推公式dp[i] += dp[i - nums[j]]的缘故,dp[0]要初始化为1,这样递归其他dp[i]的时候才会有数值基础。
至于dp[0] = 1 有没有意义呢?
其实没有意义,所以我也不去强行解释它的意义了,因为题目中也说了:给定目标值是正整数! 所以dp[0] = 1是没有意义的,仅仅是为了推导递推公式。
至于非0下标的dp[i]应该初始为多少呢?
初始化为0,这样才不会影响dp[i]累加所有的dp[i - nums[j]]。
第四,确定遍历顺序
个数可以不限使用,说明这是一个完全背包。
得到的集合是排列,说明需要考虑元素之间的顺序。
本题要求的是排列,那么这个for循环嵌套的顺序可以有说法了。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
如果把遍历nums(物品)放在外循环,遍历target的作为内循环的话,举一个例子:计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合,因为nums遍历放在外层,3只能出现在1后面!
所以本题遍历顺序最终遍历顺序:target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历。
第五,举例推导dp数组
求装满背包有几种方法,递归公式都是一样的,没有什么差别,但关键在于遍历顺序!
如果对遍历顺序没有深度理解的话,做这种完全背包的题目会很懵逼,即使题目刷过了可能也不太清楚具体是怎么过的。
4.爬楼梯(进阶版)
题目描述
假设你正在爬楼梯。需要 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 阶
#include<iostream>
#include<vector>
using namespace std;
int main(){int n,m;while(cin>>n>>m){vector<int> dp(n+1,0);dp[0]=1;for(int i=1;i<=n;i++){//遍历背包for(int j=1;j<=m;j++){//遍历物品if(i-j>=0) dp[i]+=dp[i-j];}}cout<<dp[n]<<endl;}return 0;
}
思路总结:
第一,确定dp数组以及下标的含义
dp[i]:爬到有i个台阶的楼顶,有dp[i]种方法。
第二,确定递推公式
本题呢,dp[i]有几种来源,dp[i - 1],dp[i - 2],dp[i - 3] 等等,即:dp[i - j]
那么递推公式为:dp[i] += dp[i - j]
第三,dp数组如何初始化
既然递归公式是 dp[i] += dp[i - j],那么dp[0] 一定为1,dp[0]是递归中一切数值的基础所在,如果dp[0]是0的话,其他数值都是0了。
下标非0的dp[i]初始化为0,因为dp[i]是靠dp[i-j]累计上来的,dp[i]本身为0这样才不会影响结果
第四,确定遍历顺序
这是背包里求排列问题,即:1、2 步 和 2、1 步都是上三个台阶,但是这两种方法不一样!
所以需将target放在外循环,将nums放在内循环。
每一步可以走多次,这是完全背包,内循环需要从前向后遍历。
第五,举例推导dp数组
本题看起来是一道简单题目,稍稍进阶一下其实就是一个完全背包!
如果我来面试的话,我就会先给候选人出一个 本题原题,看其表现,如果顺利写出来,进而在要求每次可以爬[1 - m]个台阶应该怎么写。
顺便再考察一下两个for循环的嵌套顺序,为什么target放外面,nums放里面。
这就能考察对背包问题本质的掌握程度,候选人是不是刷题背公式,一眼就看出来了。
这么一连套下来,如果候选人都能答出来,相信任何一位面试官都是非常满意的。
本题代码不长,题目也很普通,但稍稍一进阶就可以考察完全背包,而且题目进阶的内容在leetcode上并没有原题,一定程度上就可以排除掉刷题党了,简直是面试题目的绝佳选择!