1. 问题背景与理解
UVa 112 "Tree Summing"是ACM国际大学生程序设计竞赛(ICPC)中一道经典的二叉树处理题目。我第一次在训练赛遇到这道题时,被它独特的输入格式和递归逻辑绕得晕头转向。这道题的核心是判断给定的二叉树中是否存在一条从根节点到叶子节点的路径,使得路径上所有节点的值之和等于目标值。
题目输入采用LISP风格的括号嵌套结构表示二叉树,例如"(1(2()())(3()()))"表示:
1 / \ 2 3这种表示法的特点是空子树用"()"表示,每个非空节点由三部分组成:(节点值 左子树 右子树)。这种紧凑的表示方式对编程竞赛非常友好,但也给解析带来了挑战。
2. 输入处理与树构建
2.1 输入格式解析
题目输入由多组测试数据组成,每组数据包含:
- 一个整数S(目标求和值)
- 一个可能跨多行的LISP风格树表达式
实际处理时需要注意:
- 输入可能包含任意数量的空白字符(空格、换行、制表符)
- 数字可能为负数
- 树表达式可能跨越多行
// 示例输入处理代码 string treeStr; int target; while (cin >> target) { treeStr.clear(); char c; int parenCount = 0; do { cin >> c; if (c == '(') parenCount++; if (c == ')') parenCount--; treeStr.push_back(c); } while (parenCount > 0); // 此时treeStr包含完整的树表达式 }2.2 递归构建二叉树
构建二叉树的核心是递归下降解析:
- 遇到'('开始新节点
- 读取节点值(可能为负)
- 递归处理左子树
- 递归处理右子树
- 遇到')'结束当前节点
struct TreeNode { int val; TreeNode *left, *right; TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; TreeNode* buildTree(const string& s, int& pos) { while (pos < s.size() && isspace(s[pos])) pos++; if (s[pos++] != '(') return nullptr; while (pos < s.size() && isspace(s[pos])) pos++; if (s[pos] == ')') { pos++; return nullptr; } // 空树 int sign = 1; if (s[pos] == '-') { sign = -1; pos++; } int val = 0; while (isdigit(s[pos])) { val = val * 10 + (s[pos++] - '0'); } val *= sign; TreeNode* node = new TreeNode(val); node->left = buildTree(s, pos); node->right = buildTree(s, pos); while (pos < s.size() && s[pos] != ')') pos++; pos++; // 跳过')' return node; }3. 路径求和算法实现
3.1 递归深度优先搜索
最直观的解法是DFS递归遍历所有根到叶子的路径:
bool hasPathSum(TreeNode* root, int target, int currentSum) { if (!root) return false; currentSum += root->val; if (!root->left && !root->right) // 叶子节点 return currentSum == target; return hasPathSum(root->left, target, currentSum) || hasPathSum(root->right, target, currentSum); }3.2 迭代法实现
对于特别深的树,递归可能导致栈溢出。迭代解法使用栈同时保存节点和当前路径和:
bool hasPathSumIterative(TreeNode* root, int target) { if (!root) return false; stack<pair<TreeNode*, int>> st; st.push({root, 0}); while (!st.empty()) { auto [node, currSum] = st.top(); st.pop(); currSum += node->val; if (!node->left && !node->right) { if (currSum == target) return true; continue; } if (node->right) st.push({node->right, currSum}); if (node->left) st.push({node->left, currSum}); } return false; }4. 边界条件与特殊测试用例
4.1 常见边界情况
- 空树:无论目标值是多少都应返回false
5 () → no - 单节点树:
5 (5()()) → yes 5 (3()()) → no - 负数值节点:
-1 (-1()()) → yes 0 (-1(1()())()) → yes - 多路径情况:
5 (1(2(3()())())(4()(5()()))) → yes (1+4=5)
4.2 输入处理陷阱
- 跨行输入:
5 (1 (2()() ) (3()() ) ) - 多余空白字符:
5 ( 1 ( 2 () () ) ( 3 () () ) ) - 负数处理:
-5 (-5()())
5. 性能优化与竞赛技巧
5.1 提前终止递归
当找到符合条件的路径时立即返回,避免不必要的计算:
bool hasPathSumOpt(TreeNode* root, int target, int& currentSum) { if (!root) return false; currentSum += root->val; if (!root->left && !root->right) { bool res = (currentSum == target); currentSum -= root->val; // 回溯 return res; } bool left = hasPathSumOpt(root->left, target, currentSum); if (left) return true; // 左子树已找到,提前返回 bool right = hasPathSumOpt(root->right, target, currentSum); currentSum -= root->val; // 回溯 return right; }5.2 输入优化技巧
在编程竞赛中,使用快速IO可以显著提升性能:
ios::sync_with_stdio(false); cin.tie(nullptr);对于特别大的输入,可以一次性读取整个输入再处理:
string readAll() { stringstream buffer; buffer << cin.rdbuf(); return buffer.str(); }6. 算法复杂度分析
设树有n个节点:
- 时间复杂度:O(n),每个节点访问一次
- 空间复杂度:
- 递归:O(h),h为树高(递归栈空间)
- 迭代:O(n),最坏情况需要存储所有节点
对于平衡二叉树,空间复杂度可降至O(log n)。
7. 实际竞赛中的常见错误
- 未重置全局变量:多组测试数据时忘记重置
- 指针未初始化:新建节点时未初始化左右子树指针
- 负数解析错误:处理类似"-12"的数字时只读取了负号
- 空格处理不当:未跳过数字之间的空白字符
- 叶子节点判断错误:将只有一个子节点的中间节点误判为叶子节点
8. 扩展变种问题
8.1 输出所有满足条件的路径
修改算法记录路径节点:
void findAllPaths(TreeNode* root, int target, vector<int>& path, vector<vector<int>>& result) { if (!root) return; path.push_back(root->val); if (!root->left && !root->right && accumulate(path.begin(), path.end(), 0) == target) { result.push_back(path); } findAllPaths(root->left, target, path, result); findAllPaths(root->right, target, path, result); path.pop_back(); }8.2 路径方向限制
如只允许从左子节点向下时,需要修改遍历逻辑:
bool hasLeftPathSum(TreeNode* root, int target) { if (!root) return false; if (!root->left && !root->right) return target == root->val; bool left = false; if (root->left) left = hasLeftPathSum(root->left, target - root->val); return left; }9. 不同语言的实现差异
9.1 Python实现
Python利用其简洁语法可以写出非常紧凑的解法:
def hasPathSum(root, target): if not root: return False if not root.left and not root.right: return root.val == target return (hasPathSum(root.left, target - root.val) or hasPathSum(root.right, target - root.val))9.2 Java实现
Java需要更详细的类定义和空值检查:
public boolean hasPathSum(TreeNode root, int sum) { if (root == null) return false; if (root.left == null && root.right == null) return root.val == sum; return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val); }10. 测试用例设计策略
设计全面的测试用例应包含:
- 常规情况:
22 (5(4(11(7()())(2()()))())(8(13()())(4()(1()())))) - 极端情况:
- 单节点树
- 空树
- 只有左子树/右子树的链状树
- 负数情况:
-5 (-1(-2(-3()())())()) - 大数情况:
1000000 (999999(1()())()) - 平衡树与不平衡树
11. UVa在线评判系统注意事项
- 输出格式:必须严格匹配"yes"或"no",注意大小写
- 多组输入:需要处理到文件结束(EOF)
- 内存限制:避免不必要的内存分配
- 时间限制:Java/Python等语言需要更优化的实现
- 输出缓冲:在大量输出时考虑使用缓冲
12. 从解题到竞赛的系统训练
这道题体现了ICPC竞赛中的几个核心能力:
- 复杂输入处理能力
- 递归思维与实现能力
- 树结构的操作能力
- 边界条件处理意识
建议的延伸练习题目:
- UVa 122 - Trees on the level (树的层次遍历)
- UVa 536 - Tree Recovery (根据遍历序列重建树)
- UVa 615 - Is It A Tree? (树性质验证)