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:
- Has the same length as a given
targetPath
array - 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.
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:
- We need to ensure cities are connected by roads (path validity)
- 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 firsti+1
elements of targetPath when ending at cityj
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 EvaluatorExample 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 alln
cities (variablej
) - For each city
j
, we iterate through all its neighbors in the adjacency listg[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 sizem × n
storing minimum edit distances - The predecessor table
pre
: a 2D array of sizem × n
storing the previous city in the optimal path - The adjacency list
g
: takesO(n + E)
space where E is the number of edges - The answer array
ans
: takesO(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.
In a binary min heap, the minimum element can be found in:
Recommended Readings
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
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!