Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

710. Random Pick with Blacklist - Leetcode Solution

Problem Description

The Random Pick with Blacklist problem asks you to implement a data structure that allows for efficiently picking a random integer from the range [0, n), excluding any integers present in a given blacklist array. Every valid integer (not in the blacklist) should have an equal probability of being picked.

  • You are given two inputs: an integer n and a list blacklist of unique integers in the range [0, n).
  • You must implement a class with two methods:
    • __init__(n, blacklist): Constructor that initializes the data structure.
    • pick(): Returns a random integer in [0, n) not in the blacklist. Each valid integer must have the same probability of being returned.
  • Constraints:
    • 1 <= n <= 10^9
    • 0 <= blacklist.length < min(10^5, n - 1)
    • 0 <= blacklist[i] < n
  • Key requirements:
    - pick() should run in O(1) time.
    - You cannot use excessive memory (i.e., you can't store every valid number).

Thought Process

At first, you might consider generating random numbers in [0, n) and retrying if the number is in the blacklist. However, this approach is inefficient if the blacklist is large, since you could waste a lot of time retrying.

Next, you might think about precomputing a list of all valid numbers (those not in the blacklist) and picking randomly from it. But since n can be as large as 10^9, this would use too much memory.

The optimized approach is to remap the blacklist values that fall in the range of possible picks to valid numbers outside that range. This way, you can pick a random number in a compact range and, if needed, translate it to a valid number. This leverages the fact that the number of blacklisted items is much smaller than n.

Solution Approach

Let's break down the optimized solution step by step:

  1. Define the valid range:
    Since there are len(blacklist) numbers to exclude, there are valid_range = n - len(blacklist) valid numbers. We can pick a random integer x in [0, valid_range).
  2. Identify blacklisted numbers in the valid range:
    Some numbers in [0, valid_range) may be blacklisted. We must avoid picking them directly.
  3. Remap blacklisted numbers:
    For every blacklisted number b in [0, valid_range), we map it to a valid number in [valid_range, n) that is not blacklisted. We store these mappings in a hash map for O(1) lookups.
  4. Random pick logic:
    • Pick a random integer x in [0, valid_range).
    • If x is not blacklisted, return x.
    • If x is blacklisted, return its mapped value from the hash map.
  5. Why use a hash map?
    The hash map allows us to remap only the necessary blacklisted numbers efficiently and keep the pick() operation O(1).

This approach is memory efficient and scales well even for large n.

Example Walkthrough

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

  1. Calculate valid range:
    valid_range = 7 - 3 = 4. We'll pick random numbers in [0, 4), i.e., 0, 1, 2, 3.
  2. Blacklist in the pick range:
    2, 3 are in [0, 4) and must be remapped.
  3. Find available numbers to map to:
    Numbers in [4, 7) are 4, 5, 6. 5 is blacklisted, so valid targets are 4 and 6.
  4. Remap:
    • Map 2 → 4
    • Map 3 → 6
  5. Pick process:
    • If random pick is 0 or 1, return as is.
    • If 2, return 4 (via mapping).
    • If 3, return 6 (via mapping).

Thus, the possible results are 0, 1, 4, 6, all equally likely.

Time and Space Complexity

  • Brute-force approach:
    • Time: O(1) per pick on average if blacklist is small, but can be O(n) in the worst case if the blacklist is large (many retries).
    • Space: O(1) (no extra storage), or O(n) if storing all valid numbers.
  • Optimized remapping approach:
    • Time: O(1) per pick() operation due to hash map lookup.
    • Space: O(B), where B is the size of the blacklist, since the hash map only stores remappings for blacklisted numbers in the pick range.

The optimized approach is highly efficient and suitable for large n and moderate blacklist sizes.

Summary

The Random Pick with Blacklist problem demonstrates how clever remapping and hash maps can transform an inefficient brute-force solution into an optimal one. By reducing the random pick space and only remapping the necessary blacklisted numbers, we achieve O(1) random picks with minimal space overhead. This strategy is elegant because it leverages both mathematical insight and efficient data structures to solve a problem that at first seems daunting for large inputs.

Code Implementation

import random

class Solution:

    def __init__(self, n: int, blacklist: list[int]):
        self.n = n
        self.blacklist = set(blacklist)
        self.bound = n - len(blacklist)
        self.mapping = {}

        black_in_range = set(b for b in blacklist if b < self.bound)
        white_out_range = set(range(self.bound, n)) - self.blacklist
        white_iter = iter(white_out_range)

        for b in black_in_range:
            self.mapping[b] = next(white_iter)

    def pick(self) -> int:
        x = random.randint(0, self.bound - 1)
        return self.mapping.get(x, x)
      
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <random>

class Solution {
    int bound;
    std::unordered_map<int, int> mapping;
    std::mt19937 gen;
    std::uniform_int_distribution<> dist;
public:
    Solution(int n, std::vector<int>& blacklist) {
        int B = blacklist.size();
        bound = n - B;
        std::unordered_set<int> black(blacklist.begin(), blacklist.end());
        std::unordered_set<int> black_in_range;
        for (int b : blacklist) {
            if (b < bound) black_in_range.insert(b);
        }
        std::unordered_set<int> white_out_range;
        for (int i = bound; i < n; ++i) {
            if (!black.count(i))
                white_out_range.insert(i);
        }
        auto it = white_out_range.begin();
        for (int b : black_in_range) {
            mapping[b] = *it;
            ++it;
        }
        std::random_device rd;
        gen = std::mt19937(rd());
        dist = std::uniform_int_distribution<>(0, bound - 1);
    }
    
    int pick() {
        int x = dist(gen);
        if (mapping.count(x)) return mapping[x];
        return x;
    }
};
      
import java.util.*;

class Solution {
    private int bound;
    private Map<Integer, Integer> mapping;
    private Random rand;

    public Solution(int n, int[] blacklist) {
        int B = blacklist.length;
        bound = n - B;
        mapping = new HashMap<>();
        Set<Integer> black = new HashSet<>();
        for (int b : blacklist) black.add(b);

        Set<Integer> blackInRange = new HashSet<>();
        for (int b : blacklist) {
            if (b < bound) blackInRange.add(b);
        }
        Set<Integer> whiteOutRange = new HashSet<>();
        for (int i = bound; i < n; i++) {
            if (!black.contains(i)) whiteOutRange.add(i);
        }
        Iterator<Integer> it = whiteOutRange.iterator();
        for (int b : blackInRange) {
            mapping.put(b, it.next());
        }
        rand = new Random();
    }
    
    public int pick() {
        int x = rand.nextInt(bound);
        return mapping.getOrDefault(x, x);
    }
}
      
var Solution = function(n, blacklist) {
    this.bound = n - blacklist.length;
    this.mapping = new Map();
    let blackSet = new Set(blacklist);
    let blackInRange = new Set();
    for (let b of blacklist) {
        if (b < this.bound) blackInRange.add(b);
    }
    let whiteOutRange = [];
    for (let i = this.bound; i < n; ++i) {
        if (!blackSet.has(i)) whiteOutRange.push(i);
    }
    let idx = 0;
    for (let b of blackInRange) {
        this.mapping.set(b, whiteOutRange[idx++]);
    }
};

Solution.prototype.pick = function() {
    let x = Math.floor(Math.random() * this.bound);
    return this.mapping.has(x) ? this.mapping.get(x) : x;
};