questions D-E

This commit is contained in:
2025-05-25 11:08:40 +01:00
parent 615e3f1291
commit 360b5fa255
18 changed files with 4022 additions and 0 deletions

View File

@@ -0,0 +1,237 @@
title: Detect Squares
slug: detect-squares
difficulty: medium
leetcode_id: 2013
leetcode_url: https://leetcode.com/problems/detect-squares/
categories:
- arrays
- hash-tables
patterns:
- heap
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
&nbsp;
**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
&nbsp;
**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
&nbsp;
**Step 4: Return the total count**
- Return the accumulated count of all possible squares
&nbsp;
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.