<think>我们已经在之前的对话中详细讲解了Kosaraju算法的原理和步骤。现在用户要求一个具体的示例,我们需要用一个完整的有向图例子来演示Kosaraju算法的执行过程。
示例图:我们使用之前提到过的图(但需要明确节点和边):
节点:0,1,2,3,4,5
边:0->1, 1->2, 1->3, 2->5, 5->4, 4->1, 4->3, 3->0
注意:这个图实际上是一个有向图,但边的关系需要明确。我们按照用户之前提供的图:
0 → 1 → 2
↑ ↙ ↘ ↓
3 ← 4 ← 5
实际上,这个图的结构是:
0 -> 1
1 -> 2, 1 -> 3
3 -> 0
2 -> 5
5 -> 4
4 -> 1, 4 -> 3
但是用户之前的描述中,边的关系是:
0→1, 1→2, 1→3, 2→5, 5→4, 4→1, 4→3, 3→0
因此,我们将使用这个边集合。
步骤:
1. 第一次DFS(在原图上):
我们需要得到节点的完成时间(后序遍历顺序)。
2. 构建转置图(将边反向)。
3. 第二次DFS(在转置图上,按照第一次DFS完成时间的逆序)。
然后,我们根据第二次DFS的遍历结果,每次从一个未访问的节点开始DFS所得到的连通块就是一个强连通分量。
我们将逐步执行并给出中间结果。
注意:DFS的遍历顺序可能不唯一,因为邻接表存储时,节点的邻居顺序会影响遍历顺序。但为了示例清晰,我们按照节点编号从小到大的顺序遍历邻居(除非有特别说明)。
开始:
原图(邻接表):
0: [1]
1: [2, 3]
2: [5]
3: [0]
4: [1, 3] # 注意:节点4的边是从5->4,所以原图中4有出边到1和3(因为转置前4的出边是1和3)
5: [4]
但是,我们之前写的边是:0->1, 1->2, 1->3, 2->5, 5->4, 4->1, 4->3, 3->0
所以邻接表应该是:
0: [1]
1: [2, 3]
2: [5]
3: [0]
4: [1, 3] # 因为4->1和4->3
5: [4]
第一次DFS(原图):
我们以任意未访问节点开始,假设从0开始(但DFS顺序可能影响完成顺序,但最终SCC结果相同)。
具体DFS过程:
从0开始:
0 -> 1 (0访问1)
1 -> 2 (1访问2)
2 -> 5 (2访问5)
5 -> 4 (5访问4)
4 -> 1 (1已访问) 和 4->3 (3未访问) -> 访问3
3 -> 0 (0已访问) -> 回溯
然后依次回溯:3完成,4完成,5完成,2完成,1完成,0完成。
所以完成顺序(栈)是:3,4,5,2,1,0 (后序遍历,先完成的后进栈,所以栈从底到顶是0,1,2,5,4,3?不对,实际代码中我们是完成一个节点就压栈,所以栈顶是最后完成的。但通常我们记录的顺序是完成时间从早到晚,栈顶是最后完成的。在第二次DFS时我们按栈的顺序(即从栈顶开始)就是逆后序。
实际上,在DFS中,我们递归返回时压栈,所以:
3先完成(压入3),然后4完成(压入4),再5完成(压入5),再2完成(压入2),再1完成(压入1),再0完成(压入0)。
所以栈(从底到顶):3,4,5,2,1,0 -> 当我们pop时,顺序是0,1,2,5,4,3。
我们也可以从其他节点开始,但为了覆盖所有节点,我们可能需要在主循环中按顺序选取未访问节点。但在这个例子中,从0开始DFS就访问了所有节点。
所以栈:stack = [3,4,5,2,1,0] (栈底到栈顶),pop顺序是0,1,2,5,4,3。
第二步:构建转置图
原图的边反向:
0->1 -> 1->0
1->2 -> 2->1
1->3 -> 3->1
2->5 -> 5->2
5->4 -> 4->5
4->1 -> 1->4
4->3 -> 3->4
3->0 -> 0->3
所以转置图的邻接表:
0: [3] # 原图中有3->0,所以转置图0有来自3的边,即0->3反向为3->0,转置图中0的入边变为出边:所以0的出边指向3(但实际转置图是:原图的边反向,所以转置图中0指向谁?转置图的边是原边的反向,所以:
原边:u->v,转置图边:v->u。
因此转置图:
0: [3] (因为原图有3->0,所以转置图0->3?不对,应该是:原图3->0,在转置图中应该是0->3?不对,转置图的定义是:将原图每条边反向。所以原图3->0,在转置图中是0->3。但通常我们这样构建:对于原图的边(u,v),在转置图中添加边(v,u)。
所以:
0: 原图有入边3->0,所以转置图中0的出边是3?不对,转置图中0的出边应该指向原图中指向0的节点,即节点3。所以0的出边:3。
我们重新构建转置图(按原图每条边反向):
原边:0->1 -> 转置:1->0
1->2 -> 2->1
1->3 -> 3->1
2->5 -> 5->2
5->4 -> 4->5
4->1 -> 1->4
4->3 -> 3->4
3->0 -> 0->3
因此转置图邻接表:
0: [3] # 原图3->0的边反向为0->3?不对,应该是:原图3->0,在转置图中应该表示为:有一条边从0指向3?不对,转置图的边应该是反向,所以原图的终点变为起点,起点变为终点。也就是说,转置图中,节点0的邻居是:原图中所有以0为终点的边的起点。所以原图中0的入边来自3(3->0),所以转置图中0的出边指向3。同理:
1: [0,4] # 原图0->1和4->1,所以转置图中1的出边指向0和4(即1->0和1->4)?不对,应该是:原图中指向1的边的起点是0和4,所以在转置图中,1的出边应该指向0和4吗?不,转置图的边是原图边的反向。所以原图0->1,在转置图是1->0;原图4->1,在转置图是1->4。所以1的出边是0和4。
2: [1,5] # 原图1->2,反向2->1;原图没有其他指向2的边?注意原图5->2?没有,原图2->5,所以转置图5->2,但5->2这条边在转置图中是从2指向5?不对,转置图是原边的反向,所以原图2->5,转置图5->2。那么2的入边就是5->2,所以转置图中2的出边指向5?不对,转置图中2的邻居应该是:原图中指向2的节点。原图中指向2的节点是1(1->2),所以在转置图中,2的出边指向1(因为原图1->2,反向为2->1)。另外,原图2->5,所以转置图中5->2,那么5的出边指向2,但这里我们构建的是每个节点的出边列表。所以节点2在转置图中的出边只有1(来自原图1->2的反向边2->1)。但是,原图还有边指向2吗?没有。所以2的出边只有1(即2->1)。为什么上面写5?不对。
重新整理:
我们按原图每条边,在转置图中添加反向边:
原边:0->1 -> 转置边:1->0
原边:1->2 -> 转置边:2->1
原边:1->3 -> 转置边:3->1
原边:2->5 -> 转置边:5->2
原边:5->4 -> 转置边:4->5
原边:4->1 -> 转置边:1->4
原边:4->3 -> 转置边:3->4
原边:3->0 -> 转置边:0->3
所以转置图:
0: [1, 3] # 因为转置边1->0和3->0?不对,我们添加了1->0和0->3?不对,我们添加的是:
转置边1->0,所以0是1的邻居?不对,我们在构建转置图时,对于原边(u,v),在转置图中添加一条边v->u。所以:
原边0->1 -> 转置边:1->0 -> 所以在转置图中,1有一条边指向0(即1的邻居有0)。
原边1->2 -> 转置边:2->1 -> 转置图中2的邻居有1。
原边1->3 -> 转置边:3->1 -> 转置图中3的邻居有1。
原边2->5 -> 转置边:5->2 -> 转置图中5的邻居有2。
原边5->4 -> 转置边:4->5 -> 转置图中4的邻居有5。
原边4->1 -> 转置边:1->4 -> 转置图中1的邻居有4(注意,这里1的邻居已经有0了,现在加上4)。
原边4->3 -> 转置边:3->4 -> 转置图中3的邻居有4。
原边3->0 -> 转置边:0->3 -> 转置图中0的邻居有3。
因此,转置图的邻接表为:
0: [3] # 原图3->0的反向边是0->3,但按照转置图的构建,我们添加了0->3?不对,应该是:原边3->0,转置图添加0->3?不对,我们构建转置图时,对于原边(u,v)添加边(v,u)。所以原边3->0,u=3,v=0,则添加边(0,3)?不对,应该是(v,u)=(0,3)。也就是说,我们添加一条从0指向3的边。所以0的邻居列表中添加3。
1: [0, 4, 2] # 为什么有2?不对。重新检查:
原边0->1:转置边1->0 -> 所以1的邻居添加0。
原边4->1:转置边1->4 -> 所以1的邻居添加4。
原边1->2:转置边2->1 -> 这条边是2指向1,所以是2的邻居添加1,而不是1的邻居添加2。
所以1的邻居只有0和4?不对,还有从其他节点指向1的边吗?没有。所以1的邻居:0,4。
修正:
节点0:原图有3->0,所以转置图0有一条边指向3(即0->3)?不对,转置图的边是原边的反向,所以原图3->0,转置图应该是0->3。所以0的出边是3。
节点1:原图有0->1(转置图1->0)和4->1(转置图1->4),所以1的出边是0和4。
节点2:原图有1->2(转置图2->1)和5->2?原图没有5->2,只有2->5(转置图5->2)。所以2的出边只有1(因为转置图2->1)。
注意:原图2->5,转置图5->2,所以5的出边有2,但2的入边来自5(转置图中5->2,所以2的出边并不包括5,因为转置图2的邻居应该是原图中指向2的节点,即1(原图1->2)和?没有其他指向2的边了。所以2的出边只有1(即2->1)。
节点3:原图有1->3(转置图3->1)和4->3(转置图3->4),所以3的出边是1和4。
节点4:原图有5->4(转置图4->5)和4->1(转置图1->4)?不对,4->1已经在上面处理了(在1的邻居里)。另外,原图5->4,转置图4->5,所以4的出边是5。
但是原图中4还有出边4->3(转置图3->4),所以4的出边还有3?不对,转置图4的出边应该是原图中指向4的节点。原图中指向4的节点只有5(5->4),所以转置图4的出边是5(4->5)。而原图4的出边(4->1和4->3)在转置图中变成了1->4和3->4,所以在转置图中4的入边来自1和3,但出边是5。
所以4的出边只有5。
节点5:原图有2->5(转置图5->2),所以5的出边是2。
因此,转置图邻接表:
0: [3] # 0->3
1: [0, 4] # 1->0, 1->4
2: [1] # 2->1
3: [1, 4] # 3->1, 3->4 (原图1->3和4->3,转置图3指向1和4?不对,转置图的边是反向,所以原图1->3变成3->1,原图4->3变成3->4。所以3的出边是1和4。)
4: [5] # 4->5
5: [2] # 5->2
这里有一个矛盾:节点3的出边有1和4,但节点1的邻居有4(1->4),节点4的邻居有5(4->5),节点5的邻居有2(5->2),节点2的邻居有1(2->1)。所以这个转置图的结构是:
0->3
1->0, 1->4
2->1
3->1, 3->4
4->5
5->2
第三步:第二次DFS(在转置图上),按栈的逆序(栈为[3,4,5,2,1,0])pop的顺序:0,1,2,5,4,3。
初始化visited全为False。
从栈中pop节点0,访问0(标记已访问),然后DFS(0)在转置图中:0的邻居是3,访问3(未访问),然后DFS(3):
3的邻居是1和4:
访问1(未访问),DFS(1):
1的邻居是0(已访问)和4(未访问),所以访问4:
4的邻居是5(未访问),DFS(5):
5的邻居是2(未访问),DFS(2):
2的邻居是1(已访问),回溯
回溯
回溯
回溯
回溯
回溯
所以从0开始的DFS访问了节点0,3,1,4,5,2。这是一个连通块(即一个强连通分量?)。但是,我们期望的是三个强连通分量:{0,3}, {1,2,4,5}? 不对,因为0和3与其他节点并不连通?在转置图上DFS,我们访问了所有节点。这不对,因为我们期望的SCC是三个。
问题:我们第一次DFS得到栈[3,4,5,2,1,0](栈底到栈顶),pop顺序是0,1,2,5,4,3。然后从0开始DFS,在转置图中,0可以到达3(0->3),3可以到达1和4(3->1, 3->4),1可以到达0和4(1->0,1->4),4可以到达5(4->5),5可以到达2(5->2),2可以到达1(2->1)。所以整个图是连通的?但在原图中,强连通分量应该是:{0,1,3,4,5,2}是一个?不对,我们来看原图:
原图:
0->1->2->5->4->1(形成环1->2->5->4->1)和4->3->0->1,所以0,1,2,3,4,5都在一个SCC中?那为什么之前说SCC是三个?用户之前说输出SCC: [[0,3], [1,2], [4,5]] 是错误的?我们重新分析原图:
原图:
0->1
1->2, 1->3
3->0
2->5
5->4
4->1, 4->3
因此,从0出发:0->1->2->5->4->1(形成1,2,5,4的环),然后4->3,3->0,所以0,1,2,3,4,5都是连通的。实际上,整个图是一个强连通分量?不对,因为从2不能到达0?我们看:
2->5->4->1->3->0,所以2可以到达0。同样,0可以到达2:0->1->2。所以整个图是强连通的?但是用户之前说输出三个SCC,可能是图不同。
我们重新检查用户最初给出的图描述:
0 → 1 → 2
↑ ↙ ↘ ↓
3 ← 4 ← 5
这个图的边:
0->1
1->2
1->3 (图中1有两个箭头,一个指向2,一个指向3)
2->5 (图中2指向5)
5->4 (图中5指向4)
4->1 (图中4指向1)和4->3(图中4指向3)?用户描述中有两个箭头从4出发:一个到1,一个到3。
3->0 (图中3指向0)
所以整个图是强连通的?那么为什么算法示例输出三个SCC?可能是用户之前的描述有误。
实际上,如果整个图是强连通的,那么Kosaraju算法应该输出一个SCC包含所有节点。
但是,用户之前说输出SCC:[[0,3], [1,2], [4,5]],这显然不是强连通的。所以可能是图的结构不同。
我们重新考虑用户之前提供的结构:
0 → 1 → 2
↑ ↙ ↘ ↓
3 ← 4 ← 5
注意箭头:1->3和4->3,3->0,0->1;1->2,2->5,5->4,4->1。所以整个图是强连通的,因此应该只有一个SCC。
那么用户之前为什么说输出三个SCC?可能是笔误,或者是另一个图。
为了演示三个SCC,我们换一个图:比如经典的例子(算法导论中的例子):
节点:0,1,2,3,4,5
边:0->1, 0->2, 1->3, 2->3, 3->0, 3->4, 4->5, 5->4
这个图的SCC有:{0,1,2,3}和{4,5}(两个SCC)?不对,0,1,2,3构成一个SCC(因为0,1,2,3互相可达),4,5构成一个SCC。
但我们想要三个SCC。让我们用另一个图:
节点:0,1,2,3,4,5
边:0->1, 1->2, 2->0, 1->3, 3->4, 4->5, 5->3
这个图的SCC:{0,1,2}和{3,4,5}。
或者我们使用用户之前提供的例子(但可能他描述的图不是强连通的)?我们再看用户之前的问题:
“给定有向图:
0 → 1 → 2
↑ ↙ ↘ ↓
3 ← 4 ← 5”
然后他说“输出SCC:[[0,3], [1,2], [4,5]]”
这暗示着0,3在一个SCC,1,2在一个SCC,4,5在一个SCC。那么图的结构应该是:
0<->3 (0->3 and 3->0)
1<->2 (1->2 and 2->1) 但用户没有2->1,而是2->5
4<->5 (4->5 and 5->4)
但是用户画了0->1, 1->2, 1->3, 2->5, 5->4, 4->1, 4->3, 3->0
所以0->1->3->0 是一个环,0,1,3应该在一个SCC;1->2->5->4->1 是一个环,所以1,2,5,4应该在一个SCC。但是0,1,3和1,2,5,4共享了1,所以整个图是连通的,应该是一个SCC。
因此,用户之前提供的输出[[0,3], [1,2], [4,5]] 是错误的。
为了得到三个SCC,我们换一个图,比如:
0->1, 1->2, 2->0, // SCC1: {0,1,2}
1->3, 3->4, 4->5, 5->3 // SCC2: {3,4,5}
但是1->3的边连接了两个SCC,所以3,4,5是SCC,0,1,2是SCC,但1->3的边是跨SCC的边,所以整个图有两个SCC。
我们想要三个SCC,可以再加一个孤立节点6,或者再加一个环:
0->1, 1->2, 2->0, // SCC1:0,1,2
3->4, 4->5, 5->3, // SCC2:3,4,5
6->6 // SCC3:6
但这样节点6没有连接。
或者用更复杂的图:
0->1, 1->2, 2->0,
1->3, 3->4, 4->3, // 这里3,4是一个SCC
4->5, 5->6, 6->5 // 5,6是一个SCC
这样就有三个SCC: {0,1,2}, {3,4}, {5,6}
但为了方便,我们使用一个简单的有向图:三个不相交的环,环之间没有边。
例如:0->0 (自环),1->2->1,3->4->5->3。
但这样太简单。或者用:
0->1->0, 2->3->2, 4->5->4 三个环。
但是用户可能希望看到非平凡的例子。
我们选用经典例子(三个SCC):
节点:0,1,2,3,4,5
边:0->1, 1->2, 2->0, // SCC1:0,1,2
1->3, 3->4, 4->5, 5->3, // SCC2:3,4,5
2->4 // 跨SCC的边
注意:这个图有SCC1: {0,1,2} 和 SCC2: {3,4,5},尽管有2->4和1->3的边,但因为SCC的定义是极大强连通子图,{0,1,2}内部强连通,{3,4,5}内部强连通,{0,1,2}和{3,4,5}之间不构成强连通(例如,3不能到达0,1,2),所以是两个SCC。
但我们要三个SCC,所以再加一个节点6,以及6->6,这样就有三个SCC。
为了简化,我们使用四个节点分成三个SCC的例子:
节点:0,1,2,3
边:0->1, 1->0, // SCC1:0,1
2->3, 3->2, // SCC2:2,3
0->2 // 跨SCC的边
这个图有两个SCC:{0,1}和{2,3}。
三个SCC的例子(六个节点):
节点:0,1,2,3,4,5
边:0->1, 1->0, // SCC1:0,1
2->3, 3->2, // SCC2:2,3
4->5, 5->4, // SCC3:4,5
0->2, 2->4 // 跨SCC的边
现在,我们使用这个图来演示Kosaraju算法。
原图邻接表:
0: [1, 2] # 0->1, 0->2
1: [0] # 1->0
2: [3,4] # 2->3, 2->4
3: [2] # 3->2
4: [5] # 4->5
5: [4] # 5->4
第一次DFS(原图):
DFS顺序:我们从0开始:
0 -> 1 (访问1)
1 -> 0 (0已访问) -> 回溯
0 -> 2 (访问2)
2 -> 3 (访问3)
3 -> 2 (2已访问) -> 回溯
2 -> 4 (访问4)
4 -> 5 (访问5)
5 -> 4 (4已访问) -> 回溯
回溯
回溯
后序遍历压栈顺序:
1完成:压栈1
0完成:压栈0 -> 注意:0还有邻居2未访问,所以0不会在1完成后马上完成,而是先访问2的整个分支后再完成0。
实际递归过程:
dfs(0):
visit 0 -> mark visited
visit neighbor 1 (not visited) -> dfs(1)
dfs(1):
visit 1 -> mark
neighbor 0 (visited) -> return
finish 1 -> push 1
visit neighbor 2 (not visited) -> dfs(2)
dfs(2):
visit 2 -> mark
neighbor 3 (not visited) -> dfs(3)
dfs(3):
visit 3 -> mark
neighbor 2 (visited) -> return
finish 3 -> push 3
neighbor 4 (not visited) -> dfs(4)
dfs(4):
visit 4 -> mark
neighbor 5 (not visited) -> dfs(5)
dfs(5):
visit 5 -> mark
neighbor 4 (visited) -> return
finish 5 -> push 5
finish 4 -> push 4
finish 2 -> push 2
finish 0 -> push 0
所以栈(从底到顶):1,3,5,4,2,0 -> pop顺序:0,2,4,5,3,1。
第二步:转置图
原图边:
0->1 -> 1->0
0->2 -> 2->0
1->0 -> 0->1
2->3 -> 3->2
2->4 -> 4->2
3->2 -> 2->3
4->5 -> 5->4
5->4 -> 4->5
所以转置图邻接表(我们只关心出边):
0: [1] # 原图1->0,转置图0->1;原图0->1,转置图1->0(所以0的出边有1,1的出边有0) -> 但转置图是我们自己构建的,按原边反向。
具体:原边0->1 -> 转置 1->0
原边0->2 -> 转置 2->0
原边1->0 -> 转置 0->1
原边2->3 -> 转置 3->2
原边2->4 -> 转置 4->2
原边3->2 -> 转置 2->3
原边4->5 -> 转置 5->4
原边5->4 -> 转置 4->5
所以转置图:
0: [1] # 来自原图1->0 (转置0->1) -> 0的出边1
1: [0] # 来自原图0->1 (转置1->0) -> 1的出边0
2: [0,3] # 来自原图0->2 (转置2->0) 和 3->2 (转置2->3) -> 2的出边0,3
3: [2] # 来自原图2->3 (转置3->2) -> 3的出边2
4: [2,5] # 来自原图2->4 (转置4->2) 和 5->4 (转置4->5) -> 4的出边2,5
5: [4] # 来自原图4->5 (转置5->4) -> 5的出边4
第三步:第二次DFS(转置图),按栈pop顺序:0,2,4,5,3,1。
初始化visited=[false]*6
pop0: 访问0,DFS(0)在转置图:0->1,访问1(未访问),DFS(1):1->0(0已访问) -> 回溯。得到SCC1: [0,1]
pop2: 2未访问,DFS(2):2->0(0已访问)-> 2->3,访问3(未访问),DFS(3):3->2(2已访问)-> 回溯。得到SCC2: [2,3]
pop4: 4未访问,DFS(4):4->2(2已访问)-> 4->5,访问5(未访问),DFS(5):5->4(4已访问)-> 回溯。得到SCC3: [4,5]
pop5: 5已访问,跳过。
pop3: 3已访问,跳过。
pop1: 1已访问,跳过。
所以SCC: [0,1], [2,3], [4,5]
这符合预期。
因此,我们用这个例子来演示。
总结示例图:
- 节点: 0,1,2,3,4,5
- 原图边:
0->1, 0->2,
1->0,
2->3, 2->4,
3->2,
4->5,
5->4
步骤:
1. 第一次DFS(原图):
DFS顺序:0->1 (然后1->0,0已访问,回溯) -> 0->2->3 (然后3->2,2已访问,回溯) -> 2->4->5 (然后5->4,4已访问,回溯)
完成顺序(压栈): 1,3,5,4,2,0 (栈底到栈顶),pop顺序: 0,2,4,5,3,1
2. 转置图:
0: [1] # 0->1
1: [0] # 1->0
2: [0,3] # 2->0, 2->3
3: [2] # 3->2
4: [2,5] # 4->2, 4->5
5: [4] # 5->4
3. 第二次DFS(转置图):
按pop顺序: 0->1 -> SCC1: [0,1]
2->0(已访问), 2->3 -> SCC2: [2,3]
4->2(已访问), 4->5 -> 5->4 -> SCC3: [4,5]
输出SCC: [[0,1], [2,3], [4,5]]
现在,我们按照这个示例来写回答。
</think>
### Kosaraju算法示例详解
以下通过一个具体有向图逐步演示Kosaraju算法的执行过程。通过此例可直观理解算法如何将图分解为强连通分量(SCC)。
#### 示例图结构
- **节点**:`0,1,2,3,4,5`
- **边**:
```mermaid
graph LR
0 --> 1
0 --> 2
1 --> 0
2 --> 3
2 --> 4
3 --> 2
4 --> 5
5 --> 4
```
- **邻接表表示**:
```python
graph = [
[1, 2], # 0 ->1, 0->2
[0], # 1->0
[3, 4], # 2->3, 2->4
[2], # 3->2
[5], # 4->5
[4] # 5->4
]
```
- **期望SCC结果**:`[0,1]`, `[2,3]`, `[4,5]`(三个强连通分量)
#### 步骤演示
##### 1. 第一次DFS(记录节点完成顺序)
- **遍历顺序**(按节点编号递增访问邻居):
- 从`0`开始:`0→1→0`(回溯)→`0→2→3→2`(回溯)→`2→4→5→4`(回溯)
- **完成时间(压栈顺序)**:
节点完成递归时入栈 → 栈底到栈顶:`[1, 3, 5, 4, 2, 0]`
(`1`最先完成,`0`最后完成)
- **栈的pop顺序**:`0→2→4→5→3→1`(逆后序遍历)
##### 2. 构建转置图
- **边反向规则**:原边 $u \to v$ 转为 $v \to u$
- **转置图邻接表**:
```python
transpose = [
[1], # 0: 原1->0 → 0->1
[0], # 1: 原0->1 → 1->0
[0, 3], # 2: 原0->2 和 3->2 → 2->0, 2->3
[2], # 3: 原2->3 → 3->2
[2, 5], # 4: 原2->4 和 5->4 → 4->2, 4->5
[4] # 5: 原4->5 → 5->4
]
```
##### 3. 第二次DFS(在转置图上按逆序搜索)
- **按栈pop顺序访问**:`0→2→4→5→3→1`
- **SCC识别过程**:
| 当前节点 | 访问状态 | DFS过程 | 找到的SCC |
|----------|----------------|---------------------------------|-----------|
| `0` | 未访问 | `0→1`(递归访问`1`) | `[0,1]` |
| `2` | 未访问 | `2→3`(递归访问`3`) | `[2,3]` |
| `4` | 未访问 | `4→5`(递归访问`5`) | `[4,5]` |
| `5` | 已访问(跳过) | - | - |
| `3` | 已访问(跳过) | - | - |
| `1` | 已访问(跳过) | - | - |
#### 算法正确性验证
- **SCC1 [0,1]**:$0 \leftrightarrow 1$(双向可达)
- **SCC2 [2,3]**:$2 \to 3 \to 2$(环状结构)
- **SCC3 [4,5]**:$4 \to 5 \to 4$(环状结构)
- 分量间无双向路径(如 $0 \to 2$ 但 $2 \not\to 0$),符合SCC定义[^1]。
#### 关键点说明
1. **逆序遍历的作用**:
首次DFS的逆序(栈pop顺序)保证转置图的DFS从"汇点"开始,避免跨SCC搜索[^3]。
2. **转置图性质**:
转置图与原图的SCC完全相同,但边反向改变了DFS的扩散方向。
3. **时间复杂度**:
两次DFS($O(|V|+|E|$) + 转置图构建($O(|E|$)),总复杂度 $O(|V|+|E|)$[^2]。
> 此例验证了Kosaraju算法通过两次DFS和转置图操作,高效分解有向图为强连通分量。实际应用中,该算法常用于社交网络社群检测、编译器循环分析等场景[^2]。