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:
-
Must start with the value
1
: The first element of the segment must be1
. -
Follows a specific sequence: After the initial
1
, the values must follow the repeating pattern:2, 0, 2, 0, 2, 0, ...
-
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 (↗)
-
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 1
s in the grid), return 0
.
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:
- The initial diagonal path in one direction
- 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 gridk
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:
-
Calculate next position: From current position
(i, j)
and directionk
, compute next position as(x, y) = (i + dirs[k], j + dirs[k + 1])
-
Determine expected value: Based on the sequence pattern
1, 2, 0, 2, 0, ...
:- If current cell is
1
, next should be2
- If current cell is
2
, next should be0
- If current cell is
0
, next should be2
This is elegantly computed as:
target = 2 if grid[i][j] == 1 else (2 - grid[i][j])
- If current cell is
-
Boundary and validity check: If the next position is out of bounds or doesn't contain the expected value, return
0
-
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
- Continue in the same direction:
Main Algorithm:
- Iterate through the entire grid to find all cells containing
1
- 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 directionk
(adding 1 for the starting cell) - Track the maximum length found
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 EvaluatorExample 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 1
s 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 1
s, 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:
- Check if the next cell matches the expected sequence value
- If valid, recursively explore both continuing straight and making a turn (if available)
- Cache results to avoid recalculation
- 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 with1
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.
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
Memoization Prereq Backtracking problems backtracking Memoization is a fancy word for a simple concept so is the case for a lot of things we learn in school It means saving the previous function call result in a dictionary and reading from it when we do the exact same call again
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
Coding Interview 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
Want a Structured Path to Master System Design Too? Don’t Miss This!