題目
Given a string of numbers and operators, return all possible results from computing all the different possible ways to group numbers and operators. The valid operators are +, - and *.
Example 1
Input: "2-1-1"
.
((2-1)-1) = 0
(2-(1-1)) = 2
Output: [0, 2]
Example 2
Input: "2*3-4*5"
(2*(3-(4*5))) = -34
((2*3)-(4*5)) = -14
((2*(3-4))*5) = -10
(2*((3-4)*5)) = -10
(((2*3)-4)*5) = 10
Output: [-34, -14, -10, -10, 10]
解題思路
加括號的問題,事實上就是哪個運算先做,哪個運算後做的問題。因此我們可以用運算符作爲根結點,運算數作爲子結點把一個表達式展開成一棵樹的形式,屬於同一個子樹的兩個運算數先做運算。最後看看展開的共有多少棵不同的樹,相對應的就有幾個結果。比如上面的第二個例子,畫成樹的形式如下:
* - * * *
/ \ / \ / \ / \ / \
2 - * * * 5 2 * - 5
/ \ / \ / \ / \ / \ / \
3 * 2 3 4 5 2 - - 5 * 4
/ \ / \ / \ / \
4 5 3 4 3 4 2 3
運算樹1 運算樹2 運算樹3 運算樹4 運算樹5
樹的結構本來就是遞歸定義的,因此這道題的解題方法就是分治和遞歸。對於輸入的字符串,我們可以分別用掃描到的每個運算符作爲父結點,然後把整個字符串劃分爲兩部分(即兩個子結點)分別進行運算再合併,相當於把原問題劃分成兩個子問題(此時原問題和子問題都是同類型的問題!即都是加括號求表達式的問題,只不過子問題的規模縮小了),如此一直劃分下去,直到劃分到的子串中再也不包含運算符,此時把該子串轉化爲整數返回即可。
注意每一層遞歸應該返回對應子樹計算的所有可能結果,比如上圖中的 運算樹1
和 運算樹4
,他們對應的表達式都是 (2 * (第二層子樹))
,但是這個第二層子樹根據不同的結構可以得到兩種結果 3 - (4 * 5) = -17
和 (3 - 4) * 5 = -5
,因此在遞歸調用時,應該返回包含這兩個結果的一個 vector
,然後分別用 2
和這兩種結果 -17
和 -5
相乘得到最終的兩種結果,只有這樣纔不會漏解。
這樣的分治遞歸顯然是 make sense 的,但是可以再優化。觀察上面的運算樹我們可以發現:運算樹1
和 運算樹2
的葉子結點都要計算 4 * 5
,運算樹3
和 運算樹4
的葉子結點都要計算 3 - 4
,運算樹2
和 運算樹5
的葉子結點都要計算 2 * 3
,這些葉子結點在遞歸的過程中都會碰到,從而進行了不必要的重複計算。我們可以利用動態規劃的思想去建造一張表達式對應其運算結果的表,每次遞歸時先檢查表中是否已經保存有相同表達式的運算結果,若有則直接利用這個運算結果,不需要再向下進行遞歸。
Java代碼實現
class Solution {
public List<Integer> diffWaysToCompute(String input) {
Map<String, List<Integer> > expressions = new HashMap<>();
return getResult(input, expressions);
}
public List<Integer> getResult(String input, Map<String, List<Integer> > expressions) {
List<Integer> immediateRst = new ArrayList<>();
for (int i = 0; i < input.length(); ++i) {
if (input.charAt(i) == '+' || input.charAt(i) == '-' || input.charAt(i) == '*') {
String left = input.substring(0, i);
String right = input.substring(i + 1);
List<Integer> l = null;
List<Integer> r = null;
// 先從哈希表中尋找表達式對應的結果值,如果找不到,再進行遞歸
if (expressions.containsKey(left)) {
l = expressions.get(left);
} else {
l = getResult(left, expressions);
}
if (expressions.containsKey(right)) {
r = expressions.get(right);
} else {
r = getResult(right, expressions);
}
// 把得到的兩組結果兩兩分別運算
for (Integer num1 : l) {
for (Integer num2 : r) {
switch(input.charAt(i)) {
case '+':
immediateRst.add(num1 + num2);
break;
case '-':
immediateRst.add(num1 - num2);
break;
case '*':
immediateRst.add(num1 * num2);
}
}
}
}
}
// 如果是葉子結點,則直接轉化爲整數存入列表中,且葉子結點不需要保存進哈希表
if (immediateRst.isEmpty()) { immediateRst.add(Integer.parseInt(input)); }
else { expressions.put(input, immediateRst); }
return immediateRst;
}
}