实现的时候我们需要维护两个数组—— f 和 g,这里我们可以用从树的节点到整数的映射容器来实现,这样很方便从节点的指针映射到 f 和 g 的函数值。理论上我们可以用 DFS 或者 BFS 的任意一种框架来遍历这棵树,这里提供一个 BFS 的版本。我们用二元组 (node, parent) 作为状态,其中 node 表示当前待计算 f 和 g 的值的节点,parent 表示它的父亲。初始化的时候我们可以把每个点的 f 和 g 的值都置 0。在动态规划算法 BFS 过程中,先根据当前的点是左子树还是右子树更新 f 和 g,然后再拓展新状态入队。
classSolution: deflongestZigZag(self, root: TreeNode) -> int: f, g = collections.defaultdict(int), collections.defaultdict(int) q = collections.deque([(root, None)]) whilelen(q) > 0: node, parent = q.popleft() if parent: if parent.left == node: f[node] = g[parent] + 1 else: g[node] = f[parent] + 1 if node.left: q.append((node.left, node)) if node.right: q.append((node.right, node)) maxans = 0 for _, val in f.items(): maxans = max(maxans, val) for _, val in g.items(): maxans = max(maxans, val) return maxans
复杂度
记 n 为树上的节点总数。
时间复杂度:在 BFS 的过程中,每个点只被访问一次,所以这里的渐进时间复杂度为 O(n)。
空间复杂度:我们这里开了 f 和 g 两个大小为 n 的数组和一个用来做 BFS 的队列。每个点最多进队两次(一次在初始化的过程中,一次在动态规划的过程中),所以队列最大的时候不会超过 n 个元素。所以这里的渐进空间复杂度为 O(n)。
方法二:深度优先搜索
思路
考虑以上的 DP 对于每个节点 u 只使用到了父亲节点的信息,所以我们可以在 DFS 的时候作为参数传递下来,省掉两个数组的空间开销。
思考:是否需要把 f 和 g 的值都传递下来? 其实是不需要的,实际上我们只需要传递「当前」这个点应该走的方向(「向左」或者「向右」)dir,以及以这个点结尾的最长交错路径的长度 len。对于 dir 这个参数,这里 0 表示向左,1 表示向右。
在 DFS 的过程中,每次我们都把当前点的 len 参数和答案 maxAns 打擂台,这样可以比出一个最大的。然后我们根据 dir 分类讨论。如果当前点应该向左且可以向左,那么就让他向左走一步,新的 len 是当前的 len 加一。如果的的点应该向左但是却没有左子树呢?很无奈那就只能向右了,这个时候 len 的值应该「重置」。
思考:「重置」为什么是把 len 变成 1 而不是 0? 因为当前的点下传到它的子节点的时候已经走了一条长度为 1 的边。那么为什么 main 函数中传入的 len 值是 0 而不是 1 呢? 因为 main 函数中的 root 是没有父亲节点的,所以当前已经走过的路为 0。
/* 0 => left, 1 => right */ voiddfs(TreeNode* o, bool dir, int len){ maxAns = max(maxAns, len); if (!dir) { if (o->left) dfs(o->left, 1, len + 1); if (o->right) dfs(o->right, 0, 1); } else { if (o->right) dfs(o->right, 0, len + 1); if (o->left) dfs(o->left, 1, 1); } }