Facebook Pixel

1230. Toss Strange Coins 🔒

MediumArrayMathDynamic ProgrammingProbability and Statistics
Leetcode Link

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 to f[i][j] from f[i-1][j]
  • The coin shows heads (probability p): contributes to f[i][j] from f[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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. It shows tails - the number of heads stays the same
  2. 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 first i-1 coins and the i-th coin showed tails (probability 1 - prob[i])
  • Had j-1 heads from the first i-1 coins and the i-th coin showed heads (probability prob[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) where n 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)):

f[i][j]={(1−p)×f[i−1][j],j=0(1−p)×f[i−1][j]+p×f[i−1][j−1],j>0f[i][j] = \begin{cases} (1 - p) \times f[i - 1][j], & j = 0 \\ (1 - p) \times f[i - 1][j] + p \times f[i - 1][j - 1], & j > 0 \end{cases}

where p is prob[i-1] (the probability of the i-th coin showing heads).

Implementation Details:

  1. We iterate through each coin using enumerate(prob, 1) to get both the probability p and the 1-indexed position i
  2. 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
  3. 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]

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 Evaluator

Example 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
  • 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

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
  • 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
  • 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

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
  • 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
  • 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

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...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More