【LeetCode 每日一题】3459. 最长 V 形对角线段的长度

发布于:2025-09-13 ⋅ 阅读:(22) ⋅ 点赞:(0)

Problem: 3459. 最长 V 形对角线段的长度

整体思路

这段代码旨在解决一个复杂的路径查找问题:在一个二维网格 grid 中,寻找由交替数值(比如1和2)构成的、最长的 "V字形"对角线路径 的长度。

这个 “V字形” 路径具有以下特征:

  1. 起点:必须从值为 1 的单元格开始。
  2. 交替数值:路径上的值必须严格交替。代码中,从一个值为 1 的点出发,下一步必须是值为 2 的点,再下一步是值为 0(通过2-target计算)的点,这似乎暗示路径上的值是 1 -> 2 -> 0 -> 2 -> 0 ...
  3. 对角线移动:路径只能沿着四个对角线方向移动(右下、左下、左上、右上)。
  4. V字形:整条路径最多只能包含一次90度的转向。例如,从右下方向转为左下方向。

为了高效地解决这个问题,算法采用了 深度优先搜索 (DFS) + 记忆化搜索 (Memoization) 的策略,这本质上是一种动态规划。

其核心逻辑步骤如下:

  1. 入口与初始化

    • lenOfVDiagonal 方法是主入口。它初始化了一个三维的 memo 数组,用于记忆化存储已计算过的子问题的结果,以避免重复计算。
    • 它遍历整个 grid,将每一个值为 1 的单元格 (i, j) 视为一个潜在的V形路径的起点。
    • 对于每个起点,它会尝试从所有四个对角线方向 (k=0..3) 开始进行深度优先搜索。
  2. 深度优先搜索 (DFS) 的状态定义

    • 核心的 dfs 函数被设计用来回答:“从点 (i, j) 出发,沿着方向 k,当前是否还允许拐弯 (turn),并且期望下一个点的值为 target,能够走出的最长路径是多少?”
    • 状态参数(i, j, k, turn, target) 完整地定义了一个子问题。
      • (i, j): 当前路径的末端位置。
      • k: 当前路径的前进方向。
      • turn: 一个标志位,1 表示还可以拐弯,0 表示已经拐过弯或者不允许拐弯。
      • target: 路径中下一个单元格期望的值。
  3. DFS 的递归与状态转移

    • 前进dfs 函数首先尝试沿着当前方向 k 前进一步。
    • 剪枝/终止条件:如果前进一步后越出边界,或者遇到的单元格值不等于 target,说明此路不通,返回路径长度0。
    • 记忆化:在进行计算前,先检查 memo 数组中是否已有当前状态的解。如果有,直接返回。
    • 递归探索:从新位置出发,探索两种可能的后续路径:
      a. 直行:继续沿当前方向 k 前进。递归调用 dfs,方向不变,turn 状态不变。
      b. 拐弯仅当 turn == 1,尝试进行一次90度拐弯。递归调用 dfs,方向变为 (k+1)%4,并将 turn 状态置为 0,表示拐弯机会已用尽。
    • dfs 函数返回的是这两种可能路径中的较长者,再加上当前点的长度 1
  4. 状态编码与记忆化

    • memo 数组的第三维大小为 8 (1 << 3)。这是因为需要为一个单元格 (i, j) 存储不同状态的结果。
    • 代码通过 temp = k << 1 | turn 这个位运算,将方向 k (0-3,需2位) 和 turn (0-1,需1位) 两个状态变量压缩成一个 0-7 之间的整数索引,高效地利用了 memo 数组的空间。
  5. 结果聚合

    • 主函数 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)

  1. 状态数量:算法的核心是 dfs 函数和 memo 数组。时间复杂度取决于需要计算的不同状态的总数。一个状态由 (i, j, k, turn) 唯一确定。
    • i: 有 M 种可能。
    • j: 有 N 种可能。
    • k: 有 4 种方向。
    • turn: 有 2 种可能(能拐弯/不能拐弯)。
    • 总状态数 = M * N * 4 * 2 = 8 * M * N
  2. 单次状态计算:由于有记忆化,每个状态只会被实际计算一次。在 dfs 函数中,一次计算涉及常数次的操作(几次加法、比较、位运算、最多两次递归调用)。因此,计算每个状态的平均时间复杂度是 O(1)
  3. 入口循环:外层的 for 循环遍历整个网格,总共 M * N 次。在每个值为1的单元格,它会启动4次DFS。

综合分析
总的时间开销主要由填充整个 memo 数组决定。因此,总时间复杂度为 (状态总数) * (单次计算时间) = O(M * N * 8) * O(1),简化后为 O(M * N)

空间复杂度:O(M * N)

  1. 主要存储开销
    • 记忆化数组 memo:这是最主要的额外空间开销。memo 数组的大小是 M * N * 8。因此,这部分空间复杂度是 O(M * N)
    • 递归栈深度dfs 函数是递归的。在最坏的情况下,一条路径可能沿着对角线贯穿整个网格,其长度约为 M + N。因此,递归栈的最大深度是 O(M + N)

综合分析
算法所需的额外空间由记忆化数组和递归栈深度共同决定。由于 O(M * N) 通常远大于 O(M + N),所以空间复杂度的瓶颈在于 memo 数组。因此,总的空间复杂度为 O(M * N)

参考灵神


网站公告

今日签到

点亮在社区的每一天
去签到