Detect Squares

Detect Squares is a design question. You build a small data structure that keeps taking points. Then it can quickly count how many squares it can form with a query point. The interviewer wants to see if you can pick the right structure so the counting stays fast.

🎯 The Problem

You build an object with two actions that work on a stream of points.

An axis-aligned square is a square whose sides are parallel to the grid lines. So no tilted squares. Each side is straight up or straight across.

The rules:

  • add puts a point on the grid. The same point can be added many times.
  • count takes a query point. It returns how many axis-aligned squares form using that query point and three points already added.
  • A square needs four corners. One is the query. The other three come from added points.
  • The same point added many times counts many times.

Let us add the points (3, 10), (11, 2), and (3, 2). Then we query the point (11, 10). These four points form one square with side length 8. So the count is 1.

add (3, 10)
add (11, 2)
add (3, 2)
count (11, 10) -> 1
Explanation:
The four corners (3,10), (11,10), (11,2), (3,2)
form one axis-aligned square of side 8.

Here are the four corners of that one square on the grid.

3,10 top left

11,10 query

3,2 bottom left

11,2 bottom right

🐒 Approach 1: Try Every Triple (Brute Force)

The idea:

  • Store every added point in a list.
  • For a query, try every group of three stored points.
  • Check if those three plus the query form a square. Count the ones that work.

Why it is weak:

  • The number of groups of three grows very fast as points pile up.
  • Checking every triple per query is far too much work.
  • This times out on a real test.

Here is the try-every-triple code:

detect_squares_brute_force.py
class DetectSquares:
def __init__(self):
self.points = []
def add(self, point):
self.points.append(tuple(point))
def count(self, point):
x, y = point
answer = 0
for x1, y1 in self.points:
for x2, y2 in self.points:
for x3, y3 in self.points:
if (x1, y1) == (x, y):
continue
xs = sorted([x, x1, x2, x3])
ys = sorted([y, y1, y2, y3])
if xs[0] == xs[1] and xs[2] == xs[3] and ys[0] == ys[1] and ys[2] == ys[3] and xs[0] != xs[2] and ys[0] != ys[2]:
answer += 1
return answer

⚑ Approach 2: Pick the Diagonal, Count With a Hash Map (Best)

The idea in one line: loop over candidate diagonal corners only, and once a diagonal is fixed the other two corners are forced.

The setup:

  • Keep a hash map from each point to how many times it was added.
  • A hash map stores a key and a value and looks up the key almost instantly.
  • The key is the point. The value is its count.
  • Also keep a list of distinct points to loop over.

How it works:

  • Call the query point (qx, qy).
  • Loop over each distinct stored point (px, py).
  • Keep only points where the horizontal gap equals the vertical gap, both nonzero. That is a real diagonal corner.
  • The other two corners are then forced: (qx, py) and (px, qy).
  • Multiply the three corner counts. Add that product to the total.

Why it is fast:

  • Each query loops over the distinct points once.
  • The other corners are instant map lookups.
  • So each query is O(n), linear in the number of distinct points.

This is the operation log for the example, showing the counts grow then the query.

add 3,10 -> count 1

add 11,2 -> count 1

add 3,2 -> count 1

count 11,10

diagonal 3,2 side 8

multiply 1 x 1 x 1 = 1

Steps to Solve

  1. Keep a hash map from each point to its count.
  2. On add, increase the count for that point by one.
  3. On count, start a total at zero.
  4. Loop over every distinct stored point. Skip points on the same row or same column as the query.
  5. Keep only points where the horizontal gap equals the vertical gap. That is a diagonal corner.
  6. Multiply the counts of the diagonal point, (qx, py), and (px, qy). Add that product to the total.
  7. Return the total.

This Python version uses a dictionary from each point to its count, which is the cleanest form.

detect_squares.py
class DetectSquares:
def __init__(self):
self.counts = {} # (x, y) -> how many times added
self.points = [] # list of distinct points
def add(self, x, y):
if (x, y) not in self.counts:
self.points.append((x, y))
self.counts[(x, y)] = 0
self.counts[(x, y)] += 1
def count(self, qx, qy):
result = 0
for px, py in self.points:
dx = px - qx
dy = py - qy
# real diagonal: equal gaps, not same row or column
if dx != 0 and (dx == dy or dx == -dy):
result += (self.counts[(px, py)]
* self.counts.get((qx, py), 0)
* self.counts.get((px, qy), 0))
return result
ds = DetectSquares()
ds.add(3, 10)
ds.add(11, 2)
ds.add(3, 2)
print(ds.count(11, 10))

The output of the above code will be:

1

Let us walk through the Python version line by line.

def add(self, x, y):
if (x, y) not in self.counts:
self.points.append((x, y))
self.counts[(x, y)] = 0
self.counts[(x, y)] += 1

The first time we see a point, we add it to the points list and set its count to zero. The list holds only distinct points, so the query loop never repeats work. Then we increase the count by one. So a point added three times has a count of three.

for px, py in self.points:
dx = px - qx
dy = py - qy

We loop over each distinct stored point. dx is the horizontal gap from the query to this point. dy is the vertical gap. These two gaps decide if this point can be a diagonal corner.

if dx != 0 and (dx == dy or dx == -dy):

This is the key test. dx != 0 throws out points on the same column as the query. It also rules out the same row, because if dx is zero the gaps cannot be equal and nonzero. The part dx == dy or dx == -dy means the horizontal gap and the vertical gap have the same size. Equal sides means a square. So this point sits on a real diagonal from the query.

result += (self.counts[(px, py)]
* self.counts.get((qx, py), 0)
* self.counts.get((px, qy), 0))

Once we have the diagonal point, the other two corners are fixed. They are (qx, py) and (px, qy). We multiply three counts. The diagonal point may itself have been added many times, and so may the side corners. The product counts every combination. We use .get(..., 0) so a missing corner contributes zero, not an error.

return result

After the loop, result holds the total number of squares for this query.

⏱️ Time and Space Complexity

The brute force checks every group of three points, which grows very fast and is far too slow. The hash map version loops over the distinct points once per query, so each query is O(n) where n is the number of distinct points. The add is O(1). The space is O(n) to hold the points and counts.

Operation Time Complexity Space Complexity
count (try every triple, brute force) O(nΒ³) O(n)
add (hash map) O(1) O(n)
count (hash map) O(n) O(n)

Tip

The big idea is to pick the diagonal corner first. Once the diagonal is fixed, the other two corners are forced. So you only loop over candidate diagonals, never over all triples. That is what makes each query fast.

🧩 Key Takeaways

  • βœ… Store each point with a count, because the same point can be added many times.
  • βœ… For a query, loop over stored points and keep only true diagonal corners.
  • βœ… A diagonal corner has an equal horizontal gap and vertical gap, both nonzero.
  • βœ… Once the diagonal is fixed, the other two corners are forced, so just multiply three counts.
  • βœ… This makes each query linear in the number of distinct points, not the number of triples.

Check Your Knowledge

4 questions Show quiz Hide quiz

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What kind of square does the Detect Squares problem count?

    Why: The problem counts axis-aligned squares, meaning their sides line up with the grid.

  2. 2

    Why store a count for each point instead of just whether it exists?

    Why: A point can be added multiple times, so the number of squares multiplies by its count.

  3. 3

    How do you find a diagonal corner for a query point?

    Why: Equal nonzero horizontal and vertical gaps mean the point sits on a real square diagonal.

  4. 4

    Once the diagonal corner is chosen, how do you count the squares it gives?

    Why: The other two corners are forced, so you multiply the three corner counts to count all combinations.

πŸš€ What’s Next?