1230. Toss Strange Coins đ
Problem Description
You are given an array of coins where each coin has a specific probability of landing heads when tossed. The i
-th coin has a probability prob[i]
of showing heads.
Your task is to find the probability that exactly target
number of coins will show heads when you toss all the coins exactly once.
For example, if you have 3 coins with probabilities [0.5, 0.5, 0.5]
and you want exactly 2 coins to show heads (target = 2
), you need to calculate the probability of getting exactly 2 heads when tossing all 3 coins.
The solution uses dynamic programming where f[i][j]
represents the probability of getting exactly j
heads after tossing the first i
coins. The state transition considers two cases for each coin:
- The coin shows tails (probability
1 - p
): contributes tof[i][j]
fromf[i-1][j]
- The coin shows heads (probability
p
): contributes tof[i][j]
fromf[i-1][j-1]
The final answer is f[n][target]
, which gives the probability of getting exactly target
heads after tossing all n
coins.
Intuition
When we need to find the probability of getting exactly target
heads from tossing multiple coins, we need to consider all possible ways this can happen. This naturally leads us to think about building up the solution incrementally - what happens as we add one coin at a time?
Let's think about it step by step. If we've already tossed some coins and know the probability distributions of getting different numbers of heads, how does adding one more coin change things?
When we toss a new coin, two things can happen:
- It shows tails - the number of heads stays the same
- It shows heads - the number of heads increases by 1
This observation suggests we can build our solution by tracking probabilities as we process each coin sequentially. For each coin we add, we update our probability distribution based on what happened with the previous coins.
The key insight is that to get exactly j
heads after tossing i
coins, we could have either:
- Had
j
heads from the firsti-1
coins and thei
-th coin showed tails (probability1 - prob[i]
) - Had
j-1
heads from the firsti-1
coins and thei
-th coin showed heads (probabilityprob[i]
)
This recursive relationship naturally leads to a dynamic programming approach where we build a table f[i][j]
representing the probability of getting exactly j
heads from the first i
coins. We start with the base case f[0][0] = 1
(probability of getting 0 heads with 0 coins is 1) and build up our solution coin by coin until we reach f[n][target]
.
Learn more about Math and Dynamic Programming patterns.
Solution Approach
We implement the solution using dynamic programming with a 2D table. Let's define f[i][j]
as the probability of getting exactly j
heads after tossing the first i
coins.
Initialization:
- Create a 2D array
f
with dimensions(n+1) Ă (target+1)
wheren
is the number of coins - Set
f[0][0] = 1
as the base case (probability of 0 heads with 0 coins is 1) - All other values start at 0
State Transition:
For each coin i
(from 1 to n) and each possible number of heads j
(from 0 to min(i, target)):
where p
is prob[i-1]
(the probability of the i
-th coin showing heads).
Implementation Details:
- We iterate through each coin using
enumerate(prob, 1)
to get both the probabilityp
and the 1-indexed positioni
- For each coin, we only need to consider up to
min(i, target)
heads since:- We can't have more heads than coins tossed (hence
i
) - We don't need to track more than
target
heads
- We can't have more heads than coins tossed (hence
- For each state
f[i][j]
:- We always add the contribution from the coin showing tails:
(1 - p) * f[i-1][j]
- If
j > 0
, we also add the contribution from the coin showing heads:p * f[i-1][j-1]
- We always add the contribution from the coin showing tails:
Space Optimization Note:
While the solution uses a 2D array, it can be optimized to use only 1D space since each state f[i][j]
only depends on the previous row f[i-1]
. However, the provided solution uses the 2D approach for clarity.
The final answer is f[n][target]
, which gives the probability of getting exactly target
heads after tossing all n
coins.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with 3 coins having probabilities [0.4, 0.6, 0.5]
and we want exactly target = 2
heads.
We'll build a DP table f[i][j]
where i
represents coins processed (0 to 3) and j
represents number of heads (0 to 2).
Initial State:
f[0][0] = 1.0 (0 coins, 0 heads: certain) f[0][1] = 0 (0 coins, 1 head: impossible) f[0][2] = 0 (0 coins, 2 heads: impossible)
Processing Coin 1 (prob = 0.4):
f[1][0]
: Get 0 heads with 1 coin- Coin 1 shows tails:
(1 - 0.4) Ă f[0][0] = 0.6 Ă 1.0 = 0.6
- Coin 1 shows tails:
f[1][1]
: Get 1 head with 1 coin- Coin 1 shows tails:
(1 - 0.4) Ă f[0][1] = 0.6 Ă 0 = 0
- Coin 1 shows heads:
0.4 Ă f[0][0] = 0.4 Ă 1.0 = 0.4
- Total:
0 + 0.4 = 0.4
- Coin 1 shows tails:
Processing Coin 2 (prob = 0.6):
f[2][0]
: Get 0 heads with 2 coins- Coin 2 shows tails:
(1 - 0.6) Ă f[1][0] = 0.4 Ă 0.6 = 0.24
- Coin 2 shows tails:
f[2][1]
: Get 1 head with 2 coins- Coin 2 shows tails:
(1 - 0.6) Ă f[1][1] = 0.4 Ă 0.4 = 0.16
- Coin 2 shows heads:
0.6 Ă f[1][0] = 0.6 Ă 0.6 = 0.36
- Total:
0.16 + 0.36 = 0.52
- Coin 2 shows tails:
f[2][2]
: Get 2 heads with 2 coins- Coin 2 shows tails:
(1 - 0.6) Ă f[1][2] = 0.4 Ă 0 = 0
- Coin 2 shows heads:
0.6 Ă f[1][1] = 0.6 Ă 0.4 = 0.24
- Total:
0 + 0.24 = 0.24
- Coin 2 shows tails:
Processing Coin 3 (prob = 0.5):
f[3][0]
: Get 0 heads with 3 coins- Coin 3 shows tails:
(1 - 0.5) Ă f[2][0] = 0.5 Ă 0.24 = 0.12
- Coin 3 shows tails:
f[3][1]
: Get 1 head with 3 coins- Coin 3 shows tails:
(1 - 0.5) Ă f[2][1] = 0.5 Ă 0.52 = 0.26
- Coin 3 shows heads:
0.5 Ă f[2][0] = 0.5 Ă 0.24 = 0.12
- Total:
0.26 + 0.12 = 0.38
- Coin 3 shows tails:
f[3][2]
: Get 2 heads with 3 coins- Coin 3 shows tails:
(1 - 0.5) Ă f[2][2] = 0.5 Ă 0.24 = 0.12
- Coin 3 shows heads:
0.5 Ă f[2][1] = 0.5 Ă 0.52 = 0.26
- Total:
0.12 + 0.26 = 0.38
- Coin 3 shows tails:
Final Answer: f[3][2] = 0.38
The probability of getting exactly 2 heads when tossing all 3 coins is 0.38 or 38%.
Solution Implementation
1class Solution:
2 def probabilityOfHeads(self, prob: List[float], target: int) -> float:
3 """
4 Calculate the probability of getting exactly 'target' heads when flipping coins.
5
6 Args:
7 prob: List of probabilities where prob[i] is the probability of i-th coin showing heads
8 target: The desired number of heads
9
10 Returns:
11 The probability of getting exactly 'target' heads
12 """
13 num_coins = len(prob)
14
15 # dp[i][j] represents the probability of getting exactly j heads using first i coins
16 # Initialize with dimensions (num_coins + 1) x (target + 1)
17 dp = [[0.0] * (target + 1) for _ in range(num_coins + 1)]
18
19 # Base case: probability of getting 0 heads with 0 coins is 1
20 dp[0][0] = 1.0
21
22 # Fill the DP table
23 for i in range(1, num_coins + 1):
24 current_coin_prob = prob[i - 1] # Probability of current coin showing heads
25
26 # j represents the number of heads we want
27 # We can have at most min(i, target) heads with i coins
28 for j in range(min(i, target) + 1):
29 # Case 1: Current coin shows tails
30 # Probability = (1 - current_coin_prob) * probability of j heads from previous coins
31 dp[i][j] = (1 - current_coin_prob) * dp[i - 1][j]
32
33 # Case 2: Current coin shows heads (only if j > 0)
34 # Probability = current_coin_prob * probability of (j-1) heads from previous coins
35 if j > 0:
36 dp[i][j] += current_coin_prob * dp[i - 1][j - 1]
37
38 # Return the probability of getting exactly 'target' heads with all coins
39 return dp[num_coins][target]
40
1class Solution {
2 public double probabilityOfHeads(double[] prob, int target) {
3 int numCoins = prob.length;
4
5 // dp[i][j] represents the probability of getting exactly j heads
6 // after flipping the first i coins
7 double[][] dp = new double[numCoins + 1][target + 1];
8
9 // Base case: probability of getting 0 heads with 0 coins is 1
10 dp[0][0] = 1.0;
11
12 // Fill the DP table
13 for (int coinIndex = 1; coinIndex <= numCoins; coinIndex++) {
14 // For each coin, calculate probability for all possible head counts
15 // We can have at most min(coinIndex, target) heads
16 for (int headCount = 0; headCount <= Math.min(coinIndex, target); headCount++) {
17 // Case 1: Current coin shows tails
18 // Probability = (1 - prob[coinIndex-1]) * probability of getting headCount heads from previous coins
19 dp[coinIndex][headCount] = (1 - prob[coinIndex - 1]) * dp[coinIndex - 1][headCount];
20
21 // Case 2: Current coin shows heads (only if we need at least 1 head)
22 // Probability = prob[coinIndex-1] * probability of getting (headCount-1) heads from previous coins
23 if (headCount > 0) {
24 dp[coinIndex][headCount] += prob[coinIndex - 1] * dp[coinIndex - 1][headCount - 1];
25 }
26 }
27 }
28
29 // Return the probability of getting exactly target heads after flipping all coins
30 return dp[numCoins][target];
31 }
32}
33
1class Solution {
2public:
3 double probabilityOfHeads(vector<double>& prob, int target) {
4 int n = prob.size();
5
6 // dp[i][j] represents the probability of getting exactly j heads
7 // after flipping the first i coins
8 vector<vector<double>> dp(n + 1, vector<double>(target + 1, 0.0));
9
10 // Base case: probability of getting 0 heads with 0 coins is 1
11 dp[0][0] = 1.0;
12
13 // Iterate through each coin
14 for (int i = 1; i <= n; ++i) {
15 // Iterate through possible number of heads (up to min of coins flipped or target)
16 for (int j = 0; j <= min(i, target); ++j) {
17 // Case 1: Current coin shows tails
18 // Probability = (1 - prob[i-1]) * probability of j heads from previous i-1 coins
19 dp[i][j] = (1.0 - prob[i - 1]) * dp[i - 1][j];
20
21 // Case 2: Current coin shows heads (only if j > 0)
22 // Probability = prob[i-1] * probability of (j-1) heads from previous i-1 coins
23 if (j > 0) {
24 dp[i][j] += prob[i - 1] * dp[i - 1][j - 1];
25 }
26 }
27 }
28
29 // Return the probability of getting exactly 'target' heads after flipping all n coins
30 return dp[n][target];
31 }
32};
33
1/**
2 * Calculates the probability of getting exactly 'target' heads when tossing n coins
3 * @param prob - Array where prob[i] represents the probability of the i-th coin landing heads
4 * @param target - The exact number of heads we want to achieve
5 * @returns The probability of getting exactly 'target' heads
6 */
7function probabilityOfHeads(prob: number[], target: number): number {
8 const numCoins: number = prob.length;
9
10 // dp[i][j] represents the probability of getting exactly j heads using the first i coins
11 // Initialize 2D array with zeros
12 const dp: number[][] = new Array(numCoins + 1)
13 .fill(0)
14 .map(() => new Array(target + 1).fill(0));
15
16 // Base case: probability of getting 0 heads with 0 coins is 1
17 dp[0][0] = 1;
18
19 // Iterate through each coin
20 for (let coinIndex: number = 1; coinIndex <= numCoins; ++coinIndex) {
21 // Iterate through all possible head counts from 0 to target
22 for (let headCount: number = 0; headCount <= target; ++headCount) {
23 // Case 1: Current coin lands tails
24 // Probability = previous state * (1 - probability of heads for current coin)
25 dp[coinIndex][headCount] = dp[coinIndex - 1][headCount] * (1 - prob[coinIndex - 1]);
26
27 // Case 2: Current coin lands heads (only if headCount > 0)
28 // Probability = previous state with one less head * probability of heads for current coin
29 if (headCount > 0) {
30 dp[coinIndex][headCount] += dp[coinIndex - 1][headCount - 1] * prob[coinIndex - 1];
31 }
32 }
33 }
34
35 // Return the probability of getting exactly 'target' heads using all coins
36 return dp[numCoins][target];
37}
38
Time and Space Complexity
The time complexity of this code is O(n Ă target)
, where n
is the number of coins (length of the prob
array). This is because we have two nested loops: the outer loop iterates through all n
coins, and the inner loop iterates up to min(i, target) + 1
times, which in the worst case is target + 1
times. Each operation inside the nested loops takes constant time.
The space complexity as implemented in the code is O(n Ă (target + 1))
or simply O(n Ă target)
, since we create a 2D array f
with dimensions (n + 1) Ă (target + 1)
. However, the reference answer mentions O(target)
space complexity, which would be achievable through space optimization. Since we only need the previous row f[i-1]
to compute the current row f[i]
, we could optimize the space by using only two 1D arrays of size target + 1
(or even just one array with careful updating from right to left), reducing the space complexity to O(target)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Floating Point Precision Errors
When dealing with probability calculations involving multiple multiplications of decimal values, floating-point precision errors can accumulate, especially when the number of coins is large or probabilities are very small.
Problem Example:
# With many coins, small probabilities compound prob = [0.0001] * 100 target = 50 # The result might have significant precision errors
Solution:
- Use Python's
decimal
module for higher precision when needed - Consider working with log probabilities for very small values
- Round the final result to a reasonable number of decimal places
from decimal import Decimal, getcontext
class Solution:
def probabilityOfHeads(self, prob: List[float], target: int) -> float:
# Set higher precision if needed
getcontext().prec = 50
num_coins = len(prob)
prob_decimal = [Decimal(str(p)) for p in prob]
dp = [[Decimal(0)] * (target + 1) for _ in range(num_coins + 1)]
dp[0][0] = Decimal(1)
for i in range(1, num_coins + 1):
current_coin_prob = prob_decimal[i - 1]
for j in range(min(i, target) + 1):
dp[i][j] = (Decimal(1) - current_coin_prob) * dp[i - 1][j]
if j > 0:
dp[i][j] += current_coin_prob * dp[i - 1][j - 1]
return float(dp[num_coins][target])
2. Memory Inefficiency with Large Inputs
The 2D DP table uses O(n Ă target) space, which can be excessive when both values are large.
Problem Example:
prob = [0.5] * 10000 target = 5000 # This creates a 10001 Ă 5001 matrix, using excessive memory
Solution: Use space-optimized DP with rolling arrays since we only need the previous row:
class Solution:
def probabilityOfHeads(self, prob: List[float], target: int) -> float:
num_coins = len(prob)
# Use only two rows instead of full 2D array
prev = [0.0] * (target + 1)
curr = [0.0] * (target + 1)
prev[0] = 1.0
for i in range(1, num_coins + 1):
current_coin_prob = prob[i - 1]
for j in range(min(i, target) + 1):
curr[j] = (1 - current_coin_prob) * prev[j]
if j > 0:
curr[j] += current_coin_prob * prev[j - 1]
# Swap arrays for next iteration
prev, curr = curr, prev
return prev[target]
3. Incorrect Loop Boundaries
A common mistake is setting incorrect upper bounds for the inner loop, either processing unnecessary states or missing valid ones.
Problem Example:
# Incorrect: Processing j up to target even when i < target
for j in range(target + 1): # Wrong!
dp[i][j] = ...
# This attempts to calculate impossible states like 5 heads with 2 coins
Solution:
Always use min(i, target)
as the upper bound:
for j in range(min(i, target) + 1): # Correct
# Process only valid states
4. Edge Case: Target Greater Than Number of Coins
If target > len(prob)
, it's impossible to get more heads than coins available.
Problem Example:
prob = [0.5, 0.5] target = 3 # Impossible to get 3 heads with 2 coins
Solution: Add an early return check:
class Solution:
def probabilityOfHeads(self, prob: List[float], target: int) -> float:
num_coins = len(prob)
# Early return for impossible cases
if target > num_coins:
return 0.0
# Rest of the implementation...
What's the output of running the following function using input 56
?
1KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13 def dfs(path, res):
14 if len(path) == len(digits):
15 res.append(''.join(path))
16 return
17
18 next_number = digits[len(path)]
19 for letter in KEYBOARD[next_number]:
20 path.append(letter)
21 dfs(path, res)
22 path.pop()
23
24 res = []
25 dfs([], res)
26 return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2 '2', "abc".toCharArray(),
3 '3', "def".toCharArray(),
4 '4', "ghi".toCharArray(),
5 '5', "jkl".toCharArray(),
6 '6', "mno".toCharArray(),
7 '7', "pqrs".toCharArray(),
8 '8', "tuv".toCharArray(),
9 '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13 List<String> res = new ArrayList<>();
14 dfs(new StringBuilder(), res, digits.toCharArray());
15 return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19 if (path.length() == digits.length) {
20 res.add(path.toString());
21 return;
22 }
23 char next_digit = digits[path.length()];
24 for (char letter : KEYBOARD.get(next_digit)) {
25 path.append(letter);
26 dfs(path, res, digits);
27 path.deleteCharAt(path.length() - 1);
28 }
29}
30
1const KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13 let res = [];
14 dfs(digits, [], res);
15 return res;
16}
17
18function dfs(digits, path, res) {
19 if (path.length === digits.length) {
20 res.push(path.join(''));
21 return;
22 }
23 let next_number = digits.charAt(path.length);
24 for (let letter of KEYBOARD[next_number]) {
25 path.push(letter);
26 dfs(digits, path, res);
27 path.pop();
28 }
29}
30
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donât Miss This!