动态规划系列—动态规划VS回溯算法

本篇题解

leetcode 494 目标和
leetcode 322 零钱兑换
leetcode 518 零钱兑换II

算法解决问题的核心实际上还是穷举。问题的关键在于如何聪明、有效率地进行进行穷举,这是我们构建合适算法的目标。

动态规划和回溯算法看起来有挺多共同之处,都涉及到了【递归】和【做选择】,那么他们之间区分在哪里呢?以及这两者之间是否能够转化?

通常来讲,我们使用回溯算法去遍历的时候,就是在使用暴力穷举的方法,当数据量很庞大的时候,显而易见地就会使算法效率变得很低。所以,我们通常会引入“剪枝”的思想来进一步优化,即在遍历的时候,我们往往能通过提前预判筛查掉一些肯定不可能的情况,来降低计算量的大小。这样一种回溯的结构,实际上就是在对树进行深度遍历的过程。不管是二叉树,还是N叉树也好,关键在于在【选择列表】中做选择。下面给出回溯算法的框架,就能很体会这个场景。(来自labuladong,文末附链接。)

在这里插入图片描述

仔细一想,这当中实际上有不少节点都被重复运算了,但是计算机是没有记忆的,所以这其中我们消耗了不少精力在计算之前已经出现的结果上。所以我们提出了使用【动态规划】来解决这种“重复子问题”的情况。

通过leetcode题来看一下具体情况。

leetcode 494 目标和

在这里插入图片描述

回溯解法

这道题一拿到手,你就想肯定能用回溯求解。对于每个数我有两种【选择】,“+”或者“-”,针对每种选择我们可以得到对应【选择】下的状态,也就是当前的算术结果,当算术结果等于目标和时,我们就得到了一种解法。浅显易懂,暴力也比没做出来好。
代码如下:
这里我们使用target==0来作为判断,可以减少用res值去记录当前的算术结果,所以之间把剩余的值放进去继续递归。

void backTrack(int i,vector<int>& nums,long target){
        if(i == nums.size()){
            if(target == 0)
             res++; //得到一种解法
          return;  //结束的地方
        }
        backTrack(i+1,nums,target-nums[i]);  //选择加号
        backTrack(i+1,nums,target+nums[i]);  //选择减号
    }

动态规划–消除重叠子问题

你开始着手优化算法了。既然之间回溯存在了重复子问题的状况。则使用动态规划来解决,一般可以使用备忘录和“迭代填表”法。

备忘录

既然我们刚刚说有结果被重复的被计算?那么我们就可以把子问题记录起来,当同一个子问题再次出现的时候,就可以查询“备忘录”直接取出结果。这就是它的思想。
代码如下:

    map<string,int> memo;
    dp代表当前状态下的方案数
    int dp(vector<int>& nums, int i,long target){
        //退出状态
        if(i==nums.size()){
            if(target==0) return 1;
            return 0;
        }
        string key = to_string(i) + "," + to_string(target);
        //查询备忘录
        if(memo.find(key)!=memo.end()){
            return memo.find(key)->second;
        }
        //迭代计算
        long result = dp(nums,i+1,target-nums[i])+dp(nums,i+1,target+nums[i]);
        //记入备忘录
        memo.insert(pair<string,int>(key,result));  
        return result;
    }

转化为子集划分问题

实际上上面使用备忘录的方法仍然还不够理想。当数据量很大时,查询也消耗了许多时间。最好的情况是我们能找到状态转换之间的关系,用填表的形式,用已知的状态来求解当前未知的状态。因此状态转移方程对动态规划是至关重要的。

我们可以把目标和的这个问题转化为一个子集划分问题。对于每个数我们要从两个选择“+”和“-”进行选择,因此nums中的所有数被划分为两个子集A和B,分别代表分配“+”和“-”的数。存在如下关系:

sum(A) - sum(B) = target
sum(A) = target + sum(B)
sum(A) + sum(A) = target + sum(B) + sum(A)
2 * sum(A) = target + sum(nums)

所以 sum(A) = (target + sum(nums)) / 2,问题转换成nums中可以找出几个子集A,使得A的元素和为(target + sum(nums)) / 2。

这里翻译一下就是,子集A中的元素代表背包中选中的物品。而A的元素和就相当于背包的容量大小((target + sum(nums)) / 2),而有几个子集A,就是找出符合条件(选中物品总和等于背包容量)的方案数。

那就直接按照背包问题的情况处理了。

int subset(vector<int>&nums,int target){
       int N = nums.size();
       int sum = 0;
       for(int i = 0;i<nums.size();i++){
           sum+=nums[i];
       }
       if(sum<target || (sum+target)%2==1) return 0;
       int total = (sum+target)/2;
       //base line
       vector<vector<int>> dp(nums.size()+1, vector<int>(total+1,0));
       dp[0][0]=1;

       for(int i =1 ;i<=nums.size();i++){
           for(int j = 0;j<=total;j++){
                   if(j==0) dp[i][0] = 1;
                   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][total];
    }

其实还可以进一步优化,使用【状态压缩】的方法,将dp的二维数组转换成一维数组。因为当前的选择状态只与上一个状态有关,我们可以只存储上一个状态即可。

leetcode 322 零钱兑换

在这里插入图片描述

涉及到最值问题,实际上只需要思考状态方程的关键。

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
       vector<int> dp(amount+1,amount+1);   //金额为i需要dp[i]硬币
       dp[0] = 0;
       for(int i = 0;i<dp.size();i++){
           for(int j=0;j<coins.size();j++){
               if(i-coins[j]<0) continue;
               dp[i] = min(dp[i],dp[i-coins[j]]+1);
           }
       }
       return (dp[amount]==amount+1)?-1:dp[amount];
    }
};

leetcode 518 零钱兑换II

在这里插入图片描述

本质和目标和问题是一样的,直接给出代码。

class Solution {
public:
    int change(int amount, vector<int>& coins) {
     //base line
     vector<vector<int>> dp(coins.size()+1,vector<int>(amount+1,0));
     dp[0][0]=1;
      for(int i = 1;i<=coins.size();i++){
          for(int j=0;j<=amount;j++){
            if(j==0) dp[i][0] = 1;
            //if(coins[i]<j) dp[i][j] = 0;
            if(j>=coins[i-1]) dp[i][j] = dp[i-1][j]+dp[i][j-coins[i-1]];
            else dp[i][j]=dp[i-1][j]; 
          }
      }
    
    return dp[coins.size()][amount];

    }
};

写在最后

做动态规划题目,想清楚三点。

  1. 明确【状态】和【选择】
  2. 明确dp数组的含义
  3. 根据【选择】,明确状态转移方程的关键。
    以及考虑好baseline的情况。

参考

https://labuladong.gitbook.io/algo/dong-tai-gui-hua-xi-lie/targetsum

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值