LangChain入门:Prompt模板与结构化输出详解
2026/7/24 18:23:50
python# Tarjan 算法求有向图的强连通分量def tarjan_scc(graph): n = len(graph) # 节点数量 dfn = [-1] * n # 时间戳数组,初始化为 -1 表示未访问 low = [0] * n # 最低可达时间戳数组 stack = [] # 辅助栈 in_stack = [False] * n # 标记节点是否在栈中 scc_list = [] # 存储所有强连通分量 time = 0 # 全局时间计数器 def dfs(u): nonlocal time # 初始化当前节点 dfn[u] = low[u] = time time += 1 stack.append(u) in_stack[u] = True # 遍历所有邻居节点 for v in graph[u]: if dfn[v] == -1: # 邻居未被访问 dfs(v) # 回溯时更新 low 值:取子树能到达的最早节点 low[u] = min(low[u], low[v]) elif in_stack[v]: # 邻居在栈中(找到回边) # 用邻居的 dfn 更新 low(注意不是 low[v]) low[u] = min(low[u], dfn[v]) # 如果 u 是强连通分量的根,则弹出该分量 if low[u] == dfn[u]: component = [] while True: w = stack.pop() in_stack[w] = False component.append(w) if w == u: break scc_list.append(component) # 对每个未访问的节点执行 DFS for i in range(n): if dfn[i] == -1: dfs(i) return scc_list# 测试示例if __name__ == "__main__": # 定义一个简单的有向图:0->1, 1->2, 2->0, 2->3, 3->4, 4->3 graph = [ [1], # 节点 0 指向 1 [2], # 节点 1 指向 2 [0, 3], # 节点 2 指向 0 和 3 [4], # 节点 3 指向 4 [3] # 节点 4 指向 3 ] sccs = tarjan_scc(graph) print("强连通分量:") for i, comp in enumerate(sccs): print(f"分量 {i}: {comp}")运行上述代码,输出结果为:强连通分量:分量 0: [2, 1, 0]分量 1: [4, 3]这表明节点 {0, 1, 2} 构成一个强连通分量(形成一个环),节点 {3, 4} 构成另一个强连通分量(双向连接)。## 四、算法细节深入分析### 4.1 low 值的更新规则理解 low 值的更新是掌握 Tarjan 算法的关键。在 DFS 遍历过程中,low[u] 的更新有两种情况:-树边:当从 u 访问未被访问的邻居 v 时,递归返回后,low[u] = min(low[u], low[v])。这是因为子树中的节点可以通过回溯到达更早的节点,从而影响 u 的 low 值。-回边:当邻居 v 已经在栈中时,说明 v 是 u 的祖先节点(或同辈节点),此时 low[u] = min(low[u], dfn[v])。注意这里用的是 dfn[v] 而不是 low[v],因为回边直接连接到 v,而 v 的 low 值可能受其子树影响,但我们只关心直接通过回边到达的节点。### 4.2 为什么使用 dfn 而不是 low在回边更新时使用 dfn[v] 而非 low[v] 是一个关键设计。假设 v 的 low 值小于其 dfn,如果使用 low[v],可能会导致 u 的 low 值错误地变小,从而破坏分量的划分。使用 dfn 保证了我们只考虑直接回边的影响,避免跨分量的干扰。### 4.3 栈的作用栈用于存储当前正在处理的节点。当 low[u] == dfn[u] 时,从栈中弹出直到 u 的所有节点,这些节点构成了一个强连通分量。栈的特性确保了同一个分量中的节点在栈中是连续的,并且弹出顺序与 DFS 结束顺序一致。## 五、高级应用与优化### 5.1 缩点与 DAG 构建强连通分量的一个重要应用是缩点。将每个强连通分量收缩成一个节点,原图就变成了一个有向无环图(DAG)。这在解决依赖关系、拓扑排序等问题中非常有用。下面的代码展示了如何将原始图缩点为 DAG:pythondef build_scc_dag(graph, scc_list): n = len(graph) # 为每个节点分配所属分量的编号 comp_id = [-1] * n for idx, comp in enumerate(scc_list): for node in comp: comp_id[node] = idx # 构建 DAG 的邻接表 dag = [set() for _ in range(len(scc_list))] for u in range(n): for v in graph[u]: if comp_id[u] != comp_id[v]: dag[comp_id[u]].add(comp_id[v]) # 将 set 转换为 list 方便使用 return [list(neighbors) for neighbors in dag]# 使用之前的图sccs = tarjan_scc(graph)dag = build_scc_dag(graph, sccs)print("缩点后的 DAG 邻接表:")for i, neighbors in enumerate(dag): print(f"分量 {i} -> {neighbors}")输出结果为:缩点后的 DAG 邻接表:分量 0 -> [1]分量 1 -> []这表示分量 0 指向分量 1,形成一条简单的链结构。### 5.2 算法复杂度分析Tarjan 算法的时间复杂度为O(V + E),其中 V 是顶点数,E 是边数。每个节点恰好被访问一次,每条边恰好被遍历一次,因此与图的规模成线性关系。空间复杂度为 O(V),主要用于存储 dfn、low、栈等数组。## 六、常见问题与调试技巧### 6.1 为什么我的算法少算了分量?常见原因是:- 忘记检查in_stack条件,错误地更新了 low 值。- 在回溯更新时使用了low[v]而不是dfn[v]。- 没有对未访问的节点都执行 DFS,导致孤立的分量被忽略。### 6.2 如何处理大图?对于稀疏图(边数远小于顶点数平方),使用邻接表存储可以节省内存。对于超大图,可以考虑用迭代 DFS 替代递归,避免栈溢出。## 七、总结Tarjan 算法是一种优雅而高效的强连通分量求解算法,其核心在于利用 DFS 和两个关键数组(dfn 和 low)来识别图中的环和可达性关系。通过本文的学习,我们从一个简单的概念出发,逐步深入到算法的实现细节和高级应用。掌握 Tarjan 算法不仅能帮助我们解决图论中的强连通问题,更为理解动态规划思想在树和图中的应用提供了重要范例。建议读者多动手实践,尝试在不同类型的图上运行算法,观察 low 值的变化过程,从而真正理解其内在机制。在实际编程竞赛和工程应用中,Tarjan 算法经常与缩点、拓扑排序、2-SAT 等问题结合,是每个程序员都应该掌握的经典算法之一。