Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

1923. Longest Common Subpath - Leetcode Solution

Problem Description

The "Longest Common Subpath" problem asks you to determine the length of the longest path (as a sequence of city IDs) that appears as a contiguous subpath in every given path. Each path is an array of integers representing cities visited in order. The goal is to find the maximum length k such that there exists a sequence of k consecutive cities present in all the input paths.

  • Each path can have a different length.
  • The subpath must be contiguous (cities must be consecutive in the path).
  • City IDs can be reused across different paths.
  • There is always at least one path, and each path contains at least one city.
  • Return the maximum possible length k of a common subpath.

Thought Process

At first glance, the problem seems to invite a brute-force approach: try all possible subpaths for one path and check if they appear in all others. However, with potentially large input sizes, this would be far too slow.

To optimize, we need to efficiently check whether a subpath of a certain length exists in all paths. This is a classic scenario for binary search: if a subpath of length k exists, then so does any shorter length. So, we can binary search on the possible length of the subpath.

But how do we check efficiently if a subpath of length k exists in all paths? We need a fast way to compare subpaths, which leads us to hashing techniques. By using a rolling hash (like Rabin-Karp), we can quickly hash all subpaths of length k in each path and check for common hashes.

Solution Approach

  • Binary Search on Subpath Length:
    • Let left be 0 and right be the minimum path length among all paths.
    • For each candidate length mid, check if there is a common subpath of length mid in all paths.
    • If such a subpath exists, try a longer length; otherwise, try a shorter one.
  • Rolling Hash for Subpath Comparison:
    • For each path, use a rolling hash to compute hashes of all subpaths of length mid.
    • For the first path, store all hashes in a set.
    • For each subsequent path, keep only those hashes that also appear in the current path's set.
    • If, after processing all paths, the intersection set is non-empty, then there is a common subpath of this length.
  • Why Rolling Hash?
    • Subpaths are sequences of integers; we need a way to quickly and uniquely identify them.
    • Rolling hash allows us to compute the hash of a subpath in constant time as we slide the window.
    • By using a large modulus and base, we minimize the chance of hash collisions.
  • Final Steps:
    • The answer is the largest k for which a common subpath exists in all paths.

Example Walkthrough

Suppose we have the following input:

  • Paths: [[1,2,3,4], [2,3,4], [3,4,5]]
  1. The minimum path length is 3.
  2. Binary search tries mid = 2:
    • Subpaths of length 2: [1,2], [2,3], [3,4] (first path), [2,3], [3,4] (second), [3,4], [4,5] (third).
    • The only subpath of length 2 present in all is [3,4].
  3. Try mid = 3:
    • Subpaths: [1,2,3], [2,3,4] (first), [2,3,4] (second), [3,4,5] (third).
    • No subpath of length 3 is present in all paths.
  4. The answer is 2, since that's the longest length with a common subpath ([3,4]).

Time and Space Complexity

  • Brute-force:
    • Enumerate all subpaths of each path and check for commonality.
    • Time complexity: Exponential in the worst case, as the number of subpaths grows rapidly with path length.
  • Optimized (Binary Search + Rolling Hash):
    • Let n be the number of paths, m the minimum path length, and L the total length of all paths.
    • Binary search runs in O(\log m) iterations.
    • For each iteration, we compute rolling hashes for all subpaths of length k in each path: O(L) per iteration.
    • Total time: O(L \cdot \log m).
    • Space: O(L) for storing hashes.

Summary

The Longest Common Subpath problem is efficiently solved by combining binary search and rolling hash. Binary search narrows down the candidate subpath length, and rolling hash allows us to compare subpaths quickly across all paths. By intersecting sets of hashed subpaths, we determine the existence of a common subpath. This approach is both time and space efficient, making it suitable for large input sizes and demonstrating the power of combining classic algorithmic techniques.

Code Implementation

class Solution:
    def longestCommonSubpath(self, n, paths):
        def check(length):
            MOD = (1 << 61) - 1
            BASE = 10**5 + 7
            common = None
            for idx, path in enumerate(paths):
                h, power = 0, 1
                hashes = set()
                for i in range(length):
                    h = (h * BASE + path[i]) % MOD
                    if i < length - 1:
                        power = (power * BASE) % MOD
                hashes.add(h)
                for i in range(length, len(path)):
                    h = (h - path[i - length] * power) % MOD
                    h = (h * BASE + path[i]) % MOD
                    hashes.add(h)
                if common is None:
                    common = hashes
                else:
                    common = common & hashes
                if not common:
                    return False
            return True

        left, right = 0, min(len(p) for p in paths)
        while left < right:
            mid = (left + right + 1) // 2
            if check(mid):
                left = mid
            else:
                right = mid - 1
        return left
      
class Solution {
public:
    int longestCommonSubpath(int n, vector<vector<int>>& paths) {
        auto check = [&](int len) {
            const long long MOD = (1LL << 61) - 1;
            const long long BASE = 100007;
            unordered_set<long long> common;
            for (int idx = 0; idx < paths.size(); ++idx) {
                long long h = 0, power = 1;
                unordered_set<long long> hashes;
                for (int i = 0; i < len; ++i) {
                    h = (h * BASE + paths[idx][i]) % MOD;
                    if (i < len - 1) power = (power * BASE) % MOD;
                }
                hashes.insert(h);
                for (int i = len; i < paths[idx].size(); ++i) {
                    h = (h - paths[idx][i - len] * power % MOD + MOD) % MOD;
                    h = (h * BASE + paths[idx][i]) % MOD;
                    hashes.insert(h);
                }
                if (idx == 0) {
                    common = hashes;
                } else {
                    unordered_set<long long> temp;
                    for (auto x : hashes) {
                        if (common.count(x)) temp.insert(x);
                    }
                    common = temp;
                }
                if (common.empty()) return false;
            }
            return true;
        };
        int left = 0, right = INT_MAX;
        for (auto& path : paths) right = min(right, (int)path.size());
        while (left < right) {
            int mid = (left + right + 1) / 2;
            if (check(mid)) left = mid;
            else right = mid - 1;
        }
        return left;
    }
};
      
class Solution {
    public int longestCommonSubpath(int n, int[][] paths) {
        long MOD = (1L << 61) - 1;
        long BASE = 100007;

        int minLen = Integer.MAX_VALUE;
        for (int[] path : paths) minLen = Math.min(minLen, path.length);

        int left = 0, right = minLen;
        while (left < right) {
            int mid = (left + right + 1) / 2;
            if (check(paths, mid, BASE, MOD)) left = mid;
            else right = mid - 1;
        }
        return left;
    }

    private boolean check(int[][] paths, int len, long BASE, long MOD) {
        Set<Long> common = null;
        for (int[] path : paths) {
            long h = 0, power = 1;
            Set<Long> hashes = new HashSet<>();
            for (int i = 0; i < len; ++i) {
                h = (h * BASE + path[i]) % MOD;
                if (i < len - 1) power = (power * BASE) % MOD;
            }
            hashes.add(h);
            for (int i = len; i < path.length; ++i) {
                h = (h - path[i - len] * power % MOD + MOD) % MOD;
                h = (h * BASE + path[i]) % MOD;
                hashes.add(h);
            }
            if (common == null) {
                common = hashes;
            } else {
                common.retainAll(hashes);
            }
            if (common.isEmpty()) return false;
        }
        return true;
    }
}
      
var longestCommonSubpath = function(n, paths) {
    function check(len) {
        const MOD = BigInt((1n << 61n) - 1n);
        const BASE = BigInt(100007);
        let common = null;
        for (const path of paths) {
            let h = 0n, power = 1n;
            const hashes = new Set();
            for (let i = 0; i < len; ++i) {
                h = (h * BASE + BigInt(path[i])) % MOD;
                if (i < len - 1) power = (power * BASE) % MOD;
            }
            hashes.add(h.toString());
            for (let i = len; i < path.length; ++i) {
                h = (h - BigInt(path[i - len]) * power % MOD + MOD) % MOD;
                h = (h * BASE + BigInt(path[i])) % MOD;
                hashes.add(h.toString());
            }
            if (common === null) {
                common = hashes;
            } else {
                common = new Set([...common].filter(x => hashes.has(x)));
            }
            if (common.size === 0) return false;
        }
        return true;
    }
    let left = 0, right = Math.min(...paths.map(p => p.length));
    while (left < right) {
        let mid = Math.floor((left + right + 1) / 2);
        if (check(mid)) left = mid;
        else right = mid - 1;
    }
    return left;
};