Back to all solutions

#1923 - Longest Common Subpath

Problem Description

There is a country of n cities numbered from 0 to n - 1. In this country, there is a road connecting every pair of cities.

There are m friends numbered from 0 to m - 1 who are traveling through the country. Each one of them will take a path consisting of some cities. Each path is represented by an integer array that contains the visited cities in order. The path may contain a city more than once, but the same city will not be listed consecutively.

Given an integer n and a 2D integer array paths where paths[i] is an integer array representing the path of the ith friend, return the length of the longest common subpath that is shared by every friend's path, or 0 if there is no common subpath at all.

A subpath of a path is a contiguous sequence of cities within that path.

Solution

/**
 * @param {number} n
 * @param {number[][]} paths
 * @return {number}
 */
var longestCommonSubpath = function(n, paths) {
  if (paths.length === 0) return 0;

  paths.sort((a, b) => a.length - b.length);

  let left = 0;
  let right = paths[0].length;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);

    if (checkCommonSubpathExists(paths, mid)) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }

  return right;
};

function checkCommonSubpathExists(paths, length) {
  if (length === 0) return true;

  const firstPath = paths[0];

  if (firstPath.length < length) return false;

  const prime = 1000000007;
  const base = 100003;

  let highestPower = 1;
  for (let i = 0; i < length - 1; i++) {
    highestPower = (highestPower * base) % prime;
  }

  let hashToPositions = new Map();

  let hash = 0;
  for (let i = 0; i < length; i++) {
    hash = (hash * base + firstPath[i]) % prime;
  }

  if (!hashToPositions.has(hash)) {
    hashToPositions.set(hash, []);
  }
  hashToPositions.get(hash).push(0);

  for (let i = 1; i <= firstPath.length - length; i++) {
    hash = ((hash - firstPath[i - 1] * highestPower % prime + prime)
           % prime * base + firstPath[i + length - 1]) % prime;

    if (!hashToPositions.has(hash)) {
      hashToPositions.set(hash, []);
    }
    hashToPositions.get(hash).push(i);
  }

  for (let pathIdx = 1; pathIdx < paths.length; pathIdx++) {
    const path = paths[pathIdx];

    if (path.length < length) return false;

    const newHashToPositions = new Map();
    hash = 0;
    for (let i = 0; i < length; i++) {
      hash = (hash * base + path[i]) % prime;
    }

    if (hashToPositions.has(hash)) {
      const positions = hashToPositions.get(hash);
      for (const pos of positions) {
        let isMatch = true;
        for (let j = 0; j < length; j++) {
          if (firstPath[pos + j] !== path[j]) {
            isMatch = false;
            break;
          }
        }

        if (isMatch) {
          if (!newHashToPositions.has(hash)) {
            newHashToPositions.set(hash, []);
          }
          newHashToPositions.get(hash).push(pos);
          break;
        }
      }
    }

    for (let i = 1; i <= path.length - length; i++) {
      hash = ((hash - path[i - 1] * highestPower % prime + prime)
              % prime * base + path[i + length - 1]) % prime;

      if (hashToPositions.has(hash)) {
        const positions = hashToPositions.get(hash);
        for (const pos of positions) {
          let isMatch = true;
          for (let j = 0; j < length; j++) {
            if (firstPath[pos + j] !== path[i + j]) {
              isMatch = false;
              break;
            }
          }

          if (isMatch) {
            if (!newHashToPositions.has(hash)) {
              newHashToPositions.set(hash, []);
            }
            newHashToPositions.get(hash).push(pos);
            break;
          }
        }
      }
    }

    if (newHashToPositions.size === 0) return false;

    hashToPositions = newHashToPositions;
  }

  return hashToPositions.size > 0;
}