865.具有所有最深节点的最小子树

目标

给定一个根为 root 的二叉树,每个节点的深度是 该节点到根的最短距离 。

返回包含原始树中所有 最深节点 的 最小子树 。

如果一个节点在 整个树 的任意节点之间具有最大的深度,则该节点是 最深的 。

一个节点的 子树 是该节点加上它的所有后代的集合。

示例 1:

输入:root = [3,5,1,6,2,0,8,null,null,7,4]
输出:[2,7,4]
解释:
我们返回值为 2 的节点,在图中用黄色标记。
在图中用蓝色标记的是树的最深的节点。
注意,节点 5、3 和 2 包含树中最深的节点,但节点 2 的子树最小,因此我们返回它。

示例 2:

输入:root = [1]
输出:[1]
解释:根节点是树中最深的节点。

示例 3:

输入:root = [0,1,3,null,2]
输出:[2]
解释:树中最深的节点为 2 ,有效子树为节点 2、1 和 0 的子树,但节点 2 的子树最小。

说明:

  • 树中节点的数量在 [1, 500] 范围内。
  • 0 <= Node.val <= 500
  • 每个节点的值都是 独一无二 的。

思路

有一颗二叉树 root,返回包含所有最深节点的最小子树。

dfs 返回子树最大深度,如果左右子树的深度都等于全局的最大深度,则返回当前节点。也就是找到所有最深节点的最近公共祖先。

代码


/**
 * @date 2026-01-09 8:45
 */
public class SubtreeWithAllDeepest865 {

    public TreeNode subtreeWithAllDeepest(TreeNode root) {
        dfs(root, -1);
        return res;
    }

    public int dfs(TreeNode node, int deep) {
        if (node == null) {
            return deep;
        }
        int left = dfs(node.left, deep + 1);
        int right = dfs(node.right, deep + 1);
        int max = Math.max(left, right);
        maxDeep = Math.max(max, maxDeep);
        if (left == maxDeep && right == maxDeep) {
            res = node;
        }
        return max;
    }

}

性能

1458.两个子序列的最大点积

目标

给你两个数组 nums1 和 nums2 。

请你返回 nums1 和 nums2 中两个长度相同的 非空 子序列的最大点积。

数组的非空子序列是通过删除原数组中某些元素(可能一个也不删除)后剩余数字组成的序列,但不能改变数字间相对顺序。比方说,[2,3,5] 是 [1,2,3,4,5] 的一个子序列而 [1,5,3] 不是。

示例 1:

输入:nums1 = [2,1,-2,5], nums2 = [3,0,-6]
输出:18
解释:从 nums1 中得到子序列 [2,-2] ,从 nums2 中得到子序列 [3,-6] 。
它们的点积为 (2*3 + (-2)*(-6)) = 18 。

示例 2:

输入:nums1 = [3,-2], nums2 = [2,-6,7]
输出:21
解释:从 nums1 中得到子序列 [3] ,从 nums2 中得到子序列 [7] 。
它们的点积为 (3*7) = 21 。

示例 3:

输入:nums1 = [-1,-1], nums2 = [1,1]
输出:-1
解释:从 nums1 中得到子序列 [-1] ,从 nums2 中得到子序列 [1] 。
它们的点积为 -1 。

说明:

  • 1 <= nums1.length, nums2.length <= 500
  • -1000 <= nums1[i], nums2[i] <= 1000

思路

有两个数组 nums1nums2,求这两个数组长度相等的子序列的点积的最大值。

定义 dp[i][j] 表示 nums1 的前 i 个元素的子序列与 nums2 的前 j 个元素的子序列的最大点积

  • 如果选择 nums[i] * nums[j],剩下的子问题变成 nums1 的前 i - 1 个元素的子序列 与 nums2 的前 j - 1 个元素的子序列的点积最大值,或者为 0,因为当前已经选择了,前面可以不选。即 Math.max(0, dp[i - 1][j - 1]) + nums[i] * nums[j]
  • 如果不选 nums[i] * nums[j],问题变成 nums1 的前 i - 1 个元素的子序列 与 nums2 的前 j 个元素的子序列的点积最大值 dp[i - 1][j],以及 dp[i][j - 1]dp[i - 1][j - 1]

代码


/**
 * @date 2026-01-08 9:05
 */
public class MaxDotProduct1458 {

    public int maxDotProduct(int[] nums1, int[] nums2) {
        int n1 = nums1.length;
        int n2 = nums2.length;
        int[][] dp = new int[n1 + 1][n2 + 1];
        Arrays.fill(dp[0], Integer.MIN_VALUE);
        for (int i = 0; i <= n1; i++) {
            dp[i][0] = Integer.MIN_VALUE;
        }
        for (int i = 1; i <= n1; i++) {
            for (int j = 1; j <= n2; j++) {
                dp[i][j] = Math.max(Math.max(0, dp[i - 1][j - 1]) + nums1[i - 1] * nums2[j - 1], Math.max(dp[i - 1][j], dp[i][j - 1]));
            }
        }
        return dp[n1][n2];
    }
}

性能

1339.分裂二叉树的最大乘积

目标

给你一棵二叉树,它的根为 root 。请你删除 1 条边,使二叉树分裂成两棵子树,且它们子树和的乘积尽可能大。

由于答案可能会很大,请你将结果对 10^9 + 7 取模后再返回。

示例 1:

输入:root = [1,2,3,4,5,6]
输出:110
解释:删除红色的边,得到 2 棵子树,和分别为 11 和 10 。它们的乘积是 110 (11*10)

示例 2:

输入:root = [1,null,2,3,4,null,null,5,6]
输出:90
解释:移除红色的边,得到 2 棵子树,和分别是 15 和 6 。它们的乘积为 90 (15*6)

示例 3:

输入:root = [2,3,9,10,7,8,6,5,4,11,1]
输出:1025

示例 4:

输入:root = [1,1]
输出:1

说明:

  • 每棵树最多有 50000 个节点,且至少有 2 个节点。
  • 每个节点的值在 [1, 10000] 之间。

思路

删除二叉树的一条边,使得产生的两个子树和的乘积最大。

直接的想法是两次遍历,第一次遍历求得所有节点和。第二次遍历则是计算乘积最大值。

可以将子树和保存起来,遍历结束后直接处理和。

注意:求的是最大的乘积然后取模,而非模的最大值。先找出最大的乘积再取模,不能先对乘积取模再比较。

代码


/**
 * @date 2026-01-07 8:52
 */
public class MaxProduct1339 {

    public long res = 0L;

    public int maxProduct(TreeNode root) {
        int mod = 1000000007;
        int totalSum = dfs(root);
        dfs(root, totalSum);
        return (int) (res % mod);
    }

    public int dfs(TreeNode node) {
        if (node == null) {
            return 0;
        }
        return node.val + dfs(node.left) + dfs(node.right);
    }

    public int dfs(TreeNode node, int totalSum) {
        if (node == null) {
            return 0;
        }
        int sum = node.val + dfs(node.left, totalSum) + dfs(node.right, totalSum);
        res = Math.max(res, (long) sum * (totalSum - sum));
        return sum;
    }

}

性能

1161.最大层内元素和

目标

给你一个二叉树的根节点 root。设根节点位于二叉树的第 1 层,而根节点的子节点位于第 2 层,依此类推。

返回总和 最大 的那一层的层号 x。如果有多层的总和一样大,返回其中 最小 的层号 x。

示例 1:

输入:root = [1,7,0,7,-8,null,null]
输出:2
解释:
第 1 层各元素之和为 1,
第 2 层各元素之和为 7 + 0 = 7,
第 3 层各元素之和为 7 + -8 = -1,
所以我们返回第 2 层的层号,它的层内元素之和最大。

示例 2:

输入:root = [989,null,10250,98693,-89388,null,null,null,-32127]
输出:2

说明:

  • 树中的节点数在 [1, 10^4]范围内
  • -10^5 <= Node.val <= 10^5

思路

定义二叉树根节点为第 1 层,子节点层号依次累加,求和最大的层号,如果和相同,返回最小的层号。

BFS/DFS 都可以。

代码


/**
 * @date 2026-01-06 8:47
 */
public class MaxLevelSum1161 {

    public int maxLevelSum(TreeNode root) {
        Deque<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        int res = 1;
        int level = 1;
        int max = Integer.MIN_VALUE;
        while (!q.isEmpty()) {
            int size = q.size();
            int sum = 0;
            for (int i = 0; i < size; i++) {
                TreeNode node = q.poll();
                sum += node.val;
                if (node.left != null) {
                    q.offer(node.left);
                }
                if (node.right != null) {
                    q.offer(node.right);
                }
            }
            if (max < sum) {
                max = sum;
                res = level;
            }
            level++;
        }
        return res;
    }

}

性能

1975.最大方阵和

目标

给你一个 n x n 的整数方阵 matrix 。你可以执行以下操作 任意次 :

  • 选择 matrix 中 相邻 两个元素,并将它们都 乘以 -1 。

如果两个元素有 公共边 ,那么它们就是 相邻 的。

你的目的是 最大化 方阵元素的和。请你在执行以上操作之后,返回方阵的 最大 和。

示例 1:

输入:matrix = [[1,-1],[-1,1]]
输出:4
解释:我们可以执行以下操作使和等于 4 :
- 将第一行的 2 个元素乘以 -1 。
- 将第一列的 2 个元素乘以 -1 。

示例 2:

输入:matrix = [[1,2,3],[-1,-2,-3],[1,2,3]]
输出:16
解释:我们可以执行以下操作使和等于 16 :
- 将第二行的最后 2 个元素乘以 -1 。

说明:

  • n == matrix.length == matrix[i].length
  • 2 <= n <= 250
  • -10^5 <= matrix[i][j] <= 10^5

思路

有一个 n x n 矩阵,每次操作可以将相邻的元素乘以 -1,执行操作任意次,求能够得到的最大方阵和。

经过观察发现,可以将任意两个元素乘以 -1,只需对路径上的每个元素执行操作,改变 (cur, next) 的符号,中间每个元素的符号都被改变了两次,即首尾元素改变了符号。

只需判断矩阵中负数的个数,如果是偶数,可以将负数全部变为相反数;如果是奇数,则需要找到最小的非负数,将其变为负数,其余元素全部变为非负数。

代码


/**
 * @date 2026-01-05 9:07
 */
public class MaxMatrixSum1975 {

    public long maxMatrixSum(int[][] matrix) {
        long res = 0L;
        int negativeCnt = 0;
        int min = Integer.MAX_VALUE;
        for (int[] row : matrix) {
            for (int col : row) {
                res += Math.abs(col);
                min = Math.min(min, Math.abs(col));
                if (col < 0) {
                    negativeCnt++;
                }
            }
        }
        if (negativeCnt % 2 == 1) {
            res -= 2 * min;
        }
        return res;
    }

}

性能

1390.四因数

目标

给你一个整数数组 nums,请你返回该数组中恰有四个因数的这些整数的各因数之和。如果数组中不存在满足题意的整数,则返回 0 。

示例 1:

输入:nums = [21,4,7]
输出:32
解释:
21 有 4 个因数:1, 3, 7, 21
4 有 3 个因数:1, 2, 4
7 有 2 个因数:1, 7
答案仅为 21 的所有因数的和。

示例 2:

输入: nums = [21,21]
输出: 64

示例 3:

输入: nums = [1,2,3,4,5]
输出: 0

说明:

  • 1 <= nums.length <= 10^4
  • 1 <= nums[i] <= 10^5

思路

有一个整数数组 nums,返回恰好有 4 个因数的元素的这些因数之和,如果不存在,返回 0

首先数组元素至少有两个因数(1 和它本身),只需要再找到两个因数即可。从 2 ~ sqrt(num) 判断能否整除 num,如果可以加上 inum / i,需要特殊处理 i * i = num 的情况,这时因数只能算一个。

代码


/**
 * @date 2026-01-04 9:05
 */
public class SumFourDivisors1390 {

    public int sumFourDivisors(int[] nums) {
        int res = 0;
        for (int num : nums) {
            res += sum(num);
        }
        return res;
    }

    public int sum(int num) {
        int cnt = 2;
        int sum = num + 1;
        for (int i = 2; i * i <= num; i++) {
            if (num % i == 0) {
                if (cnt == 4) {
                    sum = 0;
                    break;
                }
                if (i * i == num) {
                    sum += i;
                    cnt++;
                } else {
                    sum += i + num / i;
                    cnt += 2;
                }

            }
        }
        return cnt == 4 ? sum : 0;
    }

}

性能

1411.给Nx3网格图涂色的方案数

目标

你有一个 n x 3 的网格图 grid ,你需要用 红,黄,绿 三种颜色之一给每一个格子上色,且确保相邻格子颜色不同(也就是有相同水平边或者垂直边的格子颜色不同)。

给你网格图的行数 n 。

请你返回给 grid 涂色的方案数。由于答案可能会非常大,请你返回答案对 10^9 + 7 取余的结果。

示例 1:

输入:n = 1
输出:12
解释:总共有 12 种可行的方法:

示例 2:

输入:n = 2
输出:54

示例 3:

输入:n = 3
输出:246

示例 4:

输入:n = 7
输出:106494

示例 5:

输入:n = 5000
输出:30228214

说明:

  • n == grid.length
  • grid[i].length == 3
  • 1 <= n <= 5000

思路

代码

性能

961.在长度2N的数组中找出重复N次的元素

目标

给你一个整数数组 nums ,该数组具有以下属性:

  • nums.length == 2 * n.
  • nums 包含 n + 1 个 不同的 元素
  • nums 中恰有一个元素重复 n 次

找出并返回重复了 n 次的那个元素。

示例 1:

输入:nums = [1,2,3,3]
输出:3

示例 2:

输入:nums = [2,1,2,5,3,2]
输出:2

示例 3:

输入:nums = [5,1,5,2,5,3,5,4]
输出:5

说明:

  • 2 <= n <= 5000
  • nums.length == 2 * n
  • 0 <= nums[i] <= 10^4
  • nums 由 n + 1 个 不同的 元素组成,且其中一个元素恰好重复 n 次

思路

长度为 2 * n 数组 numsn + 1 个不同元素,其中恰好有一个元素重复 n 次,返回该重复元素。

除了该重复元素,其余元素各不相同。

暴力做法是使用哈希表记录元素的出现次数,如果出现次数大于 1,直接返回,空间复杂度为 O(n)

对于本题,从下标 1 开始与第一个元素比较,如果相等直接返回。否则问题变成从 2n - 1 个元素中找重复 n 次的元素,重复元素占绝对多数,可以使用摩尔投票算法。

还可以根据重复元素的最小间隔来分析:

  • 假设重复元素至少隔着一个其它元素,有 n - 1 个空隙,至少有 2 * n - 1 个元素,可能。
  • 假设重复元素至少隔着两个其它元素,有 n - 1 个空隙,至少有 n + 2 * (n - 1) = 3 * n - 2 个元素,当 n = 2 时,有可能,当 n > 2 时,必定存在一个重复元素的间隔小于 2,否则元素个数不够

也就是说,可以检查当前元素与其前 123 个元素是否相等来找出该重复元素。空间复杂度为 O(1)

以上算法的时间复杂度均为 O(n),还有一种期望 O(1) 的算法,使用随机数,随机选取两个元素。它们两个相等的概率是 n/2n × (n - 1)/(2n - 1) = 1/2 * (1 - 1/n)/(2 - 1/n),当 n = 2 时,p = 1/6,当 n -> ∞p -> 1/4。期望循环次数 <= 6。

代码


/**
 * @date 2026-01-04 14:45
 */
public class RepeatedNTimes961 {

    public int repeatedNTimes_v1(int[] nums) {
        int n = nums.length;
        int candidate = 0;
        int vote = 0;
        for (int i = 1; i < n; i++) {
            if (nums[i] == nums[0]) {
                return nums[0];
            }
            if (vote == 0) {
                candidate = nums[i];
                vote = 1;
            } else if (candidate != nums[i]) {
                vote--;
            } else {
                return candidate;
            }
        }
        return candidate;
    }

    public int repeatedNTimes(int[] nums) {
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] == nums[i - 1] || (i >= 2 && nums[i] == nums[i - 2]) || (i >= 3 && nums[i] == nums[i - 3])) {
                return nums[i];
            }
        }
        return 0;
    }

}

性能

66.加一

目标

给定一个表示 大整数 的整数数组 digits,其中 digits[i] 是整数的第 i 位数字。这些数字按从左到右,从最高位到最低位排列。这个大整数不包含任何前导 0。

将大整数加 1,并返回结果的数字数组。

示例 1:

输入:digits = [1,2,3]
输出:[1,2,4]
解释:输入数组表示数字 123。
加 1 后得到 123 + 1 = 124。
因此,结果应该是 [1,2,4]。

示例 2:

输入:digits = [4,3,2,1]
输出:[4,3,2,2]
解释:输入数组表示数字 4321。
加 1 后得到 4321 + 1 = 4322。
因此,结果应该是 [4,3,2,2]。

示例 3:

输入:digits = [9]
输出:[1,0]
解释:输入数组表示数字 9。
加 1 得到了 9 + 1 = 10。
因此,结果应该是 [1,0]。

说明:

  • 1 <= digits.length <= 100
  • 0 <= digits[i] <= 9
  • digits 不包含任何前导 0。

思路

数组 digits 表示一个数字,高位在前低位在后,不含前导零,将该数字加一后返回。

由于存在进位,数组长度可能增加 1 位。仔细想想,第一位是 1 后面全是 0

从后向前遍历,如数字不是 9,将数字加一直接返回,否则,将当前位置零,继续向前进位。如果遍历结束还没有返回,说明数组长度不够,新建数组,将第一位置 1 即可。

代码


/**
 * @date 2024-06-27 0:40
 */
public class PlusOne66 {

    public int[] plusOne(int[] digits) {
        int n = digits.length;
        for (int i = n - 1; i >= 0; i--) {
            int sum = digits[i] + 1;
            if (sum == 10) {
                digits[i] = 0;
            } else {
                digits[i] = sum;
                return digits;
            }
        }
        int[] res = new int[n + 1];
        res[0] = 1;
        return res;
    }

}

性能

1970.你能穿过矩阵的最后一天

目标

给你一个下标从 1 开始的二进制矩阵,其中 0 表示陆地,1 表示水域。同时给你 row 和 col 分别表示矩阵中行和列的数目。

一开始在第 0 天,整个 矩阵都是 陆地 。但每一天都会有一块新陆地被 水 淹没变成水域。给你一个下标从 1 开始的二维数组 cells ,其中 cells[i] = [ri, ci] 表示在第 i 天,第 ri 行 ci 列(下标都是从 1 开始)的陆地会变成 水域 (也就是 0 变成 1 )。

你想知道从矩阵最 上面 一行走到最 下面 一行,且只经过陆地格子的 最后一天 是哪一天。你可以从最上面一行的 任意 格子出发,到达最下面一行的 任意 格子。你只能沿着 四个 基本方向移动(也就是上下左右)。

请返回只经过陆地格子能从最 上面 一行走到最 下面 一行的 最后一天 。

示例 1:

输入:row = 2, col = 2, cells = [[1,1],[2,1],[1,2],[2,2]]
输出:2
解释:上图描述了矩阵从第 0 天开始是如何变化的。
可以从最上面一行到最下面一行的最后一天是第 2 天。

示例 2:

输入:row = 2, col = 2, cells = [[1,1],[1,2],[2,1],[2,2]]
输出:1
解释:上图描述了矩阵从第 0 天开始是如何变化的。
可以从最上面一行到最下面一行的最后一天是第 1 天。

示例 3:

输入:row = 3, col = 3, cells = [[1,2],[2,1],[3,3],[2,2],[1,1],[1,3],[2,3],[3,2],[3,1]]
输出:3
解释:上图描述了矩阵从第 0 天开始是如何变化的。
可以从最上面一行到最下面一行的最后一天是第 3 天。

说明:

  • 2 <= row, col <= 2 * 10^4
  • 4 <= row col <= 2 10^4
  • cells.length == row * col
  • 1 <= ri <= row
  • 1 <= ci <= col
  • cells 中的所有格子坐标都是 唯一 的。

思路

有一个二进制矩阵,0 代表陆地,1 代表水域。第 0 天,整个矩阵都是陆地,之后的每一天都会有一块陆地变为水域,cells[i] = [rowi, coli] 表示第 i + 1 天,矩阵的 rowi 行,coli 列会变为水域,注意行列的下标从 1 开始。返回能从第一行走到最后一行的最后一天是哪天。

BFS 可用于判断路径是否存在,二分答案判断即可。

代码


/**
 * @date 2025-12-31 9:56
 */
public class LatestDayToCross1970 {

    public int latestDayToCross(int row, int col, int[][] cells) {
        int l = 0, r = cells.length - 1;
        int m = l + (r - l) / 2;
        while (l <= r) {
            if (check(row, col, m, cells)) {
                l = m + 1;
            } else {
                r = m - 1;
            }
            m = l + (r - l) / 2;
        }
        return r;
    }

    public int[][] directions = new int[][]{{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

    public boolean check(int row, int col, int m, int[][] cells) {
        int[][] grid = new int[row + 1][col + 1];
        for (int i = 0; i < m; i++) {
            grid[cells[i][0]][cells[i][1]] = 1;
        }
        Deque<int[]> q = new ArrayDeque<>();
        for (int i = 1; i <= col; i++) {
            if (grid[1][i] == 0) {
                grid[1][i] = 1;
                q.offer(new int[]{1, i});
            }
        }
        while (!q.isEmpty()) {
            int size = q.size();
            for (int i = 0; i < size; i++) {
                int[] cur = q.poll();
                for (int[] direction : directions) {
                    int x = cur[0] + direction[0];
                    int y = cur[1] + direction[1];
                    if (x >= 1 && x <= row && y >= 1 && y <= col && grid[x][y] == 0) {
                        if (x == row) {
                            return true;
                        }
                        grid[x][y] = 1;
                        q.offer(new int[]{x, y});
                    }
                }
            }
        }
        return false;
    }

}

性能