【leetcode】 target sum 与 Subset sum

Leetcode中的 target sum 问题其实可以转化为 Subset sum。关于Subset sum,可以参考我的前一篇博客 Ksum 与 Uncertain sum (子集和问题 Subset sum )

先贴一下 Leetcode 中关于 target sum (Leetcode 494)的问题描述:

You are given a list of non-negative integers, a1, a2, …, an, and a target, S. Now you have 2 symbols + and -. For each integer, you should choose one from + and - as its new symbol.
Find out how many ways to assign symbols to make sum of integers equal to target S.
Example 1:
Input: nums is [1, 1, 1, 1, 1], S is 3.
Output: 5
Explanation:
-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3
There are 5 ways to assign symbols to make the sum of nums be target 3.
Note:
1.The length of the given array is positive and will not exceed 20.
2.The sum of elements in the given array will not exceed 1000.
3.Your output answer is guaranteed to be fitted in a 32-bit integer.

这个问题是说给定一个数组 nums = [ a 1 , a 2 , ⋯   , a n a_1,a_2,\cdots,a_n a1,a2,,an] 和一个目标数 S,在 nums 中的每个数前面可以加上正号或符号,使得加上正负号之后的所有数之和等于目标数 S,试计算通过正负号组合再求和后得到 S 的组合数目有多少种。

这个题比较常规的解法是搜索法和动态规划法,其实还有种做法是将这个问题转化为 “子集和问题”。下面先介绍后一种方法,再介绍前面两种常规的方法。

转化为 “子集和问题” 来求解

怎么将问题转化为子集和问题呢?可以这样来思考:
根据题目意思, nums 数组中的所有元素前面需要添加正号或者负号,那么我们把添加了正号的所有元素之和记为 P P P,把所有添加了负号的元素之和记为 N N N。容易知道,有下面两个式子成立:
P + N = s u m ( nums ) P − N = S P + N = sum(\text{nums}) \\ \hspace{-1.5cm} {P - N = S} P+N=sum(nums)PN=S
于是就可以得到: P = ( s u m ( nums ) + S ) / 2 P = (sum(\text{nums})+S)/2 P=(sum(nums)+S)/2
也就是说只需要在数组 nums 中找到部分元素使得它们的和等于 ( s u m ( nums ) + S ) / 2 (sum(\text{nums})+S)/2 (sum(nums)+S)/2 就行了,这个就是 Leetcode 中的 Combination sum I II III IV(Leetcode 39, 40,216, 377),这跟之前在博客 Ksum 与 Uncertain sum (子集和问题 Subset sum ) 中介绍的子集和问题有点不同的是,之前的博客中的问题只需要判断是否存在部分元素之和等于目标数值(不存在就是0,存在就是1),而这里是需要计算有多少种组合能得到 P = ( s u m ( nums ) + S ) / 2 P = (sum(\text{nums})+S)/2 P=(sum(nums)+S)/2(不存在就是0,存在的话还需判断多少种情况)。不过万变不离其宗,其思路并没有变化,在之前我的博客 Ksum 与 Uncertain sum (子集和问题 Subset sum ) 里所写的原始问题的代码中,只需将 True 换成 1,False 换成 0,“或运算” 换成 “加法”,就可以得到这个问题的解了。

Python 代码 如下:

    def func(nums, M):
        dp = [[0 for _ in range(M+1)] for _ in range(len(nums)+1)]
        for i in range(len(nums)+1):
            dp[i][0] = 1
        for j in range(1,M+1):
            dp[0][j] = 0
        for i in range(1,len(nums)+1):
            for j in range(M+1):
                dp[i][j] = dp[i-1][j]
                if j>= nums[i-1]: # 注:由于上面做了padding,所以此处的第i个元素即为nums[i-1]
                    dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i-1]]
        return dp[len(nums)][M]
    
    
    def TargetSum(nums, S):
        if sum(nums)<S:
            return 0
        if int((S+sum(nums))%2)==1:
            return 0
        target = int((S+sum(nums))/2)
        return func(nums, target)

上面的做法中用了二维数组,当然用一维数组也可以做。令 dp[ i i i] 表示能够组合成和为 i i i 的组合数目,那么需要求解的就是 dp[ ( s u m ( nums ) + S ) / 2 (sum(\text{nums})+S)/2 (sum(nums)+S)/2] 的值了。之前在博客 Ksum 与 Uncertain sum (子集和问题 Subset sum ) 中有说,规定空集的和为0,也就是说 dp[0]=1。将其作为边界条件,那么还需要知道在一般情形中的迭代公式。可以这样来思考:

step1:如果要得到一个和为 T T T 的组合数 dp[ T T T],那么它肯定等于和为 T − term T-\text{term} Tterm 的累加,这里的 term \text{term} term 是 nums 中的元素,而且 term \text{term} term 不能重复使用。
(注: T − term T-\text{term} Tterm ≥ \ge 0)

step2:如何实现 “和为 T − term T-\text{term} Tterm 的累加” 而且 “ term \text{term} term 不能重复使用” 呢?答案是 “自叠加” 和 “循环遍历”。“自叠加” 是指的形如 “ a = a + b a=a+b a=a+b” 这样的类型,即在原始 a a a 的基础上加个 b b b 成为新的 a a a。“循环遍历” 是指的对 nums 中的每个 term \text{term} term 进行遍历,当取定一个 term \text{term} term 时,计算 dp[ T − term T-\text{term} Tterm], dp[ T − 1 − term T-1-\text{term} T1term],dp[ T − 2 − term T-2-\text{term} T2term], ⋯ \cdots (只要满足 ≥ \ge 0,可以一直进行下去)。循环遍历 term 时,由于每次遍历时 term 之间相互独立,所以每个 term 只会被使用一次。

Python 代码如下:

    def TargetSum(nums, S):
        if sum(nums)<S:
            return 0
        if int((S+sum(nums))%2)==1:
            return 0
        target = int((S+sum(nums))/2)
        dp = [0]*(target+1)
        dp[0] = 1
        for term in nums:
            i = target
            while(i>=term):
                dp[i] = dp[i] + dp[i-term]
                i = i-1
        return dp[target]
通过搜索法来求解

这个题目也可以通过搜索法来做。如果对图算法比较熟悉的话,其实搜索法应该是最容易想到解这个题的方法。

思路是将数组中的每个元素看作一个节点,从第一个元素搜索到最后一个元素为止。每经过一个元素,将其带正负号的两种情况分别都考虑,依次累加到最后一个元素。比如说对如数组 [1, 2, 1],对元素加正负号之后,目标值是 -2,那么搜索可以形象地用下面地图来描述:

可以看出有两种方式能够得到 -2,一种是 -1 + (-2) + 1 = -2,另一种是 1 + (-2) + (-1) = -2。用搜索的方法需要搜索所有情况,虽然原理很简单,但是时间复杂度较高,对较短的数组比较适宜,但是对较长的数组容易超时。

Pathon 代码如下:

    def TargetSum(nums, S):
        def dfs(depth, sums):
            if depth == len(nums):
                if sums == S:
                    return 1
                else:
                    return 0
            return dfs(depth+1, sums+nums[depth]) + dfs(depth+1, sums-nums[depth])
        return dfs(0, 0)
通过动态规划法来求解

当然,这个题也可以用常规的动态规划方法来求解。之前在将问题转化为 “子集和问题” 后虽然用了动态规划的方法,不过此处说的是针对原始问题的动态规划法,设置的 dp 和构造的子结构不一样。

对于这个题的分析,可以如下:
首先,对于给定的数组 nums,不论将其怎么组合都有个上下界,上界是 s u m ( nums ) sum(\text{nums}) sum(nums),即 nums 中所有元素均加上正号;下界是 − s u m ( nums ) -sum(\text{nums}) sum(nums),即 nums 中所有元素均加上负号。那么将 nums 中的元素添加正负号进行求和后,其和值必定落在 [ − s u m ( nums ) , s u m ( nums ) ] [-sum(\text{nums}), sum(\text{nums})] [sum(nums),sum(nums)] 之内,也就是说和值的取值长度有 2 ∗ s u m ( nums ) 2*sum(\text{nums}) 2sum(nums)+1。

由于指标不能为负,所以需要将取值区间由 [ − s u m ( nums ) , s u m ( nums ) ] [-sum(\text{nums}), sum(\text{nums})] [sum(nums),sum(nums)] 平移得到 [ 0 , 2 ∗ s u m ( nums ) [0,2*sum(\text{nums}) [0,2sum(nums)+1 ] ] ],那么之前的 0 就被平移成了 s u m ( nums ) sum(\text{nums}) sum(nums),记它为 center 值。

令 dp[ i i i][ j j j] 表示 nums 中前 i i i 个元素组合得到 j j j 的组合方法数,因为在平移前有 dp[0][0]=1,所以平移后应该是 dp[0][center] = 1,这是动态规划的 base 边界条件。下面来思考结构式子:

由于每一个 dp[ i i i][ j j j] 都可以写成 dp[ i − 1 i-1 i1][ j − a i j-a_i jai] 和 dp[ i − 1 i-1 i1][ j + a i j+a_i j+ai] 之和,其中 a i a_i ai 表示 nums 中第 i i i 个元素的值,写成公式即:
\hspace{4cm} dp[ i i i][ j j j] = dp[ i − 1 i-1 i1][ j − a i j-a_i jai] + dp[ i − 1 i-1 i1][ j + a i j+a_i j+ai]

这个思维是指,dp[ i i i][ j j j] 会接到来自 dp[ i − 1 i-1 i1][ j − a i j-a_i jai] 和 dp[ i − 1 i-1 i1][ j − a i j-a_i jai] 的值。那么,换个思维角度讲, dp[ i i i][ j j j] 也会将自己的值传递给 dp[ i + 1 i+1 i+1][ j − a i + 1 j-a_{i+1} jai+1] 和 dp[ i + 1 i+1 i+1][ j + a i + 1 j+a_{i+1} j+ai+1] ,于是就有公式如下:
\hspace{5cm} dp[ i + 1 i+1 i+1][ j − a i + 1 j-a_{i+1} jai+1] += dp[ i i i][ j j j]
\hspace{5cm} dp[ i + 1 i+1 i+1][ j + a i + 1 j+a_{i+1} j+ai+1] += dp[ i i i][ j j j]

Python 代码如下:

    def TargetSum(nums, S):
        # dp[i][j] 表示nums中前i个元素组合得到j的方法数
        sums = sum(nums)
        # 易知,nums加符号后的组合,其和一定在 [-sums, sums]之间
        if sums < S:  #  当总和要小于给定数S,那么不可能有组合使得和为S,返回0种方法
            return 0
        dp = [[0 for _ in range(2*sums+1)] for _ in range(len(nums)+1)]
        # 由于指标不能为负数,所以 [-sums, sums] 做了平移,得到 [0, 2*sums]
        # 由于原始区间范围是 [-sums, sums] ,故第sums号元素就是原始的 0,平移后记为 center
        center = sums
        dp[0][center] = 1  # 前0个元素的组合,其和为0,规定方法数为1
        for i in range(len(nums)):
            for j in range(nums[i], 2*sums+1-nums[i]):
                if dp[i][j]:
                    dp[i+1][j-nums[i]] += dp[i][j]
                    dp[i+1][j+nums[i]] += dp[i][j]
        return dp[len(nums)][S+sums]

虽然这个 target sum 问题是个比较简单的问题,但是我们做题不仅仅在于做出题目,而且还需要多方位思考是否有多种方式能够解决问题,这样才能不断锻炼思考问题的思维,才能在以后遇到一些相关问题能够迅速给出完美的解答。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值