1923. Longest Common Subpath
Problem Description
You have a country with n cities numbered from 0 to n - 1. Every pair of cities in this country is connected by a road.
There are m friends (numbered from 0 to m - 1) traveling through the country. Each friend follows a specific path through the cities, represented as an array of city numbers in the order they visit them. A friend can visit the same city multiple times in their journey, but the same city won't appear consecutively in their path.
Given:
- An integer
n(number of cities) - A 2D array
pathswherepaths[i]represents the travel path of thei-thfriend
Your task is to find the length of the longest common subpath that appears in every friend's path. If no common subpath exists among all friends, return 0.
A subpath is defined as a contiguous sequence of cities within a path. For example, if a friend's path is [1, 2, 3, 4], then [2, 3] is a subpath, but [1, 3] is not (since it's not contiguous).
The solution uses binary search combined with rolling hash to efficiently find the longest common subpath. The algorithm:
- Binary searches on the possible length of the common subpath (from 0 to the minimum path length)
- For each candidate length
k, uses rolling hash to check if there exists a subpath of lengthkthat appears in all friends' paths - The rolling hash technique allows checking all subpaths of a given length in linear time by computing hash values incrementally
- Returns the maximum length where a common subpath exists among all friends
Intuition
The key insight is recognizing a monotonic property: if a common subpath of length k exists among all friends' paths, then any common subpath of length less than k also exists. This creates a feasibility sequence:
Length k: 0 1 2 3 4 5 6 Feasible: T T T T F F F ↑ first_true_index (maximum feasible)
We want to find the maximum length where a common subpath exists. Using our standard binary search template, we track first_true_index as we search.
For each candidate length k, we define feasible(k) as: does there exist a subpath of length k that appears in all friends' paths?
To check efficiently, we use rolling hash:
- Compute hash values for all subpaths of length
kin each path - Track unique hashes per friend (avoid double-counting)
- Count occurrences across all friends
- If any hash appears in all
mpaths, return true
The rolling hash formula: for subpath [a, b, c], hash = a * base^2 + b * base^1 + c * base^0. Adjacent hashes can be computed in O(1) using prefix hashes.
Learn more about Binary Search patterns.
Solution Implementation
1class Solution:
2 def longestCommonSubpath(self, n: int, paths: List[List[int]]) -> int:
3 num_paths = len(paths)
4 max_len = max(len(path) for path in paths)
5
6 # Hash parameters
7 BASE = 133331
8 MOD = 2**64 + 1
9
10 # Precompute powers of base
11 base_powers = [1] * (max_len + 1)
12 for i in range(1, len(base_powers)):
13 base_powers[i] = base_powers[i - 1] * BASE % MOD
14
15 # Compute prefix hashes for all paths
16 path_hashes = []
17 for path in paths:
18 h = [0] * (len(path) + 1)
19 for i, city in enumerate(path, 1):
20 h[i] = (h[i - 1] * BASE + city) % MOD
21 path_hashes.append(h)
22
23 def feasible(length: int) -> bool:
24 """Check if common subpath of given length exists."""
25 if length == 0:
26 return True
27 hash_count = Counter()
28 for h in path_hashes:
29 seen = set()
30 for i in range(1, len(h) - length + 1):
31 j = i + length - 1
32 subhash = (h[j] - h[i - 1] * base_powers[length]) % MOD
33 if subhash not in seen:
34 seen.add(subhash)
35 hash_count[subhash] += 1
36 return max(hash_count.values()) == num_paths
37
38 # Binary search with standard template
39 left, right = 0, min(len(path) for path in paths)
40 first_true_index = 0
41
42 while left <= right:
43 mid = (left + right) // 2
44
45 if feasible(mid):
46 first_true_index = mid
47 left = mid + 1
48 else:
49 right = mid - 1
50
51 return first_true_index
521class Solution {
2 private static final long BASE = 133331L;
3 private long[] prefixHash;
4 private long[] basePowers;
5 private int[][] paths;
6 private Map<Long, Integer> hashCount = new HashMap<>();
7 private Map<Long, Integer> lastPathIndex = new HashMap<>();
8
9 public int longestCommonSubpath(int n, int[][] paths) {
10 this.paths = paths;
11
12 int minLen = Integer.MAX_VALUE;
13 int maxLen = 0;
14 for (int[] path : paths) {
15 minLen = Math.min(minLen, path.length);
16 maxLen = Math.max(maxLen, path.length);
17 }
18
19 prefixHash = new long[maxLen + 1];
20 basePowers = new long[maxLen + 1];
21
22 // Binary search with standard template
23 int left = 0;
24 int right = minLen;
25 int firstTrueIndex = 0;
26
27 while (left <= right) {
28 int mid = left + (right - left) / 2;
29
30 if (feasible(mid)) {
31 firstTrueIndex = mid;
32 left = mid + 1;
33 } else {
34 right = mid - 1;
35 }
36 }
37
38 return firstTrueIndex;
39 }
40
41 private boolean feasible(int length) {
42 if (length == 0) return true;
43
44 hashCount.clear();
45 lastPathIndex.clear();
46 basePowers[0] = 1;
47
48 for (int pathIdx = 0; pathIdx < paths.length; pathIdx++) {
49 int[] path = paths[pathIdx];
50
51 for (int i = 1; i <= path.length; i++) {
52 basePowers[i] = basePowers[i - 1] * BASE;
53 prefixHash[i] = prefixHash[i - 1] * BASE + path[i - 1];
54 }
55
56 for (int end = length; end <= path.length; end++) {
57 int start = end - length + 1;
58 long hash = prefixHash[end] - prefixHash[start - 1] * basePowers[length];
59
60 if (!lastPathIndex.containsKey(hash) || lastPathIndex.get(hash) != pathIdx) {
61 lastPathIndex.put(hash, pathIdx);
62 hashCount.merge(hash, 1, Integer::sum);
63 }
64 }
65 }
66
67 int maxCount = 0;
68 for (int count : hashCount.values()) {
69 maxCount = Math.max(maxCount, count);
70 }
71 return maxCount == paths.length;
72 }
73}
741class Solution {
2private:
3 static constexpr long long BASE = 133331LL;
4 vector<long long> prefixHash;
5 vector<long long> basePowers;
6 vector<vector<int>> paths;
7 unordered_map<long long, int> hashCount;
8 unordered_map<long long, int> lastPathIdx;
9
10public:
11 int longestCommonSubpath(int n, vector<vector<int>>& paths) {
12 this->paths = paths;
13
14 int minLen = INT_MAX, maxLen = 0;
15 for (auto& path : paths) {
16 minLen = min(minLen, (int)path.size());
17 maxLen = max(maxLen, (int)path.size());
18 }
19
20 prefixHash.resize(maxLen + 1);
21 basePowers.resize(maxLen + 1);
22
23 // Binary search with standard template
24 int left = 0, right = minLen;
25 int firstTrueIndex = 0;
26
27 while (left <= right) {
28 int mid = left + (right - left) / 2;
29
30 if (feasible(mid)) {
31 firstTrueIndex = mid;
32 left = mid + 1;
33 } else {
34 right = mid - 1;
35 }
36 }
37
38 return firstTrueIndex;
39 }
40
41private:
42 bool feasible(int length) {
43 if (length == 0) return true;
44
45 hashCount.clear();
46 lastPathIdx.clear();
47 basePowers[0] = 1;
48
49 for (int pathIdx = 0; pathIdx < paths.size(); pathIdx++) {
50 auto& path = paths[pathIdx];
51
52 for (int i = 1; i <= path.size(); i++) {
53 basePowers[i] = basePowers[i - 1] * BASE;
54 prefixHash[i] = prefixHash[i - 1] * BASE + path[i - 1];
55 }
56
57 for (int end = length; end <= path.size(); end++) {
58 int start = end - length + 1;
59 long long hash = prefixHash[end] - prefixHash[start - 1] * basePowers[length];
60
61 if (lastPathIdx.find(hash) == lastPathIdx.end() || lastPathIdx[hash] != pathIdx) {
62 lastPathIdx[hash] = pathIdx;
63 hashCount[hash]++;
64 }
65 }
66 }
67
68 int maxCount = 0;
69 for (auto& [h, cnt] : hashCount) {
70 maxCount = max(maxCount, cnt);
71 }
72 return maxCount == (int)paths.size();
73 }
74};
751function longestCommonSubpath(n: number, paths: number[][]): number {
2 const BASE = 133331n;
3
4 let minLen = Infinity, maxLen = 0;
5 for (const path of paths) {
6 minLen = Math.min(minLen, path.length);
7 maxLen = Math.max(maxLen, path.length);
8 }
9
10 const basePowers: bigint[] = new Array(maxLen + 1);
11 basePowers[0] = 1n;
12 for (let i = 1; i <= maxLen; i++) {
13 basePowers[i] = basePowers[i - 1] * BASE;
14 }
15
16 // Compute prefix hashes
17 const pathHashes: bigint[][] = [];
18 for (const path of paths) {
19 const h: bigint[] = new Array(path.length + 1);
20 h[0] = 0n;
21 for (let i = 1; i <= path.length; i++) {
22 h[i] = h[i - 1] * BASE + BigInt(path[i - 1]);
23 }
24 pathHashes.push(h);
25 }
26
27 const feasible = (length: number): boolean => {
28 if (length === 0) return true;
29
30 const hashCount = new Map<bigint, number>();
31 const lastIdx = new Map<bigint, number>();
32
33 for (let pathIdx = 0; pathIdx < paths.length; pathIdx++) {
34 const h = pathHashes[pathIdx];
35
36 for (let end = length; end < h.length; end++) {
37 const start = end - length + 1;
38 const hash = h[end] - h[start - 1] * basePowers[length];
39
40 if (!lastIdx.has(hash) || lastIdx.get(hash) !== pathIdx) {
41 lastIdx.set(hash, pathIdx);
42 hashCount.set(hash, (hashCount.get(hash) || 0) + 1);
43 }
44 }
45 }
46
47 let maxCount = 0;
48 for (const count of hashCount.values()) {
49 maxCount = Math.max(maxCount, count);
50 }
51 return maxCount === paths.length;
52 };
53
54 // Binary search with standard template
55 let left = 0, right = minLen;
56 let firstTrueIndex = 0;
57
58 while (left <= right) {
59 const mid = Math.floor((left + right) / 2);
60
61 if (feasible(mid)) {
62 firstTrueIndex = mid;
63 left = mid + 1;
64 } else {
65 right = mid - 1;
66 }
67 }
68
69 return firstTrueIndex;
70}
71Solution Approach
The solution uses binary search with the standard template, combined with rolling hash to check feasibility.
Template Structure:
left, right = 0, min(len(path) for path in paths)
first_true_index = 0 # Default: length 0 always works (empty subpath)
while left <= right:
mid = (left + right) // 2
if feasible(mid):
first_true_index = mid # Record this feasible length
left = mid + 1 # Try larger lengths
else:
right = mid - 1 # Try smaller lengths
return first_true_index
Preprocessing - Hash Arrays:
Compute prefix hashes for rolling hash calculation:
for path in paths:
prefix_hash = [0] * (len(path) + 1)
for i, city in enumerate(path, 1):
prefix_hash[i] = (prefix_hash[i-1] * BASE + city) % MOD
This allows O(1) subpath hash: hash(i, j) = (h[j] - h[i-1] * base^(j-i+1)) % mod
Feasibility Check:
For length k, check if any subpath appears in all paths:
- For each path, compute hashes of all subpaths of length
k - Track unique hashes per path (avoid double-counting)
- Count occurrences across all paths
- If any hash appears in all
mpaths, return true
Time Complexity: O(S × log(min_len)) where S = total path length.
Space Complexity: O(S) for prefix hash arrays.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through with paths = [[0,1,2,3,4], [2,3,4], [4,0,1,2,3]].
Step 1: Initialize
left = 0,right = 3(min path length)first_true_index = 0
Step 2: Binary search iterations
Iteration 1: left = 0, right = 3
mid = (0 + 3) // 2 = 1- Check for common subpath of length 1:
- All paths contain cities, so any shared city works
- Hash counts: city 2 appears in all 3 paths ✓
- Update:
first_true_index = 1,left = 1 + 1 = 2
Iteration 2: left = 2, right = 3
mid = (2 + 3) // 2 = 2- Check for common subpath of length 2:
- Path 0: hashes for [0,1], [1,2], [2,3], [3,4]
- Path 1: hashes for [2,3], [3,4]
- Path 2: hashes for [4,0], [0,1], [1,2], [2,3]
- Hash for [2,3] appears in all 3 paths ✓
- Update:
first_true_index = 2,left = 2 + 1 = 3
Iteration 3: left = 3, right = 3
mid = (3 + 3) // 2 = 3- Check for common subpath of length 3:
- Path 0: [0,1,2], [1,2,3], [2,3,4]
- Path 1: [2,3,4] only
- Path 2: [4,0,1], [0,1,2], [1,2,3]
- No hash appears in all 3 paths ✗
- Update:
right = 3 - 1 = 2
Step 3: Loop terminates
left = 3 > right = 2- Return
first_true_index = 2
The longest common subpath has length 2 (subpath [2,3]).
Time and Space Complexity
Time Complexity: O(S * log(min_len)) where S is the total sum of all path lengths and min_len is the length of the shortest path.
The analysis breaks down as follows:
- Binary search runs for
O(log(min_len))iterations - For each binary search iteration, we call the
check(k)function - Inside
check(k):- We iterate through all
mpaths - For each path of length
len(path), we compute rolling hashes for all subpaths of lengthk, which takesO(len(path) - k + 1)iterations - Each hash computation is
O(1)due to the precomputed prefix hashes - Total work in
check(k)isO(sum(len(path) - k + 1))for all paths, which isO(S)
- We iterate through all
- Preprocessing step computes prefix hashes for all paths in
O(S)time - Overall:
O(S) + O(S * log(min_len)) = O(S * log(min_len))
Space Complexity: O(S + mx) where S is the total sum of all path lengths and mx is the length of the longest path.
The space breakdown:
- Array
pstores powers of base up tomx + 1:O(mx) - Array
hhstores prefix hashes for all paths:O(S)total space - Inside
check(k):cntCounter can store at mostO(S)unique hash valuesvisset for each path stores at mostO(len(path))hashes
- Overall:
O(S + mx)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Using the Wrong Binary Search Template
The Pitfall: Using while left < right with left = mid instead of the standard while left <= right template.
Solution: Use the standard template:
left, right = 0, min(len(path) for path in paths)
first_true_index = 0
while left <= right:
mid = (left + right) // 2
if feasible(mid):
first_true_index = mid
left = mid + 1
else:
right = mid - 1
return first_true_index
2. Hash Collision Issues
The Pitfall: Using a small modulo or base value leads to collisions where different subpaths produce the same hash.
Solution: Use a large prime base (e.g., 133331) and large modulo (e.g., 2^64 + 1). For extra safety, consider double hashing.
3. Duplicate Counting Within a Path
The Pitfall: Counting the same subpath multiple times within one path.
Solution: Use a seen set per path to ensure each unique subpath hash is counted only once:
seen = set()
for start in range(1, len(h) - length + 1):
subhash = compute_hash(...)
if subhash not in seen:
seen.add(subhash)
hash_count[subhash] += 1
4. Off-by-One Errors in Index Calculations
The Pitfall: Confusion between 0-based and 1-based indexing for prefix hashes.
Solution: Be consistent. Use 1-based indexing where h[i] = hash of path[0:i].
Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
Recommended Readings
https assets algo monster cover_photos Binary_Search svg Binary Search Intuition Binary search is an efficient array search algorithm It works by narrowing down the search range by half each time If you have looked up a word in a physical dictionary you've already used binary search in real life Let's
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
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask him
Want a Structured Path to Master System Design Too? Don’t Miss This!