259 lines
13 KiB
YAML
259 lines
13 KiB
YAML
title: Detect Squares
|
|
slug: detect-squares
|
|
difficulty: medium
|
|
leetcode_id: 2013
|
|
leetcode_url: https://leetcode.com/problems/detect-squares/
|
|
categories:
|
|
- arrays
|
|
- hash-tables
|
|
patterns:
|
|
- slug: hashing
|
|
is_optimal: true
|
|
|
|
function_signature: "class DetectSquares:\n def __init__(self): ...\n def add(self, point: list[int]) -> None: ...\n def count(self, point: list[int]) -> int: ..."
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { operations: ["DetectSquares", "add", "add", "add", "count", "count", "add", "count"], arguments: [[], [[3, 10]], [[11, 2]], [[3, 2]], [[11, 10]], [[14, 8]], [[11, 2]], [[11, 10]]] }
|
|
expected: [null, null, null, null, 1, 0, null, 2]
|
|
- input: { operations: ["DetectSquares", "add", "add", "add", "add", "count"], arguments: [[], [[0, 0]], [[0, 1]], [[1, 0]], [[1, 1]], [[0, 0]]] }
|
|
expected: [null, null, null, null, null, 1]
|
|
hidden:
|
|
- input: { operations: ["DetectSquares", "count"], arguments: [[], [[5, 5]]] }
|
|
expected: [null, 0]
|
|
- input: { operations: ["DetectSquares", "add", "count"], arguments: [[], [[0, 0]], [[0, 0]]] }
|
|
expected: [null, null, 0]
|
|
- input: { operations: ["DetectSquares", "add", "add", "add", "add", "add", "add", "add", "add", "count"], arguments: [[], [[0, 0]], [[0, 2]], [[2, 0]], [[2, 2]], [[0, 0]], [[0, 2]], [[2, 0]], [[2, 2]], [[0, 0]]] }
|
|
expected: [null, null, null, null, null, null, null, null, null, 4]
|
|
- input: { operations: ["DetectSquares", "add", "add", "add", "add", "count", "count"], arguments: [[], [[0, 0]], [[0, 5]], [[5, 0]], [[5, 5]], [[0, 0]], [[2, 2]]] }
|
|
expected: [null, null, null, null, null, 1, 0]
|
|
- input: { operations: ["DetectSquares", "add", "add", "add", "add", "add", "add", "count"], arguments: [[], [[0, 0]], [[0, 3]], [[3, 0]], [[3, 3]], [[0, 1]], [[1, 0]], [[1, 1]]] }
|
|
expected: [null, null, null, null, null, null, null, 0]
|
|
|
|
description: |
|
|
You are given a stream of points on the X-Y plane. Design an algorithm that:
|
|
|
|
- **Adds** new points from the stream into a data structure. **Duplicate** points are allowed and should be treated as different points.
|
|
- Given a query point, **counts** the number of ways to choose three points from the data structure such that the three points and the query point form an **axis-aligned square** with **positive area**.
|
|
|
|
An **axis-aligned square** is a square whose edges are all the same length and are either parallel or perpendicular to the x-axis and y-axis.
|
|
|
|
Implement the `DetectSquares` class:
|
|
|
|
- `DetectSquares()` Initializes the object with an empty data structure.
|
|
- `void add(int[] point)` Adds a new point `point = [x, y]` to the data structure.
|
|
- `int count(int[] point)` Counts the number of ways to form **axis-aligned squares** with point `point = [x, y]` as described above.
|
|
|
|
constraints: |
|
|
- `point.length == 2`
|
|
- `0 <= x, y <= 1000`
|
|
- At most `3000` calls in total will be made to `add` and `count`
|
|
|
|
examples:
|
|
- input: |
|
|
["DetectSquares", "add", "add", "add", "count", "count", "add", "count"]
|
|
[[], [[3, 10]], [[11, 2]], [[3, 2]], [[11, 10]], [[14, 8]], [[11, 2]], [[11, 10]]]
|
|
output: "[null, null, null, null, 1, 0, null, 2]"
|
|
explanation: |
|
|
DetectSquares detectSquares = new DetectSquares();
|
|
detectSquares.add([3, 10]);
|
|
detectSquares.add([11, 2]);
|
|
detectSquares.add([3, 2]);
|
|
detectSquares.count([11, 10]); // return 1 - forms a square with the three added points
|
|
detectSquares.count([14, 8]); // return 0 - no square can be formed
|
|
detectSquares.add([11, 2]); // duplicate points are allowed
|
|
detectSquares.count([11, 10]); // return 2 - two ways to form a square now
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine you're standing at a point on a grid and looking for axis-aligned squares that include your position as one corner.
|
|
|
|
The key insight is that an **axis-aligned square** has a very specific geometric property: if you know two diagonally opposite corners, you can determine the other two corners exactly. For a square with corners at `(x1, y1)` and `(x2, y2)` to be axis-aligned, the side length must be `|x2 - x1| = |y2 - y1|`.
|
|
|
|
Think of it this way: given your query point `(px, py)`, you need to find points that could be the **diagonal opposite** corner. A point `(x, y)` qualifies as a diagonal opposite if `|px - x| == |py - y|` and this distance is greater than zero (positive area requirement).
|
|
|
|
Once you have the query point and a diagonal point, the other two corners are uniquely determined:
|
|
- Corner 3: `(px, y)` — same x as query, same y as diagonal
|
|
- Corner 4: `(x, py)` — same x as diagonal, same y as query
|
|
|
|
The number of valid squares is the product of how many times each of those other two corners appears in our data structure. If either is missing (count = 0), no square can be formed with that diagonal.
|
|
|
|
approach: |
|
|
We solve this using a **Hash Map with Point Counting**:
|
|
|
|
**Step 1: Choose the right data structure**
|
|
|
|
- Use a hash map (dictionary) that maps each point `(x, y)` to its count
|
|
- This allows O(1) lookup for point existence and handles duplicates naturally
|
|
- Also maintain a list of all points with their x-coordinates grouped for efficient iteration
|
|
|
|
|
|
|
|
**Step 2: Implement the add operation**
|
|
|
|
- When adding a point `(x, y)`, increment its count in the hash map
|
|
- This is O(1) and correctly handles duplicate points
|
|
|
|
|
|
|
|
**Step 3: Implement the count operation**
|
|
|
|
- Given query point `(px, py)`, iterate through all points `(x, y)` in the data structure
|
|
- For each point, check if it could be a diagonal corner: `|px - x| == |py - y|` and `px != x`
|
|
- If valid, calculate the two other corners: `(px, y)` and `(x, py)`
|
|
- The number of squares with this diagonal is: `count[(x, y)] * count[(px, y)] * count[(x, py)]`
|
|
- Sum up contributions from all valid diagonals
|
|
|
|
|
|
|
|
**Step 4: Return the total count**
|
|
|
|
- Return the accumulated count of all possible squares
|
|
|
|
|
|
|
|
This approach leverages the geometric constraint of axis-aligned squares to reduce a potentially O(n^3) problem (checking all triplets) to O(n) per query.
|
|
|
|
common_pitfalls:
|
|
- title: Checking All Triplets
|
|
description: |
|
|
A naive approach might try to check all combinations of three points from the data structure to see if they form a square with the query point.
|
|
|
|
With up to 3000 operations and potentially thousands of points, checking all triplets would be O(n^3) per query — far too slow.
|
|
|
|
Instead, use the geometric insight that knowing the diagonal determines the other two corners exactly.
|
|
wrong_approach: "Iterating through all triplets of stored points"
|
|
correct_approach: "Iterate through potential diagonals and look up the other two corners"
|
|
|
|
- title: Forgetting About Duplicates
|
|
description: |
|
|
The problem explicitly states that duplicate points should be treated as different points. If you store points in a set, you lose duplicate information.
|
|
|
|
For example, if `[3, 2]` is added twice, there are now two ways to use that corner in a square. You must multiply the counts of all three corners to get the correct answer.
|
|
wrong_approach: "Using a set to store unique points only"
|
|
correct_approach: "Use a dictionary mapping points to their counts"
|
|
|
|
- title: Missing the Positive Area Constraint
|
|
description: |
|
|
The problem requires squares with **positive area**. This means the diagonal distance must be greater than zero — the query point and the diagonal point must be different.
|
|
|
|
If you don't check `px != x` (or equivalently `py != y`), you might count degenerate "squares" with zero area.
|
|
wrong_approach: "Not filtering out same-point diagonals"
|
|
correct_approach: "Ensure diagonal point differs from query point"
|
|
|
|
- title: Only Checking One Diagonal Direction
|
|
description: |
|
|
Given a query point `(px, py)` and a potential diagonal at `(x, y)`, there are actually two possible squares if the side length matches. The diagonal could go "up-right to down-left" or "up-left to down-right".
|
|
|
|
When iterating through all stored points as potential diagonals, both directions are naturally covered because you check every point, not just those in one quadrant.
|
|
wrong_approach: "Only considering diagonals in one direction"
|
|
correct_approach: "Check all stored points as potential diagonals"
|
|
|
|
key_takeaways:
|
|
- "**Geometric insight reduces complexity**: Axis-aligned squares have only 2 degrees of freedom (diagonal corners), not 4 independent corners"
|
|
- "**Hash maps for counting**: When duplicates matter, map elements to their counts rather than using sets"
|
|
- "**Design pattern**: For streaming data with queries, choose data structures that optimize the most frequent operation (here, count queries benefit from O(1) point lookups)"
|
|
- "**Multiplication principle**: When counting combinations, multiply the counts of independent choices"
|
|
|
|
time_complexity: "O(1) for `add`, O(n) for `count` where n is the number of unique x-coordinates with stored points. Each query iterates through potential diagonal points and does O(1) lookups."
|
|
space_complexity: "O(n) where n is the total number of points added. We store each point's count in the hash map."
|
|
|
|
solutions:
|
|
- approach_name: Hash Map with Point Counting
|
|
is_optimal: true
|
|
code: |
|
|
from collections import defaultdict
|
|
|
|
class DetectSquares:
|
|
def __init__(self):
|
|
# Map (x, y) -> count of that point
|
|
self.point_count = defaultdict(int)
|
|
# Map x -> list of y values at that x coordinate
|
|
self.x_to_ys = defaultdict(list)
|
|
|
|
def add(self, point: list[int]) -> None:
|
|
x, y = point
|
|
# Track this point's count
|
|
self.point_count[(x, y)] += 1
|
|
# Record y at this x for efficient iteration
|
|
self.x_to_ys[x].append(y)
|
|
|
|
def count(self, point: list[int]) -> int:
|
|
px, py = point
|
|
result = 0
|
|
|
|
# Check all points that share x-coordinate with query
|
|
# These could be vertical edges of potential squares
|
|
for y in self.x_to_ys[px]:
|
|
# Skip if same point (zero area)
|
|
if y == py:
|
|
continue
|
|
|
|
# Side length of potential square
|
|
side = abs(py - y)
|
|
|
|
# Check both possible squares (left and right)
|
|
for dx in [-side, side]:
|
|
x2 = px + dx
|
|
# Count squares: multiply counts of the 3 other corners
|
|
# (px, y) is on vertical edge, (x2, py) and (x2, y) complete square
|
|
result += (
|
|
self.point_count[(px, y)] *
|
|
self.point_count[(x2, py)] *
|
|
self.point_count[(x2, y)]
|
|
)
|
|
|
|
return result
|
|
explanation: |
|
|
**Time Complexity:**
|
|
- `add`: O(1) — dictionary update and list append
|
|
- `count`: O(n) — iterate through points sharing x-coordinate with query, O(1) lookups for other corners
|
|
|
|
**Space Complexity:** O(n) — storing all points and their counts
|
|
|
|
We use two data structures: a point-to-count map for O(1) existence checks, and an x-to-ys map to efficiently find points that could form vertical edges with the query point. For each potential vertical edge, we check if the horizontal corners exist.
|
|
|
|
- approach_name: Simple Hash Map
|
|
is_optimal: false
|
|
code: |
|
|
from collections import defaultdict
|
|
|
|
class DetectSquares:
|
|
def __init__(self):
|
|
# Map (x, y) -> count of that point
|
|
self.point_count = defaultdict(int)
|
|
# Store all points for iteration
|
|
self.points = []
|
|
|
|
def add(self, point: list[int]) -> None:
|
|
x, y = point
|
|
self.point_count[(x, y)] += 1
|
|
self.points.append((x, y))
|
|
|
|
def count(self, point: list[int]) -> int:
|
|
px, py = point
|
|
result = 0
|
|
|
|
# Check every stored point as potential diagonal
|
|
for x, y in self.points:
|
|
# Must form valid diagonal: same side length, not same point
|
|
if abs(px - x) != abs(py - y) or x == px:
|
|
continue
|
|
|
|
# The other two corners are determined
|
|
# Multiply counts of all three other corners
|
|
result += (
|
|
self.point_count[(x, py)] *
|
|
self.point_count[(px, y)]
|
|
)
|
|
|
|
return result
|
|
explanation: |
|
|
**Time Complexity:**
|
|
- `add`: O(1)
|
|
- `count`: O(n) where n is total points added (including duplicates)
|
|
|
|
**Space Complexity:** O(n) — storing all points
|
|
|
|
This simpler approach iterates through all added points as potential diagonal corners. It's slightly less efficient than the optimized version because it iterates through duplicate points multiple times, but it's conceptually clearer. The diagonal point's count is implicitly handled by iterating through the points list.
|