因子依赖拓扑序:夜间重建的确定性 min-heap Kahn 调度
Factor Dependency Topo Order: Deterministic Min-Heap Kahn for Nightly Rebuild
开始编码实现 solution(n: int, deps: list[list]) -> list[int]。给定 n 个因子节点(整数 id 0..n-1)和有向边列表 deps,其中每个 [src, dst] 表示因子 src 必须在 dst 之前计算(即 dst 依赖 src),返回所有 n 个因子的一个拓扑序(list[int]),使得对每条边 src -> dst,src 在结果中先于 dst。当多个因子在同一步同时入度归零时,按 id 最小者优先 tie-break——即基于 min-heap 的确定性 Kahn 算法,不是 FIFO 队列。图中任意位置存在环时返回 [];空图(n == 0)也返回 []。
例
solution(4, [[0, 1], [0, 2], [1, 3], [2, 3]]) 返回 [0, 1, 2, 3]。因子 0 是两条并行分支的根;它出队后 1 与 2 的剩余入度同时归零,min-id tie-break 先弹 1 再弹 2,最后 3 收尾。再看:solution(0, []) 返回 [](空 universe),solution(3, [[0, 1], [1, 2], [2, 0]]) 也返回 [],因为三节点环没有任何零入度起点供 Kahn 启动。完全无依赖的图 solution(5, []) 返回 [0, 1, 2, 3, 4]——每个因子互相独立,min-heap 按 id 升序输出。
预期解法在一遍 deps 扫描中构建邻接表与入度数组(同时对 (src, dst) 去重,避免重复边重复计数),用所有零入度 id 初始化 min-heap,反复弹出 id 最小的就绪因子,并把它的后继入度减 1、归零者推入堆。总开销 O((N + M) log N),在上限规模下也能舒适地踩进 2000 ms 预算之内。基于 collections.deque 的 FIFO Kahn 也能给出 *某个* 拓扑序,但其调度依赖边的插入顺序——本契约要求确定性 min-id tie-break,使得跨机重跑、输入边重排后输出仍是字节级一致的流水线。
需要明确的契约细节:deps 中的重复边明确允许,必须在构图阶段去重(同一条依赖列两次不代表 dst 依赖 src "两次")。空图(n == 0)返回 [] 作为"没有可调度的工作"的自然哨兵。任意位置的环——即使是一个孤立分量内的小环、其他分量本来构成合法 DAG——也会让全局线性化未定义,触发哨兵 [];编排器把 [] 解读为"暂停流水线并 page 值班同事修复循环因子定义"。自环(src == dst)被输入契约禁止,畸形输入下行为未定义。
实现细节由 stubs/stub.py 提供。
实践背景
某量化基金的夜间因子重建调度器要在因子依赖图上跑:动量因子读价格残差,价格残差读行业中性化收益,行业中性化收益读原始日收益,原始日收益读公司行动调整后的价格——几百个因子、几千条边层层套娃。编排器需要确定性的线性化,使得重跑和并行机分配可以复现:下游消费者(回测器、风险引擎、纸面交易模拟器)跨次比对输出时,区分"真实数据漂移"与"调度器随机性"的唯一办法是先把调度本身钉死。id 字典序 tie-break 让每次的同点先后顺序保持一致,无论因子库注册依赖的先后、无论输入边列表的排序、无论中间字典的插入序怪癖。环则代表模型方写出了循环因子定义,必须先修才能开工——哨兵 [] 提示编排器停下流水线并 page 值班同事,而不是把半成品因子立方体发给下游。min-heap 是同时保证以上两个性质的最廉价工具。
约束条件
- 0 <= N <= 2000,N 是因子节点数(id 范围 0..N-1)
- 0 <= M <= 5000,M 是 `deps` 的长度
- deps[k] 是二元列表 [src, dst],src, dst 落在 [0, N-1],且 src != dst(不允许自环)
- 重复边允许出现,构建邻接 / 入度时必须去重
- 返回类型为 list[int];图中任意位置存在环时或 N == 0 时返回哨兵 []
样例
Case 1 · statement-example: 4 factors, two parallel branches, min-id tie-break
输入: [4,[[0,1],[0,2],[1,3],[2,3]]]
期望: [0,1,2,3]
因子 0 是两个分支的源;1 与 2 在 0 处理后同时入度归零,按最小 id 优先,先输出 1 再输出 2,最后 3 收尾。
Case 2 · visible: empty universe returns []
输入: [0,[]]
期望: []
n=0 时图上没有任何因子节点,按契约返回 []。
Case 3 · visible: fully disconnected graph emits ids 0..n-1
输入: [5,[]]
期望: [0,1,2,3,4]
无任何依赖时所有因子初始入度为 0,min-heap 按 id 升序逐个弹出。
Case 4 · visible: 3-cycle returns [] sentinel
输入: [3,[[0,1],[1,2],[2,0]]]
期望: []
三节点环 0->1->2->0 没有任何零入度起点,Kahn 无法推进,返回哨兵 []。
最近提交
还没有提交记录。
编码区
实现 solution(...)。本地运行当前支持 Python 可见样例;服务端提交会运行可见样例和隐藏测试。
默认展示公开样例。点击「运行样例」后会在这里显示实际输出;点击「提交评测」会进入隐藏测试。
Case 1 · statement-example: 4 factors, two parallel branches, min-id tie-break
输入: [4,[[0,1],[0,2],[1,3],[2,3]]]
期望: [0,1,2,3]
因子 0 是两个分支的源;1 与 2 在 0 处理后同时入度归零,按最小 id 优先,先输出 1 再输出 2,最后 3 收尾。
Case 2 · visible: empty universe returns []
输入: [0,[]]
期望: []
n=0 时图上没有任何因子节点,按契约返回 []。
Case 3 · visible: fully disconnected graph emits ids 0..n-1
输入: [5,[]]
期望: [0,1,2,3,4]
无任何依赖时所有因子初始入度为 0,min-heap 按 id 升序逐个弹出。
Case 4 · visible: 3-cycle returns [] sentinel
输入: [3,[[0,1],[1,2],[2,0]]]
期望: []
三节点环 0->1->2->0 没有任何零入度起点,Kahn 无法推进,返回哨兵 []。