【回溯 + 背包 + 动态规划】LeetCode - 494. 目标和

题目描述

题目链接
在这里插入图片描述
在这里插入图片描述

解法1:暴力枚举 - 回溯/DFS(溢出)

1. 回溯

经常说回溯算法和递归算法有点类似,都涉及递归,有的问题如果实在想不出状态转移方程,尝试用回溯算法暴力解决也是一个聪明的策略,总比写不出来解法强。

回溯算法其实是一个暴力枚举的算法,模板如下
在这里插入图片描述
关键就是搞清楚什么是「选择列表」,而对于这道题,「选择列表」就是正号 + 或者负号 -,然后利用回溯模板穷举出来所有可能的结果,数一数到底有几种组合能够凑出 target

class Solution {
private:
    int ans = 0;
public:
    void DFS(vector<int>& nums, int index, int rest)
    {
        if(index == nums.size())
        {
            if(rest == 0)//正好凑成 S
             ans++;
            return;
        }
        //二叉树
        //nums[index] 选择 - 号
        rest += nums[index];
        DFS(nums, index + 1, rest);//穷举 nums[index + 1]
        rest -= nums[index];//回溯,撤销选择

        //nums[index] 选择 + 号
        rest -= nums[index];
        DFS(nums, index + 1, rest);//穷举 nums[index + 1]
        rest += nums[index];//回溯,撤销选择
    }
    int findTargetSumWays(vector<int>& nums, int S) {
        if(nums.size() == 0) return 0;
        DFS(nums, 0, S);
        return ans;
    }
};

时间复杂度为 O(2^N),N 为 nums 的大小

这是因为这个回溯算法就是个二叉树的遍历问题,树的高度就是 nums 的长度,所以说时间复杂度就是这棵二叉树的节点数,即为 O(2^N),其实是非常低效的,而且我发现提交上去会 int 溢出

最后总结的教训是 如果不是让列举所有情况,或者没有效果很好的剪枝办法,还是不要轻易选择回溯。

2. DFS

可以把数组中每个数字前面都用负号和正号,然后进行组合的求和,并判断这个和是否会等于S,然后就标记,最后统计出等于S的组合个数就好了。

具体使用dfs,就是一个前序遍历二叉树的实现,递归地+或-每个元素,到所有元素都遍历完成的时候,最后那个判断target是否等于零。

class Solution {
public:
    int DFS(vector<int>& nums, uint index, int rest)
    {
        if(rest == 0 && index == nums.size()) return 1;
        if(index >= nums.size()) return 0;
        int ans = 0;
        ans += DFS(nums, index + 1, rest + nums[index]);
        ans += DFS(nums, index + 1, rest - nums[index]);
        return ans;
    }
    int findTargetSumWays(vector<int>& nums, int S) {
        if(nums.size() == 0) return 0;
        return DFS(nums, 0, S);
    }
};

时间复杂度为 O(2^N),N 为 nums 的大小。也是溢出。

解法2:动态规划(01背包问题)

首先,如果我们把 nums 划分成两个子集 A 和 B,分别代表分配 + 的数和分配 - 的数,这个问题就转化为一个子集划分问题,而子集划分问题又是一个典型的背包问题。动态规划总是这么玄学,让人摸不着头脑……

A、B 和 target 存在如下关系:
在这里插入图片描述
综上,可以推出 sum(A) = (target + sum(nums)) / 2,也就是把原问题转化成:nums 中存在几个子集 A,使得 A 中元素的和为 (target + sum(nums)) / 2?

这就变成了01背包问题:背包容量为 (target + sum(nums)) / 2,有 n 个物品,第 i 个物品的重量为 nums[i - 1](注意 1 <= i <= N),每个物品只有一个,问有几种不同的方法能够恰好装满这个背包

1. 确定「状态」和「选择」以及 dp数组以及下标的含义

状态就是「背包的容量」和「可选择的物品」,选择就是「装进背包」或者「不装进背包」。

dp[i][j] = x 表示,只在前 i 个物品中选择,当前背包的容量为 j,最多有 x 种方法可以恰好装满背包。

2. 确定递推公式

递推公式:dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]];

dp[i-1][j] 表示不选第 i 个物品容量正好为 j;dp[i-1][j-nums[i-1]] 表示选上第 i 个物品容量正好为 j

3. dp数组如何初始化

dp[0][0]=1(递归结束出口),dp[0][1:]=0(无效状态)

4. 确定遍历顺序

  • 如果求组合数就是外层for循环遍历物品,内层for遍历背包。如 518.零钱兑换II
  • 如果求排列数就是外层for遍历背包,内层for循环遍历物品。如377. 组合总和 Ⅳ

组合 vs 排列

5 = 2 + 2 + 1
5 = 2 + 1 + 2
这是一种组合,都是 2 2 1。但是是两种排列。组合不强调元素之间的顺序,排列强调元素之间的顺序。

本题是求组合数。

5. 举例推导dp数组

二维写法

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        if(nums.size() == 0) return 0;
        int n = nums.size(), sum = 0;
        for(int i : nums) sum += i;
        if(S > sum || (S + sum) % 2) return 0;//此时没有方案
        int bagSize = (S + sum) / 2;
        vector<vector<int> > dp(n + 1, vector<int>(bagSize + 1, 0));
        dp[0][0] = 1;
        for(int i = 1; i <= n; i++)//遍历物品
         for(int j = 0; j <= bagSize; j++)//遍历背包
         {
            if(j >= nums[i - 1])
             dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i - 1]];
            else
             dp[i][j] = dp[i - 1][j];
         }          
        return dp[n][bagSize];
    }
};

一维写法

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        if(nums.size() == 0) return 0;
        int n = nums.size(), sum = 0;
        for(int i : nums) sum += i;
        if(S > sum || (S + sum) % 2) return 0;//此时没有方案
        int bagSize = (S + sum) / 2;        
        vector<int> dp(bagSize + 1, 0);
        dp[0] = 1;
        for(int i = 0; i < n; i++)
         for(int j = bagSize; j >= nums[i]; j--)
          dp[j] += dp[j - nums[i]];
        return dp[bagSize];
    }
};

01背包的一维写法内部循环必须倒序,完全背包必须正序。背包问题一般的套路模板

如果 bagSize = (S + sum) / 2; 想不到怎么办

也可以转化为01背包问题

1. 确定「状态」和「选择」以及 dp数组以及下标的含义

状态就是「背包的容量」和「可选择的物品」,选择就是不再是nums[i]的选与不选,而是nums[i]是加还是减

dp[i][j] = x 表示,只在前 i 个物品中选择,当前背包的容量为 j,最多有 x 种方法可以恰好装满背包。

2. 确定递推公式

递推公式:dp[i][j] = dp[i-1][j + nums[i-1] + dp[i-1][j - nums[i-1]];

可以理解为nums[i-1]这个元素我可以执行加,还可以执行减,那么我dp[i][j]的结果值就是加/减之后对应位置的和。

也可以写成递推的形式:

dp[i][j + nums[i]] += dp[i - 1][j]
dp[i][j - nums[i]] += dp[i - 1][j]

3. dp数组如何初始化

按上面的定义,dp[i][j] 的值在 [-sum, sum](sum是nums数组元素之和),为了防止负数下标不被识别,可以同时+sum,也就是 -sum -> 0; 0 -> sum; sum -> 2*sum

dp[0][sum]=1(即dp[0][0] = 1,递归结束出口),dp[0][1:]=0(无效状态)

4. 确定遍历顺序

本题是求组合数。外循环为物品,内循环为背包。

5. 举例推导dp数组

二维写法

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        if(nums.size() == 0) return 0;
        int n = nums.size(), sum = 0;
        for(int i : nums) sum += i;
        if(S > sum) return 0;//此时没有方案
        int bagSize = 2 * sum;        
        vector<vector<int> > dp(n + 1, vector<int>(bagSize + 1, 0));
        //-sum 被映射到0
       // 0 被映射到 sum
      // sum 被映射到 2*sum
        dp[0][sum] = 1;
        for(int i = 1; i <= n; i++)//遍历物品
         for(int j = 0; j <= bagSize; j++)//遍历背包
         {
            int left = (j - nums[i - 1]) >= 0 ? j - nums[i - 1] : 0;
            int right = (j + nums[i - 1]) <= bagSize ? j + nums[i - 1] : 0;
            dp[i][j] = dp[i - 1][left] + dp[i -1][right];
         }
          
        return dp[n][S + sum];
    }
};

一维写法

这道题不能只使用一个一维数组,因为在动态转移方程里,每个状态dp[i][j]不仅依赖其上一行左边的值,也依赖上一行右边的值,因此无法实现一个一维数组更新,需要两个一维数组实现滚动更新

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        if(nums.size() == 0) return 0;
        int n = nums.size(), sum = 0;
        for(int i : nums) sum += i;
        if(S > sum) return 0;//此时没有方案
        int bagSize = 2 * sum;
        //-sum 被映射到0
       // 0 被映射到 sum
      // sum 被映射到 2*sum
        vector<int> dp(bagSize + 1, 0);
        vector<int> cur(bagSize + 1, 0);
        dp[sum] = 1;
        for(int i = 1; i <= n; i++)
        {
           for(int j = bagSize; j >= 0; j--)
            {
                int left = (j - nums[i - 1]) >= 0 ? j - nums[i - 1] : 0;
                int right = (j + nums[i - 1]) <= bagSize ? j + nums[i - 1] : 0;
                cur[j] = dp[left] + dp[right];
            }
            dp.swap(cur);
        }
         
        return dp[S + sum];
      
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值