1022.从根到叶的二进制数之和

目标

给出一棵二叉树,其上每个结点的值都是 0 或 1 。每一条从根到叶的路径都代表一个从最高有效位开始的二进制数。

  • 例如,如果路径为 0 -> 1 -> 1 -> 0 -> 1,那么它表示二进制数 01101,也就是 13 。

对树上的每一片叶子,我们都要找出从根到该叶子的路径所表示的数字。

返回这些数字之和。题目数据保证答案是一个 32 位 整数。

示例 1:

输入:root = [1,0,1,0,1,0,1]
输出:22
解释:(100) + (101) + (110) + (111) = 4 + 5 + 6 + 7 = 22

示例 2:

输入:root = [0]
输出:0

说明:

  • 树中的节点数在 [1, 1000] 范围内
  • Node.val 仅为 0 或 1

思路

有一颗二叉树,节点值是 0 或 1,从根到叶子路径上节点值排列成一个从高有效位开始的二进制数,求所有路径表示的二进制数之和。

从根开始 dfs 将之前路径的二进制数左移与当前值相与,如果是叶子节点则累加结果。

代码


/**
 * @date 2026-02-24 10:57
 */
public class SumRootToLeaf1022 {

    public int sumRootToLeaf(TreeNode root) {
        dfs(root, 0);
        return res;
    }

    public int res = 0;

    public void dfs(TreeNode node, int sum) {
        sum = (sum << 1) | node.val;
        if (node.left != null) {
            dfs(node.left, sum);
        }
        if (node.right != null) {
            dfs(node.right, sum);
        }
        if (node.left == null && node.right == null) {
            res += sum;
        }
    }

}

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 * int val;
 * TreeNode left;
 * TreeNode right;
 * TreeNode() {}
 * TreeNode(int val) { this.val = val; }
 * TreeNode(int val, TreeNode left, TreeNode right) {
 * this.val = val;
 * this.left = left;
 * this.right = right;
 * }
 * }
 */

性能

401.二进制手表

目标

二进制手表顶部有 4 个 LED 代表 小时(0-11),底部的 6 个 LED 代表 分钟(0-59)。每个 LED 代表一个 0 或 1,最低位在右侧。

  • 例如,下面的二进制手表读取 "4:51" 。

给你一个整数 turnedOn ,表示当前亮着的 LED 的数量,返回二进制手表可以表示的所有可能时间。你可以 按任意顺序 返回答案。

小时不会以零开头:

  • 例如,"01:00" 是无效的时间,正确的写法应该是 "1:00" 。

分钟必须由两位数组成,可能会以零开头:

  • 例如,"10:2" 是无效的时间,正确的写法应该是 "10:02" 。

示例 1:

输入:turnedOn = 1
输出:["0:01","0:02","0:04","0:08","0:16","0:32","1:00","2:00","4:00","8:00"]

示例 2:

输入:turnedOn = 9
输出:[]

说明:

  • 0 <= turnedOn <= 10

思路

有一个二进制手表,用 4 bit 表示小时, 6 bit 代表分钟。已知 bit 位为 1 的数量 turnedOn,返回所有可能的时间。

将数字按照置位数量分组,枚举不同的划分即可。

代码


/**
 * @date 2026-02-24 17:34
 */
public class ReadBinaryWatch401 {

    public List<String> readBinaryWatch(int turnedOn) {
        Map<Integer, List<Integer>> map = new HashMap<>();
        for (int i = 0; i < 60; i++) {
            int bc = Integer.bitCount(i);
            map.putIfAbsent(bc, new ArrayList<>());
            map.get(bc).add(i);
        }
        List<String> res = new ArrayList<>();
        for (int i = 0; i < 4; i++) {
            List<Integer> hours = map.get(i);
            for (Integer h : hours) {
                if (h > 11) {
                    break;
                }
                List<Integer> minutes = map.get(turnedOn - i);
                if (minutes == null) {
                    continue;
                }
                for (Integer m : minutes) {
                    res.add(h + ":" + ((m < 10) ? "0" : "") + m);
                }
            }
        }
        return res;
    }
}

性能

110.平衡二叉树

目标

给定一个二叉树,判断它是否是 平衡二叉树 。平衡二叉树 是指该树所有节点的左右子树的高度相差不超过 1。

示例 1:

输入:root = [3,9,20,null,null,15,7]
输出:true

示例 2:

输入:root = [1,2,2,3,3,null,null,4,4]
输出:false

示例 3:

输入:root = []
输出:true

说明:

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

思路

判断给定二叉树是否是平衡二叉树。

dfs 返回子树高度,判断高度差是否超过 1。

代码


/**
 * @date 2026-02-09 11:35
 */
public class IsBalanced110 {

    boolean res = true;

    public boolean isBalanced(TreeNode root) {
        dfs(root);
        return res;
    }

    public int dfs(TreeNode node) {
        if (node == null) {
            return 0;
        }
        int lh = dfs(node.left);
        int rh = dfs(node.right);
        if (Math.abs(lh - rh) > 1) {
            res = false;
        }
        return Math.max(lh, rh) + 1;
    }

}

性能

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;
    }

}

性能

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;
    }

}

性能

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;
    }

}

性能

756.金字塔转换矩阵

目标

你正在把积木堆成金字塔。每个块都有一个颜色,用一个字母表示。每一行的块比它下面的行 少一个块 ,并且居中。

为了使金字塔美观,只有特定的 三角形图案 是允许的。一个三角形的图案由 两个块 和叠在上面的 单个块 组成。模式是以三个字母字符串的列表形式 allowed 给出的,其中模式的前两个字符分别表示左右底部块,第三个字符表示顶部块。

  • 例如,"ABC" 表示一个三角形图案,其中一个 “C” 块堆叠在一个 'A' 块(左)和一个 'B' 块(右)之上。请注意,这与 "BAC" 不同,"B" 在左下角,"A" 在右下角。

你从作为单个字符串给出的底部的一排积木 bottom 开始,必须 将其作为金字塔的底部。

在给定 bottom 和 allowed 的情况下,如果你能一直构建到金字塔顶部,使金字塔中的 每个三角形图案 都是在 allowed 中的,则返回 true ,否则返回 false 。

示例 1:

输入:bottom = "BCD", allowed = ["BCC","CDE","CEA","FFF"]
输出:true
解释:允许的三角形图案显示在右边。
从最底层(第 3 层)开始,我们可以在第 2 层构建“CE”,然后在第 1 层构建“E”。
金字塔中有三种三角形图案,分别是 “BCC”、“CDE” 和 “CEA”。都是允许的。

示例 2:

输入:bottom = "AAAA", allowed = ["AAB","AAC","BCD","BBE","DEF"]
输出:false
解释:允许的三角形图案显示在右边。
从最底层(即第 4 层)开始,创造第 3 层有多种方法,但如果尝试所有可能性,你便会在创造第 1 层前陷入困境。

说明:

  • 2 <= bottom.length <= 6
  • 0 <= allowed.length <= 216
  • allowed[i].length == 3
  • 所有输入字符串中的字母来自集合 {'A', 'B', 'C', 'D', 'E', 'F'}。
  • allowed 中所有值都是 唯一的

思路

有一排积木 bottom,将其作为金字塔的底部,又有一个模式列表 allowed,每个模式是一个长度为 3 的字符串 pattern,表示 pattern[2] 堆叠在左侧的 pattern[0] 和右侧的 pattern[1] 之上。判断能否使用给定的模式堆成一个金字塔。

使用哈希表保存 pattern 下面两个与上面一个的对应关系,dfs 暴力枚举所有可能,分为两个维度,枚举当前层所有可能的排列,生成下一层所有可能的排列;枚举特定的上一层排列生成对应可能的排列。

代码


/**
 * @date 2025-12-29 9:23
 */
public class PyramidTransition756 {

    public boolean pyramidTransition(String bottom, List<String> allowed) {
        int n = bottom.length();
        Map<String, Set<Character>> map = new HashMap<>();
        for (String s : allowed) {
            String key = s.substring(0, 2);
            map.putIfAbsent(key, new HashSet<>());
            map.get(key).add(s.charAt(2));
        }
        Set<String> candidates = new HashSet<>();
        candidates.add(bottom);
        Set<String> top = dfs(1, n, map, candidates);
        return top.size() > 0;
    }

    public Set<String> dfs(int l, int n, Map<String, Set<Character>> map, Set<String> candidates) {
        if (l == n) {
            return candidates;
        }
        Set<String> next = new HashSet<>();
        for (String prev : candidates) {
            Set<String> tmp = new HashSet<>();
            dfs(1, prev, "", map, tmp);
            next.addAll(tmp);
        }
        return dfs(l + 1, n, map, next);
    }

    public void dfs(int index, String prev, String row, Map<String, Set<Character>> map, Set<String> set) {
        if (index == prev.length()) {
            if (!"".equals(row)) {
                set.add(row);
            }
            return;
        }
        String key = prev.charAt(index - 1) + String.valueOf(prev.charAt(index));
        Set<Character> characters = map.get(key);
        if (characters != null) {
            for (Character c : characters) {
                dfs(index + 1, prev, row + c, map, set);
            }
        }
    }

}

性能

2872.可以被K整除连通块的最大数目

目标

给你一棵 n 个节点的无向树,节点编号为 0 到 n - 1 。给你整数 n 和一个长度为 n - 1 的二维整数数组 edges ,其中 edges[i] = [ai, bi] 表示树中节点 ai 和 bi 有一条边。

同时给你一个下标从 0 开始长度为 n 的整数数组 values ,其中 values[i] 是第 i 个节点的 值 。再给你一个整数 k 。

你可以从树中删除一些边,也可以一条边也不删,得到若干连通块。一个 连通块的值 定义为连通块中所有节点值之和。如果所有连通块的值都可以被 k 整除,那么我们说这是一个 合法分割 。

请你返回所有合法分割中,连通块数目的最大值 。

示例 1:

输入:n = 5, edges = [[0,2],[1,2],[1,3],[2,4]], values = [1,8,1,4,4], k = 6
输出:2
解释:我们删除节点 1 和 2 之间的边。这是一个合法分割,因为:
- 节点 1 和 3 所在连通块的值为 values[1] + values[3] = 12 。
- 节点 0 ,2 和 4 所在连通块的值为 values[0] + values[2] + values[4] = 6 。
最多可以得到 2 个连通块的合法分割。

示例 2:

输入:n = 7, edges = [[0,1],[0,2],[1,3],[1,4],[2,5],[2,6]], values = [3,0,6,1,5,2,1], k = 3
输出:3
解释:我们删除节点 0 和 2 ,以及节点 0 和 1 之间的边。这是一个合法分割,因为:
- 节点 0 的连通块的值为 values[0] = 3 。
- 节点 2 ,5 和 6 所在连通块的值为 values[2] + values[5] + values[6] = 9 。
- 节点 1 ,3 和 4 的连通块的值为 values[1] + values[3] + values[4] = 6 。
最多可以得到 3 个连通块的合法分割。

说明:

  • 1 <= n <= 3 * 10^4
  • edges.length == n - 1
  • edges[i].length == 2
  • 0 <= ai, bi < n
  • values.length == n
  • 0 <= values[i] <= 10^9
  • 1 <= k <= 10^9
  • values 之和可以被 k 整除。
  • 输入保证 edges 是一棵无向树。

思路

有一个节点数量为 n 的无向树,树中节点之和可以被 k 整除,现在需要对树进行划分,要求每一个连通块中的节点和也能够被 k 整除,求最大的连通块个数。

dfs 遍历树,如果子树节点和能够整除 k 则可以与父节点断开,删除边数加 1,最终连通分量个数是删除的边数加 1

代码


/**
 * @date 2025-11-28 9:31
 */
public class MaxKDivisibleComponents2872 {

    int res = 0;

    public int maxKDivisibleComponents(int n, int[][] edges, int[] values, int k) {
        List<Integer>[] g = new ArrayList[n];
        Arrays.setAll(g, x -> new ArrayList<>());
        for (int[] edge : edges) {
            g[edge[0]].add(edge[1]);
            g[edge[1]].add(edge[0]);
        }
        dfs(0, -1, g, values, k);
        return res + 1;
    }

    public int dfs(int i, int fa, List<Integer>[] g, int[] values, int k) {
        int sum = values[i];
        for (Integer next : g[i]) {
            if (next == fa) {
                continue;
            }
            int subSum = dfs(next, i, g, values, k);
            if (subSum % k == 0) {
                res++;
            } else {
                sum = (sum + subSum) % k;
            }
        }
        return sum % k;
    }

}

性能

1780.判断一个数字是否可以表示成三的幂的和

目标

给你一个整数 n ,如果你可以将 n 表示成若干个 不同的 三的幂之和,请你返回 true ,否则请返回 false 。

对于一个整数 y ,如果存在整数 x 满足 y == 3x ,我们称这个整数 y 是三的幂。

示例 1:

输入:n = 12
输出:true
解释:12 = 31 + 32

示例 2:

输入:n = 91
输出:true
解释:91 = 30 + 32 + 34

示例 3:

输入:n = 21
输出:false

提示:

  • 1 <= n <= 10^7

思路

判断一个整数 n 能否用若干个 不同的 3 的幂之和表示。

直接的想法是使用 dfs 从小到大判断 3 的幂选或者不选。

从数学的角度考虑,如果 n 的三进制表示中包含 2 那么就无法用不同的三的幂之和表示。

代码


/**
 * @date 2025-08-14 9:54
 */
public class CheckPowersOfThree1780 {

    class Solution {
        public boolean checkPowersOfThree(int n) {
            while (n > 0) {
                if (n % 3 == 2) {
                    return false;
                }
                n /= 3;
            }
            return true;
        }
    }

    public boolean checkPowersOfThree(int n) {
        return dfs(1, 0, n);
    }

    public boolean dfs(int d, int num, int n) {
        if (num == n) {
            return true;
        } else if (num > n || d > n) {
            return false;
        }
        return dfs(d * 3, num, n) || dfs(d * 3, num + d, n);
    }

}

性能