Medium草稿★★★★★O(1) 時間 · O(n) 空間
MathGeometryHash TableDesign
Patterns🏗️ 資料結構設計#️⃣ 雜湊🧮 數學・組合
尚未複習過

解法已隱藏 — 先讀題目敘述、自己想想看,再點上方按鈕揭曉。

2013Detect SquaresMath & GeometryMediumMath & Geometry

設計一個資料結構,支援兩個操作:add(point) 加入一個點,count(point) 計算與給定點能組成軸對齊正方形的方式數。正方形的邊必須平行於 x 軸和 y 軸,且面積大於 0。

Example:

Input: [“DetectSquares”,“add”,“add”,“add”,“count”] Output: [[],[],[],[],[1]] (加入 [3,10],[11,2],[3,2] 後,count([11,10]) 回傳 1)

Intuition

TIP

核心思路:固定查詢點和一個對角點,用邊長推算另外兩個角的位置,查 Hash Map 確認是否存在。

Approaches

1. Brute Force: Enumerate All Pairs — add O(1), count O(n²) / O(n)
  • Idea: 對每個查詢,枚舉所有已加入的點對,檢查能否與查詢點構成正方形
  • Time: add O(1),count O(n^2) - n 為已加入的點數
  • Space: O(n)
class DetectSquares() {
    private val points = mutableListOf<IntArray>()

    fun add(point: IntArray) {
        points.add(point)
    }

    fun count(point: IntArray): Int {
        val (px, py) = point
        var count = 0
        for (p1 in points) {
            for (p2 in points) {
                if (p1[0] == px && p2[1] == py &&
                    Math.abs(p1[1] - py) == Math.abs(p2[0] - px) &&
                    p1[1] != py && p2[0] != px &&
                    p1[1] == p2[1] && Math.abs(p2[0] - px) > 0) {
                    // 不好寫也容易錯,見下方最佳解
                }
            }
        }
        return count
    }
}
⭐ 2. Hash Map + Enumerate Diagonal Points — add O(1), count O(n) / O(n)
  • Idea: 用 Hash Map 記錄每個座標出現的次數。枚舉所有與查詢點 y 座標不同的已知點作為對角點,計算另外兩個角是否存在
  • Time: add O(1),count O(n) - n 為不重複的點數
  • Space: O(n)
class DetectSquares() {
    private val pointCount = HashMap<Long, Int>()
    private val pointsByX = HashMap<Int, MutableList<IntArray>>()

    private fun encode(x: Int, y: Int): Long = x.toLong() * 100001 + y

    fun add(point: IntArray) {
        val key = encode(point[0], point[1])
        pointCount[key] = pointCount.getOrDefault(key, 0) + 1
        pointsByX.getOrPut(point[0]) { mutableListOf() }.add(point)
    }

    fun count(point: IntArray): Int {
        val (px, py) = point
        var result = 0
        // 枚舉與 px 相同 x 座標的點,作為正方形的同一條垂直邊
        val sameX = pointsByX[px] ?: return 0
        for (p in sameX) {
            val qy = p[1]
            if (qy == py) continue // 邊長不能為 0
            val side = qy - py
            // 正方形的另外兩個角
            val c1 = pointCount.getOrDefault(encode(px + side, py), 0)
            val c2 = pointCount.getOrDefault(encode(px + side, qy), 0)
            result += c1 * c2
            val c3 = pointCount.getOrDefault(encode(px - side, py), 0)
            val c4 = pointCount.getOrDefault(encode(px - side, qy), 0)
            result += c3 * c4
        }
        return result
    }
}

WARNING

注意重複點的處理:同一個座標可以被加入多次,因此用 count 來計數,結果要乘上各角的出現次數。

Note: Alternative Enumeration

也可以枚舉對角點(x, y 都不同於查詢點且構成正方形),但需要確保 |dx| == |dy|:

class DetectSquares() {
    private val countMap = HashMap<Long, Int>()
    private val allPoints = mutableListOf<IntArray>()

    private fun encode(x: Int, y: Int): Long = x.toLong() * 100001 + y

    fun add(point: IntArray) {
        val key = encode(point[0], point[1])
        countMap[key] = countMap.getOrDefault(key, 0) + 1
        allPoints.add(point)
    }

    fun count(point: IntArray): Int {
        val (px, py) = point
        var result = 0
        for (p in allPoints) {
            val (x, y) = p
            // 對角點必須 x != px, y != py, 且 |dx| == |dy|
            if (Math.abs(x - px) != Math.abs(y - py) || x == px || y == py) continue
            result += countMap.getOrDefault(encode(x, py), 0) *
                      countMap.getOrDefault(encode(px, y), 0)
        }
        return result
    }
}

這種方法每次 count 遍歷所有已加入的點(含重複),但避免了重複計數的問題。

🔑 Takeaways