Skip to content

Commit b8a5576

Browse files
committed
Add: Add 2025/11/26
1 parent 1b89825 commit b8a5576

File tree

3 files changed

+358
-0
lines changed

3 files changed

+358
-0
lines changed
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
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)$
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
function numberOfPaths(grid: number[][], k: number): number {
2+
const modulusBase = 1_000_000_007;
3+
4+
const rowCount = grid.length;
5+
const columnCount = grid[0].length;
6+
7+
// Total DP states for one row = columnCount * k
8+
const stateSizePerRow = columnCount * k;
9+
10+
// Precompute all valueModulo into a flat array for fast access
11+
const totalCellCount = rowCount * columnCount;
12+
const moduloGrid = new Uint8Array(totalCellCount);
13+
14+
let writeIndex = 0;
15+
for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
16+
const row = grid[rowIndex];
17+
for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
18+
moduloGrid[writeIndex] = row[columnIndex] % k;
19+
writeIndex += 1;
20+
}
21+
}
22+
23+
// Rolling DP arrays
24+
let previousRow = new Int32Array(stateSizePerRow);
25+
let currentRow = new Int32Array(stateSizePerRow);
26+
27+
let cellIndex = 0;
28+
29+
for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
30+
// Reset current DP row
31+
currentRow.fill(0);
32+
33+
for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
34+
const valueModulo = moduloGrid[cellIndex];
35+
cellIndex += 1;
36+
37+
// Base index for this cell's k remainder states
38+
const baseIndex = columnIndex * k;
39+
40+
// Handle starting cell
41+
if (rowIndex === 0 && columnIndex === 0) {
42+
currentRow[valueModulo] = 1;
43+
continue;
44+
}
45+
46+
// Pre-compute neighbor base indices
47+
const fromTopIndex = baseIndex;
48+
let fromLeftIndex = -1;
49+
50+
if (columnIndex > 0) {
51+
fromLeftIndex = (columnIndex - 1) * k;
52+
}
53+
54+
// Transition for each remainder
55+
let remainder = 0;
56+
while (remainder < k) {
57+
// Combine paths from top and left
58+
let pathCount = previousRow[fromTopIndex + remainder];
59+
60+
if (fromLeftIndex >= 0) {
61+
pathCount += currentRow[fromLeftIndex + remainder];
62+
}
63+
64+
if (pathCount !== 0) {
65+
// Compute new remainder without using modulo operator
66+
let newRemainder = remainder + valueModulo;
67+
if (newRemainder >= k) {
68+
newRemainder -= k;
69+
}
70+
71+
const targetIndex = baseIndex + newRemainder;
72+
73+
// Add contribution and reduce modulo efficiently
74+
let updatedValue = currentRow[targetIndex] + pathCount;
75+
if (updatedValue >= modulusBase) {
76+
updatedValue -= modulusBase;
77+
if (updatedValue >= modulusBase) {
78+
updatedValue %= modulusBase;
79+
}
80+
}
81+
82+
currentRow[targetIndex] = updatedValue;
83+
}
84+
85+
remainder += 1;
86+
}
87+
}
88+
89+
// Swap DP rows
90+
const tempRow = previousRow;
91+
previousRow = currentRow;
92+
currentRow = tempRow;
93+
}
94+
95+
// Return result for bottom-right cell remainder 0
96+
const resultBaseIndex = (columnCount - 1) * k;
97+
return previousRow[resultBaseIndex] % modulusBase;
98+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
function numberOfPaths(grid: number[][], k: number): number {
2+
3+
}

0 commit comments

Comments
 (0)