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)。

721.账户合并

目标

给定一个列表 accounts,每个元素 accounts[i] 是一个字符串列表,其中第一个元素 accounts[i][0] 是 名称 (name),其余元素是 emails 表示该账户的邮箱地址。

现在,我们想合并这些账户。如果两个账户都有一些共同的邮箱地址,则两个账户必定属于同一个人。请注意,即使两个账户具有相同的名称,它们也可能属于不同的人,因为人们可能具有相同的名称。一个人最初可以拥有任意数量的账户,但其所有账户都具有相同的名称。

合并账户后,按以下格式返回账户:每个账户的第一个元素是名称,其余元素是 按字符 ASCII 顺序排列 的邮箱地址。账户本身可以以 任意顺序 返回。

示例 1:

输入:accounts = [["John", "johnsmith@mail.com", "john00@mail.com"], ["John", "johnnybravo@mail.com"], ["John", "johnsmith@mail.com", "john_newyork@mail.com"], ["Mary", "mary@mail.com"]]
输出:[["John", 'john00@mail.com', 'john_newyork@mail.com', 'johnsmith@mail.com'],  ["John", "johnnybravo@mail.com"], ["Mary", "mary@mail.com"]]
解释:
第一个和第三个 John 是同一个人,因为他们有共同的邮箱地址 "johnsmith@mail.com"。 
第二个 John 和 Mary 是不同的人,因为他们的邮箱地址没有被其他帐户使用。
可以以任何顺序返回这些列表,例如答案 [['Mary','mary@mail.com'],['John','johnnybravo@mail.com'],
['John','john00@mail.com','john_newyork@mail.com','johnsmith@mail.com']] 也是正确的。

示例 2:

输入:accounts = [["Gabe","Gabe0@m.co","Gabe3@m.co","Gabe1@m.co"],["Kevin","Kevin3@m.co","Kevin5@m.co","Kevin0@m.co"],["Ethan","Ethan5@m.co","Ethan4@m.co","Ethan0@m.co"],["Hanzo","Hanzo3@m.co","Hanzo1@m.co","Hanzo0@m.co"],["Fern","Fern5@m.co","Fern1@m.co","Fern0@m.co"]]
输出:[["Ethan","Ethan0@m.co","Ethan4@m.co","Ethan5@m.co"],["Gabe","Gabe0@m.co","Gabe1@m.co","Gabe3@m.co"],["Hanzo","Hanzo0@m.co","Hanzo1@m.co","Hanzo3@m.co"],["Kevin","Kevin0@m.co","Kevin3@m.co","Kevin5@m.co"],["Fern","Fern0@m.co","Fern1@m.co","Fern5@m.co"]]

说明:

  • 1 <= accounts.length <= 1000
  • 2 <= accounts[i].length <= 10
  • 1 <= accounts[i][j].length <= 30
  • accounts[i][0] 由英文字母组成
  • accounts[i][j] (for j > 0) 是有效的邮箱地址

思路

现有一个账号名称与邮箱列表组成的二维数组,如果两个账号对应的邮箱有重合,那么认为这两个账号属于同一个人,名称一定相同。但是名称相同不代表账户相同。现在需要将同一个人的账号合并,返回格式为,[名称,邮箱1,邮箱2,...],其中邮箱按 ASCII 排序。注意,同一个记录的邮箱列表中也可能存在相同邮箱,比如 ["Kevin","Kevin4@m.co","Kevin2@m.co","Kevin2@m.co"]

直接的想法是比较名称相同的账户邮箱是否有重合,如果有则合并。先将数据整理一下,换为 Map<name, List<List<Integer>>>,然后判断集合是否有共同元素,有则合并,没有则保留。那么使用什么方式处理集合呢?如果两两比较,时间复杂度为 O(n^2),好在一个账户邮箱最多 9 个,账户数量最多1000个,数据量不大。

如果两个集合有公共邮箱,那么可以使用 a.removeAll(b) 这个函数,它的返回值是布尔类型,如果a集合调用函数之后发生变化,即移除了a与b的公共元素,则返回 true,否则 false。因此当返回 true 时,直接与b合并,否则放回队列。需要注意的问题是,集合列表 {a,b} {c,d} {d,e} {e,f} {f,b} 将 第一个集合 {a,b} 与后面的集合依次两两比较时,直到最后一个才合并为{a,b,f},错过了与前面集合的合并,因此我们需要重新与前面的集合比较。

由于需要反复地比较这些集合,又要将属于同一账户的邮箱集合从集合列表中删除,涉及到集合的动态添加与删除。如果使用 ArrayList,尽管可以使用迭代器来动态添加与删除元素,但是从中间删除效率不高,需要移动数组元素。因此我们选择队列来保存这些集合,由于我们的操作主要在首尾两端,可以使用 ArrayDequeArrayDeque 双端操作效率比 LinkedList 更高,尽管它们都能在 O(1) 时间内完成操作,但是 LinkedList 需要额外的指针操作以及潜在的缓存不命中(不是连续分配的)问题,而 ArrayDeque 基于循环数组实现,只需调整头尾指针即可。

官网题解使用的是并查集,其实刚开始我也想到了使用并查集,但之前都是在图问题中用的,如果两个节点有边连接直接合并,但本题如何判断能否合并或者说是否连通呢?通常我们使用数组列表建图,但这里节点数据的类型不同,考虑使用map,key为邮箱,value为账户下标列表。遍历原二维数组,记录已合并的下标,如果邮箱对应有其它账户下标则进入dfs。

// todo 并查集

代码


/**
 * @date 2024-07-15 8:40
 */
public class AccountsMerge721 {
    public List<List<String>> accountsMerge_v1(List<List<String>> accounts) {
        int n = accounts.size();
        Map<String, List<Integer>> map = new HashMap<>();
        for (int i = 0; i < n; i++) {
            for (int j = 1; j < accounts.get(i).size(); j++) {
                map.computeIfAbsent(accounts.get(i).get(j), x -> new ArrayList<>()).add(i);
            }
        }

        boolean[] visited = new boolean[n];
        List<List<String>> res = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            if (visited[i]) {
                continue;
            }
            visited[i] = true;
            List<String> account = accounts.get(i);
            int size = account.size();
            Set<String> mailList = new HashSet<>(account.subList(1, size));
            for (int j = 1; j < size; j++) {
                String mail = accounts.get(i).get(j);
                for (Integer index : map.get(mail)) {
                    if (visited[index]) {
                        continue;
                    }
                    dfs(index, accounts, map, mailList, visited);
                }
            }
            ArrayList<String> item = new ArrayList<>(mailList);
            Collections.sort(item);
            item.add(0, accounts.get(i).get(0));
            res.add(item);
        }
        return res;
    }

    public void dfs(int index, List<List<String>> accounts, Map<String, List<Integer>> map, Set<String> mailList, boolean[] visited) {
        visited[index] = true;
        List<String> account = accounts.get(index);
        int size = account.size();
        mailList.addAll(account.subList(1, size));
        for (int j = 1; j < size; j++) {
            String mail = accounts.get(index).get(j);
            for (Integer next : map.get(mail)) {
                if (visited[next]) {
                    continue;
                }
                dfs(next, accounts, map, mailList, visited);
            }
        }
    }

    public List<List<String>> accountsMerge(List<List<String>> accounts) {
        Map<String, Queue<Set<String>>> map = new HashMap<>();
        for (List<String> account : accounts) {
            String name = account.get(0);
            map.putIfAbsent(name, new ArrayDeque<>());
            Queue<Set<String>> queue = map.get(name);
            Set<String> mails = new TreeSet<>();
            for (int i = 1; i < account.size(); i++) {
                mails.add(account.get(i));
            }
            queue.offer(mails);
        }
        List<List<String>> res = new ArrayList<>();
        for (Map.Entry<String, Queue<Set<String>>> entry : map.entrySet()) {
            Queue<Set<String>> queue = entry.getValue();
            List<Set<String>> merged = new ArrayList<>();
            while (!queue.isEmpty()) {
                Set<String> mails = queue.poll();
                int size = queue.size();
                int cnt = 0;
                for (int i = 0; i < size; i++) {
                    Set<String> m = queue.poll();
                    if (m.removeAll(mails)) {
                        // 存在问题,(a,b)(c,d)(d,e)(e,f)(f,b) 最后一个才合并(a,b,f),错过了与前面集合的合并
                        mails.addAll(m);
                        // 这里扩展了执行次数,与前面比较过的元素重新比较
                        size += cnt;
                        cnt = 0;
                    } else {
                        queue.add(m);
                        cnt++;
                    }
                }
                merged.add(mails);
            }

            for (Set<String> set : merged) {
                List<String> l = new ArrayList<>();
                l.add(entry.getKey());
                l.addAll(set);
                res.add(l);
            }
        }

        return res;
    }
}

性能

使用队列

使用dfs

139.单词拆分

目标

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。

示例 2:

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。注意,你可以重复使用字典中的单词。

示例 3:

输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

说明:

  • 1 <= s.length <= 300
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 20
  • s 和 wordDict[i] 仅由小写英文字母组成
  • wordDict 中的所有字符串 互不相同

思路

已知一个字符串列表 wordDict 和一个字符串 s,问能否用列表中的元素拼成该字符串,列表中的元素可以重复使用。

很明显需要使用动态规划来求解,假设当前列表元素 word 的长度为 l,子字符串 sub 的长度为 i,如果 sub.substring(0, i-l) 能由字典中的词拼成并且 word.equals(sub.substring(i-l, l)) 那么 sub 也能由字典中的词拼成。

代码

/**
 * @date 2024-06-23 19:58
 */
public class WordBreak139 {
    public boolean wordBreak(String s, List<String> wordDict) {
        int n = s.length();
        boolean[] dp = new boolean[n + 1];
        dp[0] = true;
        for (int i = 1; i <= n; i++) {
            for (String word : wordDict) {
                int length = word.length();
                if (length <= i && dp[i - length] && word.equals(s.substring(i - length, i))) {
                    dp[i] = true;
                }
            }
        }
        return dp[n];
    }

    public boolean wordBreak_v1(String s, List<String> wordDict) {
        int n = s.length();
        char[] mem = new char[n + 1];
        Arrays.fill(mem, '2');
        return dfs(s, 0, wordDict, mem) == '1';
    }

    public char dfs(String s, int i, List<String> wordDict, char[] mem) {
        int n = s.length();
        if (i == n) {
            return '1';
        }
        if (mem[i] != '2') {
            return mem[i];
        }
        for (String word : wordDict) {
            if (s.startsWith(word, i) && '1' == dfs(s, i + word.length(), wordDict, mem)) {
                return mem[i] = '1';
            }
        }
        return mem[i] = '0';
    }
}

性能

最快的解法是使用记忆化搜索,可以剪枝缩小搜索范围。