← 返回编程题库
coding-decision-tree-evaluator简单免费版2000ms未尝试

Recursive Binary Decision-Tree Evaluator

Recursive Binary Decision-Tree Evaluator

开始编码

二叉决策树是随机森林、梯度提升树(XGBoost、LightGBM、CatBoost)以及任何"若此特征 ≤ 该阈值则……"规则手册背后的标准推断结构。给定一行的特征向量,你从根开始走树:每个内部节点用一个特征跟一个阈值比较一下决定向左还是向右,到了叶子就读出预测值。求值过程是教科书级递归 DFS——从根到叶恰好一条路径,深度有界,没有回溯。

实现 solution(tree: dict, features: dict) -> float。树用嵌套 dict 编码,两种节点形状:

  • 叶子{"is_leaf": True, "value": <float>} —— 直接返回 value
  • 内部{"is_leaf": False, "feature": <str>, "threshold": <float>, "left": <subtree>, "right": <subtree>} —— 取 features[feature]。若 features[feature] <= threshold 递归 left,否则递归 right。等号(精确相等)走

返回最终走到的那个叶子的 value。若树 malformed(节点缺 is_leaf、叶子缺 value、内部节点缺 feature / threshold / left / right 中任何一个),输入非法 → 应抛 ValueError。若走到的内部节点的 feature 不在 features 中,输入也非法 → 应抛 ValueError。本题在批量打分环境下把这两条错误路径都编码为哨兵返回值 -1.0(原始 spec 是抛 ValueError-1.0 是为了兼容批量打分基础设施,沿用 coding-iv-put-bisection 的约定)。

示例

1. 单叶子。 solution({"is_leaf": True, "value": 3.5}, {}) 返回 3.5。根本身就是叶子,与 features 无关。

2. 两层树。 tree = {"is_leaf": False, "feature": "x", "threshold": 0.0, "left": {"is_leaf": True, "value": -1.0}, "right": {"is_leaf": True, "value": 1.0}}。当 features = {"x": -0.5} 时,−0.5 <= 0.0 走左 → 叶子 -1.0;当 features = {"x": 0.7} 时,0.7 > 0.0 走右 → 叶子 1.0

3. 更深的递归树。 tree = {"is_leaf": False, "feature": "age", "threshold": 30.0, "left": {"is_leaf": False, "feature": "income", "threshold": 50.0, "left": {"is_leaf": True, "value": 0.1}, "right": {"is_leaf": True, "value": 0.4}}, "right": {"is_leaf": True, "value": 0.8}}。当 features = {"age": 25, "income": 80}:根处 25 <= 30 → 左;下一节点 80 > 50 → 右 → 叶子 0.4。当 features = {"age": 45}:根处 45 > 30 → 右 → 叶子 0.8(注意 income 根本没被查到)。

实战背景

这就是梯度提升树(gradient-boosted tree)模型的单行推断核心。线上服务时一个 XGBoost / LightGBM 模型动辄几百到几千棵树,给一行打分就是在每棵树上跑一遍这段递归遍历,再把所有叶子值加起来(再加 bias)。生产实现里会把递归拆成紧凑数组 (feature_id, threshold, left_idx, right_idx, value_if_leaf) 以便 cache 友好和 SIMD 批处理,但算法跟你这里要写的完全一样。对单行单树的递归走法熟练之后,再去碰特征归因(路径加权)、SHAP 值、模型导出 ONNX 之类的题目就只剩工程问题了。

约束条件

  • 树的深度至多 50(普通递归远不会触及 Python 默认递归上限 1000)。
  • 每个叶子节点形状严格为 `{'is_leaf': True, 'value': <float>}`,`value` 为有限实数。
  • 每个内部节点形状严格为 `{'is_leaf': False, 'feature': <str>, 'threshold': <float>, 'left': <subtree>, 'right': <subtree>}`:`feature` 是要在 `features` 里查的键名,`threshold` 是有限实数。
  • `features` 是字符串特征名到有限 float 值的字典。只要遍历途中实际查询到的键存在即可——多余的键允许出现且会被忽略。
  • 路由规则:内部节点上 `features[feature] <= threshold` 走 `left`(等号也走左),否则走 `right`。
  • 若任一节点缺 `is_leaf`,或叶子缺 `value`,或内部节点缺 `feature` / `threshold` / `left` / `right` 中任何一个,树视作 malformed,应当抛 `ValueError`。本批量打分环境用哨兵返回值 `-1.0` 表示。
  • 若走到的内部节点的 `feature` 不在 `features` 字典里,同样应抛 `ValueError`(同样编码为哨兵 `-1.0`)。
  • 有效叶子值的浮点容差:rel_tol = 1e-9,abs_tol = 1e-12。

样例

Case 1 · single leaf root: value returned regardless of features

输入: [{"is_leaf":true,"value":3.5},{}]

期望: 3.5

根节点本身就是叶子,没有任何分支可走——直接返回 value=3.5,与 features 无关(这里 features 给空 dict 也没事)。这是边界情形:`is_leaf` 检查必须放在最前面。

Case 2 · depth-2 statement example: x=-0.5 routes left to -1.0

输入: [{"is_leaf":false,"feature":"x","threshold":0,"left":{"is_leaf":true,"value":-1},"right":{"is_leaf":true,"value":1}},{"x":-0.5}]

期望: -1

题面例 2 第一种情形:根上 features['x']=-0.5 ≤ 阈值 0.0,走左 → 叶子值 -1.0。

Case 3 · depth-2 statement example: x=0.7 routes right to 1.0

输入: [{"is_leaf":false,"feature":"x","threshold":0,"left":{"is_leaf":true,"value":-1},"right":{"is_leaf":true,"value":1}},{"x":0.7}]

期望: 1

题面例 2 第二种情形:features['x']=0.7 > 阈值 0.0,走右 → 叶子值 1.0。

Case 4 · tie-going-left: features[x] == threshold descends left

输入: [{"is_leaf":false,"feature":"x","threshold":5,"left":{"is_leaf":true,"value":100},"right":{"is_leaf":true,"value":200}},{"x":5}]

期望: 100

等号情形:features['x']=5.0 跟 threshold 5.0 完全相等,规则是 `<=` 走左,所以拿到左叶子 100.0 而不是右叶子 200.0。这条专门拷打不小心写成 `<` 的实现。

Case 5 · missing feature key in features dict → sentinel -1.0

输入: [{"is_leaf":false,"feature":"missing_key","threshold":0,"left":{"is_leaf":true,"value":5},"right":{"is_leaf":true,"value":6}},{"some_other_key":1}]

期望: -1

根节点要查 features['missing_key'],但 features 里只有 some_other_key——必需的特征键不存在。原 spec 是抛 ValueError,本批量打分环境编码为哨兵 -1.0。

最近提交

还没有提交记录。

编码区

实现 solution(...)。本地运行当前支持 Python 可见样例;服务端提交会运行可见样例和隐藏测试。

加载编辑器...
计时0:00

默认展示公开样例。点击「运行样例」后会在这里显示实际输出;点击「提交评测」会进入隐藏测试。

Case 1 · single leaf root: value returned regardless of features

输入: [{"is_leaf":true,"value":3.5},{}]

期望: 3.5

根节点本身就是叶子,没有任何分支可走——直接返回 value=3.5,与 features 无关(这里 features 给空 dict 也没事)。这是边界情形:`is_leaf` 检查必须放在最前面。

Case 2 · depth-2 statement example: x=-0.5 routes left to -1.0

输入: [{"is_leaf":false,"feature":"x","threshold":0,"left":{"is_leaf":true,"value":-1},"right":{"is_leaf":true,"value":1}},{"x":-0.5}]

期望: -1

题面例 2 第一种情形:根上 features['x']=-0.5 ≤ 阈值 0.0,走左 → 叶子值 -1.0。

Case 3 · depth-2 statement example: x=0.7 routes right to 1.0

输入: [{"is_leaf":false,"feature":"x","threshold":0,"left":{"is_leaf":true,"value":-1},"right":{"is_leaf":true,"value":1}},{"x":0.7}]

期望: 1

题面例 2 第二种情形:features['x']=0.7 > 阈值 0.0,走右 → 叶子值 1.0。

Case 4 · tie-going-left: features[x] == threshold descends left

输入: [{"is_leaf":false,"feature":"x","threshold":5,"left":{"is_leaf":true,"value":100},"right":{"is_leaf":true,"value":200}},{"x":5}]

期望: 100

等号情形:features['x']=5.0 跟 threshold 5.0 完全相等,规则是 `<=` 走左,所以拿到左叶子 100.0 而不是右叶子 200.0。这条专门拷打不小心写成 `<` 的实现。

Case 5 · missing feature key in features dict → sentinel -1.0

输入: [{"is_leaf":false,"feature":"missing_key","threshold":0,"left":{"is_leaf":true,"value":5},"right":{"is_leaf":true,"value":6}},{"some_other_key":1}]

期望: -1

根节点要查 features['missing_key'],但 features 里只有 some_other_key——必需的特征键不存在。原 spec 是抛 ValueError,本批量打分环境编码为哨兵 -1.0。