3067. Count Pairs of Connectable Servers in a Weighted Tree Network
Problem Description
You have an unrooted weighted tree with n
servers (vertices) numbered from 0
to n - 1
. The tree is defined by an array edges
where each edges[i] = [ai, bi, weighti]
represents a bidirectional edge between servers ai
and bi
with weight weighti
. You're also given an integer signalSpeed
.
Two servers a
and b
can be connectable through a server c
if all of these conditions are met:
a < b
(servera
has a smaller number than serverb
)a != c
andb != c
(the connecting serverc
must be different from botha
andb
)- The distance from server
c
to servera
is divisible bysignalSpeed
- The distance from server
c
to serverb
is divisible bysignalSpeed
- The paths from
c
toa
and fromc
tob
don't share any edges (they branch out in different directions fromc
)
Your task is to return an array count
of length n
where count[i]
represents the number of server pairs (a, b)
that are connectable through server i
.
In other words, for each server i
, you need to count how many pairs of servers can communicate with each other using server i
as an intermediate connection point, following the rules above. The distance between servers is the sum of edge weights along the path connecting them.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem explicitly involves an unrooted weighted tree with vertices (servers) and edges. A tree is a special type of graph.
Is it a tree?
- Yes: The problem statement clearly mentions we're working with an "unrooted weighted tree" with
n
vertices andn-1
edges (sinceedges
array defines the tree structure).
DFS
- Yes: We arrive at DFS as the solution pattern.
Why DFS is appropriate for this problem:
-
Tree traversal: We need to explore paths from each potential intermediate server
c
to find all servers at specific distances (divisible bysignalSpeed
). -
Path exploration: For each server
i
acting as the intermediate node, we need to:- Explore each branch (subtree) independently
- Count servers in each branch whose distance from
i
is divisible bysignalSpeed
- Ensure paths don't share edges (naturally handled by tree structure where each branch is independent)
-
Distance calculation: DFS allows us to accumulate distances as we traverse, making it easy to check if the cumulative distance is divisible by
signalSpeed
. -
Counting valid pairs: By using DFS from each neighbor of the intermediate server, we can count valid servers in each direction and multiply these counts to get the number of valid pairs.
Conclusion: The flowchart correctly leads us to use DFS for this tree-based problem where we need to explore paths, calculate distances, and count valid server pairs through each potential intermediate server.
Intuition
The key insight is to think about what makes two servers connectable through an intermediate server. For any intermediate server c
, we need to find pairs of servers (a, b)
where both are reachable from c
at distances divisible by signalSpeed
, and they lie in different branches of the tree rooted at c
.
Since we're working with a tree, when we pick any server c
as the intermediate point, the tree naturally splits into several subtrees (branches) connected to c
. The crucial observation is that paths from c
to servers in different branches will never share edges - this is guaranteed by the tree structure where there's only one path between any two nodes.
For each server i
as the intermediate point, we can:
- Explore each of its neighboring branches independently using DFS
- Count how many servers in each branch are at a "valid" distance (divisible by
signalSpeed
) fromi
- Multiply the counts from different branches to get the number of valid pairs
For example, if server i
has three branches with 2, 3, and 4 valid servers respectively, the number of connectable pairs through i
would be 2×3 + 2×4 + 3×4 = 26
pairs.
The multiplication works because:
- We need to pick one server from one branch and another server from a different branch
- All such combinations form valid pairs (assuming
a < b
constraint is handled)
We accumulate the counts incrementally: as we process each branch, we multiply the current branch's count with the sum of all previous branches' counts, then add this branch's count to our running sum. This efficiently calculates all pair combinations without redundant work.
The DFS helper function recursively explores from a starting point, tracking the cumulative distance, and returns the count of servers at valid distances in that subtree.
Learn more about Tree and Depth-First Search patterns.
Solution Approach
First, we construct an adjacency list g
based on the edges given in the problem, where g[a]
represents all the neighbor nodes of node a
and their corresponding edge weights. This is done by iterating through the edges
array and adding bidirectional connections.
g = [[] for _ in range(n)]
for a, b, w in edges:
g[a].append((b, w))
g[b].append((a, w))
The core of the solution involves two main components:
1. DFS Helper Function
The dfs
function explores from a given node and counts servers at valid distances:
- Parameters:
a
(current node),fa
(parent node to avoid revisiting),ws
(cumulative weight/distance) - Returns: count of servers whose distance from the starting point is divisible by
signalSpeed
- Base case: If
ws % signalSpeed == 0
, we found a valid server (count = 1), otherwise 0 - Recursively explore all neighbors except the parent, accumulating distances
def dfs(a: int, fa: int, ws: int) -> int:
cnt = 0 if ws % signalSpeed else 1
for b, w in g[a]:
if b != fa:
cnt += dfs(b, a, ws + w)
return cnt
2. Main Enumeration Logic
For each potential intermediate server a
:
- Initialize a cumulative sum
s = 0
to track servers found in previous branches - For each neighbor
b
ofa
:- Call
dfs(b, a, w)
to count valid servers in that branch (starting with edge weightw
) - The number of new pairs formed =
s * t
(current branch count × previous branches' total) - Add these pairs to
ans[a]
- Update
s += t
for the next iteration
- Call
for a in range(n):
s = 0
for b, w in g[a]:
t = dfs(b, a, w)
ans[a] += s * t
s += t
The key insight in the accumulation pattern is that it efficiently counts all combinations of servers from different branches without double-counting. By multiplying the current branch's count with the accumulated sum of previous branches, we ensure each pair is counted exactly once.
Time Complexity: O(n²)
- For each of the n
nodes, we potentially visit all other nodes through DFS.
Space Complexity: O(n)
- For the adjacency list and recursion stack.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Example Input:
n = 5
servers (numbered 0 to 4)edges = [[0,1,4], [1,2,2], [1,3,4], [3,4,2]]
signalSpeed = 2
This creates a tree structure:
0 | (4) | 1 / \ (2) (4) / \ 2 3 | (2) | 4
Step 1: Build Adjacency List
g[0] = [(1, 4)] g[1] = [(0, 4), (2, 2), (3, 4)] g[2] = [(1, 2)] g[3] = [(1, 4), (4, 2)] g[4] = [(3, 2)]
Step 2: Process Each Server as Intermediate Point
Let's focus on server 1 as the intermediate point:
Server 1 has 3 branches (neighbors): 0, 2, and 3.
Branch 1 (toward server 0):
- Call
dfs(0, 1, 4)
starting with edge weight 4 - At server 0: distance = 4,
4 % 2 = 0
✓ (valid) - No more neighbors to explore
- Returns count = 1
Branch 2 (toward server 2):
- Call
dfs(2, 1, 2)
starting with edge weight 2 - At server 2: distance = 2,
2 % 2 = 0
✓ (valid) - No more neighbors to explore
- Returns count = 1
Branch 3 (toward server 3):
- Call
dfs(3, 1, 4)
starting with edge weight 4 - At server 3: distance = 4,
4 % 2 = 0
✓ (valid), count = 1 - Explore neighbor 4:
dfs(4, 3, 6)
with distance 4+2=6 - At server 4: distance = 6,
6 % 2 = 0
✓ (valid), count = 1 - Returns total count = 2
Calculating Pairs for Server 1:
- Initially
s = 0
- After branch 1:
ans[1] += 0 * 1 = 0
, updates = 1
- After branch 2:
ans[1] += 1 * 1 = 1
, updates = 2
- After branch 3:
ans[1] += 2 * 2 = 4
, updates = 4
- Total
ans[1] = 5
The 5 connectable pairs through server 1 are:
- (0, 2): distances 4 and 2, both divisible by 2 ✓
- (0, 3): distances 4 and 4, both divisible by 2 ✓
- (0, 4): distances 4 and 6, both divisible by 2 ✓
- (2, 3): distances 2 and 4, both divisible by 2 ✓
- (2, 4): distances 2 and 6, both divisible by 2 ✓
Complete Solution: Processing all servers similarly:
ans[0] = 0
(only one branch from server 0)ans[1] = 5
(as calculated above)ans[2] = 0
(only one branch from server 2)ans[3] = 2
(pairs: (1,4) and (2,4) through server 3)ans[4] = 0
(only one branch from server 4)
Final answer: [0, 5, 0, 2, 0]
The algorithm efficiently counts all valid server pairs by:
- Using DFS to explore each branch and count servers at valid distances
- Multiplying counts from different branches to get pair combinations
- Accumulating results incrementally to avoid redundant calculations
Solution Implementation
1class Solution:
2 def countPairsOfConnectableServers(
3 self, edges: List[List[int]], signalSpeed: int
4 ) -> List[int]:
5 """
6 Count pairs of connectable servers for each node where the path distance
7 between servers through the node is divisible by signalSpeed.
8
9 Args:
10 edges: List of edges [node1, node2, weight]
11 signalSpeed: The signal speed divisor
12
13 Returns:
14 List where ans[i] is the number of valid server pairs through node i
15 """
16
17 def count_servers_in_subtree(current_node: int, parent_node: int, distance_from_root: int) -> int:
18 """
19 DFS to count servers in a subtree where the distance from root is divisible by signalSpeed.
20
21 Args:
22 current_node: Current node being visited
23 parent_node: Parent of current node to avoid revisiting
24 distance_from_root: Accumulated distance from the root node
25
26 Returns:
27 Count of valid servers in this subtree
28 """
29 # Current node is a server if distance is divisible by signalSpeed
30 server_count = 1 if distance_from_root % signalSpeed == 0 else 0
31
32 # Explore all neighbors except parent
33 for neighbor, edge_weight in adjacency_list[current_node]:
34 if neighbor != parent_node:
35 server_count += count_servers_in_subtree(
36 neighbor,
37 current_node,
38 distance_from_root + edge_weight
39 )
40
41 return server_count
42
43 # Build adjacency list representation of the graph
44 num_nodes = len(edges) + 1
45 adjacency_list = [[] for _ in range(num_nodes)]
46
47 for node_a, node_b, weight in edges:
48 adjacency_list[node_a].append((node_b, weight))
49 adjacency_list[node_b].append((node_a, weight))
50
51 # Calculate result for each node as the intermediate connection point
52 result = [0] * num_nodes
53
54 for root_node in range(num_nodes):
55 # Count servers in each branch from root_node
56 cumulative_servers = 0
57
58 for neighbor, edge_weight in adjacency_list[root_node]:
59 # Count servers in this branch
60 servers_in_branch = count_servers_in_subtree(neighbor, root_node, edge_weight)
61
62 # Add pairs formed between this branch and previous branches
63 result[root_node] += cumulative_servers * servers_in_branch
64
65 # Update cumulative count for next iterations
66 cumulative_servers += servers_in_branch
67
68 return result
69
1class Solution {
2 private int signalSpeed;
3 private List<int[]>[] adjacencyList;
4
5 public int[] countPairsOfConnectableServers(int[][] edges, int signalSpeed) {
6 // Initialize graph with n nodes (edges.length + 1)
7 int nodeCount = edges.length + 1;
8 adjacencyList = new List[nodeCount];
9 this.signalSpeed = signalSpeed;
10
11 // Create adjacency list for each node
12 Arrays.setAll(adjacencyList, index -> new ArrayList<>());
13
14 // Build undirected graph from edges
15 for (int[] edge : edges) {
16 int nodeA = edge[0];
17 int nodeB = edge[1];
18 int weight = edge[2];
19
20 // Add bidirectional edges
21 adjacencyList[nodeA].add(new int[] {nodeB, weight});
22 adjacencyList[nodeB].add(new int[] {nodeA, weight});
23 }
24
25 int[] result = new int[nodeCount];
26
27 // For each node as intermediate server
28 for (int currentNode = 0; currentNode < nodeCount; currentNode++) {
29 int previousSubtreeCount = 0;
30
31 // Check all subtrees rooted at neighbors of current node
32 for (int[] neighbor : adjacencyList[currentNode]) {
33 int neighborNode = neighbor[0];
34 int edgeWeight = neighbor[1];
35
36 // Count connectable servers in this subtree
37 int currentSubtreeCount = dfs(neighborNode, currentNode, edgeWeight);
38
39 // Add pairs formed between this subtree and all previous subtrees
40 result[currentNode] += previousSubtreeCount * currentSubtreeCount;
41
42 // Update cumulative count for next iteration
43 previousSubtreeCount += currentSubtreeCount;
44 }
45 }
46
47 return result;
48 }
49
50 private int dfs(int currentNode, int parentNode, int pathWeight) {
51 // Check if current node is connectable (path weight divisible by signal speed)
52 int connectableCount = (pathWeight % signalSpeed == 0) ? 1 : 0;
53
54 // Explore all neighbors except parent
55 for (int[] neighbor : adjacencyList[currentNode]) {
56 int neighborNode = neighbor[0];
57 int edgeWeight = neighbor[1];
58
59 // Skip parent node to avoid revisiting
60 if (neighborNode != parentNode) {
61 // Recursively count connectable servers in subtree
62 connectableCount += dfs(neighborNode, currentNode, pathWeight + edgeWeight);
63 }
64 }
65
66 return connectableCount;
67 }
68}
69
1class Solution {
2public:
3 vector<int> countPairsOfConnectableServers(vector<vector<int>>& edges, int signalSpeed) {
4 // Calculate the number of nodes (vertices) in the tree
5 int numNodes = edges.size() + 1;
6
7 // Build adjacency list representation of the tree
8 // Each node maps to a list of (neighbor, weight) pairs
9 vector<vector<pair<int, int>>> adjacencyList(numNodes);
10
11 for (const auto& edge : edges) {
12 int nodeA = edge[0];
13 int nodeB = edge[1];
14 int weight = edge[2];
15
16 // Add bidirectional edges since this is an undirected tree
17 adjacencyList[nodeA].emplace_back(nodeB, weight);
18 adjacencyList[nodeB].emplace_back(nodeA, weight);
19 }
20
21 // DFS function to count connectable servers in a subtree
22 // Parameters:
23 // currentNode: current node being visited
24 // parentNode: parent node to avoid revisiting
25 // pathWeight: cumulative weight from the root of DFS to current node
26 // Returns: number of connectable servers in the subtree
27 function<int(int, int, int)> countConnectableInSubtree = [&](int currentNode, int parentNode, int pathWeight) {
28 // Check if current node is connectable (path weight divisible by signal speed)
29 int connectableCount = (pathWeight % signalSpeed == 0) ? 1 : 0;
30
31 // Explore all neighbors except the parent
32 for (const auto& [neighborNode, edgeWeight] : adjacencyList[currentNode]) {
33 if (neighborNode != parentNode) {
34 // Recursively count connectable servers in the subtree
35 connectableCount += countConnectableInSubtree(neighborNode, currentNode, pathWeight + edgeWeight);
36 }
37 }
38
39 return connectableCount;
40 };
41
42 // Result vector to store the count of connectable server pairs for each node
43 vector<int> result(numNodes);
44
45 // Process each node as a potential intermediate server
46 for (int intermediateNode = 0; intermediateNode < numNodes; ++intermediateNode) {
47 int previousSubtreeCount = 0;
48
49 // Consider each branch from the intermediate node
50 for (const auto& [neighborNode, edgeWeight] : adjacencyList[intermediateNode]) {
51 // Count connectable servers in this branch
52 int currentBranchCount = countConnectableInSubtree(neighborNode, intermediateNode, edgeWeight);
53
54 // Calculate pairs: servers from previous branches × servers from current branch
55 result[intermediateNode] += previousSubtreeCount * currentBranchCount;
56
57 // Update the cumulative count of servers from all processed branches
58 previousSubtreeCount += currentBranchCount;
59 }
60 }
61
62 return result;
63 }
64};
65
1/**
2 * Counts pairs of connectable servers in a tree network based on signal speed
3 * @param edges - Array of edges where each edge is [nodeA, nodeB, weight]
4 * @param signalSpeed - The signal speed divisor for checking connectivity
5 * @returns Array where ans[i] is the number of valid server pairs passing through node i
6 */
7function countPairsOfConnectableServers(edges: number[][], signalSpeed: number): number[] {
8 // Calculate total number of nodes (edges + 1 for a tree)
9 const nodeCount: number = edges.length + 1;
10
11 // Build adjacency list: graph[node] = array of [neighbor, edgeWeight]
12 const graph: [number, number][][] = Array.from({ length: nodeCount }, () => []);
13
14 // Populate the undirected graph
15 for (const [nodeA, nodeB, weight] of edges) {
16 graph[nodeA].push([nodeB, weight]);
17 graph[nodeB].push([nodeA, weight]);
18 }
19
20 /**
21 * Depth-first search to count valid servers in a subtree
22 * @param currentNode - Current node being visited
23 * @param parentNode - Parent node to avoid revisiting
24 * @param pathWeight - Accumulated weight from the root to current node
25 * @returns Count of valid servers in this subtree
26 */
27 const dfs = (currentNode: number, parentNode: number, pathWeight: number): number => {
28 // Check if current node is a valid server (path weight divisible by signal speed)
29 let validServerCount: number = pathWeight % signalSpeed === 0 ? 1 : 0;
30
31 // Explore all neighbors except the parent
32 for (const [neighbor, edgeWeight] of graph[currentNode]) {
33 if (neighbor !== parentNode) {
34 // Recursively count valid servers in the subtree
35 validServerCount += dfs(neighbor, currentNode, pathWeight + edgeWeight);
36 }
37 }
38
39 return validServerCount;
40 };
41
42 // Initialize result array with zeros
43 const result: number[] = Array(nodeCount).fill(0);
44
45 // For each node, calculate pairs of connectable servers passing through it
46 for (let rootNode = 0; rootNode < nodeCount; ++rootNode) {
47 let previousSubtreeCount: number = 0;
48
49 // Process each subtree rooted at neighbors of the current node
50 for (const [neighbor, edgeWeight] of graph[rootNode]) {
51 // Count valid servers in this subtree
52 const currentSubtreeCount: number = dfs(neighbor, rootNode, edgeWeight);
53
54 // Add pairs formed between this subtree and all previous subtrees
55 result[rootNode] += previousSubtreeCount * currentSubtreeCount;
56
57 // Update the running count of servers from previous subtrees
58 previousSubtreeCount += currentSubtreeCount;
59 }
60 }
61
62 return result;
63}
64
Time and Space Complexity
Time Complexity: O(n²)
The algorithm iterates through each node as a potential intermediate server (outer loop runs n
times). For each node a
, it explores all its neighbors and performs a DFS from each neighbor. The DFS traverses the tree structure excluding the subtree containing node a
, visiting at most n-1
nodes. Since we do this for each neighbor of every node, and in the worst case (like a star graph), we might traverse nearly all nodes multiple times. Across all iterations, each edge is traversed O(n)
times, resulting in O(n²)
total time complexity.
Space Complexity: O(n)
The space is used for:
- The adjacency list
g
which stores all edges, requiringO(n)
space (since there aren-1
edges in a tree) - The recursion stack for DFS, which in the worst case (a linear tree) can go up to depth
n
, requiringO(n)
space - The answer array
ans
of sizen
, requiringO(n)
space
Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrectly Counting the Intermediate Node Itself
A common mistake is including the intermediate node c
when counting valid servers in each branch. According to the problem constraints, the intermediate node should NOT be counted as part of the connectable pairs since a != c
and b != c
.
Incorrect approach:
def dfs(a: int, fa: int, ws: int) -> int:
# Wrong: Always counting current node when ws == 0
cnt = 1 if ws == 0 else 0
for b, w in g[a]:
if b != fa:
cnt += dfs(b, a, ws + w)
return cnt
Correct approach:
def dfs(a: int, fa: int, ws: int) -> int:
# Correct: Only count if ws % signalSpeed == 0 AND ws != 0
cnt = 1 if ws % signalSpeed == 0 and ws > 0 else 0
for b, w in g[a]:
if b != fa:
cnt += dfs(b, a, ws + w)
return cnt
2. Double-Counting Server Pairs
Another pitfall is counting the same pair (a, b)
multiple times when iterating through branches. The solution uses a cumulative approach (s * t
) to ensure each pair is counted exactly once, but developers might mistakenly try to count all combinations directly.
Incorrect approach:
for a in range(n):
branches = []
for b, w in g[a]:
t = dfs(b, a, w)
branches.append(t)
# Wrong: This counts each pair twice (once as (i,j) and once as (j,i))
for i in range(len(branches)):
for j in range(len(branches)):
if i != j:
ans[a] += branches[i] * branches[j]
Correct approach:
for a in range(n):
s = 0
for b, w in g[a]:
t = dfs(b, a, w)
ans[a] += s * t # Only pairs with previous branches
s += t
3. Misunderstanding Distance Calculation
The distance should start from the edge weight when exploring branches, not from 0. Starting from 0 would incorrectly include the intermediate node in the distance calculation.
Incorrect approach:
for a in range(n):
s = 0
for b, w in g[a]:
# Wrong: Starting distance from 0 ignores the first edge
t = dfs(b, a, 0)
ans[a] += s * t
s += t
Correct approach:
for a in range(n):
s = 0
for b, w in g[a]:
# Correct: Start with the edge weight w
t = dfs(b, a, w)
ans[a] += s * t
s += t
How does quick sort divide the problem into subproblems?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!