2435. Paths in Matrix Whose Sum Is Divisible by K
Problem Description
You have an m x n
grid of integers and need to find paths from the top-left corner (0, 0)
to the bottom-right corner (m-1, n-1)
. You can only move right or down at each step.
The task is to count how many such paths have a total sum that is divisible by a given integer k
. As you traverse a path, you add up all the numbers in the cells you visit, and you want this sum to be divisible by k
(i.e., sum % k == 0
).
Since the number of valid paths could be very large, return the answer modulo 10^9 + 7
.
For example, if you have a grid and k = 5
, you need to find all possible paths from top-left to bottom-right where the sum of all elements along the path is divisible by 5.
The solution uses dynamic programming with memoization. The function dfs(i, j, s)
computes the number of valid paths starting from position (i, j)
where s
represents the current path sum modulo k
. At each cell, you can either move down to (i+1, j)
or right to (i, j+1)
, updating the running sum modulo k
. When you reach the destination (m-1, n-1)
, you check if the total sum is divisible by k
(i.e., s == 0
).
Intuition
The key insight is that we don't need to track the actual sum of each path - we only care whether the sum is divisible by k
. This means we can work with remainders (modulo k
) instead of actual sums, which keeps our numbers bounded between 0
and k-1
.
Think about it this way: if we have a path sum of 1000 and k = 7
, we only need to know that 1000 % 7 = 6
. If we add another cell with value 8, the new remainder is (6 + 8) % 7 = 0
, making it divisible by k
. This modular arithmetic property allows us to track just the remainder at each step.
Since we can only move right or down, this naturally forms a recursive structure. At each cell (i, j)
, we face two choices:
- Move down to
(i+1, j)
- Move right to
(i, j+1)
For each choice, we update our running remainder by adding the current cell's value and taking modulo k
.
The overlapping subproblems emerge because multiple paths can reach the same cell with the same remainder. For instance, reaching cell (2, 2)
with remainder 3
could happen via different routes, but the number of valid paths from (2, 2)
to the destination with starting remainder 3
is always the same. This makes memoization perfect - we can cache results based on three parameters: row position, column position, and current remainder.
The base case is when we reach the bottom-right corner: we return 1
if our accumulated remainder is 0
(meaning the path sum is divisible by k
), otherwise 0
.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution uses memoization search (top-down dynamic programming) to efficiently count the valid paths.
We define a recursive function dfs(i, j, s)
where:
i, j
represents the current position in the grids
represents the current path sum modulok
The function returns the number of paths from position (i, j)
to the destination (m-1, n-1)
such that when combined with the current remainder s
, the total path sum is divisible by k
.
Implementation Details:
-
Base Cases:
- If we go out of bounds (
i >= m
orj >= n
), return0
- If we reach the destination
(m-1, n-1)
, check if the final remainder is0
. If yes, we found a valid path, return1
; otherwise return0
- If we go out of bounds (
-
Recursive Step:
- Add the current cell value to our running remainder:
s = (s + grid[i][j]) % k
- Explore both possible moves:
- Move down:
dfs(i + 1, j, s)
- Move right:
dfs(i, j + 1, s)
- Move down:
- Sum up the results from both directions
- Add the current cell value to our running remainder:
-
Memoization:
- The
@cache
decorator automatically memoizes the function results - This prevents recalculating the same state
(i, j, s)
multiple times - Since there are
m × n × k
possible states, this significantly improves efficiency
- The
-
Modular Arithmetic:
- All intermediate results are taken modulo
10^9 + 7
to prevent overflow - The final answer is
dfs(0, 0, 0)
, starting from the top-left with initial remainder0
- All intermediate results are taken modulo
The recurrence relation can be expressed as:
dfs(i, j, s) = dfs(i + 1, j, (s + grid[i][j]) % k) + dfs(i, j + 1, (s + grid[i][j]) % k)
The time complexity is O(m × n × k)
since we compute each state at most once, and the space complexity is also O(m × n × k)
for the memoization cache.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Consider a 2×3 grid with k = 3
:
grid = [[1, 2, 1], [1, 1, 2]]
We want to count paths from (0,0) to (1,2) where the path sum is divisible by 3.
Starting the recursion: dfs(0, 0, 0)
- Current position: (0, 0), value = 1
- Current remainder: (0 + 1) % 3 = 1
- We can move right to (0, 1) or down to (1, 0)
Branch 1: Move right from (0,0) → (0,1): dfs(0, 1, 1)
-
Current position: (0, 1), value = 2
-
Current remainder: (1 + 2) % 3 = 0
-
We can move right to (0, 2) or down to (1, 1)
Branch 1.1: (0,1) → (0,2):
dfs(0, 2, 0)
-
Current position: (0, 2), value = 1
-
Current remainder: (0 + 1) % 3 = 1
-
Can only move down to (1, 2)
Branch 1.1.1: (0,2) → (1,2):
dfs(1, 2, 1)
- At destination (1, 2), value = 2
- Final remainder: (1 + 2) % 3 = 0 ✓
- Return 1 (valid path found!)
Branch 1.2: (0,1) → (1,1):
dfs(1, 1, 0)
-
Current position: (1, 1), value = 1
-
Current remainder: (0 + 1) % 3 = 1
-
Can only move right to (1, 2)
Branch 1.2.1: (1,1) → (1,2):
dfs(1, 2, 1)
- At destination (1, 2), value = 2
- Final remainder: (1 + 2) % 3 = 0 ✓
- Return 1 (valid path found!)
-
Branch 2: Move down from (0,0) → (1,0): dfs(1, 0, 1)
-
Current position: (1, 0), value = 1
-
Current remainder: (1 + 1) % 3 = 2
-
Can only move right to (1, 1)
Branch 2.1: (1,0) → (1,1):
dfs(1, 1, 2)
-
Current position: (1, 1), value = 1
-
Current remainder: (2 + 1) % 3 = 0
-
Can only move right to (1, 2)
Branch 2.1.1: (1,1) → (1,2):
dfs(1, 2, 0)
- At destination (1, 2), value = 2
- Final remainder: (0 + 2) % 3 = 2 ✗
- Return 0 (path sum not divisible by 3)
-
Final Result:
- Path 1: (0,0) → (0,1) → (0,2) → (1,2): sum = 1+2+1+2 = 6, divisible by 3 ✓
- Path 2: (0,0) → (0,1) → (1,1) → (1,2): sum = 1+2+1+2 = 6, divisible by 3 ✓
- Path 3: (0,0) → (1,0) → (1,1) → (1,2): sum = 1+1+1+2 = 5, not divisible by 3 ✗
Total valid paths = 2
Note how memoization helps: dfs(1, 2, 1)
is called twice (from branches 1.1.1 and 1.2.1), but we only compute it once and reuse the cached result.
Solution Implementation
1class Solution:
2 def numberOfPaths(self, grid: List[List[int]], k: int) -> int:
3 """
4 Find the number of paths from top-left to bottom-right where the sum of values
5 along the path is divisible by k.
6
7 Args:
8 grid: 2D grid of integers
9 k: The divisor to check path sums against
10
11 Returns:
12 Number of valid paths modulo 10^9 + 7
13 """
14 from functools import cache
15
16 @cache
17 def dfs(row: int, col: int, current_sum: int) -> int:
18 """
19 Recursively explore paths using DFS with memoization.
20
21 Args:
22 row: Current row position
23 col: Current column position
24 current_sum: Running sum modulo k of the path so far
25
26 Returns:
27 Number of valid paths from current position to destination
28 """
29 # Check if current position is out of bounds
30 if row < 0 or row >= rows or col < 0 or col >= cols:
31 return 0
32
33 # Update the running sum with current cell value (modulo k)
34 current_sum = (current_sum + grid[row][col]) % k
35
36 # Check if we've reached the destination (bottom-right corner)
37 if row == rows - 1 and col == cols - 1:
38 # Return 1 if path sum is divisible by k, otherwise 0
39 return int(current_sum == 0)
40
41 # Explore two possible moves: down and right
42 move_down = dfs(row + 1, col, current_sum)
43 move_right = dfs(row, col + 1, current_sum)
44
45 # Return total number of valid paths from current position
46 total_paths = (move_down + move_right) % MOD
47 return total_paths
48
49 # Initialize grid dimensions
50 rows = len(grid)
51 cols = len(grid[0])
52
53 # Define modulo constant for large numbers
54 MOD = 10**9 + 7
55
56 # Start DFS from top-left corner with initial sum of 0
57 result = dfs(0, 0, 0)
58
59 # Clear the cache to free memory
60 dfs.cache_clear()
61
62 return result
63
1class Solution {
2 // Grid dimensions
3 private int rows;
4 private int cols;
5
6 // Divisor for path sum
7 private int divisor;
8
9 // Modulo value for result
10 private static final int MOD = (int) 1e9 + 7;
11
12 // Input grid
13 private int[][] grid;
14
15 // Memoization cache: dp[row][col][remainder] = number of paths
16 private int[][][] dp;
17
18 /**
19 * Counts the number of paths from top-left to bottom-right
20 * where the sum of values along the path is divisible by k
21 *
22 * @param grid 2D grid with values
23 * @param k divisor to check path sum against
24 * @return number of valid paths modulo 10^9 + 7
25 */
26 public int numberOfPaths(int[][] grid, int k) {
27 // Initialize instance variables
28 this.grid = grid;
29 this.divisor = k;
30 this.rows = grid.length;
31 this.cols = grid[0].length;
32
33 // Initialize memoization array with -1 (unvisited state)
34 this.dp = new int[rows][cols][k];
35 for (int[][] matrix : dp) {
36 for (int[] row : matrix) {
37 Arrays.fill(row, -1);
38 }
39 }
40
41 // Start DFS from top-left corner with initial sum 0
42 return dfs(0, 0, 0);
43 }
44
45 /**
46 * Recursive DFS to explore all paths and count valid ones
47 *
48 * @param row current row position
49 * @param col current column position
50 * @param currentSum current path sum modulo k
51 * @return number of valid paths from current position
52 */
53 private int dfs(int row, int col, int currentSum) {
54 // Check boundaries
55 if (row < 0 || row >= rows || col < 0 || col >= cols) {
56 return 0;
57 }
58
59 // Update current sum with current cell value
60 currentSum = (currentSum + grid[row][col]) % divisor;
61
62 // Check memoization cache
63 if (dp[row][col][currentSum] != -1) {
64 return dp[row][col][currentSum];
65 }
66
67 // Base case: reached bottom-right corner
68 if (row == rows - 1 && col == cols - 1) {
69 // Return 1 if sum is divisible by k, 0 otherwise
70 return currentSum == 0 ? 1 : 0;
71 }
72
73 // Recursive case: explore down and right paths
74 int pathCount = dfs(row + 1, col, currentSum) + dfs(row, col + 1, currentSum);
75 pathCount %= MOD;
76
77 // Store result in cache
78 dp[row][col][currentSum] = pathCount;
79
80 return pathCount;
81 }
82}
83
1class Solution {
2public:
3 int numberOfPaths(vector<vector<int>>& grid, int k) {
4 int rows = grid.size();
5 int cols = grid[0].size();
6 const int MOD = 1e9 + 7;
7
8 // dp[i][j][remainder] = number of paths from (i,j) to (rows-1, cols-1)
9 // with path sum % k = remainder
10 vector<vector<vector<int>>> dp(rows, vector<vector<int>>(cols, vector<int>(k, -1)));
11
12 // Define recursive DFS function with memoization
13 function<int(int, int, int)> dfs = [&](int row, int col, int currentSum) -> int {
14 // Check if out of bounds
15 if (row < 0 || row >= rows || col < 0 || col >= cols) {
16 return 0;
17 }
18
19 // Update current sum with current cell value and take modulo k
20 currentSum = (currentSum + grid[row][col]) % k;
21
22 // Base case: reached bottom-right corner
23 if (row == rows - 1 && col == cols - 1) {
24 // Return 1 if sum is divisible by k, otherwise 0
25 return currentSum == 0 ? 1 : 0;
26 }
27
28 // Check memoization cache
29 if (dp[row][col][currentSum] != -1) {
30 return dp[row][col][currentSum];
31 }
32
33 // Calculate number of valid paths by exploring both directions
34 // Move down: (row + 1, col)
35 // Move right: (row, col + 1)
36 int pathCount = dfs(row + 1, col, currentSum) + dfs(row, col + 1, currentSum);
37 pathCount %= MOD;
38
39 // Store result in cache before returning
40 dp[row][col][currentSum] = pathCount;
41 return pathCount;
42 };
43
44 // Start DFS from top-left corner (0, 0) with initial sum 0
45 return dfs(0, 0, 0);
46 }
47};
48
1/**
2 * Calculates the number of paths from top-left to bottom-right in a grid
3 * where the sum of values along the path is divisible by k
4 * @param grid - 2D array representing the grid of values
5 * @param k - The divisor to check path sums against
6 * @returns Number of valid paths modulo 10^9 + 7
7 */
8function numberOfPaths(grid: number[][], k: number): number {
9 const MOD = 1000000007; // 10^9 + 7 for modular arithmetic
10 const rows = grid.length;
11 const cols = grid[0].length;
12
13 // dp[i][j][remainder] = number of paths to cell (i,j) with sum % k = remainder
14 // Using 1-indexed for easier boundary handling
15 const dp: number[][][] = Array.from({ length: rows + 1 }, () =>
16 Array.from({ length: cols + 1 }, () =>
17 new Array(k).fill(0)
18 )
19 );
20
21 // Initialize: one way to reach the cell before the start with remainder 0
22 dp[0][1][0] = 1;
23
24 // Fill the DP table
25 for (let i = 0; i < rows; i++) {
26 for (let j = 0; j < cols; j++) {
27 // For each possible remainder value from previous cells
28 for (let prevRemainder = 0; prevRemainder < k; prevRemainder++) {
29 // Calculate new remainder when including current cell value
30 const newRemainder = (grid[i][j] + prevRemainder) % k;
31
32 // Update paths to current cell (i+1, j+1) with new remainder
33 // Paths can come from above (i, j+1) or from left (i+1, j)
34 dp[i + 1][j + 1][newRemainder] = (
35 dp[i][j + 1][prevRemainder] + // From above
36 dp[i + 1][j][prevRemainder] + // From left
37 dp[i + 1][j + 1][newRemainder] // Existing paths with same remainder
38 ) % MOD;
39 }
40 }
41 }
42
43 // Return number of paths to bottom-right corner with sum divisible by k (remainder = 0)
44 return dp[rows][cols][0];
45}
46
Time and Space Complexity
The time complexity is O(m × n × k)
, where m
is the number of rows, n
is the number of columns, and k
is the given modulus value. This is because the DFS function uses memoization with the @cache
decorator, and there are at most m × n × k
unique states. Each state is defined by three parameters: the current position (i, j)
which has m × n
possible values, and the remainder s
which can take values from 0
to k-1
, giving k
possible values. Each state is computed only once due to memoization, and each computation takes O(1)
time (excluding recursive calls).
The space complexity is O(m × n × k)
. This consists of two main components:
- The memoization cache stores up to
m × n × k
unique states - The recursion stack depth in the worst case is
O(m + n)
(moving from top-left to bottom-right)
Since m × n × k
dominates m + n
in most practical cases, the overall space complexity is O(m × n × k)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Modulo Operation on Path Sum
One of the most common mistakes is forgetting to apply modulo k
consistently when tracking the path sum. Some developers might track the actual sum and only check divisibility at the end, which can lead to integer overflow for large grids or incorrect remainder calculations.
Incorrect approach:
# Wrong: Tracking actual sum instead of remainder
def dfs(row, col, total_sum):
total_sum += grid[row][col] # Can overflow!
if row == rows - 1 and col == cols - 1:
return int(total_sum % k == 0)
Correct approach:
# Right: Always work with remainder modulo k
def dfs(row, col, current_sum):
current_sum = (current_sum + grid[row][col]) % k
if row == rows - 1 and col == cols - 1:
return int(current_sum == 0)
2. Boundary Check Logic Error
Another frequent mistake is checking boundaries after accessing the grid element, which causes IndexError. The boundary check must happen before any grid access.
Incorrect approach:
def dfs(row, col, current_sum):
# Wrong: Accessing grid before boundary check
current_sum = (current_sum + grid[row][col]) % k
if row >= rows or col >= cols: # Too late!
return 0
Correct approach:
def dfs(row, col, current_sum):
# Right: Check boundaries first
if row >= rows or col >= cols:
return 0
current_sum = (current_sum + grid[row][col]) % k
3. Memory Leak from Cache Not Being Cleared
When using @cache
or @lru_cache
, the memoization dictionary persists between test cases in competitive programming platforms. This can cause incorrect results or memory issues if the same Solution instance is reused.
Solution: Always clear the cache after getting the result:
result = dfs(0, 0, 0) dfs.cache_clear() # Important for multiple test cases return result
4. Forgetting to Apply MOD to Intermediate Results
The problem requires the final answer modulo 10^9 + 7
, but it's easy to forget applying this to intermediate calculations when combining path counts.
Incorrect approach:
# Wrong: Not applying MOD to intermediate sum move_down = dfs(row + 1, col, current_sum) move_right = dfs(row, col + 1, current_sum) return move_down + move_right # Can exceed MOD!
Correct approach:
# Right: Apply MOD to prevent overflow move_down = dfs(row + 1, col, current_sum) move_right = dfs(row, col + 1, current_sum) return (move_down + move_right) % MOD
5. State Design Inefficiency
Some implementations might pass unnecessary information in the recursive state, such as the entire path or a copy of the grid, which drastically increases memory usage and slows down the solution.
Key insight: You only need to track:
- Current position (row, col)
- Current path sum modulo k
Any additional state information is redundant and harmful to performance.
How many ways can you arrange the three letters A, B and C?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!