Facebook Pixel

2328. Number of Increasing Paths in a Grid

Problem Description

You have an m x n grid filled with integers. From any cell in the grid, you can move to any of its 4 adjacent cells (up, down, left, right).

Your task is to count all possible paths in the grid where:

  • The values along the path are strictly increasing (each cell's value must be greater than the previous cell's value)
  • You can start from any cell in the grid
  • You can end at any cell in the grid

Two paths are considered different if they visit a different sequence of cells, even if they have the same starting and ending points.

Since the total number of such paths can be very large, return the result modulo 10^9 + 7.

For example, if you're at a cell with value 5, you can only move to adjacent cells with values greater than 5 to maintain a strictly increasing path. Each cell can be the starting point of multiple paths of different lengths - even a single cell by itself counts as a valid path of length 1.

The solution uses Dynamic Programming with memoization through a DFS approach. The function dfs(i, j) calculates the number of strictly increasing paths starting from position (i, j). For each cell, it:

  1. Counts itself as a path of length 1
  2. Recursively explores all adjacent cells with larger values
  3. Sums up all possible paths from that starting position

The final answer is the sum of dfs(i, j) for all positions (i, j) in the grid, representing all possible strictly increasing paths starting from every cell.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: The grid can be viewed as a graph where each cell is a node, and edges exist between adjacent cells. We can move from one cell to another if they are adjacent and satisfy the strictly increasing condition.

Is it a tree?

  • No: The graph is not a tree. There can be multiple paths between cells, and the structure can have cycles (though we only traverse in strictly increasing order, which prevents revisiting cells in a single path).

Is the problem related to directed acyclic graphs?

  • Yes: When we consider only valid moves (from a cell to an adjacent cell with a strictly larger value), the graph becomes a DAG. The strictly increasing constraint ensures no cycles can exist in our traversal paths.

Topological Sort?

  • While topological sort could be applied here, the problem is more about counting paths rather than ordering. Let's continue exploring other options.

Looking at the problem characteristics:

  • We need to explore all possible paths from every starting position
  • Each path must maintain a strictly increasing order
  • We need to count all such valid paths

Does the problem have small constraints?

  • The constraints can vary, but the problem requires exploring all paths systematically.

DFS/backtracking?

  • Yes: DFS is perfect for this problem because:
    1. We need to explore all possible paths from each starting cell
    2. The strictly increasing constraint naturally creates a termination condition
    3. We can use memoization to avoid recalculating paths from the same cell
    4. DFS allows us to explore each path completely before moving to the next

Conclusion: The flowchart leads us to use DFS with memoization. The grid forms a graph where we perform DFS from each cell, exploring all strictly increasing paths. The memoization ensures we don't recalculate the number of paths from any cell more than once, making the solution efficient.

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

Intuition

When we look at this problem, we need to count all possible strictly increasing paths in a grid. The key insight is that from any cell, we can explore multiple paths, and these paths can have different lengths - even a single cell counts as a valid path.

Think of each cell as a potential starting point for exploration. From any cell with value v, we can only move to adjacent cells with values greater than v. This creates a natural recursive structure: the number of paths starting from a cell equals 1 (the cell itself) plus the sum of all paths that can be reached from its valid neighbors.

For example, if we're at cell (i, j) with value 5, and it has two adjacent cells with values 7 and 8, then:

  • We have 1 path of length 1 (just staying at the current cell)
  • Plus all paths that start from the cell with value 7
  • Plus all paths that start from the cell with value 8

This recursive relationship suggests using DFS. However, we might visit the same cell multiple times from different starting points or different paths. If we've already calculated the number of paths from cell (x, y), we shouldn't recalculate it - we should reuse that result. This is where memoization comes in.

The strictly increasing constraint is crucial because it:

  1. Prevents infinite loops (we can never return to a previously visited cell in the same path)
  2. Creates a natural DAG structure when we consider valid moves
  3. Ensures that once we calculate paths from a cell, that count remains valid regardless of how we reached that cell

Therefore, we use DFS with memoization: for each cell in the grid, we calculate how many strictly increasing paths start from that cell, cache the result, and sum up all these counts to get our final answer.

Learn more about Depth-First Search, Breadth-First Search, Graph, Topological Sort, Memoization and Dynamic Programming patterns.

Solution Approach

The solution implements DFS with memoization using Python's @cache decorator. Let's walk through the implementation step by step:

Core DFS Function: We define dfs(i, j) which calculates the number of strictly increasing paths starting from position (i, j). The @cache decorator automatically memoizes results, storing them in a dictionary to avoid redundant calculations.

Base Case: Each cell itself forms a valid path of length 1, so we initialize ans = 1 for every cell we visit.

Exploring Neighbors: We use pairwise((-1, 0, 1, 0, -1)) to generate direction pairs: (-1, 0), (0, 1), (1, 0), (0, -1), representing moves up, right, down, and left respectively. For each direction:

  • Calculate the new position: x, y = i + a, j + b
  • Check if the new position is valid: 0 <= x < m and 0 <= y < n
  • Check if we can move there (strictly increasing): grid[i][j] < grid[x][y]
  • If both conditions are met, recursively calculate paths from (x, y) and add to our answer

Modulo Operation: Since the answer can be very large, we apply modulo 10^9 + 7 at two key points:

  1. When accumulating paths in the DFS function: ans = (ans + dfs(x, y)) % mod
  2. When computing the final sum: sum(...) % mod

Final Aggregation: We call dfs(i, j) for every cell (i, j) in the grid and sum all results. This gives us the total count of all strictly increasing paths starting from any cell.

Time Complexity: O(m * n) where each cell is computed once due to memoization, and each computation checks at most 4 neighbors.

Space Complexity: O(m * n) for the memoization cache storing results for each cell.

The memoization is crucial here - without it, we would repeatedly recalculate paths from the same cells, leading to exponential time complexity. With memoization, each cell's path count is calculated exactly once, making the solution efficient.

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:

[1, 3, 5]
[2, 4, 6]

We'll use DFS with memoization to count all strictly increasing paths.

Step 1: Start DFS from cell (0,0) with value 1

  • Count itself as 1 path
  • Check neighbors:
    • Up: out of bounds
    • Right: (0,1) has value 3 > 1 βœ“
    • Down: (1,0) has value 2 > 1 βœ“
    • Left: out of bounds
  • Recursively call dfs(0,1) and dfs(1,0)

Step 2: DFS from cell (0,1) with value 3

  • Count itself as 1 path
  • Check neighbors:
    • Up: out of bounds
    • Right: (0,2) has value 5 > 3 βœ“
    • Down: (1,1) has value 4 > 3 βœ“
    • Left: (0,0) has value 1 < 3 βœ—
  • Recursively call dfs(0,2) and dfs(1,1)

Step 3: DFS from cell (0,2) with value 5

  • Count itself as 1 path
  • Check neighbors:
    • Right: out of bounds
    • Down: (1,2) has value 6 > 5 βœ“
    • Others: either out of bounds or smaller values
  • Recursively call dfs(1,2)

Step 4: DFS from cell (1,2) with value 6

  • Count itself as 1 path
  • All neighbors have smaller values
  • Return 1

Working backwards with memoization:

  • dfs(1,2) = 1 (just itself)
  • dfs(0,2) = 1 + dfs(1,2) = 1 + 1 = 2
  • dfs(1,1) = 1 + dfs(1,2) = 1 + 1 = 2 (can go to cell with value 6)
  • dfs(0,1) = 1 + dfs(0,2) + dfs(1,1) = 1 + 2 + 2 = 5
  • dfs(1,0) = 1 + dfs(1,1) = 1 + 2 = 3 (can go to cell with value 4)
  • dfs(0,0) = 1 + dfs(0,1) + dfs(1,0) = 1 + 5 + 3 = 9

Similarly calculating for all starting positions:

  • dfs(0,0) = 9 (already calculated)
  • dfs(0,1) = 5 (already cached)
  • dfs(0,2) = 2 (already cached)
  • dfs(1,0) = 3 (already cached)
  • dfs(1,1) = 2 (already cached)
  • dfs(1,2) = 1 (already cached)

Final answer: 9 + 5 + 2 + 3 + 2 + 1 = 22 total paths

These 22 paths include:

  • 6 paths of length 1 (each cell by itself)
  • Paths like [1β†’3], [1β†’2], [3β†’5], [3β†’4], etc.
  • Longer paths like [1β†’3β†’5], [1β†’2β†’4β†’6], [1β†’3β†’5β†’6], etc.

The memoization ensures we calculate each cell's contribution only once, even though multiple paths might pass through it.

Solution Implementation

1class Solution:
2    def countPaths(self, grid: List[List[int]]) -> int:
3        """
4        Count the number of strictly increasing paths in the grid.
5        Each path starts from any cell and moves to adjacent cells with larger values.
6        """
7      
8        # Use memoization to cache results of subproblems
9        @cache
10        def dfs(row: int, col: int) -> int:
11            """
12            Calculate the number of increasing paths starting from (row, col).
13          
14            Args:
15                row: Current row index
16                col: Current column index
17          
18            Returns:
19                Number of paths starting from this cell (including single cell path)
20            """
21            # Start with 1 to count the path containing only the current cell
22            path_count = 1
23          
24            # Define four directions: up, right, down, left
25            directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
26          
27            # Explore all four adjacent cells
28            for delta_row, delta_col in directions:
29                next_row = row + delta_row
30                next_col = col + delta_col
31              
32                # Check if the next cell is within bounds and has a greater value
33                if (0 <= next_row < rows and 
34                    0 <= next_col < cols and 
35                    grid[row][col] < grid[next_row][next_col]):
36                    # Add paths from the next cell to current count
37                    path_count = (path_count + dfs(next_row, next_col)) % MOD
38          
39            return path_count
40      
41        # Define constants
42        MOD = 10**9 + 7
43        rows = len(grid)
44        cols = len(grid[0])
45      
46        # Calculate total paths by summing paths starting from each cell
47        total_paths = 0
48        for i in range(rows):
49            for j in range(cols):
50                total_paths = (total_paths + dfs(i, j)) % MOD
51      
52        return total_paths
53
1class Solution {
2    // Memoization array to store number of paths from each cell
3    private int[][] memo;
4    // Input grid
5    private int[][] grid;
6    // Grid dimensions
7    private int rows;
8    private int cols;
9    // Modulo value for large numbers
10    private static final int MOD = (int) 1e9 + 7;
11
12    /**
13     * Count the number of strictly increasing paths in the grid.
14     * Each path can start from any cell and move to adjacent cells with strictly greater values.
15     * 
16     * @param grid 2D array of integers
17     * @return Total number of strictly increasing paths modulo 10^9 + 7
18     */
19    public int countPaths(int[][] grid) {
20        this.rows = grid.length;
21        this.cols = grid[0].length;
22        this.grid = grid;
23        this.memo = new int[rows][cols];
24      
25        int totalPaths = 0;
26      
27        // Calculate paths starting from each cell in the grid
28        for (int row = 0; row < rows; row++) {
29            for (int col = 0; col < cols; col++) {
30                totalPaths = (totalPaths + dfs(row, col)) % MOD;
31            }
32        }
33      
34        return totalPaths;
35    }
36
37    /**
38     * Depth-first search to count increasing paths starting from cell (row, col).
39     * Uses memoization to avoid recalculating paths from the same cell.
40     * 
41     * @param row Current row index
42     * @param col Current column index
43     * @return Number of increasing paths starting from this cell
44     */
45    private int dfs(int row, int col) {
46        // Return memoized result if already computed
47        if (memo[row][col] != 0) {
48            return memo[row][col];
49        }
50      
51        // Every cell itself forms a path of length 1
52        int pathCount = 1;
53      
54        // Direction vectors for up, right, down, left movements
55        int[] directions = {-1, 0, 1, 0, -1};
56      
57        // Explore all four adjacent cells
58        for (int dir = 0; dir < 4; dir++) {
59            int nextRow = row + directions[dir];
60            int nextCol = col + directions[dir + 1];
61          
62            // Check if next cell is within bounds and has a strictly greater value
63            if (nextRow >= 0 && nextRow < rows && 
64                nextCol >= 0 && nextCol < cols && 
65                grid[row][col] < grid[nextRow][nextCol]) {
66              
67                // Add paths from the next cell to current path count
68                pathCount = (pathCount + dfs(nextRow, nextCol)) % MOD;
69            }
70        }
71      
72        // Memoize and return the result
73        memo[row][col] = pathCount;
74        return pathCount;
75    }
76}
77
1class Solution {
2public:
3    int countPaths(vector<vector<int>>& grid) {
4        const int MOD = 1e9 + 7;
5        int rows = grid.size();
6        int cols = grid[0].size();
7      
8        // dp[i][j] stores the number of increasing paths starting from cell (i, j)
9        // Using 0 to indicate unvisited cells
10        vector<vector<int>> dp(rows, vector<int>(cols, 0));
11      
12        // Define DFS function to calculate paths starting from cell (row, col)
13        function<int(int, int)> dfs = [&](int row, int col) -> int {
14            // If already computed, return memoized result
15            if (dp[row][col] != 0) {
16                return dp[row][col];
17            }
18          
19            // Every cell itself forms a path of length 1
20            int pathCount = 1;
21          
22            // Direction vectors for moving up, right, down, left
23            int directions[5] = {-1, 0, 1, 0, -1};
24          
25            // Explore all 4 adjacent cells
26            for (int dir = 0; dir < 4; ++dir) {
27                int nextRow = row + directions[dir];
28                int nextCol = col + directions[dir + 1];
29              
30                // Check if next cell is within bounds and has a strictly greater value
31                if (nextRow >= 0 && nextRow < rows && 
32                    nextCol >= 0 && nextCol < cols && 
33                    grid[row][col] < grid[nextRow][nextCol]) {
34                    // Add paths from the next cell to current path count
35                    pathCount = (pathCount + dfs(nextRow, nextCol)) % MOD;
36                }
37            }
38          
39            // Memoize and return the result
40            dp[row][col] = pathCount;
41            return pathCount;
42        };
43      
44        // Calculate total paths by summing paths starting from each cell
45        int totalPaths = 0;
46        for (int i = 0; i < rows; ++i) {
47            for (int j = 0; j < cols; ++j) {
48                totalPaths = (totalPaths + dfs(i, j)) % MOD;
49            }
50        }
51      
52        return totalPaths;
53    }
54};
55
1/**
2 * Counts the number of strictly increasing paths in a grid
3 * Each path must have strictly increasing values when moving to adjacent cells
4 * @param grid - 2D array of numbers representing the grid
5 * @returns Total number of strictly increasing paths modulo 10^9 + 7
6 */
7function countPaths(grid: number[][]): number {
8    const MOD: number = 1e9 + 7;
9    const rows: number = grid.length;
10    const cols: number = grid[0].length;
11  
12    // Memoization array to store the number of paths starting from each cell
13    const memo: number[][] = new Array(rows)
14        .fill(0)
15        .map(() => new Array(cols).fill(0));
16  
17    /**
18     * Performs depth-first search to count paths starting from position (row, col)
19     * Uses memoization to avoid redundant calculations
20     * @param row - Current row index
21     * @param col - Current column index
22     * @returns Number of strictly increasing paths starting from (row, col)
23     */
24    const dfs = (row: number, col: number): number => {
25        // Return cached result if already computed
26        if (memo[row][col] !== 0) {
27            return memo[row][col];
28        }
29      
30        // Each cell itself forms a path of length 1
31        let pathCount: number = 1;
32      
33        // Direction vectors for exploring 4 adjacent cells (up, right, down, left)
34        const directions: number[] = [-1, 0, 1, 0, -1];
35      
36        // Explore all 4 adjacent cells
37        for (let k = 0; k < 4; k++) {
38            const nextRow: number = row + directions[k];
39            const nextCol: number = col + directions[k + 1];
40          
41            // Check if next cell is within bounds and has a strictly greater value
42            if (nextRow >= 0 && nextRow < rows && 
43                nextCol >= 0 && nextCol < cols && 
44                grid[row][col] < grid[nextRow][nextCol]) {
45              
46                // Add paths from the next cell to current path count
47                pathCount = (pathCount + dfs(nextRow, nextCol)) % MOD;
48            }
49        }
50      
51        // Cache and return the result
52        memo[row][col] = pathCount;
53        return pathCount;
54    };
55  
56    // Calculate total paths by summing paths starting from each cell
57    let totalPaths: number = 0;
58    for (let i = 0; i < rows; i++) {
59        for (let j = 0; j < cols; j++) {
60            totalPaths = (totalPaths + dfs(i, j)) % MOD;
61        }
62    }
63  
64    return totalPaths;
65}
66

Time and Space Complexity

Time Complexity: O(m Γ— n)

The algorithm uses dynamic programming with memoization through the @cache decorator. Each cell (i, j) in the grid is visited and computed exactly once due to memoization. When dfs(i, j) is called for a specific cell, it explores up to 4 neighboring cells, which is a constant operation O(1). Since we call dfs for all m Γ— n cells in the grid (from the main function's sum comprehension), and each cell's result is computed only once and then cached, the total time complexity is O(m Γ— n).

Space Complexity: O(m Γ— n)

The space complexity consists of two main components:

  1. Memoization cache: The @cache decorator stores the result for each unique (i, j) pair. In the worst case, we compute and store results for all m Γ— n cells, requiring O(m Γ— n) space.
  2. Recursion call stack: In the worst case (when the grid values form a strictly increasing path through all cells), the recursion depth could reach O(m Γ— n). However, this is typically much less in practice.

The dominant factor is the memoization cache, giving us an overall space complexity of O(m Γ— n), where m and n are the number of rows and columns in the grid, respectively.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Forgetting to Count Single-Cell Paths

A critical mistake is forgetting that each cell by itself forms a valid path of length 1. Many developers initially write:

# INCORRECT - Missing single-cell paths
def dfs(row, col):
    path_count = 0  # Wrong! Should start with 1
    for delta_row, delta_col in directions:
        # ... exploring neighbors
    return path_count

This error leads to returning 0 for cells that have no adjacent cells with larger values (local maxima), completely missing all single-cell paths. The correct approach initializes path_count = 1 to account for the current cell as a valid path.

2. Incorrect Modulo Application

Applying modulo operations incorrectly or inconsistently can cause integer overflow or wrong results:

# INCORRECT - Missing modulo in intermediate calculations
def dfs(row, col):
    path_count = 1
    for delta_row, delta_col in directions:
        if valid_move:
            path_count = path_count + dfs(next_row, next_col)  # Missing modulo!
    return path_count % MOD

# Also INCORRECT - Applying modulo only at the end
total_paths = sum(dfs(i, j) for i in range(rows) for j in range(cols)) % MOD

The correct approach applies modulo at every addition to prevent overflow:

  • Inside DFS: path_count = (path_count + dfs(next_row, next_col)) % MOD
  • In final sum: Sum incrementally with modulo rather than summing everything first

3. Using Wrong Inequality for Path Validity

Using non-strict inequality (<=) instead of strict inequality (<) violates the problem requirement:

# INCORRECT - Allows equal values
if grid[row][col] <= grid[next_row][next_col]:  # Wrong!
    path_count = (path_count + dfs(next_row, next_col)) % MOD

# CORRECT - Strictly increasing
if grid[row][col] < grid[next_row][next_col]:
    path_count = (path_count + dfs(next_row, next_col)) % MOD

4. Manual Memoization Implementation Errors

Attempting manual memoization without proper initialization or key management:

# INCORRECT - Common manual memoization mistakes
memo = {}  # Might forget to initialize

def dfs(row, col):
    if (row, col) in memo:
        return memo[(row, col)]
  
    path_count = 1
    # ... calculation logic
  
    # Forgot to store result before returning!
    return path_count  # Should be: memo[(row, col)] = path_count; return path_count

Using @cache decorator eliminates these manual memoization errors and is cleaner.

5. Direction Array Implementation Issues

Incorrectly defining or iterating through directions:

# INCORRECT - Wrong direction pairs
directions = [-1, 0, 1, 0, -1]  # This is not a list of tuples!
for i in range(4):
    delta_row = directions[i]  # IndexError or wrong values
    delta_col = directions[i+1]

# CORRECT - Clear direction tuples
directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
for delta_row, delta_col in directions:
    # Clean and readable

The solution should use clear, explicit direction pairs to avoid index confusion and improve code readability.

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

How does quick sort divide the problem into subproblems?


Recommended Readings

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

Load More