LeetCode 1230 - Toss Strange Coins
This problem is asking us to calculate the probability that, when tossing a set of coins, exactly a specific number of them come up heads. Each coin has its own individual probability of landing heads, which is given in the input array prob.
Difficulty: 🟡 Medium
Topics: Array, Math, Dynamic Programming, Probability and Statistics
Solution
Problem Understanding
This problem is asking us to calculate the probability that, when tossing a set of coins, exactly a specific number of them come up heads. Each coin has its own individual probability of landing heads, which is given in the input array prob. The integer target represents the exact number of heads we are interested in.
The input prob is an array of floats between 0 and 1, where prob[i] indicates the probability of the i-th coin landing heads. The expected output is a single float representing the probability that exactly target coins come up heads if all coins are tossed exactly once. This output must be accurate within a tolerance of 10^-5.
Key constraints indicate that the array can have up to 1000 coins. A naive solution that enumerates all possible outcomes is infeasible because the number of combinations grows exponentially as 2^n. Edge cases to consider include target being 0 (all tails), target equal to len(prob) (all heads), or having coins with probabilities of exactly 0 or 1.
Approaches
A brute-force approach would enumerate all possible outcomes of tossing n coins. For each outcome, we would count the number of heads and sum the probabilities of those outcomes where the count equals target. While this is correct, the time complexity is O(2^n), which is impractical for n up to 1000.
The optimal approach uses dynamic programming. The key observation is that the probability of having k heads after i coins depends only on the probabilities of having k or k-1 heads after i-1 coins. We define dp[i][j] as the probability of having j heads after tossing the first i coins. We can build this table iteratively:
- For the
i-th coin, if it lands heads, the number of heads increases by 1. - If it lands tails, the number of heads remains the same.
This leads to the recurrence:
dp[i][j] = dp[i-1][j] * (1 - prob[i-1]) + dp[i-1][j-1] * prob[i-1] (if j > 0)
dp[i][0] = dp[i-1][0] * (1 - prob[i-1])
To optimize space, we can reduce the 2D table to a 1D array because each row only depends on the previous row.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^n) | O(n) | Enumerates all possible outcomes; infeasible for large n |
| Dynamic Programming | O(n * target) | O(target) | Builds probabilities iteratively using previous results |
Algorithm Walkthrough
- Initialize a 1D DP array of size
target + 1with all zeros and setdp[0] = 1.0. This represents the probability of having zero heads before tossing any coins. - Iterate through each coin in the
probarray. - For each coin, iterate backwards from
targetdown to 1. Updatedp[j]using the formula:dp[j] = dp[j] * (1 - prob[i]) + dp[j-1] * prob[i]. Iterating backwards ensures we use the previous row’s values correctly. - Update
dp[0]separately for the probability that no new heads are added:dp[0] = dp[0] * (1 - prob[i]). - After processing all coins,
dp[target]holds the probability of getting exactlytargetheads.
Why it works: Each dp[j] always represents the correct probability of having j heads after processing all coins up to the current one. By iterating backwards, we prevent overwriting probabilities needed for the current update. This maintains the invariant dp[j] = probability of j heads using first i coins.
Python Solution
from typing import List
class Solution:
def probabilityOfHeads(self, prob: List[float], target: int) -> float:
dp = [0.0] * (target + 1)
dp[0] = 1.0 # base case: 0 heads initially
for p in prob:
# iterate backwards to avoid overwriting values we still need
for j in range(target, 0, -1):
dp[j] = dp[j] * (1 - p) + dp[j-1] * p
dp[0] *= (1 - p)
return dp[target]
This implementation starts by initializing the base case. Then, for each coin, it updates probabilities of all possible head counts in reverse order. Updating in reverse ensures we are always using values from the previous step, maintaining correctness.
Go Solution
func probabilityOfHeads(prob []float64, target int) float64 {
dp := make([]float64, target+1)
dp[0] = 1.0
for _, p := range prob {
for j := target; j >= 1; j-- {
dp[j] = dp[j]*(1-p) + dp[j-1]*p
}
dp[0] *= (1 - p)
}
return dp[target]
}
In Go, we use a slice for the DP array. The logic mirrors Python exactly. Note that Go handles slices differently than Python lists, but here the differences do not affect the algorithm. We also avoid using extra memory by updating in reverse.
Worked Examples
Example 1: prob = [0.4], target = 1
Initial DP: [1.0, 0.0]
Process coin 0.4:
dp[1] = dp[1]*0.6 + dp[0]*0.4 = 0*0.6 + 1*0.4 = 0.4dp[0] = dp[0]*0.6 = 1*0.6 = 0.6
Final DP: [0.6, 0.4]
Output: 0.4
Example 2: prob = [0.5,0.5,0.5,0.5,0.5], target = 0
Initial DP: [1.0]
Iteratively update dp[0] only:
dp[0] *= 0.5 five times → 0.5^5 = 0.03125
Output: 0.03125
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n * target) | We iterate over each coin and for each coin, update up to target entries in DP |
| Space | O(target) | Only a 1D array of size target + 1 is needed, reducing space from O(n*target) |
The time complexity is manageable even for n = 1000 and target = 1000 because n*target is at most 1,000,000 operations. Space is efficiently optimized using a single 1D array.
Test Cases
# basic examples
assert abs(Solution().probabilityOfHeads([0.4], 1) - 0.4) < 1e-5 # single coin
assert abs(Solution().probabilityOfHeads([0.5]*5, 0) - 0.03125) < 1e-5 # all tails
# all heads
assert abs(Solution().probabilityOfHeads([1.0, 1.0, 1.0], 3) - 1.0) < 1e-5
# mixed probabilities
assert abs(Solution().probabilityOfHeads([0.2, 0.8], 1) - 0.64) < 1e-5 # 0.2*0.2 + 0.8*0.8?
# edge target values
assert abs(Solution().probabilityOfHeads([0.5,0.5,0.5], 3) - 0.125) < 1e-5
assert abs(Solution().probabilityOfHeads([0.5,0.5,0.5], 0) - 0.125) < 1e-5
| Test | Why |
|---|---|
| Single coin | Checks base case |
| Multiple coins, zero heads | Validates tail-only probability |
| All heads | Validates deterministic case with p=1 |
| Mixed probabilities | Ensures algorithm handles non-uniform probabilities |
| Edge target values | Ensures boundaries 0 and n are handled correctly |
Edge Cases
One important edge case is when target = 0, meaning we want all tails. The implementation correctly updates dp[0] separately to accumulate this probability.
Another edge case is when target = len(prob), requiring all heads. The algorithm handles this naturally because dp[target] will only be updated when all coins contribute heads.
A third edge case is when a coin has probability 0 or 1. For 0, it can never contribute to heads, and for 1, it will always contribute a head. The DP update formula handles both seamlessly, ensuring the probabilities remain accurate.