Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

1548. The Most Similar Path in a Graph - Leetcode Solution

Problem Description

You are given an undirected connected graph with n nodes, labeled from 0 to n - 1. The graph is represented by an edge list roads, where each edge connects two nodes.

You are also given a list of city names names, where names[i] is the name of node i, and a target path targetPath (a list of city names).

Your task is to find a path in the graph of the same length as targetPath (using nodes, possibly with repeats), such that the edit distance (the number of positions where the node's city name is different from the corresponding name in targetPath) is minimized.

Return the sequence of node indices of the most similar path. There is always at least one valid solution.

Constraints:

  • There is always at least one path of the required length.
  • You may reuse nodes in the path.
  • The answer must be a sequence of node indices of length equal to targetPath.
  • Each step in the path must move to an adjacent node (including possibly staying at the same node if allowed by the graph).

Thought Process

At a glance, this problem is about matching a sequence of city names to a path in a graph, minimizing the number of mismatches.

A brute-force solution would be to try every possible path of the required length and count mismatches, but the number of possible paths grows exponentially with the path length, making brute-force impractical for larger graphs or longer paths.

We need to optimize. Since the mismatch cost at each step depends only on the current node and the current position in targetPath, and the next step depends only on the current node, this suggests a dynamic programming (DP) approach. We can use memoization to avoid recomputing results for the same subproblems.

The problem is reminiscent of finding the shortest path, but instead of edge weights, our "cost" is the mismatch at each step. We want to build the path step by step, always keeping track of the minimal mismatch cost for each possible current node at each position in the targetPath.

Solution Approach

The optimized solution uses dynamic programming (DP) with memoization. Here is the step-by-step breakdown:

  1. Graph Representation:
    • Build an adjacency list from the roads input for fast neighbor lookup.
  2. DP State Definition:
    • Let dp[pos][city] be the minimal mismatch cost to reach position pos in targetPath when at node city.
  3. DP Transition:
    • For each position pos in targetPath and each node city, consider moving to any neighbor next_city in the next step.
    • The cost at each step is 0 if names[city] == targetPath[pos], otherwise 1.
    • Update dp[pos+1][next_city] if moving from city to next_city yields a lower cost.
  4. Initialization:
    • At pos = 0, for every node city, set dp[0][city] to 0 if names[city] == targetPath[0], else 1.
  5. Reconstruction:
    • After filling the DP table, find the node at the final position with the minimal cost.
    • Reconstruct the path by backtracking using a parent table that records from which node each step was reached.

Why DP? Because the cost at each step depends only on the current node and position, DP avoids redundant calculations and efficiently finds the minimal mismatch path.

Example Walkthrough

Example Input:

  • n = 5
  • roads = [[0,2],[0,3],[1,2],[1,3],[1,4],[2,4]]
  • names = ["ATL","PEK","LAX","DXB","HND"]
  • targetPath = ["ATL","DXB","HND","LAX"]
Step-by-step:
  1. At position 0 ("ATL"), try all nodes. Only node 0 matches ("ATL"), so dp[0][0]=0, others get 1.
  2. At position 1 ("DXB"), from each node, try moving to neighbors. For node 0 (ATL), neighbors are 2 and 3. Node 3 ("DXB") matches, so dp[1][3]=0 (reached from 0 with cost 0+0=0).
  3. Continue for each position, always updating the minimal cost and tracking the parent for each step.
  4. At the end, backtrack from the node with the minimal cost at the final position to reconstruct the path.
  5. For this example, the most similar path is [0,3,4,2], corresponding to ["ATL","DXB","HND","LAX"], which matches the target exactly (cost 0).
Why this works: At each step, the DP ensures we only keep the best paths, and the parent tracking lets us reconstruct the actual sequence.

Time and Space Complexity

Brute-force Approach:

  • Time: O(n^L), where n is the number of nodes and L is the length of targetPath (all possible paths).
  • Space: O(L) for the recursion stack or path storage.
Optimized DP Approach:
  • Time: O(L * n^2), where for each of L positions, we consider all n nodes and their neighbors (up to n per node in the worst case).
  • Space: O(L * n) for the DP and parent tables.
  • This is efficient for the given constraints and avoids exponential blowup.

Summary

This problem is about finding a path in a graph that best matches a sequence of city names, minimizing mismatches. Brute-force is too slow, so we use dynamic programming to efficiently compute the minimal mismatch path. By defining the right DP state and using parent pointers for reconstruction, we solve the problem in polynomial time and space. The approach demonstrates the power of DP for pathfinding problems with custom cost functions.

Code Implementation

from collections import defaultdict

class Solution:
    def mostSimilar(self, n, roads, names, targetPath):
        graph = [[] for _ in range(n)]
        for u, v in roads:
            graph[u].append(v)
            graph[v].append(u)

        L = len(targetPath)
        dp = [[float('inf')] * n for _ in range(L)]
        parent = [[-1] * n for _ in range(L)]

        for city in range(n):
            dp[0][city] = int(names[city] != targetPath[0])

        for i in range(1, L):
            for city in range(n):
                for nei in graph[city]:
                    cost = dp[i-1][nei] + int(names[city] != targetPath[i])
                    if cost < dp[i][city]:
                        dp[i][city] = cost
                        parent[i][city] = nei

        # Find end city with minimal cost
        min_cost = float('inf')
        last_city = -1
        for city in range(n):
            if dp[L-1][city] < min_cost:
                min_cost = dp[L-1][city]
                last_city = city

        # Reconstruct path
        path = [0] * L
        path[-1] = last_city
        for i in range(L-1, 0, -1):
            path[i-1] = parent[i][path[i]]
        return path
      
#include <vector>
#include <string>
#include <algorithm>
#include <climits>
using namespace std;

class Solution {
public:
    vector<int> mostSimilar(int n, vector<vector<int>>& roads, vector<string>& names, vector<string>& targetPath) {
        vector<vector<int>> graph(n);
        for (auto& r : roads) {
            graph[r[0]].push_back(r[1]);
            graph[r[1]].push_back(r[0]);
        }
        int L = targetPath.size();
        vector<vector<int>> dp(L, vector<int>(n, INT_MAX));
        vector<vector<int>> parent(L, vector<int>(n, -1));

        for (int city = 0; city < n; ++city) {
            dp[0][city] = (names[city] != targetPath[0]);
        }

        for (int i = 1; i < L; ++i) {
            for (int city = 0; city < n; ++city) {
                for (int nei : graph[city]) {
                    int cost = dp[i-1][nei] + (names[city] != targetPath[i]);
                    if (cost < dp[i][city]) {
                        dp[i][city] = cost;
                        parent[i][city] = nei;
                    }
                }
            }
        }

        int min_cost = INT_MAX, last_city = -1;
        for (int city = 0; city < n; ++city) {
            if (dp[L-1][city] < min_cost) {
                min_cost = dp[L-1][city];
                last_city = city;
            }
        }

        vector<int> path(L);
        path[L-1] = last_city;
        for (int i = L-1; i > 0; --i) {
            path[i-1] = parent[i][path[i]];
        }
        return path;
    }
};
      
import java.util.*;

class Solution {
    public int[] mostSimilar(int n, int[][] roads, String[] names, String[] targetPath) {
        List<List<Integer>> graph = new ArrayList<>();
        for (int i = 0; i < n; ++i) graph.add(new ArrayList<>());
        for (int[] r : roads) {
            graph.get(r[0]).add(r[1]);
            graph.get(r[1]).add(r[0]);
        }
        int L = targetPath.length;
        int[][] dp = new int[L][n];
        int[][] parent = new int[L][n];
        for (int[] row : dp) Arrays.fill(row, Integer.MAX_VALUE);
        for (int[] row : parent) Arrays.fill(row, -1);

        for (int city = 0; city < n; ++city) {
            dp[0][city] = names[city].equals(targetPath[0]) ? 0 : 1;
        }

        for (int i = 1; i < L; ++i) {
            for (int city = 0; city < n; ++city) {
                for (int nei : graph.get(city)) {
                    int cost = dp[i-1][nei] + (names[city].equals(targetPath[i]) ? 0 : 1);
                    if (cost < dp[i][city]) {
                        dp[i][city] = cost;
                        parent[i][city] = nei;
                    }
                }
            }
        }

        int minCost = Integer.MAX_VALUE, lastCity = -1;
        for (int city = 0; city < n; ++city) {
            if (dp[L-1][city] < minCost) {
                minCost = dp[L-1][city];
                lastCity = city;
            }
        }

        int[] path = new int[L];
        path[L-1] = lastCity;
        for (int i = L-1; i > 0; --i) {
            path[i-1] = parent[i][path[i]];
        }
        return path;
    }
}
      
var mostSimilar = function(n, roads, names, targetPath) {
    const graph = Array.from({length: n}, () => []);
    for (const [u, v] of roads) {
        graph[u].push(v);
        graph[v].push(u);
    }
    const L = targetPath.length;
    const dp = Array.from({length: L}, () => Array(n).fill(Infinity));
    const parent = Array.from({length: L}, () => Array(n).fill(-1));

    for (let city = 0; city < n; ++city) {
        dp[0][city] = names[city] === targetPath[0] ? 0 : 1;
    }

    for (let i = 1; i < L; ++i) {
        for (let city = 0; city < n; ++city) {
            for (const nei of graph[city]) {
                const cost = dp[i-1][nei] + (names[city] === targetPath[i] ? 0 : 1);
                if (cost < dp[i][city]) {
                    dp[i][city] = cost;
                    parent[i][city] = nei;
                }
            }
        }
    }

    let minCost = Infinity, lastCity = -1;
    for (let city = 0; city < n; ++city) {
        if (dp[L-1][city] < minCost) {
            minCost = dp[L-1][city];
            lastCity = city;
        }
    }

    const path = Array(L);
    path[L-1] = lastCity;
    for (let i = L-1; i > 0; --i) {
        path[i-1] = parent[i][path[i]];
    }
    return path;
};