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 可见样例;服务端提交会运行可见样例和隐藏测试。
默认展示公开样例。点击「运行样例」后会在这里显示实际输出;点击「提交评测」会进入隐藏测试。
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。