力扣- -241.为运算表达式设计优先级

算法 专栏收录该内容
130 篇文章 0 订阅

力扣- -241.为运算表达式设计优先级(分治算法)

一、题目描述

在这里插入图片描述

二、分析

  • 看到这道题的第一感觉肯定是复杂,我要穷举出所有可能的加括号方式,是不是还要考虑括号的合法性?是不是还要考虑计算的优先级?

  • 是的,这些都要考虑,但是不需要我们来考虑。利用分治思想和递归函数,算法会帮我们考虑一切细节,也许这就是算法的魅力吧,哈哈哈。

废话不多说,解决本题的关键有两点:

  • 1、不要思考整体,而是把目光聚焦局部,只看一个运算符。

  • 该问题只要思考每个部分需要做什么,而不要思考整体需要做什么。

  • 说白了,解决递归相关的算法问题,就是一个化整为零的过程,你必须瞄准一个小的突破口,然后把问题拆解,大而化小,利用递归函数来解决。

  • 2、明确递归函数的定义是什么,相信并且利用好函数的定义。

  • 这也是前文经常提到的一个点,因为递归函数要自己调用自己,你必须搞清楚函数到底能干嘛,才能正确进行递归调用。

下面来具体解释下这两个关键点怎么理解。

  • 我们先举个例子,比如我给你输入这样一个算式:
1 + 2 * 3 - 4 * 5
  • 请问,这个算式有几种加括号的方式?请在一秒之内回答我。

  • 估计你回答不出来,因为括号可以嵌套,要穷举出来肯定得费点功夫。

  • 不过呢,嵌套这个事情吧,我们人类来看是很头疼的,但对于算法来说嵌套括号不要太简单,一次递归就可以嵌套一层,一次搞不定大不了多递归几次。

  • 所以,作为写算法的人类,我们只需要思考,如果不让括号嵌套(即只加一层括号),有几种加括号的方式?

  • 还是上面的例子,显然我们有四种加括号方式:

(1) + (2 * 3 - 4 * 5)

(1 + 2) * (3 - 4 * 5)

(1 + 2 * 3) - (4 * 5)

(1 + 2 * 3 - 4) * (5)
  • 发现规律了么?其实就是按照运算符进行分割,给每个运算符的左右两部分加括号,这就是之前说的第一个关键点,不要考虑整体,而是聚焦每个运算符。

  • 现在单独说上面的第三种情况:

(1 + 2 * 3) - (4 * 5)
  • 我们用减号-作为分隔,把原算式分解成两个算式1 + 2 * 3和4 * 5

  • 分治分治,分而治之,这一步就是把原问题进行了「分」,我们现在要开始「治」了。

  • 1 + 2 * 3可以有两种加括号的方式,分别是:

(1) + (2 * 3) = 7

(1 + 2) * (3) = 9
  • 或者我们可以写成这种形式:
1 + 2 * 3 = [9, 7]
  • 4 * 5当然只有一种加括号方式,就是4 * 5 = [20]。

  • 然后呢,你能不能通过上述结果推导出(1 + 2 * 3) - (4 * 5)有几种加括号方式,或者说有几种不同的结果?

  • 显然,可以推导出来(1 + 2 * 3) - (4 * 5)有两种结果,分别是:

9 - 20 = -11

7 - 20 = -13
  • 那你可能要问了,1 + 2 * 3 = [9, 7]的结果是我们自己看出来的,如何让算法计算出来这个结果呢?
vector<int> diffWaysToCompute(string input)
  • 这个函数不就是干这个事儿的吗?这就是我们之前说的第二个关键点,明确函数的定义,相信并且利用这个函数定义。

  • 你甭管这个函数怎么做到的,你相信它能做到,然后用就行了,最后它就真的能做到了。

  • 那么,对于(1 + 2 * 3) - (4 * 5)这个例子,我们的计算逻辑其实就是这段代码:

vector<int> diffWaysToCompute("(1 + 2 * 3) - (4 * 5)") {
    vector<int> res;
    /****** 分 ******/
    vector<int> left = diffWaysToCompute("1 + 2 * 3");
    vector<int> right = diffWaysToCompute("4 * 5");
    /****** 治 ******/
    for (int a : left)
        for (int b : right)
            res.emplace_back(a - b);

    return res;
}
  • 好,现在(1 + 2 * 3) - (4 * 5)这个例子是如何计算的,你应该完全理解了吧,那么回来看我们的原始问题。

  • 原问题1 + 2 * 3 - 4 * 5是不是只有(1 + 2 * 3) - (4 * 5)这一种情况?是不是只能从减号-进行分割?

不是,每个运算符都可以把原问题分割成两个子问题,刚才已经列出了所有可能的分割方式:

(1) + (2 * 3 - 4 * 5)

(1 + 2) * (3 - 4 * 5)

(1 + 2 * 3) - (4 * 5)

(1 + 2 * 3 - 4) * (5)

所以,我们需要穷举上述的每一种情况,可以进一步细化一下解法代码

三、代码

class Solution {
public:
    vector<int> diffWaysToCompute(string input) {
        if(input.empty()) {
            return {};
        }
        
        //每个递归的结果
        vector<int> ret;
	
		//循环便利找符号
        for(size_t i = 0;i < input.size(); ++i) {
            if(input[i] == '+' || input[i] == '-' || input[i] == '*') {
            	//递归左右
                vector<int> left = diffWaysToCompute(input.substr(0, i));
                vector<int> right = diffWaysToCompute(input.substr(i + 1, input.size() - i));
				
				//计算结果
                for(auto &l : left) {
                    for( auto &r : right) {
                        if(input[i] == '+') {
                            ret.emplace_back(l + r);
                        }
                        else if(input[i] == '-') {
                            ret.emplace_back(l - r);
                        }
                        else {
                            ret.emplace_back(l * r);
                        }
                        
                    }
                }
            }
        }

		//递归出口
		//代表本次递归下来没找到符号,就是只有数字,直接返回即可
        if(ret.empty()) {
            ret.emplace_back(stoi(input));
            return ret;
        }
        
        return ret;
    }
};
  • 这段代码应该很好理解了吧,就是扫描输入的算式input,每当遇到运算符就进行分割,递归计算出结果后,根据运算符来合并结果

  • 这就是典型的分治思路,先「分」后「治」,先按照运算符将原问题拆解成多个子问题,然后通过子问题的结果来合成原问题的结果。

  • 当然,一个重点在这段代码:

// base case
// 如果 ret 为空,说明算式是一个数字,没有运算符
if(ret.empty()) {
	ret.emplace_back(stoi(input));
	return ret;
}
  • 递归函数必须有个 base case 用来结束递归,其实这段代码就是我们分治算法的 base case,代表着你「分」到什么时候可以开始「治」。

  • 我们是按照运算符进行「分」的,一直这么分下去,什么时候是个头?显然,当算式中不存在运算符的时候就可以结束了。

  • 那为什么以ret.empty()作为判断条件?因为当算式中不存在运算符的时候,就不会触发 if 语句,也就不会给ret中添加任何元素。

四、优化

// 备忘录
std::map<string, vector<int>> memo;

vector<int> diffWaysToCompute(string input) {
    // 避免重复计算
    if (memo.count(input)) {
        return memo[input];
    }
    
    /****** 其他都不变 ******/
    
    /***********************/

    // 将结果添加进备忘录
    memo[input] = res;

    return res;
}
  • 1
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值