Facebook Pixel

3879. Maximum Distinct Path Sum in a Binary Tree πŸ”’

LeetCode β†—

Problem Description

You are given the root of a binary tree, where each node contains an integer value.

A valid path in the tree is a sequence of connected nodes such that:

  • The path can start and end at any node in the tree.
  • The path does not need to pass through the root.
  • All node values along the path are distinct.

Return an integer denoting the maximum possible sum of node values among all valid paths.

In other words, you may pick any simple path that connects two nodes in the tree (or a single node by itself). The only constraint is that no two nodes on that chosen path may share the same value. Among all such paths that satisfy this distinctness condition, you need to find the one whose sum of node values is the largest, and return that sum.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why DFS?

This problem maps to DFS through a short path in the full flowchart.

Is it agraph?yesIs it atree?yesDFS

The problem operates on a binary tree converted to an undirected graph, explored with DFS and backtracking to find the maximum distinct-value path sum.

Open in Flowchart
Show step-by-step reasoning

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: A binary tree is a special form of graph. Each node connects to its children (and we can also treat the parent link as an edge), so the structure is made up of nodes and edges.

Is it a tree?

  • Yes: The input is explicitly the root of a binary tree, where every node connects to its left child, right child, and parent, forming a tree structure with no cycles.

Conclusion: Following the path Is it a graph? β†’ Yes β†’ Is it a tree? β†’ Yes leads us directly to the DFS node. The flowchart suggests using a Depth-First Search approach to traverse the tree, explore every possible path while tracking visited values to ensure distinctness, and compute the maximum distinct path sum.

Intuition

The first key observation is that although the input is a binary tree, a valid path can start and end at any node and does not need to pass through the root. This means the path can go "up" through a parent and then "down" into another subtree. If we only think in terms of left and right child pointers, expressing such a path becomes awkward, because a path may bend at some node and travel toward its parent.

To make movement in any direction easy, we convert the tree into an undirected graph. For each node, we record all of its neighbors: its parent, its left child, and its right child. Once we have this adjacency information stored in a hash table g, a path in the tree simply becomes a simple path in an undirected graph, where we are free to move from a node to any of its neighbors.

Now the problem turns into: find the simple path with the maximum sum, subject to the constraint that all node values along the path are distinct. Since a path is just a chain of connected nodes, we can build it greedily from a starting node by repeatedly extending into a neighbor that hasn't been used yet. This naturally fits a Depth-First Search: from a given node, we explore each neighbor, recursively compute the best path sum we can collect by continuing in that direction, and keep the best one.

The distinctness condition is handled with a vis set that records the values currently on our path. Before stepping into a node, we check whether its value is already in vis; if it is, that branch is invalid and contributes 0. Otherwise we add the value, explore deeper, and then remove it when backtracking so that other paths can still use that value. This add-explore-remove pattern is classic backtracking layered on top of DFS.

Finally, because the optimal path can begin anywhere, we try every node as a starting point. For each starting node we run the DFS to get the best extendable path sum from it, reset vis, and track the overall maximum. The largest value found across all starting nodes is our answer.

Pattern Learn more about Tree, Depth-First Search and Binary Tree patterns.

Solution Approach

We use DFS + Hash Table. We treat the tree as an undirected graph, using a hash table g to store the adjacent nodes of each node, where g[node] contains the parent node, left child node, and right child node of node.

Step 1: Build the adjacency table with the first DFS.

We run a depth-first search dfs(node, p) to traverse the tree and build the hash table g. For each node, we add its parent node p, left child node, and right child node to g[node]:

  • g[node].append(p) β€” records the parent.
  • g[node].append(node.left) β€” records the left child.
  • g[node].append(node.right) β€” records the right child.

Then we recurse into dfs(node.left, node) and dfs(node.right, node). After this step, every node knows all of its neighbors, so we can freely move in any direction along a path.

Step 2: Compute the best path sum from each node with the second DFS.

We use another depth-first search dfs2(node) to compute the maximum path sum starting from a node, using a hash set vis to record the node values already on the current path so that all values stay distinct.

For each node in dfs2:

  • If the node is None, or its value is already in vis, we return 0 (this branch cannot extend the path).
  • Otherwise, we add node.val to vis and set res = node.val.
  • We initialize best = 0 and traverse every neighbor nxt in g[node], recursively calling dfs2(nxt) and updating best = max(best, dfs2(nxt)). This picks the single best direction to extend the path.
  • We then backtrack by removing node.val from vis, so other paths can reuse this value.
  • Finally we return res + best, i.e. the node's own value plus the best extension.

Step 3: Try every starting node.

Because the optimal path can begin anywhere, we iterate over every node in g, compute dfs2(node), and track the maximum in ans (initialized to -inf). After each starting node, we call vis.clear() to reset the visited set for the next independent search. The final ans is the maximum distinct path sum.

Complexity Analysis:

  • Building g takes O(n) time and O(n) space, where n is the number of nodes.
  • The second DFS is run once per node, and each run may explore the path branching out from that node, so the time cost is higher than a single traversal β€” this approach trades simplicity for speed and works well within the small constraints implied by the distinctness requirement.

Example Walkthrough

Consider the following small binary tree:

        1
       / \
      2   3
     /
    1

Node values: root 1 has children 2 (left) and 3 (right). Node 2 has a left child 1.

Step 1: Build the adjacency table g with the first DFS.

We call dfs(root, None) and record each node's parent, left child, and right child. Using the node positions to disambiguate (since two nodes share the value 1), the adjacency table looks like:

Node (value @ position)Neighbors in g[node]
1 (root)None, 2, 3
21 (root), 1 (leaf), None
31 (root), None, None
1 (leaf)2, None, None

Now the tree is an undirected graph: from any node we can move to its parent or either child.

Step 2 & 3: Run dfs2 from each node, tracking the best path sum.

We try each node as a starting point, resetting vis between runs. (Recall dfs2(None) returns 0, and a node whose value is already in vis also returns 0.)

  • Start at root 1: Add 1 to vis, res = 1.

    • Neighbor 2: not in vis. Add 2, res = 2.
      • Neighbor 1 (root): value 1 already in vis β†’ returns 0.
      • Neighbor 1 (leaf): value 1 already in vis β†’ returns 0.
      • Best extension from 2 is 0, so dfs2(2) returns 2. Backtrack (remove 2).
    • Neighbor 3: not in vis. Add 3, all its other neighbors are None, so dfs2(3) returns 3. Backtrack.
    • best = max(2, 3) = 3. Returns 1 + 3 = 4.
    • Path used: root 1 β†’ 3. Sum = 4.
  • Start at 2: Add 2, res = 2.

    • Neighbor root 1: add 1, then its children 2 (in vis) β†’ 0 and 3 β†’ 3. So dfs2(1 root) returns 1 + 3 = 4.
    • Neighbor leaf 1: add 1, its only other neighbor is 2 (in vis) β†’ 0. Returns 1.
    • best = max(4, 1) = 4. Returns 2 + 4 = 6.
    • Path used: leaf-side aside β€” the best is 2 β†’ 1(root) β†’ 3. Sum = 6.
  • Start at 3: Add 3, res = 3.

    • Neighbor root 1: returns 1 + best(2). From 2, neighbors are root 1 (in vis) and leaf 1 β†’ 1. So dfs2(2) returns 3, making dfs2(1 root) return 4.
    • Returns 3 + 4 = 7.
    • Path used: 3 β†’ 1(root) β†’ 2 β†’ 1(leaf)? No β€” leaf 1 clashes with root 1, so the chain is 3 β†’ 1(root) β†’ 2. Sum = 6. (The recursion correctly stops the leaf branch because value 1 is already on the path.)
  • Start at leaf 1: symmetric exploration yields at best 1 β†’ 2 β†’ 1(root)? The second 1 clashes, so the path is 1(leaf) β†’ 2 β†’ ... extending into root 1 is blocked. Best is 1 + 2 + 3 via 1(leaf) β†’ 2 β†’ 1(root)? Again root 1 clashes with leaf 1. So the best from here is 1(leaf) β†’ 2 = 3. Sum = 3.

Result:

Tracking the maximum across all starting nodes, the best valid path is 2 β†’ 1(root) β†’ 3 with distinct values {2, 1, 3} and a sum of 6. The answer ans = 6.

This illustrates the core ideas: the graph conversion lets the path bend through the root, the vis set enforces distinct values (correctly blocking the duplicate 1s from coexisting on a path), and trying every starting node guarantees we find the globally maximum path.

Solution Implementation

1from typing import Optional
2from collections import defaultdict
3from math import inf
4
5
6# Definition for a binary tree node.
7# class TreeNode:
8#     def __init__(self, val=0, left=None, right=None):
9#         self.val = val
10#         self.left = left
11#         self.right = right
12class Solution:
13    def maxSum(self, root: Optional[TreeNode]) -> int:
14        # Build an adjacency list, treating the binary tree as an undirected graph.
15        # For each node we store its parent, left child, and right child.
16        def build_graph(node: Optional[TreeNode], parent: Optional[TreeNode]) -> None:
17            if node is None:
18                return
19            graph[node].append(parent)
20            graph[node].append(node.left)
21            graph[node].append(node.right)
22            build_graph(node.left, node)
23            build_graph(node.right, node)
24
25        # Depth-first search to find the maximum-sum path starting from the given node.
26        # 'visited' tracks node values already on the current path to avoid revisiting.
27        def find_max_path(node: Optional[TreeNode]) -> int:
28            if node is None or node.val in visited:
29                return 0
30            visited.add(node.val)
31            # The best extension we can take from any neighbor of the current node.
32            best_neighbor = 0
33            for neighbor in graph[node]:
34                best_neighbor = max(best_neighbor, find_max_path(neighbor))
35            # Backtrack: remove the current node so other paths can use it.
36            visited.remove(node.val)
37            # Current node value plus the best path extension from a neighbor.
38            return node.val + best_neighbor
39
40        # Adjacency list mapping each node to its connected nodes.
41        graph = defaultdict(list)
42        build_graph(root, None)
43
44        # Set of node values currently on the path being explored.
45        visited = set()
46        answer = -inf
47        # Try each node as a potential starting point of the best path.
48        for node in graph:
49            answer = max(answer, find_max_path(node))
50            visited.clear()
51        return answer
52```
53
54A few notes on the changes I made:
55
561. **Naming standardization**: `dfs` β†’ `build_graph` (it constructs the graph), `dfs2` β†’ `find_max_path` (it computes the maximum path sum), `g` β†’ `graph`, `p` β†’ `parent`, `nxt` β†’ `neighbor`, `best` β†’ `best_neighbor`, `res` β†’ eliminated in favor of a direct return, `vis` β†’ `visited`, `ans` β†’ `answer`. The method name `maxSum` was kept unchanged as requested.
57
582. **Python3 syntax**: Added explicit type hints (`Optional[TreeNode]`, `-> None`, `-> int`) and the necessary imports (`Optional`, `defaultdict`, `inf`).
59
603. **Clarity improvement**: In `find_max_path`, the original separated `res = node.val` then `res += best`. I simplified it to a single `return node.val + best_neighbor` after backtracking, which is functionally identical and cleaner.
61
62One important caveat about correctness: this approach uses node **values** in the `visited` set rather than node **identity**. If the tree contains duplicate values, the cycle-detection logic can incorrectly block valid paths. A more robust version would track visited **nodes** (by identity) instead:
63
64```python3
65def find_max_path(node: Optional[TreeNode]) -> int:
66    if node is None or node in visited:
67        return 0
68    visited.add(node)
69    best_neighbor = 0
70    for neighbor in graph[node]:
71        best_neighbor = max(best_neighbor, find_max_path(neighbor))
72    visited.remove(node)
73    return node.val + best_neighbor
74
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 *     int val;
5 *     TreeNode left;
6 *     TreeNode right;
7 *     TreeNode() {}
8 *     TreeNode(int val) { this.val = val; }
9 *     TreeNode(int val, TreeNode left, TreeNode right) {
10 *         this.val = val;
11 *         this.left = left;
12 *         this.right = right;
13 *     }
14 * }
15 */
16class Solution {
17    // Adjacency list: maps each tree node to its neighbors (parent + children).
18    private final Map<TreeNode, List<TreeNode>> graph = new HashMap<>();
19    // Tracks nodes currently on the DFS path to prevent revisiting them.
20    private final Set<TreeNode> visited = new HashSet<>();
21
22    /**
23     * Computes the maximum path sum by treating the binary tree as an
24     * undirected graph and exploring the best path starting from every node.
25     *
26     * @param root the root of the binary tree
27     * @return the maximum achievable path sum
28     */
29    public int maxSum(TreeNode root) {
30        // Build the undirected adjacency representation of the tree.
31        dfs(root, null);
32
33        int ans = Integer.MIN_VALUE;
34        // Try starting a path from each node and keep the global maximum.
35        for (TreeNode node : graph.keySet()) {
36            ans = Math.max(ans, dfs2(node));
37            visited.clear();
38        }
39        return ans;
40    }
41
42    /**
43     * Builds the adjacency list by linking each node to its parent
44     * and to both of its children.
45     *
46     * @param node   the current node being processed
47     * @param parent the parent of the current node (null for the root)
48     */
49    private void dfs(TreeNode node, TreeNode parent) {
50        if (node == null) {
51            return;
52        }
53        // Ensure the current node has an entry in the graph.
54        graph.computeIfAbsent(node, key -> new ArrayList<>());
55        // Connect the node to its parent and children.
56        graph.get(node).add(parent);
57        graph.get(node).add(node.left);
58        graph.get(node).add(node.right);
59
60        // Recurse into the children, passing the current node as their parent.
61        dfs(node.left, node);
62        dfs(node.right, node);
63    }
64
65    /**
66     * Performs a DFS from the given node to find the maximum sum of a simple
67     * path (no repeated nodes) extending from it.
68     *
69     * @param node the current node in the traversal
70     * @return the best path sum reachable starting from this node
71     */
72    private int dfs2(TreeNode node) {
73        // Skip null nodes or nodes already on the current path.
74        if (node == null || visited.contains(node)) {
75            return 0;
76        }
77        // Mark the node as part of the current path.
78        visited.add(node);
79
80        int result = node.val;
81        int best = 0;
82        // Explore all neighbors and keep the most valuable extension.
83        for (TreeNode next : graph.getOrDefault(node, Collections.emptyList())) {
84            best = Math.max(best, dfs2(next));
85        }
86
87        // Backtrack: remove the node so other paths can use it.
88        visited.remove(node);
89        return result + best;
90    }
91}
92
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 *     int val;
5 *     TreeNode *left;
6 *     TreeNode *right;
7 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14    int maxSum(TreeNode* root) {
15        // adjacency list: maps each node to its neighbors (parent, left child, right child)
16        unordered_map<TreeNode*, vector<TreeNode*>> adjacency;
17        // tracks visited node values during a single path traversal to avoid revisiting
18        unordered_set<int> visited;
19
20        // Build an undirected graph from the binary tree.
21        // For each node, record its parent and its two children as neighbors.
22        auto buildGraph = [&](this auto&& buildGraph, TreeNode* node, TreeNode* parent) -> void {
23            if (!node) return;
24            adjacency[node].push_back(parent);
25            adjacency[node].push_back(node->left);
26            adjacency[node].push_back(node->right);
27            buildGraph(node->left, node);
28            buildGraph(node->right, node);
29        };
30
31        // Compute the maximum path sum starting from the given node.
32        // It explores neighbors recursively, marking values as visited to
33        // prevent cycles within the current path.
34        auto maxPathFrom = [&](this auto&& maxPathFrom, TreeNode* node) -> int {
35            // Stop if the node is null or its value is already on the current path.
36            if (!node || visited.count(node->val)) return 0;
37            visited.insert(node->val);
38            int current = node->val;
39            int bestNeighbor = 0;
40            // Extend the path through the best-scoring neighbor.
41            for (auto next : adjacency[node]) {
42                bestNeighbor = max(bestNeighbor, maxPathFrom(next));
43            }
44            // Backtrack: remove this value so other paths can reuse it.
45            visited.erase(node->val);
46            return current + bestNeighbor;
47        };
48
49        // Construct the graph starting from the root (root has no parent).
50        buildGraph(root, nullptr);
51
52        // Try every node as a potential starting point and keep the best path sum.
53        int ans = INT_MIN;
54        for (auto& [node, _] : adjacency) {
55            ans = max(ans, maxPathFrom(node));
56            visited.clear();
57        }
58        return ans;
59    }
60};
61
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 *     val: number
5 *     left: TreeNode | null
6 *     right: TreeNode | null
7 *     constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 *         this.val = (val===undefined ? 0 : val)
9 *         this.left = (left===undefined ? null : left)
10 *         this.right = (right===undefined ? null : right)
11 *     }
12 * }
13 */
14
15// Adjacency map: each node maps to its neighbors (parent, left child, right child)
16let graph: Map<TreeNode, (TreeNode | null)[]>;
17
18// Set tracking values currently on the active path (to avoid revisiting by value)
19let visited: Set<number>;
20
21/**
22 * Build an undirected graph from the binary tree.
23 * For each node, record its parent and both children as neighbors.
24 * @param node - the current tree node
25 * @param parent - the parent of the current node
26 */
27function buildGraph(node: TreeNode | null, parent: TreeNode | null): void {
28    if (!node) return;
29    // Initialize the neighbor list for this node if absent
30    if (!graph.has(node)) graph.set(node, []);
31    // Connect this node to its parent and its two children
32    graph.get(node)!.push(parent, node.left, node.right);
33    // Recurse into children, passing the current node as their parent
34    buildGraph(node.left, node);
35    buildGraph(node.right, node);
36}
37
38/**
39 * Compute the maximum path sum extending from the given node,
40 * choosing the single best neighbor branch and avoiding values
41 * already present on the current path.
42 * @param node - the current node being explored
43 * @returns the best sum reachable starting at this node
44 */
45function findMaxPath(node: TreeNode | null): number {
46    // Stop if the node is null or its value is already on the path
47    if (!node || visited.has(node.val)) return 0;
48
49    // Mark this node's value as part of the current path
50    visited.add(node.val);
51
52    const currentValue = node.val;
53    let bestBranch = 0;
54
55    // Explore all neighbors and keep the largest extension
56    for (const neighbor of graph.get(node) || []) {
57        bestBranch = Math.max(bestBranch, findMaxPath(neighbor));
58    }
59
60    // Backtrack: remove this node's value before returning
61    visited.delete(node.val);
62
63    return currentValue + bestBranch;
64}
65
66/**
67 * Find the maximum path sum over the tree, treating it as an
68 * undirected graph and ensuring no value repeats along a path.
69 * @param root - the root of the binary tree
70 * @returns the maximum achievable path sum
71 */
72function maxSum(root: TreeNode | null): number {
73    // Reset global state for this invocation
74    graph = new Map<TreeNode, (TreeNode | null)[]>();
75    visited = new Set<number>();
76
77    // Construct the adjacency representation of the tree
78    buildGraph(root, null);
79
80    let answer = -Infinity;
81
82    // Try starting a path from every node and keep the best result
83    for (const node of graph.keys()) {
84        answer = Math.max(answer, findMaxPath(node));
85        visited.clear();
86    }
87
88    return answer;
89}
90

Time and Space Complexity

Time Complexity: O(n^2)

The algorithm consists of two phases:

  1. Building the graph (dfs): This traverses every node in the tree exactly once, appending each node's parent and children into the adjacency list g. This phase costs O(n), where n is the number of nodes.

  2. Computing the maximum path sum (dfs2): The outer loop iterates over every node in g, which is O(n) iterations. For each starting node, dfs2 performs a depth-first search over the graph. Because the tree is treated as an undirected graph (each node is connected to its parent and its children), and the vis set is cleared after each outer iteration, every individual dfs2 call can visit up to all n nodes in the worst case (for example, in a skewed/path-like tree where the graph forms a long chain). This makes each dfs2 traversal cost O(n).

Combining the outer loop with the per-node traversal gives O(n) * O(n) = O(n^2).

Space Complexity: O(n)

The space usage comes from:

  • The adjacency list g, which stores up to 3 entries per node (parent, left child, right child), requiring O(n) space.
  • The recursion stack of dfs and dfs2, which in the worst case (a skewed tree forming a chain) can reach a depth of O(n).
  • The vis set, which holds at most n node values, requiring O(n) space.

Therefore, the overall space complexity is O(n).

Pattern Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Confusing "distinct values" with "distinct nodes" in the visited set

The most subtle and dangerous mistake in this solution is tracking node values in the visited set rather than node identity. This conflates two different responsibilities:

  1. Cycle prevention β€” in an undirected graph, you must avoid walking back into a node you already came from, otherwise the recursion loops forever (or, in a tree, you'd bounce between parent and child endlessly).
  2. The distinctness constraint β€” the problem requires that all node values along the chosen path be distinct.

When all values happen to be unique, value-based tracking accidentally satisfies both purposes. But the moment the tree contains duplicate values, the logic breaks in two ways:

  • False blocking (correctness bug): Suppose two different nodes both have value 5, and an optimal path legitimately routes through one of them. If a node with value 5 was visited earlier on a separate, unrelated branch that got backtracked incorrectly, or if the same value appears elsewhere, the search may treat a valid extension as already-visited and return 0, silently discarding a better path.

  • Infinite recursion risk: The visited set is the only thing preventing the DFS from walking from a child back to its parent and then back to the child. If you ever loosen the value check (e.g., to allow duplicate values per the problem's actual intent), you simultaneously destroy the cycle guard, and the recursion can revisit a neighbor it just came from.

Solution: Separate the two concerns. Use node identity for cycle prevention, and a value counter for the distinctness constraint:

from collections import defaultdict, Counter
from math import inf
from typing import Optional


class Solution:
    def maxSum(self, root: Optional[TreeNode]) -> int:
        def build_graph(node, parent):
            if node is None:
                return
            graph[node].extend([parent, node.left, node.right])
            build_graph(node.left, node)
            build_graph(node.right, node)

        def find_max_path(node):
            # Identity check prevents walking backward / cycling.
            if node is None or node in on_path:
                return 0
            # Value check enforces the distinct-value constraint.
            if value_count[node.val] > 0:
                return 0

            on_path.add(node)
            value_count[node.val] += 1

            best_neighbor = 0
            for neighbor in graph[node]:
                best_neighbor = max(best_neighbor, find_max_path(neighbor))

            on_path.remove(node)
            value_count[node.val] -= 1
            return node.val + best_neighbor

        graph = defaultdict(list)
        build_graph(root, None)

        on_path = set()              # node identities currently on the path
        value_count = Counter()      # values currently on the path
        answer = -inf
        for node in graph:
            answer = max(answer, find_max_path(node))
            on_path.clear()
            value_count.clear()
        return answer

Pitfall 2: Forgetting to reset state between starting nodes

Because the outer loop tries every node as a path origin, any mutable state shared across runs (visited, value_count, on_path) must be cleared after each find_max_path call. The provided code does call visited.clear(), but it's easy to overlook when refactoring β€” and if you maintain two structures (identity + counter, as above), you must reset both. Leftover state from a previous starting node will incorrectly block valid paths in the next search.

Solution: Clear all path-tracking structures at the end of each iteration, as shown above. Alternatively, rely solely on backtracking (every add is paired with a remove) so the structures return to empty naturally β€” but keeping an explicit clear() as a safety net guards against an unbalanced add/remove introduced during future edits.

Pitfall 3: Mistaking this for the classic "Binary Tree Maximum Path Sum"

A reader familiar with LeetCode 124 (Binary Tree Maximum Path Sum) might assume a single-pass, post-order traversal in O(n) suffices β€” at each node, combine the best left and right downward chains. That elegant approach works only because LeetCode 124 has no distinctness constraint, so a node's best contribution depends purely on its subtree.

Here, the distinctness requirement makes a node's optimal extension depend on which values already appear on the path leading into it. That context is path-dependent, not subtree-local, so the clean O(n) recurrence does not apply, and a "turning" path (one that bends through a node using both children) needs special handling under the constraint.

Solution: Recognize that the distinctness condition forces a graph-style exploration with backtracking rather than a pure tree DP. Be aware of the consequences:

  • Single-direction limitation: The code above computes the best straight extension (node.val + best_neighbor), which captures a path with node as an endpoint. To also account for paths where node is the bend point (joining two distinct neighbor chains), you'd combine the two best neighbor results β€” while ensuring the value sets of the two branches don't collide. This is significantly harder under the distinctness constraint and is the part most likely to be silently wrong.
  • Complexity: This explores branching paths from every node and is far costlier than O(n). It's viable only when the distinctness constraint keeps usable path lengths short. Don't expect it to scale to large trees with many repeated values.

Pitfall 4: Iterating over graph while it can mutate via defaultdict

graph is a defaultdict(list). Inside find_max_path, accessing graph[neighbor] for a node that was never inserted (it shouldn't happen here since every real node is built, but None neighbors are filtered by the early return) would create a new empty entry. Mutating a dict while iterating over it with for node in graph: raises RuntimeError: dictionary changed size during iteration.

Solution: Either guard against None before indexing (the early if node is None return already does this), or iterate over a snapshot of the keys to be safe:

for node in list(graph):
    answer = max(answer, find_max_path(node))
    on_path.clear()
    value_count.clear()

Wrapping the keys in list(...) decouples the loop from the live dictionary, eliminating any risk of accidental in-loop mutation.

Ready to land your dream job?

Unlock your dream job with a 5-minute quiz for a personalized study roadmap!

Get My Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

Which data structure is used to implement priority queue?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More