743.网络延迟时间

目标

有 n 个网络节点,标记为 1 到 n。

给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。

现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。

示例 1:

输入:times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2
输出:2

示例 2:

输入:times = [[1,2,1]], n = 2, k = 1
输出:1

示例 3:

输入:times = [[1,2,1]], n = 2, k = 2
输出:-1

说明:

  • 1 <= k <= n <= 100
  • 1 <= times.length <= 6000
  • times[i].length == 3
  • 1 <= ui, vi <= n
  • ui != vi
  • 0 <= wi <= 100
  • 所有 (ui, vi) 对都 互不相同(即,不含重复边)

思路

有一个 n 个节点的有向图 ,节点标记为 1 ~ n,求从其中某个节点 k 出发访问到所有其它节点的最短时间。

即从 k 出发求出到达所有其它节点的最短路径,然后取其中的最 值。

Floyd 算法的基本思想是动态规划。定义 dp[i][j] 表示从节点 i 到 节点 j 的最短路径,对于所有其它中间节点 m,更新 dp[i][j] = Math.min(dp[i][j], dp[i][m] + dp[m][j]),时间复杂度 O(n^3)。

如果 i -> j 有直接的通路则初始化 dp[i][j] 为路径的权值,否则为 INF

但是本题不需要其它起点的最短路径,因此可以使用 Dijkstra 算法、Bellman-Ford 算法 或者 SPFA 算法。

图的表示可以使用邻接矩阵、邻接表、前向星、链式前向星等结构。

代码


/**
 * @date 2024-11-25 9:08
 */
public class NetworkDelayTime743 {

    public int networkDelayTime(int[][] times, int n, int k) {
        int[][] dp = new int[n + 1][n + 1];
        for (int[] cost : dp) {
            Arrays.fill(cost, 20000);
        }
        for (int[] edge : times) {
            dp[edge[0]][edge[1]] = edge[2];
        }
        for (int m = 1; m <= n; m++) {
            for (int i = 1; i <= n; i++) {
                for (int j = 1; j <= n; j++) {
                    if (i == j || i == m || j == m) {
                        continue;
                    }
                    dp[i][j] = Math.min(dp[i][j], dp[i][m] + dp[m][j]);
                }
            }
        }
        int res = -1;
        for (int i = 1; i <= n; i++) {
            if (i == k) {
                continue;
            }
            res = Math.max(dp[k][i], res);
        }
        return res == 20000 ? -1 : res;
    }

}

性能

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

}

性能

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

3112.访问消失节点的最少时间

目标

给你一个二维数组 edges 表示一个 n 个点的无向图,其中 edges[i] = [ui, vi, lengthi] 表示节点 ui 和节点 vi 之间有一条需要 lengthi 单位时间通过的无向边。

同时给你一个数组 disappear ,其中 disappear[i] 表示节点 i 从图中消失的时间点,在那一刻及以后,你无法再访问这个节点。

注意,图有可能一开始是不连通的,两个节点之间也可能有多条边。

请你返回数组 answer ,answer[i] 表示从节点 0 到节点 i 需要的 最少 单位时间。如果从节点 0 出发 无法 到达节点 i ,那么 answer[i] 为 -1 。

示例 1:

输入:n = 3, edges = [[0,1,2],[1,2,1],[0,2,4]], disappear = [1,1,5]
输出:[0,-1,4]
解释:
我们从节点 0 出发,目的是用最少的时间在其他节点消失之前到达它们。
对于节点 0 ,我们不需要任何时间,因为它就是我们的起点。
对于节点 1 ,我们需要至少 2 单位时间,通过 edges[0] 到达。但当我们到达的时候,它已经消失了,所以我们无法到达它。
对于节点 2 ,我们需要至少 4 单位时间,通过 edges[2] 到达。

示例 2:

输入:n = 3, edges = [[0,1,2],[1,2,1],[0,2,4]], disappear = [1,3,5]
输出:[0,2,3]
解释:
我们从节点 0 出发,目的是用最少的时间在其他节点消失之前到达它们。
对于节点 0 ,我们不需要任何时间,因为它就是我们的起点。
对于节点 1 ,我们需要至少 2 单位时间,通过 edges[0] 到达。
对于节点 2 ,我们需要至少 3 单位时间,通过 edges[0] 和 edges[1] 到达。

示例 3:

输入:n = 2, edges = [[0,1,1]], disappear = [1,1]
输出:[0,-1]
解释:
当我们到达节点 1 的时候,它恰好消失,所以我们无法到达节点 1 。

说明:

  • 1 <= n <= 5 * 10^4
  • 0 <= edges.length <= 10^5
  • edges[i] == [ui, vi, lengthi]
  • 0 <= ui, vi <= n - 1
  • 1 <= lengthi <= 10^5
  • disappear.length == n
  • 1 <= disappear[i] <= 10^5

思路

有一个n节点的带权无向图,权值表示经过该路径需要的时间。还有一个含有n个元素的数组,表示节点存在时间,即节点在该时间之后消失。让我们返回从0节点到达每个节点的最少时间,如到达时节点刚好消失则认为无法到达,返回-1。两节点之间可能有多条边,并且允许自己到自己的路径

直接的想法是使用迪杰斯特拉算法求出到达各节点的最少时间,然后与节点消失时间比较。但是最短路径可能随着节点消失而变得不可达,因此需要在遍历的时候判断节点是否消失。

又重新手写了一遍,纠正了以前的误区:

  • 对于距离dis初始化为INF的判断,认为INF+cost可能溢出。这是完全没有必要的,当前节点的dis一定已经更新过了。
  • 容易写成dfs,每次都取当前节点邻居中最小的。但这样求得的可能不是最短路径。关于最短路径问题:
    • 不带权,bfs
    • 非负权,dijkstra
    • 有负权,bellman-ford
    • 多源(寻找图中所有顶点对之间最短路径)Floyd, 属于动态规划算法
  • dijkstra 适用于DAG,对于无向图,需要避免环,或者向反方向查找。dijkstra类似于bfs,不过是取所有已访问节点的相邻节点中最小的,对于已处理的节点没有前往该节点更短的路径。
  • dijkstra 算法的核心在于其选择下一个扩展顶点的策略和路径长度的累计方式,这与动态规划直接填充整个解决方案空间的策略有所不同。
  • dijkstra 算法不依赖于特定的数据结构来选择最小节点,而是依赖算法本身的逻辑,不使用堆优化的实现依然可以得到正确的结果,只不过进行了不必要的计算。

代码

/**
 * @date 2024-07-18 0:20
 */
public class MinimumTime3112 {

    public int[] minimumTime(int n, int[][] edges, int[] disappear) {
        List<int[]>[] g = new ArrayList[n];
        for (int[] edge : edges) {
            if (g[edge[0]] == null) {
                g[edge[0]] = new ArrayList<>();
            }
            if (g[edge[1]] == null) {
                g[edge[1]] = new ArrayList<>();
            }
            g[edge[0]].add(new int[]{edge[1], edge[2]});
            g[edge[1]].add(new int[]{edge[0], edge[2]});
        }
        int[] dis = new int[n];
        dis[0] = 0;
        for (int i = 1; i < n; i++) {
            dis[i] = Integer.MAX_VALUE;
        }
        PriorityQueue<int[]> q = new PriorityQueue<>((a, b) -> a[1] - b[1]);
        q.offer(new int[]{0, 0});
        while (!q.isEmpty()) {
            int[] node = q.poll();
            int cur = node[0];
            if (g[cur] == null || node[1] > dis[cur]){
                continue;
            }
            for (int[] item : g[cur]) {
                int next = item[0];
                if (next == cur){
                    continue;
                }
                int cost = item[1];
                int totalCost = dis[cur] + cost;
                if (dis[next] <= totalCost || (cur != 0 && disappear[cur] <= dis[cur])) {
                    continue;
                }
                if (totalCost < disappear[next]) {
                    dis[next] = totalCost;
                    q.offer(new int[]{next, totalCost});
                }
            }
        }
        for (int i = 0; i < n; i++) {
            if (dis[i] >= disappear[i]) {
                dis[i] = -1;
            }
        }
        return dis;
    }
}

性能

将PriorityQueue改为LinkedList,不使用堆优化的反而更快,这就是测试用例的问题了。

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

928.尽量减少恶意软件的传播II

目标

给定一个由 n 个节点组成的网络,用 n x n 个邻接矩阵 graph 表示。在节点网络中,只有当 graph[i][j] = 1 时,节点 i 能够直接连接到另一个节点 j。

一些节点 initial 最初被恶意软件感染。只要两个节点直接连接,且其中至少一个节点受到恶意软件的感染,那么两个节点都将被恶意软件感染。这种恶意软件的传播将继续,直到没有更多的节点可以被这种方式感染。

假设 M(initial) 是在恶意软件停止传播之后,整个网络中感染恶意软件的最终节点数。

我们可以从 initial 中删除一个节点,并完全移除该节点以及从该节点到任何其他节点的任何连接。

请返回移除后能够使 M(initial) 最小化的节点。如果有多个节点满足条件,返回索引 最小的节点 。

示例 1:

输入:graph = [[1,1,0],[1,1,0],[0,0,1]], initial = [0,1]
输出:0

示例 2:

输入:graph = [[1,1,0],[1,1,1],[0,1,1]], initial = [0,1]
输出:1

示例 3:

输入:graph = [[1,1,0,0],[1,1,1,0],[0,1,1,1],[0,0,1,1]], initial = [0,1]
输出:1

说明:

  • n == graph.length
  • n == graph[i].length
  • 2 <= n <= 300
  • graph[i][j] 是 0 或 1.
  • graph[i][j] == graph[j][i]
  • graph[i][i] == 1
  • 1 <= initial.length < n
  • 0 <= initial[i] <= n - 1
  • initial 中每个整数都不同

思路

这个和昨天的题的区别是移除节点之后原来连通的区域可能就断开了。刚开始想,昨天的需要排除掉同一连通区域存在多个感染节点的情况,今天这个就不能排除了。但是其影响的节点数也不能通过连通区域节点个数来计算。处理起来就比较复杂了,不能简单地根据直接相连的节点数来判断。当连通区域中含有多个感染节点时,需要区分边缘感染节点与中间感染节点,边缘感染节点又与孤立的感染节点相同,都是减少1。然后还要考虑连通区域仅有一个感染节点的情况。

区分 单一边缘感染节点 与 孤立感染节点

2、3 感染,返回3

0 - 1
|
3

2

无需区分 多个边缘感染节点 与 孤立感染节点

1、2、3 感染,返回1

0 - 1
|
3

2

区分 中间感染节点 与 孤立感染节点,并且不能仅根据直接相连的非感染节点来判断

0 、2、4、8 感染,返回8

     7
     | \
0    4 - 6
     |
1 -  8 - 3
     |
2    5

错了好多次,终于调试过了。正面确实不太好求解。总结一下就是:

  1. 连通区域存在多个感染节点
    • 去掉边缘的感染节点,感染节点总数减少1,全是边缘感染节点与包含中间感染节点是一样的
    • 去掉非边缘感染节点,需要dfs获取不含感染节点路径的节点总数
  2. 连通区域仅有1个感染节点(可以是孤立感染节点、边缘节点、中间节点)
    • 感染节点总数减少连通区域节点个数

最终答案需要获取以上减少最多的节点,如果存在多个,返回下标最小的。

代码里是按边缘感染节点与中间感染节点分的:

  1. 边缘感染节点
    • 孤立感染节点,减1
    • 连通区域内有多个边缘感染节点,减1
    • 连通区域内仅有一个边缘感染节点,减连通区域节点个数
  2. 中间感染节点(如果存在中间节点就不考虑边缘节点了,因为题目中限制了1 <= initial.length < n,一定存在可以减少2个的中间节点,分析到这里时我以为我发现了官网的漏洞,错误的实现也能通过,想要贡献测试用例呢,结果提示测试用例非有效值。如果是小于等于n这个解法就得多一个判断条件initial.length == n,直接取最小下标
    • 仅有一个中间感染节点,连通区域节点个数
    • 有多个中间感染节点,dfs搜索不含感染节点路径上的非感染节点个数,如果有感染节点,那么它也是减1,不过这里不再比较了,原因上面也说过了。

代码中d存的是中间节点(包括指向自身的边大于2),如果d为空则表示连通区域全是边缘感染节点(边为2),或孤立感染节点(边为1)。

对于全是边缘感染节点与孤立感染节点的情况,取下标最小即可。而对于中间感染节点,通过dfs来查找连通个数。如果通过dfs查找的个数为1,并且它还是中间感染节点,那么它周围全是感染节点。按道理来说,应该与边缘节点一起取最小下标。但是题目给出了限制,那么一定存在一个可以减少2的中间节点。

通过上面的分析只是说明了该问题正向分析的复杂性,如果不是不断尝试,很难直接把上面的所有情况想清楚。所以,上面的分析也没有太大的用处,过一段时间重做这个题,还是会踩坑。

官网题解使用的是逆向思维,统计的是从每个非感染节点出发不经过感染节点所经历的个数,在dfs过程中使用状态机来标识感染节点的个数。如果只遇到了1个感染节点,那么累加刚才遍历的节点个数,而如果有多个,那么就只能减少它自己。因此,如果存在只遇到一个感染节点的情况,就取个数最大的。否则取下标最小的。

其实,只遇到一个感染节点的情况包括了上面的单一边缘感染节点、中间单一感染节点以及多个中间感染节点(dfs非感染个数不为0的情况,即路径上不含有感染节点)的情况,而遇到多个感染节点,则说明被多个感染节点包围/半包围(对应全是边缘节点、边缘与中间、全中间,后面两种情况上面的算法忽略掉了),并且取最小下标直接包括了孤立感染节点。

可以发现同样是一步处理,我们赋予它不同的内涵,其所应对的场景就大为不同。

代码


/**
 * @date 2024-04-17 8:46
 */
public class MinMalwareSpread928 {
    public int[] u;
    TreeSet<Integer> s;
    HashSet<Integer> d = new HashSet<>();
    List<Integer>[] g;

    public void merge(int x, int y) {
        HashSet<Integer> tmp = new HashSet<>();
        int rx = find(x, tmp);
        int ry = find(y, tmp);
        d.addAll(tmp);
        if (s.contains(rx) && s.contains(ry)) {
            if (rx > ry) {
                u[rx] = ry;
            } else if (rx < ry) {
                u[ry] = rx;
            }
        } else if (s.contains(ry)) {
            u[rx] = ry;
        } else {
            u[ry] = rx;
        }
    }

    public int find(int x, HashSet<Integer> tmp) {
        if (x != u[x]) {
            if (s.contains(x) && s.contains(u[x])) {
                if (g[x].size() > 2) {
                    tmp.add(x);
                }
                if (g[u[x]].size() > 2) {
                    tmp.add(u[x]);
                }
            }
            x = find(u[x], tmp);
        }
        return u[x];
    }

    public int find(int x) {
        if (x != u[x]) {
            x = find(u[x]);
        }
        return u[x];
    }

    public int count(int x) {
        int cnt = 0;
        int rt = find(x);
        for (int i = 0; i < u.length; i++) {
            if (rt == find(i)) {
                cnt++;
            }
        }
        return cnt;
    }

    public int countMalware(int x) {
        int cnt = 0;
        int rt = find(x);
        for (int i = 0; i < u.length; i++) {
            if (rt == find(i) && s.contains(i)) {
                cnt++;
            }
        }
        return cnt;
    }

    public int adjacencyUninfected(int x, int parent) {
        int cnt = 1;
        boolean[] visited = new boolean[u.length];
        for (Integer node : g[x]) {
            if (parent == node || node == x || visited[node]) {
                continue;
            }
            visited = new boolean[u.length];
            if (!s.contains(node)) {
                int subCnt = dfs(node, x, visited);
                if (subCnt != 0) {
                    cnt += subCnt;
                }
            }
        }
        return cnt;
    }

    public int dfs(int x, int parent, boolean[] visited) {
        if (s.contains(x)) {
            return 0;
        }
        int cnt = 1;
        for (Integer node : g[x]) {
            if (parent == node || node == x || visited[node]) {
                visited[node] = true;
                continue;
            }
            visited[node] = true;
            if (s.contains(node)) {
                return 0;
            }
            int subCnt = dfs(node, x, visited);
            if (subCnt == 0) {
                return 0;
            } else {
                cnt += subCnt;
            }
        }
        return cnt;
    }

    public int minMalwareSpread(int[][] graph, int[] initial) {
        int n = graph.length;
        g = new ArrayList[n];
        u = new int[n];
        for (int i = 0; i < n; i++) {
            g[i] = new ArrayList<>(n);
            u[i] = i;
        }

        s = new TreeSet<>();
        for (int i : initial) {
            s.add(i);
        }
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (graph[i][j] == 1) {
                    g[i].add(j);
                    merge(i, j);
                }
            }
        }
        int res = Integer.MAX_VALUE;
        int tmp = Integer.MAX_VALUE;
        TreeSet<Integer> ini = new TreeSet<>((x, y) -> count(y) - count(x) == 0 ? (adjacencyUninfected(y, -1) - adjacencyUninfected(x, -1) == 0 ? x - y : adjacencyUninfected(y, -1) - adjacencyUninfected(x, -1)) : count(y) - count(x));
        if (d.isEmpty()) {
            // d为空表示连通区域仅有1个感染节点
            for (int i : initial) {
                if (countMalware(i) == 1 && count(i) > 1) {
                    // 连通区域节点数大于1
                    if (tmp == Integer.MAX_VALUE) {
                        tmp = i;
                    } else {
                        int ci = count(i);
                        int ct = count(tmp);
                        if (ci > ct) {
                            // 取连通区域节点数大的
                            tmp = i;
                        } else if (ci == ct) {
                            // 如果相等取下标小的
                            tmp = Math.min(i, tmp);
                        }
                    }
                } else {
                    // 对于孤立节点,直接取索引最小的即可
                    res = Math.min(i, res);
                }
            }
            // 如果全部是孤立节点,取res,否则取tmp
            return tmp == Integer.MAX_VALUE ? res : tmp;
        } else {
            ini.addAll(d);
        }

        return ini.first();
    }
}

性能

924.尽量减少恶意软件的传播

目标

给出了一个由 n 个节点组成的网络,用 n × n 个邻接矩阵图 graph 表示。在节点网络中,当 graph[i][j] = 1 时,表示节点 i 能够直接连接到另一个节点 j。

一些节点 initial 最初被恶意软件感染。只要两个节点直接连接,且其中至少一个节点受到恶意软件的感染,那么两个节点都将被恶意软件感染。这种恶意软件的传播将继续,直到没有更多的节点可以被这种方式感染。

假设 M(initial) 是在恶意软件停止传播之后,整个网络中感染恶意软件的最终节点数。

如果从 initial 中移除某一节点能够最小化 M(initial), 返回该节点。如果有多个节点满足条件,就返回索引最小的节点。

请注意,如果某个节点已从受感染节点的列表 initial 中删除,它以后仍有可能因恶意软件传播而受到感染。

示例 1:

输入:graph = [[1,1,0],[1,1,0],[0,0,1]], initial = [0,1]
输出:0

示例 2:

输入:graph = [[1,0,0],[0,1,0],[0,0,1]], initial = [0,2]
输出:0

示例 3:

输入:graph = [[1,1,1],[1,1,1],[1,1,1]], initial = [1,2]
输出:1

说明:

  • n == graph.length
  • n == graph[i].length
  • 2 <= n <= 300
  • graph[i][j] == 0 或 1.
  • graph[i][j] == graph[j][i]
  • graph[i][i] == 1
  • 1 <= initial.length <= n
  • 0 <= initial[i] <= n - 1
  • initial 中所有整数均不重复

思路

初始 已感染恶意软件的节点集合中去掉一个节点使得整个网络的感染节点数量最小,返回这个节点。注意,从初始被感染的集合中去除,并不代表后续不会再被感染。如果还有与它连通的恶意节点,那么仍会被感染,最终计算感染节点时要算上。

因此,如果被感染节点是连通的,去掉任一感染节点后,总的感染节点数量不会改变。这时需要将索引最小的节点返回。

刚开始的想法是先排除相互的连通的感染节点,然后取剩余节点中连接节点个数最多的那个。

这个想法没错,但是具体实现的时候,仅仅判断直接相连的两个节点是否同时在感染列表显然是不对的,因为存在间接连接的情况。并且直接从感染集合移除还好影响后续其它节点的判断。

于是想到了使用并查集。

官网的解法类似,将连通的节点染成同一颜色,然后在感染节点中看是否有颜色唯一的节点,即该连通区域中只有一个感染节点,然后找出连通区域节点数最大的,如果有多个颜色唯一节点,返回下标最小的。如果没有颜色唯一的节点,那么移除任一感染节点,总的感染数都不会减少,直接取下标最小的即可。

判断区域是否连通可以使用并查集,也可以使用深度优先搜索。

代码

/**
 * @date 2024-04-16 8:29
 */
public class MinMalwareSpread924 {

    public int[] u;
    TreeSet<Integer> s;
    HashSet<Integer> d = new HashSet<>();

    public void merge(int x, int y) {
        HashSet<Integer> tmp = new HashSet<>();
        int rx = find(x, tmp);
        int ry = find(y, tmp);
        d.addAll(tmp);
        if (s.contains(rx) && s.contains(ry)) {
            if (rx > ry) {
                u[rx] = ry;
            } else if (rx < ry) {
                u[ry] = rx;
            }
        } else if (s.contains(ry)) {
            u[rx] = ry;
        } else {
            u[ry] = rx;
        }
    }

    public int find(int x, HashSet<Integer> tmp) {
        if (x != u[x]) {
            if (s.contains(x) && s.contains(u[x])) {
                tmp.add(x);
                tmp.add(u[x]);
            }
            x = find(u[x], tmp);
        }
        return u[x];
    }

    public int find(int x) {
        if (x != u[x]) {
            x = find(u[x]);
        }
        return u[x];
    }

    public int count(int x) {
        int cnt = 0;
        int rt = find(x);
        for (int i = 0; i < u.length; i++) {
            if (rt == find(i)) {
                cnt++;
            }
        }
        return cnt;
    }

    public int minMalwareSpread(int[][] graph, int[] initial) {
        int n = graph.length;
        List<Integer>[] g = new ArrayList[n];
        u = new int[n];
        for (int i = 0; i < n; i++) {
            g[i] = new ArrayList<>(n);
            u[i] = i;
        }

        s = new TreeSet<>();
        for (int i : initial) {
            s.add(i);
        }
        int res = s.first();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (graph[i][j] == 1) {
                    g[i].add(j);
                    merge(i, j);
                }
            }
        }
        if (s.size() == d.size()) {
            return res;
        }
        TreeSet<Integer> ini = new TreeSet<>((x, y) -> count(y) - count(x) == 0 ? x - y : count(y) - count(x));
        for (int i : initial) {
            if (!d.contains(i)) {
                ini.add(i);
            }
        }

        return ini.first();
    }

}

性能

2924.找到冠军II

目标

一场比赛中共有 n 支队伍,按从 0 到 n - 1 编号。每支队伍也是 有向无环图(DAG, Directed Acyclic Graph) 上的一个节点。

给你一个整数 n 和一个下标从 0 开始、长度为 m 的二维整数数组 edges 表示这个有向无环图,其中 edges[i] = [ui, vi] 表示图中存在一条从 ui 队到 vi 队的有向边。

从 a 队到 b 队的有向边意味着 a 队比 b 队 强 ,也就是 b 队比 a 队 弱 。

在这场比赛中,如果不存在某支强于 a 队的队伍,则认为 a 队将会是 冠军 。

如果这场比赛存在 唯一 一个冠军,则返回将会成为冠军的队伍。否则,返回 -1 。

注意:

是形如 a1, a2, ..., an, an+1 的一个序列,且满足:节点 a1 与节点 an+1 是同一个节点;节点 a1, a2, ..., an 互不相同;对于范围 [1, n] 中的每个 i ,均存在一条从节点 ai 到节点 ai+1 的有向边。

有向无环图 是不存在任何环的有向图。

说明:

  • 1 <= n <= 100
  • m == edges.length
  • 0 <= m <= n * (n - 1) / 2
  • edges[i].length == 2
  • 0 <= edge[i][j] <= n - 1
  • edges[i][0] != edges[i][1]
  • 生成的输入满足:如果 a 队比 b 队强,就不存在 b 队比 a 队强
  • 生成的输入满足:如果 a 队比 b 队强,b 队比 c 队强,那么 a 队比 c 队强

思路

如果只有一个节点,那它就是冠军。入度非0的不是冠军。多个没有强弱关系的的节点,返回-1,例如,n=2,边为空。

只需计算没有被标记为weaker节点的index,如果多于1个返回-1。

代码

/**
 * @date 2024-04-13 19:22
 */
public class FindChampion2924 {
    public int findChampion(int n, int[][] edges) {
        boolean[] weaker = new boolean[n];
        for (int[] edge : edges) {
            weaker[edge[1]] = true;
        }
        int championIndex = -1;
        for (int i = 0; i < weaker.length; i++) {
            if (weaker[i]) {
                continue;
            }
            if (championIndex != -1) {
                return -1;
            }
            championIndex = i;
        }
        return championIndex;
    }
}

性能

2192.有向无环图中一个节点的所有祖先

目标

给你一个正整数 n ,它表示一个 有向无环图 中节点的数目,节点编号为 0 到 n - 1 (包括两者)。

给你一个二维整数数组 edges ,其中 edges[i] = [fromi, toi] 表示图中一条从 fromi 到 toi 的单向边。

请你返回一个数组 answer,其中 answer[i]是第 i 个节点的所有 祖先 ,这些祖先节点 升序 排序。

如果 u 通过一系列边,能够到达 v ,那么我们称节点 u 是节点 v 的 祖先 节点。

说明:

  • 1 <= n <= 1000
  • 0 <= edges.length <= min(2000, n * (n - 1) / 2)
  • edges[i].length == 2
  • 0 <= fromi, toi <= n - 1
  • fromi != toi
  • 图中不会有重边。
  • 图是 有向 且 无环 的。

思路

这个题要求所有节点的祖先节点集合,最直接的想法就是广度遍历然后记录父节点,然后下一个节点的祖先节点就是其父节点加上父节点的祖先节点。

需要注意的点是图不一定连通,所以选定一个起点不一定能遍历到所有节点。如果直接将所有节点加入队列容易超时。解决方法是先找到没有父节点的根节点,然后再广度遍历。

如果节点已经在队列中就不需要重复放入队列了,因为该节点的祖先集合可以由队列中的节点一起更新。

代码

/**
 * @date 2024-04-04 21:49
 */
public class GetAncestors2192 {

    /** 先找出没有parent的节点放入队列,然后广度优先遍历即可*/
    public List<List<Integer>> getAncestors(int n, int[][] edges) {
        List<Integer>[] g = new ArrayList[n];
        Set<Integer>[] dp = new TreeSet[n];
        List<List<Integer>> res = new ArrayList<>(n);
        Deque<Integer> q = new ArrayDeque<>();
        boolean[] hasParent = new boolean[n];
        for (int i = 0; i < g.length; i++) {
            g[i] = new ArrayList<>();
            dp[i] = new TreeSet<>();
        }
        for (int[] edge : edges) {
            g[edge[0]].add(edge[1]);
            hasParent[edge[1]] = true;
        }
        for (int i = 0; i < hasParent.length; i++) {
            if (!hasParent[i]) {
                q.offer(i);
            }
        }
        while (!q.isEmpty()) {
            Integer from = q.poll();
            for (int i = 0; i < g[from].size(); i++) {
                dp[g[from].get(i)].addAll(dp[from]);
                dp[g[from].get(i)].add(from);
                if (!q.contains(g[from].get(i))) {
                    q.offer(g[from].get(i));
                }
            }
        }
        for (Set<Integer> integers : dp) {
            res.add(new ArrayList<>(integers));
        }
        return res;
    }
}

性能

勉强过关,官网还介绍了拓扑排序的方法,有机会再更新吧。