Problem Statement in English
You’re given a m x n integer matrix grid and an integer k. You can only move either down or right at any point in time.
Your task is to find the number of paths from the top-left corner to the bottom-right corner of the matrix such that the sum of the values along the path is divisible by k.
Approach
We can use dynamic programming with memoization to solve this problem. The idea is to define a recursive function dp(r, c, rem) that returns the number of paths from the cell (r, c) to the bottom-right corner such that the sum of the values along the path modulo k is equal to rem.
So along with the current position (r, c), we also keep track of the current sum modulo k (denoted as rem). At each cell, we have two choices: move down or move right. We will recursively call the dp function for both choices and accumulate the results.
Using a hashmap for the cache results in a Time Limit Exceeded (TLE) error, so we will use a 3D list for caching the results instead.
Bottom-Up Explanation
From the NeetCode video I was also able to understand the intuition behind the bottom-up approach. We basically build the solution from the bottom up rather than recursively reaching the bottom most solution and then propagating the results back up.
So essentially we iterate from the bottom-right corner, since we already know the answer to that cell. The answer to that cell is the remainder that you would need to have when you reach that cell in order to have a total sum divisible by k. So that’s this formula: (k - (grid[N - 1][M - 1] % k)) % k. The reason for the outer modulo is to handle the case when (k - (grid[N - 1][M - 1] % k)) is k, so we wrap such elements back to 0.
For each cell cell we also have k possible remainders (0 to k-1). So we iterate through those as well.
class Solution:
def numberOfPaths(self, grid: List[List[int]], k: int) -> int:
N = len(grid)
M = len(grid[0])
MOD = 10**9 + 7
cache = [[[0] * k for _ in range(M + 1)] for _ in range(N + 1)]
target_remainder = (k - (grid[N - 1][M - 1] % k)) % k
cache[N - 1][M - 1][target_remainder] = 1
for i in reversed(range(N)):
for j in reversed(range(M)):
if i == N - 1 and j == M - 1:
continue
for rem in range(k):
new_rem = (rem + grid[i][j]) % k
cache[i][j][rem] = (
(cache[i + 1][j][new_rem]) % MOD +
(cache[i][j + 1][new_rem]) % MOD
) % MOD
return cache[0][0][0]
This can be optimized further, since at any point we only need the current row and the next row. However, for clarity, we will stick with the 3D list approach here.
Solution in Python
class Solution:
def numberOfPaths(self, grid: List[List[int]], k: int) -> int:
N = len(grid)
M = len(grid[0])
MOD = 10**9 + 7
cache = [[[None] * k for _ in range(M)] for _ in range(N)]
def dp(r, c, rem):
if r >= N or c >= M:
return 0
if r == N - 1 and c == M - 1:
return 0 if (rem + grid[r][c]) % k else 1
if cache[r][c][rem] is not None:
return cache[r][c][rem]
cache[r][c][rem] = (
dp(r + 1, c, (rem + grid[r][c]) % k) % MOD
+ dp(r, c + 1, (rem + grid[r][c]) % k) % MOD
) % MOD
return cache[r][c][rem]
return dp(0, 0, 0)
Complexity
Time: $O(N \times M \times k)$
Since we are calculating the result for each cell in the grid for each possible remainder when divided byk.Space: $O(N \times M \times k)$
Since we are calculating the result for each cell in the grid for each possible remainder when divided byk.
Mistakes I Made
I had to watch NeetCode’s video solution for this.
And we are done.