统计无向图中无法互相到达点对数[经典建邻接表+DFS统计 -> 并查集优化][并查集手册/写的详细]

经典建邻接表+DFS统计 -> 并查集优化

  • 前言
  • 一、统计无向图中无法互相到达点对数
  • 二、经典建邻接表+DFS统计 -> 并查集优化
    • 1、经典建邻接表+DFS统计
    • 2、并查集优化
  • 总结
  • 参考文献

前言

给定节点和边(节点对),求关于连通分量及其变种。第一想法都是根据节点和节点对(边)来建立邻接矩阵,在DFS做事。
但还有一种和连通分量紧密相关的思想,并查集,它不建立完整的图,但能做相应的事,所以运行时间短。
它将原有的图,从中选出一个节点作为根节点,所有直接/间接相连的节点都作为子节点,能解很多连通分量/连通分量变种问题。
并查集除上述优点外,我感觉它的数组hash比hashMap快,毕竟数组为JVM层面,且连续内层分配,比API快。

一、统计无向图中无法互相到达点对数

统计无向图中无法互相到达点对数[经典建邻接表+DFS统计 -> 并查集优化][并查集手册/写的详细]_第1张图片
统计无向图中无法互相到达点对数[经典建邻接表+DFS统计 -> 并查集优化][并查集手册/写的详细]_第2张图片

二、经典建邻接表+DFS统计 -> 并查集优化

1、经典建邻接表+DFS统计

public class CountPairs {
    public long countPairs(int n, int[][] edges) {
        // 加入所有节点。
        for (int i = 0; i < n; i++) addNode(i);
        for (int[] edge : edges) addEdge(edge[0], edge[1]);
        List<Long> arr = new ArrayList<>();
        for (int i = 0; i < n; i++) if (graph.containsKey(i)) arr.add(dfs(i));
        //
        long[] prefix = new long[arr.size()];
        long sum = 0, ans = 0;
        int idx = 0;

        for (long i : arr) prefix[idx++] = (sum += i);
        for (int i = 0; i < arr.size() - 1; i++) ans += arr.get(i) * (prefix[arr.size() - 1] - prefix[i]);
        return ans;
    }

    private long dfs(int n) {
        long rs = 1;
        Set<Integer> next = graph.get(n);
        if (next == null) return rs;
        graph.remove(n);
        for (Integer i : next) {
            if (graph.containsKey(i)) rs += dfs(i);
        }
        return rs;
    }

    private void addEdge(int a, int b) {
        addNode(a);
        addNode(b);

        graph.get(a).add(b);
        graph.get(b).add(a);
    }

    private void addNode(int b) {
        if (!graph.containsKey(b)) graph.put(b, new HashSet<>());
    }

    Map<Integer, Set<Integer>> graph = new HashMap<>();
}

2、并查集优化

// 练习哈并查集,它是关于连通分量的利器。
class CountPairs2 {
    public long countPairs(int n, int[][] edges) {
        // 并查集标准第一步:初始化。
        int[] father = new int[n];
        Arrays.fill(father, -1);
        // 并查集标准第二步:union
        cnt = n;// 这里想统计连通分量个数,方便等会统计每个连通分量的节点数,可以用到数组,而不是可扩容的list.| 也可直接list
        for (int[] edge : edges) union(father, edge[0], edge[1]);
        // 非特定逻辑,具体需求具体写。
        // 统计每个连通分量节点数。
        int[] arr = new int[cnt];
        cnt = 0;
        for (int f : father) if (f < 0) arr[cnt++] = -f;
        // 非特定逻辑,具体需求具体写。
        // 得到不同连通分量的节点数,利用前缀和,空间换时间。
        long[] prefix = new long[arr.length];
        long sum = 0;
        cnt = 0;
        for (int a : arr) prefix[cnt++] = (sum += a);
        // 得到结果
        long ans = 0;
        for (int i = 0; i < arr.length - 1; i++) ans += arr[i] * (prefix[prefix.length - 1] - prefix[i]);
        return ans;
    }

    int cnt = 0;// 连通分量个数。

    private void union(int[] father, int m, int n) {
        // 并查集标准第三步:findFather
        int r1 = findFather(father, m);
        int r2 = findFather(father, n);
        // 非同一逻辑,根据实际情况来写。
        if (r1 != r2) {
            // 合并两者,就将一个作为根,得到其整个树的节点数,这里负数表示。
            father[r1] += father[r2];
            // 并把另一个作为子树,并把其根节点修改。
            father[r2] = r1;
            // 可选操作,当用list时,则不必。
            --cnt;
        }
    }

    private int findFather(int[] father, int r) {
        // 一路返回根,并把根作为路径上每个节点的根.
        // 体现在 if (father[r] != r) father[r] = findFather(father, father[r]); return father[r];这将把有深度的树平铺起来。

        // 但是这里不行,因为根节点的值不是等于其下标,是负数,需要统计节点个数。
        // 这里需要一改传统做法,要灵活变通。

        // 若该节点是根
        if (father[r] < 0) return r;
        return father[r] = findFather(father, father[r]);
    }
}

总结

1)经典邻接表建立 + 在dfs遍历(图操作的基础)做各自事。
2)并查集将图的形状改掉,但不影响解题,则它降低的运行时间就变成了优点。
3)并查集除上述优点外,我感觉它的数组hash比hashMap快,毕竟数组为JVM层面,且连续内层分配,比API快。
4)并查集经典三步,每步都可因具体需求,而灵活变通写法。具体三步为:初始化数组father[size]{}(数组hash非常nice)、union(int[],int,int)联结两节点、findFather(int[],int)寻找根节点。

参考文献

[1] LeetCode 统计无向图中无法互相到达点对数

你可能感兴趣的:(数据机构与算法,连通分量,邻接表,深度优先搜索,并查集,Java)