There are two types of soup: type A and type B. Initially we have N
ml of each type of soup. There are four kinds of operations:
- Serve 100 ml of soup A and 0 ml of soup B
- Serve 75 ml of soup A and 25 ml of soup B
- Serve 50 ml of soup A and 50 ml of soup B
- Serve 25 ml of soup A and 75 ml of soup B
When we serve some soup, we give it to someone and we no longer have it. Each turn, we will choose from the four operations with equal probability 0.25. If the remaining volume of soup is not enough to complete the operation, we will serve as much as we can. We stop once we no longer have some quantity of both types of soup.
Note that we do not have the operation where all 100 ml's of soup B are used first.
Return the probability that soup A will be empty first, plus half the probability that A and B become empty at the same time.
Example: Input: N = 50 Output: 0.625 Explanation: If we choose the first two operations, A will become empty first. For the third operation, A and B will become empty at the same time. For the fourth operation, B will become empty first. So the total probability of A becoming empty first plus half the probability that A and B become empty at the same time, is 0.25 * (1 + 1 + 0.5 + 0) = 0.625.
Notes:
0 <= N <= 10^9
.- Answers within
10^-6
of the true value will be accepted as correct.
這道題給了我們兩種湯,A和B,開始時各給了N毫升的。然后說是有下面四種操作:
1. 供應100毫升A湯,0毫升B湯。
2. 供應75毫升A湯,25毫升B湯。
3. 供應50毫升A湯,50毫升B湯。
4. 供應25毫升A湯,75毫升B湯。
我們選擇每種操作的概率是一樣的,讓我們返回A湯先供應完的概率加上A湯和B湯同時供應完的一半概率。又給了一個例子來幫助我們理解。說實話,博主覺得這道題挺讓人費解的,反正博主是沒有啥思路,是直接研究答案的,現在就照着大神們的帖子來講一講吧。
先來看這四種操作,由於概率相同,所以每一種操作都的有,所以這四種操作可以想象成迷宮遍歷的周圍四個方向,那么我們就可以用遞歸來做。再看一下題目中給的N的范圍,可以到10的9次方,而每次湯的消耗最多不過100毫升,由於純遞歸基本就是暴力搜索,所以我們需要加上記憶數組memo,來避免重復運算,提高運行的效率。既然用記憶數組,我們不想占用太多空間,可以對工件進行優化。怎么優化呢,我們發現湯的供應量都是25的倍數,所以我們可以將25毫升當作一份湯的量,所以這四種操作就變成了:
1. 供應4份A湯,0份B湯。
2. 供應3份A湯,1份B湯。
3. 供應2份A湯,2份B湯。
4. 供應1份A湯,3份B湯。
所以我們的湯份數就是可以通過除以25來獲得,由於N可能不是25的倍數,會有余數,但是就算不到一份的量,也算是完成了一個操作,所以我們可以直接加上24再除以25就可以得到正確的份數。那么接下來就是調用遞歸了,其實遞歸函數很直接了當,首先判斷如果兩種湯都沒了,那么返回0.5,因為題目中說了如果兩種湯都供應完了,返回一半的概率;如果A湯沒了,返回1;如果B湯沒了,返回0;如果上面的情況都沒有進入,說明此時A湯和B湯都有剩余,所以我們先查記憶數組memo,如果其大於0,說明當前情況已經被計算過了,我們直接返回該值即可。如果沒有的話,我們就要計算這種情況的值,通過對四種情況分別調用遞歸函數中,將返回的概率值累加后除以4即可。這道題還有一個很大的優化,就是當N大過某一個數值的時候,返回的都是1。這里的4800就是這個閾值返回,這樣的話memo數組的大小就可以是200x200了,至於是怎么提前設定的,博主就不知道了,估計是強行試出來的吧,參見代碼如下:
解法一:
class Solution { public: double memo[200][200]; double soupServings(int N) { return N >= 4800 ? 1.0 : f((N + 24) / 25, (N + 24) / 25); } double f(int a, int b) { if (a <= 0 && b <= 0) return 0.5; if (a <= 0) return 1.0; if (b <= 0) return 0; if (memo[a][b] > 0) return memo[a][b]; memo[a][b] = 0.25 * (f(a - 4, b) + f(a - 3, b - 1) + f(a - 2, b - 2) + f(a - 1, b - 3)); return memo[a][b]; } };
下面這種解法的思路基本一樣,就是沒有用二維數組,而是用了一個HashMap來保存計算過的值,建立字符串到double到映射,這里的字符串是由A湯和B湯的剩余量拼成的,為了保證唯一性,將二者的值先轉為字符串,然后在中間加一個冒號拼在一起。由於是字符串,所以我們也不用將毫升數變成份數,直接就原樣保存吧,參見代碼如下:
解法二:
class Solution { public: unordered_map<string, double> m; double soupServings(int N) { return N >= 4800 ? 1.0 : f(N, N); } double f(int a, int b) { if (a <= 0 && b <= 0) return 0.5; if (a <= 0) return 1.0; if (b <= 0) return 0; string spoon = to_string(a) + ":" + to_string(b); if (!m.count(spoon)) { m[spoon] = 0.25 * (f(a - 100, b) + f(a - 75, b - 25) + f(a - 50, b - 50) + f(a - 25, b - 75)); } return m[spoon]; } };
參考資料:
https://leetcode.com/problems/soup-servings/discuss/125809/Java-soup-(spoon-included)