力扣- -241.为运算表达式设计优先级
力扣- -241.為運算表達式設計優先級(分治算法)
文章目錄
- 力扣- -241.為運算表達式設計優先級(分治算法)
- 一、題目描述
- 二、分析
- 三、代碼
- 四、優化
一、題目描述
二、分析
-
看到這道題的第一感覺肯定是復雜,我要窮舉出所有可能的加括號方式,是不是還要考慮括號的合法性?是不是還要考慮計算的優先級?
-
是的,這些都要考慮,但是不需要我們來考慮。利用分治思想和遞歸函數,算法會幫我們考慮一切細節,也許這就是算法的魅力吧,哈哈哈。
廢話不多說,解決本題的關鍵有兩點:
-
1、不要思考整體,而是把目光聚焦局部,只看一個運算符。
-
該問題只要思考每個部分需要做什么,而不要思考整體需要做什么。
-
說白了,解決遞歸相關的算法問題,就是一個化整為零的過程,你必須瞄準一個小的突破口,然后把問題拆解,大而化小,利用遞歸函數來解決。
-
2、明確遞歸函數的定義是什么,相信并且利用好函數的定義。
-
這也是前文經常提到的一個點,因為遞歸函數要自己調用自己,你必須搞清楚函數到底能干嘛,才能正確進行遞歸調用。
下面來具體解釋下這兩個關鍵點怎么理解。
- 我們先舉個例子,比如我給你輸入這樣一個算式:
-
請問,這個算式有幾種加括號的方式?請在一秒之內回答我。
-
估計你回答不出來,因為括號可以嵌套,要窮舉出來肯定得費點功夫。
-
不過呢,嵌套這個事情吧,我們人類來看是很頭疼的,但對于算法來說嵌套括號不要太簡單,一次遞歸就可以嵌套一層,一次搞不定大不了多遞歸幾次。
-
所以,作為寫算法的人類,我們只需要思考,如果不讓括號嵌套(即只加一層括號),有幾種加括號的方式?
-
還是上面的例子,顯然我們有四種加括號方式:
-
發現規律了么?其實就是按照運算符進行分割,給每個運算符的左右兩部分加括號,這就是之前說的第一個關鍵點,不要考慮整體,而是聚焦每個運算符。
-
現在單獨說上面的第三種情況:
-
我們用減號-作為分隔,把原算式分解成兩個算式1 + 2 * 3和4 * 5。
-
分治分治,分而治之,這一步就是把原問題進行了「分」,我們現在要開始「治」了。
-
1 + 2 * 3可以有兩種加括號的方式,分別是:
- 或者我們可以寫成這種形式:
-
而4 * 5當然只有一種加括號方式,就是4 * 5 = [20]。
-
然后呢,你能不能通過上述結果推導出(1 + 2 * 3) - (4 * 5)有幾種加括號方式,或者說有幾種不同的結果?
-
顯然,可以推導出來(1 + 2 * 3) - (4 * 5)有兩種結果,分別是:
- 那你可能要問了,1 + 2 * 3 = [9, 7]的結果是我們自己看出來的,如何讓算法計算出來這個結果呢?
-
這個函數不就是干這個事兒的嗎?這就是我們之前說的第二個關鍵點,明確函數的定義,相信并且利用這個函數定義。
-
你甭管這個函數怎么做到的,你相信它能做到,然后用就行了,最后它就真的能做到了。
-
那么,對于(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 - 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 用來結束遞歸,其實這段代碼就是我們分治算法的 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; }總結
以上是生活随笔為你收集整理的力扣- -241.为运算表达式设计优先级的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 摩尔投票法(力扣- -229. 求众数
- 下一篇: 力扣174. 地下城游戏