Advertisement

Shortest Path in a Weighted Tree - LeetCode 3515 Solution

Shortest Path in a Weighted Tree - Complete Solution Guide

Shortest Path in a Weighted Tree is LeetCode problem 3515, a Hard level challenge. This complete guide provides step-by-step explanations, multiple solution approaches, and optimized code in python3, java, cpp, c.

Problem Statement

You are given an integer n and an undirected, weighted tree rooted at node 1 with n nodes numbered from 1 to n . This is represented by a 2D array edges of length n - 1 , where edges[i] = [u i , v i , w i ] indicates an undirected edge from node u i to v i with weight w i . You are also given a 2D integer array queries of length q , where each queries[i] is either: [1, u, v, w'] – Update the weight of the edge between nodes u and v to w' , where (u, v) is guaranteed to be an edge present i

Detailed Explanation

The problem requires us to maintain and query shortest path distances from a root node (node 1) to other nodes in a weighted tree. We are given an initial tree structure and a series of queries. There are two types of queries: update the weight of an existing edge, and compute the shortest path distance from the root to a given node. The critical challenge is efficiently handling both edge weight updates and shortest path distance computations after each update.

Solution Approach

The solution uses a combination of Depth-First Search (DFS) and a Binary Indexed Tree (BIT) to efficiently handle edge weight updates and shortest path queries. First, a DFS is performed to precompute the initial shortest path distances from the root to all nodes, along with the depth of each node, start/end times based on the traversal order, and subtree sizes. The edge weights are stored in a hash map for easy access during updates. The start and end times are based on the order nodes are visited during DFS, which allows for range update operations over subtrees. When an edge weight is updated, the difference between the new and old weights (delta) is propagated to all nodes in the subtree rooted at the child node of that edge using the BIT. When a shortest path distance query is received, the initial precomputed distance is combined with the cumulative effect of all edge weight updates affecting the path from the root to the queried node, which is obtained using BIT queries.

Step-by-Step Algorithm

  1. Step 1: Tree Representation: Build an adjacency list representation of the tree using the given `edges` array. Also, create a hash map (edge_map) to store the edge weights for efficient lookup.
  2. Step 2: Depth-First Search (DFS): Perform a DFS traversal of the tree, starting from the root node (node 1). During the traversal, calculate and store the following information for each node:
  3. - parent[node]: The parent of the node in the DFS tree.
  4. - depth[node]: The depth of the node from the root.
  5. - dist[node]: The shortest path distance from the root to the node.
  6. - start_time[node]: The time when the node is first visited during the DFS traversal.
  7. - end_time[node]: The time when the subtree rooted at the node is completely explored.
  8. - subtree_size[node]: The size of the subtree rooted at the node.
  9. Step 3: Binary Indexed Tree (BIT) Initialization: Create a BIT with a size slightly larger than the number of nodes (n+2). This BIT will be used to efficiently track the cumulative effect of edge weight updates on subtree distances.
  10. Step 4: Query Processing: Iterate through the `queries` array and process each query as follows:
  11. - Update Query (q[0] == 1):
  12. - Identify the edge to be updated (u, v).
  13. - Determine the child node of the edge (the deeper node).
  14. - Calculate the delta (difference between the new and old edge weights).
  15. - Update the edge weight in the `edge_map`.
  16. - Use the BIT to add the delta to the subtree rooted at the child node using range update (add delta at start_time[child] and subtract delta at end_time[child] + 1).
  17. - Shortest Path Query (q[0] == 2):
  18. - Retrieve the precomputed distance from the root to the node (x) from `dist[x]`.
  19. - Use the BIT to query the cumulative effect of edge weight updates on the path from the root to the node (x) by querying at `start_time[x]`.
  20. - Add the precomputed distance and the cumulative effect to get the updated shortest path distance.
  21. Step 5: Return Result: After processing all queries, return the array of shortest path distances calculated for the shortest path queries.
  22. Step 6: Free Memory (in C solution) Properly free allocated memory to avoid memory leaks.

Key Insights

  • Insight 1: Representing the tree: Using an adjacency list to represent the tree allows efficient traversal and neighbor access.
  • Insight 2: Efficient path calculation: Directly recalculating the shortest path for each query after updates would be inefficient. Instead, use Depth-First Search (DFS) to precompute initial distances and then use a Binary Indexed Tree (BIT) to efficiently track the impact of edge weight changes on subtree distances.
  • Insight 3: Subtree Updates: When updating an edge weight, the shortest path distances to nodes within the subtree rooted at the *child* node of that edge are affected. The BIT is used to apply these delta changes to the appropriate subtrees. Ordering the DFS traversal according to start and end times simplifies subtree updates.

Complexity Analysis

Time Complexity: O(n + q*logn)

Space Complexity: O(n)

Topics

This problem involves: Array, Tree, Depth-First Search, Binary Indexed Tree, Segment Tree.

Companies

Asked at: Juspay.