You have 4 cards each containing a number from 1 to 9. You need to judge whether they could operated through *
, /
, +
, -
, (
, )
to get the value of 24.
Example 1:
Input: [4, 1, 8, 7] Output: True Explanation: (8-4) * (7-1) = 24
Example 2:
Input: [1, 2, 1, 2] Output: False
Note:
- The division operator
/
represents real division, not integer division. For example, 4 / (1 - 2/3) = 12. - Every operation done is between two numbers. In particular, we cannot use
-
as a unary operator. For example, with[1, 1, 1, 1]
as input, the expression-1 - 1 - 1 - 1
is not allowed. - You cannot concatenate numbers together. For example, if the input is
[1, 2, 1, 2]
, we cannot write this as 12 + 12.
這道題就是經典的24點游戲了,記得小時候經常玩這個游戲,就是每個人發四張牌,看誰最快能算出24,這完全是腦力大比拼啊,不是拼的牌技。玩的多了,就會摸出一些套路來,比如盡量去湊2和12,3和8,4和6等等,但是對於一些特殊的case,比如 [1, 5, 5, 5] 這種,正確的解法是 5 * (5 - 1 / 5),一般人都會去試加減乘,和能整除的除法,而像這種帶小數的確實很難想到,但是程序計算就沒問題,可以遍歷所有的情況,這也是這道題的實際意義所在吧。那么既然是要遍歷所有的情況,我們應該隱約感覺到應該是要使用遞歸來做的。我們想,任意的兩個數字之間都可能進行加減乘除,其中加法和乘法對於兩個數字的前后順序沒有影響,但是減法和除法是有影響的,而且做除法的時候還要另外保證除數不能為零。我們要遍歷任意兩個數字,然后對於這兩個數字,嘗試各種加減乘除后得到一個新數字,將這個新數字加到原數組中,記得原來的兩個數要移除掉,然后調用遞歸函數進行計算,我們可以發現每次調用遞歸函數后,數組都減少一個數字,那么當減少到只剩一個數字了,就是最后的計算結果,所以我們在遞歸函數開始時判斷,如果數組只有一個數字,且為24,說明可以算出24,結果res賦值為true返回。這里我們的結果res是一個全局的變量,如果已經為true了,就沒必要再進行運算了,所以第一行應該是判斷結果res,為true就直接返回了。我們遍歷任意兩個數字,分別用p和q來取出,然后進行兩者的各種加減乘除的運算,將結果保存進數組臨時數組t,記得要判斷除數不為零。然后將原數組nums中的p和q移除,遍歷臨時數組t中的數字,將其加入數組nums,然后調用遞歸函數,記得完成后要移除數字,恢復狀態,這是遞歸解法很重要的一點。最后還要把p和q再加回原數組nums,這也是還原狀態,參見代碼如下:
解法一:
class Solution { public: bool judgePoint24(vector<int>& nums) { bool res = false; double eps = 0.001; vector<double> arr(nums.begin(), nums.end()); helper(arr, eps, res); return res; } void helper(vector<double>& nums, double eps, bool& res) { if (res) return; if (nums.size() == 1) { if (abs(nums[0] - 24) < eps) res = true; return; } for (int i = 0; i < nums.size(); ++i) { for (int j = 0; j < i; ++j) { double p = nums[i], q = nums[j]; vector<double> t{p + q, p - q, q - p, p * q}; if (p > eps) t.push_back(q / p); if (q > eps) t.push_back(p / q); nums.erase(nums.begin() + i); nums.erase(nums.begin() + j); for (double d : t) { nums.push_back(d); helper(nums, eps, res); nums.pop_back(); } nums.insert(nums.begin() + j, q); nums.insert(nums.begin() + i, p); } } } };
來看一種很不同的遞歸寫法,這里將加減乘除操作符放到了一個數組ops中。並且沒有用全局變量res,而是讓遞歸函數帶有bool型返回值。在遞歸函數中,還是要先看nums數組的長度,如果為1了,說明已經計算完成,直接看結果是否為0就行了。然后遍歷任意兩個數字,注意這里的i和j都分別從0到了數組長度,而上面解法的j是從0到i,這是因為上面解法將p - q, q - p, q / q, q / p都分別列出來了,而這里僅僅是nums[i] - nums[j], nums[i] / nums[j],所以i和j要交換位置,但是為了避免加法和乘法的重復計算,我們可以做個判斷,還有別忘記了除數不為零的判斷,i和j不能相同的判斷。我們建立一個臨時數組t,將非i和j位置的數字都加入t,然后遍歷操作符數組ops,每次取出一個操作符,然后將nums[i]和nums[j]的計算結果加入t,調用遞歸函數,如果遞歸函數返回true了,那么就直接返回true。否則移除剛加入的結果,還原t的狀態,參見代碼如下:
解法二:
class Solution { public: bool judgePoint24(vector<int>& nums) { double eps = 0.001; vector<char> ops{'+', '-', '*', '/'}; vector<double> arr(nums.begin(), nums.end()); return helper(arr, ops, eps); } bool helper(vector<double>& nums, vector<char>& ops, double eps) { if (nums.size() == 1) return abs(nums[0] - 24) < eps; for (int i = 0; i < nums.size(); ++i) { for (int j = 0; j < nums.size(); ++j) { if (i == j) continue; vector<double> t; for (int k = 0; k < nums.size(); ++k) { if (k != i && k != j) t.push_back(nums[k]); } for (char op : ops) { if ((op == '+' || op == '*') && i > j) continue; if (op == '/' && nums[j] < eps) continue; switch(op) { case '+': t.push_back(nums[i] + nums[j]); break; case '-': t.push_back(nums[i] - nums[j]); break; case '*': t.push_back(nums[i] * nums[j]); break; case '/': t.push_back(nums[i] / nums[j]); break; } if (helper(t, ops, eps)) return true; t.pop_back(); } } } return false; } };
討論:博主在調試的時候,遇到了這個test case: [1, 3, 4, 6],返回的是true,但是博主心算了一會,並沒有想出其是如何算出24的。所以博主在想,能不能修改下代碼,使得其能將運算的過程返回出來。其實並不難改,基於解法二來改一下,我們發現,計算后的結果被存入了臨時數組t,進行下一次遞歸,我們需要將這個過程保存下來,用一個字符串數組,比如"1+3",或者"1-3"等等,這個數組跟數組t大小相同,操作基本相同,同時需要被傳入到下一次遞歸函數中,而在下一次遞歸函數中,數組t中取出的就是4和-2,但是字符串數組就可以取出"1+3"和"1-3",我們就可以繼續和別的數進行計算了,比如要乘以4,我們需要給取出的字符串加上括號,就變成了(1+3)*4了,就通過這種方法就可以將過程返回了,運行test case: [1, 3, 4, 6],返回得到:
(6/(1-(3/4)))
沒有問題,還有就是,由於組成24的方法可能不止1種,我們可以將所有情況都返回,那么我們的遞歸函數就不要有返回值,這樣可以遍歷完所有的情況,對於test case: [1, 2, 3, 8],返回如下:
((8-2)*(1+3))
(8/(1-(2/3)))
(3/((2-1)/8))
(3*(8/(2-1)))
(8*(3*(2-1)))
(2*(1+(3+8)))
(8/((2-1)/3))
(3*(8*(2-1)))
((1+3)*(8-2))
((3*8)/(2-1))
(8*(3/(2-1)))
((3*8)*(2-1))
(2*(8+(1+3)))
((2-1)*(3*8))
(2*(3+(1+8)))
被驚到了有木有!居然有這么多種計算方法可以得到24~ (修改后的代碼貼在了評論區二樓 :)
參考資料:
https://leetcode.com/problems/24-game/
https://leetcode.com/problems/24-game/discuss/107685/679-24-game-c-recursive
https://leetcode.com/problems/24-game/discuss/107673/java-easy-to-understand-backtracking