Facebook Pixel

3459. Length of Longest V-Shaped Diagonal Segment

Problem Description

You are given a 2D integer matrix grid of size n x m, where each element can only be 0, 1, or 2.

The problem asks you to find the length of the longest V-shaped diagonal segment in the grid. A V-shaped diagonal segment follows these specific rules:

  1. Must start with the value 1: The first element of the segment must be 1.

  2. Follows a specific sequence: After the initial 1, the values must follow the repeating pattern: 2, 0, 2, 0, 2, 0, ...

  3. Movement along diagonals: The segment moves along diagonal directions. There are four possible diagonal directions:

    • Top-left to bottom-right (↘)
    • Bottom-right to top-left (↖)
    • Top-right to bottom-left (↙)
    • Bottom-left to top-right (↗)
  4. Can make one turn: The segment starts moving in one diagonal direction and can optionally make at most one 90-degree clockwise turn to continue in another diagonal direction. The sequence pattern must be maintained throughout the entire path, including after the turn.

For example, a valid V-shaped segment could:

  • Start at a cell with value 1
  • Move diagonally right-down with values 2, 0, 2, 0...
  • Make a 90-degree clockwise turn
  • Continue diagonally left-down with the sequence continuing from where it left off

The length of a V-shaped diagonal segment is the total number of cells it contains, including the starting 1.

Your task is to find the length of the longest such V-shaped diagonal segment in the entire grid. If no valid V-shaped segment exists (for instance, if there are no 1s in the grid), return 0.

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

Intuition

To find the longest V-shaped diagonal segment, we need to consider that every valid segment must start with a 1. This gives us our starting points - we should check every cell containing 1 in the grid as a potential beginning of a V-shaped segment.

From each 1, we can move in four possible diagonal directions. Since we're looking for the longest segment, we need to explore all four directions from each starting point.

The key insight is that a V-shaped segment has two parts:

  1. The initial diagonal path in one direction
  2. An optional continuation after making a 90-degree clockwise turn

This naturally leads to a recursive exploration where at each step, we have two choices:

  • Continue in the same diagonal direction
  • Make a turn (if we haven't already) and continue in the new direction

We need to track our current position (i, j), the direction we're moving k, and whether we still have a turn available cnt. The sequence pattern (1, 2, 0, 2, 0, ...) determines what value we should expect at the next position - if the current cell is 1, the next should be 2; if it's 2, the next should be 0; if it's 0, the next should be 2.

The four diagonal directions can be represented using direction vectors. For a 90-degree clockwise turn, we simply move to the next direction in our circular list of directions: (k + 1) % 4.

Since we might revisit the same state multiple times (same position, same direction, same turn availability), memoization helps us avoid redundant calculations. Each state (i, j, k, cnt) will always yield the same maximum length, so we can cache these results.

The overall approach becomes: try starting from every 1 in the grid, explore all four initial directions, and for each path, recursively find the maximum length by considering both continuing straight and making a turn at each valid position.

Learn more about Memoization and Dynamic Programming patterns.

Solution Approach

The solution uses memoized depth-first search (DFS) to explore all possible V-shaped diagonal segments starting from each 1 in the grid.

Core Function Design:

We implement a recursive function dfs(i, j, k, cnt) where:

  • (i, j) represents the current position in the grid
  • k indicates the diagonal direction (0-3 for the four diagonal directions)
  • cnt tracks whether we can still make a turn (1 means we can turn, 0 means we've already turned)

Direction Representation:

The four diagonal directions are encoded using a direction array dirs = (1, 1, -1, -1, 1). This clever representation allows us to get direction vectors:

  • Direction 0: (dirs[0], dirs[1]) = (1, 1) - moving down-right
  • Direction 1: (dirs[1], dirs[2]) = (1, -1) - moving down-left
  • Direction 2: (dirs[2], dirs[3]) = (-1, -1) - moving up-left
  • Direction 3: (dirs[3], dirs[4]) = (-1, 1) - moving up-right

DFS Logic:

  1. Calculate next position: From current position (i, j) and direction k, compute next position as (x, y) = (i + dirs[k], j + dirs[k + 1])

  2. Determine expected value: Based on the sequence pattern 1, 2, 0, 2, 0, ...:

    • If current cell is 1, next should be 2
    • If current cell is 2, next should be 0
    • If current cell is 0, next should be 2

    This is elegantly computed as: target = 2 if grid[i][j] == 1 else (2 - grid[i][j])

  3. Boundary and validity check: If the next position is out of bounds or doesn't contain the expected value, return 0

  4. Recursive exploration: We have two options:

    • Continue in the same direction: dfs(x, y, k, cnt)
    • Make a 90-degree clockwise turn (if cnt > 0): dfs(x, y, (k + 1) % 4, 0)

    We take the maximum of these options and add 1 for the current cell

Main Algorithm:

  1. Iterate through the entire grid to find all cells containing 1
  2. For each 1 found at position (i, j):
    • Try all four diagonal directions as starting directions
    • Call dfs(i, j, k, 1) + 1 for each direction k (adding 1 for the starting cell)
    • Track the maximum length found

Memoization:

The @cache decorator automatically memoizes the dfs function results. This prevents redundant calculations when the same state (i, j, k, cnt) is encountered multiple times during the search.

Time Complexity: O(m × n × 4 × 2) = O(m × n) where each cell can be visited with at most 4 directions and 2 turn states.

Space Complexity: O(m × n × 4 × 2) for the memoization cache plus recursion stack space.

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 this 4x4 grid:

1  2  0  1
0  2  0  2
2  0  2  0
1  2  0  2

Step 1: Find all starting positions We scan the grid and find 1s at positions (0,0), (0,3), and (3,0).

Step 2: Explore from position (0,0)

Starting from (0,0) with value 1, we try all four diagonal directions:

  • Direction 0 (down-right):

    • From (0,0), move to (1,1)
    • Expected value: 2 (since current is 1)
    • Grid[1][1] = 2 ✓ Valid!
    • Continue from (1,1), move to (2,2)
    • Expected value: 0 (since current is 2)
    • Grid[2][2] = 2 ✗ Invalid, this path ends
    • Length so far: 1 (start) + 1 (continued) = 2
  • Direction 1 (down-left):

    • From (0,0), move to (1,-1)
    • Out of bounds! This path ends immediately
    • Length: 1

Let's focus on a more promising path from position (0,3):

Step 3: Explore from position (0,3) with direction 1 (down-left)

  • Start at (0,3) with value 1
  • Move down-left to (1,2): Expected 2, found 0 ✗ Invalid

Step 4: Explore from position (0,3) with direction 0 (down-right)

  • Start at (0,3) with value 1
  • Move down-right to (1,4): Out of bounds!

Step 5: Explore from position (3,0) with direction 0 (down-right)

  • Start at (3,0) with value 1
  • Move down-right to (4,1): Out of bounds!

Step 6: Explore from position (3,0) with direction 3 (up-right)

This is where it gets interesting:

  • Start at (3,0) with value 1, can_turn = 1
  • Move up-right to (2,1): Expected 2, found 0 ✗
  • But wait, we can try other directions...

Let's try a path that includes a turn. From position (0,0):

Direction 0 with a turn:

  • Start at (0,0) with value 1, direction 0 (down-right), can_turn = 1

  • Move to (1,1): Expected 2, found 2 ✓

  • From (1,1), we have two choices:

    Choice A: Continue straight (direction 0)

    • Move to (2,2): Expected 0, found 2 ✗ Path ends

    Choice B: Make a 90° clockwise turn to direction 1 (down-left)

    • Move to (2,0): Expected 0, found 2 ✗ Path ends

The DFS function returns the maximum of these choices plus 1 for the current cell.

Memoization in action: If we later explore from another starting point and reach position (1,1) with direction 0 and can_turn = 1, we don't need to recalculate - we just return the cached result.

Final Result: After exploring all possible V-shaped segments from all starting 1s, we find the maximum length. In this example, the longest valid V-shaped segment has length 2 (starting from (0,0) going down-right one step).

The key insight is that at each step, we:

  1. Check if the next cell matches the expected sequence value
  2. If valid, recursively explore both continuing straight and making a turn (if available)
  3. Cache results to avoid recalculation
  4. Return the maximum path length found

Solution Implementation

1class Solution:
2    def lenOfVDiagonal(self, grid: List[List[int]]) -> int:
3        from functools import cache
4      
5        @cache
6        def dfs(row: int, col: int, direction: int, can_change_direction: int) -> int:
7            """
8            Depth-first search to find the longest diagonal path.
9          
10            Args:
11                row: Current row position
12                col: Current column position
13                direction: Current direction index (0-3 for 4 diagonal directions)
14                can_change_direction: Flag indicating if direction change is allowed (1 or 0)
15          
16            Returns:
17                Length of the longest valid path from the next position
18            """
19            # Calculate next position based on current direction
20            next_row = row + directions[direction]
21            next_col = col + directions[direction + 1]
22          
23            # Determine the target value for the next cell
24            # If current cell is 1, target is 2; otherwise adjust based on current value
25            if grid[row][col] == 1:
26                target_value = 2
27            else:
28                target_value = 2 - grid[row][col]
29          
30            # Check if next position is valid and has the target value
31            if not (0 <= next_row < rows and 0 <= next_col < cols) or grid[next_row][next_col] != target_value:
32                return 0
33          
34            # Continue in the same direction
35            result = dfs(next_row, next_col, direction, can_change_direction)
36          
37            # If direction change is allowed, try changing direction
38            if can_change_direction > 0:
39                new_direction = (direction + 1) % 4
40                result = max(result, dfs(next_row, next_col, new_direction, 0))
41          
42            return 1 + result
43      
44        # Grid dimensions
45        rows = len(grid)
46        cols = len(grid[0])
47      
48        # Direction vectors for diagonal movements: SE, SW, NW, NE
49        # (row_delta, col_delta) pairs in cyclic order
50        directions = (1, 1, -1, -1, 1)
51      
52        max_length = 0
53      
54        # Iterate through all cells in the grid
55        for i in range(rows):
56            for j in range(cols):
57                # Start path only from cells with value 1
58                if grid[i][j] == 1:
59                    # Try all 4 diagonal directions
60                    for k in range(4):
61                        # Calculate max path length starting from this cell
62                        # Add 1 to include the starting cell itself
63                        path_length = dfs(i, j, k, 1) + 1
64                        max_length = max(max_length, path_length)
65      
66        return max_length
67
1class Solution {
2    // Grid dimensions
3    private int rows, cols;
4  
5    // Direction vectors for diagonal movements: 
6    // (1,1) = down-right, (1,-1) = down-left, (-1,-1) = up-left, (-1,1) = up-right
7    private final int[] directionOffsets = {1, 1, -1, -1, 1};
8  
9    // Memoization array: [row][col][direction][canChangeDirection]
10    // Stores the maximum diagonal length from position (row, col) in given direction
11    // with or without the ability to change direction once
12    private Integer[][][][] memo;
13
14    public int lenOfVDiagonal(int[][] grid) {
15        rows = grid.length;
16        cols = grid[0].length;
17        memo = new Integer[rows][cols][4][2];
18      
19        int maxDiagonalLength = 0;
20      
21        // Try starting from each cell that contains value 1
22        for (int row = 0; row < rows; row++) {
23            for (int col = 0; col < cols; col++) {
24                if (grid[row][col] == 1) {
25                    // Try all 4 diagonal directions from this starting point
26                    for (int direction = 0; direction < 4; direction++) {
27                        // Start with ability to change direction once (canChange = 1)
28                        int diagonalLength = dfs(grid, row, col, direction, 1) + 1;
29                        maxDiagonalLength = Math.max(maxDiagonalLength, diagonalLength);
30                    }
31                }
32            }
33        }
34      
35        return maxDiagonalLength;
36    }
37
38    /**
39     * Performs depth-first search to find the maximum diagonal length
40     * @param grid The input grid
41     * @param currentRow Current row position
42     * @param currentCol Current column position
43     * @param direction Current direction (0-3 representing 4 diagonal directions)
44     * @param canChangeDirection Whether we can still change direction (1 = yes, 0 = no)
45     * @return Maximum diagonal length from the next position (excluding current position)
46     */
47    private int dfs(int[][] grid, int currentRow, int currentCol, int direction, int canChangeDirection) {
48        // Return memoized result if already computed
49        if (memo[currentRow][currentCol][direction][canChangeDirection] != null) {
50            return memo[currentRow][currentCol][direction][canChangeDirection];
51        }
52      
53        // Calculate next position based on current direction
54        int nextRow = currentRow + directionOffsets[direction];
55        int nextCol = currentCol + directionOffsets[direction + 1];
56      
57        // Determine the expected value at next position
58        // Pattern appears to alternate between values (1 -> 2 -> 3 -> 1...)
59        int currentValue = grid[currentRow][currentCol];
60        int expectedNextValue = currentValue == 1 ? 2 : (2 - currentValue);
61      
62        // Check if next position is valid and contains expected value
63        if (nextRow < 0 || nextRow >= rows || nextCol < 0 || nextCol >= cols 
64            || grid[nextRow][nextCol] != expectedNextValue) {
65            memo[currentRow][currentCol][direction][canChangeDirection] = 0;
66            return 0;
67        }
68      
69        // Continue in same direction from next position
70        int maxLength = dfs(grid, nextRow, nextCol, direction, canChangeDirection);
71      
72        // If we can still change direction, try changing to next clockwise diagonal
73        if (canChangeDirection > 0) {
74            int newDirection = (direction + 1) % 4;
75            int lengthWithDirectionChange = dfs(grid, nextRow, nextCol, newDirection, 0);
76            maxLength = Math.max(maxLength, lengthWithDirectionChange);
77        }
78      
79        // Store result in memoization array and return
80        memo[currentRow][currentCol][direction][canChangeDirection] = 1 + maxLength;
81        return 1 + maxLength;
82    }
83}
84
1class Solution {
2public:
3    // Maximum grid dimensions
4    static constexpr int MAX_SIZE = 501;
5  
6    // Memoization table: [row][col][direction][canTurn]
7    // dp[i][j][dir][canTurn] stores the maximum diagonal length starting from (i,j) 
8    // going in direction 'dir', with ability to turn once if canTurn is 1
9    int dp[MAX_SIZE][MAX_SIZE][4][2];
10
11    int lenOfVDiagonal(vector<vector<int>>& grid) {
12        int rows = grid.size();
13        int cols = grid[0].size();
14      
15        // Direction vectors for 4 diagonal directions:
16        // 0: down-right, 1: down-left, 2: up-left, 3: up-right
17        int directionOffsets[5] = {1, 1, -1, -1, 1};
18      
19        // Initialize memoization table with -1 (unvisited state)
20        memset(dp, -1, sizeof(dp));
21
22        // DFS function to find the longest diagonal path with at most one turn
23        auto dfs = [&](this auto&& dfs, int row, int col, int direction, int canTurn) -> int {
24            // Return cached result if already computed
25            if (dp[row][col][direction][canTurn] != -1) {
26                return dp[row][col][direction][canTurn];
27            }
28          
29            // Calculate next position based on current direction
30            int nextRow = row + directionOffsets[direction];
31            int nextCol = col + directionOffsets[direction + 1];
32          
33            // Determine the expected value at next position (alternating pattern)
34            // If current is 1, expect 2; if current is 0, expect 2; if current is 2, expect 1
35            int expectedValue = grid[row][col] == 1 ? 2 : (2 - grid[row][col]);
36          
37            // Check if next position is valid and has the expected value
38            if (nextRow < 0 || nextRow >= rows || nextCol < 0 || nextCol >= cols || 
39                grid[nextRow][nextCol] != expectedValue) {
40                dp[row][col][direction][canTurn] = 0;
41                return 0;
42            }
43          
44            // Continue in the same direction from next position
45            int result = dfs(nextRow, nextCol, direction, canTurn);
46          
47            // If we still have a turn available, try turning to the next direction
48            if (canTurn > 0) {
49                int turnResult = dfs(nextRow, nextCol, (direction + 1) % 4, 0);
50                result = max(result, turnResult);
51            }
52          
53            // Cache and return the result (add 1 for current edge)
54            dp[row][col][direction][canTurn] = 1 + result;
55            return 1 + result;
56        };
57
58        int maxDiagonalLength = 0;
59      
60        // Try starting from each cell with value 1
61        for (int i = 0; i < rows; ++i) {
62            for (int j = 0; j < cols; ++j) {
63                if (grid[i][j] == 1) {
64                    // Try all 4 diagonal directions from this starting point
65                    for (int dir = 0; dir < 4; ++dir) {
66                        // Start with ability to turn once (canTurn = 1)
67                        // Add 1 for the starting cell itself
68                        maxDiagonalLength = max(maxDiagonalLength, dfs(i, j, dir, 1) + 1);
69                    }
70                }
71            }
72        }
73      
74        return maxDiagonalLength;
75    }
76};
77
1// Maximum grid dimensions
2const MAX_SIZE = 501;
3
4// Memoization table: [row][col][direction][canTurn]
5// dp[i][j][dir][canTurn] stores the maximum diagonal length starting from (i,j) 
6// going in direction 'dir', with ability to turn once if canTurn is 1
7const dp: number[][][][] = Array(MAX_SIZE).fill(null).map(() => 
8    Array(MAX_SIZE).fill(null).map(() => 
9        Array(4).fill(null).map(() => 
10            Array(2).fill(-1)
11        )
12    )
13);
14
15function lenOfVDiagonal(grid: number[][]): number {
16    const rows = grid.length;
17    const cols = grid[0].length;
18  
19    // Direction vectors for 4 diagonal directions:
20    // 0: down-right, 1: down-left, 2: up-left, 3: up-right
21    const directionOffsets = [1, 1, -1, -1, 1];
22  
23    // Reset memoization table to -1 (unvisited state)
24    for (let i = 0; i < rows; i++) {
25        for (let j = 0; j < cols; j++) {
26            for (let dir = 0; dir < 4; dir++) {
27                for (let turn = 0; turn < 2; turn++) {
28                    dp[i][j][dir][turn] = -1;
29                }
30            }
31        }
32    }
33
34    // DFS function to find the longest diagonal path with at most one turn
35    function dfs(row: number, col: number, direction: number, canTurn: number): number {
36        // Return cached result if already computed
37        if (dp[row][col][direction][canTurn] !== -1) {
38            return dp[row][col][direction][canTurn];
39        }
40      
41        // Calculate next position based on current direction
42        const nextRow = row + directionOffsets[direction];
43        const nextCol = col + directionOffsets[direction + 1];
44      
45        // Determine the expected value at next position (alternating pattern)
46        // If current is 1, expect 2; if current is 0, expect 2; if current is 2, expect 1
47        const expectedValue = grid[row][col] === 1 ? 2 : (2 - grid[row][col]);
48      
49        // Check if next position is valid and has the expected value
50        if (nextRow < 0 || nextRow >= rows || nextCol < 0 || nextCol >= cols || 
51            grid[nextRow][nextCol] !== expectedValue) {
52            dp[row][col][direction][canTurn] = 0;
53            return 0;
54        }
55      
56        // Continue in the same direction from next position
57        let result = dfs(nextRow, nextCol, direction, canTurn);
58      
59        // If we still have a turn available, try turning to the next direction
60        if (canTurn > 0) {
61            const turnResult = dfs(nextRow, nextCol, (direction + 1) % 4, 0);
62            result = Math.max(result, turnResult);
63        }
64      
65        // Cache and return the result (add 1 for current edge)
66        dp[row][col][direction][canTurn] = 1 + result;
67        return 1 + result;
68    }
69
70    let maxDiagonalLength = 0;
71  
72    // Try starting from each cell with value 1
73    for (let i = 0; i < rows; i++) {
74        for (let j = 0; j < cols; j++) {
75            if (grid[i][j] === 1) {
76                // Try all 4 diagonal directions from this starting point
77                for (let dir = 0; dir < 4; dir++) {
78                    // Start with ability to turn once (canTurn = 1)
79                    // Add 1 for the starting cell itself
80                    maxDiagonalLength = Math.max(maxDiagonalLength, dfs(i, j, dir, 1) + 1);
81                }
82            }
83        }
84    }
85  
86    return maxDiagonalLength;
87}
88

Time and Space Complexity

The time complexity is O(m × n), where m and n are the number of rows and columns in the matrix, respectively.

Despite the nested loops and recursive DFS calls, the @cache decorator ensures that each unique state (i, j, k, cnt) is computed only once. Since there are at most m × n positions, 4 directions, and 2 possible values for cnt (0 or 1), the total number of unique states is bounded by O(m × n × 4 × 2) = O(m × n). Each state computation takes O(1) time excluding recursive calls. The outer loops iterate through all m × n cells and try 4 directions, giving O(m × n × 4) = O(m × n) iterations. Therefore, the overall time complexity is O(m × n).

The space complexity is O(m × n), where m and n are the number of rows and columns in the matrix, respectively.

The space is dominated by the memoization cache from the @cache decorator, which stores up to O(m × n × 4 × 2) = O(m × n) unique states. Additionally, the recursion stack depth in the worst case could be O(min(m, n)) when traversing diagonally across the grid. Since O(min(m, n)) ≤ O(m × n), the overall space complexity is O(m × n).

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

Common Pitfalls

1. Incorrect Direction Change Logic

Pitfall: A common mistake is implementing the 90-degree clockwise turn incorrectly. The problem specifically requires a clockwise turn, but developers might:

  • Allow turning in any direction (not just clockwise)
  • Allow multiple turns instead of at most one
  • Reset the direction change capability after making a turn

Example of incorrect implementation:

# WRONG: Allows turning to any direction
for new_direction in range(4):
    if new_direction != direction:
        result = max(result, dfs(next_row, next_col, new_direction, 0))

# WRONG: Allows multiple turns
result = max(result, dfs(next_row, next_col, (direction + 1) % 4, can_change_direction))

Solution: Ensure exactly one clockwise turn by using (direction + 1) % 4 and setting the turn flag to 0 after using it.

2. Sequence Pattern Continuation After Turn

Pitfall: Failing to maintain the sequence pattern after making a turn. The sequence 1, 2, 0, 2, 0, ... must continue seamlessly even after changing direction. Some might incorrectly restart the pattern after a turn.

Example scenario:

  • Path goes: 1 → 2 → 0 (moving southeast)
  • Makes a turn at the cell with value 0
  • Next cell must be 2 (continuing the pattern), not starting over with 1

Solution: The target value calculation should depend only on the current cell's value, not on whether a turn was made:

target_value = 2 if grid[row][col] == 1 else (2 - grid[row][col])

3. Starting Cell Counting

Pitfall: Forgetting to include the starting cell (with value 1) in the total length count. The DFS function returns the length from the next position, not including the current starting position.

Wrong approach:

max_length = max(max_length, dfs(i, j, k, 1))  # Missing the starting cell

Solution: Always add 1 for the starting cell:

path_length = dfs(i, j, k, 1) + 1

4. Memoization State Insufficiency

Pitfall: Not including all necessary state variables in the memoization. If you forget to include the can_change_direction parameter in the cache key, the function might return incorrect cached results for states that should be different.

Wrong approach:

@cache
def dfs(row, col, direction):  # Missing can_change_direction parameter
    # This would incorrectly treat paths with and without turn capability as the same

Solution: Include all state variables that affect the computation:

@cache
def dfs(row, col, direction, can_change_direction):
    # Properly distinguishes between states with different turn capabilities

5. Edge Case: Single Cell Path

Pitfall: Not handling the case where a cell with value 1 has no valid adjacent cells to continue the path. The function should still return 1 (the length of just the starting cell).

Solution: The current implementation handles this correctly by returning path_length = 0 + 1 = 1 when dfs returns 0 for all directions.

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

Is the following code DFS or BFS?

void search(Node root) {
  if (!root) return;
  visit(root);
  root.visited = true;
  for (Node node in root.adjacent) {
    if (!node.visited) {
      search(node);
    }
  }
}

Recommended Readings

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

Load More