十二月做题总结(下)
2022-12-12
1.【452】用最少数量的箭引爆气球(中)
题目链接: 用最少数量的箭引爆气球
题目描述:
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组points
,其中points[i] = [xstart, xend]
表示水平直径在xstart
和 xend
之间的气球。你不知道气球的确切y
坐标。
一支弓箭可以沿着 x
轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart
,xend
,且满足 xstart ≤ x ≤ xend
,则该气球会被引爆.可以射出的弓箭的数量没有限制。弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points
,返回引爆所有气球所必须射出的最小弓箭数。
涉及知识点: 贪心(区间)
思路: 本题的思想也比较直观,越多气球区间重合,则需要射出的箭就越少,则局部最优:当气球出现重叠,一起射,所用弓箭最少。全局最优:把所有气球射爆所用弓箭最少。
可以先对数组中按气球的起始(或结束)直径位置进行排序,然后再依次遍历看是否重合,是否需要射出新的箭。
代码:
class Solution {
public int findMinArrowShots(int[][] points) {
// 按气球直径的起始位置从小到大排序
// 这个是Interger内置的排序方法
Arrays.sort(points, (a, b) -> Integer.compare(a[0], b[0]));
int count = 1; // 箭数
for(int i = 1;i<points.length;i++){
if(points[i-1][1]<points[i][0]){
count ++;
}else{
points[i][1] = Math.min(points[i][1], points[i - 1][1]);
}
}
return count;
}
}
2.【435】无重叠区间(中)
题目链接: 无重叠区间问题
题目描述:
给定一个区间的集合intervals
,其中 intervals[i] = [starti, endi]
。返回需要移除区间的最小数量,使剩余区间互不重叠。
涉及知识点: 贪心(区间)
思路: 这道题的思路和射箭题很类似,但实现和射箭不太一样,射箭题中是按起始位置从小到大排序,然后从左向右去遍历,靠左尽可能让气球重叠。但本题是按终止位置从小到大排序,则从左到右去遍历选右边界小的,这是因为因为右边界越小越好,只要右边界越小,留给下一个区间的空间就越大,所以要优先选右边界小的。
代码:
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
// 按照区间右边界升序排序
Arrays.sort(intervals, (a, b) -> Integer.compare(a[1], b[1]));
// 如果这里按左边界排序,可能会出现左边界相同时,右边界较大的排在前面的情况
// 根据下面的代码,就不能获得尽量小的右边界,会使结果偏大
int count = 0; //用于记录重叠的区间数
int min_right = Integer.MIN_VALUE;
for(int i = 0;i<intervals.length;i++){
if(intervals[i][0]>=min_right){
//说明两个区间不重叠
min_right = intervals[i][1];
}else{
count++;
}
}
return count;
}
}
3.【763】划分字母区间(中)
题目链接: 划分字母区间
题目描述:
给你一个字符串s
。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是s
返回一个表示每个字符串片段的长度的列表。
涉及知识点: 贪心(区间)
思路: 这道题其实有点不太像贪心,主要思想是用每个字母的最远出现距离来决定在哪里切断字符串,所以就进行两次遍历,第一次需要借助容器来记录每个字母的出现的最远距离下标;第二次则是将每个字母对应的最远距离赋给字符串中出现的字母,并且要比较当前字符下标是否等于最远下标,如果是则说明找到了子字符串发边界。
代码:
class Solution {
public List<Integer> partitionLabels(String s) {
char[] chars = s.toCharArray();
List<Integer> list = new LinkedList<>(); //记录结果
int[] count = new int[26]; //记录更新26个字母对应的最远下标
for(int i = 0; i<chars.length;i++){
count[chars[i]-'a'] = i;
}
int imax = 0;
int last = -1;
for(int i = 0;i<chars.length;i++){
imax = Math.max(imax,count[chars[i]-'a']);
if(i == imax){
list.add(i-last);
last=i;
}
}
return list;
}
}
4.【56】合并区间(中)
题目链接: 合并区间问题
题目描述:
以数组intervals
表示若干个区间的集合,其中单个区间为intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
涉及知识点: 贪心(区间)
思路: 本题的思路很直观,就是先按左边界排序,然后去比较相邻两个区间,如果两个区间有重叠部分,那么就更新新区间的右边界为两个区间右边界的较大值,然后拿这个新区间去与后面的区间比较;如果两个区间不重叠,则说明找到了符合要求的区间,加入结果集中。注意:循环结束后还要执行一次将区间加入结果集的操作,不然会漏一个。局部最优:每次合并都取最大的右边界,这样就可以合并更多的区间了,整体最优:合并所有重叠的区间。
代码:
class Solution {
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
List<int[]> res = new LinkedList<>();
int start = intervals[0][0];
int end = intervals[0][1];
for(int i = 1; i<intervals.length;i++){
if(intervals[i][0]>end){
res.add(new int[]{start,end});
start = intervals[i][0];
end = intervals[i][1];
}else if(intervals[i][1]>end){
end = intervals[i][1];
}
}
res.add(new int[]{start,end});
return res.toArray(new int[res.size()][]);
}
}
2022-12-13
【知识点】动态规划
(1)基本内容
动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。前面做的一些贪心的题目也可以用动态规划解决。
(2)动规五部曲
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
如果动态规划的题目代码出bug,则可以把dp数组打印出来,看结果是否和自己预想的一致。
5.【509】斐波那契数(easy)
题目链接: 斐波那契数
题目描述:
斐波那契数 (通常用 F(n)
表示)形成的序列称为 斐波那契数列 。该数列由 0
和 1
开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1
给定 n
,请计算 F(n)
。
涉及知识点: 动态规划
思路: 本题是经典的动规入门题:
- 确定dp数组以及下标的含义:第
i
个数的斐波那契数值是dp[i]
- 确定递推公式:状态转移方程
dp[i] = dp[i - 1] + dp[i - 2]
- dp数组初始化:
dp[0] = 0;dp[1] = 1
- 确定遍历顺序:从前到后遍历
- (debug)举例推导dp数组:当N为10的时候,dp数组应为
0 1 1 2 3 5 8 13 21 34 55
,
如果代码结果不对,就把dp数组打印出来看和推导的数列是不是一致的。
代码:
class Solution {
public int fib(int n) {
if(n<=1){
return n;
}
int[] dp = new int[n+1];
dp[0] = 0;
dp[1] = 1;
for(int i = 2;i<=n;i++){
dp[i] = dp[i-2] + dp[i-1];
}
return dp[n];
}
}
// 本题中也可以只维护两个数值来记录结果
class Solution {
public int fib(int n) {
if(n<=1){
return n;
}
int a = 0, b = 1, c = 0;
// 遍历n-1次
for (int i = 1; i < n; i++) {
c = a + b;
a = b;
b = c;
}
return c;
}
}
6.【70】爬楼梯(easy)
题目链接: 爬楼梯问题
题目描述:
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
涉及知识点: 动态规划
思路: 本题也是经典的动规入门题:
- 确定dp数组以及下标的含义: 爬到第
i
层楼梯,有dp[i]
种方法 - 确定递推公式:
dp[i]
可以有两个方向推出来,首先是dp[i - 1]
,上i-1
层楼梯,有dp[i - 1]
种方法,那么下一步跳一个台阶就是dp[i]
了;还有就是dp[i - 2]
,上i-2
层楼梯,有dp[i - 2]
种方法,那么再一步跳两个台阶就是dp[i]
了,故dp[i]= dp[i - 1]+dp[i - 2]
- dp数组初始化:不考虑
n=0
的情况,dp[1] = 1;dp[2] = 2
- 确定遍历顺序:从前到后遍历
这道题其实分析下来和斐波那契数列题的思想是一致的,只是dp数组初始化时不太一样。
代码:
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-1]+dp[i-2];
}
return dp[n];
}
}
2022-12-29新增进阶版内容
前面的动规方法是直接用斐波那契的思想,但是如果将题目改为一步可以走1~m阶楼梯,那么就可以考虑用完全背包的思想,把1阶,2阶,… m阶看成物品,楼顶就是背包,那么就和《17.【377】 组合总和 Ⅳ》的解法基本一致了。
思路:
dp[i]
:爬到有i
个台阶的楼顶,有dp[i]
种方法。- 递推公式:
dp[i] += dp[i - j]
- dp数组初始化:
dp[0] = 1
,下标非0
的dp[i]
初始化为0
- 确定遍历顺序:求排列,背包循环放外层,物品循环放内层。
代码:
class Solution {
// 完全背包方法
public int climbStairs(int n) {
int[] dp = new int[n+1];
dp[0] = 1;
for(int j =0;j<=n;j++){
for(int i =1;i<=2;i++){
if(j>=i){
dp[j] += dp[j-i];
}
}
}
return dp[n];
}
}
7.【746】使用最小花费爬楼梯(easy)
题目链接: 使用最小花费爬楼梯
题目描述:
给你一个整数数组 cost
,其中 cost[i]
是从楼梯第 i
个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。你可以选择从下标为 0
或下标为 1
的台阶开始爬楼梯。请你计算并返回达到楼梯顶部的最低花费。
涉及知识点: 动态规划
思路:
- 确定dp数组以及下标的含义: 到达第
i
台阶所花费的最少体力为dp[i]。 - 确定递推公式:有两个途径可以到达
dp[i]
,dp[i - 1]
跳到dp[i]
需要花费dp[i - 1] + cost[i - 1]
;dp[i - 2]
跳到dp[i]
需要花费dp[i - 2] + cost[i - 2]
。选两者中最小的,dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])
; - dp数组初始化:初始站在第
0
个或1
台阶是不花费的,只有爬的时候才要,故有dp[0]=dp[1]=0
。 - 确定遍历顺序:从前到后遍历
代码:
class Solution {
public int minCostClimbingStairs(int[] cost) {
int[] dp = new int[cost.length+1];
dp[0] = 0;
dp[1] = 0;
for(int i = 2;i<=cost.length;i++){
dp[i] = Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
}
return dp[cost.length];
}
}
2022-12-14
8.【62】不同路径(medium)
题目链接: 不同路径问题
题目描述:
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
涉及知识点: 动态规划
思路:
- 确定dp数组以及下标的含义: 表示从
(0,0)
出发,到(i, j)
有dp[i][j]
条不同的路径。 - 确定递推公式:
dp[i][j]
只能由dp[i - 1][j]
和dp[i][j - 1]
得来,则dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
。 - dp数组初始化:因为从
(0, 0)
的位置到(i, 0)
或(0,j)
的路径只有一条,则dp[i][0]
和dp[0][j]
一定都是1。 - 确定遍历顺序:从前到后和从上到下遍历。
代码:
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 = 0; 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][j-1] + dp[i-1][j];
}
}
return dp[m-1][n-1];
}
}
9.【63】不同路径 II(medium)
题目链接: 不同路径问题
题目描述:
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1
和 0
来表示。
涉及知识点: 动态规划
思路:
- 确定dp数组以及下标的含义: 表示从
(0,0)
出发,到(i, j)
有dp[i][j]
条不同的路径。 - 确定递推公式:当
(i,j)
位置没有障碍物时,dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
。 - dp数组初始化:在没有遇到障碍物前,
dp[i][0]
和dp[0][j]
都是1
,障碍物及其之后或之下的位置因为到达不了,所以都为0
。 - 确定遍历顺序:从前到后和从上到下遍历。
代码:
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int row = obstacleGrid.length;
int col = obstacleGrid[0].length;
int[][] dp = new int[row][col];
for(int i = 0; i < row; i++){
if(obstacleGrid[i][0]==1){
break;
}
dp[i][0] = 1;
}
for(int j = 0; j < col; j++){
if(obstacleGrid[0][j]==1){
break;
}
dp[0][j] = 1;
}
for(int i = 1; i < row; i++){
for(int j = 1; j < col; j++){
if(obstacleGrid[i][j]==0){
dp[i][j] = dp[i-1][j]+dp[i][j-1];
}
}
}
return dp[row-1][col-1];
}
}
10.【343】整数拆分(medium)
题目链接: 整数拆分问题
题目描述:
给定一个正整数 n
,将其拆分为 k
个 正整数的和( k >= 2
),并使这些整数的乘积最大化。
返回你可以获得的最大乘积 。
涉及知识点: 动态规划
思路:
- 确定dp数组以及下标的含义: 分拆数字
i
,可以得到的最大乘积为dp[i]
。 - 确定递推公式:
dp[i]
可以由j * (i - j)
直接相乘,或者是j * dp[i - j]
,相当于是拆分(i - j)
,则dp[i] = max({dp[i], (i - j) * j, dp[i - j] * j})
,这里要比较dp[i]
是因为在递推公式推导的过程中,每次计算dp[i],最后取最大的。 - dp数组初始化:
dp[0]
和dp[1]
的初始化无意义,只需要初始化dp[2] = 1
。 - 确定遍历顺序:从前到后遍历。
代码:
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(j*(dp[i-j]),j*(i-j)));
}
}
return dp[n];
}
}
2022-12-18
11.【96】不同的二叉搜索树(medium)
题目链接: 不同的二叉搜索树问题
题目描述:
给你一个整数 n
,求恰由 n
个节点组成且节点值从 1
到 n
互不相同的二叉搜索树有多少种?返回满足题意的二叉搜索树的种数。
涉及知识点: 动态规划
思路:
这道题最开始是在二叉树章节想尝试做的,后来发现需要用动规来解,于是就放到了这里来做,其实主要用的就是动规,和二叉树相关的部分就是关于二叉搜索树的定义吧。
- 确定dp数组以及下标的含义:
1
到i
为节点组成的二叉搜索树的个数为dp[i]
。 - 确定递推公式:
dp[i]
+=dp[以j为头结点左子树节点数量]
*dp[以j为头结点右子树节点数量]
即dp[i] += dp[j - 1] * dp[i - j]
,j-1
为j
为头结点左子树节点数量,i-j
为以j
为头结点右子树节点数量。 - dp数组初始化:
dp[0]=1
。 - 确定遍历顺序:用
j
来遍历i
里面每一个数作为头结点的状态。
代码:
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[i-j]*dp[j-1];
}
}
return dp[n];
}
}
2022-12-21
【知识点】0-1背包问题
(1)什么是背包问题
背包问题: 有n
件物品和一个最多能背重量为w
的背包。第i件物品的重量是weight[i]
,得到的价值是value[i]
。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
背包问题可以分为以下几类:
其中,0-1背包和完全背包在面试中考察较多,0-1背包更是基础中的基础。
(2)二维dp数组0-1背包
动规五部曲:
-
确定dp数组以及下标的含义:
dp[i][j]
表示从下标为[0-i]
的物品里任意取,放进容量为j
的背包,价值总和最大是多少。 -
确定递推公式: 有两个方向推出来
dp[i][j]
-
不放物品
i
:由dp[i - 1][j]
推出,即背包容量为j
,里面不放物品i
的最大价值。(其实就是当物品i
的重量大于背包j
的重量时,物品i
无法放进背包中,所以被背包内的价值依然和前面相同。) -
放物品
i
:由dp[i - 1][j - weight[i]]
推出,dp[i - 1][j - weight[i]]
为背包容量为j - weight[i]
的时候不放物品i
的最大价值,那么dp[i - 1][j - weight[i]] + value[i]
就是背包放物品i得到的最大价值。
递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
- 初始化dp数组:
- 确定遍历顺序:
在二维dp数组的情况下,先遍历物品或先遍历背包容量都是可以的,因为非零背包和背包容量对应的值都来自于其上方和左上角,这两种遍历顺序都可以保证上方和左上角有值。 - 打印dp数组验证对错
(3)一维dp数组(滚动数组)
对二维dp数组进行压缩,可以用一维数组来代替。
- 确定dp数组以及下标的含义: 容量为
j
的背包,所背的物品价值可以最大为dp[j]
。 - 确定递推公式: 与二维数组类似:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
- 初始化dp数组:
都初始化为0即可 - 确定遍历顺序:
// 一定要按这样的遍历顺序才可以得到正确结果
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
如果正序遍历背包,则会重复取同一物品,可以理解为待更新的dp[j]
的值应该来自于旧的dp[j-1]
和dp[j-weight[i]]
,如果采用正序遍历,那么再更新dp[j]
的值时用的数据是新的dp[j-1]
和dp[j-weight[i]]
,所以会得到错误结果。
如果先遍历背包再遍历物品,那么每个dp[j]
就只会放入一个物品,显然不对。
- 打印dp数组验证对错
2022-12-26
12.【416】分割等和子集(medium)
题目链接: 分割等和子集问题
题目描述:
给你一个 只包含正整数的非空数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
涉及知识点: 动态规划(零一背包)
思路:
这道题是可以使用零一背包解决的经典问题,只需要搞清楚对应的含义,就可以代入零一背包的解法。即:
- 数组和的一半
target
是背包的容量,也是背包期望的价值 nums[i]
既是物品i
的重量,也是物品的价值
代入零一背包的解题思路:
dp[j]
表示: 容量为j
的背包,所背的物品价值可以最大为dp[j]
。- 递推公式:
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
- dp数组初始化:
dp[0]=0
,非0下标都初始化为0(如果题目给的价值有负数,那么非0下标就要初始化为负无穷)。 - 确定遍历顺序:用一维
dp
数组的情况下,遍历物品的for循环放在外层,遍历背包的for循环放在内层且倒序遍历。
代码:
class Solution {
public boolean canPartition(int[] nums) {
if(nums==null||nums.length==0){
return false;
}
int n = nums.length;
int sum = 0;
for(int num:nums){
sum+=num;
}
if(sum%2!=0){
return false;
}
int target = sum/2;
int[] dp = new int[target+1];
dp[0] = 0;
for(int i = 0;i<n;i++){
for(int j = target;j>=nums[i];j--){
dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
}
}
return dp[target]==target;
}
}
13.【1049】最后一块石头的重量 II(medium)
题目链接: 最后一块石头的重量 II
题目描述:
有一堆石头,用整数数组stones
表示。其中 stones[i]
表示第 i
块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x
和 y
,且 x <= y
。那么粉碎的可能结果如下:
- 如果
x == y
,那么两块石头都会被完全粉碎; - 如果
x != y
,那么重量为x
的石头将会完全粉碎,而重量为y
的石头新重量为y-x
。
最后,最多只会剩下一块石头。返回此石头最小的可能重量 。如果没有石头剩下,就返回 0
。
涉及知识点: 动态规划(零一背包)
思路:
这道题很容易被题目弯弯绕绕的迷惑,但是剖析其本质,其实还是零一背包问题,可以这样理解:把石头尽量均分成重量相似的两堆,两堆石头相撞,即它们的重量相减,剩下的就是最终的重量,这样一来就可以按照上一题《分割等和子集》的思路来做。
dp[j]
表示: 容量为j
的背包,所背的物品价值可以最大为dp[j]
。- 递推公式:
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
- dp数组初始化:
dp[0]=0
,非0下标都初始化为0。 - 确定遍历顺序:用一维
dp
数组的情况下,遍历物品的for循环放在外层,遍历背包的for循环放在内层且倒序遍历。
代码:
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 target = sum/2; //这里是向下取整,尽量均分
int[] dp = new int[target+1];
for(int i = 0; i < n; i++){
for(int j = target; j>=stones[i];j--){
dp[j] = Math.max(dp[j],dp[j-stones[i]]+stones[i]);
}
}
return sum-2*dp[target]; //得到最终的剩余重量
}
}
14.【494】目标和(medium)
题目链接: 目标和问题
题目描述:
给你一个整数数组nums
和一个整数target
。
向数组中的每个整数前添加 '+'
或 '-'
,然后串联起所有整数,可以构造一个表达式 :
- 例如,
nums = [2, 1]
,可以在2
之前添加'+'
,在1
之前添加'-'
,然后串联起来得到表达式"+2-1"
。
返回可以通过上述方法构造的、运算结果等于target
的不同表达式的数目。
涉及知识点: 动态规划(零一背包)
思路:
这道题也可以转为零一背包的思路,把前面加+
的值放在一堆,作为left
,同样把前面加-
的值放在一堆,作为right
,根据题意可以得到两个公式:left+right=sum
&left-right=target
,两个方程可以解得left=(sum+target)/2
,则题目就变成了求装满容量为left
的背包有几种方法,这里的(sum+target)/2
一定是可以除尽,如果除不尽则说明没有组合能得到target
。
dp[j]
表示: 装满容量为j
的背包,有dp[j]
种方法。- 递推公式:
dp[j] += dp[j - nums[i]]
注意:求装满背包有几种方法类似的题目,递推公式基本都是如此 - dp数组初始化:
dp[0]=1
,非0下标都初始化为0。 - 确定遍历顺序:用一维
dp
数组的情况下,遍历物品的for循环放在外层,遍历背包的for循环放在内层且倒序遍历。
代码:
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];
}
// 除不尽则说明不存在组合可以得到target
if((sum+target)%2!=0){
return 0;
}
// 去掉不可能的情况,可以过滤target是比nums的和的绝对值还大的负数的情况
if(Math.abs(target)>sum){
return 0;
}
// left表示正数的和
int left = (sum + target)/2;
int[] dp = new int[left+1];
dp[0] = 1;
for(int i = 0;i < n;i++){
for(int j = left;j >= nums[i];j--){
dp[j] += dp[j-nums[i]];
}
}
return dp[left];
}
}
2022-12-27
15.【474】一和零(medium)
题目链接: 一和零问题
题目描述:
给你一个二进制字符串数组 strs
和两个整数 m
和 n
。
请你找出并返回 strs
的最大子集的长度,该子集中最多有 m
个 0
和 n
个 1
。
子集:如果 x
的所有元素也是 y
的元素,集合 x
是集合 y
的 子集 。
涉及知识点: 动态规划(零一背包)
思路:
这道题涉及到两个维度的零一背包,即一个维度表示0
的数量,另一个维度表示1
的数量,因此这里要用二维dp
数组才能表示,但他的本质仍是一维dp
数组。
dp[i][j]
表示: 最多有i个0
和j个1
的strs
的最大子集的大小为dp[i][j]
。- 递推公式:每个字符串有
zeroNum个0
,oneNum个1
,则dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1)
- dp数组初始化:
dp[0]=0
,非0下标都初始化为0。 - 确定遍历顺序:遍历物品的for循环放在外层,遍历背包的for循环放在内层且倒序遍历。
代码:
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
// 两个维度的零一背包问题
int[][] dp = new int[m+1][n+1];
dp[0][0] = 0;
for(String str:strs){ //遍历物品
int zero = 0; //0的个数
int one = 0; //1的个数
for(char ch:str.toCharArray()){
if(ch=='0'){
zero++;
}else{
one++;
}
}
for(int i = m;i >= zero;i--){ //遍历背包
for(int j = n; j >= one;j--){
dp[i][j] = Math.max(dp[i-zero][j-one]+1,dp[i][j]);
}
}
}
return dp[m][n];
}
}
【知识点】完全背包问题
(1)什么是完全背包
有N
件物品和一个最多能背重量为W
的背包。第i件物品的重量是weight[i]
,得到的价值是value[i]
。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
(2)完全背包与01背包的不同
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。解题方法上基本上一致,不同之处在于:
- 01背包为了保证每件物品只使用一次,所以在遍历背包时是倒序遍历,而在完全背包中则是郑旭便利。
- 纯完全背包中的遍历物品循环和遍历背包循环的顺序可以颠倒,不影响最后结果。但非纯背包问题中,先物品后背包的情况下是组合,先背包后物品的情况下是排列。
16.【518】零钱兑换 II(medium)
题目链接: 零钱兑换2
题目描述:
给你一个整数数组 coins
表示不同面额的硬币,另给一个整数 amount
表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0
。
假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带符号整数。
涉及知识点: 动态规划(完全背包)
思路:
本题和《目标和》类似,不过这里是完全背包。
dp[j]
表示:凑成总金额j的货币组合数为dp[j]
。- 递推公式:
dp[j] += dp[j - coins[i]];
- dp数组初始化:
dp[0]=1
,非0下标都初始化为0。 - 确定遍历顺序:遍历物品的for循环放在外层,遍历背包的for循环放在内层且正序遍历。
代码:
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];
}
}
2022-12-29
17.【377】 组合总和 Ⅳ(medium)
题目链接: 组合总和 Ⅳ
题目描述:
给你一个由不同整数组成的数组nums
,和一个目标整数target
。请你从nums
中找出并返回总和为target
的元素组合的个数。题目数据保证答案符合 32 位整数范围。请注意,顺序不同的序列被视作不同的组合。
涉及知识点: 动态规划(完全背包)
思路:
本题和《零钱兑换 II》的思想是类似的,不过前者是求组合,因此先遍历物品再遍历背包,本题其实是求排列,所以要先遍历背包再遍历物品。
dp[j]
表示:凑成目标正整数为i的排列个数为dp[i]
。- 递推公式:
dp[j] += dp[j - nums[i]];
- dp数组初始化:
dp[0]=1
,非0下标都初始化为0。 - 确定遍历顺序:因为是求排列,所以遍历背包的for循环放在外层,遍历物品的for循环放在内层。
代码:
class Solution {
public int combinationSum4(int[] nums, int target) {
int dp[] = new int[target+1];
dp[0] = 1;
// 求排列,所以要先遍历背包再遍历物品
for(int j = 0;j<=target;j++){
for(int i = 0;i<nums.length;i++){
if(j>=nums[i]){
dp[j] += dp[j-nums[i]];
}
}
}
return dp[target];
}
}
18.【322】零钱兑换(medium)
题目链接: 零钱兑换
题目描述:
给你一个整数数组coins
,表示不同面额的硬币;以及一个整数amount
表示总金额。
计算并返回可以凑成总金额所需的最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
涉及知识点: 动态规划(完全背包)
思路:
本题求的是最少硬币个数,也是比较典型的完全背包问题,但是要注意初始化不能随便设为0了。
dp[j]
:凑足总额为j所需钱币的最少个数为dp[j]
- 递推公式:
dp[j] = min(dp[j - coins[i]] + 1, dp[j])
,因为是求最小数量,所以dp[j - coins[i]] + 1
就代表加上当前这个硬币后的数量。 - dp数组初始化:
dp[0]=0
,非0下标要初始为正整数的最大值。 - 确定遍历顺序:因为是求数量,所以先遍历背包还是先遍历物品都可以,不影响硬币数量。
代码:
class Solution {
public int coinChange(int[] coins, int amount) {
int max = Integer.MAX_VALUE;
int[] dp = new int[amount+1];
dp[0] = 0;
for (int j = 1; j < dp.length; j++) {
dp[j] = max;
}
for(int i = 0; i<coins.length; i++){
for(int j = coins[i];j<=amount;j++){
//只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要
if (dp[j - coins[i]] != max) {
dp[j] = Math.min(dp[j],dp[j-coins[i]]+1);
}
}
}
return dp[amount]==max?-1:dp[amount];
}
}
2022-12-30
19.【279】完全平方数(medium)
题目链接: 完全平方数
题目描述:
给你一个整数n
,返回 和为 n
的完全平方数的最少数量 。
完全平方数是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
涉及知识点: 动态规划(完全背包)
思路:
本题其实和《18.【322】零钱兑换》的思路是一致的,把完全平方数看成物品,n看成背包即可。
dp[j]
:和为j
的完全平方数的最少数量为dp[j]
- 递推公式:
dp[j] = min(dp[j - i * i] + 1, dp[j])
- dp数组初始化:
dp[0]=0
,非0下标要初始为正整数的最大值。 - 确定遍历顺序:因为是求数量,所以先遍历背包还是先遍历物品都可以。
代码:
class Solution {
public int numSquares(int n) {
int[] dp = new int[n+1];
dp[0] = 0;
int max = Integer.MAX_VALUE;
for(int i =1;i<=n;i++){
dp[i] = max;
}
for(int i = 0;i*i<=n;i++){
for(int j = i*i;j<=n; j++){
if(dp[j-i*i]!=max){
dp[j] = Math.min(dp[j],dp[j-i*i]+1);
}
}
}
return dp[n];
}
}
20.【139】单词拆分(medium)
题目链接: 单词拆分
题目描述:
给你一个字符串s
和一个字符串列表wordDict
作为字典。请判断是否可以利用字典中出现的单词拼接出s
。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
涉及知识点: 动态规划(完全背包)
思路:
本题把单词表中的单词看作物品,把字符串看作背包,就可以用完全背包的思想来做,但是dp数组、递推公式等和常规的背包问题不太一样,要注意下。
dp[i]
:字符串长度为i
的话,dp[i]
为true
,表示可以拆分为一个或多个在字典中出现的单词。- 递推公式:如果确定
dp[j]
是true
,且[j, i]
这个区间的子串出现在字典里,那么dp[i]
一定是true
。(j < i )
。所以递推公式是if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true
。 - dp数组初始化:
dp[0]=true
,非0下标初始为false
。 - 确定遍历顺序:这里要考虑单词的顺序,其实求的是排列数,所以要先遍历背包再遍历物品。
代码:
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
boolean[] dp = new boolean[s.length()+1];
dp[0] = true;
for(int i = 0;i<=s.length();i++){
for(String str:wordDict){
int length = str.length();
if(i>=length&&dp[i-length]&& str.equals(s.substring(i - length, i))){
dp[i] = true;
}
}
}
return dp[s.length()];
}
}
【知识点】背包问题总结
背包问题中的递推公式和遍历顺序规律性和代表性,以下从这两个对其进行总结。
(1)背包递推公式
问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
,对应题目如下:
问装满背包有几种方法: dp[j] += dp[j - nums[i]]
,对应题目如下:
问背包装满最大价值: dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
,对应题目如下:
问装满背包所有物品的最小个数: dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
,对应题目如下:
(2)遍历顺序
01背包:
- 二维dp数组:先遍历物品还是先遍历背包都可以,且第二层for循环是从小到大遍历。
- 一维dp数组:只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。
完全背包:
- 对于纯完全背包的一维dp数组实现,先遍历物品或先遍历背包都可以,且第二层for循环是从小到大遍历。
- 如果求组合数,则外层for循环遍历物品,内层for遍历背包。
- 如果求排列数,则外层for遍历背包,内层for循环遍历物品。
相关题目如下:- 求组合数:518. 零钱兑换 II
- 求排列数:377.组合总和Ⅳ、70. 爬楼梯进阶版(完全背包)
- 如果求最小数,则哪种遍历顺序都可以。
相关题目如下:
2022-12-31
21.【198】打家劫舍(medium)
题目链接: 打家劫舍
题目描述:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
涉及知识点: 动态规划
思路:
这道题翻译一下就是说如果偷第i
家,那么第i-1
家就不能偷,可以选择偷第i-2
家,根据这个理解,其实就和爬楼梯的初始解法有点类似。
dp[i]
:考虑下标i(包括i)
以内的房屋,最多可以偷窃的金额为dp[i]
。- 递推公式:
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
,前者选择不偷第i
间,后者选择偷第i
间。 - dp数组初始化:
dp[0]=nums[0],dp[1]=max(nums[0],nums[1])
- 确定遍历顺序:从小到大遍历
代码:
class Solution {
public int rob(int[] nums) {
if(nums==null||nums.length==0){
return 0;
}
if(nums.length==1){
return nums[0];
}
int[] dp = new int[nums.length];
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];
}
}
22.【213】 打家劫舍 II(medium)
题目链接: 打家劫舍 II
题目描述:
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。 给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
涉及知识点: 动态规划
思路:
本题的难点在于拆解环形结构,其实可以理解为要分成两种情况,如果考虑偷第一家就不能偷最后一家,反之亦然,所以可以分成两个线性数组,最后看哪种情况偷的钱最多,递归的逻辑和打家劫舍问题是一样的。
dp[i]
:考虑下标i(包括i)
以内的房屋,最多可以偷窃的金额为dp[i]
。- 递推公式:
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
,前者选择不偷第i
间,后者选择偷第i
间。 - dp数组初始化:
dp[0]=nums[0],dp[1]=max(nums[0],nums[1])
- 确定遍历顺序:从小到大遍历
代码:
class Solution {
public int rob(int[] nums) {
if(nums==null||nums.length==0){
return 0;
}
int numsLen = nums.length;
if(numsLen==1){
return nums[0];
}
if(numsLen==2){
return Math.max(nums[0],nums[1]);
}
//偷头不偷尾,偷尾不偷头
int result = Math.max(robResult(nums,0,numsLen-1),robResult(nums,1,numsLen));
return result;
}
public int robResult(int[] nums,int start,int end) {
int len = end-start;
int[] dp = new int[len];
dp[0] = nums[start];
dp[1] = Math.max(nums[start],nums[start+1]);
for(int i = 2; i < len; i++){
dp[i] = Math.max(dp[i-2]+nums[i+start],dp[i-1]);
}
return dp[len-1];
}
}
23.【213】 打家劫舍 III(medium)
题目链接: 打家劫舍 III
题目描述:
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root
。
除了root
之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 **两个直接相连的房子在同一天晚上被打劫 **,房屋将自动报警。
给定二叉树的 root
。返回在不触动警报的情况下,小偷能够盗取的最高金额 。
涉及知识点: 动态规划
思路:
这道题是树形动规的一个入门题,解题的大框架要以后序遍历为主,中间穿插动规部分,本题的dp数组形式也和常规的不一样,这里用dp[2]来记录偷还是不偷当前节点的房子。
dp[2]
:0表示不偷当前节点获得的最大金额,1表示偷当前节点获得的最大金额。- 递推公式:
dp[0] = max(left[0],left[1])+max(right[0],right[1]);dp[1]=node.val+left[0]+right[0]
- dp数组初始化:在递归的终止条件成立时,即node==null时,赋值dp[2]={0,0},相当于初始化
- 确定遍历顺序:后序遍历
代码:
class Solution {
public int rob(TreeNode root) {
// 树形dp
int[] result = robTree(root);
return Math.max(result[0],result[1]);
}
public int[] robTree(TreeNode node){
int[] res = new int[2];
if(node==null){
return res;
}
int[] left = robTree(node.left);
int[] right = robTree(node.right);
// 0不偷 1偷
res[0] = Math.max(left[0],left[1])+Math.max(right[0],right[1]);
res[1] = node.val+left[0]+right[0];
return res;
}
}
2022结束啦~~2023要加油^_^