Solving Leetcode Interviews in Seconds with AI: Number of Good Paths
Introduction
In this blog post, we will explore how to solve the LeetCode problem "2421" using AI. LeetCode is a popular platform for preparing for coding interviews, and with the help of AI tools like Chatmagic, we can generate solutions quickly and efficiently - helping you pass the interviews and get the job offer without having to study for months.
Problem Statement
There is a tree (i.e. a connected, undirected graph with no cycles) consisting of n nodes numbered from 0 to n - 1 and exactly n - 1 edges. You are given a 0-indexed integer array vals of length n where vals[i] denotes the value of the ith node. You are also given a 2D integer array edges where edges[i] = [ai, bi] denotes that there exists an undirected edge connecting nodes ai and bi. A good path is a simple path that satisfies the following conditions: The starting node and the ending node have the same value. All nodes between the starting node and the ending node have values less than or equal to the starting node (i.e. the starting node's value should be the maximum value along the path). Return the number of distinct good paths. Note that a path and its reverse are counted as the same path. For example, 0 -> 1 is considered to be the same as 1 -> 0. A single node is also considered as a valid path. Example 1: Input: vals = [1,3,2,1,3], edges = [[0,1],[0,2],[2,3],[2,4]] Output: 6 Explanation: There are 5 good paths consisting of a single node. There is 1 additional good path: 1 -> 0 -> 2 -> 4. (The reverse path 4 -> 2 -> 0 -> 1 is treated as the same as 1 -> 0 -> 2 -> 4.) Note that 0 -> 2 -> 3 is not a good path because vals[2] > vals[0]. Example 2: Input: vals = [1,1,2,2,3], edges = [[0,1],[1,2],[2,3],[2,4]] Output: 7 Explanation: There are 5 good paths consisting of a single node. There are 2 additional good paths: 0 -> 1 and 2 -> 3. Example 3: Input: vals = [1], edges = [] Output: 1 Explanation: The tree consists of only one node, so there is one good path. Constraints: n == vals.length 1 <= n <= 3 * 104 0 <= vals[i] <= 105 edges.length == n - 1 edges[i].length == 2 0 <= ai, bi < n ai != bi edges represents a valid tree.
Explanation
Here's a breakdown of the approach, complexity, and the Python code:
Key Idea: The core idea is to use the Disjoint Set Union (DSU) data structure. We process nodes in increasing order of their values. For each node, we merge the connected components of its neighbors that have values less than or equal to it. The number of good paths involving the current node is then determined by the number of nodes in each of the merged components.
Union by Rank and Path Compression: Standard DSU optimizations are used to improve performance. Union by rank minimizes tree height, and path compression flattens the tree structure during find operations.
Counting Paths: When merging components connected to a node with value
val, we calculate the number of good paths that end at that node. For each connected component (after merging), we multiply the size of that component by the size of other components and increment to the total count of good paths.Complexity:
- Time Complexity: O(n log n) due to sorting the nodes by value. The DSU operations take nearly constant time on average due to path compression and union by rank.
- Space Complexity: O(n) to store the parent, rank, and sizes for the DSU, and the adjacency list for the tree.
Code
class DSU:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
self.size = [1] * n
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # Path compression
return self.parent[x]
def union(self, x, y):
root_x = self.find(x)
root_y = self.find(y)
if root_x != root_y:
if self.rank[root_x] < self.rank[root_y]:
self.parent[root_x] = root_y
self.size[root_y] += self.size[root_x]
elif self.rank[root_x] > self.rank[root_y]:
self.parent[root_y] = root_x
self.size[root_x] += self.size[root_y]
else:
self.parent[root_y] = root_x
self.size[root_x] += self.size[root_y]
self.rank[root_x] += 1
return True
return False
def count_good_paths(vals, edges):
n = len(vals)
adj = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v)
adj[v].append(u)
nodes = sorted(range(n), key=lambda i: vals[i])
dsu = DSU(n)
good_paths = n # Each node is a good path
for node in nodes:
for neighbor in adj[node]:
if vals[neighbor] <= vals[node]:
dsu.union(node, neighbor)
root = dsu.find(node)
count = {}
for neighbor in adj[node]:
if vals[neighbor] <= vals[node]:
neighbor_root = dsu.find(neighbor)
count[neighbor_root] = count.get(neighbor_root, 0) + 1
total_pairs = 0
for root_node, freq in count.items():
total_pairs += (dsu.size[root_node])*(dsu.size[root_node]-1)//2
count_dict = {}
for neighbor in adj[node]:
if vals[neighbor] <= vals[node]:
root_neighbor = dsu.find(neighbor)
count_dict[root_neighbor] = dsu.size[root_neighbor]
paths = 0
for k1 in count_dict:
paths += count_dict[k1] * (count_dict[k1]-1) // 2
root_counts = {}
for i in range(n):
root = dsu.find(i)
root_counts[root] = root_counts.get(root, 0) + 1
for root in root_counts:
good_paths += (dsu.size[root] * (dsu.size[root] - 1)) // 2
return good_paths