2013. Detect Squares - Explanation

Problem Link

Description

You are given a stream of points consisting of x-y coordinates on a 2-D plane. Points can be added and queried as follows:

  • Add - new points can be added to the stream into a data structure. Duplicate points are allowed and should be treated as separate points.
  • Query - Given a single query point, count the number of ways to choose three additional points from the data structure such that the three points and the query point form a square. The square must have all sides parallel to the x-axis and y-axis, i.e. no diagonal squares are allowed. Recall that a square must have four equal sides.

Implement the CountSquares class:

  • CountSquares() Initializes the object.
  • void add(int[] point) Adds a new point point = [x, y].
  • int count(int[] point) Counts the number of ways to form valid squares with point point = [x, y] as described above.

Example 1:

Input: 
["CountSquares", "add", [[1, 1]], "add", [[2, 2]], "add", [[1, 2]], "count", [[2, 1]], "count", [[3, 3]], "add", [[2, 2]], "count", [[2, 1]]]
       
Output:
[null, null, null, null, 1, 0, null, 2]

Explanation:
CountSquares countSquares = new CountSquares();
countSquares.add([1, 1]);
countSquares.add([2, 2]);
countSquares.add([1, 2]);

countSquares.count([2, 1]);   // return 1.
countSquares.count([3, 3]);   // return 0.
countSquares.add([2, 2]);     // Duplicate points are allowed.
countSquares.count([2, 1]);   // return 2. 

Constraints:

  • point.length == 2
  • 0 <= x, y <= 1000


Topics

Recommended Time & Space Complexity

You should aim for a solution with O(1) time for each add() call, O(n) time for each count() call, and O(n) space, where n is the total number of points.


Hint 1

Initially, we can store the points in a global list for the add() call. For the count() call, a brute force approach would use three nested loops to check other points except for the query point, resulting in an O(n^3) time solution. Can you think of a better way? Maybe you should consider the observation that can be drawn from the diagonal of a square.


Hint 2

In a square's diagonal, the absolute difference between the x-coordinates is equal to the absolute difference between the y-coordinates of the two endpoints, and neither difference can be zero. Using these two points, we can determine the other diagonal endpoints.


Hint 3

We store points in a hash map instead of a list for O(1) lookups, treating duplicate points as one while tracking their frequencies. For the count() function, we iterate through points that, along with the query point, can form a diagonal. Let the query point be (qx, qy) and the other point be (x, y), ensuring they form a diagonal. What could be the other two points? Maybe you should consider the points forming a right-to-left diagonal, treating (qx, qy) as the top-right corner.


Hint 4

The other two points are point1 (x, qy) and point2 (qx, y). For counting, we simply add count of point1 * count of point2 to the result res.


Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Hash Maps - Using dictionaries to store and retrieve point counts in O(1) time
  • Coordinate Geometry - Understanding axis-aligned squares and diagonal relationships between corners
  • Nested Hash Maps - Using two-level hash maps to efficiently query points by x-coordinate then y-coordinate

1. Hash Map - I

Intuition

We are asked to count how many axis-aligned squares can be formed using a given point as one corner and previously added points as the other three corners.

Key observations about an axis-aligned square:

  • All sides are parallel to the x-axis and y-axis
  • If (px, py) is one corner and (x, y) is the diagonal opposite corner, then:
    • |px - x| == |py - y| (equal side lengths)
    • x != px and y != py (otherwise it would not form a square)
  • The other two required corners must be:
    • (x, py)
    • (px, y)

So the idea is:

  • Fix the query point (px, py)
  • Try every previously added point (x, y) as a possible diagonal
  • If it forms a valid square diagonal, multiply how many times the other two required points exist

A hash map lets us quickly check how many times a specific point was added.

Algorithm

Data Structures

  • ptsCount: a hash map storing how many times each point appears
  • pts: a list of all added points (including duplicates)

add(point)

  1. Increment the count of point in ptsCount
  2. Append point to the list pts

count(point)

  1. Initialize res = 0
  2. Let (px, py) be the query point
  3. For every stored point (x, y) in pts:
    • Check if (x, y) can be the diagonal opposite corner:
      • |px - x| == |py - y|
      • x != px and y != py
    • If not valid, skip
  4. If valid:
    • The other two corners must be (x, py) and (px, y)
    • Add to the result:
      • ptsCount[(x, py)] * ptsCount[(px, y)]
  5. Return res
class CountSquares:
    def __init__(self):
        self.ptsCount = defaultdict(int)
        self.pts = []

    def add(self, point: List[int]) -> None:
        self.ptsCount[tuple(point)] += 1
        self.pts.append(point)

    def count(self, point: List[int]) -> int:
        res = 0
        px, py = point
        for x, y in self.pts:
            if (abs(py - y) != abs(px - x)) or x == px or y == py:
                continue
            res += self.ptsCount[(x, py)] * self.ptsCount[(px, y)]
        return res

Time & Space Complexity

  • Time complexity: O(1)O(1) for add()add(), O(n)O(n) for count()count().
  • Space complexity: O(n)O(n)

2. Hash Map - II

Intuition

We want to count how many axis-aligned squares can be formed using the given query point (x1, y1) as one corner.

For an axis-aligned square:

  • One side is vertical and one side is horizontal
  • If we pick another point (x1, y2) on the same vertical line (same x1), then:
    • the side length is side = y2 - y1
    • this determines where the square’s other x-coordinates must be:
      • x3 = x1 + side (square to the right)
      • x4 = x1 - side (square to the left)

So for each possible vertical partner (x1, y2), we can form up to two squares:

  1. Square to the right needs points:
    • (x3, y1) and (x3, y2)
  2. Square to the left needs points:
    • (x4, y1) and (x4, y2)

Because points can be added multiple times, the total number of squares is the product of the counts of the required points.

This version uses a nested hash map:

  • ptsCount[x][y] = how many times point (x, y) was added
    which makes counting fast and avoids storing a list of all points.

Algorithm

Data Structure

  • ptsCount: nested hash map
    • outer key: x-coordinate
    • inner key: y-coordinate
    • value: count of that point

add(point)

  1. Let the point be (x, y).
  2. Increment ptsCount[x][y].

count(point)

  1. Initialize res = 0.
  2. Let the query point be (x1, y1).
  3. Iterate over all y2 such that a point (x1, y2) exists (same x-coordinate):
  4. Compute the side length:
    • side = y2 - y1
    • If side == 0, skip (same point, no square)
  5. Compute possible horizontal positions:
    • x3 = x1 + side (right square)
    • x4 = x1 - side (left square)
  6. Add squares formed to the right:
    • ptsCount[x1][y2] * ptsCount[x3][y1] * ptsCount[x3][y2]
  7. Add squares formed to the left:
    • ptsCount[x1][y2] * ptsCount[x4][y1] * ptsCount[x4][y2]
  8. Return res.
class CountSquares:

    def __init__(self):
        self.ptsCount = defaultdict(lambda: defaultdict(int))

    def add(self, point: List[int]) -> None:
        self.ptsCount[point[0]][point[1]] += 1

    def count(self, point: List[int]) -> int:
        res = 0
        x1, y1 = point
        for y2 in self.ptsCount[x1]:
            side = y2 - y1
            if side == 0:
                continue

            x3, x4 = x1 + side, x1 - side
            res += (self.ptsCount[x1][y2] * self.ptsCount[x3][y1] *
                    self.ptsCount[x3][y2])

            res += (self.ptsCount[x1][y2] * self.ptsCount[x4][y1] *
                    self.ptsCount[x4][y2])
        return res

Time & Space Complexity

  • Time complexity: O(1)O(1) for add()add(), O(n)O(n) for count()count().
  • Space complexity: O(n)O(n)

Common Pitfalls

Forgetting to Check Both Diagonal Conditions

A common mistake is only checking that the side lengths are equal (|px - x| == |py - y|) without also verifying that the points are not on the same horizontal or vertical line. Both conditions are necessary for a valid square diagonal.

# Wrong: Missing the x != px and y != py checks
if abs(py - y) == abs(px - x):  # Accepts invalid diagonals
    ...

# Correct: Must also ensure points form a true diagonal
if abs(py - y) == abs(px - x) and x != px and y != py:
    ...

Not Handling Duplicate Points

The problem allows adding the same point multiple times. Using a set instead of a counter for point tracking will lose count information, causing incorrect results when the same point appears multiple times. The number of squares should multiply by the count of each required corner point.

Confusing Diagonal vs Adjacent Corners

When iterating through stored points to find potential squares, some solutions incorrectly look for adjacent corners instead of diagonal corners. Given the query point (px, py) and a candidate diagonal (x, y), the other two corners must be (x, py) and (px, y), not (px + side, py) and (px, py + side).