Facebook Pixel

2013. Detect Squares

MediumDesignArrayHash TableCounting
Leetcode Link

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:

  1. Constructor DetectSquares(): Initializes an empty data structure to store points.

  2. 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.

  3. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Early termination: If there are no points with x-coordinate x1, we can't form any squares, so return 0.

  2. Enumerate horizontal partners: For each x-coordinate x2 in our data structure where x2 ≠ 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 whether x2 is to the right or left of x1
  3. 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
  4. 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 Evaluator

Example 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.

  1. Extract query point: x1 = 0, y1 = 0

  2. Check if x1 exists: x1 = 0 is in cnt, so we continue.

  3. 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
  4. 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:

  1. Fixing a horizontal edge from the query point
  2. Calculating the required positions of the other two corners
  3. 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 access self.cnt[x] and counter increment self.cnt[x][y] += 1 are O(1) operations. Therefore, the time complexity is O(1).

  • count(point): The method iterates through all unique x-coordinates in self.cnt.keys(). For each x-coordinate x2 different from x1, it performs constant-time lookups in the nested dictionary structure (accessing counter values). The number of unique x-coordinates can be at most n where n is the total number of points added. Therefore, the time complexity is O(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, all n points added could be unique, requiring storage for all of them. Therefore, the space complexity is O(n), where n 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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

How many times is a tree node visited in a depth first search?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More