There is an m x n
binary grid matrix
with all the values set 0
initially. Design an algorithm to randomly pick an index (i, j)
where matrix[i][j] == 0
and flips it to 1
. All the indices (i, j)
where matrix[i][j] == 0
should be equally likely to be returned.
Optimize your algorithm to minimize the number of calls made to the built-in random function of your language and optimize the time and space complexity.
Implement the Solution
class:
Solution(int m, int n)
Initializes the object with the size of the binary matrix m
and n
.int[] flip()
Returns a random index [i, j]
of the matrix where matrix[i][j] == 0
and flips it to 1
.void reset()
Resets all the values of the matrix to be 0
.
Example 1:
Input ["Solution", "flip", "flip", "flip", "reset", "flip"] [[3, 1], [], [], [], [], []] Output [null, [1, 0], [2, 0], [0, 0], null, [2, 0]] Explanation Solution solution = new Solution(3, 1); solution.flip(); // return [1, 0], [0,0], [1,0], and [2,0] should be equally likely to be returned. solution.flip(); // return [2, 0], Since [1,0] was returned, [2,0] and [0,0] solution.flip(); // return [0, 0], Based on the previously returned indices, only [0,0] can be returned. solution.reset(); // All the values are reset to 0 and can be returned. solution.flip(); // return [2, 0], [0,0], [1,0], and [2,0] should be equally likely to be returned.
Constraints:
1 <= m, n <= 104
flip
.1000
calls will be made to flip
and reset
.This problem requires designing a data structure that efficiently handles random selection of unflipped cells (cells with value 0) from an m x n binary matrix, flipping the selected cell to 1, and resetting the matrix. The key is to minimize calls to the random number generator and optimize time and space complexity.
The optimal approach uses a combination of reservoir sampling and a hash map.
Algorithm:
Initialization (__init__
or Solution
constructor):
m
and n
of the matrix.total
to m * n
, representing the total number of available cells (initially all 0s).mp
or map
) to store the mapping between randomly generated indices and their actual indices in the matrix. This map is crucial for efficient flipping and resetting.Flipping (flip
):
total
because one cell will be flipped.x
between 0 (inclusive) and total
(inclusive). This represents the index to be flipped after considering the already flipped cells.x
is already mapped to a different index in mp
. If it is, retrieve the actual index (idx
) from the map. Otherwise, idx
is simply x
.mp
to map x
to the index of the last available cell (originally total
but now total -1
). This is the essence of reservoir sampling; it maintains the equal probability of selection for all remaining cells.[idx // n, idx % n]
from idx
.Resetting (reset
):
total
to m * n
.mp
).Time and Space Complexity:
__init__
/ Solution
: O(1) time and space. Initialization takes constant time and space.flip
: O(1) amortized time and O(k) space, where k is the number of flipped cells. While hash map lookups can be O(1) on average, in the worst-case scenario (many collisions), it could degrade to O(k). However, the amortized time complexity remains O(1) due to efficient hash table operations. Space is proportional to the number of flipped cells stored in the hash map.reset
: O(k) time and O(1) space. Resetting clears the hash map, which takes time proportional to the number of entries (flipped cells). The space complexity remains constant as the map is cleared.Code Examples:
Python:
import random
class Solution:
def __init__(self, m: int, n: int):
self.m = m
self.n = n
self.total = m * n
self.mp = {} # Hash map to track flipped cells
def flip(self) -> list[int]:
self.total -= 1
x = random.randint(0, self.total) # Generates a random index within the remaining available cells
idx = self.mp.get(x, x) # Get actual index or use random index if not already mapped
self.mp[x] = self.mp.get(self.total, self.total) # Map the random index to the last unflipped cell index
return [idx // self.n, idx % self.n] #Convert the linear index back to row and column
def reset(self) -> None:
self.total = self.m * self.n
self.mp.clear()
Java:
import java.util.*;
class Solution {
private int m, n, total;
private Random rand = new Random();
private Map<Integer, Integer> mp = new HashMap<>(); //Hashmap to track flipped cells
public Solution(int m, int n) {
this.m = m;
this.n = n;
this.total = m * n;
}
public int[] flip() {
total--;
int x = rand.nextInt(total +1); // Generates a random index within the remaining available cells
int idx = mp.getOrDefault(x, x); // Get actual index or use random index if not already mapped
mp.put(x, mp.getOrDefault(total, total)); // Map the random index to the last unflipped cell index
return new int[] {idx / n, idx % n}; //Convert the linear index back to row and column
}
public void reset() {
total = m * n;
mp.clear();
}
}
The Java and Python code demonstrate the same core algorithm. The use of a hash map significantly improves the efficiency of the random flipping process, making it suitable for large matrices. The reservoir sampling technique ensures that all unflipped cells have an equal probability of being selected.