Problem: 3459. 最长 V 形对角线段的长度
整体思路
这段代码旨在解决一个复杂的路径查找问题:在一个二维网格 grid
中,寻找由交替数值(比如1和2)构成的、最长的 "V字形"对角线路径 的长度。
这个 “V字形” 路径具有以下特征:
- 起点:必须从值为
1
的单元格开始。 - 交替数值:路径上的值必须严格交替。代码中,从一个值为
1
的点出发,下一步必须是值为2
的点,再下一步是值为0
(通过2-target
计算)的点,这似乎暗示路径上的值是1 -> 2 -> 0 -> 2 -> 0 ...
。 - 对角线移动:路径只能沿着四个对角线方向移动(右下、左下、左上、右上)。
- V字形:整条路径最多只能包含一次90度的转向。例如,从右下方向转为左下方向。
为了高效地解决这个问题,算法采用了 深度优先搜索 (DFS) + 记忆化搜索 (Memoization) 的策略,这本质上是一种动态规划。
其核心逻辑步骤如下:
入口与初始化:
lenOfVDiagonal
方法是主入口。它初始化了一个三维的memo
数组,用于记忆化存储已计算过的子问题的结果,以避免重复计算。- 它遍历整个
grid
,将每一个值为1
的单元格(i, j)
视为一个潜在的V形路径的起点。 - 对于每个起点,它会尝试从所有四个对角线方向 (
k=0..3
) 开始进行深度优先搜索。
深度优先搜索 (DFS) 的状态定义:
- 核心的
dfs
函数被设计用来回答:“从点(i, j)
出发,沿着方向k
,当前是否还允许拐弯 (turn
),并且期望下一个点的值为target
,能够走出的最长路径是多少?” - 状态参数:
(i, j, k, turn, target)
完整地定义了一个子问题。(i, j)
: 当前路径的末端位置。k
: 当前路径的前进方向。turn
: 一个标志位,1
表示还可以拐弯,0
表示已经拐过弯或者不允许拐弯。target
: 路径中下一个单元格期望的值。
- 核心的
DFS 的递归与状态转移:
- 前进:
dfs
函数首先尝试沿着当前方向k
前进一步。 - 剪枝/终止条件:如果前进一步后越出边界,或者遇到的单元格值不等于
target
,说明此路不通,返回路径长度0。 - 记忆化:在进行计算前,先检查
memo
数组中是否已有当前状态的解。如果有,直接返回。 - 递归探索:从新位置出发,探索两种可能的后续路径:
a. 直行:继续沿当前方向k
前进。递归调用dfs
,方向不变,turn
状态不变。
b. 拐弯:仅当turn == 1
时,尝试进行一次90度拐弯。递归调用dfs
,方向变为(k+1)%4
,并将turn
状态置为0
,表示拐弯机会已用尽。 dfs
函数返回的是这两种可能路径中的较长者,再加上当前点的长度1
。
- 前进:
状态编码与记忆化:
memo
数组的第三维大小为8
(1 << 3
)。这是因为需要为一个单元格(i, j)
存储不同状态的结果。- 代码通过
temp = k << 1 | turn
这个位运算,将方向k
(0-3,需2位) 和turn
(0-1,需1位) 两个状态变量压缩成一个0-7
之间的整数索引,高效地利用了memo
数组的空间。
结果聚合:
- 主函数
lenOfVDiagonal
不断用每次dfs
返回的路径长度(加上起点的1)来更新全局最大值ans
,最终得到整个网格中的最长V形路径。
- 主函数
完整代码
class Solution {
// DIRT: 定义四个对角线方向的偏移量
// {1, 1}: 右下, {1, -1}: 左下, {-1, -1}: 左上, {-1, 1}: 右上
private static final int[][] DIRT = { { 1, 1 }, { 1, -1 }, { -1, -1 }, { -1, 1 } };
/**
* 计算网格中最长的V形交替数值对角线路径的长度。
* @param grid 输入的二维网格
* @return 最长路径的长度
*/
public int lenOfVDiagonal(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
int ans = 0;
// memo: 记忆化数组。memo[i][j][state] 存储从(i,j)出发在特定状态下的最长路径。
// state 通过方向k和拐弯标记turn编码而来,共 4*2=8 种状态。
int[][][] memo = new int[m][n][1 << 3];
// 遍历所有单元格,寻找值为 1 的起点
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
// 对于每个起点,尝试从所有四个对角线方向开始搜索
for (int k = 0; k < 4; k++) {
// dfs返回的是后续路径长度,所以要 +1 包含起点本身
ans = Math.max(ans, dfs(i, j, k, 1, 2, grid, memo) + 1);
}
}
}
}
return ans;
}
/**
* 深度优先搜索函数,计算从 (i, j) 出发的特定状态下的最长路径。
* @param i 当前行
* @param j 当前列
* @param k 当前前进方向的索引 (0-3)
* @param turn 是否还允许拐弯 (1:是, 0:否)
* @param target 下一个单元格期望的值
* @param grid 网格
* @param memo 记忆化数组
* @return 从 (i, j) 的下一个点开始的最长路径长度
*/
private int dfs(int i, int j, int k, int turn, int target, int[][] grid, int[][][] memo) {
int m = grid.length;
int n = grid[0].length;
// 1. 沿当前方向 k 前进一步
i += DIRT[k][0];
j += DIRT[k][1];
// 2. 终止条件:如果越界或遇到的值不符合期望,则路径中断
if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] != target) {
return 0;
}
// 3. 状态编码与记忆化检查
// 将方向 k (0-3) 和 拐弯标记 turn (0-1) 压缩成一个 0-7 的整数
int temp = k << 1 | turn;
if (memo[i][j][temp] != 0) {
return memo[i][j][temp];
}
// 4. 递归探索
// a. 继续沿当前方向直行。下一个目标值根据代码是 2-target。
int res = dfs(i, j, k, turn, 2 - target, grid, memo);
// b. 如果还允许拐弯 (turn == 1),则尝试进行90度拐弯
// (k+1)%4 实现方向的循环切换(如右下->左下)
if (turn == 1) {
// 拐弯后,turn 标记变为 0,不再允许拐弯
res = Math.max(res, dfs(i, j, (k + 1) % 4, 0, 2 - target, grid, memo));
}
// 5. 记录结果并返回
// 返回的是后续路径的最大长度,所以要 +1 包含当前点 (i, j)
return memo[i][j][temp] = res + 1;
}
}
时空复杂度
时间复杂度:O(M * N)
- 状态数量:算法的核心是
dfs
函数和memo
数组。时间复杂度取决于需要计算的不同状态的总数。一个状态由(i, j, k, turn)
唯一确定。i
: 有M
种可能。j
: 有N
种可能。k
: 有4
种方向。turn
: 有2
种可能(能拐弯/不能拐弯)。- 总状态数 =
M * N * 4 * 2 = 8 * M * N
。
- 单次状态计算:由于有记忆化,每个状态只会被实际计算一次。在
dfs
函数中,一次计算涉及常数次的操作(几次加法、比较、位运算、最多两次递归调用)。因此,计算每个状态的平均时间复杂度是 O(1)。 - 入口循环:外层的
for
循环遍历整个网格,总共M * N
次。在每个值为1的单元格,它会启动4次DFS。
综合分析:
总的时间开销主要由填充整个 memo
数组决定。因此,总时间复杂度为 (状态总数) * (单次计算时间) = O(M * N * 8) * O(1)
,简化后为 O(M * N)。
空间复杂度:O(M * N)
- 主要存储开销:
- 记忆化数组
memo
:这是最主要的额外空间开销。memo
数组的大小是M * N * 8
。因此,这部分空间复杂度是 O(M * N)。 - 递归栈深度:
dfs
函数是递归的。在最坏的情况下,一条路径可能沿着对角线贯穿整个网格,其长度约为M + N
。因此,递归栈的最大深度是 O(M + N)。
- 记忆化数组
综合分析:
算法所需的额外空间由记忆化数组和递归栈深度共同决定。由于 O(M * N)
通常远大于 O(M + N)
,所以空间复杂度的瓶颈在于 memo
数组。因此,总的空间复杂度为 O(M * N)。
参考灵神