问题描述
问题分析
- 该题类似于排列问题,一个元素可以使用多次。而 LeetCode 518. Coin Change 2 类似于组合问题,同样一个元素可以使用多次,两题都是统计可能性的次数,因为该题还要考虑顺序,所以肯定比 518统计的次数多。
- 画出递归树,从递归函数入手,然后到记忆化搜索,然后到动态规划。注意,也可以先对数组进行排序来优化。
- 该题中记忆化搜索要比动态规划运行时间更快,原因在于,记忆化搜索依旧是自顶向下,而动态规划是自底向上,举一个极端的例子,便是
nums = {100}, target = 100
若是自顶向下,两次递归即可。而动态规划需要从dp[0]
一直计算到dp[100]
,中间包含很多多余计算。 - 记忆化搜素可以用数组,也可以用map,但map更加节省空间,并且,数组要注意初始化为何值才能保证与以后存入的值不冲突
- 关于 Follow up :
- 如果数组中也存在负数,那么便会有无数种结果,比如对于
nums = {1, -1, 2 , -2}
,只要能形成0,那么便无数种结果,所以,只能限制每个元素用的数量,或者限制序列的长度。
经验教训
- 关于排列与组合问题的区别,377与518,联系dfs时的排列组合。
- 记忆化搜索与动态规划
代码实现
public int combinationSum4(int[] nums, int target) {
if (nums == null) {
return 0;
}
int[] dp = new int[target + 1];
return count(nums, target);
}
public int count(int[] nums, int remain) {
if (remain == 0) {
return 1;
}
int res = 0;
for (int i = 0; i < nums.length; ++i) {
if (remain >= nums[i]) {
res += count(nums, remain - nums[i], dp);
}
}
return res;
}
public int combinationSum4(int[] nums, int target) {
if (nums == null) {
return 0;
}
int[] dp = new int[target + 1];
Arrays.fill(dp, -1);
return count(nums, target, dp);
}
public int count(int[] nums, int remain, int[] dp) {
if (remain == 0) {
return 1;
}
if (dp[remain] != -1) {
return dp[remain];
}
int res = 0;
for (int i = 0; i < nums.length; ++i) {
if (remain >= nums[i]) {
res += count(nums, remain - nums[i], dp);
}
}
dp[remain] = res;
return res;
}
public int combinationSum4(int[] nums, int target) {
if (nums == null) {
return 0;
}
int[] dp = new int[target + 1];
dp[0] = 1;
for (int i = 1; i < dp.length; ++i) {
int res = 0;
for (int j = 0; j < nums.length; ++j) {
if (nums[j] <= i) {
res += dp[i - nums[j]];
}
}
dp[i] = res;
}
return dp[target];
}
public int combinationSum4(int[] nums, int target) {
if (nums == null) {
return 0;
}
int[] dp = new int[target + 1];
Arrays.sort(nums);
dp[0] = 1;
for (int i = 1; i < dp.length; ++i) {
int res = 0;
for (int j = 0; j < nums.length; ++j) {
if (nums[j] > i) {
break;
}
res += dp[i - nums[j]];
}
dp[i] = res;
}
return dp[target];
}