1863.找出所有子集的异或总和再求和

目标

一个数组的 异或总和 定义为数组中所有元素按位 XOR 的结果;如果数组为 空 ,则异或总和为 0 。

  • 例如,数组 [2,5,6] 的 异或总和 为 2 XOR 5 XOR 6 = 1 。

给你一个数组 nums ,请你求出 nums 中每个 子集 的 异或总和 ,计算并返回这些值相加之 和 。

注意:在本题中,元素 相同 的不同子集应 多次 计数。

数组 a 是数组 b 的一个 子集 的前提条件是:从 b 删除几个(也可能不删除)元素能够得到 a 。

示例 1:

输入:nums = [1,3]
输出:6
解释:[1,3] 共有 4 个子集:
- 空子集的异或总和是 0 。
- [1] 的异或总和为 1 。
- [3] 的异或总和为 3 。
- [1,3] 的异或总和为 1 XOR 3 = 2 。
0 + 1 + 3 + 2 = 6

示例 2:

输入:nums = [5,1,6]
输出:28
解释:[5,1,6] 共有 8 个子集:
- 空子集的异或总和是 0 。
- [5] 的异或总和为 5 。
- [1] 的异或总和为 1 。
- [6] 的异或总和为 6 。
- [5,1] 的异或总和为 5 XOR 1 = 4 。
- [5,6] 的异或总和为 5 XOR 6 = 3 。
- [1,6] 的异或总和为 1 XOR 6 = 7 。
- [5,1,6] 的异或总和为 5 XOR 1 XOR 6 = 2 。
0 + 5 + 1 + 6 + 4 + 3 + 7 + 2 = 28

示例 3:

输入:nums = [3,4,5,6,7,8]
输出:480
解释:每个子集的全部异或总和值之和为 480 。

说明:

  • 1 <= nums.length <= 12
  • 1 <= nums[i] <= 20

思路

计算数组所有子序列的异或和之和。

dfs 枚举所有子序列。

代码


/**
 * @date 2025-04-05 19:47
 */
public class SubsetXORSum1863 {

    int res;

    public int subsetXORSum(int[] nums) {
        dfs(0, 0, nums);
        return res;
    }

    public void dfs(int index, int xor, int[] nums) {
        int n = nums.length;
        if (index == n) {
            res += xor;
            return;
        }
        dfs(index + 1, xor, nums);
        dfs(index + 1, xor ^ nums[index], nums);
    }

}

性能

1123.最深叶节点的最近公共祖先

目标

给你一个有根节点 root 的二叉树,返回它 最深的叶节点的最近公共祖先 。

回想一下:

  • 叶节点 是二叉树中没有子节点的节点
  • 树的根节点的 深度 为 0,如果某一节点的深度为 d,那它的子节点的深度就是 d+1
  • 如果我们假定 A 是一组节点 S 的 最近公共祖先,S 中的每个节点都在以 A 为根节点的子树中,且 A 的深度达到此条件下可能的最大值。

示例 1:

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

示例 2:

输入:root = [1]
输出:[1]
解释:根节点是树中最深的节点,它是它本身的最近公共祖先。

示例 3:

输入:root = [0,1,3,null,2]
输出:[2]
解释:树中最深的叶节点是 2 ,最近公共祖先是它自己。

说明:

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

思路

找到二叉树最深的叶节点并返回它们的最近公共祖先。

dfs 记录最大深度,返回子树深度,如果左右子树深度相同且等于最大深度则更新返回节点。

代码


/**
 * @date 2025-04-04 19:09
 */
public class LcaDeepestLeaves1123 {

    TreeNode res;
    int maxDepth;

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

    public int dfs(TreeNode node, int depth) {
        if (node == null) {
            return depth;
        }
        int leftDepth = dfs(node.left, depth + 1);
        int rightDepth = dfs(node.right, depth + 1);
        int d = Math.max(leftDepth, rightDepth);
        maxDepth = Math.max(maxDepth, d);
        if (rightDepth == maxDepth && leftDepth == maxDepth) {
            res = node;
        }
        return d;
    }
}

性能

1367.二叉树中的链表

目标

给你一棵以 root 为根的二叉树和一个 head 为第一个节点的链表。

如果在二叉树中,存在一条一直向下的路径,且每个点的数值恰好一一对应以 head 为首的链表中每个节点的值,那么请你返回 True ,否则返回 False 。

一直向下的路径的意思是:从树中某个节点开始,一直连续向下的路径。

示例 1:

输入:head = [4,2,8], root = [1,4,4,null,2,2,null,1,null,6,8,null,null,null,null,1,3]
输出:true
解释:树中蓝色的节点构成了与链表对应的子路径。

示例 2:

输入:head = [1,4,2,6], root = [1,4,4,null,2,2,null,1,null,6,8,null,null,null,null,1,3]
输出:true

示例 3:

输入:head = [1,4,2,6,8], root = [1,4,4,null,2,2,null,1,null,6,8,null,null,null,null,1,3]
输出:false
解释:二叉树中不存在一一对应链表的路径。

说明:

  • 二叉树和链表中的每个节点的值都满足 1 <= node.val <= 100 。
  • 链表包含的节点数目在 1 到 100 之间。
  • 二叉树包含的节点数目在 1 到 2500 之间。

思路

判断二叉树中是否存在给定的路径 head,路径以链表的形式给出。

dfs 判断是否存在相同的路径。这道题的难点在于没有要求起点为 root,因此需要枚举所有节点为起点的情况。针对每一个起点我们最多检查 100 次,总共大概 2.5 * 10^5 次。

代码


/**
 * @date 2024-12-30 8:56
 */
public class IsSubPath1367 {

    public ListNode head;

    public boolean isSubPath(ListNode head, TreeNode root) {
        this.head = head;
        return dfs(root, head);
    }

    public boolean dfs(TreeNode cur, ListNode node) {
        if (node == null) {
            return true;
        }
        if (cur == null) {
            return false;
        }
        return cur.val == node.val && (dfs(cur.left, node.next) || dfs(cur.right, node.next))
                || node == head && (dfs(cur.left, head) || dfs(cur.right, head));
    }

}

性能

2056.棋盘上有效移动组合的数目

目标

有一个 8 x 8 的棋盘,它包含 n 个棋子(棋子包括车,后和象三种)。给你一个长度为 n 的字符串数组 pieces ,其中 pieces[i] 表示第 i 个棋子的类型(车,后或象)。除此以外,还给你一个长度为 n 的二维整数数组 positions ,其中 positions[i] = [ri, ci] 表示第 i 个棋子现在在棋盘上的位置为 (ri, ci) ,棋盘下标从 1 开始。

棋盘上每个棋子都可以移动 至多一次 。每个棋子的移动中,首先选择移动的 方向 ,然后选择 移动的步数 ,同时你要确保移动过程中棋子不能移到棋盘以外的地方。棋子需按照以下规则移动:

  • 车可以 水平或者竖直 从 (r, c) 沿着方向 (r+1, c),(r-1, c),(r, c+1) 或者 (r, c-1) 移动。
  • 后可以 水平竖直或者斜对角 从 (r, c) 沿着方向 (r+1, c),(r-1, c),(r, c+1),(r, c-1),(r+1, c+1),(r+1, c-1),(r-1, c+1),(r-1, c-1) 移动。
  • 象可以 斜对角 从 (r, c) 沿着方向 (r+1, c+1),(r+1, c-1),(r-1, c+1),(r-1, c-1) 移动。

移动组合 包含所有棋子的 移动 。每一秒,每个棋子都沿着它们选择的方向往前移动 一步 ,直到它们到达目标位置。所有棋子从时刻 0 开始移动。如果在某个时刻,两个或者更多棋子占据了同一个格子,那么这个移动组合 不有效 。

请你返回 有效 移动组合的数目。

注意:

  • 初始时,不会有两个棋子 在 同一个位置。
  • 有可能在一个移动组合中,有棋子不移动。
  • 如果两个棋子 直接相邻 且两个棋子下一秒要互相占据对方的位置,可以将它们在同一秒内 交换位置。

示例 1:

输入:pieces = ["rook"], positions = [[1,1]]
输出:15
解释:上图展示了棋子所有可能的移动。

示例 2:

输入:pieces = ["queen"], positions = [[1,1]]
输出:22
解释:上图展示了棋子所有可能的移动。

示例 3:

输入:pieces = ["bishop"], positions = [[4,3]]
输出:12
解释:上图展示了棋子所有可能的移动。

示例 4:

输入:pieces = ["rook","rook"], positions = [[1,1],[8,8]]
输出:223
解释:每个车有 15 种移动,所以总共有 15 * 15 = 225 种移动组合。
但是,有两个是不有效的移动组合:
- 将两个车都移动到 (8, 1) ,会导致它们在同一个格子相遇。
- 将两个车都移动到 (1, 8) ,会导致它们在同一个格子相遇。
所以,总共有 225 - 2 = 223 种有效移动组合。
注意,有两种有效的移动组合,分别是一个车在 (1, 8) ,另一个车在 (8, 1) 。
即使棋盘状态是相同的,这两个移动组合被视为不同的,因为每个棋子移动操作是不相同的。

示例 5:

输入:pieces = ["queen","bishop"], positions = [[5,7],[3,4]]
输出:281
解释:总共有 12 * 24 = 288 种移动组合。
但是,有一些不有效的移动组合:
- 如果后停在 (6, 7) ,它会阻挡象到达 (6, 7) 或者 (7, 8) 。
- 如果后停在 (5, 6) ,它会阻挡象到达 (5, 6) ,(6, 7) 或者 (7, 8) 。
- 如果象停在 (5, 2) ,它会阻挡后到达 (5, 2) 或者 (5, 1) 。
在 288 个移动组合当中,281 个是有效的。

说明:

  • n == pieces.length
  • n == positions.length
  • 1 <= n <= 4
  • pieces 只包含字符串 "rook" ,"queen" 和 "bishop" 。
  • 棋盘上最多只有一个后。
  • 1 <= ri, ci <= 8
  • 每一个 positions[i] 互不相同。

思路

有一个 8 x 8 的棋盘,行列编号从 1 ~ 8。棋盘上有 n 个棋子,pieces[i] 表示棋子 i 的类型,rook 表示车,queen 表示皇后,bishop 表示象,positions[i] = [ri, ci] 表示棋子 i 所在行编号为 ri,所在列编号为 ci。棋盘上的每个棋子都可以移动 至多 一步,不同的棋子移动方式不同,参考国际象棋。车走竖线或横线,象走斜线,皇后走竖线、横线或斜线,不能跳过其它棋子。

特别注意,我们求的是 有效移动 组合,重点在 移动 而不是最终状态,比如示例4。

棋盘大小固定,考虑车移动或者不移动,它最多有 15 种移动的可能,包括不移动或者移动到相应方向上的其它格子共 1 + 2 * 7 种。由于棋盘是偶数没有中间格子,所以象有 8 + 6 种。同理皇后可能的移动方式最多有 28 种,包括不移动或者移动相应方向上的其它格子 1 + 2 * 7 + 7 + 6 种,如果它位于一条 8 格的对角线上,除去自身还剩 7 格,它所在的另一对角线剩余 6 个格子。

题目描述里面非常让人迷惑的一点是既允许相邻棋子同时交换位置,而示例5又说棋子会阻挡其它棋子通行。这个题的难点在于如果按照棋盘状态去枚举的话,当两个棋子相遇的时候不知道是否应该穿越,如果穿越的话就会移动另一个棋子,如何去重?我们必须将移动的步数考虑进去,如果两个棋子相遇时都没有到达目标位置那么它们可以穿过。否则,某个棋子提前停下,另一个棋子行进到相同的位置发生碰撞,不符合题目条件。

参考题解思路写出来了。

代码


/**
 * @date 2024-12-04 14:24
 */
public class CountCombinations2056 {

    public static class Move {
        public int x;
        public int y;
        public int dx;
        public int dy;
        public int step;

        public Move(int x, int y, int dx, int dy, int step) {
            this.x = x;
            this.y = y;
            this.dx = dx;
            this.dy = dy;
            this.step = step;
        }
    }

    public int[][] rookDir = new int[][]{{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    public int[][] bishopDir = new int[][]{{-1, -1}, {1, 1}, {1, -1}, {-1, 1}};
    public int[][] queenDir = new int[][]{{-1, 0}, {1, 0}, {0, -1}, {0, 1}, {-1, -1}, {1, 1}, {1, -1}, {-1, 1}};

    /**
     * 预处理每个棋子可行的移动方案、起点、方向、步数
     */
    public int countCombinations(String[] pieces, int[][] positions) {
        int n = pieces.length;
        Move[][] moves = new Move[n][];
        for (int i = 0; i < n; i++) {
            moves[i] = generateAllValidMove(pieces[i], positions[i]);
        }
        Move[] curMove = new Move[n];
        return dfs(0, n, curMove, moves);
    }

    /**
     * 回溯,先从棋子0中选一个可行的移动方案,然后进入下一层,
     * 从棋子1中选一个可行的移动方案,判断当前方案与前面所选的移动方案是否存在某一时刻使得两个棋子占据同一个格子
     * 如果不存在就进入下一层,否则枚举另一个方案。
     * i 表示枚举第 i 个棋子的合法移动方案
     * curMove 表示前面棋子选择的移动方案,用于下一层比较移动方案是否合法
     * moves 表示所有棋子的移动方案
     */
    public int dfs(int i, int n, Move[] curMove, Move[][] moves) {
        if (i == n) {
            return 1;
        }
        int cnt = 0;
        here:
        for (Move move : moves[i]) {
            for (int j = 0; j < i; j++) {
                if (!valid(move, curMove[j])) {
                    continue here;
                }
            }
            curMove[i] = move;
            cnt += dfs(i + 1, n, curMove, moves);
        }
        return cnt;
    }

    public boolean valid(Move move, Move curMove) {
        int x1 = move.x;
        int y1 = move.y;
        int x2 = curMove.x;
        int y2 = curMove.y;
        // 步数小的会提前停下来,如果它们坐标相同说明存在在某一时刻使得两个棋子移动到同一个位置上,不合法
        for (int i = 0; i < Math.max(move.step, curMove.step); i++) {
            if (i < move.step) {
                x1 += move.dx;
                y1 += move.dy;
            }
            if (i < curMove.step) {
                x2 += curMove.dx;
                y2 += curMove.dy;
            }
            if (x1 == x2 && y1 == y2) {
                return false;
            }
        }
        return true;
    }

    /**
     * 生成该棋子所有可能的移动方案
     * 起点,方向,步数
     */
    public Move[] generateAllValidMove(String pieces, int[] positions) {
        int[][] directions = null;
        switch (pieces) {
            case "rook":
                directions = rookDir;
                break;
            case "bishop":
                directions = bishopDir;
                break;
            case "queen":
                directions = queenDir;
                break;
            default:
        }
        if (directions == null) {
            return null;
        }
        List<Move> moves = new ArrayList<>();
        int x = positions[0];
        int y = positions[1];
        // 将不移动的情况加入集合
        moves.add(new Move(x, y, 0, 0, 0));
        // 遍历该棋子允许的行进方向,
        for (int[] dir : directions) {
            int dx = dir[0];
            int dy = dir[1];
            int step = 0;
            x = positions[0] + dx;
            y = positions[1] + dy;
            while (x > 0 && x <= 8 && y > 0 && y <= 8) {
                moves.add(new Move(positions[0], positions[1], dx, dy, ++step));
                x += dx;
                y += dy;
            }
        }
        return moves.toArray(new Move[0]);
    }

}

性能

3249.统计好节点的数目

目标

现有一棵 无向 树,树中包含 n 个节点,按从 0 到 n - 1 标记。树的根节点是节点 0 。给你一个长度为 n - 1 的二维整数数组 edges,其中 edges[i] = [ai, bi] 表示树中节点 ai 与节点 bi 之间存在一条边。

如果一个节点的所有子节点为根的 子树 包含的节点数相同,则认为该节点是一个 好节点。

返回给定树中 好节点 的数量。

子树 指的是一个节点以及它所有后代节点构成的一棵树。

示例 1:

输入:edges = [[0,1],[0,2],[1,3],[1,4],[2,5],[2,6]]
输出:7
说明:
树的所有节点都是好节点。

示例 2:

输入:edges = [[0,1],[1,2],[2,3],[3,4],[0,5],[1,6],[2,7],[3,8]]
输出:6
说明:
树中有 6 个好节点。上图中已将这些节点着色。

示例 3:

输入:edges = [[0,1],[1,2],[1,3],[1,4],[0,5],[5,6],[6,7],[7,8],[0,9],[9,10],[9,12],[10,11]]
输出:12
解释:
除了节点 9 以外其他所有节点都是好节点。

说明:

  • 2 <= n <= 10^5
  • edges.length == n - 1
  • edges[i].length == 2
  • 0 <= ai, bi < n
  • 输入确保 edges 总表示一棵有效的树。

思路

树中的任一节点,如果以它的子节点为根的子树包含相同的节点数量,则称该节点为好节点。注意没有要求子节点是好节点,只统计子树整体的节点个数。求给定树的好节点个数。

dfs 获取子树节点数目,判断各子树的节点个数是否相同。叶子节点没有子树,可认为子树节点个数为 0 也是好节点。

代码


/**
 * @date 2024-11-14 9:32
 */
public class CountGoodNodes3249 {
    int res = 0;
    List<Integer>[] g;

    public int countGoodNodes(int[][] edges) {
        g = new List[edges.length + 1];
        int n = g.length;
        for (int i = 0; i < n; i++) {
            g[i] = new ArrayList<>();
        }
        for (int i = 0; i < edges.length; i++) {
            g[edges[i][0]].add(edges[i][1]);
            g[edges[i][1]].add(edges[i][0]);
        }
        dfs(-1, 0);
        return res;
    }

    public int dfs(int parent, int cur) {
        int num = 1;
        int prev = -1;
        boolean equal = true;
        for (Integer next : g[cur]) {
            if (next == parent) {
                continue;
            }
            int childNum = dfs(cur, next);
            if (prev != -1 && prev != childNum) {
                equal = false;
            }
            prev = childNum;
            num += childNum;
        }
        if (equal) {
            res++;
        }
        return num;
    }

}

性能

3235.判断矩形的两个角落是否可达

目标

给你两个正整数 xCorner 和 yCorner 和一个二维整数数组 circles ,其中 circles[i] = [xi, yi, ri] 表示一个圆心在 (xi, yi) 半径为 ri 的圆。

坐标平面内有一个左下角在原点,右上角在 (xCorner, yCorner) 的矩形。你需要判断是否存在一条从左下角到右上角的路径满足:路径 完全 在矩形内部,不会 触碰或者经过 任何 圆的内部和边界,同时 只 在起点和终点接触到矩形。

如果存在这样的路径,请你返回 true ,否则返回 false 。

示例 1:

输入:X = 3, Y = 4, circles = [[2,1,1]]
输出:true
解释:
黑色曲线表示一条从 (0, 0) 到 (3, 4) 的路径。

示例 2:

输入:X = 3, Y = 3, circles = [[1,1,2]]
输出:false
解释:
不存在从 (0, 0) 到 (3, 3) 的路径。

示例 3:

输入:X = 3, Y = 3, circles = [[2,1,1],[1,2,1]]
输出:false
解释:
不存在从 (0, 0) 到 (3, 3) 的路径。

示例 4:

输入:X = 4, Y = 4, circles = [[5,5,1]]
输出:true
解释:

说明:

  • 3 <= xCorner, yCorner <= 10^9
  • 1 <= circles.length <= 1000
  • circles[i].length == 3
  • 1 <= xi, yi, ri <= 10^9

思路

有一个以原点为左下顶点, [xCorner, yCorner] 为右上顶点的矩形,还有一些圆 circlescircles[i, j, r] 表示圆的圆心在 (i, j) 半径为 r。问是否存在一条从原点到 [xCorner, yCorner] 的路径,满足路径在矩形内部(不与矩形边界重合),且不触碰或经过任何园的内部与边界。

评论说这是史上分数最高的题目,周赛全球也没几个人做出来,直接放弃了。

代码

性能

690.员工的重要性

目标

你有一个保存员工信息的数据结构,它包含了员工唯一的 id ,重要度和直系下属的 id 。

给定一个员工数组 employees,其中:

  • employees[i].id 是第 i 个员工的 ID。
  • employees[i].importance 是第 i 个员工的重要度。
  • employees[i].subordinates 是第 i 名员工的直接下属的 ID 列表。

给定一个整数 id 表示一个员工的 ID,返回这个员工和他所有下属的重要度的 总和。

示例 1:

输入:employees = [[1,5,[2,3]],[2,3,[]],[3,3,[]]], id = 1
输出:11
解释:员工 1 自身的重要度是 5 ,他有两个直系下属 2 和 3 ,而且 2 和 3 的重要度均为 3 。因此员工 1 的总重要度是 5 + 3 + 3 = 11 。

示例 2:

输入:employees = [[1,2,[5]],[5,-3,[]]], id = 5
输出:-3
解释:员工 5 的重要度为 -3 并且没有直接下属。因此,员工 5 的总重要度为 -3。

说明:

  • 1 <= employees.length <= 2000
  • 1 <= employees[i].id <= 2000
  • 所有的 employees[i].id 互不相同。
  • -100 <= employees[i].importance <= 100
  • 一名员工最多有一名直接领导,并可能有多名下属。
  • employees[i].subordinates 中的 ID 都有效。

思路

有一个数据结构 Employee,属性有 id、重要性、下属id集合。现在要查找给定id员工的重要性,即自身及下属的重要性之和。

直接使用map映射id与员工对象,使用dfs或者bfs搜索并累加重要性即可。

代码


/**
 * @date 2024-08-26 14:59
 */
public class GetImportance690 {
    class Employee {
        public int id;
        public int importance;
        public List<Integer> subordinates;
    }

    public int getImportance(List<Employee> employees, int id) {
        Employee root = null;
        Map<Integer, Employee> map = new HashMap<>();
        for (Employee employee : employees) {
            map.put(employee.id, employee);
        }
        int res = 0;
        Queue<Employee> q = new ArrayDeque<>(employees.size());
        q.offer(map.get(id));
        while (!q.isEmpty()) {
            int size = q.size();
            for (int i = 0; i < size; i++) {
                Employee node = q.poll();
                res += node.importance;
                for (Integer subordinate : node.subordinates) {
                    q.offer(map.get(subordinate));
                }
            }
        }
        return res;
    }
}

性能

600.不含连续1的非负整数

目标

给定一个正整数 n ,请你统计在 [0, n] 范围的非负整数中,有多少个整数的二进制表示中不存在 连续的 1 。

示例 1:

输入: n = 5
输出: 5
解释: 
下面列出范围在 [0, 5] 的非负整数与其对应的二进制表示:
0 : 0
1 : 1
2 : 10
3 : 11
4 : 100
5 : 101
其中,只有整数 3 违反规则(有两个连续的 1 ),其他 5 个满足规则。

示例 2:

输入: n = 1
输出: 2

示例 3:

输入: n = 2
输出: 3

说明:

  • 1 <= n <= 10^9

思路

给定一个正整数n,求 [0, n] 范围内整数的二进制表示中不含连续1的整数个数。

由于n最大10^9,如果挨个判断整数是否含连续的bit 1,实际复杂度为O(31n),超时。

既然暴力解不行只能考虑其它方法。分析n的二进制表示,将问题转换为一定限制条件下的排列组合问题。求得二进制表示之后可以使用dfs来计算组合数。如果不使用记忆化搜索同样会超时,这里使用状态压缩来记录重复的子问题。状态指方法参数的组合,如果cur为1,则将高位置1与index相与,第二个维度0表示不可以将0改为1,1表示可以将0改为1。

官网题解使用的是递推。

代码

/**
 * @date 2024-08-05 10:20
 */
public class FindIntegers600 {

    public int findIntegers(int n) {
        List<Integer> bitmap = new ArrayList<>(32);
        while (n > 0) {
            bitmap.add(n & 1);
            n >>= 1;
        }
        int[][] mem = new int[(1 << 5) | (bitmap.size() - 1)][2];
        return dfs(0, bitmap, bitmap.size() - 1, false, mem);
    }

    public int dfs(int pre, List<Integer> bitmap, int index, boolean zeroToOne, int[][] mem) {
        int cur = bitmap.get(index);
        if (index == 0) {
            return pre == 0 && (cur == 1 || zeroToOne) ? 2 : 1;
        }
        int res = 0;
        index--;
        int size = bitmap.size();
        int key = (1 << 5) | index;
        int zto = zeroToOne ? 1 : 0;
        if (pre == 1 && cur == 1) {
            // 如果前一个是1,当前也是1,将1改为0,允许后续的0改为1
            if (mem[index][1] == 0) {
                mem[index][1] = dfs(cur - 1, bitmap, index, true, mem);
            }
            res = mem[index][1];
        } else if (pre == 0 && cur == 1) {
            // 如果前一个是0,当前是1,将1改为0,允许后续的0改为1,或者当前不变,后续是否允许0变1取决于zeroToOne
            if (mem[index][1] == 0) {
                mem[index][1] = dfs(cur - 1, bitmap, index, true, mem);
            }
            if (mem[key][zto] == 0) {
                mem[key][zto] = dfs(cur, bitmap, index, zeroToOne, mem);
            }
            res = mem[index][1] + mem[key][zto];
        } else if (pre == 0 && cur == 0) {
            // 如果前一个是0,当前是0,当前不变,后续是否允0变1许取决于zeroToOne,如果当前zeroToOne为true,将0改为1
            if (mem[index][zto] == 0) {
                mem[index][zto] = dfs(cur, bitmap, index, zeroToOne, mem);
            }
            res = mem[index][zto];
            if (zeroToOne) {
                if (mem[key][zto] == 0) {
                    mem[key][zto] = dfs(cur + 1, bitmap, index, zeroToOne, mem);
                }
                res += mem[key][zto];
            }
        } else {
            // 如果前一个是1,当前是0,当前不变,后续是否允许0变1取决于zeroToOne
            if (mem[index][zto] == 0) {
                mem[index][zto] = dfs(cur, bitmap, index, zeroToOne, mem);
            }
            res = mem[index][zto];
        }
        return res;
    }

}

性能

572.另一棵树的子树

目标

给你两棵二叉树 root 和 subRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false 。

二叉树 tree 的一棵子树包括 tree 的某个节点和这个节点的所有后代节点。tree 也可以看做它自身的一棵子树。

示例 1:

输入:root = [3,4,5,1,2], subRoot = [4,1,2]
输出:true

示例 2:

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

说明:

  • root 树上的节点数量范围是 [1, 2000]
  • subRoot 树上的节点数量范围是 [1, 1000]
  • -10^4 <= root.val <= 10^4
  • -10^4 <= subRoot.val <= 10^4

思路

判断给定的树是否是另一颗树的子树。

刚开始想以某种顺序(先/中/后)遍历给定的树并记录访问节点,然后遍历另一棵树,如果节点值等于给定树的根节点则以同样的顺序记录其子树,再比较记录是否相同。但是这并不能保证结构是相同的。

只能逐个节点比较。进一步优化可以计算树的高度,只比较高度相同的。//todo

看了官网题解可以引入两个null值,进行串匹配的时候可以使用 KMP 或者 Rabin-Karp 算法。//todo

也提供了将树映射为hash值来比较的思路。//todo

代码

/**
 * @date 2024-08-04 15:36
 */
public class IsSubtree572 {

    public boolean isSubtree(TreeNode root, TreeNode subRoot) {
        if (root == null) {
            return false;
        }
        if (root.val == subRoot.val) {
            if (compare(root, subRoot)) {
                return true;
            }
        }
        return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot);
    }

    public boolean compare(TreeNode root, TreeNode subRoot) {
        if (root.val != subRoot.val) {
            return false;
        }
        boolean res = false;
        if (root.left != null && subRoot.left != null) {
            res = compare(root.left, subRoot.left);
        } else if (root.left == null && subRoot.left == null){
            res = true;
        }
        if (root.right != null && subRoot.right != null) {
            res = compare(root.right, subRoot.right) && res;
        } else if (root.right == null && subRoot.right == null) {
        } else {
            res = false;
        }
        return res;
    }

}

性能

2101.引爆最多的炸弹

目标

给你一个炸弹列表。一个炸弹的 爆炸范围 定义为以炸弹为圆心的一个圆。

炸弹用一个下标从 0 开始的二维整数数组 bombs 表示,其中 bombs[i] = [xi, yi, ri] 。xi 和 yi 表示第 i 个炸弹的 X 和 Y 坐标,ri 表示爆炸范围的 半径 。

你需要选择引爆 一个 炸弹。当这个炸弹被引爆时,所有 在它爆炸范围内的炸弹都会被引爆,这些炸弹会进一步将它们爆炸范围内的其他炸弹引爆。

给你数组 bombs ,请你返回在引爆 一个 炸弹的前提下,最多 能引爆的炸弹数目。

示例 1:

输入:bombs = [[2,1,3],[6,1,4]]
输出:2
解释:
上图展示了 2 个炸弹的位置和爆炸范围。
如果我们引爆左边的炸弹,右边的炸弹不会被影响。
但如果我们引爆右边的炸弹,两个炸弹都会爆炸。
所以最多能引爆的炸弹数目是 max(1, 2) = 2 。

示例 2:

输入:bombs = [[1,1,5],[10,10,5]]
输出:1
解释:
引爆任意一个炸弹都不会引爆另一个炸弹。所以最多能引爆的炸弹数目为 1 。

示例 3:

输入:bombs = [[1,2,3],[2,3,1],[3,4,2],[4,5,3],[5,6,4]]
输出:5
解释:
最佳引爆炸弹为炸弹 0 ,因为:
- 炸弹 0 引爆炸弹 1 和 2 。红色圆表示炸弹 0 的爆炸范围。
- 炸弹 2 引爆炸弹 3 。蓝色圆表示炸弹 2 的爆炸范围。
- 炸弹 3 引爆炸弹 4 。绿色圆表示炸弹 3 的爆炸范围。
所以总共有 5 个炸弹被引爆。

说明:

  • 1 <= bombs.length <= 100
  • bombs[i].length == 3
  • 1 <= xi, yi, ri <= 10^5

思路

坐标平面上有一些炸弹,并且已知炸弹的爆炸范围。现在可以选择引爆其中的一颗炸弹,被引爆炸弹的爆炸范围内的其它炸弹也会被引爆,问最多可以引爆的炸弹数量。

我们可以将问题转换为有向图,一枚炸弹能够波及到的其它炸弹认为是连通的,然后遍历每一枚炸弹,求出连通炸弹数量最多的个数即可。

那么如何建立这个有向图呢?固定一个,依次与其余的节点比较,时间复杂度为O(n^2),炸弹最多100个,应该可行。

实现过程中需要判断爆炸范围内的是否存在其它炸弹,可以使用炸弹坐标(圆心)之间的距离与各自的引爆半径相比较。这里需要注意防止数据溢出,另外还有一个小技巧,比较 sqrt(dx^2 + dy^2) 与 半径 r 的效率没有 dx^2 + dy^2r^2 高。

网友最快的题解使用的是 Floyd 算法。// todo

代码

/**
 * @date 2024-07-22 9:24
 */
public class MaximumDetonation2101 {

    public int maximumDetonation_v1(int[][] bombs) {
        int n = bombs.length;
        List<Integer>[] g = new List[n];
        for (int i = 0; i < n; i++) {
            g[i] = new ArrayList<>();
        }
        for (int i = 0; i < n; i++) {
            int ix = bombs[i][0];
            int iy = bombs[i][1];
            long ir = bombs[i][2];
            for (int j = i + 1; j < n; j++) {
                int jx = bombs[j][0];
                int jy = bombs[j][1];
                long jr = bombs[j][2];
                // 防止溢出
                long diffx = ix - jx;
                long diffy = iy - jy;
                long dist = diffx * diffx + diffy * diffy;
                if (ir * ir >= dist) {
                    g[i].add(j);
                }
                if (jr * jr >= dist) {
                    g[j].add(i);
                }
            }
        }
        int res = 0;
        for (int i = 0; i < n; i++) {
            boolean[] visited = new boolean[n];
            res = Math.max(res, dfs(i, g, visited));
        }
        return res;
    }

    int dfs(int root, List<Integer>[] g, boolean[] visited) {
        visited[root] = true;
        int res = 1;
        for (Integer next : g[root]) {
            if (visited[next]) {
                continue;
            }
            res += dfs(next, g, visited);
        }
        return res;
    }

}

性能

图中至多有 n^2 条边,dfs的时间复杂度是O(n^2),然后再遍历n个起点,时间复杂度为O(n^3)。