3243.新增道路查询后的最短距离I

目标

给你一个整数 n 和一个二维整数数组 queries。

有 n 个城市,编号从 0 到 n - 1。初始时,每个城市 i 都有一条单向道路通往城市 i + 1( 0 <= i < n - 1)。

queries[i] = [ui, vi] 表示新建一条从城市 ui 到城市 vi 的单向道路。每次查询后,你需要找到从城市 0 到城市 n - 1 的最短路径的长度。

返回一个数组 answer,对于范围 [0, queries.length - 1] 中的每个 i,answer[i] 是处理完前 i + 1 个查询后,从城市 0 到城市 n - 1 的最短路径的长度。

示例 1:

输入: n = 5, queries = [[2, 4], [0, 2], [0, 4]]
输出: [3, 2, 1]
解释:
新增一条从 2 到 4 的道路后,从 0 到 4 的最短路径长度为 3。
新增一条从 0 到 2 的道路后,从 0 到 4 的最短路径长度为 2。
新增一条从 0 到 4 的道路后,从 0 到 4 的最短路径长度为 1。

示例 2:

输入: n = 4, queries = [[0, 3], [0, 2]]
输出: [1, 1]
解释:
新增一条从 0 到 3 的道路后,从 0 到 3 的最短路径长度为 1。
新增一条从 0 到 2 的道路后,从 0 到 3 的最短路径长度仍为 1。

说明:

  • 3 <= n <= 500
  • 1 <= queries.length <= 500
  • queries[i].length == 2
  • 0 <= queries[i][0] < queries[i][1] < n
  • 1 < queries[i][1] - queries[i][0]
  • 查询中没有重复的道路。

思路

n 个城市,刚开始每一个城市 i 有一条单项道路通向城市 i + 1,有一个二维数组 queriesqueries[i] 表示增加一条从 queries[i][0]queries[i][1] 的单项道路,返回 answer 数组,answer[i] 表示增加了 queries[i] 之后从城市 0 到城市 n - 1 的最短路径。

建图,每次增加路径之后使用BFS计算最短路径。注意过滤重复元素,否则过不了。

也可以使用动态规划来做。

定义 dp[i] 表示从城市 i 到城市 n - 1 的最短路径,每添加一条路径,受影响的只有包括起点在内前面的城市。当增加一条道路 [l, r],状态转移方程为 dp[l] = Math.min(dp[r] + 1, dp[l]),表示从 r 到达终点加一步 与 原来值的最小值。我们需要同步更新 l 之前的 dp 值。我们可以倒序遍历 [0, l)dp[j] = Math.min(dp[j], dp[j + 1] + 1) 取其自身与 后面元素 dp 值加一的较小值,同时还要考虑区间内有前面查询新增的道路,比如前面有以 j 为起点的查询,还要再取一个较小值 Math.min(dp[j], dp[end] + 1)end 表示之前增加的道路的终点。

代码


/**
 * @date 2024-11-19 0:47
 */
public class ShortestDistanceAfterQueries3243 {

    public int[] shortestDistanceAfterQueries_v2(int n, int[][] queries) {
        int[] dp = new int[n];
        int ql = queries.length;
        int[] res = new int[ql];
        for (int i = 0; i < n; i++) {
            dp[i] = n - i - 1;
        }
        Map<Integer, List<Integer>> map = new HashMap<>(ql);
        for (int i = 0; i < ql; i++) {
            int l = queries[i][0];
            int r = queries[i][1];
            dp[l] = Math.min(dp[r] + 1, dp[l]);
            for (int j = l - 1; j >= 0; j--) {
                dp[j] = Math.min(dp[j], dp[j + 1] + 1);
                if (map.containsKey(j)) {
                    for (Integer end : map.get(j)) {
                        dp[j] = Math.min(dp[j], dp[end] + 1);
                    }
                }
            }
            res[i] = dp[0];
            map.putIfAbsent(l, new ArrayList<>());
            map.get(l).add(r);
        }
        return res;
    }

    public int[] shortestDistanceAfterQueries_v1(int n, int[][] queries) {
        List<Integer>[] g = new ArrayList[n];
        for (int i = 0; i < g.length; i++) {
            g[i] = new ArrayList<>();
            g[i].add(i + 1);
        }
        int ql = queries.length;
        int[] res = new int[ql];
        for (int i = 0; i < ql; i++) {
            g[queries[i][0]].add(queries[i][1]);
            res[i] = bfs_v1(g);
        }
        return res;
    }

    public int bfs_v1(List<Integer>[] g) {
        int n = g.length;
        List<Integer> list = new ArrayList<>();
        boolean[] visited = new boolean[n];
        list.add(0);
        for (int res = 1; ; res++) {
            List<Integer> tmp = list;
            int size = tmp.size();
            list = new ArrayList<>();
            for (int i = 0; i < size; i++) {
                Integer cur = tmp.get(i);
                for (Integer next : g[cur]) {
                    if (next == n - 1) {
                        return res;
                    }
                    if (!visited[next]) {
                        visited[next] = true;
                        list.add(next);
                    }
                }
            }
        }
    }

    public int[] shortestDistanceAfterQueries(int n, int[][] queries) {
        List<Integer>[] g = new ArrayList[n];
        for (int i = 0; i < g.length; i++) {
            g[i] = new ArrayList<>();
            g[i].add(i + 1);
        }
        int ql = queries.length;
        int[] res = new int[ql];
        for (int i = 0; i < ql; i++) {
            g[queries[i][0]].add(queries[i][1]);
            res[i] = bfs(g);
        }
        return res;
    }

    public int bfs(List<Integer>[] g) {
        int res = 0, n = g.length;
        Queue<Integer> q = new ArrayDeque<>();
        q.offer(0);
        here:
        while (!q.isEmpty()) {
            int size = q.size();
            Set<Integer> set = new HashSet<>();
            for (int i = 0; i < size; i++) {
                int cur = q.poll();
                if (cur == n - 1) {
                    break here;
                }
                set.addAll(g[cur]);
            }
            q.addAll(set);
            res++;
        }
        return res;
    }

}

性能

815.公交路线[中秋快乐]

目标

给你一个数组 routes ,表示一系列公交线路,其中每个 routes[i] 表示一条公交线路,第 i 辆公交车将会在上面循环行驶。

  • 例如,路线 routes[0] = [1, 5, 7] 表示第 0 辆公交车会一直按序列 1 -> 5 -> 7 -> 1 -> 5 -> 7 -> 1 -> ... 这样的车站路线行驶。

现在从 source 车站出发(初始时不在公交车上),要前往 target 车站。 期间仅可乘坐公交车。

求出 最少乘坐的公交车数量 。如果不可能到达终点车站,返回 -1 。

示例 1:

输入:routes = [[1,2,7],[3,6,7]], source = 1, target = 6
输出:2
解释:最优策略是先乘坐第一辆公交车到达车站 7 , 然后换乘第二辆公交车到车站 6 。

示例 2:

输入:routes = [[7,12],[4,5,15],[6],[15,19],[9,12,13]], source = 15, target = 12
输出:-1

说明:

  • 1 <= routes.length <= 500.
  • 1 <= routes[i].length <= 10^5
  • routes[i] 中的所有值 互不相同
  • sum(routes[i].length) <= 10^5
  • 0 <= routes[i][j] < 10^6
  • 0 <= source, target < 10^6

思路

求不带权的最短路径直接使用BFS即可,但这题求的是换乘次数最少的路径。我们可以将问题转化为带权的最短路径问题,如果需要换乘,将权重设为1,否则权重为0。

// todo

代码

性能

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

性能

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

2850.将石头分散到网格图的最少移动次数

目标

给你一个大小为 3 * 3 ,下标从 0 开始的二维整数矩阵 grid ,分别表示每一个格子里石头的数目。网格图中总共恰好有 9 个石头,一个格子里可能会有 多个 石头。

每一次操作中,你可以将一个石头从它当前所在格子移动到一个至少有一条公共边的相邻格子。

请你返回每个格子恰好有一个石头的 最少移动次数 。

示例 1:

输入:grid = [[1,1,0],[1,1,1],[1,2,1]]
输出:3
解释:让每个格子都有一个石头的一个操作序列为:
1 - 将一个石头从格子 (2,1) 移动到 (2,2) 。
2 - 将一个石头从格子 (2,2) 移动到 (1,2) 。
3 - 将一个石头从格子 (1,2) 移动到 (0,2) 。
总共需要 3 次操作让每个格子都有一个石头。
让每个格子都有一个石头的最少操作次数为 3 。

示例 2:

输入:grid = [[1,3,0],[1,0,0],[1,0,3]]
输出:4
解释:让每个格子都有一个石头的一个操作序列为:
1 - 将一个石头从格子 (0,1) 移动到 (0,2) 。
2 - 将一个石头从格子 (0,1) 移动到 (1,1) 。
3 - 将一个石头从格子 (2,2) 移动到 (1,2) 。
4 - 将一个石头从格子 (2,2) 移动到 (2,1) 。
总共需要 4 次操作让每个格子都有一个石头。
让每个格子都有一个石头的最少操作次数为 4 。

说明:

  • grid.length == grid[i].length == 3
  • 0 <= grid[i][j] <= 9
  • grid 中元素之和为 9 。

思路

有一个3 * 3 的二维矩阵,有9个石头散落在其中,每次可以将石头移到相邻的格子里,问每个格子一块石头最少需要移动几次。

有多余石头的格子到没有石头格子移动的次数为其曼哈顿距离要想使移动次数最小,我们只需要从没有石头的格子向四个方向查找有多余石头的格子即可

并非是沿四个方向搜索,而是BFS找最短路径。 遍历四个方向,那么只能沿着该方向查找,而BFS则是由内层向外层查找,体会二者的不同。但这题使用BFS也无法保证得到的是最小移动次数,考虑下面的情况:

从0开始取最近的并不能保证得到最优解,比如下面这种情况:

3,2,0      3,1,1      2,1,1      2,1,1      2,1,1      1,1,1
0,1,0  ->  0,1,0  ->  1,1,0  ->  1,1,1  ->  1,1,1  ->  1,1,1
0,3,0      0,3,0      0,3,0      0,2,0      1,1,0      1,1,1
       1          1          2           1          4
左下角的应该从第一个元素取:

3,2,0      3,1,1      2,1,1      2,1,1      1,1,1      1,1,1
0,1,0  ->  0,1,0  ->  1,1,0  ->  1,1,1  ->  1,1,1  ->  1,1,1
0,3,0      0,3,0      0,3,0      0,2,0      1,2,0      1,1,1
       1          1          2           2          1

尽管这题使用BFS求解不了,但还是有一些收获的。BFS很容易错写成每次从队列取一个元素,然后判断该元素是否满足条件,不满足就将其邻接节点加入队列。当需要进行层次计数的时候就不对了,应该在每次循环的第一步记录队列中元素个数 k,本次处理中就循环判断这k个元素,在循环过程中判断是否满足条件,不满足的将其邻接节点加入队列,因为我们已经在前面计数了,因此这些邻接节点将在下一次循环中处理。

如果取最近的多余石头这种贪心策略不行的话,那么问题就不在于最短路径了。而应从整体上考虑从哪里移动到哪里才是最优的,可以尝试记忆化搜索解空间。我们可以很容易枚举出哪些格子没有石头,哪些格子石头多于1个,只需枚举它们的组合并取其曼哈顿距离之和最小值即可。

这里的核心问题是如何遍历这两个列表的组合,我想到的方法就是使用回溯算法,每向下递归一层就标记为已访问,而返回时再取消其标记。并且如果不保存重复子问题的话,执行会超时。这里的重复子问题是两组数据未访问元素相同,而已访问数据的组合不同。例如: [a,b,c,d,e,f,g] [h,i,j,k,l,m,n] 前面两个元素组合 (a, h) (b, i)(a, i) (b, h) 剩余的元素的组合情况完全相同。

最终使用状态压缩与回溯解出来了。如果不记录重复的子问题的话,dfs方法要调用3705927296次,而使用记忆化搜索只需调用12868次。

官网题解也是类似的思路,只不过遍历组合的方式不同,它是固定一个列表不变,另一个进行全排列。//todo 有空再研究一下官网题解吧

代码

/**
 * @date 2024-07-20 15:55
 */
public class MinimumMoves2850 {

    public int minimumMoves_v2(int[][] grid) {
        List<int[]> zeros = new ArrayList<>();
        List<int[]> more = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (grid[i][j] == 0) {
                    zeros.add(new int[]{i, j});
                } else if (grid[i][j] > 1) {
                    for (int k = 0; k < grid[i][j] - 1; k++) {
                        more.add(new int[]{i, j});
                    }
                }
            }
        }
        int k = zeros.size();
        int res = Integer.MAX_VALUE;
        int[][] mem = new int[255][255];

        for (int i = 0; i < k; i++) {
            // 状态压缩
            int zerosVisited = 0x000000ff;
            zerosVisited ^= 1 << i;
            int[] zero = zeros.get(i);
            for (int j = 0; j < k; j++) {
                int moreVisited = 0x000000ff;
                moreVisited ^= 1 << j;
                int[] m = more.get(j);
                int distance = Math.abs(zero[0] - m[0]) + Math.abs(zero[1] - m[1]);
                res = Math.min(res, distance + dfs_v2(zeros, more, zerosVisited, moreVisited, 1, mem));
            }
        }
        return res;
    }

    public int dfs_v2(List<int[]> zeros, List<int[]> more, int zerosVisited, int moreVisited, int level, int[][] mem) {
        if (level == zeros.size()) {
            return 0;
        }
        int k = zeros.size();
        int res = Integer.MAX_VALUE;
        for (int i = 0; i < k; i++) {
            if (((zerosVisited >> i) & 1) == 0) {
                continue;
            }
            zerosVisited ^= 1 << i;
            int[] zero = zeros.get(i);
            for (int j = 0; j < k; j++) {
                if (((moreVisited >> j) & 1) == 0) {
                    continue;
                }
                moreVisited ^= 1 << j;
                int[] m = more.get(j);
                int distance = Math.abs(zero[0] - m[0]) + Math.abs(zero[1] - m[1]);
                if (mem[zerosVisited][moreVisited] == 0) {
                    // 重复的子问题是两边剩余的元素均相同
                    mem[zerosVisited][moreVisited] = dfs_v2(zeros, more, zerosVisited, moreVisited, level + 1, mem);
                }
                res = Math.min(res, distance + mem[zerosVisited][moreVisited]);
                // 回溯
                moreVisited ^= 1 << j;
            }
            zerosVisited ^= 1 << i;
        }
        return res;
    }

}

性能