Java强连通分量(含面试大厂题和源码)

强连通分量(Strongly Connected Component, SCC)是图论中的一个概念,用于描述有向图中的一种特殊子图。在有向图中,如果两个顶点u和v之间的所有路径都是有向的,并且u可以到达v,同时v也可以到达u,那么这两个顶点就属于同一个强连通分量。

在强连通分量中,任意两个顶点都是相互可达的。一个有向图可以被分解为多个强连通分量,这些分量之间是相互独立的。

算法步骤

  1. 对有向图进行拓扑排序:首先,对给定的有向图进行拓扑排序,得到一个顶点序列,使得图中的每个有向边都从序列前面的顶点指向后面的顶点。

  2. 逆向图:创建原图的逆向图,即原图中的每条边方向都反转。

  3. 对逆向图进行拓扑排序:对逆向图进行拓扑排序,得到逆序的顶点序列。

  4. 构建强连通分量:将拓扑排序和逆序拓扑排序中,相同位置的顶点分为一组,这些顶点即为一个强连通分量。

Java实现

以下是使用Java实现强连通分量检测的示例代码:

import java.util.*;

public class StronglyConnectedComponents {
    private int V; // 顶点的数量
    private int[] low;
    private int[] disc;
    private Stack<Integer> stack;
    private List<List<Integer>> sccList;
    private int time;

    public StronglyConnectedComponents(int V) {
        this.V = V;
        low = new int[V];
        disc = new int[V];
        stack = new Stack<>();
        sccList = new ArrayList<>();
        time = 0;
    }

    public void addEdge(int source, int dest) {
        // 有向边添加
    }

    public List<List<Integer>> getSCCs() {
        // 1. 对原图进行DFS遍历
        for (int i = 0; i < V; i++) {
            if (disc[i] == -1) {
                DFS(i);
            }
        }

        // 2. 创建逆向图并对其进行DFS遍历
        // ...

        // 返回强连通分量列表
        return sccList;
    }

    private void DFS(int v) {
        disc[v] = low[v] = ++time;
        stack.push(v);
        boolean isSCC = true;

        List<Integer> adj = getAdj(v); // 获取邻接顶点列表
        for (Integer u : adj) {
            if (disc[u] == -1) {
                DFS(u);
                low[v] = Math.min(low[v], low[u]);
                isSCC &= (low[u] <= disc[v] && disc[u] < low[v]);
            } else if (stack.contains(u)) {
                low[v] = Math.min(low[v], disc[u]);
                isSCC &= (disc[u] < low[v]);
            }
        }

        if (isSCC && disc[v] == low[v]) {
            List<Integer> scc = new ArrayList<>();
            while (!stack.isEmpty()) {
                int u = stack.pop();
                scc.add(u);
                if (u == v) break;
            }
            sccList.add(scc);
        }
    }

    private List<Integer> getAdj(int v) {
        // 获取顶点v的邻接顶点列表
        return new ArrayList<>();
    }

    public static void main(String[] args) {
        StronglyConnectedComponents scc = new StronglyConnectedComponents(5);
        // 添加边
        List<List<Integer>> sccList = scc.getSCCs();
        for (List<Integer> scc : sccList) {
            System.out.println(scc);
        }
    }
}

在上述代码中,DFS方法用于对原图进行深度优先搜索,同时找到强连通分量。getAdj方法应该返回传入顶点的所有邻接顶点的列表。addEdge方法用于添加有向边。注意,上面的代码是一个框架,具体实现需要根据实际的图结构进行调整。

强连通分量的算法通常用于处理有向图中的循环依赖问题,例如在任务调度、编译器构建依赖关系等场景中。在面试中,大厂可能会考察候选人对图算法的理解和实现能力,特别是与强连通分量(SCC)相关的算法。以下是三道与强连通分量相关的面试题目,以及它们的Java源码示例。

1. 找出图中的所有强连通分量

题目:给定一个有向图,实现一个算法来找出图中所有的强连通分量。

解题思路:使用Kosaraju算法,该算法基于深度优先搜索(DFS)。

源码

import java.util.*;

public class KosarajuSCC {
    private int V; // 顶点数量
    private int[] disc, low;
    private Stack<Integer> stack;
    private List<List<Integer>> sccList;
    private int time;
    private List<List<Integer>> adj;

    public KosarajuSCC(int V) {
        this.V = V;
        this.adj = new ArrayList<>(V);
        for (int i = 0; i < V; i++) {
            adj.add(new ArrayList<>());
        }
    }

    public void addEdge(int source, int dest) {
        adj.get(source).add(dest);
    }

    public List<List<Integer>> getSCCs() {
        disc = new int[V];
        low = new int[V];
        stack = new Stack<>();
        sccList = new ArrayList<>();
        time = 0;

        // 调用DFS从每一个未访问顶点开始
        for (int i = 0; i < V; i++) {
            if (disc[i] == -1) {
                DFS(i);
            }
        }

        // 逆向图的DFS
        Arrays.fill(disc, -1);
        for (int i = 0; i < V; i++) {
            if (disc[i] == -1) {
                DFSReverse(i);
            }
        }

        return sccList;
    }

    private void DFS(int v) {
        disc[v] = low[v] = ++time;
        for (Integer u : adj.get(v)) {
            if (disc[u] == -1) {
                DFS(u);
                low[v] = Math.min(low[v], low[u]);
            }
        }
    }

    private void DFSReverse(int v) {
        boolean isSCC = true;
        if (disc[v] == -1) {
            for (Integer u : adj.get(v)) {
                if (disc[u] == -1) {
                    DFSReverse(u);
                    low[v] = Math.min(low[v], low[u]);
                    isSCC &= (low[u] <= disc[v]);
                }
            }
            if (isSCC && disc[v] == low[v]) {
                List<Integer> scc = new ArrayList<>();
                while (!stack.isEmpty()) {
                    int u = stack.pop();
                    scc.add(u);
                    disc[u] = V;
                    if (u == v) break;
                }
                sccList.add(scc);
            }
        }
        stack.push(v);
        disc[v] = 0;
    }

    public static void main(String[] args) {
        KosarajuSCC scc = new KosarajuSCC(5);
        scc.addEdge(1, 0);
        scc.addEdge(0, 2);
        scc.addEdge(2, 1);
        scc.addEdge(0, 3);
        scc.addEdge(3, 4);

        List<List<Integer>> sccList = scc.getSCCs();
        for (List<Integer> scc : sccList) {
            System.out.println(scc);
        }
    }
}

2. 检测图中是否存在环

题目:给定一个图,判断图中是否存在环。

解题思路:使用DFS,通过检测递归栈中的顶点是否已经访问过。

源码

import java.util.*;

public class GraphCycleDetection {
    private int V; // 顶点数量
    private List<List<Integer>> adj; // 邻接表
    private boolean[] visited;
    private boolean[] recStack;
    private boolean isCycle;

    public GraphCycleDetection(int V) {
        this.V = V;
        adj = new ArrayList<>(V);
        for (int i = 0; i < V; i++) {
            adj.add(new ArrayList<>());
        }
        visited = new boolean[V];
        recStack = new boolean[V];
        isCycle = false;
    }

    public void addEdge(int source, int dest) {
        adj.get(source).add(dest);
    }

    public boolean isCyclic() {
        for (int i = 0; i < V; i++) {
            if (!visited[i]) {
                isCycle = false;
                DFS(i);
                if (isCycle) break;
            }
        }
        return isCycle;
    }

    private void DFS(int v) {
        visited[v] = true;
        recStack[v] = true;

        for (Integer neighbour : adj.get(v)) {
            if (!visited[neighbour]) {
                DFS(neighbour);
            } else if (recStack[neighbour]) {
                isCycle = true;
            }
        }

        recStack[v] = false;
    }

    public static void main(String[] args) {
        GraphCycleDetection graph = new GraphCycleDetection(4);
        graph.addEdge(0, 1);
        graph.addEdge(1, 2);
        graph.addEdge(2, 3);
        graph.addEdge(3, 1); // 此边创建环

        System.out.println("图中是否存在环: " + graph.isCyclic());
    }
}

3. 最长递增子序列

题目:给定一个未排序的整数数组,找到其中的最长递增子序列。

解题思路:将问题转化为最短路径问题,通过构建一个有向图,其中边表示序列中满足条件的数对,然后使用BFS或拓扑排序找到最长路径。

源码

import java.util.*;

public class LongestIncreasingSubsequence {
    public int lengthOfLIS(int[] nums) {
        if (nums == null || nums.length == 0) return 0;

        int n = nums.length;
        int[] dp = new int[n];
        int size = 0;

        for (int i : nums) {
            int l = 0, r = size;
            while (l != r) {
                int m = l + (r - l) / 2;
                if (dp[m] < i) {
                    l = m + 1;
                } else {
                    r = m;
                }
            }
            dp[l] = i;
            if (l == size) size++;
        }

        return size;
    }

    public static void main(String[] args) {
        int[] nums = {10, 9, 2, 5, 3, 7, 101, 18};
        LongestIncreasingSubsequence lis = new LongestIncreasingSubsequence();
        System.out.println("最长递增子序列的长度是: " + lis.lengthOfLIS(nums));
    }
}

在上述代码中,我们使用了二分查找和动态规划的方法来解决最长递增子序列问题,而不是将其转化为最短路径问题,因为这种方法更高效且易于实现。然而,如果面试官要求使用图算法来解决,那么可能需要构建一个有向图,并使用拓扑排序或BFS来找到最长路径。这通常涉及到更复杂的图构建和算法实现。

  • 10
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值