想象你刚被任命为一家智能工厂的质检主管。设计图上有几十个工位,箭头标着“必须先等它完成”。开工前的头等大事,是检查这些箭头之间有没有致命的循环:A工位要等B的零件,B要等C的原料,C却坚持要等A的加工件——三个工位互相掣肘,整条流水线永远无法运转。这类“循环依赖”在计算机科学家眼中,就是有向图里藏了一个环。 好消息是,检查一张图有没有环,不需要肉眼盯着箭头一步步数。有两种经典办法:第一种是派一名巡检员,带白、灰、黑三色粉笔,沿箭头边走边标记,一旦发现前方有个工位正被涂成“灰色”——人还在半路上呢——就说明这条路绕回了自己。第二种是从源头开工:先把所有不需要等待别人的工位排进日程,每完工一个,就解锁一批新的可开工工位,看在最后能不能把整个车间都排进去。 这件事的专业名字叫“有向无环图(DAG)测试”。在AI的因果推断里,它是每个因果模型的入场券:只有确认图上没有“因果绕回自己”的环路,后续的效应估计、可信决策才有了立足之地。

岗位关联

图测试是因果推断与可信AI系统的守门员。掌握DFS着色法与Kahn拓扑排序,你就能快速识破循环依赖,确保因果模型建立在合法图结构之上,这是AI从业者的基本功。

目录
  1. 可视化演示
  2. DAG基本概念
  3. 环路与依赖
  4. DFS三色标记法
  5. Kahn入度消减
  6. 双法对比
  7. 因果图校验
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示有向无环图检测两大算法:DFS着色法(白→灰→黑三色标记回溯检测环)、Kahn拓扑排序(入度消减逐层移除节点)、DAG与有环图对比验证,通过步进式交互理解环路检测原理。

本节配有交互动效(进入视口后自动加载,约 3KB)。

DFS着色法:三色标记环路检测 灰色标记:节点正在递归访问中 黑色标记:节点及后继已完成 if edge(u,v) && color[v]==GRAY → cycle 遇到灰色后继节点则存在环路

DAG基本概念

把新建工厂的图纸铺在桌上,你会看到许多方框,方框之间连着带箭头的线。方框是工位,箭头代表“必须等它先完工”。例如“和面”指向“擀皮”,擀皮工位就得等和面工位送出面团来。这样一张处处都有先后次序的图纸,就是图论里的一张“有向图”。

图纸上的有向图并不总能健康成立。假如存在一条路线,让你从某个工位出发,沿着箭头走了一圈,最后又绕回这个工位——比如A等B,B等C,C又要等A——整条流水线就会陷入“谁也不能先开工”的僵局。这种没有回路的健康图纸,就叫“有向无环图”,缩写为DAG。英文全称是Directed Acyclic Graph,核心品格就两个字:无环。

形式化地,DAG由两个集合构成:顶点集V装着图中所有节点,有向边集E装着所有带方向的箭头。每条有向边(u,v)都表示u必须先于v完成。“无环”则要求图中不存在任何一条长度大于等于1、从某个顶点出发又回到该顶点的路径。这样一张图天然对应数学中的“偏序关系”:部分节点之间有先后顺序,但并非任意两个节点都需要直接比较——就像“和面”和“包饺子”有关系,“调馅”和“包饺子”也有关系,但“和面”与“调馅”之间未必有直接箭头。

回到那张车间图纸:只要你能在图里找出一条绕回起点的回路,这张图就算不上DAG,流水线就得返工。判断一个图是否是DAG,正是“图测试”这门手艺做的事。而在因果推断里,这份图纸上的“工位”会变成变量,“箭头”会变成因果假设——无环,是因果模型合法性的底线。

∄ v0,v1,…,vk∈V,(vi,vi+1)∈E,  v0=vk,  k≥1\nexists\, v_0, v_1, \ldots, v_k \in V,\quad (v_i, v_{i+1}) \in E,\; v_0 = v_k,\; k \ge 1
对小白:这个公式说的是——图中不存在一条“从某节点出发、沿箭头一步一步走、最后又能回到自己”的路径。只要找不到这样的路径,图就是无环的。
  1. 节点序列沿箭头方向依次经过的节点集合,记录了一条完整路径。
    v0,v1,…,vk∈Vv_0, v_1, \ldots, v_k \in V
  2. 邻接合法路径上任意相邻两步都必须是图纸上真实存在的箭头,即都属于边集E。
    (vi,vi+1)∈E(v_i, v_{i+1}) \in E
  3. 首尾相等路径的终点和起点是同一个节点,说明箭头绕了一个圈。
    v0=vkv_0 = v_k
  4. 排除平凡要求至少走过一条边,否则把“原地不动”也算成环就没有意义了。
    k≥1k \ge 1

环路与依赖

你随手抓起一支红笔,在图纸上圈出了一组工位:油漆工位要等装配完成,装配工位要等冲压完成,冲压工位却又贴着“待油漆件到位”的便签。三个工位首尾相扣,谁都在等别人先动,这就是有向图里的“环”。在工厂里这叫死锁,在因果推断里这叫循环论证。

环的正式定义很简洁:从某个节点出发,沿着有向边经过一个或多个节点,最终回到出发点。一旦环出现,依赖的先后顺序就被打破了。对因果推断来说,环意味着某个变量经由一串因果箭头变成了自己的原因——这种“自我循环”的假设会让后续所有定量计算都失去根基。因果图的要求是原因发生在结果之前,环却让原因同时是结果。

因此,检测环的价值远超“让图纸好看”。在因果图建模中,每个节点代表一个变量,每条边代表一条因果假设。如果图中藏有环,说明模型内部自相矛盾——一个效应项既在原因项之前,又在原因项之后。图测试正是这层防线的哨兵:在模型进入因果效应估计之前,先断言图结构是否合法。这也是为什么DAG测试会被安排在因果推断流程的最前端。

红笔圈出的三角互等,就是典型的有向环。图测试的任务,就是把这类“三角债”从图纸里揪出来,保证每条依赖链都干干净净地延伸到终点。一旦发现环,就要回到建模阶段,修正或删除导致闭环的边,直到图重新恢复无环的合法状态。

∃  v0→v1→⋯→vk→v0\exists\; v_0 \to v_1 \to \cdots \to v_k \to v_0
对小白:这就是“环”的图示语言——从起点出发,箭头一串走完,最后一根箭头又指回起点,形成首尾相接的闭环。
  1. 出发点环路上的起点节点,整条环从它开始。
    v0v_0
  2. 中间链路经过若干有向边到达的节点序列,每一步都严格按箭头方向走。
    v1→⋯→vkv_1 \to \cdots \to v_k
  3. 闭合最后一条边从终点指回起点,路径闭合,环宣告成立。
    →v0\to v_0

DFS三色标记法

巡检员李工带着白、灰、黑三色粉笔出发了。他沿箭头走,每进入一个工位就涂上灰色——表示“我正走在通往它的路上”;如果确认从这条支路再走不出任何东西,就把工位涂成黑色——“这条路彻底查清了,安全”;还没碰过的工位则保持白色。这就是深度优先搜索(DFS)的探路节奏:一条路走到黑,撞了墙再回头。

DFS的核心是“递归回溯”。从某节点u进入时,u被涂灰;然后逐个检查u指向的下游v:v是白色就递归进入,v是灰色就立刻报警,v是黑色就跳过。全部下游查完,u涂黑并返回上一步。这种“进入标灰、返回标黑”的节奏,与递归函数的调用栈完美对应。

三色标记的精妙之处在于“灰色”这一状态。v是灰色意味着v此刻还在递归栈中——也就是我们正沿着某条路径走到v,而这条路还没走完。这时u指向v,等于一条路径从u出发绕了一圈又回到了v所在的路上,构成一条“回边”。回边等价于环,一旦出现,DFS立即返回“有环”。如果v是黑色,则v的整棵子树都查过且安全,这条u→v边只是交叉指向历史节点,完全合法。

李工最后在C工位发现:C的箭头指向A,而A墙上的粉笔还是灰色——自己正站在绕回起点的路上。这一记回边,就是环的罪证。图测试的第一把利剑,就这样出鞘了。值得注意的是,三色标记法检测环的关键窗口期是“u尚未出栈”的这段时间,一旦错过,回边就会被误判成合法交叉边。

visit(u):  color[u]=gray;∀v∈adj(u),  color[v]=gray⇒∃ cycle\text{visit}(u):\; color[u]=\text{gray};\quad \forall v \in adj(u),\; color[v]=\text{gray} \Rightarrow \exists\, \text{cycle}
对小白:这段公式用大白话说就是——每访问一个节点先把它标灰;如果它指向的下游节点里有一个也是灰色,就说明绕回了“正在走的路上”,图中存在环。
  1. 进入标灰递归函数进入节点u的瞬间,把u标记为灰色,表示u正在当前探索路径上。
    color[u]=graycolor[u]=\text{gray}
  2. 遍历下游逐个访问u的所有邻接节点,也就是图纸上从u出发指向的所有工位。
    ∀v∈adj(u)\forall v \in adj(u)
  3. 灰遇灰判环若v也是灰色,说明v所在路径尚未走完,而u的箭头却连回了v,此即回边、此即环。
    color[v]=gray⇒∃ cyclecolor[v]=\text{gray} \Rightarrow \exists\, \text{cycle}
  4. 返回标黑所有下游探查完毕且无回边,u才涂黑,表示整个子树安全。
    color[u]=blackcolor[u]=\text{black}

Kahn入度消减

另一位质检员王工换了个思路:他不钻进箭头迷宫,而是站在车间门口看“谁最先能开工”。每个工位都挂着一块计数牌,上面写着还有几个上游没完工——这个数字在算法里叫“入度”。入度为0的工位,没有等待对象,立刻可以排上日程。入度越大,说明要等的人越多,越不能着急。

王工手里有一个待办队列。他先把所有入度为0的工位全部入队,然后逐个弹出:弹出一个工位,就把它完工,同时把它指向的所有下游工位的入度牌各减1。谁家的入度牌归零了,就说明它的全部上游都已完成,于是入队等待派工。如此反复,直到队列变空。整个过程就像多米诺骨牌依次倒下:每倒一块,就为下一块清空一个障碍。

最终统计已派工序列的长度。如果长度恰好等于节点总数,说明每个节点都被排进了一条合法的先后链条,图是DAG;如果长度小于节点总数,剩下那些入度永远降不到0的节点,就处在互相等待的环里——它们被环锁死,谁也排不进去。因此Kahn算法本身就是天然环检测器:序列长度小于节点数等价于有环。此外,它产出的拓扑序列还自带执行顺序,可直接用于任务调度场景。

王工盯着计数牌,发现油漆、装配、冲压三个工位的入度牌永远停在1——它们互相捧着对方的产品发呆。Kahn算法不需要钻进迷宫,只凭“永远派不完的工单”就断定:这不是DAG。这个方法还有个额外福利:即便图有环,它给出的部分序列也仍然保持合法的先后关系,能为人工修正提供线索。

indeg(v)=∣{u∈V:(u,v)∈E}∣,len(order)=∣V∣  ⟺  DAGindeg(v) = |\{u \in V : (u,v) \in E\}|, \quad len(order) = |V| \iff \text{DAG}
对小白:第一个等号告诉你怎么数“入度”——就是数有几个箭头指向某个节点。第二个等号是说:排出来的总人数等于总节点数,才说明图无环。
  1. 入度统计记录每个节点被多少条有向边指向,即它的直接上游数量。
    indeg(v)=∣{u∈V:(u,v)∈E}∣indeg(v) = |\{u \in V : (u,v) \in E\}|
  2. 零入度入队所有入度为0的节点进入待处理队列,它们是“无需等待”的开端。
    indeg(v)=0⇒enqueue(v)indeg(v) = 0 \Rightarrow \text{enqueue}(v)
  3. 出队消减每个节点出队后,将其下游节点的入度各减1,相当于“解锁”一道工序。
    ∀(v,w)∈E:  indeg(w)−=1\forall (v,w) \in E:\; indeg(w) \mathrel{-}= 1
  4. 长度校验最终序列长度等于节点总数,即所有节点都成功入队,图才是DAG。
    len(order)=∣V∣  ⟺  DAGlen(order) = |V| \iff \text{DAG}

双法对比

李工的巡检法和王工的派工法,是两种性格截然不同的质检风格。李工喜欢一杆子插到底,适合在现场快速锁定可疑小圈子;王工则擅长从源头层层推进,顺手就把整条流水线的排班表列了出来——这是大型车间最需要的产出。两者都能回答“有没有环”,但各自适合的场景并不相同。

从理论复杂度看,两把剑不分伯仲:它们都只用O(|V|+|E|)的线性时间完成检测。每个节点至多被涂一次色或入一次队,每条边至多被检查一次或消减一次。对于百万节点规模的大型依赖图,两者的耗时都在可接受范围内。这也是它们成为工业界主流环检测算法的根本原因。

差异在工程细节上显现:DFS三色标记最自然的写法是递归,代码简洁直觉清晰,但图深度极大时(比如十万层依赖链)可能触发栈溢出;Kahn则完全用队列迭代,不存在递归深度风险,而且天然产出一个合法的拓扑序。因此选型建议很直接:只问“有没有环”且图规模较小,用DFS;要“排序+检测”二合一,或图深度不可控,用Kahn。如果执意用DFS应对深图,记得改为显式栈的迭代版本。

李工和王工的考核表上,两人都以线性时间的成绩过关。车间主管最终给出的建议是:小作坊请李工,大厂房请王工——两张王牌都考不倒,才是合格的图测试。实际工程中,许多框架甚至两手准备:先用Kahn求拓扑序列,再用DFS二次校验异常边,互为印证。

O(∣V∣+∣E∣)O(|V| + |E|)
对小白:O(V+E)表示算法耗时随“节点数加边数”线性增长。节点翻倍,耗时大约也翻倍,不会突然爆炸。
  1. 节点处理每个节点在两种算法中都只入一次队或涂一次色,因此贡献 |V| 的工作量。
    O(∣V∣)O(|V|)
  2. 边处理DFS逐条检查邻接边,Kahn逐条消减出边,每条边只处理一次,贡献 |E| 的工作量。
    O(∣E∣)O(|E|)
  3. 线性合并两部分相加,总复杂度随图规模线性增长,是这类问题的最优量级。
    O(∣V∣+∣E∣)O(|V| + |E|)

因果图校验

现在回到更大的舞台:因果推断。这里的“工位”换成了变量——药量、血压、康复时间;“箭头”换成了因果假设——药量影响血压,血压影响康复。如果因果图中出现“药量→血压→用药习惯→药量”这类环,就等于在说:一个效应通过一系列原因影响了自己,这在因果推断里完全不成立。原因必须先于结果,这是因果关系的铁律。

所以DAG测试是因果图建模的前置校验步骤。无论因果图来自领域专家手工绘制,还是来自PC、LiNGAM等因果结构学习算法,第一步都要做环检测。只有通过检测的无环图,才允许进入因果效应估计、后门调整、工具变量等后续流程。这一步不是可选项,而是因果推断流程的强制性入场券。

为什么无环如此重要?因为在DAG上,所有变量的联合概率分布可以分解为每个变量在其父节点给定条件下的条件概率之积。这个分解依赖一个合法的拓扑顺序:每个变量只依赖它的直接原因,而不依赖它的后代。有环的图找不到这样的顺序,分解便无从谈起。可信AI系统中的因果模块,之所以能在决策时给出“干预某变量会导致什么”的回答,正是因为有DAG测试这道闸门守住了结构合法性。

图纸上盖下“通过DAG检测”的印章,因果推断的流水线才敢开机。从三色粉笔到入度队列,图测试用最简单的工具,守护着AI世界里最根本的因果秩序。这也是因果推断从理论走向可信AI系统落地时,最不可省略的一步基础校验。

P(x1,…,xn)=∏i=1nP(xi∣pa(xi))P(x_1,\dots,x_n)=\prod_{i=1}^{n} P(x_i \mid pa(x_i))
对小白:整个式子说的是——把所有变量的联合概率,拆成“每个变量在已知其直接原因后的条件概率”的连乘。只在无环图上,这种拆分才有意义。
  1. 联合分布等号左边是所有变量的联合概率,描述整个系统所有取值组合的可能性。
    P(x1,…,xn)P(x_1,\dots,x_n)
  2. 父节点集合pa(x_i)是x_i在因果图中的直接父节点,即直接影响x_i的原因集合。
    pa(xi)pa(x_i)
  3. 条件概率连乘每个变量的概率在给定其父节点后相乘,即链条式分解。
    ∏i=1nP(xi∣pa(xi))\prod_{i=1}^{n} P(x_i \mid pa(x_i))
  4. 合法性前提该分解公式仅在DAG上成立;有环时不存在这样的拓扑顺序,分解失效。
    DAG validity⇒factorization\text{DAG validity} \Rightarrow \text{factorization}

AI 应用场景

因果门诊——新药因果效应评估前的结构体检

阶段:数据建模 / 结构校验

解决的问题:传统做法靠专家手工绘制因果图后直接跑模型,一个隐藏环就能让效应估计完全失真。AI用DAG测试自动扫描专家图与学习图,发现环后给出预警,避免下游分析建立在自相矛盾的因果假设上。

具体案例:某医疗团队分析“血压→用药剂量→不良反应”数据,发现自动生成的结构图中含“不良反应→用药剂量”反向边从而成环。他们用DFS三色标记定位回边,删除该边后重新检测,才得到合法DAG,随后才开始估计药物效应。整个过程从“模型跑出荒谬结果”转变为“基建校验先行”。

MLOps任务流水线的死锁防线

阶段:模型训练与部署编排

解决的问题:训练、评估、部署任务构成复杂依赖流,若配置中的任务互相依赖成环,调度器会永远等待。传统排错靠人工盯着任务状态面板,效率低下。

具体案例:某平台工程师将Kahn拓扑排序嵌入CI流水线:每次任务配置变更先跑一遍检测,若拓扑序列长度小于节点数,立即拒绝发布并给出剩余节点清单。上线后循环依赖事故从每月数次降到零,发布耗时也因早期预警而大幅缩短。

Python 演示

python52 行
"""DAG图测试: DFS三色标记 + Kahn入度消减"""from collections import deque  def dfs_has_cycle(graph):    """DFS三色标记法检测有向图是否存在环. 0=白, 1=灰, 2=黑"""    color = {node: 0 for node in graph}     def visit(node):        color[node] = 1          # 进入递归, 标记灰色        for nxt in graph[node]:            if color[nxt] == 1:  # 下游也在递归栈中 -> 回边                return True            if color[nxt] == 0 and visit(nxt):                return True        color[node] = 2          # 子树探查完毕, 标记黑色        return False     return any(color[node] == 0 and visit(node) for node in graph)  def kahn_topo(graph):    """Kahn入度消减: 返回拓扑序列, 长度小于节点数说明有环"""    indeg = {node: 0 for node in graph}    for node in graph:        for nxt in graph[node]:            indeg[nxt] += 1     queue = deque([n for n in graph if indeg[n] == 0])    order = []    while queue:        cur = queue.popleft()        order.append(cur)        for nxt in graph[cur]:            indeg[nxt] -= 1            if indeg[nxt] == 0:                queue.append(nxt)    return order  if __name__ == "__main__":    dag = {"A": ["B", "C"], "B": ["D"], "C": ["D"], "D": []}    cyc = {"A": ["B"], "B": ["C"], "C": ["A"]}     for name, g in [("acyclic", dag), ("cyclic", cyc)]:        order = kahn_topo(g)        has_cycle = dfs_has_cycle(g)        print(f"[{name}] DFS has cycle: {has_cycle}, Kahn order: {order}")        if len(order) == len(g) and not has_cycle:            print("  -> PASS: the graph is a DAG")        else:            print("  -> FAIL: cycle found, not a DAG")
目的:用DFS三色标记法与Kahn入度消减法分别检测两张有向图,互为印证并输出是否DAG的结论。
  1. color[node] = 1进入节点先涂灰,表示该节点正在递归栈里。
  2. if color[nxt] == 1:下游节点也是灰色,说明路径绕回了自己,立即判定有环。
  3. color[node] = 2子树全部探完,节点涂黑并返回False。
  4. indeg[nxt] += 1为每条边(u,v)的终点v累加入度,即统计它的上游依赖数。
  5. queue = deque([n for n in graph if indeg[n] == 0])所有入度为0的节点入队,它们无需等待。
  6. order.append(cur)出队即表示该工序可排入合法先后序列。
  7. indeg[nxt] -= 1当前节点完工,为其下游节点解除一个等待依赖。
  8. len(order) == len(g)只有序列长度等于节点数,才能确认图中无环、是DAG。

常见误区

误区凡是提到DFS检测环,我就觉得必须写递归函数,否则没法实现三色标记。

正解递归只是DFS的一种表达方式,本质是栈。用显式栈同样能实现DFS,只是需要自己手工维护“正在访问”的节点集合,在入栈时标灰、出栈时标黑。真正关键的是“回边判定”这套语义,与递归写法没有必然绑定。深度极大的图出现栈溢出时,迭代版DFS才是正解。

为何常见:教科书几乎都以递归形式展示DFS,学习者接触的代码范式单一,误以为递归是DFS的唯一形态。

误区教材说拓扑排序只适用于DAG,所以我以为有环图上运行Kahn算法会直接报错或者没有输出。

正解Kahn算法在有环图上也能正常运行,只是序列长度小于节点总数。它不仅能通过“长度不足”暴露环的存在,还顺带把无环部分的偏序关系排了出来。这恰恰是它作为环检测器的价值所在——用“排不完”来宣告有环。

为何常见:把“只适用于DAG”理解成了“遇到非DAG会崩溃”,忽略了算法本身是自然运行并在结果长度上暴露问题的。

误区黑色不就是“访问过”吗?那白色也是“没访问过”,两色标识访问状态不就行了吗?灰色感觉多余。

正解灰色承载的是“正在递归栈中”这一关键状态。回边检测的本质是:当前节点指向了一个仍在栈中的节点,路径绕回了自己。若只用黑白色,无法区分“曾经来过”与“此刻正在路上”——B→A后再遇到A是合法的交叉边,不是环。树遍历可以用两色,因为有向图的DFS必须三色。

为何常见:把图的DFS和树的遍历混为一谈。树的祖先节点不会再次出现,图的节点却可能被多条路径重复引用。

误区为了让排序结果看起来更“重要”,我觉得应该先把入度大的节点排到前面,它们依赖多、地位关键。

正解Kahn算法的铁律是“零入度优先”——只有当前没有任何上游依赖的节点才能出队。入度大的节点恰恰要等所有上游都消减完毕、入度降为0之后才有资格入队。优先处理入度大的节点只会让还没就绪的工序提前开工,破坏“依赖优先”的语义。

为何常见:日常生活中的“重要的事先做”直觉被错误套用到了依赖排序上,忽略了拓扑序关注的是“就绪状态”而非“重要性”。

误区我觉得至少三个节点围成一圈才叫环,两个节点互相指向只是“双向关系”,不是环。

正解在有向图中,环的长度没有下限要求。A→B且B→A就构成了长度为2的有向环,表示A、B互相依赖。流水线上,A工位等B的产出、B工位等A的产出,同样会陷入死锁。DAG禁止一切有向环,包括这种“双向箭头”。

为何常见:口语中的“环”通常让人联想到三个及以上节点围成的图形,忽略了有向图中长度为2的回路的合法存在。

面试题

Q1. 有向无环图的英文缩写是?

单选题 · easy

  • A.深度优先搜索算法
  • B.有向无环图结构← 正确答案
  • C.广度优先搜索算法
  • D.卷积神经网络模型

答案:B。DAG是Directed Acyclic Graph的缩写,直译就是“有向无环图”。A是DFS深度优先搜索,C是BFS广度优先搜索,D是CNN卷积神经网络,它们都与“图结构合法性”这个主题无关。

Q2. DFS三色标记法中,灰色节点代表什么?

单选题 · easy

  • A.尚未被访问过的节点
  • B.正在递归栈中的节点← 正确答案
  • C.已经完成访问的节点
  • D.从图中删除掉的节点

答案:B。三色标记法中,白色是尚未访问,黑色是访问完成,灰色专门表示“当前正在递归栈中、路径尚未走完”。回边判定的核心依据就是“遇到了灰色节点”,说明绕回了正在探索的路上。

Q3. Kahn算法每一步优先处理入度为多少的节点?

单选题 · easy

  • A.入度为零的节点← 正确答案
  • B.入度最大的节点
  • C.出度为零的节点
  • D.出度最大的节点

答案:A。Kahn算法的要点是“零入度优先”:只有没有任何上游依赖的节点才能入队出队。入度越大代表依赖越多,越需要等待上游逐步消减;出度则与“有多少下游”相关,与能否开工没有直接关系。

Q4. DFS三色标记法中,发现“回边”指的是当前节点指向?

单选题 · mid

  • A.白色节点且已入栈
  • B.黑色节点且已完成
  • C.灰色节点且在栈中← 正确答案
  • D.任意颜色节点都可能

答案:C。回边是当前节点指向某个灰色节点,意味着从该灰色节点出发的路径尚未走完,却先绕回了它。白色节点会递归进入,黑色节点是历史交叉边,只有灰色标志“正在路上”,遇到它才能断定存在环。

Q5. 一个有6个节点的有向图,Kahn算法得到的拓扑序列长度为5,说明?

单选题 · mid

  • A.图一定是DAG
  • B.图中存在环路← 正确答案
  • C.算法运行出错了
  • D.图不包含任何边

答案:B。Kahn算法结束时,若序列长度小于节点总数,说明剩余节点永远无法让入度降为0——它们只能处在互相等待的环里。6个节点排出5个,必然有一个及以上环节点没有入队,图不是DAG。

Q6. 拓扑排序最典型的应用场景是?

单选题 · mid

  • A.给一组数字按大小排序
  • B.得出无环依赖的执行顺序← 正确答案
  • C.找出图中最长的路径
  • D.计算网络的连通分量

答案:B。拓扑排序把有依赖关系的任务排成一个合法的先后序列,比如课程安排、任务调度。数值排序是排序算法的范畴,最长路径与连通分量是图论中的其他问题,与“依赖编排”的语义不同。

Q7. 下列哪张有向图无法通过DAG检测?

单选题 · mid

  • A.A指向B, B指向C的链条
  • B.A指向B和C, B和C指向D
  • C.A指向B, B指向C, C指向A← 正确答案
  • D.A指向B, B指向C, A指向D

答案:C。选项C中A→B、B→C、C→A首尾相接构成有向环,无法通过检测。其余三个选项都是链状或树状结构,不存在任何一条能绕回自身的路径,因此是合法DAG。

Q8. 关于DFS三色标记法和Kahn算法,下列说法正确的有?

多选题 · hard

  • A.两者都能检出有向图中的环← 正确答案
  • B.Kahn算法能顺带给出拓扑序列← 正确答案
  • C.DFS三色法需要借助队列实现
  • D.两种算法时间复杂度相同← 正确答案

答案:ABD。DFS用三色标记检测回边,Kahn用入度消减检测序列长度不足,两者都能检出环,且复杂度都是O(V+E)。Kahn天然产出拓扑序列;DFS通常用递归或显式栈而非队列,故C错误。

Q9. 在因果推断中,DAG前置校验的作用是什么?

多选题 · hard

  • A.确保因果假设不形成循环依赖← 正确答案
  • B.为后续因果效应估计提供合法结构← 正确答案
  • C.直接计算出因果效应的大小
  • D.自动消除数据中的全部混杂偏倚

答案:AB。DAG测试的任务是确认因果图无环,剔除循环依赖,为效应估计提供合法的图结构前提。它本身不计算效应数值,也不处理数据混杂——那些是后续估计阶段和调整方法的事。

Q10. 关于DFS三色标记法与Kahn算法的复杂度与适用场景,正确的有?

多选题 · extreme

  • A.两种方法的时间复杂度都是O(V+E)← 正确答案
  • B.深度很大的图用递归DFS可能栈溢出← 正确答案
  • C.Kahn算法必须使用递归方式实现
  • D.Kahn算法适合给出任务调度的合法顺序← 正确答案

答案:ABD。两者复杂度均为O(V+E),这是线性时间的最优量级。深度很大的图递归DFS可能栈溢出,可换显式栈或直接用Kahn。Kahn基于队列迭代实现,并非递归,且其拓扑序列天然适合任务调度,故C错误。