2013. Detect Squares
Problem Description
You need to design a data structure that can store points on a 2D plane and count how many axis-aligned squares can be formed using stored points and a query point.
An axis-aligned square is a square where:
- All four sides have the same length
- All sides are either parallel or perpendicular to the x-axis and y-axis (no rotation)
- The square has positive area (not a single point)
The DetectSquares
class should support three operations:
-
Constructor
DetectSquares()
: Initializes an empty data structure to store points. -
Method
add(point)
: Takes a point[x, y]
and adds it to the data structure. Duplicate points are allowed - if you add the same point multiple times, each occurrence counts as a separate point. -
Method
count(point)
: Takes a query point[x, y]
and returns the number of ways to choose three points from the stored data structure such that these three points together with the query point form an axis-aligned square.
For the count
method, you're essentially finding all possible combinations of three stored points that, when combined with the query point as the fourth corner, create a valid axis-aligned square. Since duplicate points are treated as different points, if a point appears multiple times in the data structure, each occurrence can be used separately in forming different squares.
For example, if you have points at (0,0)
, (0,1)
, (1,0)
, and (1,1)
in your data structure, and you query with point (0,0)
, you would count how many ways you can pick three points from the stored points to form a square with (0,0)
as one corner.
Intuition
When we need to form an axis-aligned square with a query point, we need to think about what constraints define such a square. Since the square must be axis-aligned, its sides must be parallel to the x and y axes. This means if we have one corner at (x1, y1)
, the other three corners must follow a specific pattern.
The key insight is that for any axis-aligned square, if we know two diagonal corners, we can determine the other two corners. Given a query point (x1, y1)
as one corner, we can think of it forming a diagonal with another point. But which diagonal point should we consider?
Instead of thinking about diagonals directly, let's think about the structure of an axis-aligned square. If (x1, y1)
is one corner, and we want to form a square with side length d
, then:
- One adjacent corner could be at
(x2, y1)
where|x2 - x1| = d
(horizontally aligned) - From these two points, the remaining two corners must be at
(x1, y1 ± d)
and(x2, y1 ± d)
This gives us a systematic way to search: for each point that shares the same y-coordinate as our query point but has a different x-coordinate, we can calculate the distance d = |x2 - x1|
. This distance becomes our potential square side length. Then we just need to check if the other two required corners exist in our data structure.
Since we might have the square extending either upward or downward from the horizontal edge we found, we need to check both possibilities:
- Square above: corners at
(x1, y1 + d)
and(x2, y1 + d)
- Square below: corners at
(x1, y1 - d)
and(x2, y1 - d)
For storage, a nested hash map structure cnt[x][y]
works perfectly because:
- We can quickly check if a specific point exists
- We can track the count of duplicate points (important since duplicates are treated as separate points)
- We can easily iterate through all x-coordinates to find potential horizontal partners
The counting logic uses multiplication because if point A appears m
times, point B appears n
times, and point C appears p
times, then the total number of ways to pick one of each is m × n × p
.
Solution Approach
The solution uses a nested hash table structure to efficiently store and query points. Here's how each component works:
Data Structure:
self.cnt = defaultdict(Counter)
creates a nested dictionary where:- The outer dictionary keys are x-coordinates
- The inner
Counter
objects map y-coordinates to their occurrence counts - This structure allows
cnt[x][y]
to give us the count of point(x, y)
Add Method:
def add(self, point: List[int]) -> None:
x, y = point
self.cnt[x][y] += 1
- Simply increments the count for point
(x, y)
- Handles duplicates naturally by maintaining a count
Count Method:
def count(self, point: List[int]) -> int:
x1, y1 = point
if x1 not in self.cnt:
return 0
ans = 0
for x2 in self.cnt.keys():
if x2 != x1:
d = x2 - x1
ans += self.cnt[x2][y1] * self.cnt[x1][y1 + d] * self.cnt[x2][y1 + d]
ans += self.cnt[x2][y1] * self.cnt[x1][y1 - d] * self.cnt[x2][y1 - d]
return ans
The counting algorithm works as follows:
-
Early termination: If there are no points with x-coordinate
x1
, we can't form any squares, so return 0. -
Enumerate horizontal partners: For each x-coordinate
x2
in our data structure wherex2 ≠ x1
:- Calculate the distance
d = x2 - x1
(this will be the side length of potential squares) - Note:
d
can be positive or negative depending on whetherx2
is to the right or left ofx1
- Calculate the distance
-
Check for two possible squares:
- Square extending upward: We need points at:
(x2, y1)
- the horizontal partner(x1, y1 + d)
- vertical from query point(x2, y1 + d)
- the diagonal corner
- Square extending downward: We need points at:
(x2, y1)
- the horizontal partner(x1, y1 - d)
- vertical from query point(x2, y1 - d)
- the diagonal corner
- Square extending upward: We need points at:
-
Count combinations: For each valid square configuration, multiply the counts of all three required points. This gives us the number of ways to choose one instance of each point when duplicates exist.
The beauty of this approach is that by fixing the horizontal edge first (from (x1, y1)
to (x2, y1)
), we deterministically know where the other two corners must be for an axis-aligned square. We don't need to search through all possible combinations of three points - we only need to verify if the required points exist at the calculated positions.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to illustrate how the solution works.
Setup: Suppose we've added the following points to our data structure:
add([0, 0])
add([0, 2])
add([2, 0])
add([2, 2])
- added twice (duplicate)add([2, 2])
add([1, 1])
Our data structure cnt
now looks like:
cnt[0][0] = 1 cnt[0][2] = 1 cnt[2][0] = 1 cnt[2][2] = 2 (appears twice) cnt[1][1] = 1
Query: count([0, 0])
Let's count how many squares can be formed with (0, 0)
as one corner.
-
Extract query point:
x1 = 0, y1 = 0
-
Check if x1 exists:
x1 = 0
is incnt
, so we continue. -
Iterate through all x-coordinates:
When x2 = 0: Skip since
x2 == x1
When x2 = 2:
- Calculate distance:
d = 2 - 0 = 2
- Check upward square (need points at
(2,0)
,(0,2)
,(2,2)
):cnt[2][0] = 1
✓cnt[0][0 + 2] = cnt[0][2] = 1
✓cnt[2][0 + 2] = cnt[2][2] = 2
✓- Count:
1 × 1 × 2 = 2
squares
- Check downward square (need points at
(2,0)
,(0,-2)
,(2,-2)
):cnt[2][0] = 1
✓cnt[0][0 - 2] = cnt[0][-2] = 0
✗ (doesn't exist)- Count:
1 × 0 × 0 = 0
squares
When x2 = 1:
- Calculate distance:
d = 1 - 0 = 1
- Check upward square (need points at
(1,0)
,(0,1)
,(1,1)
):cnt[1][0] = 0
✗ (doesn't exist)- Count:
0 × ... = 0
squares
- Check downward square (need points at
(1,0)
,(0,-1)
,(1,-1)
):cnt[1][0] = 0
✗ (doesn't exist)- Count:
0 × ... = 0
squares
- Calculate distance:
-
Total count:
2 + 0 + 0 = 2
The function returns 2, meaning there are 2 ways to form squares with (0,0)
as one corner. Both ways use the points (0,0)
, (2,0)
, (0,2)
, and one instance of (2,2)
. Since (2,2)
appears twice in our data structure, we can choose either occurrence, giving us 2 different combinations.
Visual representation of the found square:
(0,2) ---- (2,2) | | | | (0,0) ---- (2,0)
This example demonstrates how the algorithm efficiently finds squares by:
- Fixing a horizontal edge from the query point
- Calculating the required positions of the other two corners
- Multiplying the counts to handle duplicates correctly
Solution Implementation
1from collections import defaultdict, Counter
2from typing import List
3
4
5class DetectSquares:
6 def __init__(self):
7 # Dictionary where key is x-coordinate and value is a Counter of y-coordinates
8 # This allows efficient lookup of points by x-coordinate
9 # cnt[x][y] represents the count of point (x, y)
10 self.point_count = defaultdict(Counter)
11
12 def add(self, point: List[int]) -> None:
13 """
14 Add a point to the data structure.
15
16 Args:
17 point: A list of two integers [x, y] representing coordinates
18 """
19 x_coord, y_coord = point
20 # Increment the count for this specific point
21 self.point_count[x_coord][y_coord] += 1
22
23 def count(self, point: List[int]) -> int:
24 """
25 Count the number of ways to form a square with the given point as one vertex.
26 The square must be axis-aligned (sides parallel to x and y axes).
27
28 Args:
29 point: A list of two integers [x, y] representing the query point
30
31 Returns:
32 The number of ways to form squares with the given point
33 """
34 query_x, query_y = point
35
36 # Early return if no points exist with the same x-coordinate
37 if query_x not in self.point_count:
38 return 0
39
40 total_squares = 0
41
42 # Iterate through all unique x-coordinates in our data structure
43 for other_x in self.point_count.keys():
44 # Skip if it's the same x-coordinate (need different x for square)
45 if other_x == query_x:
46 continue
47
48 # Calculate the side length of potential square
49 side_length = other_x - query_x
50
51 # Check for squares above the horizontal line through query point
52 # We need points at: (other_x, query_y), (query_x, query_y + side_length),
53 # and (other_x, query_y + side_length)
54 total_squares += (self.point_count[other_x][query_y] *
55 self.point_count[query_x][query_y + side_length] *
56 self.point_count[other_x][query_y + side_length])
57
58 # Check for squares below the horizontal line through query point
59 # We need points at: (other_x, query_y), (query_x, query_y - side_length),
60 # and (other_x, query_y - side_length)
61 total_squares += (self.point_count[other_x][query_y] *
62 self.point_count[query_x][query_y - side_length] *
63 self.point_count[other_x][query_y - side_length])
64
65 return total_squares
66
67
68# Your DetectSquares object will be instantiated and called as such:
69# obj = DetectSquares()
70# obj.add(point)
71# param_2 = obj.count(point)
72
1class DetectSquares {
2 // Map to store points: x-coordinate -> (y-coordinate -> count)
3 private Map<Integer, Map<Integer, Integer>> pointCountMap = new HashMap<>();
4
5 public DetectSquares() {
6 }
7
8 /**
9 * Adds a point to the data structure
10 * @param point Array containing [x, y] coordinates
11 */
12 public void add(int[] point) {
13 int x = point[0];
14 int y = point[1];
15
16 // Get or create the map for this x-coordinate, then increment count for y-coordinate
17 pointCountMap.computeIfAbsent(x, key -> new HashMap<>())
18 .merge(y, 1, Integer::sum);
19 }
20
21 /**
22 * Counts the number of ways to form axis-aligned squares with the given point as one vertex
23 * @param point Array containing [x, y] coordinates of the query point
24 * @return Number of valid squares that can be formed
25 */
26 public int count(int[] point) {
27 int queryX = point[0];
28 int queryY = point[1];
29
30 // If no points exist with the same x-coordinate, no squares can be formed
31 if (!pointCountMap.containsKey(queryX)) {
32 return 0;
33 }
34
35 int totalSquares = 0;
36
37 // Iterate through all x-coordinates to find potential diagonal points
38 for (Map.Entry<Integer, Map<Integer, Integer>> entry : pointCountMap.entrySet()) {
39 int diagonalX = entry.getKey();
40
41 // Skip if the x-coordinate is the same (not a diagonal point)
42 if (diagonalX != queryX) {
43 // Calculate the side length of the potential square
44 int sideLength = diagonalX - queryX;
45
46 // Get point counts for the query x-coordinate and diagonal x-coordinate
47 Map<Integer, Integer> queryXPoints = pointCountMap.get(queryX);
48 Map<Integer, Integer> diagonalXPoints = entry.getValue();
49
50 // Check for square formed with diagonal point above
51 // Need points at: (diagonalX, queryY), (queryX, queryY + sideLength), (diagonalX, queryY + sideLength)
52 totalSquares += diagonalXPoints.getOrDefault(queryY, 0)
53 * queryXPoints.getOrDefault(queryY + sideLength, 0)
54 * diagonalXPoints.getOrDefault(queryY + sideLength, 0);
55
56 // Check for square formed with diagonal point below
57 // Need points at: (diagonalX, queryY), (queryX, queryY - sideLength), (diagonalX, queryY - sideLength)
58 totalSquares += diagonalXPoints.getOrDefault(queryY, 0)
59 * queryXPoints.getOrDefault(queryY - sideLength, 0)
60 * diagonalXPoints.getOrDefault(queryY - sideLength, 0);
61 }
62 }
63
64 return totalSquares;
65 }
66}
67
68/**
69 * Your DetectSquares object will be instantiated and called as such:
70 * DetectSquares obj = new DetectSquares();
71 * obj.add(point);
72 * int param_2 = obj.count(point);
73 */
74
1class DetectSquares {
2public:
3 DetectSquares() {
4 // Constructor - no initialization needed as unordered_map is empty by default
5 }
6
7 void add(vector<int> point) {
8 int x = point[0];
9 int y = point[1];
10 // Increment the count of points at coordinate (x, y)
11 // Using nested map: outer key is x-coordinate, inner key is y-coordinate
12 pointCount[x][y]++;
13 }
14
15 int count(vector<int> point) {
16 int x1 = point[0];
17 int y1 = point[1];
18
19 // If no points exist with x-coordinate x1, no squares can be formed
20 if (!pointCount.count(x1)) {
21 return 0;
22 }
23
24 int totalSquares = 0;
25
26 // Iterate through all unique x-coordinates in our point collection
27 for (auto& [x2, yCountMap] : pointCount) {
28 // Skip if x2 equals x1 (we need different x-coordinates for a square)
29 if (x2 != x1) {
30 // Calculate the horizontal distance between x1 and x2
31 int distance = x2 - x1;
32
33 // Get the map of y-coordinates and their counts for x1
34 auto& x1Points = pointCount[x1];
35
36 // Check for squares with diagonal from (x1, y1) to (x2, y1 + distance)
37 // This forms a square with vertices:
38 // (x1, y1), (x2, y1), (x2, y1 + distance), (x1, y1 + distance)
39 totalSquares += yCountMap[y1] * x1Points[y1 + distance] * yCountMap[y1 + distance];
40
41 // Check for squares with diagonal from (x1, y1) to (x2, y1 - distance)
42 // This forms a square with vertices:
43 // (x1, y1), (x2, y1), (x2, y1 - distance), (x1, y1 - distance)
44 totalSquares += yCountMap[y1] * x1Points[y1 - distance] * yCountMap[y1 - distance];
45 }
46 }
47
48 return totalSquares;
49 }
50
51private:
52 // Nested hash map to store point counts
53 // Structure: x-coordinate -> (y-coordinate -> count)
54 unordered_map<int, unordered_map<int, int>> pointCount;
55};
56
57/**
58 * Your DetectSquares object will be instantiated and called as such:
59 * DetectSquares* obj = new DetectSquares();
60 * obj->add(point);
61 * int param_2 = obj->count(point);
62 */
63
1// Nested map to store point counts
2// Structure: x-coordinate -> (y-coordinate -> count)
3const pointCount: Map<number, Map<number, number>> = new Map();
4
5/**
6 * Adds a point to the data structure
7 * @param point - Array containing [x, y] coordinates
8 */
9function add(point: number[]): void {
10 const x = point[0];
11 const y = point[1];
12
13 // Initialize inner map for x-coordinate if it doesn't exist
14 if (!pointCount.has(x)) {
15 pointCount.set(x, new Map<number, number>());
16 }
17
18 // Get the inner map for this x-coordinate
19 const yCountMap = pointCount.get(x)!;
20
21 // Increment the count of points at coordinate (x, y)
22 const currentCount = yCountMap.get(y) || 0;
23 yCountMap.set(y, currentCount + 1);
24}
25
26/**
27 * Counts the number of ways to form axis-aligned squares with the given point
28 * @param point - Array containing [x, y] coordinates of the query point
29 * @returns Number of ways to form squares with the given point as one vertex
30 */
31function count(point: number[]): number {
32 const x1 = point[0];
33 const y1 = point[1];
34
35 // If no points exist with x-coordinate x1, no squares can be formed
36 if (!pointCount.has(x1)) {
37 return 0;
38 }
39
40 let totalSquares = 0;
41
42 // Get the map of y-coordinates and their counts for x1
43 const x1Points = pointCount.get(x1)!;
44
45 // Iterate through all unique x-coordinates in our point collection
46 for (const [x2, yCountMap] of pointCount.entries()) {
47 // Skip if x2 equals x1 (we need different x-coordinates for a square)
48 if (x2 !== x1) {
49 // Calculate the horizontal distance between x1 and x2
50 const distance = Math.abs(x2 - x1);
51
52 // Check for squares with diagonal from (x1, y1) to (x2, y1 + distance)
53 // This forms a square with vertices:
54 // (x1, y1), (x2, y1), (x2, y1 + distance), (x1, y1 + distance)
55 const countAtY1 = yCountMap.get(y1) || 0;
56 const countAtY1PlusDistance = yCountMap.get(y1 + distance) || 0;
57 const x1CountAtY1PlusDistance = x1Points.get(y1 + distance) || 0;
58 totalSquares += countAtY1 * x1CountAtY1PlusDistance * countAtY1PlusDistance;
59
60 // Check for squares with diagonal from (x1, y1) to (x2, y1 - distance)
61 // This forms a square with vertices:
62 // (x1, y1), (x2, y1), (x2, y1 - distance), (x1, y1 - distance)
63 const countAtY1MinusDistance = yCountMap.get(y1 - distance) || 0;
64 const x1CountAtY1MinusDistance = x1Points.get(y1 - distance) || 0;
65 totalSquares += countAtY1 * x1CountAtY1MinusDistance * countAtY1MinusDistance;
66 }
67 }
68
69 return totalSquares;
70}
71
Time and Space Complexity
Time Complexity:
-
add(point)
: The operation involves accessing a nested dictionary and incrementing a counter value. Both dictionary accessself.cnt[x]
and counter incrementself.cnt[x][y] += 1
areO(1)
operations. Therefore, the time complexity isO(1)
. -
count(point)
: The method iterates through all unique x-coordinates inself.cnt.keys()
. For each x-coordinatex2
different fromx1
, it performs constant-time lookups in the nested dictionary structure (accessing counter values). The number of unique x-coordinates can be at mostn
wheren
is the total number of points added. Therefore, the time complexity isO(n)
.
Space Complexity:
- The data structure uses a nested dictionary where the outer dictionary stores x-coordinates as keys, and each value is a
Counter
object storing y-coordinate frequencies. In the worst case, alln
points added could be unique, requiring storage for all of them. Therefore, the space complexity isO(n)
, wheren
is the number of points in the data stream.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Attempting to Find All Four Corners Instead of Three
Pitfall: A common mistake is trying to find all four corners of a square when one corner (the query point) is already given. This leads to overcounting since the query point shouldn't be selected from the stored points.
Incorrect approach:
def count(self, point: List[int]) -> int:
x1, y1 = point
ans = 0
# Wrong: Looking for all 4 corners including the query point
for x2 in self.point_count:
if x2 != x1:
d = x2 - x1
# This would count the query point if it exists in storage
ans += (self.point_count[x1][y1] * # WRONG! Don't count query point
self.point_count[x2][y1] *
self.point_count[x1][y1 + d] *
self.point_count[x2][y1 + d])
return ans
Solution: The query point serves as one fixed corner. You only need to find three other points from the stored data structure to form a square.
2. Forgetting to Check Both Upward and Downward Squares
Pitfall: When fixing a horizontal edge from (x1, y1)
to (x2, y1)
, there are two possible squares: one extending upward and one extending downward. Many implementations only check one direction.
Incorrect approach:
def count(self, point: List[int]) -> int:
x1, y1 = point
ans = 0
for x2 in self.point_count:
if x2 != x1:
d = abs(x2 - x1) # Using abs loses direction information
# Only checking upward squares
ans += (self.point_count[x2][y1] *
self.point_count[x1][y1 + d] *
self.point_count[x2][y1 + d])
return ans
Solution: Always check both y1 + d
and y1 - d
directions. The side length d
should maintain its sign (positive or negative) to ensure the square is properly formed.
3. Using Absolute Value for Distance Calculation
Pitfall: Using abs(x2 - x1)
for the side length breaks the geometric relationship needed to form a proper square.
Why it's wrong: If x2
is to the left of x1
(negative d
), the square extending "upward" actually needs points at y1 + d
(which would be y1 + negative value = y1 - |d|
). Using absolute value would incorrectly look for points at y1 + |d|
.
Correct approach: Keep the signed distance d = x2 - x1
. This ensures:
- When
x2 > x1
(d > 0): Forms squares to the right - When
x2 < x1
(d < 0): Forms squares to the left The signed distance maintains the correct geometric relationships for both cases.
4. Not Handling Duplicate Points Correctly
Pitfall: Treating duplicate points as a single point or not properly counting all combinations when duplicates exist.
Example scenario: If point (0, 1)
is added 3 times and forms part of a valid square, you should count 3 different ways to pick this point.
Solution: Use a Counter or dictionary to track the count of each point, then multiply the counts when calculating combinations. The formula cnt[x2][y1] * cnt[x1][y1 + d] * cnt[x2][y1 + d]
naturally handles this by multiplication.
5. Inefficient Point Storage Structure
Pitfall: Using a list of points and checking all combinations with nested loops leads to O(n³) complexity for the count operation.
Inefficient approach:
def count(self, point: List[int]) -> int:
points = self.stored_points # List of all points
count = 0
# O(n³) - checking all triplets
for p1 in points:
for p2 in points:
for p3 in points:
if forms_square(point, p1, p2, p3):
count += 1
return count
Solution: Use the nested hash table structure defaultdict(Counter)
for O(1) point lookup and O(n) count operation where n is the number of unique x-coordinates.
How many times is a tree node visited in a depth first search?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!