动态规划和回溯算法到底谁是谁爹? :: labuladong的算法小抄 #1066
Replies: 32 comments 4 replies
-
if(sum + target < 0 || (sum + target) % 2) return 0 判断条件需要这样 |
Beta Was this translation helpful? Give feedback.
-
使用Python解题, 文中代码逻辑 sum < target 应该改为 sum < abs(target) 不然报错 例: 此时target < sum 不触发提前返回特殊值。但在DP数组定义时 由于(sum + target) / 2 是负数,所以DP数组其实为空。后续for循环触发异常。 |
Beta Was this translation helpful? Give feedback.
-
sum(A) - sum(B) = target 这个式子怎么理解。为什么不是sum(A)+ sum(B) = target |
Beta Was this translation helpful? Give feedback.
-
(sum(nums) + target) % 2 为负数时,会导致角标越界,抛出异常 |
Beta Was this translation helpful? Give feedback.
-
dp解法答案好像不对 |
Beta Was this translation helpful? Give feedback.
-
感谢各位指出的问题,力扣的题目改了,我之前没有考虑到 |
Beta Was this translation helpful? Give feedback.
-
Base case那段解释有点疑惑:
假设 nums[]=[0, 0, 0] , sum=0, 那么有 dp[0][0]=1, dp[1][0]=2 , dp[2][0]=4,跟题解的 dp[..][0]=1有悖。 因此在dp转移里需要从 j = 0 开始遍历,因为在nums[i] = 0的情况下,dp[..][0]=1的说法并不正确
|
Beta Was this translation helpful? Give feedback.
-
@lumaster 你说的有道理,是我考虑的有纰漏,base case 应该仅仅是 |
Beta Was this translation helpful? Give feedback.
-
不太认同文中说的为了美观,而选择不加参数的做法,那样写反而增加了理解成本不是吗? function findTargetSumWays(nums: number[], target: number): number {
let result = 0
const helper = (nums: number[], i: number, sum: number) => {
if(i === nums.length) {
if(sum === target) {
result++
}
return
}
sum += nums[i]
helper(nums, i + 1, sum)
sum -= nums[i]
sum -= nums[i]
helper(nums, i + 1, sum)
sum += nums[i]
}
helper(nums, 0, 0)
return result
}; |
Beta Was this translation helpful? Give feedback.
-
一个添加 |
Beta Was this translation helpful? Give feedback.
-
@renlindong 思路不同,正常人喜欢累加,目标值知道就可以减呀 |
Beta Was this translation helpful? Give feedback.
-
(sum + target)%2 == 1:sum和target一定要么都是奇数要么都是偶数,因为我们的目标是将sum进行符号变换进而得到target,而对sum中每个数的符号进行改变,变化量一定是偶数 |
Beta Was this translation helpful? Give feedback.
-
我理解累加和累减的区别: |
Beta Was this translation helpful? Give feedback.
-
东哥,这篇文章的动态规划用的解法里,base case和之前一篇文章的感觉有点出入,让我迷茫了。这篇的base case是dp[0][..] = 0, dp[..][0] = 0, 唯独dp[0][0]=1,但是之前的完全背包问题是dp[0][..] = 0, dp[..][0] = 1。现在有点迷糊 |
Beta Was this translation helpful? Give feedback.
-
贴一个直接DP法,由于j可能为负所以没法用数组保存,因此存在重复子问题计算,但C++依然能通过 class Solution {
public:
// 返回从1到i个数中,目标和为j的组合数目
int dp(vector<int>& nums, int i, int j) {
if (i == -1) {
if (j == 0) return 1;
return 0;
}
// 对应两种选择,nums[i]为正号或nums[i]为负号
return dp(nums, i - 1, j - nums[i]) + dp(nums, i - 1, j + nums[i]);
}
int findTargetSumWays(vector<int>& nums, int target) {
int n = nums.size();
return dp(nums, n - 1, target);
}
}; |
Beta Was this translation helpful? Give feedback.
-
@lhcezx 👍,其实这种情况也是可以用备忘录的,python 可以把元组 |
Beta Was this translation helpful? Give feedback.
-
@labuladong 感谢东哥回复,补充一下带备忘录版 class Solution {
unordered_map<string, int> memo;
public:
// 返回从1到i个数中,目标和为j的组合数目
int dp(vector<int>& nums, int i, int j) {
string key = to_string(i) + "," + to_string(j);
if (memo.count(key)) return memo[key];
if (i == -1) {
if (j == 0) return 1;
return 0;
}
// 对应两种选择,nums[i]为正号或nums[i]为负号
return memo[key] = dp(nums, i - 1, j - nums[i]) + dp(nums, i - 1, j + nums[i]);
}
int findTargetSumWays(vector<int>& nums, int target) {
int n = nums.size();
return dp(nums, n - 1, target);
}
}; 有一个小问题,我这里如果在key中的i和j之间不加逗号会得不到正确的答案,没想明白为什么一定要加一个逗号?能举个例子吗? |
Beta Was this translation helpful? Give feedback.
-
@lhcezx 中间加一些特殊分隔符是为了表明这是一个数对儿,即 |
Beta Was this translation helpful? Give feedback.
-
补一个记忆化搜索,不拼接字符串,仍然使用二维int数组,速度比拼接字符串查HashMap要快点。 class Solution {
public int findTargetSumWays(int[] nums, int target) {
for (int i = 0; i < nums.length; i++) {
maxVal += nums[i];
}
minVal = -maxVal;
memo = new int[nums.length + 1][maxVal - minVal + 1];
for (int[] ints : memo) {
Arrays.fill(ints, Integer.MIN_VALUE);
}
int ans = process(nums, 0, 0, target);
return ans;
}
int[][] memo;
int minVal = 0;
int maxVal = 0;
public int process(int[] nums, int index, int sumOfPath, int target) {
if (index == nums.length) {
return sumOfPath == target ? 1 : 0;
}
if (memo[index][sumOfPath + maxVal] != Integer.MIN_VALUE) {
return memo[index][sumOfPath + maxVal];
}
memo[index][sumOfPath + maxVal] = process(nums, index + 1, sumOfPath + nums[index], target)
+ process(nums, index + 1, sumOfPath - nums[index], target);
return memo[index][sumOfPath + maxVal];
}
} |
Beta Was this translation helpful? Give feedback.
-
提供一下直接计算组合数量的思路 Solution.1 从上一行的可能的来源,计算当前数值的组合树
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int numSize = nums.length;
int sum = Arrays.stream(nums).sum();
if(target > sum || target < -sum) {
return 0;
}
// 1-number of numbers used
// 2-add or minus
// 3-result value
int[][] numCombinations = new int[numSize + 1][sum + 1];
// init
// only result = 0 and 0 number to use, has 1 combination
numCombinations[0][0] = 1;
for(int i = 1; i <= numSize; i++) {
int num = nums[i-1];
for(int j = 0; j <= sum; j++) {
// plus
numCombinations[i][j] = numCombinations[i-1][Math.abs(j - num)];
// minus
if(j + num <= sum) {
numCombinations[i][j] += numCombinations[i-1][j + num];
}
}
}
// Arrays.stream(numCombinations).map(Arrays::toString).forEach(System.out::println);
return numCombinations[numSize][Math.abs(target)];
}
} Solution.2 从上一行的组合数量,分发给下一行的组合数
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int numSize = nums.length;
int sum = Arrays.stream(nums).sum();
if(target > sum || target < -sum) {
return 0;
}
// 1-number of numbers used
// 2-plus or minus
// 3-result value
int[][] numCombinations = new int[numSize + 1][sum*2 + 1];
// init
// only result = 0 and 0 number to use, has 1 combination
numCombinations[0][sum] = 1;
for(int i = 1; i <= numSize; i++) {
int num = nums[i-1];
for(int j = 0; j < sum*2 + 1; j++) {
// add count from last row, only when last row can reach
if(numCombinations[i-1][j] == 0) {
continue;
}
// plus
numCombinations[i][j + num] += numCombinations[i-1][j];
// minus
numCombinations[i][j - num] += numCombinations[i-1][j];
}
}
// Arrays.stream(numCombinations).map(Arrays::toString).forEach(System.out::println);
return numCombinations[numSize][sum + target];
}
} Comparison
|
Beta Was this translation helpful? Give feedback.
-
麻了,没想到可以这样转化 |
Beta Was this translation helpful? Give feedback.
-
对照二维 dp,只要把 dp 数组的第一个维度全都去掉就行了,唯一的区别就是这里的 j 要从后往前遍历,原因如下: 因为二维压缩到一维的根本原理是,dp[j] 和 dp[j-nums[i-1]] 还没被新结果覆盖的时候,相当于二维 dp 中的 dp[i-1][j] 和 dp[i-1][j-nums[i-1]]。 那么,我们就要做到:在计算新的 dp[j] 的时候,dp[j] 和 dp[j-nums[i-1]] 还是上一轮外层 for 循环的结果。 如果你从前往后遍历一维 dp 数组,dp[j] 显然是没问题的,但是 dp[j-nums[i-1]] 已经不是上一轮外层 for 循环的结果了,这里就会使用错误的状态,当然得不到正确的答案。解释的妙,读了之后顿时有醐醍灌顶,茅塞顿开之感!!! |
Beta Was this translation helpful? Give feedback.
-
HashMap<String, Integer> memo = new HashMap<>(); |
Beta Was this translation helpful? Give feedback.
-
本题和416. Partition Equal Subset Sum的区别是本题的num[i]可以=0,所以base case不能仅在remain==0的时候,必须是remain == 0 && index == -1 |
Beta Was this translation helpful? Give feedback.
-
回溯加备忘录后,子问题数也是n*m,n为数组大小,m为目标值,那dp的子问题数也是这么多,为什么dp会快这么多,是因为递归调用的开销吗? |
Beta Was this translation helpful? Give feedback.
-
有没有人发现最后的动态规划的思路,对于[0,0,0,0,0,0,1]这种存在0的是不适用的哇。应该先把0相关的踢掉 |
Beta Was this translation helpful? Give feedback.
-
这里不明白二维的为什么j不能从1开始,我看子集背包问题里,二维的j是从1开始的,区别是那边先初始化了一下 |
Beta Was this translation helpful? Give feedback.
-
文章链接点这里:动态规划和回溯算法到底谁是谁爹?
评论礼仪 见这里,违者直接拉黑。
Beta Was this translation helpful? Give feedback.
All reactions