【Java】HOT100+代码随想录 动态规划(上)背包问题

目录

理论基础

 一、基础题目

LeetCode509:斐波那契数

LeetCode70:爬楼梯

LeetCode746:使用最小花费爬楼梯

LeetCode62:不同路径

 LeetCode63:不同路径ii

 LeetCode343:整数拆分

 LeetCode96:不同的二叉搜索树

二、背包问题

2.1 01背包

01背包理论基础1:二维dp数组

01背包理论基础2:滚动/一维dp数组

LeetCode416:分割等和子集(求是否能正好装满)

LeetCode1049:最后一块石头的重量ii(最多装多少)

 LeetCode493:目标和(求装满背包有多少种方法)

LeetCode474:一和零(求装满背包最多有多少个物品)

2.2 完全背包

理论基础

LeetCode518:零钱兑换ii(求组合数)

 LeetCode377:组合总和iv(求排列)

 LeetCode70:爬楼梯(求排列数)

LeetCode322:零钱兑换(求最小数)

LeetCode279:完全平方数(求最小数)

LeetCode139:单词拆分

2.3 多重背包

理论基础


理论基础

动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。

所以动态规划中每一个状态一定是由上一个状态推导出来的;

举例:

有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

动态规划中dp[j]是由dp[j-weight[i]]推导出来的,然后取max(dp[j], dp[j - weight[i]] + value[i])。

这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的

  • 状态转移公式(递推公式)在动态规划中相当重要
  • 动态规划五部曲如下:
  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化(很多时候递推公式决定了dp数组要如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

在debug时把dp数组打印出来,看看究竟是不是按照自己思路推导的


 一、基础题目

LeetCode509:斐波那契数

思路:每一个数字等于前两项之和,给定n求F(n)。动规五部曲——

①dp数组和下标的含义:dp[i]即第i个数的斐波那契数

②递归公式:题干中就给出了,dp[i] = dp[i - 1] + dp[i - 2];

③初始化:题干中也给出了,dp[0] = 0,dp[1] = 1;

④遍历顺序:从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历

⑤举例推导:按照递推公式,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-1] + dp[i-2];}return dp[n];}
}

LeetCode70:爬楼梯

思路:每次你可以爬 1 或 2 个台阶

①dp数组和下标的含义:爬到第i阶,有dp[i]种方法

②递归公式:爬到i-1阶有dp[i-1]种方法,然后再爬一阶就是dp[i];同理,爬到i-2阶有dp[i-2]种方法,然后再爬两阶就是dp[i]。所以dp[i] = dp[i - 1] + dp[i - 2];

③初始化:可以推测出,dp[0] = 0(不重要),dp[1] = 1,dp[2] = 2;

④遍历顺序:从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出遍历的顺序一定是从前到后遍历

⑤举例推导:按照递推公式,n为5时,dp数组应该是如下的:

0 1 2 3 5 8

发现不就是斐波那契数吗?!同样的代码一点毛病没有

public int climbStairs(int n) {int[] dp = new int[n + 1];dp[0] = 1;dp[1] = 1;for (int i = 2; i <= n; i++) {dp[i] = dp[i - 1] + dp[i - 2];}return dp[n];
}

也可以使用变量优化一下空间复杂度

class Solution {public int climbStairs(int n) {if(n <= 2) return n;int a = 1, b = 2, sum = 0;for(int i = 3; i <= n; i++){sum = a + b;  // f(i - 1) + f(i - 2)a = b;        // 记录f(i - 1),即下一轮的f(i - 2)b = sum;      // 记录f(i),即下一轮的f(i - 1)}return b;}
}

LeetCode746:使用最小花费爬楼梯

思路:可以选择从下标0或者1开始爬,爬1-2阶

①dp数组和下标的含义:爬到第i阶,所花费的最少体力是dp[i]

②递归公式:要算得dp[i]有两种方法,一个是从dp[i-1]花cost[i-1]向上爬一阶,一个是行dp[i-2]花cost[i-2]向上爬两阶,最终使用哪种是最少话费呢?

那就是dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);

③初始化:可以推测出,dp[0] = 0,dp[1] = 0;

④遍历顺序:从递归公式中可以看出遍历的顺序一定是从前到后遍历

⑤举例推导

class Solution {public int minCostClimbingStairs(int[] cost) {int len = cost.length;int[] dp = new int[len + 1];// 从下标为 0 或下标为 1 的台阶开始,因此支付费用为0dp[0] = 0;dp[1] = 0;// 计算到达每一层台阶的最小费用for (int i = 2; i <= len; i++) {dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);}return dp[len];}
}

LeetCode62:不同路径

思路:如果采用深度搜索,则这棵树的深度为m+n-1,而深搜代码的时间复杂度为O(2^(m + n - 1) - 1),这是非常大的。那么采用动态规划,从(0,0)出发,到(m,n):

①dp数组和下标的含义:dp[i][j]表示从(0,0)出发,到达(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[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理

for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) 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-1][j] + dp[i][j-1];}}return dp[m-1][n-1];}
}

 LeetCode63:不同路径ii

思路:相比1,给出了障碍物,路径需要绕开障碍物。与1的区别仅仅在于:

1. 初始化时如果障碍在边上,则仅初始化障碍前的dp值为1,障碍后仍为0(无法到达)

2. 在遍历时跳过给有障碍的dp赋值

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 && obstacleGrid[i][0] == 0;i++){dp[i][0] = 1;}for(int i=0;i<n && obstacleGrid[0][i] == 0;i++){dp[0][i] = 1;}for(int i=1;i<m;i++){for(int j=1;j<n;j++){if(obstacleGrid[i][j] == 1)   continue;dp[i][j] = dp[i-1][j] + dp[i][j-1];}}return dp[m-1][n-1];}
}

 LeetCode343:整数拆分

思路:给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 

①dp数组和下标的含义:分拆数字i所得到的最大乘积是dp[i]

②递归公式:要算得dp[i]有两种方法,从1遍历j,一个是j*(i-j),一个是j*dp[i-j]。可以这么理解,j * (i - j) 是单纯的把整数拆分为两个数相乘,而j * dp[i - j]是拆分成两个以及两个以上的个数相乘。

那就是dp[i] = max({dp[i], (i - j) * j, dp[i - j] * j});

在取最大值的时候,为什么还要比较dp[i]呢?因为在递推公式推导的过程中,每次计算dp[i],取最大的而已。

③初始化:可以推测出,dp[2] = 1,dp[3] = 2;(n从3开始拆)

④遍历顺序:从递归公式中可以看出遍历的顺序一定是从前到后遍历的,且先有dp[i - j]再有dp[i]。i从3-n,j从1到i-1(不包括i-1),更优化的遍历方法是:j直接从1到i/2,这是因为一定是拆分成m个近似相同的子数相乘才是最大的

⑤举例推导

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*(i-j),j*dp[i-j]));}}return dp[n];}
}

 LeetCode96:不同的二叉搜索树

思路:重点是推导公式,容易看出

dp[3],就是 元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量,

元素1为头结点搜索树的数量 = 右子树有2个元素的搜索树数量 * 左子树有0个元素的搜索树数量

元素2为头结点搜索树的数量 = 右子树有1个元素的搜索树数量 * 左子树有1个元素的搜索树数量

元素3为头结点搜索树的数量 = 右子树有0个元素的搜索树数量 * 左子树有2个元素的搜索树数量

dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]

① dp的概念:dp[i]就是以1到i为节点构成的二叉搜索树的个数

②递归公式:i从2开始遍历到n,j从1开始遍历到i,dp[i] += dp[j-1]*dp[i-j];

③初始化:dp[0]=1; dp[1] = 1;

④遍历顺序和举例推导

class Solution {public int numTrees(int n) {//初始化 dp 数组int[] dp = new int[n + 1];//初始化0个节点和1个节点的情况dp[0] = 1;dp[1] = 1;for (int i = 2; i <= n; i++) {for (int j = 1; j <= i; j++) {//对于第i个节点,需要考虑1作为根节点直到i作为根节点的情况,所以需要累加//一共i个节点,对于根节点j时,左子树的节点个数为j-1,右子树的节点个数为i-jdp[i] += dp[j - 1] * dp[i - j];}}return dp[n];}
}

二、背包问题

2.1 01背包

01背包理论基础1:二维dp数组

(物品下标-背包容量)

背包问题有多种背包方式,常见的有:01背包、完全背包、多重背包、分组背包和混合背包等等。

一个商品如果可以重复多次放入是完全背包,而只能放入一次是01背包,写法还是不一样的

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

二维数组 01背包:

  1. dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少
  2. 递归公式:不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品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的价值),就是背包放物品i得到的最大价值。
  3.  dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 
  4. 初始化:关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。根据递推公式可以看出需要得知dp[i][0]和dp[0][j]的初始值,才能开始递推。可以一开始就统一把dp数组统一初始为0。当背包容量为0时,无论怎么选取物品,都无法放入背包中,即价值为0,dp[i][0] = 0;当放入序号为0的物品时且背包容量为0-j时,所能放下物品的最大价值:当j<weight[0]时,背包里放不下物品0,dp[0][j]应该是0;当j>=weight[0]时,背包里可以放下编号0物品,dp[0][j]应该是value[0]。
  5. 遍历顺序:问题来了,先遍历物品还是先遍历背包重量呢?其实都可以!! 但是先遍历物品更好理解
  6. 举例推导:自己推导一下

例题:卡码网46 携带研究材料(给定背包容量,求装满的最大价值)

public class Main {public static void main(String[] args) {int[] weight = {2,2,3,1,5,2};int[] value = {2,3,1,5,4,3};int bagSize = 1;testWeightBagProblem(weight,value,bagSize);}public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){// 创建dp数组int goods = weight.length;  // 获取物品的数量int[][] dp = new int[goods][bagSize + 1];// 初始化dp数组// 创建数组后,其中默认的值就是0for (int j = weight[0]; j <= bagSize; j++) {dp[0][j] = value[0];}// 填充dp数组for (int i = 1; i < weight.length; i++) {for (int j = 1; j <= bagSize; j++) {if (j < weight[i]) {/*** 当前背包的容量都没有当前物品i大的时候,是不放物品i的* 那么前i-1个物品能放下的最大价值就是当前情况的最大价值*/dp[i][j] = dp[i-1][j];} else {/*** 当前背包的容量可以放下物品i* 那么此时分两种情况:*    1、不放物品i*    2、放物品i* 比较这两种情况下,哪种背包中物品的最大价值最大*/dp[i][j] = Math.max(dp[i-1][j] , dp[i-1][j-weight[i]] + value[i]);}}}System.out.println(dp[goods-1][bagSize]);}
}

01背包理论基础2:滚动/一维dp数组

对于背包问题其实状态都是可以压缩的。

在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:

dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);

即重复利用上一层,与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。

  1. 在一维数组中,dp[j]代表:容量为j的背包,所背的物品价值可以最大为dp[j];
  2. 递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);仍然相当于其中一层的不放物品i和放物品i
  3. 初始化:看一下递推公式,可知只需初始化dp[0]]=0即可;
  4. 遍历顺序:重点是倒序遍历,是为了确保物品i只被放入一次。二维的就不用倒序,因为二维dp[i][j]计算是通过上一层dp[i - 1][j]得来的,不会覆盖,而由于一维数组用的是同一个dp[j],所以会从前向后覆盖,影响dp值的计算,所以为了避免重复需要倒序。
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]);}
}

public class Main {public static void main(String[] args) {int[] weight = {2,2,3,1,5,2};int[] value = {2,3,1,5,4,3};int bagSize = 1;testWeightBagProblem(weight,value,bagSize);}public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){// 创建dp数组int goods = weight.length;  // 获取物品的数量int[] dp = new int[bagSize + 1];// 初始化dp数组,直接默认全为0// 填充dp数组,放不下的即j < weight[i]就默认不改变,一维数组直接继承上一层无需赋值,简化了代码for (int i = 0; i < goods; i++) {for (int j = bagSize; j >= weight[i]; j--) {dp[j] = Math.max(dp[j] , dp[j-weight[i]] + value[i]);}}System.out.println(dp[bagSize]);}
}

总结: 

1. 为什么二维背包两个for循环的嵌套顺序这么写?反过来写行不行?再讲一讲初始化的逻辑。

答:两个for循环的顺序无非是先遍历物品还是先遍历背包容量,在二维数组中其实都行得通,但是遍历物品更好理解。根据递推公式,每一个dp[i][j]其实都是由上一个dp[i-1][j]或者上一个dp[i - 1][j - weight[i]] 推导出来的,如果以表格的形式来看,位于dp[i][j]的左上角,如果先遍历物品,就是在每一个物品都遍历一遍不同的背包重量,从左到右,从上到下;而如果先遍历背包容量就是先从上到下,在从左到右,由于数据位于左上角,所以两个for的顺序都不影响dp[i][j]的推导。而这种推导方式显然要得到所有的dp[0][j]和dp[i][0]公式才能接着往下,往右推导。

2. 改成一维以后,两个for循环的顺序反过来写行不行?为什么?

答:不可以,改成一维之后只能先遍历物品再遍历背包容量,这是由于一维dp的写法中背包容量一定是要倒序遍历,其本质上还是对二维数组的遍历,右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖,保证每个物品只添加一次。如果非要换顺序,倒序遍历的作用便失去了。


LeetCode416:分割等和子集(求是否能正好装满)

思路:是否可以将数组分割成两个子集,使得子集和相等。

怎么转换成背包问题呢?

首先,每个元素我们只用一次,所以是01背包问题。背包的最大容量是target = sum/2,放入n个元素,每个元素的值既是他的value,也是他的weight;dp[j]的定义是容量为j的背包里能装的最大重量,如果正好装满,即dp[j] = j

其次,01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);对于本题,相当于背包里放入数值,那么物品i的重量是nums[i],其价值也是nums[i]。所以递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);01背包的初始化:dp[0] = 0即可。

首先得到背包,最终做判断如果dp[target] = target,就说明可以分割等和子集

class Solution {public boolean canPartition(int[] nums) {int sum = 0;for(int i= 0;i<nums.length;i++){sum += nums[i];}if(sum % 2 == 1)    return false;int target = sum/2;int[] dp = new int[target+1];for(int i = 0;i<nums.length;i++){for(int j = target;j>=nums[i];j--){dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);}}if(dp[target] == target){return true;}return false;}
}

LeetCode1049:最后一块石头的重量ii(最多装多少)

思路:其实本质上就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小这样就化解成01背包问题了,同样是每个物品重量和价值都为stones[i]。target = sum/2。

得到背包后,不用完全相等,一堆石头是dp[target],另一堆是sum - dp[target]。由于target = sum/2是向下取整,所以sum - dp[target] > dp[target],

所以最终的最小石头重量为 (sum - dp[target]) - dp[target]。

class Solution {public int lastStoneWeightII(int[] nums) {int sum = 0;for(int i= 0;i<nums.length;i++){sum += nums[i];}int target = sum/2;int[] dp = new int[target+1];for(int i = 0;i<nums.length;i++){for(int j = target;j>=nums[i];j--){dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);}}return sum - 2*dp[target];}
}

 LeetCode493:目标和(求装满背包有多少种方法)

思路:如何转化成01背包问题——

一部分left为+,一部分right为-,left-right = target,left+rght = sum,已知sum和target,就可以求出 left = (target + sum)/2 ,此时问题就变成了在集合中找bagSize = left的组合。

之前的几道题都是求容量为j的背包,最多能装多少,本题则是说装满有几种方法,是组合问题

* 首先排除两种情况,即①target的绝对值>sum,以及②(target+sum)除以2的余数不为0

1. dp[j]:容量为j的背包能有dp[j]种方法,

2. 递推公式:求方法的总和——dp[j] += dp[j - nums[i]]

3. 初始化:dp[0] = 1,别的初始化为0;背包容量为0的话,[0]可以有一种取法,[0,0,0,0,0]有32种取法,也是dp[0] = 1叠加起来的

4. 遍历顺序就是01背包的先遍历物品,再倒序遍历背包容量

5. 举例推导:

这道题也可以用回溯来做,甚至可以列出所有组合,dp仅仅可以求个数

class Solution {public int findTargetSumWays(int[] nums, int target) {int sum =0;for(int i=0;i<nums.length;i++){sum += nums[i];}if(Math.abs(target) > sum)  return 0;if((target+sum)%2 == 1) return 0;int bagSize = (target+sum)/2;int[] dp = new int[bagSize+1];dp[0] = 1;for(int i=0;i<nums.length;i++){for(int j=bagSize;j>=nums[i];j--){dp[j] += dp[j-nums[i]];}}return dp[bagSize];}
}

LeetCode474:一和零(求装满背包最多有多少个物品)

思路:strs数组里的元素就相当于是物品,包含zeroNum个0,oneNum个1,这就是一个典型的01背包! 只不过物品的重量有了两个维度而已。

dp[i][j]就相当于最多有i个0和j个1的最大子集大小,递推通过加一来实现即可。字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])。

相当于还是先遍历物品,在内循环里通过构建zeroNum和oneNum两个双循环来遍历背包容量。

01背包递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

该二维weight递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);

其实就是把dp变成二维的,本质还是一样的

class Solution {public int findMaxForm(String[] strs, int m, int n) {int[][] dp = new int[m+1][n+1];for(String str: strs){    //遍历物品int zeroNum = 0;int oneNum = 0;//得到每个物品的weight,分为0和1两个维度for(char ch: str.toCharArray()){if(ch == '0'){zeroNum++;}else{oneNum++;}}for(int i = m; i>=zeroNum; i--){for(int j = n; j>=oneNum; j--){dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);}}}return dp[m][n];}
}

2.2 完全背包

理论基础

区别于01背包:每件物品都可以放入背包无限次,可以重复放入

首先再回顾一下01背包的核心代码

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]);}
}

我们知道01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。

而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即:

// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);}
}

 且在纯完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!

例题:携带研究材料(完全背包)

public class Main {public static void main(String[] args) {int[] weight = {1,2,3,4};int[] value = {2,4,4,5};int bagSize = 5;// 创建dp数组int goods = weight.length;  // 获取物品的数量int[] dp = new int[bagSize + 1];// 初始化dp数组,直接默认全为0// 填充dp数组,放不下的即j < weight[i]就默认不改变,一维数组直接继承上一层无需赋值,简化了代码for (int i = 0; i < goods; i++) {for (int j = weight[i]; j <= bagSize; j++) {dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);}}System.out.println(dp[bagSize]);}
}

LeetCode518:零钱兑换ii(求组合数)

不同面额的硬币(可以重复使用)和一个总金额,求组合数

一个关键的知识点:

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

如果求排列数就是外层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];}
}

 LeetCode377:组合总和iv(求排列)

思路:同上,只是调一个for的顺序,顺序不同的序列被视作不同的组合所以是求排列

class Solution {public int combinationSum4(int[] nums, int target) {int[] dp = new int[target+1];dp[0] = 1;for(int i=0;i<= target;i++){for(int j=0;j<nums.length;j++){if(i >= nums[j])    dp[i] += dp[i-nums[j]];}}return dp[target];}
}

 LeetCode70:爬楼梯(求排列数)

思路:这其实就是完全背包的求排列数,秒啦

import java.util.Scanner;
public class Main{public static void main(String[] args){Scanner sc = new Scanner(System.in);while(sc.hasNextInt()){int n = sc.nextInt();int m = sc.nextInt();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];}}System.out.println(dp[n]);}}
}

LeetCode322:零钱兑换(求最小数)

凑成目标的最小硬币数量

思路:相比于LeetCode474:一和零是01背包求最大个数(二维weight)

这道题只不过改成了是完全背包求最小个数(weight就是数值)

这个初始化dp[0]=0,且由于求的是最小值,所以需要把所有dp一开始初始化为max才能进行比较

不多说了上代码!

class Solution {public int coinChange(int[] coins, int amount) {int max = Integer.MAX_VALUE;int[] dp = new int[amount+1];//初始化dp数组为最大值for (int j = 0; j < dp.length; j++) {dp[j] = max;}dp[0] = 0;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];}
}

LeetCode279:完全平方数(求最小数)

思路:其实就是元素1-10的平方,给定target,上道题稍微改动一下就能过

class Solution {public int numSquares(int n) {int max = Integer.MAX_VALUE;int[] dp = new int[n+1];//初始化dp数组为最大值for (int j = 0; j < dp.length; j++) {dp[j] = max;}dp[0] = 0;for(int i=1;i<=10;i++){for(int j=i*i;j<=n;j++){//这个if是为了防止凑不成,而因为有1所以一定可以凑成,所以if可以略去if(dp[j-i*i]!= max) dp[j] = Math.min(dp[j],dp[j-i*i]+1);}}return dp[n]==max?-1:dp[n];}
}

LeetCode139:单词拆分

判断单词是否可以被空格拆分为字典里的词

思路:关于单词拆分,回溯法也能做(这个再说);如何转换成背包呢?单词即物品,字符串即背包,这道题就转换成了物品是否能正好把背包装满。

1. dp[i]代表i前面的子串是否能由字典构成,true/false

2. 递推:如果dp[j]==true && [j,i]子串也属于字典则dp[i]==true(i<j)

3. 初始化:dp[0] = true

class Solution {public boolean wordBreak(String s, List<String> wordDict) {boolean[] dp = new boolean[s.length()+1];HashSet<String> set = new HashSet<>(wordDict);dp[0] = true;for(int i=1;i<=s.length();i++){for(int j=0;j<i;j++){if(dp[j] == true && set.contains(s.substring(j,i))){//本身子字符串就不包含i,所以i就是下一个单词开始的第一个字符dp[i] = true;}}}return dp[s.length()];}
}

2.3 多重背包

理论基础

相当于n种物品和一个容量为v的背包,每种物品只有mi件,耗费空间ci,价值是wi

多重背包摊开可以变成01背包

例题:卡码网56 携带矿石资源

import java.util.Scanner;
class multi_pack{public static void main(String [] args) {Scanner sc = new Scanner(System.in);/*** bagWeight:背包容量* n:物品种类*/int bagWeight, n;//获取用户输入数据,中间用空格隔开,回车键换行bagWeight = sc.nextInt();n = sc.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] = sc.nextInt();for (int i = 0; i < n; i++) value[i] = sc.nextInt();for (int i = 0; i < n; i++) nums[i] = sc.nextInt();int[] dp = new int[bagWeight + 1];//先遍历物品再遍历背包,作为01背包处理for (int i = 0; i < n; i++) {for (int j = bagWeight; j >= weight[i]; j--) {//遍历每种物品的个数for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) {dp[j] = Math.max(dp[j], dp[j - k * weight[i]] + k * value[i]);}}}System.out.println(dp[bagWeight]);}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/13398.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue uniapp 小程序 判断日期是今天(显示时分秒)、昨天、本周的周几、超出本周显示年月日

效果图&#xff1a; util.js /*** 转换时间*/ const messageFormat (datetime) >{ let result "";let currentTime new Date();if(isToday(datetime)){result datetime.substring(11,16);}else if(isYesterday(datetime)){result "昨天";}else if(…

操作系统磁盘管理类问题

例题&#xff1a;在磁盘上存储数据的排列方式会影响1/0服务的总时间。假设每个磁道被划分成10个物理块&#xff0c;每个物理块存放1个逻辑记录。逻辑记录R1,R2....R10存放在同一个磁道上&#xff0c;记录的排列顺序如下表所示&#xff1a; 假定磁盘的旋转速度为10ms/周&#xf…

VMware虚拟机-安装程序无法自动安装virtual machine......_windows server 2008 R2

系统版本&#xff1a;windows server 2008 R2 问题-安装程序无法自动安装virtual machine… 在使用虚拟机安装windows server 2008 R2系统中&#xff0c;安装VMware Tools工具安祖啊寄给你失败&#xff0c;提示安装程序无法自动安装virtual machine…&#xff0c;必须手动安装…

从源头到洞察:大数据时代的数据提取与分析实战指南

随着科技的飞速发展&#xff0c;大数据已经成为现代社会的核心驱动力之一。从商业决策到科学研究&#xff0c;从政策制定到个人生活&#xff0c;数据无处不在&#xff0c;影响着我们的每一个决策。然而&#xff0c;如何从海量的数据中提取有价值的信息&#xff0c;并转化为深刻…

List类

什么是 List 在集合框架中&#xff0c; List 是一个接口&#xff0c;继承自 Collection 。 Collection 也是一个接口 &#xff0c;该接口中规范了后序容器中常用的一些方法&#xff0c;具体如下所示&#xff1a; List 中提供了好的方法&#xff0c;具体如下&#xff1a; List…

Conda 常用命令大全

Conda 常用命令大全 配置源conda配置清华源pip配置清华源pip配置阿里源 环境管理创建一个新的虚拟环境列出虚拟环境激活虚拟环境退出虚拟环境删除虚拟环境复制某个虚拟环境 conda包管理列出全部包安装包卸载包 pip包管理列出全部包安装包卸载包 其他命令查询 conda 版本查看环境…

C语言详解:数组指针

数组指针是指针 int* p[10] 这是指针数组的写法 &#xff0c;因为【】的优先级比*高&#xff0c; 所以为了解决优先级问题&#xff0c;加&#xff08;&#xff09; int(* p)[10]&arr;//数组的地址要存起来 说明p是指针&#xff08;首先与*结合&#xff09;&#xff0c…

哈希表法快速求解最长连续序列 | 力扣128题详细解析

❤️❤️❤️ 欢迎来到我的博客。希望您能在这里找到既有价值又有趣的内容&#xff0c;和我一起探索、学习和成长。欢迎评论区畅所欲言、享受知识的乐趣&#xff01; 推荐&#xff1a;数据分析螺丝钉的首页 格物致知 终身学习 期待您的关注 导航&#xff1a; LeetCode解锁100…

关于电源3(整流滤波电路)

整流滤波电路 框图 一共有四种整流电路 以下是自己参考别人的文章https://blog.csdn.net/zhuguanlin121/article/details/130653498?ops_request_misc%257B%2522request%255Fid%2522%253A%2522171582622316800215096518%2522%252C%2522scm%2522%253A%252220140713.130102334…

jenkins配置不同版本nodeJS,保姆级叫你配置

问题描述&#xff1a;公司jenkins被改了nodejs版本适配其他项目导致以前的项目构建失败&#xff0c;原因就是nodejs版本太高或太低导致&#xff0c;这里教大家不去更改服务器默认版本&#xff0c;当需要特殊版本直接在jenkins里配置即可。 过程 1、安装nodeJS插件 1.1点击管…

怎么把照片变小做头像?多种方法教你图片改尺寸

现在在社交媒体平台或者是社交软件上&#xff0c;我们经常会去更改头像来展示自己&#xff0c;但是有时候我们拍摄的照片太大无法直接用作头像&#xff0c;这时候就需要去修改图片尺寸&#xff0c;将图片改大小到合适的数值才能使用&#xff0c;那么如何快速的将图片改大小呢&a…

Ansys Mechanical|中远程点的Behavior该如何设置?

Remote point是ANSYS mechanical中的一种常见节点自由度耦合建模形式&#xff0c;在转动装配体中的连接转动副、或者在施加远端约束及远端载荷的时候&#xff0c;我们经常用到远端单元来耦合一个面或者一条线。例如销轴似的滚动摩擦连接&#xff0c;如果我们希望将两个物体通过…

小白git

克隆 &#xff1a;git clone 链接地址 如果没有.git文件的话&#xff1a;git init 切换分支&#xff1a;cd 目录 拉代码&#xff1a;git pull 查看你自己改了那些文件&#xff1a;git status 添加道本地暂存区&#xff1a;git add * 提交到远端&#xff1a;git commit …

吴恩达深度学习笔记:优化算法 (Optimization algorithms)2.9-2.10

目录 第二门课: 改善深层神经网络&#xff1a;超参数调试、正 则 化 以 及 优 化 (Improving Deep Neural Networks:Hyperparameter tuning, Regularization and Optimization)第二周&#xff1a;优化算法 (Optimization algorithms)2.9 学习率衰减(Learning rate decay) 第二门…

HP5V80、HP5V105、HP3V28电比例驱动柱塞泵放大器

HP5V80、HP5V105、HP3V28、HP3V45、HP3V60、HP3V80、HP3V125、HP3V140带电比例控制泵放大器&#xff0c;变排量泵的排量可通过由BEUEC比例放大器输出到比例电磁阀电流变化而进行调整&#xff0c;控制电流范围为300mA至800mA(24VDC)或600mA至1600mA(12VDC)。主要适合应用于工程机…

【联通官网及APP注册/登录安全分析报告】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞 …

「AI模型瘦身术」——知识蒸馏技术综述

使用KD原因 遇到问题&#xff1a;从产业发展的角度来看工业化将逐渐过渡到智能化&#xff0c;边缘计算逐渐兴起预示着 AI 将逐渐与小型化智能化的设备深度融合&#xff0c;这也要求模型更加的便捷、高效、轻量以适应这些设备的部署。 解决方案&#xff1a;知识蒸馏技术 知识…

Logic Pro X for Mac v11.0.0激活版:专业音频制作软件

对于音乐创作者来说&#xff0c;一个稳定、高效的工作流程至关重要。Logic Pro X for Mac提供了一系列工作流程优化功能&#xff0c;让你能够更快捷、高效地完成音乐创作。从添加音轨、录制音频&#xff0c;到混音和编曲&#xff0c;每一个步骤都如丝般顺滑。同时&#xff0c;L…

Maven 依赖排查

先从项目去看显而易见&#xff0c;假如我们有一个项目&#xff0c;父工程中包含一些子工程&#xff0c;如下&#xff1a; 我们想看一下samples-account中的依赖关系&#xff0c;那么我们可以打开 samples-account的pom文件&#xff0c;查看其maven依赖关系图。 我们可以看到此项…

ARM 交叉编译搭建SSH

一、源码下载 zlib&#xff1a;zlib-1.3.1.tar.xz openssl&#xff1a;openssl-0.9.8d.tar.gz openssh&#xff1a;openssh-4.6p1.tar.gz 二、交叉编译 1、zlib 编译参考这里 2、openssl tar -xf openssl-0.9.8d.tar.gz ./Configure --prefix/opt/ssh/openssl os/compile…