416. Partition Equal Subset Sum
1 | class Solution { |
上面是官方的题解. 因为有一行很让我费解, 于是我专门记录下来.
1 | class Solution { |
这个是常规的想法. 递归函数的功能就是告诉我从pos(inclusive)到结尾的所有数字能否凑到sum. 思路也很简单, 我们从第0个元素开始, 如果我知道从1到最后包含的数字能否凑到sum或者sum - nums[0]那么我就知道答案了. 因为如果能凑到sum, 那么nums[0]和剩下的那些数字就可以凑成sum. 如果能凑到sum - nums[0], 那么nums[0]和这些数字就能凑到sum, 其他的数字也可以凑到sum.
需要注意的是, 我们要使用Boolean数组而不是boolean数组. 因为boolean数组默认初始化为false, 如果我们知道在某个pos时某个sum凑不到, 我们记录在boolean数组中为false, 那我们再次遇到pos时凑某个sum的情况的时候, 我们如何知道这个情况是被走过的, 还是初始化的false. 于是我们使用Boolean数组, 此时初始化为null. 那么当我们遇到某个pos要凑sum时, 我们发现这个位置的Boolean不是null, 那我们就不需要再走了, 直接return这里的答案即可.
时间复杂度: O(m * n)也就是需要知道在每个pos凑0到halfSum的值.
空间复杂度: O(m * n)2D array.
1 | class Solution { |
dp的解法. 我们规定dp[i][j]表示的是是否能用前i个数字凑成j. 那么dp[i][j] = dp[i - 1][j] | dp[i - 1][j - nums[i]]. 也就是如果前n - 1个数字能够凑成j或者前n - 1个数字能够凑成j - nums[i], 那么前n个数字就能凑成j. 因此每次某一行某个位置行不行都是看上一行这个位置, 或者上一行j - nums[i]这个位置. 这个和uniquepath的看左边和上边的有点儿相似. 但还是不一样.
这样的话, 如果用1D array, 我们就得从右往左遍历. 因为dp记录的是上一行的情况. 我们要的也是上一行的情况. 新的dp[j]是否为true就要看此时的dp[j]是否为true(上一行这个位置是否为true)或者dp[j - nums[i]]是否为true(上一行能否凑成j - nums[i]).
这样就明白为什么从右往左了.
如果从左往右, 一个是和我们想的逻辑不符, 实际情况就是某个数字会被使用多次. 比如:
[1, 2, 10]
我们开始看只包含前1个元素. 0是默认可以凑成. 1可以凑成, 2我们会看2 - nums[0]等于什么, 发现是true, 因此也可以, 但其实只有前一个元素, 我们是凑不成2的. 我们这里把1使用了两次. 看2 - nums[0]就意味着我们在不使用nums[0]的情况下看前面的元素能不能构成2 - nums[0], 但是我们之前记录的却是使用nums[0]的情况. 因此不能从左往右遍历.
dp[n][m] [0, n] 能否凑到m.
dp[n][m] = dp[n - 1]m - nums[n] || dp[n - 1][m](不含nums[n]那就是前n - 1个数字能否凑到m)
dp[n][m - nums[i]]是看能[0, n]能否凑到m - nums[i], 如果能凑到也不代表一定能凑到m, 因为凑m - nums[i]可能就用到了nums[i], 这样会造成nums[i]的重复利用.
因此dp[n][m]看的就是头顶的值即dp[n - 1][m]和头顶左边的某个值即dp[n - 1][m - nums[i]]也就是为什么我们要倒着遍历而且j到nums[i]就停是因为如果j - nums[i]小于0, 代表凑不到. 于是j之前的值都不用再遍历了dp[n][0]应该初始化为什么呢? true. 比如nums[n]是6, dp[n][6] = dp[n][6 - nums[n]] || dp[n - 1][6], nums[n]等于6说明自己本身就能够凑到, 因此我们应该让dp[n][0]为true才能满足这个判断.