强连通分量(Strongly Connected Component, SCC)是图论中的一个概念,用于描述有向图中的一种特殊子图。在有向图中,如果两个顶点u和v之间的所有路径都是有向的,并且u可以到达v,同时v也可以到达u,那么这两个顶点就属于同一个强连通分量。
在强连通分量中,任意两个顶点都是相互可达的。一个有向图可以被分解为多个强连通分量,这些分量之间是相互独立的。
算法步骤
-
对有向图进行拓扑排序:首先,对给定的有向图进行拓扑排序,得到一个顶点序列,使得图中的每个有向边都从序列前面的顶点指向后面的顶点。
-
逆向图:创建原图的逆向图,即原图中的每条边方向都反转。
-
对逆向图进行拓扑排序:对逆向图进行拓扑排序,得到逆序的顶点序列。
-
构建强连通分量:将拓扑排序和逆序拓扑排序中,相同位置的顶点分为一组,这些顶点即为一个强连通分量。
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来找到最长路径。这通常涉及到更复杂的图构建和算法实现。