|
| 1 | +# 3347. Maximum Frequency of an Element After Performing Operations II |
| 2 | + |
| 3 | +You are given an integer array `nums` and two integers `k` and `numOperations`. |
| 4 | + |
| 5 | +You must perform an operation `numOperations` times on `nums`, where in each operation you: |
| 6 | + |
| 7 | +- Select an index `i` that was not selected in any previous operations. |
| 8 | +- Add an integer in the range `[-k, k]` to `nums[i]`. |
| 9 | + |
| 10 | +Return the maximum possible frequency of any element in `nums` after performing the operations. |
| 11 | + |
| 12 | +**Constraints:** |
| 13 | + |
| 14 | +- `1 <= nums.length <= 10^5` |
| 15 | +- `1 <= nums[i] <= 10^9` |
| 16 | +- `0 <= k <= 10^9` |
| 17 | +- `0 <= numOperations <= nums.length` |
| 18 | + |
| 19 | +## 基礎思路 |
| 20 | + |
| 21 | +本題要我們在一個整數陣列中進行最多 `numOperations` 次操作。每次操作可選擇一個**未被選過的索引** `i`,並將 `nums[i]` 加上一個介於 `[-k, k]` 的整數。最終需找出任意元素能達到的**最高出現頻率**。 |
| 22 | + |
| 23 | +在思考解法時,我們需要特別注意幾個要點: |
| 24 | + |
| 25 | +- 每次操作只能對**不同索引**進行一次; |
| 26 | +- 每個元素的值可在範圍 `[nums[i] - k, nums[i] + k]` 內自由調整; |
| 27 | +- 若兩個數值區間有重疊,就可能被調整成相同的數; |
| 28 | +- 我們希望透過最多 `numOperations` 次調整,讓某個數值的出現頻率最大化。 |
| 29 | + |
| 30 | +為了解決這個問題,可以採取以下策略: |
| 31 | + |
| 32 | +1. **排序後分析鄰近關係**:因為相近的數值較容易透過調整重合,所以先排序以方便使用滑動視窗。 |
| 33 | +2. **滑動視窗找最大可重疊範圍**:找出在區間長度不超過 `2k` 的最大子集,代表這些元素可被調成同一值。 |
| 34 | +3. **考慮現有元素為目標值的情況**:對每個不同數值,計算多少數在 `[value - k, value + k]` 範圍內可被轉為該值。 |
| 35 | +4. **結合兩種情境**: |
| 36 | + - 一種是任意目標(可自由選目標值); |
| 37 | + - 另一種是選用現有元素作為目標; |
| 38 | + 最後取兩者的最大值作為答案。 |
| 39 | + |
| 40 | +## 解題步驟 |
| 41 | + |
| 42 | +### Step 1:處理空陣列的特例 |
| 43 | + |
| 44 | +若陣列為空,直接回傳 0。 |
| 45 | + |
| 46 | +```typescript |
| 47 | +// 若陣列為空,無法形成頻率,直接回傳 0 |
| 48 | +if (nums.length === 0) { |
| 49 | + return 0; |
| 50 | +} |
| 51 | +``` |
| 52 | + |
| 53 | +### Step 2:初始化排序陣列 |
| 54 | + |
| 55 | +使用 `Int32Array` 儲存並排序,確保運算一致且利於滑動視窗。 |
| 56 | + |
| 57 | +```typescript |
| 58 | +// 建立型別化陣列以提升數值處理效率,並排序(遞增) |
| 59 | +const arr = Int32Array.from(nums); |
| 60 | +arr.sort(); |
| 61 | + |
| 62 | +const n = arr.length; |
| 63 | +``` |
| 64 | + |
| 65 | +### Step 3:Case A — 任意目標值(可自由調整成同一區間內) |
| 66 | + |
| 67 | +使用滑動視窗找出最大範圍,使最大值與最小值差不超過 `2k`。 |
| 68 | +這代表所有這些數都可被調整至同一數值。 |
| 69 | + |
| 70 | +```typescript |
| 71 | +// 使用滑動視窗找出最大範圍 (max - min ≤ 2k) |
| 72 | +let leftPointer = 0; |
| 73 | +let maxWithinRange = 1; |
| 74 | + |
| 75 | +for (let rightPointer = 0; rightPointer < n; rightPointer++) { |
| 76 | + // 若視窗寬度超出 2k,向右收縮左指標 |
| 77 | + while (arr[rightPointer] - arr[leftPointer] > 2 * k) { |
| 78 | + leftPointer += 1; |
| 79 | + } |
| 80 | + const windowSize = rightPointer - leftPointer + 1; |
| 81 | + if (windowSize > maxWithinRange) { |
| 82 | + maxWithinRange = windowSize; // 更新最大區間長度 |
| 83 | + } |
| 84 | +} |
| 85 | + |
| 86 | +// 根據操作上限取最小值(不能超過 numOperations) |
| 87 | +const bestArbitrary = Math.min(maxWithinRange, numOperations); |
| 88 | +``` |
| 89 | + |
| 90 | +### Step 4:Case B — 以現有元素作為目標值 |
| 91 | + |
| 92 | +逐一考慮每個不同數值 `v`,找出所有可被轉為 `v` 的元素數量。 |
| 93 | +統計當前值的出現次數、在 `[v - k, v + k]` 範圍內的總元素數,並計算可能轉換數量。 |
| 94 | + |
| 95 | +```typescript |
| 96 | +// 初始化最佳結果與雙指標 |
| 97 | +let bestExisting = 1; |
| 98 | +let leftBound = 0; |
| 99 | +let rightBound = -1; |
| 100 | +let startIndex = 0; |
| 101 | + |
| 102 | +// 逐一處理每個不同的數值群組 |
| 103 | +while (startIndex < n) { |
| 104 | + let endIndex = startIndex; |
| 105 | + const value = arr[startIndex]; |
| 106 | + |
| 107 | + // 找出同值的群組範圍 |
| 108 | + while (endIndex + 1 < n && arr[endIndex + 1] === value) { |
| 109 | + endIndex += 1; |
| 110 | + } |
| 111 | + |
| 112 | + // 定義可轉換範圍 [value - k, value + k] |
| 113 | + const minAllowed = value - k; |
| 114 | + const maxAllowed = value + k; |
| 115 | + |
| 116 | + // 向右移動 leftBound,確保 arr[leftBound] >= minAllowed |
| 117 | + while (leftBound < n && arr[leftBound] < minAllowed) { |
| 118 | + leftBound += 1; |
| 119 | + } |
| 120 | + |
| 121 | + // 擴展 rightBound,直到 arr[rightBound] > maxAllowed |
| 122 | + while (rightBound + 1 < n && arr[rightBound + 1] <= maxAllowed) { |
| 123 | + rightBound += 1; |
| 124 | + } |
| 125 | + |
| 126 | + // 當前值出現次數 |
| 127 | + const countEqual = endIndex - startIndex + 1; |
| 128 | + |
| 129 | + // 在可轉換範圍內的總元素數 |
| 130 | + const totalWithin = rightBound >= leftBound ? (rightBound - leftBound + 1) : 0; |
| 131 | + |
| 132 | + // 可被轉換成當前值的數量 |
| 133 | + const convertible = totalWithin > countEqual ? (totalWithin - countEqual) : 0; |
| 134 | + |
| 135 | + // 計算選此值為目標時可達最大頻率 |
| 136 | + const candidate = countEqual + Math.min(numOperations, convertible); |
| 137 | + if (candidate > bestExisting) { |
| 138 | + bestExisting = candidate; // 更新最佳結果 |
| 139 | + } |
| 140 | + |
| 141 | + // 移動至下一組不同數值 |
| 142 | + startIndex = endIndex + 1; |
| 143 | +} |
| 144 | +``` |
| 145 | + |
| 146 | +### Step 5:合併兩種情境並回傳最終結果 |
| 147 | + |
| 148 | +取兩種策略的最大值,且不得超過陣列長度。 |
| 149 | + |
| 150 | +```typescript |
| 151 | +// 結合兩種策略結果,並確保不超過 n |
| 152 | +const best = Math.max(bestExisting, bestArbitrary); |
| 153 | +return best < n ? best : n; |
| 154 | +``` |
| 155 | + |
| 156 | +## 時間複雜度 |
| 157 | + |
| 158 | +- 排序需 $O(n \log n)$; |
| 159 | +- Case A 與 Case B 各使用滑動視窗掃描一次,皆為 $O(n)$; |
| 160 | +- 總時間複雜度為 $O(n \log n)$。 |
| 161 | + |
| 162 | +> $O(n \log n)$ |
| 163 | +
|
| 164 | +## 空間複雜度 |
| 165 | + |
| 166 | +- 使用一份排序陣列與少量指標變數; |
| 167 | +- 其餘操作皆為原地運算,額外空間為常數級。 |
| 168 | +- 總空間複雜度為 $O(n)$(主要來自複製陣列)。 |
| 169 | + |
| 170 | +> $O(n)$ |
0 commit comments