Facebook Pixel

710. Random Pick with Blacklist

Problem Description

You need to design a random number generator that picks integers from the range [0, n-1], but excludes certain numbers specified in a blacklist.

Given:

  • An integer n representing the upper bound of the range (exclusive)
  • An array blacklist containing unique integers that should never be returned

The task requires implementing a Solution class with two methods:

  1. Constructor Solution(int n, int[] blacklist): Initializes the random number generator with the range size n and the list of forbidden numbers
  2. Pick method pick(): Returns a random integer from [0, n-1] that is not in the blacklist

Key requirements:

  • All valid numbers (those in range and not blacklisted) must have equal probability of being selected
  • The algorithm should minimize calls to the built-in random function

The solution uses a remapping strategy:

  • It calculates k as the count of valid numbers (n - len(blacklist))
  • Creates a mapping dictionary d to handle blacklisted numbers in the range [0, k-1]
  • For each blacklisted number less than k, it maps it to a valid number in the range [k, n-1]
  • When pick() is called, it generates a random number in [0, k-1] and returns either the number itself (if not blacklisted) or its mapped replacement

This approach ensures uniform distribution while only requiring one random call per pick operation.

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

Intuition

The key insight is that we want to generate random numbers efficiently without repeatedly calling random until we get a non-blacklisted number, which could be very inefficient if many numbers are blacklisted.

Think of it this way: if we have n total numbers and len(blacklist) forbidden ones, we actually have k = n - len(blacklist) valid numbers to choose from. The challenge is how to uniformly select from these k valid numbers.

The clever idea is to create a "virtual" array of size k where all elements are valid. We can generate a random index in [0, k-1] and map it to an actual valid number. But we don't want to physically create this array as it could be huge.

Here's the mapping strategy:

  1. Divide the original range [0, n-1] into two parts: [0, k-1] and [k, n-1]
  2. Some blacklisted numbers fall in [0, k-1] and some in [k, n-1]
  3. The blacklisted numbers in [k, n-1] are already "out of the way" - we'll never generate them since we only pick from [0, k-1]
  4. For blacklisted numbers in [0, k-1], we need to swap them with valid numbers from [k, n-1]

This is like having a deck of cards where we move all the "bad" cards to the bottom. When we draw from the top k cards, if we would draw a bad card that's still in the top portion, we have a pre-computed mapping that tells us which good card from the bottom to use instead.

The beauty is that this requires only one random call per pick() operation - we generate a random number in [0, k-1] and either use it directly or look up its replacement in our mapping dictionary.

Learn more about Math, Binary Search and Sorting patterns.

Solution Approach

Let's walk through the implementation step by step:

Initialization (__init__ method):

  1. Calculate valid count: First, we compute k = n - len(blacklist), which represents the total number of valid (non-blacklisted) integers we can return.

  2. Create mapping dictionary: Initialize an empty dictionary d that will store mappings for blacklisted numbers in the range [0, k-1].

  3. Convert blacklist to set: Create a set black from the blacklist for O(1) lookup time when checking if a number is blacklisted.

  4. Build the remapping:

    • Start with a pointer i = k (beginning of the second half of our range)
    • For each blacklisted number b:
      • If b < k (it's in the first half where we'll generate random numbers):
        • Find the next valid number starting from i: Keep incrementing i while i is also blacklisted
        • Map b to this valid number: self.d[b] = i
        • Increment i for the next potential mapping

Pick method (pick):

  1. Generate random index: Call randrange(self.k) to get a random number x in [0, k-1].

  2. Return mapped value: Use self.d.get(x, x) which:

    • Returns self.d[x] if x is a blacklisted number that we've mapped
    • Returns x itself if it's not in the mapping (meaning it's already a valid number)

Example walkthrough: If n = 7 and blacklist = [2, 3, 5]:

  • k = 7 - 3 = 4 (we have 4 valid numbers)
  • Valid numbers are: [0, 1, 4, 6]
  • Blacklisted numbers in [0, 3]: 2 and 3
  • Valid numbers in [4, 6]: 4 and 6
  • Mappings created: {2: 4, 3: 6}
  • When pick() generates:
    • 0 → returns 0 (not mapped)
    • 1 → returns 1 (not mapped)
    • 2 → returns 4 (mapped)
    • 3 → returns 6 (mapped)

This ensures uniform distribution across all valid numbers with only one random call per pick.

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 concrete example with n = 10 and blacklist = [3, 5, 8, 9].

Initialization Phase:

  1. Calculate valid count: k = 10 - 4 = 6 (we have 6 valid numbers to choose from)

  2. Our valid numbers are: [0, 1, 2, 4, 6, 7]

  3. Split our perspective:

    • Range we'll generate from: [0, 5] (indices 0 through k-1)
    • Range we'll remap to if needed: [6, 9] (indices k through n-1)
  4. Identify problematic blacklisted numbers:

    • Blacklisted in [0, 5]: 3 and 5 (these need remapping)
    • Blacklisted in [6, 9]: 8 and 9 (already out of the way)
  5. Build remapping:

    • Start with i = 6
    • Process b = 3:
      • Since 3 < 6, we need to map it
      • Check i = 6: not blacklisted ✓
      • Map: d[3] = 6
      • Increment: i = 7
    • Process b = 5:
      • Since 5 < 6, we need to map it
      • Check i = 7: not blacklisted ✓
      • Map: d[5] = 7
      • Increment: i = 8
    • Process b = 8: Since 8 ≥ 6, skip (no mapping needed)
    • Process b = 9: Since 9 ≥ 6, skip (no mapping needed)

Final mapping dictionary: {3: 6, 5: 7}

Pick Phase:

When pick() is called, it generates a random number in [0, 5]:

  • If it generates 0 → returns 0 (valid, not mapped)
  • If it generates 1 → returns 1 (valid, not mapped)
  • If it generates 2 → returns 2 (valid, not mapped)
  • If it generates 3 → returns 6 (using mapping d[3] = 6)
  • If it generates 4 → returns 4 (valid, not mapped)
  • If it generates 5 → returns 7 (using mapping d[5] = 7)

Each of the 6 valid numbers [0, 1, 2, 4, 6, 7] has exactly 1/6 probability of being returned, achieving perfect uniform distribution with just one random call per pick operation.

Solution Implementation

1from typing import List
2from random import randrange
3
4class Solution:
5    def __init__(self, n: int, blacklist: List[int]):
6        # Calculate the size of the valid range [0, k) where we'll pick from
7        # All blacklisted numbers will be remapped if they fall in this range
8        self.valid_range_size = n - len(blacklist)
9      
10        # Dictionary to remap blacklisted numbers in [0, k) to valid numbers in [k, n)
11        self.remap_dict = {}
12      
13        # Start searching for replacement values from position k
14        replacement_index = self.valid_range_size
15      
16        # Convert blacklist to set for O(1) lookup
17        blacklist_set = set(blacklist)
18      
19        # Process each blacklisted number
20        for blacklisted_num in blacklist:
21            # Only remap if the blacklisted number is in our picking range [0, k)
22            if blacklisted_num < self.valid_range_size:
23                # Find the next non-blacklisted number in range [k, n)
24                while replacement_index in blacklist_set:
25                    replacement_index += 1
26              
27                # Map the blacklisted number to a valid replacement
28                self.remap_dict[blacklisted_num] = replacement_index
29                replacement_index += 1
30
31    def pick(self) -> int:
32        # Pick a random number from [0, k)
33        random_num = randrange(self.valid_range_size)
34      
35        # If it's a blacklisted number (exists in remap dict), return its replacement
36        # Otherwise, return the number itself
37        return self.remap_dict.get(random_num, random_num)
38
39
40# Your Solution object will be instantiated and called as such:
41# obj = Solution(n, blacklist)
42# param_1 = obj.pick()
43
1class Solution {
2    // Map to store remapping of blacklisted numbers in range [0, k) to valid numbers in range [k, n)
3    private Map<Integer, Integer> remapping = new HashMap<>();
4    // Random number generator
5    private Random random = new Random();
6    // Size of the valid range [0, k) where k = n - blacklist.length
7    private int validRangeSize;
8
9    /**
10     * Constructor initializes the data structure with total range n and blacklist.
11     * Strategy: We want to pick uniformly from [0, n) excluding blacklist.
12     * We map the valid range to [0, k) where k = n - blacklist.length.
13     * Any blacklisted number in [0, k) is remapped to a non-blacklisted number in [k, n).
14     * 
15     * @param n Total range size [0, n)
16     * @param blacklist Array of blacklisted numbers to exclude
17     */
18    public Solution(int n, int[] blacklist) {
19        // Calculate the size of valid numbers (total - blacklisted)
20        validRangeSize = n - blacklist.length;
21      
22        // Create a set of blacklisted numbers for O(1) lookup
23        Set<Integer> blacklistSet = new HashSet<>();
24        for (int blacklistedNum : blacklist) {
25            blacklistSet.add(blacklistedNum);
26        }
27      
28        // Start searching for valid replacement numbers from position k
29        int replacementIndex = validRangeSize;
30      
31        // Remap blacklisted numbers in range [0, k) to valid numbers in range [k, n)
32        for (int blacklistedNum : blacklist) {
33            // Only need to remap if the blacklisted number is in our target range [0, k)
34            if (blacklistedNum < validRangeSize) {
35                // Find the next non-blacklisted number in range [k, n)
36                while (blacklistSet.contains(replacementIndex)) {
37                    replacementIndex++;
38                }
39                // Map the blacklisted number to a valid replacement
40                remapping.put(blacklistedNum, replacementIndex);
41                replacementIndex++;
42            }
43        }
44    }
45
46    /**
47     * Returns a random integer from the range [0, n) that is not in the blacklist.
48     * Picks uniformly at random from all valid numbers.
49     * 
50     * @return A random non-blacklisted integer
51     */
52    public int pick() {
53        // Generate random number in range [0, k)
54        int randomValue = random.nextInt(validRangeSize);
55        // If the number was remapped (was blacklisted), return its mapping; otherwise return itself
56        return remapping.getOrDefault(randomValue, randomValue);
57    }
58}
59
60/**
61 * Your Solution object will be instantiated and called as such:
62 * Solution obj = new Solution(n, blacklist);
63 * int param_1 = obj.pick();
64 */
65
1class Solution {
2public:
3    // Map to store remapping of blacklisted numbers in range [0, k)
4    unordered_map<int, int> remapping;
5    // Number of valid (non-blacklisted) numbers
6    int validCount;
7
8    Solution(int n, vector<int>& blacklist) {
9        // Calculate the number of valid numbers (total - blacklisted)
10        validCount = n - blacklist.size();
11      
12        // Starting position for finding replacement values
13        int replacementIndex = validCount;
14      
15        // Convert blacklist to set for O(1) lookup
16        unordered_set<int> blacklistSet(blacklist.begin(), blacklist.end());
17      
18        // Remap blacklisted numbers that fall within [0, validCount)
19        for (int& blacklistedNum : blacklist) {
20            // Only remap if the blacklisted number is in our pick range
21            if (blacklistedNum < validCount) {
22                // Find the next non-blacklisted number from [validCount, n)
23                while (blacklistSet.count(replacementIndex)) {
24                    ++replacementIndex;
25                }
26                // Map the blacklisted number to a valid number outside pick range
27                remapping[blacklistedNum] = replacementIndex;
28                ++replacementIndex;
29            }
30        }
31    }
32
33    int pick() {
34        // Generate random number in range [0, validCount)
35        int randomNum = rand() % validCount;
36      
37        // If the number is remapped (was blacklisted), return the mapped value
38        // Otherwise, return the number itself
39        return remapping.count(randomNum) ? remapping[randomNum] : randomNum;
40    }
41};
42
43/**
44 * Your Solution object will be instantiated and called as such:
45 * Solution* obj = new Solution(n, blacklist);
46 * int param_1 = obj->pick();
47 */
48
1// Map to store remapping of blacklisted numbers in range [0, validCount)
2let remapping: Map<number, number>;
3// Number of valid (non-blacklisted) numbers
4let validCount: number;
5
6function Solution(n: number, blacklist: number[]): void {
7    // Calculate the number of valid numbers (total - blacklisted)
8    validCount = n - blacklist.length;
9  
10    // Initialize the remapping map
11    remapping = new Map<number, number>();
12  
13    // Starting position for finding replacement values
14    let replacementIndex = validCount;
15  
16    // Convert blacklist to set for O(1) lookup
17    const blacklistSet = new Set<number>(blacklist);
18  
19    // Remap blacklisted numbers that fall within [0, validCount)
20    for (const blacklistedNum of blacklist) {
21        // Only remap if the blacklisted number is in our pick range
22        if (blacklistedNum < validCount) {
23            // Find the next non-blacklisted number from [validCount, n)
24            while (blacklistSet.has(replacementIndex)) {
25                replacementIndex++;
26            }
27            // Map the blacklisted number to a valid number outside pick range
28            remapping.set(blacklistedNum, replacementIndex);
29            replacementIndex++;
30        }
31    }
32}
33
34function pick(): number {
35    // Generate random number in range [0, validCount)
36    const randomNum = Math.floor(Math.random() * validCount);
37  
38    // If the number is remapped (was blacklisted), return the mapped value
39    // Otherwise, return the number itself
40    return remapping.has(randomNum) ? remapping.get(randomNum)! : randomNum;
41}
42
43/**
44 * Your Solution object will be instantiated and called as such:
45 * Solution(n, blacklist);
46 * const param_1 = pick();
47 */
48

Time and Space Complexity

Time Complexity:

  • Initialization (__init__): O(B) where B is the length of the blacklist.

    • Creating a set from blacklist: O(B)
    • Iterating through each element in blacklist: O(B)
    • For each blacklisted number less than k, we iterate through the while loop. In the worst case, we check at most B blacklisted numbers in the range [k, n), so the while loop runs O(B) times total across all iterations.
    • Overall initialization: O(B)
  • Pick method: O(1)

    • Generating a random number: O(1)
    • Dictionary lookup: O(1) average case
    • Return operation: O(1)

Space Complexity: O(B)

  • The dictionary self.d stores at most B mappings (one for each blacklisted number that is less than k)
  • The set black stores B elements
  • Additional variables (k, i, etc.) use O(1) space
  • Overall space complexity: O(B)

The algorithm uses a remapping strategy where it maps blacklisted numbers in the range [0, k) to valid numbers in the range [k, n), where k = n - len(blacklist). This allows for constant-time random selection from the valid numbers.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Forgetting to Skip Already Blacklisted Replacement Values

The Pitfall: A common mistake is not checking if the replacement index itself is blacklisted when building the remapping dictionary. If you simply map blacklisted numbers in [0, k) to consecutive numbers starting from k, you might accidentally map to another blacklisted number.

Incorrect Implementation:

def __init__(self, n: int, blacklist: List[int]):
    self.valid_range_size = n - len(blacklist)
    self.remap_dict = {}
    replacement_index = self.valid_range_size
  
    for blacklisted_num in blacklist:
        if blacklisted_num < self.valid_range_size:
            # WRONG: Not checking if replacement_index is also blacklisted!
            self.remap_dict[blacklisted_num] = replacement_index
            replacement_index += 1

Why it fails: If n = 7 and blacklist = [2, 3, 5], this would map 2 → 4 and 3 → 5. But 5 is also blacklisted! This would cause pick() to potentially return 5, violating the constraint.

Solution: Always check if the replacement index is in the blacklist set before using it:

while replacement_index in blacklist_set:
    replacement_index += 1
self.remap_dict[blacklisted_num] = replacement_index

2. Not Converting Blacklist to Set for Efficient Lookups

The Pitfall: Using the blacklist array directly for membership checking instead of converting it to a set first.

Inefficient Implementation:

def __init__(self, n: int, blacklist: List[int]):
    self.valid_range_size = n - len(blacklist)
    self.remap_dict = {}
    replacement_index = self.valid_range_size
  
    for blacklisted_num in blacklist:
        if blacklisted_num < self.valid_range_size:
            # INEFFICIENT: O(m) lookup for each check where m = len(blacklist)
            while replacement_index in blacklist:  
                replacement_index += 1
            self.remap_dict[blacklisted_num] = replacement_index
            replacement_index += 1

Why it's problematic: This creates O(m²) time complexity in the worst case, where m is the blacklist size. Each in blacklist check takes O(m) time.

Solution: Convert to a set first for O(1) lookups:

blacklist_set = set(blacklist)
while replacement_index in blacklist_set:  # O(1) lookup
    replacement_index += 1

3. Mapping All Blacklisted Numbers Instead of Only Those in [0, k)

The Pitfall: Creating unnecessary mappings for blacklisted numbers that are already outside the picking range.

Wasteful Implementation:

def __init__(self, n: int, blacklist: List[int]):
    self.valid_range_size = n - len(blacklist)
    self.remap_dict = {}
  
    for blacklisted_num in blacklist:
        # WRONG: Mapping even numbers >= k which will never be picked
        self.remap_dict[blacklisted_num] = some_valid_number

Why it's problematic: Since pick() only generates numbers in [0, k), blacklisted numbers >= k will never be generated anyway. Creating mappings for them wastes memory and initialization time.

Solution: Only create mappings for blacklisted numbers less than k:

if blacklisted_num < self.valid_range_size:
    # Only then create the mapping
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

The three-steps of Depth First Search are:

  1. Identify states;
  2. Draw the state-space tree;
  3. DFS on the state-space tree.

Recommended Readings

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

Load More