LeetCode 2013 - Detect Squares
The problem presents a stream of points in a 2D plane and asks us to design a data structure that supports two operations efficiently: adding points and counting squares.
Difficulty: 🟡 Medium
Topics: Array, Hash Table, Design, Counting, Data Stream
Solution
Problem Understanding
The problem presents a stream of points in a 2D plane and asks us to design a data structure that supports two operations efficiently: adding points and counting squares. The add operation inserts a point into the data structure, and the count operation calculates the number of axis-aligned squares that can be formed using the query point and any three points already stored. An axis-aligned square is defined as a square whose sides are parallel or perpendicular to the x-axis and y-axis, meaning the coordinates of its vertices follow a strict pattern along the axes.
The input consists of integer points [x, y] where 0 <= x, y <= 1000. Duplicate points are allowed, which means we may have multiple instances of the same coordinate, and each instance counts separately when forming squares. The output is an integer representing the number of distinct ways to form an axis-aligned square using the query point and three previously added points.
The constraints-at most 3000 calls to add and count-suggest that a naive brute-force solution that iterates through all possible triplets of points for each count operation would likely be too slow. Edge cases include queries on points that have no potential square partners, duplicate points, and squares with sides of length zero (which are invalid as the area must be positive).
Approaches
Brute Force Approach
The brute-force approach involves storing all added points in a list. For a count query, we would iterate over all combinations of three points from the stored list and check if they form a square with the query point. To validate a square, we would calculate all pairwise distances and ensure that there are two equal distances for sides and one longer distance for the diagonal. This approach works in principle but has a time complexity of O(n^3) per query where n is the number of points, which is unacceptable for 3000 operations because it could lead to over 27 billion iterations in the worst case.
Optimal Approach
The key insight is that we are only counting axis-aligned squares, meaning the sides are strictly horizontal or vertical. This allows us to exploit hash maps to store the frequency of points for efficient lookup. For a query point (x, y), an axis-aligned square's other three vertices must lie on the same x or y coordinates:
- The square has sides parallel to the axes, so if one vertex is
(x, y), the opposite vertex must be(x + side_length, y + side_length)or similar variations. - By iterating only through points that share the same x or y coordinate with the query point, we reduce the search space drastically.
- We maintain a map of maps where the outer map is keyed by x-coordinate and the inner map counts occurrences of y-coordinates, enabling O(1) lookup for points at specific positions.
This reduces the count query to O(n) where n is the number of points along the same x-coordinate as the query, and adding a point is O(1).
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n^3) per query | O(n) | Iterates all triplets to check squares, too slow |
| Optimal | O(n) per query, O(1) per add | O(n) | Uses hashmap to count points, leverages axis alignment for efficiency |
Algorithm Walkthrough
- Initialize Data Structure: Create a dictionary
points_countwhere each key is an x-coordinate, and the value is another dictionary mapping y-coordinates to their counts. - Add Operation: When adding a point
(x, y), incrementpoints_count[x][y]by 1. Ifxorydoes not exist in the maps, initialize them. - Count Operation: For a query point
(x, y):
- Initialize a variable
total_squares = 0. - Iterate over all x-coordinates
col_xinpoints_countwherecol_x != x. - For each candidate x-coordinate, calculate the potential square's side length
side = col_x - x. - Check the two possible y-coordinates
(y + side)and(y - side)for forming a square. - For each candidate vertex, multiply the counts of the three other points that would form a square with
(x, y)and add tototal_squares.
- Return Result: After iterating all candidates, return
total_squares.
Why it works: The algorithm leverages the properties of axis-aligned squares. By iterating only through points on different x-coordinates and checking the corresponding y-coordinates, we ensure that all squares are considered exactly once, accounting for duplicate points through the stored counts.
Python Solution
from collections import defaultdict
from typing import List
class DetectSquares:
def __init__(self):
self.points_count = defaultdict(lambda: defaultdict(int))
def add(self, point: List[int]) -> None:
x, y = point
self.points_count[x][y] += 1
def count(self, point: List[int]) -> int:
x, y = point
total_squares = 0
if x not in self.points_count:
return 0
for col_x, y_map in self.points_count.items():
if col_x == x:
continue
side_length = col_x - x
# Check for squares above and below the query point
for delta_y in [side_length, -side_length]:
y2 = y + delta_y
if y2 in y_map:
count1 = self.points_count[x].get(y, 0)
count2 = y_map[y]
count3 = y_map[y2]
total_squares += count1 * count2 * count3
return total_squares
Explanation: The add function updates the count of each point in a nested dictionary. The count function iterates over possible x-coordinates for the other vertices, computes side lengths, and checks the presence of corresponding y-coordinates to ensure an axis-aligned square. Multiplying the counts handles duplicate points correctly.
Go Solution
type DetectSquares struct {
pointsCount map[int]map[int]int
}
func Constructor() DetectSquares {
return DetectSquares{pointsCount: make(map[int]map[int]int)}
}
func (this *DetectSquares) Add(point []int) {
x, y := point[0], point[1]
if this.pointsCount[x] == nil {
this.pointsCount[x] = make(map[int]int)
}
this.pointsCount[x][y]++
}
func (this *DetectSquares) Count(point []int) int {
x, y := point[0], point[1]
total := 0
for colX, yMap := range this.pointsCount {
if colX == x {
continue
}
side := colX - x
for _, deltaY := range []int{side, -side} {
y2 := y + deltaY
if count, ok := yMap[y2]; ok {
total += this.pointsCount[x][y] * yMap[y] * count
}
}
}
return total
}
Go-specific Notes: The nested maps mimic Python's defaultdict. Nil checks ensure safe map operations. Multiplication of counts handles duplicates.
Worked Examples
Example Input Sequence:
["DetectSquares", "add", "add", "add", "count", "count", "add", "count"]
[[], [[3, 10]], [[11, 2]], [[3, 2]], [[11, 10]], [[14, 8]], [[11, 2]], [[11, 10]]]
| Step | Operation | points_count state | count result |
|---|---|---|---|
| 1 | add([3,10]) | {3: {10: 1}} | - |
| 2 | add([11,2]) | {3:{10:1},11:{2:1}} | - |
| 3 | add([3,2]) | {3:{10:1,2:1},11:{2:1}} | - |
| 4 | count([11,10]) | same | 1 |
| 5 | count([14,8]) | same | 0 |
| 6 | add([11,2]) | {3:{10:1,2:1},11:{2:2}} | - |
| 7 | count([11,10]) | same | 2 |
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time (add) | O(1) | Insertion in hashmap is constant time |
| Time (count) | O(n) | Iterate over all x-coordinates in the map |
| Space | O(n) | Store counts of all added points |
The time complexity for count is efficient because it does not require checking all combinations of three points; it only examines relevant candidates along aligned axes.
Test Cases
obj = DetectSquares()
obj.add([3, 10])
obj.add([11, 2])
obj.add([3, 2])
assert obj.count([11, 10]) == 1 # basic square test
assert obj.count([14, 8]) == 0 # no squares
obj.add([11, 2])