|
| 1 | +# 3623. Count Number of Trapezoids I |
| 2 | + |
| 3 | +You are given a 2D integer array `points`, where `points[i] = [x_i, y_i]` represents the coordinates of the $i^{th}$ point on the Cartesian plane. |
| 4 | + |
| 5 | +A horizontal trapezoid is a convex quadrilateral with at least one pair of horizontal sides (i.e. parallel to the x-axis). |
| 6 | +Two lines are parallel if and only if they have the same slope. |
| 7 | + |
| 8 | +Return the number of unique horizontal trapezoids that can be formed by choosing any four distinct points from `points`. |
| 9 | + |
| 10 | +Since the answer may be very large, return it modulo `10^9 + 7`. |
| 11 | + |
| 12 | +**Constraints:** |
| 13 | + |
| 14 | +- `4 <= points.length <= 10^5` |
| 15 | +- `–10^8 <= x_i, y_i <= 10^8` |
| 16 | +- All points are pairwise distinct. |
| 17 | + |
| 18 | +## 基礎思路 |
| 19 | + |
| 20 | +本題要求計算所有能由四個不同點組成的「水平梯形」數量。 |
| 21 | +水平梯形的核心特徵為:至少要有一組上下底邊彼此平行且**水平**,也就是兩條邊必須位於不同的 y 座標上,並且各自由同一條水平線上的兩點形成。 |
| 22 | + |
| 23 | +可歸納以下核心觀察: |
| 24 | + |
| 25 | +* **水平線上的兩點可形成一條水平線段** |
| 26 | + 若在某個固定的 y 值上有 `c` 個點,則能形成水平線段的數量為 $\binom{c}{2}$。 |
| 27 | + |
| 28 | +* **一個水平梯形需要兩條位於不同 y 座標的水平線段** |
| 29 | + 因此只要從所有水平線段中挑選任意兩條,且它們分屬不同的 y 座標,即構成一個合法的水平梯形。 |
| 30 | + |
| 31 | +* **直接兩兩枚舉會超時,必須以組合方式計算** |
| 32 | + 若將所有水平線段總數記為 `S`,且每條 y 線上的線段數為 `s_i`, |
| 33 | + 則所有可挑兩條線段的方式為 $\binom{S}{2}$, |
| 34 | + 但其中包含了挑選同一 y 座標下的兩條線段(無法構成梯形),需扣除 $\sum \binom{s_i}{2}$。 |
| 35 | + |
| 36 | +* **組合式展開後可化為前綴形式:** |
| 37 | + |
| 38 | + $$ |
| 39 | + \binom{S}{2} - \sum \binom{s_i}{2} |
| 40 | + = \frac{S^2 - \sum s_i^2}{2} |
| 41 | + $$ |
| 42 | + |
| 43 | + 透過平方與平方和即可在不使用巢狀迴圈的情況下完成計算。 |
| 44 | + |
| 45 | +* **數值非常大,需使用 BigInt 並在過程中進行模運算** |
| 46 | + 過程需保持所有運算於模空間內,以避免溢位與效能問題。 |
| 47 | + |
| 48 | +基於上述分析,可以採用以下策略: |
| 49 | + |
| 50 | +* **以雜湊表統計每條水平線上的點數。** |
| 51 | +* **轉換每條水平線的點數為線段數,並累積線段總和與其平方總和。** |
| 52 | +* **透過組合式計算跨水平線的線段配對數量。** |
| 53 | +* **最後套用模運算與模反元素完成除以 2 的動作。** |
| 54 | + |
| 55 | +此策略能在一次掃描與一次彙整後完成所有計算,適合處理最大 `10^5` 筆資料。 |
| 56 | + |
| 57 | +## 解題步驟 |
| 58 | + |
| 59 | +### Step 1:初始化模常數與點數統計表 |
| 60 | + |
| 61 | +建立必要的模常數與雜湊表,用於記錄每個 y 座標出現的點數。 |
| 62 | + |
| 63 | +```typescript |
| 64 | +// 使用 BigInt 以確保大型數字運算的安全性 |
| 65 | +const modulus = 1000000007n; |
| 66 | +const modularInverseTwo = 500000004n; // 預先計算的 2 的模反元素 |
| 67 | + |
| 68 | +// 計算每條水平線上有多少個點(相同 y 值) |
| 69 | +const coordinateYToCountMap = new Map<number, number>(); |
| 70 | +const numberOfPoints = points.length; |
| 71 | + |
| 72 | +for (let index = 0; index < numberOfPoints; index += 1) { |
| 73 | + const coordinateY = points[index][1]; |
| 74 | + const existingCount = coordinateYToCountMap.get(coordinateY) ?? 0; |
| 75 | + coordinateYToCountMap.set(coordinateY, existingCount + 1); |
| 76 | +} |
| 77 | +``` |
| 78 | + |
| 79 | +### Step 2:累積所有水平線段數與其平方總和 |
| 80 | + |
| 81 | +逐一檢查每條 y 水平線上的點數,若至少兩點,則可形成線段。 |
| 82 | +我們累積線段總數與線段數平方的總和(後續用於組合式計算)。 |
| 83 | + |
| 84 | +```typescript |
| 85 | +// 這兩個變數分別儲存: |
| 86 | +// 1. 所有 y 線上的水平線段總數 |
| 87 | +// 2. 所有水平線段數量的平方總和(用於避免巢狀迴圈) |
| 88 | +let sumOfSegmentsModulo = 0n; |
| 89 | +let sumOfSegmentSquaresModulo = 0n; |
| 90 | + |
| 91 | +// 對每條水平線,計算其能產生多少線段 |
| 92 | +for (const countOfPointsOnLine of coordinateYToCountMap.values()) { |
| 93 | + if (countOfPointsOnLine >= 2) { |
| 94 | + const countBig = BigInt(countOfPointsOnLine); |
| 95 | + |
| 96 | + // 此水平線上所能形成的水平線段數 |
| 97 | + const numberOfSegments = (countBig * (countBig - 1n)) / 2n; |
| 98 | + |
| 99 | + const numberOfSegmentsModulo = numberOfSegments % modulus; |
| 100 | + |
| 101 | + // 累積所有線段數 |
| 102 | + sumOfSegmentsModulo = |
| 103 | + (sumOfSegmentsModulo + numberOfSegmentsModulo) % modulus; |
| 104 | + |
| 105 | + // 累積線段數平方(未來用於扣除同線上的組合) |
| 106 | + sumOfSegmentSquaresModulo = |
| 107 | + (sumOfSegmentSquaresModulo + |
| 108 | + (numberOfSegmentsModulo * numberOfSegmentsModulo) % modulus) % |
| 109 | + modulus; |
| 110 | + } |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +### Step 3:計算跨水平線的線段配對總數 |
| 115 | + |
| 116 | +使用前述公式: |
| 117 | + |
| 118 | +$$ |
| 119 | +\frac{S^2 - \sum s_i^2}{2} |
| 120 | +$$ |
| 121 | + |
| 122 | +先將全部線段總數平方,再扣除同水平線內的線段配對。 |
| 123 | + |
| 124 | +```typescript |
| 125 | +// 將總線段數平方,用於組合計算 |
| 126 | +const sumOfSegmentsSquaredModulo = |
| 127 | + (sumOfSegmentsModulo * sumOfSegmentsModulo) % modulus; |
| 128 | + |
| 129 | +// 扣除所有「同一條水平線上的線段互配」之組合,使只剩跨線的情況 |
| 130 | +let result = |
| 131 | + (sumOfSegmentsSquaredModulo - |
| 132 | + sumOfSegmentSquaresModulo + |
| 133 | + modulus) % modulus; |
| 134 | +``` |
| 135 | + |
| 136 | +### Step 4:透過模反元素完成除以 2,並回傳答案 |
| 137 | + |
| 138 | +套用除 2 的模運算,最後轉成一般數字回傳。 |
| 139 | + |
| 140 | +```typescript |
| 141 | +// 使用模反元素執行除以 2 |
| 142 | +result = (result * modularInverseTwo) % modulus; |
| 143 | + |
| 144 | +// 回傳最終答案(其值介於 0 至 1e9+7 之間,可安全轉 number) |
| 145 | +return Number(result); |
| 146 | +``` |
| 147 | + |
| 148 | +## 時間複雜度 |
| 149 | + |
| 150 | +- 需掃描所有點一次以統計 y 座標。 |
| 151 | +- 需掃描所有不同 y 類別一次以計算組合資料。 |
| 152 | +- 其餘皆為常數時間運算。 |
| 153 | +- 總時間複雜度為 $O(n)$。 |
| 154 | + |
| 155 | +> $O(n)$ |
| 156 | +
|
| 157 | +## 空間複雜度 |
| 158 | + |
| 159 | +- 需要一個雜湊表儲存所有 y 座標的點數。 |
| 160 | +- 類別最多與點數同階,因此空間為線性。 |
| 161 | +- 總空間複雜度為 $O(n)$。 |
| 162 | + |
| 163 | +> $O(n)$ |
0 commit comments