题目分析
题目链接:https://leetcode.com/problems...
从表面上看,题目问有多少种方式为表达式增加括号,实际上等价于找出表达式所有可能的计算次序,每一种画括号的方式一一对应于一种计算次序。
增加括号的过程虽然在纸上写着简单,但是如果真的用程序来模拟它,会非常繁琐且容易出错。对于这种难以用程序模拟的过程,应该尝试找出等价、更容易编码的过程。
表达式的计算次序很容易被计算机表示,因为表达式的计算是递归的(语法树),我们可以通过递归的方式确定表达式的计算次序:
对于一个表达式,我们只要确定它最后计算哪个符号,这个符号的两边就是子表达式,然后递归地确定子表达式的计算次序,直到表达式由单个数字组成。
递归实现
class Solution {
public:
vector<int> diffWaysToCompute(string input) {
vector<int> res;
const int size = input.length();
for (int i = 0; i < size; ++i) {
char c = input[i];
if (c == '+' || c == '-' || c == '*') {
auto v1 = diffWaysToCompute(input.substr(0, i));
auto v2 = diffWaysToCompute(input.substr(i + 1));
for (int r1 : v1)
for (int r2 : v2)
res.push_back(c == '+' ? r1 + r2 : c == '-' ? r1 - r2 : r1 * r2);
}
}
if (res.empty()) res.push_back(stoi(input));
return res;
}
};
这是一个非常典型的分治算法,它会尝试枚举出所有可能的语法树。首先它确定一个根节点(最后计算的符号),然后,根据2个子表达式,找出所有可能的左右子树。
改进为动态规划算法
递归实现分治有一个弊端:子问题会被重复计算很多次。具体到这个问题,题目给出的例子中:
2*3
这个乘法计算了2次,同理,3-4
、4*5
都计算了2次,如果输入表达式再长一些,会有更长的式子被重复计算、重复次数更多。
这是递归的“遗忘”特性造成的,除非你手动在递归过程中保存结果,否则如果递归算法下次递归到同一个子表达式,它会重复计算一遍。
动态规划恰好能避免这个问题。
class Solution {
public:
vector<int> diffWaysToCompute(string input) {
// 数字保存到nums,将算符保存到ops
stringstream ss(input + '+');
int num = 0;
char op = ' ';
vector<int> nums;
vector<int> ops;
while (ss >> num && ss >> op) {
nums.push_back(num);
ops.push_back(op);
}
ops.pop_back();
const int size = nums.size();
vector<vector<vector<int>>> dp(size, vector<vector<int>>(size));
for (int i = 0; i < size; ++i) {
dp[i][i].push_back(nums[i]);
for (int j = i - 1; j >= 0; --j) {
// 计算[j, i](nums数组的下标范围)的所有可能结果
for (int k = j; k < i; ++k) {
// 左子表达式:[j,k], 右子表达式:[k+1,i]
const vector<int> left = dp[j][k];
const vector<int> right = dp[k + 1][i];
for (int x : left)
for (int y : right) switch (ops[k]) {
case '+':
dp[j][i].push_back(x + y);
break;
case '-':
dp[j][i].push_back(x - y);
break;
case '*':
dp[j][i].push_back(x * y);
break;
}
}
}
}
return dp[0][size - 1];
}
};
动态规划中,很重要的一点就是确定合适的子问题计算次序,使得计算父问题的时候,子问题在此之前就已经计算好了。
在上面的实现中,i是右游标,j是左游标。i从0开始往右移动,j从i-1开始往左移动,依次计算表达式[j, i]所有可能的结果。按照这种计算顺序,当我们将表达式[j, i]划分为左子表达式[j,k], 右子表达式[k+1,i]时,这两个子表达式的结果必定在此之前已经算好了,且已经存储在dp数组中。
更多阅读
- 《算法概论》6.3对比递归和动态规划的部分: Recursion? No, thanks.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。