Minimum Weighted Subgraph With the Required Paths II
HardPrompt
You are given an undirected weighted tree with n nodes, numbered from 0 to n - 1. It is represented by a 2D integer array edges of length n - 1, where edges[i] = [ui, vi, wi] indicates that there is an edge between nodes ui and vi with weight wi.
Additionally, you are given a 2D integer array queries, where queries[j] = [src1j, src2j, destj].
Return an array answer of length equal to queries.length, where answer[j] is the minimum total weight of a subtree such that it is possible to reach destj from both src1j and src2j using edges in this subtree.
A subtree here is any connected subset of nodes and edges of the original tree forming a valid tree.
Example 1:
Input: edges = [[0,1,2],[1,2,3],[1,3,5],[1,4,4],[2,5,6]], queries = [[2,3,4],[0,2,5]]
Output: [12,11]
Explanation:
The blue edges represent one of the subtrees that yield the optimal answer.

-
answer[0]: The total weight of the selected subtree that ensures a path fromsrc1 = 2andsrc2 = 3todest = 4is3 + 5 + 4 = 12. -
answer[1]: The total weight of the selected subtree that ensures a path fromsrc1 = 0andsrc2 = 2todest = 5is2 + 3 + 6 = 11.
Example 2:
Input: edges = [[1,0,8],[0,2,7]], queries = [[0,1,2]]
Output: [15]
Explanation:

answer[0]: The total weight of the selected subtree that ensures a path fromsrc1 = 0andsrc2 = 1todest = 2is8 + 7 = 15.
Constraints:
3 <= n <= 105edges.length == n - 1edges[i].length == 30 <= ui, vi < n1 <= wi <= 1041 <= queries.length <= 105queries[j].length == 30 <= src1j, src2j, destj < nsrc1j,src2j, anddestjare pairwise distinct.- The input is generated such that
edgesrepresents a valid tree.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach directly simulates the problem statement for each query. It finds the two required paths, from src1 to dest and src2 to dest, and then computes the total weight of their union. This is straightforward but inefficient due to redundant computations across queries.
Algorithm
- For each query
(src1, src2, dest):- Initialize an empty set,
union_edges, to store unique edges and a variabletotal_weight = 0. - Find the path from
src1todestusing a graph traversal like BFS or DFS. This involves keeping track of parent pointers and then backtracking fromdest. - Iterate through the edges of this path. For each edge, add it to
union_edges. If the edge was not already in the set, add its weight tototal_weight. - Find the path from
src2todestusing another traversal. - Iterate through the edges of this second path, again adding the weight to
total_weightonly for new edges. - The final
total_weightis the answer for the current query.
- Initialize an empty set,
Walkthrough
The core idea is to treat each query independently. For a given query (src1, src2, dest), we need to identify the set of edges that form the paths P(src1, dest) and P(src2, dest). The union of these edges forms the minimal required subtree.
We can use a graph traversal algorithm like Breadth-First Search (BFS) or Depth-First Search (DFS) to find these paths. For instance, to find P(src1, dest), we start a traversal from src1. During the traversal, we keep track of the parent of each visited node. Once dest is reached, we can reconstruct the path by backtracking from dest to src1 using the parent pointers. The same process is repeated for P(src2, dest). To get the total weight, we collect all unique edges from both paths and sum their weights. A HashSet is suitable for storing the unique edges to avoid double-counting the edges in the overlapping part of the paths.
// Helper methods to be part of a solution class.// The main logic would iterate through queries and call solveQueryBruteForce. private long solveQueryBruteForce(int src1, int src2, int dest, int n, java.util.List<java.util.List<int[]>> adj) { java.util.List<int[]> path1 = findPath(src1, dest, n, adj); java.util.List<int[]> path2 = findPath(src2, dest, n, adj); java.util.Set<String> uniqueEdges = new java.util.HashSet<>(); long totalWeight = 0; for (int[] edge : path1) { int u = Math.min(edge[0], edge[1]); int v = Math.max(edge[0], edge[1]); if (uniqueEdges.add(u + "-" + v)) { totalWeight += edge[2]; } } for (int[] edge : path2) { int u = Math.min(edge[0], edge[1]); int v = Math.max(edge[0], edge[1]); if (uniqueEdges.add(u + "-" + v)) { totalWeight += edge[2]; } } return totalWeight;} private java.util.List<int[]> findPath(int start, int end, int n, java.util.List<java.util.List<int[]>> adj) { java.util.Map<Integer, int[]> parentMap = new java.util.HashMap<>(); // node -> {parent, weight_to_parent} java.util.Queue<Integer> q = new java.util.LinkedList<>(); q.add(start); parentMap.put(start, new int[]{-1, 0}); while (!q.isEmpty()) { int u = q.poll(); if (u == end) break; for (int[] edge : adj.get(u)) { int v = edge[0]; int w = edge[1]; if (!parentMap.containsKey(v)) { parentMap.put(v, new int[]{u, w}); q.add(v); } } } java.util.List<int[]> path = new java.util.ArrayList<>(); int curr = end; while (parentMap.containsKey(curr) && parentMap.get(curr)[0] != -1) { int[] p_info = parentMap.get(curr); int p = p_info[0]; int w = p_info[1]; path.add(new int[]{p, curr, w}); curr = p; } return path;}Complexity
Time
O(Q * N), where Q is the number of queries and N is the number of nodes. Each query involves two full graph traversals, each taking up to O(N) time.
Space
O(N), where N is the number of nodes. Each query requires space for the traversal data structures (e.g., visited set, parent pointers, queue/stack).
Trade-offs
Pros
Simple to conceptualize and implement.
Requires no complex data structures or precomputation.
Cons
Extremely inefficient for a large number of queries, as it recomputes paths from scratch every time.
Will result in a 'Time Limit Exceeded' (TLE) verdict for the given problem constraints.
This efficient approach leverages precomputation to answer queries quickly. It's based on a key mathematical property of trees: the total weight of the minimal subtree connecting three nodes (src1, src2, dest) can be calculated as (dist(src1, src2) + dist(src1, dest) + dist(src2, dest)) / 2. The distance dist(u, v) between any two nodes can be found efficiently using their Lowest Common Ancestor (LCA).
Algorithm
- Preprocessing Phase (O(N log N)):
- Build an adjacency list representation of the tree from the input edges.
- Pick a root (e.g., node 0) and perform a DFS traversal.
- During DFS, for each node
u, compute itsdepth[u], weighted distance from the rootdist_from_root[u], and its parentup[u][0]. - After the DFS, populate the binary lifting table
up[u][i]fori > 0using the relationup[u][i] = up[up[u][i-1]][i-1].
- Query Phase (O(log N) per query):
- For each query
(src1, src2, dest):- Find the three Lowest Common Ancestors:
lca12 = LCA(src1, src2),lca1d = LCA(src1, dest), andlca2d = LCA(src2, dest)using the binary lifting table. - Calculate the three path distances using the formula:
dist(u, v) = dist_from_root[u] + dist_from_root[v] - 2 * dist_from_root[LCA(u, v)]. - The final answer is
(dist(src1, src2) + dist(src1, dest) + dist(src2, dest)) / 2.
- Find the three Lowest Common Ancestors:
- For each query
Walkthrough
The distance formula is dist(u, v) = dist_root(u) + dist_root(v) - 2 * dist_root(LCA(u, v)), where dist_root(x) is the weighted distance from a fixed root to node x. To implement this, we first preprocess the tree.
-
Preprocessing: We perform a single DFS from an arbitrary root (e.g., node 0). This traversal computes the depth, distance from the root, and the immediate parent for every node. Then, we build a binary lifting table,
up[u][i], which allows us to find the2^i-th ancestor of any nodeu. Building this table takesO(N log N)time. -
Querying: With the precomputed tables, an LCA query for two nodes
uandvcan be answered inO(log N)time. For each query(src1, src2, dest), we perform three LCA lookups and use the distance formula to find the three required path lengths. Finally, we apply the main formula to get the answer. This makes each query very fast.
class Solution { private java.util.List<java.util.List<int[]>> adj; private int n; private int LOGN; private int[] depth; private long[] distFromRoot; private int[][] up; public long[] minWeight(int n, int[][] edges, int[][] queries) { this.n = n; this.LOGN = (int) (Math.log(n) / Math.log(2)) + 1; this.adj = new java.util.ArrayList<>(); for (int i = 0; i < n; i++) { adj.add(new java.util.ArrayList<>()); } for (int[] edge : edges) { adj.get(edge[0]).add(new int[]{edge[1], edge[2]}); adj.get(edge[1]).add(new int[]{edge[0], edge[2]}); } this.depth = new int[n]; this.distFromRoot = new long[n]; this.up = new int[n][LOGN]; for(int i=0; i<n; i++) java.util.Arrays.fill(up[i], -1); dfs(0, -1, 0, 0L); for (int j = 1; j < LOGN; j++) { for (int i = 0; i < n; i++) { if (up[i][j - 1] != -1) { up[i][j] = up[up[i][j - 1]][j - 1]; } } } long[] ans = new long[queries.length]; for (int i = 0; i < queries.length; i++) { int src1 = queries[i][0]; int src2 = queries[i][1]; int dest = queries[i][2]; long d12 = getDist(src1, src2); long d1d = getDist(src1, dest); long d2d = getDist(src2, dest); ans[i] = (d12 + d1d + d2d) / 2; } return ans; } private void dfs(int u, int p, int d, long currentDist) { depth[u] = d; distFromRoot[u] = currentDist; up[u][0] = p; for (int[] edge : adj.get(u)) { int v = edge[0]; int w = edge[1]; if (v != p) { dfs(v, u, d + 1, currentDist + w); } } } private int lca(int u, int v) { if (depth[u] < depth[v]) { int temp = u; u = v; v = temp; } for (int j = LOGN - 1; j >= 0; j--) { if (up[u][j] != -1 && depth[u] - (1 << j) >= depth[v]) { u = up[u][j]; } } if (u == v) return u; for (int j = LOGN - 1; j >= 0; j--) { if (up[u][j] != -1 && up[v][j] != -1 && up[u][j] != up[v][j]) { u = up[u][j]; v = up[v][j]; } } return up[u][0]; } private long getDist(int u, int v) { int lcaNode = lca(u, v); return distFromRoot[u] + distFromRoot[v] - 2 * distFromRoot[lcaNode]; }}Complexity
Time
O((N + Q) * log N). Preprocessing the tree takes O(N log N) time. Each of the Q queries is then answered in O(log N) time.
Space
O(N log N), primarily for storing the binary lifting table `up`.
Trade-offs
Pros
Highly efficient and scalable for a large number of queries.
This is the standard and optimal solution for this class of problems on trees.
Cons
Implementation is more complex than the brute-force approach.
Requires understanding of advanced concepts like Lowest Common Ancestor and binary lifting.
Uses more memory due to the precomputation table.
Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.