|
| 1 | +# 2435. Paths in Matrix Whose Sum Is Divisible by K |
| 2 | + |
| 3 | +You are given a 0-indexed `m x n` integer matrix `grid` and an integer `k`. |
| 4 | +You are currently at position `(0, 0)` and you want to reach position `(m - 1, n - 1)` moving only down or right. |
| 5 | + |
| 6 | +Return the number of paths where the sum of the elements on the path is divisible by `k`. |
| 7 | +Since the answer may be very large, return it modulo `10^9 + 7`. |
| 8 | + |
| 9 | +**Constraints:** |
| 10 | + |
| 11 | +- `m == grid.length` |
| 12 | +- `n == grid[i].length` |
| 13 | +- `1 <= m, n <= 5 * 10^4` |
| 14 | +- `1 <= m * n <= 5 * 10^4` |
| 15 | +- `0 <= grid[i][j] <= 100` |
| 16 | +- `1 <= k <= 50` |
| 17 | + |
| 18 | +## 基礎思路 |
| 19 | + |
| 20 | +本題要求計算從左上角 `(0,0)` 移動到右下角 `(m-1,n-1)` 的所有路徑中,**路徑元素總和可被 `k` 整除的路徑數量**;每一步只能向右或向下,因此每條路徑長度固定為 `m+n−1`。 |
| 21 | +因為 `m*n ≤ 5*10^4`,矩陣可能非常細長,但總格子數不會太大,因此適合使用 DP。 |
| 22 | + |
| 23 | +要注意的核心觀察: |
| 24 | + |
| 25 | +- **每條路徑都有固定方向**:只能往右或下,使得每個格子的路徑只來自「上方」與「左方」。 |
| 26 | +- **我們並非要計算總和,而是總和 mod k**:因此對每個格子,我們必須記錄「到達此格子的所有路徑,其累積總和 mod k 的方式」。 |
| 27 | +- **DP 狀態設計**:對每一格 `(i,j)` 與每個可能餘數 `r (0 ≤ r < k)`,記錄能到達 `(i,j)` 且累積餘數為 `r` 的路徑數。 |
| 28 | +- **數量極大需取模**:DP 過程中需持續 `% 1e9+7`。 |
| 29 | +- **滾動 DP(Row Rolling)優化空間**:因為每格的 DP 只依賴同 row 左邊與上一 row 的同 column,因此只需兩個 row 的 DP 陣列即可,大幅降低記憶體。 |
| 30 | + |
| 31 | +整體 DP 轉移設計為: |
| 32 | + |
| 33 | +- 從上方 `(i−1,j)` 的同餘數路徑數 |
| 34 | +- 從左方 `(i,j−1)` 的同餘數路徑數 |
| 35 | +- 加上當前格子的數字 `v`,得新餘數 `(r + v) % k` |
| 36 | + |
| 37 | +利用滾動陣列可在線性複雜度中完成整體計算。 |
| 38 | + |
| 39 | +## 解題步驟 |
| 40 | + |
| 41 | +### Step 1:預處理基本參數 |
| 42 | + |
| 43 | +計算矩陣大小、每一列 DP 的狀態數量,並建立一個壓平的一維 `moduloGrid`, |
| 44 | +用來儲存每個格子的 `grid[i][j] % k` 結果,以加速後續 DP。 |
| 45 | + |
| 46 | +```typescript |
| 47 | +const modulusBase = 1_000_000_007; |
| 48 | + |
| 49 | +const rowCount = grid.length; |
| 50 | +const columnCount = grid[0].length; |
| 51 | + |
| 52 | +// 每一列的 DP 狀態總數 = columnCount * k |
| 53 | +const stateSizePerRow = columnCount * k; |
| 54 | + |
| 55 | +// 將所有格子的 (value % k) 預先壓平成一維陣列,以加速存取 |
| 56 | +const totalCellCount = rowCount * columnCount; |
| 57 | +const moduloGrid = new Uint8Array(totalCellCount); |
| 58 | + |
| 59 | +let writeIndex = 0; |
| 60 | +for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) { |
| 61 | + const row = grid[rowIndex]; |
| 62 | + for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) { |
| 63 | + moduloGrid[writeIndex] = row[columnIndex] % k; |
| 64 | + writeIndex += 1; |
| 65 | + } |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +### Step 2:初始化滾動 DP 陣列 |
| 70 | + |
| 71 | +使用滾動陣列 `previousRow` 與 `currentRow`, |
| 72 | +每一列都需要維護 `columnCount * k` 個餘數狀態。 |
| 73 | + |
| 74 | +```typescript |
| 75 | +// 滾動 DP 陣列(上一列與當前列) |
| 76 | +let previousRow = new Int32Array(stateSizePerRow); |
| 77 | +let currentRow = new Int32Array(stateSizePerRow); |
| 78 | + |
| 79 | +// 指向壓平格子的索引 |
| 80 | +let cellIndex = 0; |
| 81 | +``` |
| 82 | + |
| 83 | +### Step 3:外層迴圈 — 逐 row 計算 DP |
| 84 | + |
| 85 | +進入每一列時,需先將 `currentRow` 清空, |
| 86 | +接著才逐 column 填入 DP 狀態。 |
| 87 | + |
| 88 | +```typescript |
| 89 | +for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) { |
| 90 | + // 重置當前列的 DP 狀態 |
| 91 | + currentRow.fill(0); |
| 92 | + |
| 93 | + // ... |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +### Step 4:內層迴圈 — 處理每個格子 `(rowIndex, columnIndex)` |
| 98 | + |
| 99 | +依序讀取壓平後的 `moduloGrid`, |
| 100 | +並計算此格子對應在 DP 陣列中的「餘數區段起點」。 |
| 101 | + |
| 102 | +```typescript |
| 103 | +for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) { |
| 104 | + // Step 3:外層 row 初始化 |
| 105 | + |
| 106 | + for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) { |
| 107 | + const valueModulo = moduloGrid[cellIndex]; |
| 108 | + cellIndex += 1; |
| 109 | + |
| 110 | + // 每個 column 都對應 k 個餘數狀態,因此 baseIndex 是此格的起點 |
| 111 | + const baseIndex = columnIndex * k; |
| 112 | + |
| 113 | + // ... |
| 114 | + } |
| 115 | +} |
| 116 | +``` |
| 117 | + |
| 118 | +### Step 5:處理起點 `(0,0)` |
| 119 | + |
| 120 | +若目前在第一列第一欄,則起點唯一的餘數為 `valueModulo` 本身, |
| 121 | +路徑數量為 1。 |
| 122 | + |
| 123 | +```typescript |
| 124 | +for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) { |
| 125 | + // Step 3:外層 row 處理 |
| 126 | + |
| 127 | + for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) { |
| 128 | + // Step 4:讀取 valueModulo |
| 129 | + |
| 130 | + // 處理起點 (0,0) |
| 131 | + if (rowIndex === 0 && columnIndex === 0) { |
| 132 | + currentRow[valueModulo] = 1; |
| 133 | + continue; |
| 134 | + } |
| 135 | + |
| 136 | + // ... |
| 137 | + } |
| 138 | +} |
| 139 | +``` |
| 140 | + |
| 141 | +### Step 6:計算來自上方與左方的 DP 來源位置 |
| 142 | + |
| 143 | +上方來源永遠存在於 `previousRow` 中, |
| 144 | +左方來源僅在 columnIndex > 0 時有效。 |
| 145 | + |
| 146 | +```typescript |
| 147 | +for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) { |
| 148 | + // Step 3:外層 row 處理 |
| 149 | + |
| 150 | + for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) { |
| 151 | + // Step 4:讀取 valueModulo |
| 152 | + |
| 153 | + // Step 5:處理起點 (0,0) |
| 154 | + |
| 155 | + // 計算上一列與左邊格子的餘數區段起點 |
| 156 | + const fromTopIndex = baseIndex; |
| 157 | + let fromLeftIndex = -1; |
| 158 | + |
| 159 | + if (columnIndex > 0) { |
| 160 | + fromLeftIndex = (columnIndex - 1) * k; |
| 161 | + } |
| 162 | + |
| 163 | + // ... |
| 164 | + } |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +### Step 7:對每個餘數 `remainder` 進行 DP 狀態轉移 |
| 169 | + |
| 170 | +從上方與左方的餘數分別取出可行路徑, |
| 171 | +並計算新餘數 `(remainder + valueModulo) % k`, |
| 172 | +將結果累加到 `currentRow[targetIndex]`。 |
| 173 | + |
| 174 | +```typescript |
| 175 | +for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) { |
| 176 | + // Step 3:外層 row 處理 |
| 177 | + |
| 178 | + for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) { |
| 179 | + // Step 4:讀取 valueModulo |
| 180 | + |
| 181 | + // Step 5:處理起點 (0,0) |
| 182 | + |
| 183 | + // Step 6:計算來自上方與左方的 DP 來源位置 |
| 184 | + |
| 185 | + // 針對每一種餘數進行狀態轉移 |
| 186 | + let remainder = 0; |
| 187 | + while (remainder < k) { |
| 188 | + // 將上方與左方的路徑數合併 |
| 189 | + let pathCount = previousRow[fromTopIndex + remainder]; |
| 190 | + |
| 191 | + if (fromLeftIndex >= 0) { |
| 192 | + pathCount += currentRow[fromLeftIndex + remainder]; |
| 193 | + } |
| 194 | + |
| 195 | + if (pathCount !== 0) { |
| 196 | + // 計算新餘數(避免使用 % 運算) |
| 197 | + let newRemainder = remainder + valueModulo; |
| 198 | + if (newRemainder >= k) { |
| 199 | + newRemainder -= k; |
| 200 | + } |
| 201 | + |
| 202 | + const targetIndex = baseIndex + newRemainder; |
| 203 | + |
| 204 | + // 將路徑數加入目標狀態,並做模處理 |
| 205 | + let updatedValue = currentRow[targetIndex] + pathCount; |
| 206 | + if (updatedValue >= modulusBase) { |
| 207 | + updatedValue -= modulusBase; |
| 208 | + if (updatedValue >= modulusBase) { |
| 209 | + updatedValue %= modulusBase; |
| 210 | + } |
| 211 | + } |
| 212 | + |
| 213 | + currentRow[targetIndex] = updatedValue; |
| 214 | + } |
| 215 | + |
| 216 | + remainder += 1; |
| 217 | + } |
| 218 | + } |
| 219 | +} |
| 220 | +``` |
| 221 | + |
| 222 | +### Step 8:完成一 row 後進行滾動 DP 陣列交換 |
| 223 | + |
| 224 | +下一列計算時,要讓 `currentRow` 成為新的 `previousRow`。 |
| 225 | + |
| 226 | +```typescript |
| 227 | +// 交換 DP 列,推進到下一列 |
| 228 | +const tempRow = previousRow; |
| 229 | +previousRow = currentRow; |
| 230 | +currentRow = tempRow; |
| 231 | +``` |
| 232 | + |
| 233 | +### Step 9:回傳右下角餘數為 0 的路徑數 |
| 234 | + |
| 235 | +右下角位於 columnCount−1,其餘數 0 的狀態即為最終答案。 |
| 236 | + |
| 237 | +```typescript |
| 238 | +// 回傳右下角餘數為 0 的路徑數 |
| 239 | +const resultBaseIndex = (columnCount - 1) * k; |
| 240 | +return previousRow[resultBaseIndex] % modulusBase; |
| 241 | +``` |
| 242 | + |
| 243 | +## 時間複雜度 |
| 244 | + |
| 245 | +- 每個格子要處理 `k` 種餘數(`k ≤ 50`) |
| 246 | +- 總格子數 `m * n ≤ 5*10^4` |
| 247 | +- 總時間複雜度為 $O((m \times n) \cdot k)$。 |
| 248 | + |
| 249 | +> $O(m \times n \times k)$ |
| 250 | +
|
| 251 | +## 空間複雜度 |
| 252 | + |
| 253 | +- 使用兩個大小為 `columnCount * k` 的 DP 陣列作為滾動 row |
| 254 | +- 額外使用 `moduloGrid` 來存取格子的 `value % k` |
| 255 | +- 總空間複雜度為 $O(n \times k)$。 |
| 256 | + |
| 257 | +> $O(n \times k)$ |
0 commit comments