2435. Paths in Matrix Whose Sum Is Divisible by K
Problem Description
In this LeetCode problem, we have a two-dimensional grid representing a matrix with m
rows and n
columns, and we are tasked with finding paths from the top left corner (0, 0)
to the bottom right corner (m - 1, n - 1)
. The only permitted movements are either right or down. Each cell in the grid contains an integer, and we want to consider only those paths for which the sum of the integers along the path is divisible by a given integer k
.
The problem statement asks us to return the total number of such paths modulo 10^9 + 7
. This large number is used to prevent overflow issues due to very large result values, which is a common practice in computational problems.
Intuition
To arrive at the solution, we have to think in terms of dynamic programming, which is a method for solving problems by breaking them down into simpler subproblems. The main idea behind the approach is to create a 3-dimensional array dp
where each element dp[i][j][s]
represents the number of ways to reach cell (i, j)
such that the sum of all elements in the path modulo k
is s
.
Here's how we can think through it:
-
Initialize a 3-dimensional array
dp
of sizem x n x k
with zeroes, which will store the count of paths that lead to a certain remainder when the sum of path elements is divided byk
. The third dimensions
represents all possible remainders[0, k-1]
. -
Set
dp[0][0][grid[0][0] % k]
to 1 as a base case since there's one way to be at the starting cell with the sum equal to the element of that cell modulok
. -
Start iterating over the grid, cell by cell. At each cell, we want to update the
dp
array for all possible sums modulok
. We consider two possibilities to arrive at a cell(i, j)
: from the cell above(i - 1, j)
and from the cell to the left(i, j - 1)
. For each of these cells, we add to the path count for the current remainders
. -
We calculate the new remainder
t
after including the current cell's value using the formulat = ((s - grid[i][j] % k) + k) % k
. This gives us the remainder from the previous cell that would lead to a current remainders
after addinggrid[i][j]
. -
If the cell above
(i - 1, j)
is valid, we add the number of ways to reach it with the remaindert
todp[i][j][s]
. If the cell to the left(i, j - 1)
is valid, we do the same. -
Since we only care about the counts modulo
10^9 + 7
, we take the modulo at each update step to keep numbers in the range. -
Finally, we're interested in the number of paths that have a sum divisible by
k
when we've reached the bottom-right cell. This corresponds todp[m - 1][n - 1][0]
, the number of paths with a remainder of0
, which we return as the answer.
By following these steps, we can fill up our dp
table and compute the required value efficiently, avoiding the need to explicitly enumerate all possible paths, which would be impractical for large grids.
Learn more about Dynamic Programming patterns.
Solution Approach
The implementation utilizes dynamic programming to efficiently compute the number of paths that lead to the bottom-right corner of the grid with sums divisible by k
:
-
Initialization: We initialize a 3D list,
dp
, of sizem * n * k
, wherem
is the number of rows andn
is the number of columns in the grid. This array will store the count of paths for each possible sum modulok
(s
ranges from 0 tok-1
) for each cell. Initially, all elements are set to 0. -
Base Case: The path count of the starting position
(0, 0)
is set such thatdp[0][0][grid[0][0] % k]
is 1, because there is only one way to be at the starting cell and the sum equals the element in the starting cell modulok
.dp[0][0][grid[0][0] % k] = 1
-
Main Logic: We iterate through each cell
(i, j)
in the grid, and for each cell, we iterate through all possible remainderss
(from 0 tok-1
). We updatedp[i][j][s]
by adding the number of ways to reach either the cell above(i - 1, j)
or the cell to the left(i, j - 1)
that would result in a remainders
after adding the value of the current cell.The transition formula used is:
t = ((s - grid[i][j] % k) + k) % k if i: dp[i][j][s] += dp[i - 1][j][t] if j: dp[i][j][s] += dp[i][j - 1][t]
For each
dp
update, we take the modulo operation to ensure the result stays within the required range:dp[i][j][s] %= mod
-
Final Result: The number of paths where the sum of the elements is divisible by
k
is the last cell's valuedp[m - 1][n - 1][0]
, since we are interested in paths with a sum that has a remainder of 0 after modulo division byk
.return dp[-1][-1][0]
By following this approach, we can compute the number of valid paths without visiting all possible paths, which would be computationally expensive especially on larger grids. This solution leverages the property of modulo operation and the principle of dynamic programming, specifically memoization, to store intermediate results and avoid repetitive work. The modular arithmetic ensures that we handle large numbers efficiently and prevent arithmetic overflow, which is a common issue in problems involving counting and combinatorics on a large scale.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's illustrate the dynamic programming approach with a small example. Consider a 2x3
grid with the following values:
[ [1, 1, 2], [2, 3, 4] ]
And let k = 3
. We want to find all the paths from the top left corner to the bottom right with sums divisible by k
.
-
Initialization: We first set up a 3D array
dp
of size2x3x3
(sincem = 2
,n = 3
, andk = 3
). We initialize all elements to 0. -
Base case: For the starting cell
(0, 0)
with the value1
, we setdp[0][0][1 % 3] = dp[0][0][1] = 1
. There is one way to be at(0, 0)
with a sum that modulok
is1
. -
Main logic:
-
Starting with cell
(0, 1)
with the value1
, the remainders
will be((1 + 1) % 3) = 2
. Since we can only arrive from the left, we will updatedp[0][1][2]
to 1. -
Now for cell
(0, 2)
with the value2
,s
will be((2 + 2) % 3) = 1
. Updatedp[0][2][1]
to 1 as we can only arrive from the left. -
For cell
(1, 0)
with the value2
,s
will be((1 + 2) % 3) = 0
. Updatedp[1][0][0]
to 1 since we can only arrive from above. -
At cell
(1, 1)
with the value3
, we check from above(0, 1)
and left(1, 0)
. From above,t = ((s - 3 % 3) + 3) % 3 = s
, we adddp[0][1][t] = dp[0][1][s]
todp[1][1][s]
. We will do the same for the left cell(1, 0)
. Hence,dp[1][1][0]
will be updated to 2 (1 from above, 1 from the left). -
Lastly, for cell
(1, 2)
with the value4
, the remainders
will be((0 + 4) % 3) = 1
. Updatedp[1][2][1]
by adding counts from above(1, 1)
with valuedp[1][1][t]
wheret = ((1 - 4 % 3) + 3) % 3 = 0
, and from the left(1, 2)
with valuedp[1][1][1]
. Thus,dp[1][2][1]
will increase by 2 (all from above, nothing from the left, asdp[1][1][1]
is 0).
-
-
Final result: We look at
dp[1][2][0]
, but in our case, the number of paths that end with a sum divisible byk
is stored indp[1][2][1]
. Since the bottom right cell ((1, 2)
) has a sum remainders = 1
, not0
, there are no paths that sum up to a number divisible byk
, and the method would return0
.
Using this approach, we managed to calculate the required paths without exhaustively iterating through all paths. We used a combination of iterative updates based on previous states and modular arithmetic to maintain efficiency and correctness.
Solution Implementation
1class Solution:
2 def numberOfPaths(self, grid, k):
3 # Obtain the dimensions of the grid
4 num_rows, num_cols = len(grid), len(grid[0])
5
6 # Initialize a dynamic programming table to hold counts of paths
7 # with different remainders modulo k at each cell
8 # dp[row][col][remainder] will store the number of ways to reach
9 # cell (row, col) such that the path sum has a remainder of `remainder` when divided by k
10 dp = [[[0] * k for _ in range(num_cols)] for _ in range(num_rows)]
11 # Set the initial case for the starting cell (top-left corner)
12 dp[0][0][grid[0][0] % k] = 1
13
14 # Define the modulo operation base
15 mod_base = 10**9 + 7
16
17 # Iterate through each cell in the grid
18 for row in range(num_rows):
19 for col in range(num_cols):
20 # For each cell, iterate through all possible remainders
21 for remainder in range(k):
22 # Compute the adjusted remainder to update the paths count
23 adjusted_remainder = ((remainder - grid[row][col] % k) + k) % k
24 # If there is a row above the current cell, add the number of paths from the cell above
25 if row:
26 dp[row][col][remainder] += dp[row - 1][col][adjusted_remainder]
27 # If there is a column to the left of the current cell, add the number of paths from the cell to the left
28 if col:
29 dp[row][col][remainder] += dp[row][col - 1][adjusted_remainder]
30 # Apply modulo operation to avoid large integers
31 dp[row][col][remainder] %= mod_base
32
33 # Return the result, which is the number of paths that lead to the bottom-right corner of the grid
34 # with a path sum divisible by k (remainder 0)
35 return dp[-1][-1][0] # dp[num_rows - 1][num_cols - 1][0] in non-Pythonic indexing
36
1class Solution {
2 // Define the modulus constant for preventing integer overflow
3 private static final int MOD = (int) 1e9 + 7;
4
5 public int numberOfPaths(int[][] grid, int k) {
6 // m and n represent the dimensions of the grid
7 int numRows = grid.length;
8 int numCols = grid[0].length;
9
10 // 3D dp array to store the number of ways to reach a cell (i, j)
11 // such that the path sum modulo k is s
12 int[][][] dp = new int[numRows][numCols][k];
13
14 // Base case: start at the top-left corner of the grid
15 dp[0][0][grid[0][0] % k] = 1;
16
17 // Iterate over all cells of the grid
18 for (int i = 0; i < numRows; ++i) {
19 for (int j = 0; j < numCols; ++j) {
20 // Try all possible sums modulo k
21 for (int sumModK = 0; sumModK < k; ++sumModK) {
22 // Calculate the modulo to identify how the current value of grid contributes to the new sum
23 int remainder = ((sumModK - grid[i][j] % k) + k) % k;
24
25 // If not in the first row, add paths from the cell above
26 if (i > 0) {
27 dp[i][j][sumModK] += dp[i - 1][j][remainder];
28 }
29 // If not in the first column, add paths from the cell on the left
30 if (j > 0) {
31 dp[i][j][sumModK] += dp[i][j - 1][remainder];
32 }
33
34 // Use modulus operation to prevent integer overflow
35 dp[i][j][sumModK] %= MOD;
36 }
37 }
38 }
39
40 // The result is the number of ways to reach the bottom-right corner such that path sum modulo k is 0
41 return dp[numRows - 1][numCols - 1][0];
42 }
43}
44
1#include <vector>
2#include <functional>
3using namespace std;
4
5class Solution {
6public:
7 int numberOfPaths(vector<vector<int>>& grid, int k) {
8 // Dimensions of the grid
9 int m = grid.size(), n = grid[0].size();
10 // Modulo value to prevent overflow
11 const int MOD = 1e9 + 7;
12
13 // A 3D vector to store the number of paths, with the third dimension representing the sum modulo k
14 vector<vector<vector<int>>> memo(m, vector<vector<int>>(n, vector<int>(k, -1)));
15
16 // Define the depth-first search function using std::function for recursion
17 function<int(int, int, int)> dfs = [&](int row, int col, int sum) {
18 // Base case: outside of the grid bounds, return 0
19 if (row < 0 || row >= m || col < 0 || col >= n) return 0;
20
21 // Add the current cell's value to the running sum and apply modulo k
22 sum = (sum + grid[row][col]) % k;
23
24 // If we reached the bottom-right cell, return 1 if sum modulo k is 0, otherwise return 0
25 if (row == m - 1 && col == n - 1) {
26 return sum == 0 ? 1 : 0;
27 }
28
29 // Check if this state has already been computed
30 if (memo[row][col][sum] != -1) {
31 return memo[row][col][sum];
32 }
33
34 // Recurse to the right cell and the bottom cell and sum their path counts
35 int pathCount = dfs(row + 1, col, sum) + dfs(row, col + 1, sum);
36 // Apply modulo operations to prevent overflow
37 pathCount %= MOD;
38
39 // Cache the result in the memoization table
40 memo[row][col][sum] = pathCount;
41
42 // Return the total number of paths from this cell
43 return pathCount;
44 };
45
46 // Call the DFS function starting from the top-left cell of the grid with an initial sum of 0
47 return dfs(0, 0, 0);
48 }
49};
50
1function numberOfPaths(grid: number[][], k: number): number {
2 // Define the modulo constant for the final answer
3 const MOD = 10 ** 9 + 7;
4
5 // Get the dimensions of the grid
6 const numRows = grid.length;
7 const numCols = grid[0].length;
8
9 // Initialize a 3D array to store the number of ways to reach a cell
10 // such that the sum of values mod k is a certain remainder
11 let paths = Array.from({ length: numRows + 1 }, () =>
12 Array.from({ length: numCols + 1 }, () => new Array(k).fill(0)),
13 );
14
15 // There is one way to reach the starting position (0,0) with a sum of 0 (mod k)
16 paths[0][1][0] = 1;
17
18 // Iterate over all cells in the grid
19 for (let row = 0; row < numRows; row++) {
20 for (let col = 0; col < numCols; col++) {
21 // Iterate over all possible remainders
22 for (let remainder = 0; remainder < k; remainder++) {
23 // Compute the next key as the sum of the current cell's value and the previous remainder, mod k
24 let newRemainder = (grid[row][col] + remainder) % k;
25
26 // Update the number of ways to reach the current cell such that the sum of values mod k
27 // is the new remainder. Take into account paths from the top and from the left.
28 // Ensure the sum is within the MOD range
29 paths[row + 1][col + 1][newRemainder] =
30 (paths[row][col + 1][remainder] + paths[row + 1][col][remainder] + paths[row + 1][col + 1][newRemainder]) % MOD;
31 }
32 }
33 }
34
35 // The answer is the number of ways to reach the bottom-right corner
36 // such that the total sum mod k is 0
37 return paths[numRows][numCols][0];
38}
39
Time and Space Complexity
The provided Python code defines a method to calculate the number of paths on a 2D grid where the sum of the values along the path is divisible by k
. It uses dynamic programming to store the counts for intermediate paths where the sum of the values modulo k
is a specific remainder.
Time Complexity:
The time complexity of the given code can be analyzed by considering the three nested loops:
- The outermost loop runs for
m
iterations, wherem
is the number of rows in the grid. - The middle loop runs for
n
iterations for eachi
, wheren
is the number of columns in the grid. - The innermost loop runs for
k
iterations for each combination ofi
andj
.
Combining these, we get m * n * k
iterations in total. Within the innermost loop, all operations are constant time. Hence, the time complexity is O(m * n * k)
.
Space Complexity:
The space complexity is determined by the size of the dp
array, which stores intermediate counts for each cell and each possible remainder modulo k
:
- The
dp
array is a 3-dimensional array with dimensionsm
,n
, andk
. - This results in a space requirement for
m * n * k
integers.
Hence, the space complexity of the code is also O(m * n * k)
.
Learn more about how to find time and space complexity quickly using problem constraints.
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
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
LeetCode 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 we
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 algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!