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.
