You are given a tree consisting of n
nodes numbered from 0
to n - 1
. You are also given a 0-indexed integer array vals
of length n
where vals[i]
denotes the value of the i<sup>th</sup>
node. You are also given a 2D integer array edges
where edges[i] = [a<sub>i</sub>, b<sub>i</sub>]
denotes that there exists an undirected edge connecting nodes a<sub>i</sub>
and b<sub>i</sub>
.
A good path is a simple path that satisfies the following conditions:
Return the number of distinct good paths. Note that a path and its reverse are counted as the same path. A single node is also considered a valid path.
For example, consider the following input:
vals = [1, 3, 2, 1, 3], edges = [[0, 1], [0, 2], [2, 3], [2, 4]]
In this case, the output should be 6. There are 5 good paths consisting of a single node. There is 1 additional good path: 1 -> 0 -> 2 -> 4
.
As another example:
vals = [1, 1, 2, 2, 3], edges = [[0, 1], [1, 2], [2, 3], [2, 4]]
In this case, the output should be 7. There are 5 good paths consisting of a single node. There are 2 additional good paths: 0 -> 1
and 2 -> 3
.
Can you design an algorithm to efficiently count the number of distinct good paths in the tree?
When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:
The brute force method for finding good paths involves checking every single possible path between each pair of locations. We explore all ways to get from one location to another. The key is that we only consider paths where the highest value we encounter on the path is equal to the values of the start and end locations.
Here's how the algorithm would work step-by-step:
def number_of_good_paths_brute_force(values, edges):
number_of_locations = len(values)
adjacency_list = [[] for _ in range(number_of_locations)]
for start_node, end_node in edges:
adjacency_list[start_node].append(end_node)
adjacency_list[end_node].append(start_node)
good_path_count = 0
for start_location in range(number_of_locations):
for end_location in range(start_location, number_of_locations):
# Consider each pair of locations as potential start and end points
def find_all_paths(current_location, end_location, current_path):
current_path.append(current_location)
if current_location == end_location:
yield current_path
else:
for neighbor in adjacency_list[current_location]:
if neighbor not in current_path:
yield from find_all_paths(neighbor, end_location, current_path[:])
for path in find_all_paths(start_location, end_location, []):
# Check if the current path is a 'good path'
maximum_value_on_path = max(values[location] for location in path)
if maximum_value_on_path == values[start_location] == values[end_location]:
# Increment counter since we have a good path
good_path_count += 1
return good_path_count
The problem asks us to find 'good paths' in a graph based on node values. The most efficient solution cleverly groups nodes with similar values and then connects these groups to count the paths.
Here's how the algorithm would work step-by-step:
def number_of_good_paths(values, edges):
number_of_nodes = len(values)
parent = list(range(number_of_nodes))
node_values = sorted([(values[i], i) for i in range(number_of_nodes)])
adjacency_list = [[] for _ in range(number_of_nodes)]
for node_a, node_b in edges:
adjacency_list[node_a].append(node_b)
adjacency_list[node_b].append(node_a)
def find(node):
if parent[node] != node:
parent[node] = find(parent[node])
return parent[node]
def union(node_a, node_b):
root_a = find(node_a)
root_b = find(node_b)
if root_a != root_b:
parent[root_a] = root_b
good_paths_count = 0
# Initially, each node is a good path of length 1
for _ in range(number_of_nodes):
good_paths_count += 1
node_index = 0
while node_index < number_of_nodes:
current_value = node_values[node_index][0]
current_nodes = []
# Group all nodes with the same value.
while node_index < number_of_nodes and node_values[node_index][0] == current_value:
current_nodes.append(node_values[node_index][1])
node_index += 1
# Iterate through neighbors and union if neighbor's value <= current value.
for node in current_nodes:
for neighbor in adjacency_list[node]:
if values[neighbor] <= current_value:
union(node, neighbor)
# Count paths within the current value's club.
group_counts = {}
for node in current_nodes:
root = find(node)
if root not in group_counts:
group_counts[root] = 0
group_counts[root] += 1
# Calculate number of good paths based on group sizes.
for count in group_counts.values():
good_paths_count += count * (count - 1) // 2
return good_paths_count
Case | How to Handle |
---|---|
Empty values array or edges array | Return 0, since no paths exist in an empty graph. |
values array with a single node (n=1) | Return 1, as the single node forms a valid path. |
Graph with all nodes having the same value | The number of good paths will be the number of connected components, and connections between nodes with the same value must be accounted for. |
Graph forms a single chain (linear graph) | Ensure the algorithm efficiently traverses the chain without unnecessary computations. |
Graph is a complete graph (all nodes connected to each other) | The number of edges will be n*(n-1)/2, ensuring the algorithm doesn't exceed time limits due to excessive processing of edges. |
Values are very large, leading to potential integer overflow when counting paths. | Use a 64-bit integer type (long long in C++, long in Java) to store the path counts. |
Edges are duplicated in the input | The algorithm should handle duplicate edges without affecting the correctness of path counting, perhaps by using a set to represent edges in each component. |
Disconnected graph with multiple components. | Process each connected component separately and sum the number of good paths from each to arrive at the global solution. |