图论总结

本文详细介绍了图论中的拓扑排序、欧拉图、并查集、最小生成树、回溯算法和多源最短路等概念,结合具体算法如Hierholzer、Kruskal和Floyd,并给出了力扣相关题目作为实践示例。

拓扑排序

拓扑排序的本质是:广度优先遍历 + 贪心算法。
拓扑排序是广度优先遍历和贪心算法应用于有向图的专有名词。
应用场景:任务调度计划、课程安排。
拓扑排序的作用:
1.得到一个拓扑序,拓扑序不唯一;
2.检测有向图是否有环。(补充:无向图中检测是否有环,使用的数据结构是并查集。)
算法流程:
1、在开始排序前,扫描对应的存储空间(使用邻接表),将入度为 0的结点放入队列。
2、只要队列非空,就从队首取出入度为 0 的结点,将这个结点输出到结果集中,并且将这个结点的所有邻接结点(它指向的结点)的入度减 1,在减 1 以后,如果这个被减 1的邻接结点的入度为 0,则入队。
3、当队列为空的时候,检查结果集中的顶点个数是否和图的顶点数相等即可。

在代码具体实现的时候,除了保存入度为 0 的队列,我们还需要两个辅助的数据结构:
1、邻接表:通过结点的索引,我们能够得到这个结点的后继结点;
2、入度数组:通过结点的索引,我们能够得到指向这个结点的结点个数。

力扣相关题目

1.207. 课程表
代码如下:

class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        if (numCourses < 0) {
            return false;
        }
        if (null == prerequisites || 0 == prerequisites.length || 0 == prerequisites[0].length) {
            return true;
        }
        // 入度数组
        int[] inDegree = new int[numCourses];
        // 图的邻接表实现
        HashSet<Integer>[] adj = new HashSet[numCourses];
        for (int i = 0; i < numCourses; i ++) {
            adj[i] = new HashSet<>();
        }
        for (int[] p : prerequisites) {
            inDegree[p[0]] ++;
            adj[p[1]].add(p[0]);
        }
        // 队列:java推荐实现
        Queue<Integer> queue = new LinkedList<>();
        // 找到一开始入度为0的顶点
        for (int i = 0; i < numCourses; i ++) {
            if (inDegree[i] == 0) {
                queue.add(i);
            }
        }
        int cnt = 0;
        while (!queue.isEmpty()) {
            Integer top = queue.poll();
            cnt ++;
            for (Integer successor : adj[top]) {
                inDegree[successor] --;
                if (inDegree[successor] == 0) {
                    queue.add(successor);
                }
            }
        }
        return cnt == numCourses;
    }
}

2.210. 课程表 II

// 拓扑排序的模板
class Solution {
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        int[] res = new int[numCourses];
        if (numCourses < 0 ) {
            return res;
        }
        if ((null == prerequisites || 0 == prerequisites.length || 0 == prerequisites[0].length)) {
            for (int i = 0; i < numCourses; i++) {
                res[i] = numCourses - i - 1;
            }
            return res;
        }
        int[] inDegree = new int[numCourses];
        HashSet<Integer>[] adj = new HashSet[numCourses];
        for (int i = 0; i < numCourses; i ++) {
            adj[i] = new HashSet<>();
        }
        for (int[] p : prerequisites) {
            inDegree[p[0]] ++;
            adj[p[1]].add(p[0]);
        }
        
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i ++) {
            if (inDegree[i] == 0) {
                queue.offer(i);
            }
        }
        int cnt = 0;
        while (!queue.isEmpty()) {
            Integer top = queue.poll();
            res[cnt ++] = top;
            for (Integer successor : adj[top]) {
                inDegree[successor] --;
                if (inDegree[successor] == 0) {
                    queue.add(successor);
                }
            }
        }
        return cnt == numCourses ? res : new int[0];
    }
}

3.802. 找到最终的安全状态
参考资料
代码如下:

class Solution {
    public List<Integer> eventualSafeNodes(int[][] graph) {
        int N = graph.length;
        List<Set<Integer>> gra = new ArrayList<>();
        List<Set<Integer>> rgra = new ArrayList<>();
        boolean[] visited = new boolean[N];
        List<Integer> res = new ArrayList<>();
        for (int i = 0; i < N; i ++) {
            gra.add(new HashSet<>());
            rgra.add(new HashSet<>());
        }
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < N; i ++) {
            if (0 == graph[i].length) {
                queue.offer(i);
            }
            for (int j : graph[i]) {
                gra.get(i).add(j);
                rgra.get(j).add(i);
            }
        }
        while (!queue.isEmpty()) {
            int j = queue.poll();
            visited[j] = true;
            for (int i : rgra.get(j)) {
                gra.get(i).remove(j);
                if (gra.get(i).isEmpty()) {
                    queue.offer(i);
                }
            }
        }
        for (int i = 0; i < N; i ++) {
            if (visited[i]) {
                res.add(i);
            }
        }
        return res;
    }
}

欧拉图

判断图是欧拉图或者是半欧拉图的条件:
无向图:
欧拉图充要条件:图是连通的且没有奇度顶点。
半欧拉图充要条件:图是连通的且恰有2个奇度顶点。
有向图:
欧拉图充要条件: 图的所有顶点都属于同一个连通量内且每一顶点的出度、入度相同。
半欧拉图充要条件:图的所有顶点都属于同一个连通量内且
1.恰有一个顶点的出度与入度差为1。
2.恰有一个顶点的入度与出度差为1。
3.其他顶点的出度与入度相同。

Hierholzer 算法

1.332. 重新安排行程

思路及算法

Hierholzer 算法用于在连通图中寻找欧拉路径,其流程如下:
1.从起点出发,进行深度优先搜索。
2.每次沿着某条边从某个顶点移动到另外一个顶点的时候,都需要删除这条边。
3.如果没有可移动的路径,则将所在节点加入到栈中,并返回。
参考资料:官方题解
代码如下:

class Solution {
    // 图的邻接表实现,由于题目要求要按字典序,所以用优先级队列,如果没有要求或者是int类型,可以用List
    Map<String, PriorityQueue<String>> map = new HashMap<>();
    List<String> itinerary = new ArrayList<>();
    public List<String> findItinerary(List<List<String>> tickets) {
        for (List<String> ticket : tickets) {
            String src = ticket.get(0);
            String dst = ticket.get(1);
            if (!map.containsKey(src)) {
                map.put(src, new PriorityQueue<String>());
            }
            map.get(src).offer(dst);
        }
        dfs("JFK");
        Collections.reverse(itinerary);
        return itinerary;
    }
    private void dfs(String cur) {
        while (map.containsKey(cur) && map.get(cur).size() > 0) {
            String temp = map.get(cur).poll();
            dfs(temp);
        }
        itinerary.add(cur);
    }
}

2.753. 破解保险箱
参考资料
代码如下:

class Solution {
    Set<Integer> seen = new HashSet<Integer>();
    StringBuffer ans = new StringBuffer();
    int highest;
    int k;

    public String crackSafe(int n, int k) {
        highest = (int) Math.pow(10, n - 1);
        this.k = k;
        dfs(0);
        for (int i = 1; i < n; i++) {
            ans.append('0');
        }
        return ans.toString();
    }

    public void dfs(int node) {
        for (int x = 0; x < k; ++x) {
            int nei = node * 10 + x;
            if (!seen.contains(nei)) {
                seen.add(nei);
                dfs(nei % highest);
                ans.append(x);
            }
        }
    }
}

并查集

并查集是一种树型的数据结构。
并查集的作用:
1.处理不相交的集合的合并、查询问题。
2.检查一个无向图是否有环。
3.计算无向图的连通分量数目。
并查集模板:

class DSU {
    private int[] parents;
    public DSU(int n) {
        parents = new int[n];
        for (int i = 0; i < n; i ++) {
            parents[i] = i;
        }
    }
    // 路径压缩:隔代压缩
    public int find(int x) {
        while (x != parents[x]) {
            parents[x] = parents[parents[x]];
            x = parents[x];
        }
        return x;
    }
    // 合并
    public void union(int x, int y) {
        parents[find(x)] = find(y);
    }
    // 检查是否连接
    public boolean isConnected(int x, int y) {
        return find(x) == find(y);
    }
}

1.684. 冗余连接
代码如下:

class Solution {
    public int[] findRedundantConnection(int[][] edges) {
       int N = edges.length;
       DSU dsu = new DSU(N + 1);
       int[] res = new int[2];
       for (int[] edge : edges) {
           int from = edge[0];
           int to = edge[1];
           if (!dsu.isConnected(from, to)) {
               dsu.union(from, to);
           } else {
               res[0] = from;
               res[1] = to;
           }
       }
       return res;
    }
}

class DSU {
    private int[] parents;
    public DSU(int n) {
        parents = new int[n];
        for (int i = 0; i < n; i ++) {
            parents[i] = i;
        }
    }
    public int find(int x) {
        while (x != parents[x]) {
            parents[x] = parents[parents[x]];
            x = parents[x];
        }
        return x;
    }
    public void union(int x, int y) {
        parents[find(x)] = find(y);
    }
    public boolean isConnected(int x, int y) {
        return find(x) == find(y);
    }
}

2.323. 无向图中连通分量的数目
代码如下:


class Solution {
    public int countComponents(int n, int[][] edges) {
        int num = n;
        DSU dsu = new DSU(n);
        for (int[] edge : edges) {
            int from = edge[0];
            int to = edge[1];
            if (!dsu.isConnected(from, to)) {
                dsu.union(from, to);
                num --;
            }
        }
        return num;
    }
}

class DSU {
    private int[] parents;
    public DSU(int n) {
        parents = new int[n];
        for (int i = 0; i < n; i ++) {
            parents[i] = i;
        }
    }
    public int find(int x) {
        while (x != parents[x]) {
            parents[x] = parents[parents[x]];
            x = parents[x];
        }
        return x;
    }
    public void union (int x, int y) {
        parents[find(x)] = find(y);
    }
    public boolean isConnected(int x, int y) {
        return find(x) == find(y);
    }
}

3.261. 以图判树
代码如下:

class Solution {
    // 树是连通量为1,无环图
    public boolean validTree(int n, int[][] edges) {
        DSU dsu = new DSU(n);
        int num = n;
        for (int[] edge : edges) {
            int from = edge[0];
            int to = edge[1];
            if (!dsu.isConnected(from, to)) {
                dsu.union(from, to);
                num --;
            } else {
                return false;
            }
        }
        return num == 1 ? true : false;
    }
}

class DSU {
    private int[] parents;
    public DSU (int n) {
        parents = new int[n];
        for (int i = 0; i < n; i ++) {
            parents[i] = i;
        }
    }
    public int find(int x) {
        while (x != parents[x]) {
            parents[x] = parents[parents[x]];
            x = parents[x];
        }
        return x;
    }
    public void union(int x, int y) {
        parents[find(x)] = find(y);
    }
    public boolean isConnected(int x, int y) {
        return find(x) == find(y);
    }
}

最小生成树

kruskal 算法:
按照边的权重顺序(从小到大)处理所有的边,将边加入到最小生成树中,加入的边不会与已经加入的边构成环,直到树中含有 N - 1 条边为止。这些边会由一片森林变成一个树,这个树就是图的最小生成树。
1.1135. 最低成本联通所有城市
代码如下:

// Kruskal算法
class Solution {
    public int minimumCost(int N, int[][] connections) {
        // 从小到大排序
        Arrays.sort(connections, (a, b) -> a[2] - b[2]);
        DSU dsu = new DSU(N);
        int count = 0;
        int cost = 0;
        for (int[] conn : connections) {
            if (!dsu.isConnnect(conn[0], conn[1])) {
                dsu.union(conn[0], conn[1]);
                count ++;
                cost += conn[2];
            }
            // 最小生成树的边数等于N - 1
            if (count == N - 1) {
                break;
            }
        }
        if (count != N - 1) {
            return -1;
        }
        return cost;
    }
}
// 并查集类
class DSU {
    private int[] parent;
    public DSU(int N) {
        // 顶点序号1~N
        parent = new int[N + 1];
        for (int i = 0; i <= N; i ++) {
            parent[i] = i;
        }
    }
    // 隔代压缩(路径压缩)
    private int find(int x) {
        while (x != parent[x]) {
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        return parent[x];
    }
    public void union(int x, int y) {
        int parentx = find(x);
        int parenty = find(y);
        if (parentx != parenty) {
            parent[parentx] = parenty;
        }
    }
    public boolean isConnnect(int x, int y) {
        return find(x) == find(y);
    }
}

回溯算法

1.797. 所有可能的路径
代码如下:

class Solution {
    List<List<Integer>> res;
    public List<List<Integer>> allPathsSourceTarget(int[][] graph) {
        res = new ArrayList<>();
        List<Integer> path = new ArrayList<>();
        path.add(0);
        dfsBacktracking(0, path, graph);
        return res;
    }
    private void dfsBacktracking(int cur, List<Integer> path, int[][] graph) {
        if (cur == graph.length - 1) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (int node : graph[cur]) {
            path.add(node);
            dfsBacktracking(node, path, graph);
            path.remove(path.size() - 1);
        }
    }
}

2.1059. 从始点到终点的所有路径
代码如下:

class Solution {
    public boolean leadsToDestination(int n, int[][] edges, int source, int destination) {
        List<Integer>[] adj = new ArrayList[n];
        boolean[] visited = new boolean[n];
        boolean[] onPath = new boolean[n];
        for (int i = 0; i < n; i ++) {
            adj[i] = new ArrayList<>();
        }
        for (int[] edge : edges) {
            int from = edge[0];
            int to = edge[1];
            adj[from].add(to);
        }
        return dfs(adj, source, destination, visited, onPath);
    }
    private boolean dfs(List<Integer>[] adj, int node, int destination, boolean[] visited, boolean[] onPath) {
        if (0 == adj[node].size()) {
            return node == destination;
        }
        visited[node] = true;
        onPath[node] = true;
        for (Integer next : adj[node]) {
            if ((visited[next] && onPath[next]) || (!visited[next] && !dfs(adj, next, destination, visited, onPath))) {
                return false;
            }
        }
        onPath[node] = false;
        return true;
    }
}

多源选最短路

floyd算法

1.1334. 阈值距离内邻居最少的城市
代码如下:

class Solution {
    public int findTheCity(int n, int[][] edges, int distanceThreshold) {
        // floyd用邻接矩阵存储图
        int[][] map = new int[n][n];
        for (int i = 0; i < n; i ++) {
            for (int j = 0; j < n; j ++) {
                if (i == j) {
                    // 自己则为0
                    map[i][j] = 0;
                } else {
                    // 默认到达不了,设置为+无穷大
                    map[i][j] = Integer.MAX_VALUE;
                }
            }
        }
        for (int[] edge : edges) {
            map[edge[0]][edge[1]] = edge[2];
            map[edge[1]][edge[0]] = edge[2];
        }

        // floyd模板
        for (int k = 0; k < n; k ++) {
            for (int i = 0; i < n; i ++) {
                for (int j = 0; j < n; j ++) {
                    if (i != j && j != k && map[i][k] != Integer.MAX_VALUE && map[k][j] != Integer.MAX_VALUE) {
                        map[i][j] = Math.min(map[i][j], map[i][k] + map[k][j]);
                    }
                }
            }
        }

        int min = n + 1;
        int res = -1;
        for (int i = 0; i < n; i ++) {
            int count = 0;
            for (int j = 0; j < n; j ++) {
                if (i != j && map[i][j] <= distanceThreshold) {
                    count ++;
                }
            }
            if (min >= count) {
                min = count;
                res = i;
            }
        }
        return res;
    }
}

图论代码技巧

1.邻接表实现(Set,List实现都可以)
 HashSet<Integer>[] adj = new HashSet[numCourses];
 for (int i = 0; i < numCourses; i ++) {
            adj[i] = new HashSet<>();
        }
        for (int[] p : prerequisites) {
            int from = p[1];
            int to = p[0];
            adj[from].add(to);
        }       
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值