Facebook Pixel

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).

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 grid
  • s represents the current path sum modulo k

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:

  1. Base Cases:

    • If we go out of bounds (i >= m or j >= n), return 0
    • If we reach the destination (m-1, n-1), check if the final remainder is 0. If yes, we found a valid path, return 1; otherwise return 0
  2. 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)
    • Sum up the results from both directions
  3. 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
  4. 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 remainder 0

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 Evaluator

Example 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:

  1. The memoization cache stores up to m × n × k unique states
  2. 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How many ways can you arrange the three letters A, B and C?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More