什么时候先遍历背包?什么时候先遍历物品
先给出结论:
当问题为组合问题时:先遍历物品,再遍历背包
当问题为排列问题时:先遍历背包,再遍历物品
下面结合leetcode上两道题进行讲解
组合问题
518. 零钱兑换 II - 力扣(LeetCode) (leetcode-cn.com)
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
分析:
每一个面额的硬币都有无限多个,属于完全背包问题,以下使用一维滚动数组形式
使用动态规划五部曲进行思路的梳理
1.确定dp数组及其下标含义
dp[j]代表容量为j的背包有几种方法可以装满,结合本题就是凑成总金额为j的组合数有几个
2.确定递推公式
ap[j]+=dp[j - coins[i]]求取可以得到的组合数
3.初始化dp数组
容量为0的时候需要注意,那就是凑成总金额为0有一种可能,就是一个都不选,而不是0,这点需要注意,剩下的未处理。所以都初始化为0即可
4.确定遍历顺序
一维完全背包,先遍历物品还是先遍历背包都可以(因为一维数组实际上是二维数组的状态压缩,每次使用到上边和左边的数据,不管是先遍历物品还是先遍历背包都可以保证该点的前一节点有数据),遍历背包是正序遍历,代表可以重复选择一个物品
但是这道题特殊就特殊在遍历顺序的选取上*
先遍历物品,再遍历背包
物品是正序的只有一遍遍历,比如要得到3,结果1,2选择完了,不可能再出现2,1,属于【组合问题】
先遍历背包,再遍历物品
物品会有多次遍历,比如要得到3,结果1,2选择完了还可能会出现2,1的情况,属于【排列问题】
实现代码(以Java为例):
int n = coins.length;
int m = amount;
int[] dp = new int[m + 1];
dp[0] = 1;
for(int i = 0; i < n; i++) {
for(int j = coins[i]; j <= m; j++) {
dp[j] += dp[j - coins[i]];//累加计算组合数
}
}
return dp[m];
排列问题
377. 组合总和 Ⅳ - 力扣(LeetCode) (leetcode-cn.com)
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
分析
动规分析的思路和上面一样,这里就不再赘述,唯一不同是不同的排列代表不同的结果,所以再遍历顺序时需要先遍历背包再遍历物品
物品会有多次遍历,比如要得到3,结果1,2选择完了还可能会出现2,1的情况,属于【排列问题】
关于为什么先遍历背包再遍历物品可以得出排列结果,大家可以想到先遍历物品的话物品只会有一遍的遍历,只能按顺序选,而先遍历背包再遍历物品的话物品就会有多次的遍历,每次都从头选取,会出现像1,2和2,1这种情况,所以先遍历背包再遍历物品可以解决排列问题
代码实现(以Java为例):
//完全背包排列问题(区别于零钱兑换II的组合问题)
int n = nums.length;
int m = target;
int[] dp = new int[m + 1];
dp[0] = 1;
for(int j = 0; j <= m; j++) {
for(int i = 0; i <n; i++) {
if(j >= nums[i]) dp[j] += dp[j - nums[i]];
}
}
return dp[m];