动态规划系列—动态规划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
    评论
DFS(深度优先搜索)是一种常见的图遍历算法,它使用递归或栈的方式,从一个顶点出发,沿着一条路径一直到达最深的节点,然后回溯到上一层继续遍历其他节点。DFS常被用于解决图的连通性问题、路径问题等。在实际应用中,可以使用DFS进行状态搜索、图的遍历、拓扑排序等。 剪枝是指在搜索过程中,通过一系列的策略判断,提前终止当前搜索分支,并跳过一些无用的搜索路径,从而减少搜索时间。剪枝的核心在于提前排除某些明显不符合条件的状态,以减少无效搜索的时间开销,提高效率。在算法设计中,剪枝通常会利用一些特定的性质或条件进行判断,从而缩小搜索空间。 动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划通常用于求解最优化问题,它通过定义状态和状态转移方程,采用自底向上的思路,逐步求解每个子问题的最优值,最终得到原问题的最优解。动态规划的核心是存储已经计算过的子问题的解,避免了重复计算。 贪心算法是一种基于局部最优解的策略,它通过每一步选择在当前状态下最优的解,以期望得到全局最优解。贪心算法的基本思想是由局部最优解推导出全局最优解,通常通过贪心选择性质、最优子结构和贪心选择构成三部分。贪心算法相比其他算法,如动态规划,它的优势在于简单、高效,但缺点在于不能保证获取到全局最优解,只能得到一个近似解。 综上所述,DFS、剪枝、动态规划和贪心算法算法设计和问题求解中都发挥着重要的作用。具体使用哪种算法取决于问题的性质和要求,需要在实际应用中进行综合考虑和选择。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值