1548. The Most Similar Path in a Graph

Problem Description

You are given a graph with n cities numbered from 0 to n-1, connected by m bidirectional roads. Each road in roads[i] = [ai, bi] connects city ai with city bi. Every city has a name consisting of exactly three uppercase English letters, provided in the array names. The graph is fully connected, meaning you can reach any city from any other city.

Your task is to find a path through the graph that:

  1. Has the same length as a given targetPath array
  2. Has the minimum edit distance compared to targetPath

The edit distance between two paths is calculated as the number of positions where the city names differ. For example:

  • If your path visits cities with names ["AAA", "BBB", "CCC"]
  • And the target path is ["AAA", "BBB", "DDD"]
  • The edit distance is 1 (only the third position differs)

The path you return must be valid, meaning there must be a direct road between consecutive cities in your path (ans[i] and ans[i+1] must be connected by a road).

You need to return an array containing the city indices (not names) that form the path with minimum edit distance. If multiple paths have the same minimum edit distance, you can return any one of them.

Example: If targetPath = ["ATL", "PEK", "LAX"] and your graph has cities with similar names connected appropriately, you need to find a sequence of 3 connected cities whose names most closely match this target sequence.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that we need to find the best matching path of a specific length, which suggests using dynamic programming. Think of it like aligning two sequences - our actual path through the graph and the target path.

At each step along the target path, we need to decide which city to visit. The challenge is that we can't just greedily pick the city with the best name match at each position because:

  1. We need to ensure cities are connected by roads (path validity)
  2. A suboptimal choice early on might lead to better overall matching later

This naturally leads to a dynamic programming formulation where we consider: "What's the minimum edit distance if we've matched the first i positions of the target path and we're currently at city j?"

For each position in the target path, we need to try all possible cities we could be at, but we can only reach a city from its neighbors. So for city j at position i, we look at all its neighbors k and check: what was the best edit distance to reach neighbor k at position i-1? Then we add the cost of being at city j for position i (which is 0 if names match, 1 if they don't).

The formula f[i][j] = min(f[i-1][k] + cost) for all neighbors k captures this idea - we're building up optimal solutions for longer paths based on optimal solutions for shorter paths.

To reconstruct the actual path (not just the minimum distance), we need to remember which predecessor city gave us the optimal value at each step. This is why we maintain a pre array to track the optimal previous city for each state, allowing us to backtrack from the end to build the complete path.

Learn more about Graph and Dynamic Programming patterns.

Solution Implementation

1class Solution: 2 def mostSimilar( 3 self, n: int, roads: List[List[int]], names: List[str], targetPath: List[str] 4 ) -> List[int]: 5 # Build adjacency list for the graph 6 adjacency_list = [[] for _ in range(n)] 7 for city_a, city_b in roads: 8 adjacency_list[city_a].append(city_b) 9 adjacency_list[city_b].append(city_a) 10 11 path_length = len(targetPath) 12 13 # dp[i][j] = minimum edit distance to reach city j at position i in targetPath 14 dp = [[float('inf')] * n for _ in range(path_length)] 15 16 # predecessor[i][j] = previous city when reaching city j at position i with minimum cost 17 predecessor = [[-1] * n for _ in range(path_length)] 18 19 # Initialize first position: cost is 0 if name matches, 1 if it doesn't 20 for city in range(n): 21 dp[0][city] = 0 if names[city] == targetPath[0] else 1 22 23 # Fill DP table for remaining positions 24 for position in range(1, path_length): 25 for current_city in range(n): 26 # Check all neighbors of current_city 27 for previous_city in adjacency_list[current_city]: 28 # Calculate cost: previous cost + mismatch penalty 29 mismatch_cost = 0 if names[current_city] == targetPath[position] else 1 30 total_cost = dp[position - 1][previous_city] + mismatch_cost 31 32 # Update if we found a better path 33 if total_cost < dp[position][current_city]: 34 dp[position][current_city] = total_cost 35 predecessor[position][current_city] = previous_city 36 37 # Find the city with minimum cost at the last position 38 min_cost = float('inf') 39 best_last_city = 0 40 for city in range(n): 41 if dp[path_length - 1][city] < min_cost: 42 min_cost = dp[path_length - 1][city] 43 best_last_city = city 44 45 # Reconstruct the path by backtracking through predecessors 46 result_path = [0] * path_length 47 current_city = best_last_city 48 for position in range(path_length - 1, -1, -1): 49 result_path[position] = current_city 50 current_city = predecessor[position][current_city] 51 52 return result_path 53
1class Solution { 2 public List<Integer> mostSimilar(int n, int[][] roads, String[] names, String[] targetPath) { 3 // Build adjacency list for the graph 4 List<Integer>[] graph = new List[n]; 5 Arrays.setAll(graph, i -> new ArrayList<>()); 6 for (int[] road : roads) { 7 int cityA = road[0]; 8 int cityB = road[1]; 9 graph[cityA].add(cityB); 10 graph[cityB].add(cityA); 11 } 12 13 int pathLength = targetPath.length; 14 final int INFINITY = 1 << 30; 15 16 // dp[i][j] = minimum edit distance when at position i in targetPath and at city j 17 int[][] dp = new int[pathLength][n]; 18 // predecessor[i][j] = the previous city when at position i and city j 19 int[][] predecessor = new int[pathLength][n]; 20 21 // Initialize arrays with infinity and -1 22 for (int i = 0; i < pathLength; i++) { 23 Arrays.fill(dp[i], INFINITY); 24 Arrays.fill(predecessor[i], -1); 25 } 26 27 // Base case: first position in targetPath, can start from any city 28 for (int city = 0; city < n; city++) { 29 dp[0][city] = targetPath[0].equals(names[city]) ? 0 : 1; 30 } 31 32 // Fill DP table for each position in targetPath 33 for (int position = 1; position < pathLength; position++) { 34 for (int currentCity = 0; currentCity < n; currentCity++) { 35 // Check all neighbors of current city 36 for (int previousCity : graph[currentCity]) { 37 // Calculate cost: previous cost + current mismatch cost 38 int cost = dp[position - 1][previousCity] + 39 (targetPath[position].equals(names[currentCity]) ? 0 : 1); 40 41 // Update if we found a better path 42 if (cost < dp[position][currentCity]) { 43 dp[position][currentCity] = cost; 44 predecessor[position][currentCity] = previousCity; 45 } 46 } 47 } 48 } 49 50 // Find the city with minimum cost at the last position 51 int minCost = INFINITY; 52 int lastCity = 0; 53 for (int city = 0; city < n; city++) { 54 if (dp[pathLength - 1][city] < minCost) { 55 minCost = dp[pathLength - 1][city]; 56 lastCity = city; 57 } 58 } 59 60 // Reconstruct the path by backtracking through predecessors 61 List<Integer> result = new ArrayList<>(); 62 for (int position = pathLength - 1; position >= 0; position--) { 63 result.add(lastCity); 64 lastCity = predecessor[position][lastCity]; 65 } 66 67 // Reverse to get the path from start to end 68 Collections.reverse(result); 69 return result; 70 } 71} 72
1class Solution { 2public: 3 vector<int> mostSimilar(int n, vector<vector<int>>& roads, vector<string>& names, vector<string>& targetPath) { 4 // Build adjacency list for the graph 5 vector<vector<int>> graph(n); 6 for (const auto& road : roads) { 7 int cityA = road[0]; 8 int cityB = road[1]; 9 graph[cityA].push_back(cityB); 10 graph[cityB].push_back(cityA); 11 } 12 13 int pathLength = targetPath.size(); 14 15 // dp[i][j] = minimum edit distance when visiting city j at position i in the path 16 vector<vector<int>> dp(pathLength, vector<int>(n, INT_MAX)); 17 18 // previous[i][j] = previous city when visiting city j at position i (for path reconstruction) 19 vector<vector<int>> previous(pathLength, vector<int>(n, -1)); 20 21 // Initialize first position: cost is 0 if names match, 1 if they don't 22 for (int city = 0; city < n; ++city) { 23 dp[0][city] = (targetPath[0] != names[city]) ? 1 : 0; 24 } 25 26 // Fill DP table for each position in the target path 27 for (int pos = 1; pos < pathLength; ++pos) { 28 for (int currentCity = 0; currentCity < n; ++currentCity) { 29 // Check all neighbors of current city 30 for (int prevCity : graph[currentCity]) { 31 // Calculate cost: previous cost + current mismatch cost 32 int cost = dp[pos - 1][prevCity] + ((targetPath[pos] != names[currentCity]) ? 1 : 0); 33 34 // Update if we found a better path 35 if (cost < dp[pos][currentCity]) { 36 dp[pos][currentCity] = cost; 37 previous[pos][currentCity] = prevCity; 38 } 39 } 40 } 41 } 42 43 // Find the ending city with minimum edit distance 44 int endCity = 0; 45 int minDistance = INT_MAX; 46 for (int city = 0; city < n; ++city) { 47 if (dp[pathLength - 1][city] < minDistance) { 48 minDistance = dp[pathLength - 1][city]; 49 endCity = city; 50 } 51 } 52 53 // Reconstruct the path by backtracking through previous array 54 vector<int> result(pathLength); 55 int currentCity = endCity; 56 for (int pos = pathLength - 1; pos >= 0; --pos) { 57 result[pos] = currentCity; 58 currentCity = previous[pos][currentCity]; 59 } 60 61 return result; 62 } 63}; 64
1/** 2 * Finds the most similar path in a graph to a target path 3 * Uses dynamic programming to minimize edit distance 4 * 5 * @param n - Number of cities/nodes in the graph 6 * @param roads - Array of bidirectional roads between cities [cityA, cityB] 7 * @param names - Array of city names where names[i] is the name of city i 8 * @param targetPath - Target path of city names to match 9 * @returns Array of city indices representing the most similar path 10 */ 11function mostSimilar( 12 n: number, 13 roads: number[][], 14 names: string[], 15 targetPath: string[], 16): number[] { 17 // Build adjacency list representation of the graph 18 const adjacencyList: number[][] = Array.from({ length: n }, () => []); 19 for (const [cityA, cityB] of roads) { 20 adjacencyList[cityA].push(cityB); 21 adjacencyList[cityB].push(cityA); 22 } 23 24 const pathLength = targetPath.length; 25 26 // DP table: minEditDistance[i][j] = minimum edit distance to reach position i ending at city j 27 const minEditDistance: number[][] = Array.from( 28 { length: pathLength }, 29 () => Array.from({ length: n }, () => Infinity) 30 ); 31 32 // Previous city tracker: previousCity[i][j] stores the previous city when at position i and city j 33 const previousCity: number[][] = Array.from( 34 { length: pathLength }, 35 () => Array.from({ length: n }, () => -1) 36 ); 37 38 // Initialize first position: cost is 0 if city name matches target, 1 otherwise 39 for (let city = 0; city < n; ++city) { 40 minEditDistance[0][city] = names[city] === targetPath[0] ? 0 : 1; 41 } 42 43 // Fill DP table for each position in the target path 44 for (let position = 1; position < pathLength; ++position) { 45 for (let currentCity = 0; currentCity < n; ++currentCity) { 46 // Check all neighboring cities that could lead to current city 47 for (const neighborCity of adjacencyList[currentCity]) { 48 // Calculate cost: previous cost + current mismatch cost 49 const cost = minEditDistance[position - 1][neighborCity] + 50 (names[currentCity] === targetPath[position] ? 0 : 1); 51 52 // Update if we found a better path 53 if (cost < minEditDistance[position][currentCity]) { 54 minEditDistance[position][currentCity] = cost; 55 previousCity[position][currentCity] = neighborCity; 56 } 57 } 58 } 59 } 60 61 // Find the ending city with minimum edit distance 62 let endCity = 0; 63 let minimumDistance = Infinity; 64 for (let city = 0; city < n; ++city) { 65 if (minEditDistance[pathLength - 1][city] < minimumDistance) { 66 minimumDistance = minEditDistance[pathLength - 1][city]; 67 endCity = city; 68 } 69 } 70 71 // Reconstruct the path by backtracking through previous cities 72 const resultPath: number[] = Array(pathLength).fill(0); 73 for (let position = pathLength - 1; position >= 0; --position) { 74 resultPath[position] = endCity; 75 endCity = previousCity[position][endCity]; 76 } 77 78 return resultPath; 79} 80

Solution Approach

The implementation uses dynamic programming with the following components:

1. Graph Construction: First, we build an adjacency list g where g[i] contains all cities directly connected to city i:

g = [[] for _ in range(n)] for a, b in roads:  g[a].append(b)  g[b].append(a)

2. DP Table Initialization: We create a 2D DP table f[i][j] where:

  • i represents the position in the target path (0 to m-1)
  • j represents the current city (0 to n-1)
  • f[i][j] stores the minimum edit distance to match the first i+1 elements of targetPath when ending at city j

We also maintain a predecessor table pre[i][j] to track which city we came from to achieve the optimal value at f[i][j].

3. Base Case: For the first position (i=0), we can start at any city. The cost is 0 if the city name matches targetPath[0], otherwise 1:

for j, s in enumerate(names):  f[0][j] = targetPath[0] != s # 0 if match, 1 if not

4. State Transition: For each subsequent position i in the target path and each possible current city j, we check all neighbors k of city j:

for i in range(1, m):  for j in range(n):  for k in g[j]:  t = f[i-1][k] + (targetPath[i] != names[j])  if t < f[i][j]:  f[i][j] = t  pre[i][j] = k

The transition formula is: f[i][j] = min(f[i-1][k] + cost) where k is a neighbor of j and cost = 1 if names[j] != targetPath[i], otherwise cost = 0.

5. Finding the Optimal End City: After filling the DP table, we find which city at the last position gives the minimum edit distance:

k = 0 mi = inf for j in range(n):  if f[-1][j] < mi:  mi = f[-1][j]  k = j

6. Path Reconstruction: Using the predecessor array, we backtrack from the optimal end city to reconstruct the entire path:

ans = [0] * m for i in range(m - 1, -1, -1):  ans[i] = k  k = pre[i][k]

The time complexity is O(m × n × d) where m is the length of targetPath, n is the number of cities, and d is the average degree of cities. The space complexity is O(m × n) for the DP and predecessor tables.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate the solution approach.

Given:

  • Cities: [0, 1, 2, 3] with names ["AAA", "BBB", "CCC", "AAA"]
  • Roads: [[0,1], [1,2], [2,3], [1,3]]
  • Target Path: ["AAA", "CCC", "AAA"]

Graph Structure:

 0(AAA)---1(BBB)---2(CCC)  | |  +----3(AAA)+

Step 1: Initialize DP Table

For position 0 (matching "AAA"):

  • f[0][0] = 0 (city 0 has name "AAA" - matches!)
  • f[0][1] = 1 (city 1 has name "BBB" - no match)
  • f[0][2] = 1 (city 2 has name "CCC" - no match)
  • f[0][3] = 0 (city 3 has name "AAA" - matches!)

Step 2: Fill position 1 (matching "CCC")

For city 0 at position 1:

  • Neighbors of 0: [1]
  • From city 1: f[0][1] + (targetPath[1] != "AAA") = 1 + 1 = 2
  • So f[1][0] = 2, pre[1][0] = 1

For city 1 at position 1:

  • Neighbors of 1: [0, 2, 3]
  • From city 0: f[0][0] + (targetPath[1] != "BBB") = 0 + 1 = 1
  • From city 2: f[0][2] + (targetPath[1] != "BBB") = 1 + 1 = 2
  • From city 3: f[0][3] + (targetPath[1] != "BBB") = 0 + 1 = 1
  • Best is 1, so f[1][1] = 1, pre[1][1] = 0 (or 3)

For city 2 at position 1:

  • Neighbors of 2: [1, 3]
  • From city 1: f[0][1] + (targetPath[1] != "CCC") = 1 + 0 = 1
  • From city 3: f[0][3] + (targetPath[1] != "CCC") = 0 + 0 = 0
  • Best is 0, so f[1][2] = 0, pre[1][2] = 3

For city 3 at position 1:

  • Neighbors of 3: [1, 2]
  • From city 1: f[0][1] + (targetPath[1] != "AAA") = 1 + 1 = 2
  • From city 2: f[0][2] + (targetPath[1] != "AAA") = 1 + 1 = 2
  • So f[1][3] = 2, pre[1][3] = 1 (or 2)

Step 3: Fill position 2 (matching "AAA")

For city 0 at position 2:

  • Neighbors of 0: [1]
  • From city 1: f[1][1] + (targetPath[2] != "AAA") = 1 + 0 = 1
  • So f[2][0] = 1, pre[2][0] = 1

For city 1 at position 2:

  • Neighbors of 1: [0, 2, 3]
  • From city 0: f[1][0] + (targetPath[2] != "BBB") = 2 + 1 = 3
  • From city 2: f[1][2] + (targetPath[2] != "BBB") = 0 + 1 = 1
  • From city 3: f[1][3] + (targetPath[2] != "BBB") = 2 + 1 = 3
  • Best is 1, so f[2][1] = 1, pre[2][1] = 2

For city 2 at position 2:

  • Neighbors of 2: [1, 3]
  • From city 1: f[1][1] + (targetPath[2] != "CCC") = 1 + 1 = 2
  • From city 3: f[1][3] + (targetPath[2] != "CCC") = 2 + 1 = 3
  • So f[2][2] = 2, pre[2][2] = 1

For city 3 at position 2:

  • Neighbors of 3: [1, 2]
  • From city 1: f[1][1] + (targetPath[2] != "AAA") = 1 + 0 = 1
  • From city 2: f[1][2] + (targetPath[2] != "AAA") = 0 + 0 = 0
  • Best is 0, so f[2][3] = 0, pre[2][3] = 2

Step 4: Find optimal end and reconstruct path

Minimum at position 2: f[2][3] = 0 (city 3 is best)

Backtracking:

  • Position 2: city 3
  • Position 1: pre[2][3] = 2 → city 2
  • Position 0: pre[1][2] = 3 → city 3

Final path: [3, 2, 3] with edit distance 0 (perfect match: "AAA", "CCC", "AAA")

Time and Space Complexity

Time Complexity: O(m × n²)

The algorithm uses dynamic programming where:

  • The outer loop iterates through the targetPath of length m (from index 1 to m-1)
  • For each position i, we iterate through all n cities (variable j)
  • For each city j, we iterate through all its neighbors in the adjacency list g[j]

In the worst case, a city can be connected to all other n-1 cities, making the innermost loop run up to n-1 times. Therefore, the total time complexity is O(m × n × n) = O(m × n²).

Additionally, building the adjacency list takes O(E) where E is the number of edges (roads), and the final path reconstruction takes O(m) time, but these don't affect the overall complexity.

Space Complexity: O(m × n)

The space is used for:

  • The DP table f: a 2D array of size m × n storing minimum edit distances
  • The predecessor table pre: a 2D array of size m × n storing the previous city in the optimal path
  • The adjacency list g: takes O(n + E) space where E is the number of edges
  • The answer array ans: takes O(m) space

The dominant space usage comes from the two 2D arrays (f and pre), each requiring O(m × n) space, resulting in an overall space complexity of O(m × n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Graph Connectivity Assumption

A critical pitfall is assuming the graph is always fully connected without validation. If the graph has disconnected components, the DP approach might fail to find valid paths or produce incorrect results.

Problem Example:

# Disconnected graph scenario n = 4 roads = [[0, 1], [2, 3]] # Cities 0-1 are connected, 2-3 are connected, but no path from 0/1 to 2/3 names = ["AAA", "BBB", "CCC", "DDD"] targetPath = ["AAA", "CCC"] # Requires going from city 0 to city 2

Solution: Add validation to ensure reachable paths exist:

# After filling DP table, check if any valid path exists valid_path_exists = False for city in range(n):  if dp[path_length - 1][city] < float('inf'):  valid_path_exists = True  break  if not valid_path_exists:  # Handle the case - could return empty list or raise exception  return []

2. Predecessor Array Not Properly Initialized for Starting Cities

The predecessor array for the first position remains -1, which can cause issues during path reconstruction if not handled carefully.

Problem Example: When backtracking, accessing predecessor[0][current_city] returns -1, which might be used incorrectly if the reconstruction loop isn't properly bounded.

Solution: Ensure the path reconstruction loop properly handles the first position:

# Modified reconstruction that explicitly handles the boundary result_path = [0] * path_length current_city = best_last_city for position in range(path_length - 1, 0, -1): # Stop at position 1, not 0  result_path[position] = current_city  current_city = predecessor[position][current_city] result_path[0] = current_city # Set the first city

3. Self-Loop Roads Breaking DP Logic

If the input contains self-loops (a city connected to itself), the DP might incorrectly consider staying in the same city as a valid transition.

Problem Example:

roads = [[0, 0], [0, 1], [1, 2]] # City 0 has a self-loop

Solution: Filter out self-loops when building the adjacency list:

adjacency_list = [[] for _ in range(n)] for city_a, city_b in roads:  if city_a != city_b: # Exclude self-loops  adjacency_list[city_a].append(city_b)  adjacency_list[city_b].append(city_a)

4. Memory Optimization Overlooked

For large inputs, the full DP table might consume excessive memory when only the previous row is needed for computation.

Solution: Use rolling array optimization:

# Instead of dp[path_length][n], use two arrays prev_dp = [float('inf')] * n curr_dp = [float('inf')] * n  # Initialize first position for city in range(n):  prev_dp[city] = 0 if names[city] == targetPath[0] else 1  # Process remaining positions for position in range(1, path_length):  curr_dp = [float('inf')] * n  for current_city in range(n):  for previous_city in adjacency_list[current_city]:  mismatch_cost = 0 if names[current_city] == targetPath[position] else 1  curr_dp[current_city] = min(curr_dp[current_city],  prev_dp[previous_city] + mismatch_cost)  prev_dp = curr_dp

Note: This optimization requires maintaining the full predecessor array for path reconstruction, so it only saves partial memory.

Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

The three-steps of Depth First Search are:

  1. Identify states;
  2. Draw the state-space tree;
  3. DFS on the state-space tree.

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More