Facebook Pixel

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 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 3-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")

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

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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the minimum element can be found in:


Recommended Readings

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

Load More