We are given two soups, A and B, each starting with n
mL. On each turn, the program randomly picks one of the following four serving operations, each with equal probability (0.25):
- Serve 100 mL from A, 0 mL from B
- Serve 75 mL from A, 25 mL from B
- Serve 50 mL from A, 50 mL from B
- Serve 25 mL from A, 75 mL from B
The process stops when either A or B becomes empty. If an operation asks to serve more soup than is available, it only serves what remains. The goal is to calculate the probability that soup A is emptied before B, plus half the probability that both become empty in the same step.
Intuition
Each turn reduces both A and B. The outcome depends on which soup finishes first or whether they both finish at the same time. The four operations form a probabilistic branching structure, and the problem can be seen as calculating the expected result from a probability tree.
We can use memoization to store the results of overlapping subproblems. Importantly, when n
becomes large (specifically, above 4500), the result converges very close to 1.0
. So we can short-circuit the calculation in such cases.
To simplify calculations, we convert n
into units of 25 mL. This helps limit the total number of states we need to compute.
Approach
We define a recursive function p(a, b)
that returns the probability that soup A is emptied before B (plus half if they empty at the same time), starting from a
units of A and b
units of B. We reduce the soup volume in units of 25 mL.
We use a 2D array size[a][b]
to memoize results to avoid recalculating the same state.
C++ Code
class Solution {
public:
double p(int a, int b)
{
if (a <= 0 && b <= 0)
return 0.5;
if (a <= 0)
return 1;
if (b <= 0)
return 0;
auto& res = size[a][b];
if (res == 0)
res = 0.25 * (p(a - 4, b) + p(a - 3, b - 1) + p(a - 2, b - 2) + p(a - 1, b - 3));
return res;
}
double soupServings(int n) {
if (n > 4500) return 1;
int m = ceil(n / 25.0);
return p(m, m);
}
double size[300][300];
};
JavaScript Version
var soupServings = function(n) {
if (n > 4500) return 1;
const memo = new Map();
const dp = (a, b) => {
if (a <= 0 && b <= 0) return 0.5;
if (a <= 0) return 1;
if (b <= 0) return 0;
const key = `${a},${b}`;
if (memo.has(key)) return memo.get(key);
const result = 0.25 * (
dp(a - 4, b) +
dp(a - 3, b - 1) +
dp(a - 2, b - 2) +
dp(a - 1, b - 3)
);
memo.set(key, result);
return result;
};
const m = Math.ceil(n / 25);
return dp(m, m);
};
Python Version
import math
from functools import lru_cache
class Solution:
def soupServings(self, n: int) -> float:
if n > 4500:
return 1.0
m = math.ceil(n / 25)
@lru_cache(None)
def dp(a, b):
if a <= 0 and b <= 0:
return 0.5
if a <= 0:
return 1.0
if b <= 0:
return 0.0
return 0.25 * (
dp(a - 4, b) +
dp(a - 3, b - 1) +
dp(a - 2, b - 2) +
dp(a - 1, b - 3)
)
return dp(m, m)
Time and Space Complexity
Time Complexity:
- The number of unique states we compute is proportional to
O(n^2)
wheren
is the number of 25 mL units (i.e.,ceil(n / 25)
). - So worst-case is
O(300 * 300) = 90,000
states whenn = 4500
.
Space Complexity:
- We use a 2D array or memoization table of size
O(n^2)
to cache results.
Final Thoughts
This problem blends probability and recursion. The key insight is to limit the state space by grouping soup quantities in multiples of 25. For large n
, the probability of A being finished first becomes so close to 1.0 that we can safely return 1 as an approximation. Memoization helps to efficiently handle overlapping subproblems without recomputing.
The Code snippets in multiple languages helps a lot,
Loved it !