|
| 1 | +# 2048. Next Greater Numerically Balanced Number |
| 2 | + |
| 3 | +An integer `x` is numerically balanced if for every digit `d` in the number `x`, there are exactly `d` occurrences of that digit in `x`. |
| 4 | + |
| 5 | +Given an integer `n`, return the smallest numerically balanced number strictly greater than `n`. |
| 6 | + |
| 7 | +**Constraints:** |
| 8 | + |
| 9 | +- `0 <= n <= 10^6` |
| 10 | + |
| 11 | +## 基礎思路 |
| 12 | + |
| 13 | +本題要求找出**嚴格大於給定整數 `n` 的最小數值平衡數(numerically balanced number)**。 |
| 14 | + |
| 15 | +一個整數被稱為數值平衡數,是指其每個數位 `d` 在整個數中**恰好出現 `d` 次**。 |
| 16 | +例如: |
| 17 | + |
| 18 | +- `22` 是平衡的(數位 2 出現兩次)。 |
| 19 | +- `1333` 是平衡的(1 出現一次,3 出現三次)。 |
| 20 | +- `122` 則不是(2 出現兩次沒錯,但 1 只出現一次,不平衡於 2)。 |
| 21 | + |
| 22 | +題目限制 `0 ≤ n ≤ 10^6`,而符合條件的數字不可能含有超過 7 的數位, |
| 23 | +因為若要出現 8 次數位 `8`,至少需 8 位數以上。因此所有合法的平衡數都在 7 位以內。 |
| 24 | + |
| 25 | +基於此觀察,可採用 **預先生成 + 二分搜尋** 的策略: |
| 26 | + |
| 27 | +1. **預生成所有平衡數** |
| 28 | + 列舉所有由數位 {1,…,7} 所構成的子集合, |
| 29 | + 並對每個子集產生「每個數位 d 出現 d 次」的所有排列組合。 |
| 30 | + 由於總位數上限為 7,生成總量有限。 |
| 31 | + |
| 32 | +2. **排序與壓縮儲存** |
| 33 | + 將所有結果排序後以 `Uint32Array` 儲存,利於高效查詢。 |
| 34 | + |
| 35 | +3. **查詢階段使用二分搜尋** |
| 36 | + 對給定 `n`,使用 `upperBound` 尋找第一個嚴格大於 `n` 的元素。 |
| 37 | + |
| 38 | +## 解題步驟 |
| 39 | + |
| 40 | +### Step 1:`upperBound` — 已排序陣列中找第一個嚴格大於目標的元素 |
| 41 | + |
| 42 | +說明:在升冪排序的數列中,使用二分搜尋回傳第一個 `> target` 的索引;若不存在則回傳長度。 |
| 43 | + |
| 44 | +```typescript |
| 45 | +/** |
| 46 | + * Perform a binary search to find the first element strictly greater than the given target. |
| 47 | + * @param sortedArray - A sorted Uint32Array of ascending numbers. |
| 48 | + * @param targetValue - The number to compare against. |
| 49 | + * @returns The index of the first element > targetValue, or sortedArray.length if not found. |
| 50 | + */ |
| 51 | +function upperBound(sortedArray: Uint32Array, targetValue: number): number { |
| 52 | + let leftIndex = 0 |
| 53 | + let rightIndex = sortedArray.length |
| 54 | + |
| 55 | + while (leftIndex < rightIndex) { |
| 56 | + const middleIndex = (leftIndex + rightIndex) >>> 1 |
| 57 | + if (sortedArray[middleIndex] > targetValue) { |
| 58 | + rightIndex = middleIndex |
| 59 | + } else { |
| 60 | + leftIndex = middleIndex + 1 |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + return leftIndex |
| 65 | +} |
| 66 | +``` |
| 67 | + |
| 68 | +### Step 2:`PRECOMPUTED_NUMERICALLY_BALANCED_NUMBERS` — 預先生成所有位數 ≤ 7 的平衡數 |
| 69 | + |
| 70 | +說明:以位元子集枚舉 `{1..7}`,計算該子集的總位數(各數位 `d` 需出現 `d` 次),若總位數 ≤ 7,則以遞迴回溯產生所有排列;最後排序並打包成 `Uint32Array`。 |
| 71 | + |
| 72 | +```typescript |
| 73 | +/** |
| 74 | + * Precompute all numerically balanced numbers with up to 7 digits (using digits 1 through 7). |
| 75 | + * |
| 76 | + * A number is numerically balanced if, for every digit d in the number, |
| 77 | + * the count of digit d is exactly equal to d. |
| 78 | + */ |
| 79 | +const PRECOMPUTED_NUMERICALLY_BALANCED_NUMBERS: Uint32Array = (() => { |
| 80 | + const allBalancedNumbers: number[] = [] |
| 81 | + const remainingDigitCounts = new Int8Array(8) // index 1..7 represents the remaining count for each digit |
| 82 | + |
| 83 | + /** |
| 84 | + * Recursive helper to generate all possible numeric combinations that satisfy the balanced condition. |
| 85 | + * @param digitsRemaining - Number of digits left to place. |
| 86 | + * @param currentValue - Current partial integer being formed. |
| 87 | + */ |
| 88 | + function generateNumbersRecursively(digitsRemaining: number, currentValue: number) { |
| 89 | + if (digitsRemaining === 0) { |
| 90 | + allBalancedNumbers.push(currentValue) |
| 91 | + return |
| 92 | + } |
| 93 | + |
| 94 | + // Try placing each digit that still has remaining occurrences |
| 95 | + for (let digit = 1; digit <= 7; digit++) { |
| 96 | + if (remainingDigitCounts[digit] > 0) { |
| 97 | + remainingDigitCounts[digit]-- |
| 98 | + |
| 99 | + // Build next integer by appending the digit |
| 100 | + const nextValue = currentValue * 10 + digit |
| 101 | + generateNumbersRecursively(digitsRemaining - 1, nextValue) |
| 102 | + |
| 103 | + // Backtrack after exploring this branch |
| 104 | + remainingDigitCounts[digit]++ |
| 105 | + } |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | + /** |
| 110 | + * Generate all possible digit subsets (from 1 to 7) where each digit d appears exactly d times. |
| 111 | + * Skip subsets whose total digit count exceeds 7. |
| 112 | + */ |
| 113 | + function generateAllSubsets() { |
| 114 | + const totalSubsets = 1 << 7 // 2^7 possible subsets of digits {1..7} |
| 115 | + |
| 116 | + for (let subsetMask = 1; subsetMask < totalSubsets; subsetMask++) { |
| 117 | + let totalDigitCount = 0 |
| 118 | + |
| 119 | + // Compute total digit occurrences for this subset |
| 120 | + for (let bitIndex = 0; bitIndex < 7; bitIndex++) { |
| 121 | + if ((subsetMask & (1 << bitIndex)) !== 0) { |
| 122 | + totalDigitCount += bitIndex + 1 |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + // Skip invalid subsets that exceed 7 total digits |
| 127 | + if (totalDigitCount === 0 || totalDigitCount > 7) { |
| 128 | + continue |
| 129 | + } |
| 130 | + |
| 131 | + // Initialize remaining counts for this subset |
| 132 | + for (let digit = 1; digit <= 7; digit++) { |
| 133 | + remainingDigitCounts[digit] = (subsetMask & (1 << (digit - 1))) !== 0 ? digit : 0 |
| 134 | + } |
| 135 | + |
| 136 | + // Begin recursive generation for this subset |
| 137 | + generateNumbersRecursively(totalDigitCount, 0) |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | + // Generate all balanced numbers once |
| 142 | + generateAllSubsets() |
| 143 | + |
| 144 | + // Sort results for binary search compatibility |
| 145 | + allBalancedNumbers.sort((a, b) => a - b) |
| 146 | + |
| 147 | + // Pack into a Uint32Array for cache efficiency |
| 148 | + const packedArray = new Uint32Array(allBalancedNumbers.length) |
| 149 | + for (let index = 0; index < allBalancedNumbers.length; index++) { |
| 150 | + packedArray[index] = allBalancedNumbers[index] |
| 151 | + } |
| 152 | + |
| 153 | + return packedArray |
| 154 | +})() |
| 155 | +``` |
| 156 | + |
| 157 | +### Step 3:`nextBeautifulNumber` — 查找最小且嚴格大於 `n` 的平衡數 |
| 158 | + |
| 159 | +說明:利用 Step 1 的二分搜尋在 Step 2 的預生成表中查找第一個嚴格大於 `n` 的元素;若超出範圍則拋出錯誤。 |
| 160 | + |
| 161 | +```typescript |
| 162 | +/** |
| 163 | + * Find the smallest numerically balanced number strictly greater than n. |
| 164 | + * |
| 165 | + * A number is numerically balanced if, for every digit d in it, |
| 166 | + * the count of digit d is exactly d. |
| 167 | + * |
| 168 | + * Constraints: 0 <= n <= 10^6 |
| 169 | + * |
| 170 | + * @param inputNumber - The given integer. |
| 171 | + * @returns The smallest numerically balanced number strictly greater than inputNumber. |
| 172 | + * @throws RangeError - If no numerically balanced number exists that is strictly greater than inputNumber. |
| 173 | + */ |
| 174 | +function nextBeautifulNumber(inputNumber: number): number { |
| 175 | + // Binary search to locate the first precomputed number strictly greater than inputNumber |
| 176 | + const foundIndex = upperBound(PRECOMPUTED_NUMERICALLY_BALANCED_NUMBERS, inputNumber) |
| 177 | + |
| 178 | + // If not found, signal to the caller that the request is out of the supported range |
| 179 | + if (foundIndex >= PRECOMPUTED_NUMERICALLY_BALANCED_NUMBERS.length) { |
| 180 | + throw new RangeError( |
| 181 | + `No numerically balanced number strictly greater than ${inputNumber} exists within the precomputed range.` |
| 182 | + ) |
| 183 | + } |
| 184 | + |
| 185 | + // Return the next numerically balanced number |
| 186 | + return PRECOMPUTED_NUMERICALLY_BALANCED_NUMBERS[foundIndex] |
| 187 | +} |
| 188 | +``` |
| 189 | + |
| 190 | +## 時間複雜度 |
| 191 | + |
| 192 | +- **預生成階段**:設 **`n`** 為「所有位數 ≤ 7 的數值平衡數總數」。 |
| 193 | + - 每個數字需被完整遞迴生成一次,成本為 $O(n)$;生成後需排序以支援二分搜尋,成本為 $O(n \log n)$。 |
| 194 | + - 因此預生成階段的最壞時間複雜度為 $O(n \log n)$。 |
| 195 | +- **查詢階段(單次)**: |
| 196 | + - 預生成完成後,使用二分搜尋尋找第一個嚴格大於輸入值的平衡數,耗時 $O(\log n)$。 |
| 197 | +- **最壞情況(首次呼叫)**: |
| 198 | + - 當快取尚未建立時,系統需執行完整的預生成與查詢流程,整體最壞時間複雜度為 $O(n \log n)$。 |
| 199 | +- 總時間複雜度為 $O(n \log n)$。 |
| 200 | + |
| 201 | +> $O(n \log n)$ |
| 202 | +
|
| 203 | +## 空間複雜度 |
| 204 | + |
| 205 | +- 預生成集合需儲存所有平衡數,為 $O(n)$。 |
| 206 | +- 輔助結構(如遞迴狀態陣列、遮罩變數等)僅需常數額外空間。 |
| 207 | +- 總空間複雜度為 $O(n)$。 |
| 208 | + |
| 209 | +> $O(n)$ |
0 commit comments