视频讲解:
带你学透完全背包问题! 和 01背包有什么差别?遍历顺序上有什么讲究?_哔哩哔哩_bilibili
动态规划之完全背包,装满背包有多少种方法?组合与排列有讲究!| LeetCode:518.零钱兑换II_哔哩哔哩_bilibili
动态规划之完全背包,装满背包有几种方法?求排列数?| LeetCode:377.组合总和IV_哔哩哔哩_bilibili
52. 完全背包问题
思路:与所预料的一样,就是01背包问题的重量遍历顺序从头开始遍历即可求解,另外优化的地方在于重量可以从当前物品对应的重量开始进行遍历,因此按物品外重量内的遍历顺序来遍历的话,只有j>=weights[i]才会进行递推,所以不妨直接在for循环赋值上就体现出来。
代码随想录 ==>
01背包中二维dp数组的两个for遍历的先后循序是可以颠倒了,一维dp数组的两个for循环先后循序一定是先遍历物品,再遍历背包容量。
在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!
因为dp[j] 是根据下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。
import java.util.*;
// 时间复杂度O(n^2)
// 空间复杂度O(n)
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt();
int V = scanner.nextInt();
int[] weights = new int[N];
int[] values = new int[N];
for(int i=0; i<N; i++){
weights[i] = scanner.nextInt();
values[i] = scanner.nextInt();
}
// dp数组的值表示的是放入的最大的价值
int[][] dp = new int[N][V+1];
for(int j=1; j<=V; j++){
if(j >= weights[0])
dp[0][j] = dp[0][j-weights[0]]+values[0];
}
for(int i=1; i<N; i++){
for(int j=1; j<=V; j++){
if(j >= weights[i]){
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-weights[i]]+values[i]);
}
else
dp[i][j] = dp[i-1][j];
}
}
System.out.println(dp[N-1][V]);
return;
}
}
518. 零钱兑换II
思路:(题目类型:放满完全背包问题),但是却是第一次遇到的存在不可放的情况的题目。由于统计的是可行的找零策略,是求组合的问题。而针对不可行数值,无需进行额外的操作,仍然保留dp[j]+=dp[j-nums[i]]的递推公式,不可找零的数值将存在传递性,可自行将当前位置的不可行传递到下一个位置。这个策略的存在为之后的 零钱兑换 提供关键的思路。
// 时间复杂度O(n^2)
// 空间复杂度O(n)
class Solution {
public int change(int amount, int[] coins) {
// coins的所有零钱种类作为物品数量
int N = coins.length;
// dp数组表示零钱i可有几种可行的找零策略
int[] dp = new int[amount+1];
// 固定搭配
dp[0] = 1;
// 可重复放值且组合形式的遍历
for(int i=0; i<N; i++){
for(int j=coins[i]; j<=amount; j++){
dp[j] += dp[j-coins[i]];
}
}
return dp[amount];
}
}
377. 组合总和 Ⅳ
思路:关键理解物品与重量循环顺序颠倒后,即将求组合变成了求排列。(题目类型:放满完全背包问题)
// 时间复杂度O(n^2)
// 空间复杂度O(n)
class Solution {
public int combinationSum4(int[] nums, int target) {
// dp数组的含义是容量为j的背包可以由由nums数组的元素组成的方式有几种
int[] dp = new int[target+1];
// 递推公式, dp[j] += dp[j-nums[i]]
// 初始化,仅仅是为了推导
dp[0] = 1;
// !! 求组合,外层for循环遍历物品;求排列,则外层for循环遍历重量,从而可以使得访问物品
// 外层for循环是物品的时候,每次更新的是整个dp,意义在于判断各个重量可以加入几个当前的nums[i],更新的牵扯也是关联到j-nums[i]的位置,
// 外层for循环是重量的时候,每次更新的是dp数组中的某个位置,是用所有的物品去去试探当前重量可以放入几种物品,5-2和5-3两个位置的内容会加到一个位置里面去,而上一种里面,5-2加上了dp[3],但此时所考虑的物品仅仅是i之前的,i之后的不考虑;所以3,5都是由1和2拼出来的;只有到了3之后,才会将1,2,3三个都考虑;
// 但是外层for循环重量的时候,是在每个重量,都去考虑1,2,3,所以5是从4,1 3,2 2,3 1,4 这样的全面进行考虑的
for(int j=1; j<=target; j++){
for(int i=0; i<nums.length; i++)
if(j >= nums[i])
dp[j] += dp[j-nums[i]];
}
return dp[target];
}
}