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:
- Constructor
Solution(int n, int[] blacklist)
: Initializes the random number generator with the range sizen
and the list of forbidden numbers - 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.
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:
- Divide the original range
[0, n-1]
into two parts:[0, k-1]
and[k, n-1]
- Some blacklisted numbers fall in
[0, k-1]
and some in[k, n-1]
- 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]
- 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):
-
Calculate valid count: First, we compute
k = n - len(blacklist)
, which represents the total number of valid (non-blacklisted) integers we can return. -
Create mapping dictionary: Initialize an empty dictionary
d
that will store mappings for blacklisted numbers in the range[0, k-1]
. -
Convert blacklist to set: Create a set
black
from the blacklist for O(1) lookup time when checking if a number is blacklisted. -
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 incrementingi
whilei
is also blacklisted - Map
b
to this valid number:self.d[b] = i
- Increment
i
for the next potential mapping
- Find the next valid number starting from
- If
- Start with a pointer
Pick method (pick
):
-
Generate random index: Call
randrange(self.k)
to get a random numberx
in[0, k-1]
. -
Return mapped value: Use
self.d.get(x, x)
which:- Returns
self.d[x]
ifx
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)
- Returns
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
and3
- Valid numbers in
[4, 6]
:4
and6
- Mappings created:
{2: 4, 3: 6}
- When
pick()
generates:0
→ returns0
(not mapped)1
→ returns1
(not mapped)2
→ returns4
(mapped)3
→ returns6
(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 EvaluatorExample Walkthrough
Let's walk through a concrete example with n = 10
and blacklist = [3, 5, 8, 9]
.
Initialization Phase:
-
Calculate valid count:
k = 10 - 4 = 6
(we have 6 valid numbers to choose from) -
Our valid numbers are:
[0, 1, 2, 4, 6, 7]
-
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)
- Range we'll generate from:
-
Identify problematic blacklisted numbers:
- Blacklisted in
[0, 5]
:3
and5
(these need remapping) - Blacklisted in
[6, 9]
:8
and9
(already out of the way)
- Blacklisted in
-
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
- Since
- Process
b = 5
:- Since
5 < 6
, we need to map it - Check
i = 7
: not blacklisted ✓ - Map:
d[5] = 7
- Increment:
i = 8
- Since
- Process
b = 8
: Since8 ≥ 6
, skip (no mapping needed) - Process
b = 9
: Since9 ≥ 6
, skip (no mapping needed)
- Start with
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
→ returns0
(valid, not mapped) - If it generates
1
→ returns1
(valid, not mapped) - If it generates
2
→ returns2
(valid, not mapped) - If it generates
3
→ returns6
(using mappingd[3] = 6
) - If it generates
4
→ returns4
(valid, not mapped) - If it generates
5
→ returns7
(using mappingd[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)
whereB
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 mostB
blacklisted numbers in the range[k, n)
, so the while loop runsO(B)
times total across all iterations. - Overall initialization:
O(B)
- Creating a set from blacklist:
-
Pick method:
O(1)
- Generating a random number:
O(1)
- Dictionary lookup:
O(1)
average case - Return operation:
O(1)
- Generating a random number:
Space Complexity: O(B)
- The dictionary
self.d
stores at mostB
mappings (one for each blacklisted number that is less thank
) - The set
black
storesB
elements - Additional variables (
k
,i
, etc.) useO(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
The three-steps of Depth First Search are:
- Identify states;
- Draw the state-space tree;
- DFS on the state-space tree.
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
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Want a Structured Path to Master System Design Too? Don’t Miss This!