Random Pick with Blacklist
HardPrompt
You are given an integer n and an array of unique integers blacklist. Design an algorithm to pick a random integer in the range [0, n - 1] that is not in blacklist. Any integer that is in the mentioned range and not in blacklist should be equally likely to be returned.
Optimize your algorithm such that it minimizes the number of calls to the built-in random function of your language.
Implement the Solution class:
Solution(int n, int[] blacklist)Initializes the object with the integernand the blacklisted integersblacklist.int pick()Returns a random integer in the range[0, n - 1]and not inblacklist.
Example 1:
Input
["Solution", "pick", "pick", "pick", "pick", "pick", "pick", "pick"]
[[7, [2, 3, 5]], [], [], [], [], [], [], []]
Output
[null, 0, 4, 1, 6, 1, 0, 4]
Explanation
Solution solution = new Solution(7, [2, 3, 5]);
solution.pick(); // return 0, any integer from [0,1,4,6] should be ok. Note that for every call of pick,
// 0, 1, 4, and 6 must be equally likely to be returned (i.e., with probability 1/4).
solution.pick(); // return 4
solution.pick(); // return 1
solution.pick(); // return 6
solution.pick(); // return 1
solution.pick(); // return 0
solution.pick(); // return 4
Constraints:
1 <= n <= 1090 <= blacklist.length <= min(105, n - 1)0 <= blacklist[i] < n- All the values of
blacklistare unique. - At most
2 * 104calls will be made topick.
Approaches
3 approaches with complexity analysis and trade-offs.
The most straightforward idea is to generate a list of all valid numbers (the "whitelist") during initialization. The pick() method then simply selects a random element from this pre-computed list.
Algorithm
- Initialize a
HashSetfrom theblacklistarray. - Initialize an
ArrayListwhitelist. - Loop from
i = 0ton-1. Ifiis not in theblacklistset, add it towhitelist. - To
pick, generate a random index up towhitelist.size()and return the element at that index.
Walkthrough
In this approach, we first build a complete list of all numbers that are not in the blacklist. This is done once during the initialization of the Solution object.
Constructor (Solution(n, blacklist))
- First, we convert the
blacklistarray into aHashSetfor efficient O(1) average time lookups. This helps in quickly checking if a number is blacklisted. - We create a new
ArrayListto store the valid numbers, let's call itwhitelist. - We then iterate through all numbers from
0ton-1. - For each number
i, we check if it exists in theblacklistHashSet. - If
iis not in theblacklist, we add it to ourwhitelistArrayList.
pick() Method
- The size of the
whitelistisW, which isn - blacklist.length. - We generate a random integer
indexin the range[0, W-1]. - We return the element at
whitelist.get(index). All valid numbers are in the list, so picking a random index gives a uniform probability to each valid number.
This approach is simple but fundamentally flawed by the problem's constraints, where n can be up to 10<sup>9</sup>.
class Solution { private List<Integer> whitelist; private Random rand; public Solution(int n, int[] blacklist) { this.whitelist = new ArrayList<>(); this.rand = new Random(); Set<Integer> blacklistSet = new HashSet<>(); for (int b : blacklist) { blacklistSet.add(b); } for (int i = 0; i < n; i++) { if (!blacklistSet.contains(i)) { this.whitelist.add(i); } } } public int pick() { int randomIndex = rand.nextInt(this.whitelist.size()); return this.whitelist.get(randomIndex); }}Complexity
Time
Constructor: `O(n + B)`. The loop runs `n` times. Given `n` can be up to 10^9, this is too slow and will cause a "Time Limit Exceeded" error. `pick()`: `O(1)`.
Space
`O(n - B)` to store the `whitelist`, where `B` is the blacklist size. Since `n` can be 10^9, this will cause an "Out of Memory" error.
Trade-offs
Pros
The
pick()operation is very fast, O(1).The logic is simple to understand.
Cons
Extremely high time complexity in the constructor, making it infeasible for large
n.Extremely high space complexity, making it infeasible for large
n.
This approach involves generating a random number in the full range [0, n-1] and checking if it's in the blacklist. If it is, we "reject" it and try again. We repeat this process until we find a number that is not blacklisted.
Algorithm
- Store
nand convertblacklistto aHashSet. - In a loop, generate a random number
rfrom[0, n-1]. - If
ris not in theblacklistset, returnr. - Otherwise, repeat the process.
Walkthrough
This method uses a probabilistic technique called rejection sampling. Instead of pre-calculating the valid numbers, we generate a random number from the entire range and check its validity on the fly.
Constructor (Solution(n, blacklist))
- Store the value of
n. - Convert the
blacklistarray into aHashSetfor efficient O(1) average time lookups. This is crucial for the performance of thepick()method.
pick() Method
- Enter a loop that continues until a valid number is found.
- Inside the loop, generate a random integer
candidatein the range[0, n-1]. - Check if the
candidateis present in theblacklistHashSet. - If it's not in the
blacklist, it's a valid number. Return thecandidateand exit the loop. - If it is in the
blacklist, the loop continues, and a new number is generated in the next iteration.
Each number in [0, n-1] has an equal chance of being generated. By only returning the non-blacklisted ones, we ensure that every valid number has an equal probability of being chosen.
class Solution { private int n; private Set<Integer> blacklistSet; private Random rand; public Solution(int n, int[] blacklist) { this.n = n; this.rand = new Random(); this.blacklistSet = new HashSet<>(); for (int b : blacklist) { this.blacklistSet.add(b); } } public int pick() { while (true) { int candidate = rand.nextInt(n); if (!blacklistSet.contains(candidate)) { return candidate; } } }}Complexity
Time
Constructor: `O(B)` to create the `HashSet`. `pick()`: The expected time complexity is `O(n / (n - B))`. If `B` is much smaller than `n`, this is close to O(1). However, if `B` is close to `n`, this can be very large, potentially leading to a "Time Limit Exceeded" error.
Space
`O(B)` to store the `blacklist` in a `HashSet`, where `B` is the blacklist size.
Trade-offs
Pros
Relatively simple to implement.
Low memory usage compared to the whitelist approach.
Fast constructor.
Cons
The
pick()method's performance is unpredictable and can be very slow if the density of blacklisted numbers is high.It does not guarantee minimizing the calls to the random function, which is a requirement of the problem.
This is the optimal approach that addresses the shortcomings of the previous methods, especially the large value of n. The core idea is to remap blacklisted numbers that fall within an initial, smaller valid range to valid numbers that fall outside this range. This allows us to pick a random number from a contiguous range and get a valid, uniformly distributed result with a single random call.
Algorithm
- Calculate
W = n - blacklist.length. - Partition the
blacklistinto numbers< Wand numbers>= W. Store the latter in aHashSet. - Initialize a pointer
p = W. - For each blacklisted number
b < W, find the smallest numberv >= pthat is not in the blacklist set. Mapbtovand updatep = v + 1. - To
pick, generate a random numberkin[0, W-1]. Ifkis in the map, return its mapped value. Otherwise, returnk.
Walkthrough
Let B be the size of the blacklist. The number of whitelisted (valid) integers is W = n - B. Our goal is to pick a random number from these W valid integers. We can do this by generating a random index k in the range [0, W-1] and mapping this index to a unique valid number.
The numbers in the range [0, W-1] are our initial candidates. However, some of these candidates might be in the blacklist. The key insight is that the number of blacklisted values in [0, W-1] is equal to the number of whitelisted values in [W, n-1]. We can therefore create a mapping: for every blacklisted number b < W, we find a unique whitelisted number v >= W and map b to v.
Constructor (Solution(n, blacklist)) Algorithm:
- Calculate the size of the whitelist:
W = n - blacklist.length. - Create a
HashSet,tailBlacklist, containing all blacklisted numbers that are greater than or equal toW. This helps us quickly find valid numbers in the tail section[W, n-1]. - Create a
HashMap,mapping, to store the remappings. - Initialize a pointer
ptoW. This pointer will scan for available whitelisted numbers in the tail section. - Iterate through each number
bin the originalblacklist. Ifbis less thanW, we need to remap it. - To find a replacement for
b, we use our pointerp. We advancepuntil we find a number that is not intailBlacklist. - Once we find such a valid number
p, we create the mappingmapping.put(b, p)and incrementp.
pick() Method Algorithm:
- Generate a random integer
kin the range[0, W-1]. - If
kis a key in ourmapping, it meanskwas a blacklisted number. We return its mapped value:mapping.get(k). - Otherwise,
kwas a valid number in[0, W-1]to begin with, so we returnk.
This ensures that any pick from [0, W-1] results in a unique valid number, and all W valid numbers have an equal chance of being chosen.
class Solution { private Map<Integer, Integer> mapping; private int whitelistSize; private Random rand; public Solution(int n, int[] blacklist) { this.mapping = new HashMap<>(); this.rand = new Random(); this.whitelistSize = n - blacklist.length; // Store blacklisted numbers >= whitelistSize in a set for fast lookup. Set<Integer> tailBlacklist = new HashSet<>(); for (int b : blacklist) { if (b >= this.whitelistSize) { tailBlacklist.add(b); } } int p = this.whitelistSize; for (int b : blacklist) { if (b < this.whitelistSize) { // Find the next available whitelisted number in the tail. while (tailBlacklist.contains(p)) { p++; } // Map the blacklisted number `b` to the whitelisted number `p`. this.mapping.put(b, p); p++; } } } public int pick() { int k = rand.nextInt(this.whitelistSize); // If k is a remapped blacklisted number, return its new value. // Otherwise, k is a valid number itself. return this.mapping.getOrDefault(k, k); }}Complexity
Time
Constructor: `O(B)`, where `B` is the length of the `blacklist`. We iterate through the blacklist a constant number of times. The `while` loop's pointer `p` only ever increases and is incremented at most `B` times in total. Thus, the total time is `O(B)`. `pick()`: `O(1)` on average, as it involves a random number generation and a hash map lookup.
Space
`O(B)` to store the mapping and the set of tail-end blacklisted numbers. The size of the map is at most `B`.
Trade-offs
Pros
Optimal
pick()performance:O(1)time.Guarantees a single call to the random number generator per
pick().Efficient constructor
O(B)that handles largencorrectly.Memory usage is proportional to the size of the blacklist, not
n.
Cons
The logic is more complex to reason about and implement correctly compared to simpler approaches.
Solutions
Solution
class Solution { private Map < Integer , Integer > d = new HashMap <>(); private Random rand = new Random (); private int k ; public Solution ( int n , int [] blacklist ) { k = n - blacklist . length ; int i = k ; Set < Integer > black = new HashSet <>(); for ( int b : blacklist ) { black . add ( b ); } for ( int b : blacklist ) { if ( b < k ) { while ( black . contains ( i )) { ++ i ; } d . put ( b , i ++); } } } public int pick () { int x = rand . nextInt ( k ); return d . getOrDefault ( x , x ); } } /** * Your Solution object will be instantiated and called as such: * Solution obj = new Solution(n, blacklist); * int param_1 = obj.pick(); */class Solution { public: unordered_map < int , int > d ; int k ; Solution ( int n , vector < int >& blacklist ) { k = n - blacklist . size (); int i = k ; unordered_set < int > black ( blacklist . begin (), blacklist . end ()); for ( int & b : blacklist ) { if ( b < k ) { while ( black . count ( i )) ++ i ; d [ b ] = i ++ ; } } } int pick () { int x = rand () % k ; return d . count ( x ) ? d [ x ] : x ; } }; /** * Your Solution object will be instantiated and called as such: * Solution* obj = new Solution(n, blacklist); * int param_1 = obj->pick(); */class Solution : def __init__ ( self , n : int , blacklist : List [ int ]): self . k = n - len ( blacklist ) self . d = {} i = self . k black = set ( blacklist ) for b in blacklist : if b < self . k : while i in black : i += 1 self . d [ b ] = i i += 1 def pick ( self ) -> int : x = randrange ( self . k ) return self . d . get ( x , x ) # Your Solution object will be instantiated and called as such: # obj = Solution(n, blacklist) # param_1 = obj.pick()Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.