LeetCode 3391 - Design a 3D Binary Matrix with Efficient Layer Tracking
The problem asks us to design a data structure that manages a 3D binary matrix of size n x n x n. Every cell initially contains 0, and we must support three operations efficiently: 1. setCell(x, y, z) sets a specific cell to 1. 2.
Difficulty: 🟡 Medium
Topics: Array, Hash Table, Design, Heap (Priority Queue), Matrix, Ordered Set
Solution
Problem Understanding
The problem asks us to design a data structure that manages a 3D binary matrix of size n x n x n. Every cell initially contains 0, and we must support three operations efficiently:
setCell(x, y, z)sets a specific cell to1.unsetCell(x, y, z)sets a specific cell back to0.largestMatrix()returns the indexxwhose 2D layermatrix[x]contains the greatest number of1s.
The important detail is that the matrix is grouped by its first dimension. Each matrix[x] is effectively a full n x n layer. We are not asked to count all 1s in the entire 3D structure. Instead, we only care about how many 1s exist inside each individual layer x.
If multiple layers contain the same maximum number of 1s`, we must return the largest index among them.
For example, suppose:
- Layer
0contains 3 ones - Layer
1contains 5 ones - Layer
2contains 5 ones
Then largestMatrix() must return 2, because although layers 1 and 2 tie, the larger index wins.
The constraints are very important for determining the correct approach:
n <= 100, so the full matrix contains at most1,000,000cells.- Up to
10^5update operations occur. - Up to
10^4queries tolargestMatrix()occur.
A naive solution that scans the entire matrix repeatedly would become too slow.
There are also several subtle edge cases:
- Calling
setCellon a cell that is already1should not double count it. - Calling
unsetCellon a cell that is already0should not reduce counts below zero. - When all layers contain the same number of ones, we must return the largest layer index.
- Multiple updates may occur on the same coordinates repeatedly.
The problem guarantees all coordinates are valid, so we do not need bounds checking.
Approaches
Brute Force Approach
The most straightforward solution is to explicitly store the entire 3D matrix and recompute the answer whenever largestMatrix() is called.
We can represent the matrix using a 3D array:
matrix[x][y][z]
For every setCell or unsetCell, we simply update the cell value.
Then, whenever largestMatrix() is called, we scan every layer:
- Count how many
1s exist in layerx - Track the maximum count
- Resolve ties by choosing the largest index
This approach is correct because it directly computes the exact number of ones in every layer each time.
However, it is inefficient. Each query requires scanning all n^3 cells.
Since:
n <= 100largestMatrix()may be called10^4times
The worst case becomes:
10^4 * 100^3 = 10^10
which is far too slow.
Optimal Approach
The key observation is that largestMatrix() only depends on the number of ones inside each layer.
We do not actually need to repeatedly scan the full matrix. Instead, we can maintain the count for each layer incrementally as updates happen.
The optimal strategy is:
- Store which cells are currently
1 - Maintain an array
layer_count[x] - Whenever a cell changes:
- Increment or decrement the corresponding layer count
- For
largestMatrix():
- Scan only the
nlayer counts - Return the layer with maximum count
- Break ties by choosing the larger index
This reduces query complexity dramatically.
Because n <= 100, scanning all layer counts is extremely cheap.
We use a hash set to track active cells so duplicate updates do not corrupt the counts.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n³) per query | O(n³) | Recomputes all counts every time |
| Optimal | O(1) updates, O(n) query | O(number of active cells + n) | Maintains counts incrementally |
Algorithm Walkthrough
Optimal Algorithm
- Initialize a hash set called
active_cells.
This set stores every coordinate currently equal to 1.
Each cell is represented as a tuple:
(x, y, z)
We use a hash set because membership checks are O(1).
2. Initialize an array layer_count of size n.
layer_count[x] stores how many cells in layer x currently contain 1.
3. For setCell(x, y, z):
-
Create the tuple
(x, y, z) -
Check whether it already exists in
active_cells -
If not:
-
Add it to the set
-
Increment
layer_count[x]
This prevents double counting.
4. For unsetCell(x, y, z):
-
Create the tuple
(x, y, z) -
Check whether it exists in
active_cells -
If it does:
-
Remove it from the set
-
Decrement
layer_count[x]
This prevents counts from becoming negative.
5. For largestMatrix():
-
Start with:
-
best_index = 0 -
best_count = layer_count[0] -
Iterate through every layer index
x -
If:
-
layer_count[x] > best_count
update the best layer
- or
layer_count[x] == best_countandx > best_index
update because ties prefer larger indices
6. Return best_index.
Why it works
The algorithm maintains an invariant:
layer_count[x] always equals the number of active cells in layer x.
Every successful setCell increases the count exactly once, and every successful unsetCell decreases it exactly once.
Therefore, at any moment, scanning layer_count gives the exact number of ones in every layer. Since largestMatrix() selects the layer with maximum count and resolves ties correctly, the returned answer is always correct.
Python Solution
class Matrix3D:
def __init__(self, n: int):
self.n = n
self.active_cells = set()
self.layer_count = [0] * n
def setCell(self, x: int, y: int, z: int) -> None:
cell = (x, y, z)
if cell not in self.active_cells:
self.active_cells.add(cell)
self.layer_count[x] += 1
def unsetCell(self, x: int, y: int, z: int) -> None:
cell = (x, y, z)
if cell in self.active_cells:
self.active_cells.remove(cell)
self.layer_count[x] -= 1
def largestMatrix(self) -> int:
best_index = 0
best_count = self.layer_count[0]
for x in range(1, self.n):
current_count = self.layer_count[x]
if (
current_count > best_count
or (
current_count == best_count
and x > best_index
)
):
best_count = current_count
best_index = x
return best_index
# Your Matrix3D object will be instantiated and called as such:
# obj = Matrix3D(n)
# obj.setCell(x,y,z)
# obj.unsetCell(x,y,z)
# param_3 = obj.largestMatrix()
The implementation closely follows the algorithm described earlier.
The constructor initializes two important structures:
active_cells, which tracks which coordinates currently contain1layer_count, which stores how many active cells exist in each layer
The setCell method first checks whether the cell is already active. If it is not, we insert it into the set and increment the corresponding layer count.
The unsetCell method performs the reverse operation. It only decrements the count if the cell was actually active.
The largestMatrix method scans all layer counts and tracks the best layer seen so far. The tie-breaking rule is implemented directly in the condition:
current_count == best_count and x > best_index
which ensures larger indices win ties.
Go Solution
type Matrix3D struct {
activeCells map[[3]int]bool
layerCount []int
n int
}
func Constructor(n int) Matrix3D {
return Matrix3D{
activeCells: make(map[[3]int]bool),
layerCount: make([]int, n),
n: n,
}
}
func (this *Matrix3D) SetCell(x int, y int, z int) {
cell := [3]int{x, y, z}
if !this.activeCells[cell] {
this.activeCells[cell] = true
this.layerCount[x]++
}
}
func (this *Matrix3D) UnsetCell(x int, y int, z int) {
cell := [3]int{x, y, z}
if this.activeCells[cell] {
delete(this.activeCells, cell)
this.layerCount[x]--
}
}
func (this *Matrix3D) LargestMatrix() int {
bestIndex := 0
bestCount := this.layerCount[0]
for x := 1; x < this.n; x++ {
currentCount := this.layerCount[x]
if currentCount > bestCount ||
(currentCount == bestCount && x > bestIndex) {
bestCount = currentCount
bestIndex = x
}
}
return bestIndex
}
/**
* Your Matrix3D object will be instantiated and called as such:
* obj := Constructor(n);
* obj.SetCell(x,y,z);
* obj.UnsetCell(x,y,z);
* param_3 := obj.LargestMatrix();
*/
The Go implementation mirrors the Python logic very closely.
The main difference is how the active cells are stored. Since Go does not support tuples directly, we use a fixed-size array:
[3]int{x, y, z}
as the map key.
The map value is a boolean indicating whether the cell is active.
Go slices are used for layerCount, and integer overflow is not a concern because counts never exceed n², which is at most 10,000.
Worked Examples
Example 1
Input:
["Matrix3D", "setCell", "largestMatrix", "setCell", "largestMatrix", "setCell", "largestMatrix"]
[[3], [0,0,0], [], [1,1,2], [], [0,0,1], []]
Initial state:
| Layer | Count |
|---|---|
| 0 | 0 |
| 1 | 0 |
| 2 | 0 |
All counts tie, so answer would be 2.
Step 1
setCell(0,0,0)
Active cells:
{(0,0,0)}
Layer counts:
| Layer | Count |
|---|---|
| 0 | 1 |
| 1 | 0 |
| 2 | 0 |
Step 2
largestMatrix()
Layer 0 has the largest count.
Return:
0
Step 3
setCell(1,1,2)
Active cells:
{(0,0,0), (1,1,2)}
Layer counts:
| Layer | Count |
|---|---|
| 0 | 1 |
| 1 | 1 |
| 2 | 0 |
Layers 0 and 1 tie.
Larger index wins.
Return:
1
Step 4
setCell(0,0,1)
Active cells:
{(0,0,0), (1,1,2), (0,0,1)}
Layer counts:
| Layer | Count |
|---|---|
| 0 | 2 |
| 1 | 1 |
| 2 | 0 |
Layer 0 now leads.
Return:
0
Example 2
Input:
["Matrix3D", "setCell", "largestMatrix", "unsetCell", "largestMatrix"]
Initial counts:
| Layer | Count |
|---|---|
| 0 | 0 |
| 1 | 0 |
| 2 | 0 |
| 3 | 0 |
Step 1
setCell(2,1,1)
Counts:
| Layer | Count |
|---|---|
| 0 | 0 |
| 1 | 0 |
| 2 | 1 |
| 3 | 0 |
Largest layer:
2
Step 2
unsetCell(2,1,1)
Counts:
| Layer | Count |
|---|---|
| 0 | 0 |
| 1 | 0 |
| 2 | 0 |
| 3 | 0 |
All layers tie.
Largest index wins.
Return:
3
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(1) for updates, O(n) for queries | Hash set operations are constant time, and largestMatrix() scans all layers |
| Space | O(k + n) | k is the number of active cells |
The update operations only perform hash set insertions or deletions plus a single counter modification, so they are constant time on average.
The query operation scans only the layer counts, not the entire matrix. Since n <= 100, this is very efficient.
The space complexity depends on how many cells currently contain 1. In the worst case, all n³ cells may become active.
Test Cases
# Example 1
obj = Matrix3D(3)
obj.setCell(0, 0, 0)
assert obj.largestMatrix() == 0 # single active cell in layer 0
obj.setCell(1, 1, 2)
assert obj.largestMatrix() == 1 # tie, larger index wins
obj.setCell(0, 0, 1)
assert obj.largestMatrix() == 0 # layer 0 now has more ones
# Example 2
obj = Matrix3D(4)
obj.setCell(2, 1, 1)
assert obj.largestMatrix() == 2 # layer 2 leads
obj.unsetCell(2, 1, 1)
assert obj.largestMatrix() == 3 # all tie, largest index wins
# Duplicate set should not double count
obj = Matrix3D(2)
obj.setCell(0, 0, 0)
obj.setCell(0, 0, 0)
assert obj.largestMatrix() == 0 # still only one active cell
# Duplicate unset should not go negative
obj = Matrix3D(2)
obj.unsetCell(1, 1, 1)
assert obj.largestMatrix() == 1 # all zero, largest index wins
# Multiple cells in same layer
obj = Matrix3D(3)
obj.setCell(1, 0, 0)
obj.setCell(1, 0, 1)
obj.setCell(1, 1, 1)
assert obj.largestMatrix() == 1 # layer 1 has three cells
# Tie between multiple layers
obj = Matrix3D(5)
obj.setCell(1, 0, 0)
obj.setCell(3, 0, 0)
assert obj.largestMatrix() == 3 # tie resolved by larger index
# All layers empty
obj = Matrix3D(6)
assert obj.largestMatrix() == 5 # all zero, largest index wins
# Stress repeated updates
obj = Matrix3D(3)
for _ in range(100):
obj.setCell(2, 2, 2)
assert obj.largestMatrix() == 2 # repeated set should not increase count
for _ in range(100):
obj.unsetCell(2, 2, 2)
assert obj.largestMatrix() == 2 # all zero again
Test Summary
| Test | Why |
|---|---|
| Example 1 | Validates normal updates and tie-breaking |
| Example 2 | Validates unset behavior |
| Duplicate set | Ensures counts are not double incremented |
| Duplicate unset | Ensures counts never become negative |
| Multiple cells same layer | Verifies layer accumulation |
| Tie between layers | Confirms largest index rule |
| All layers empty | Validates default tie behavior |
| Stress repeated updates | Tests idempotent operations |
Edge Cases
One important edge case occurs when all layers contain the same number of ones. A careless implementation might return the first layer with the maximum count, but the problem explicitly requires returning the largest index. The implementation handles this by updating the answer even when counts tie, provided the current index is larger.
Another important edge case involves repeated setCell calls on the same coordinate. Without tracking whether a cell is already active, the layer count would incorrectly increase multiple times. Using a hash set prevents this because we only increment when the cell was previously absent.
A similar issue exists for repeated unsetCell calls. If we blindly decrement the layer count, it could become negative. The implementation avoids this by checking whether the cell exists before removing it and decrementing the counter.
A final subtle case is when n = 1. In this scenario, there is only one layer, so largestMatrix() must always return 0. The algorithm naturally handles this because the scan begins with layer 0, and there are no additional layers to compare against.