# Manacher's algorithm

회문 구하는 알고리즘, 회문이란 좌우가 대칭인 문자열로 짝수, 홀수에 따라 구하는 방식을 다르게 구현하지만 manacher's algorithm을 이용하면 빠른 시간에 회문의 길이를 측정 할 수 있다.

일반적인 회문을 검색하는 방법은 특정 문자를 중심으로 양쪽으로 같은 문자가 있는지 찾는 방식을 취할 수 있다. 해당방식으로 회문을 찾을 경우에는 회문이 짝수 길이를 갖는지, 홀수 길이를 갖는지에 따라 방식을 다르게 구현 할 수 있다.

이러한 방식을 따라 manacher's algorithm을 생각 해 볼 수 있다.

이 알고리즘은 기존에 검색된 회문 정보를 다시 사용하여 검색에 사용합니다. 해당 방식을 간단하게 살펴보려면 일반적인 회문 검색 방식을 살펴볼 필요가 있습니다.

# 회문 검색 방식

  1. 중심점을 이동하며 검색을 시작한다. ( 0 <= i < string.length)
  2. 중심점 양옆 문자가 같은지 확인한다. (string[i+j] === string[i-j])
  3. 양 옆 문자가 같다면 계속해서 진행한다. (j++)
  4. 같지 않다면 중심점을 이동한다.
  5. 문자열 끝까지 중심점을 이동하며 진행한다.
  6. 짝수길이의 회문에 같은 방식으로 검색한다. (j=i 홀수길이 회문 검색 시작점 , j = i+1 짝수길이 회문 검색 시작점)

문자열 전체를 구성하는 회문을 찾는 방식은 다음과 같다.

function solution(s) {
  let start = 0,
    length = 0;
  for (let i = 0; i < s.length; i++) {
    // 전체 문자열 검사
    let left = i,
      right = i; // 홀수 길이 회문 조사
    while (left >= 0 && right < s.length && s[left] === s[right]) {
      if (length < right - left + 1) {
        start = left;
        length = right - left + 1;
      }
      left--;
      right++;
    }
    left = i;
    right = i + 1; // 짝수 길이 회문 조사
    while (left >= 0 && right < s.length && s[left] === s[right]) {
      if (length < right - left + 1) {
        start = left;
        length = right - left + 1;
      }
      left--;
      right++;
    }
  }
  return s.slice(start, start + length);
}

위와 같은 방식의 회문조사는 각 중심 위치에서 양쪽을 두번에 걸처셔 조사하기 때문에, 최대 시간 복잡도를 O(n^2)을 갖게 된다.

시간 복잡도를 많이 낮출수 있는 manacher's algorithm은 위와 같은 방식에서 기존에 검색된 회문의 정보를 이용하는 방법을 추가하게 된다.

# manacher's algorithm

위의 검색 방식에서 확인한 회문의 정보를 이용하는 방식은 다음과 같은 절차를 따른다.

  1. 조사한 회문의 길이가 없다면 기존 회문 조사와 같은 방식을 따른다.
  2. 해당 중심점에서 회문의 반지름을 저장한다. (c: 중심, r: 반지름)
  3. 중심점 + 반지름을 통해 현재 조사한 위치를 저장한다. ( c+r: 현재 조사된 인덱스)
  4. 현재 조사중인 새로운 중심(i)이 가장 최근에 조사된 회문 정보 (c, r)과 어떤 관계인지 확인한다.
    • 4-1 현재 중심(i)과 최근 회문 중심(c)의 거리를 구한다. (i-c)
    • 4-2 계산된 거리만큼 회문 중심을 기준으로 대칭 이동한다. (j = c - (i-c) = 2*c - i )
    • 4-3 대칭점의 회문 길이를 확인한다. (j점의 회문 길이 확인)
    • 4-5 j중심의 회문 길이가 r-i보다 큰가 => i점의 회문 길이는 r-i 이다.
    • 4-6 j중심의 회문 길이가 r-i보다 작은가 => i점의 회문 길이는 j점의 회문 길이와 같다
    • 4-7 j중심의 회문 길이가 r-i와 같은가 => 1번으로 돌아가 다시 시행한다.
  5. 관계에 따라 현재 중심(i)에서의 회문을 저장한다.
  6. 위와 같은 방법을 문자열 전체를 검사한다.

4-5~7이 되는 이유는 가장 최근에 조사된 회문의 정보를 이용하면 알 수 있다.

모든 정보는 c점을 중심으로 한 회문의 일부라는 점에서 확인할 수 있다.

c점을 중심으로한 회문에 속하는 j,i점은 c점을 중심으로 r만큼 떨어진 거리만큼은 완벽하게 일치한다. 그렇지 않으면 c를 중심으로한 회문의 반지름의 길이가 r이 나타날 수 없기 때문이다.

  • 4-5: j점을 중심으로 하는 회문의 길이가 r-i보다 큰 경우

j를 중심으로 한 회문의 일부가,c를 중심으로한 회문의 반지름인 r을 벗어나는 경우를 생각 해볼 수 있다. 이 경우 i점은 c를 중심으로 r을 벗어나는 경우의 문자가 j점을 중심으로 r을 벗어나는 경우의 문자와 달라졌기 때문에, c를 중심으로 r만큼의 반지름 회문이 발생하게 된다고 볼 수 있다.

이러한 이유로 i를 중심으로 한 회문의 길이는 r-i만큼의 길이를 갖게 된다.

  • 4-6: j점을 중심으로 하는 회문의 길이가 r-i보다 작은 경우

4-5와는 반대로 r-i보다 작은경우는 완벽히 c를 중심으로한 회문에 j를 중심으로한 회문이 속하는 경우로 i를 중심으로한 회문의 길이와 완벽히 같아야 한다.

  • 4-7: j점을 중심으로 하는 회문의 길이가 r-i와 같은 경우

두 개모두 해당하지 않는 경우로 j의 회문의 길이가 정확히 r-i와 같은 경우에는 i의 회문의 길이가 반드시 r-i라는 보장이 없다. c를 중심으로한 c-r-i번째 문자와 c+r+1의 문자가 다른 이유가 i를 중심으로 회문의 x만 큼 떨어진 곳의 문자가 다르다는 뜻은 아니기 때문이다.

이로 인해 i를 기준으로 회문 조사를 다시 진행해야 한다.

이와 같은 방법으로 조사를 진행하면 짝수길이의 회문을 조사할 수 없다. 해당 짝수 길이의 회문을 조사하기 위해서는 문자열 사이사이에 조사에 관여하지 않는 특수 문자 (@, #,등...)을 추가하여 조사를 진행해야 한다.

앞뒤에도 추가하지 않으면 마지막에 오는 반복 짝수문자 검출에 실패하기 때문에 앞뒤에도 해당 특수문자를 추가해야 한다.

var longestPalindrome = function (s) {
  if (s.length === 1) return s;
  let str = ["@", ...s.split("").join("@").split(""), "@"].join("");
  let r = -1,
    p = -1;
  let result = {
    index: 0,
    radius: 0,
  };
  let res = [];

  for (let i = 0; i < str.length; i++) {
    if (r < i) {
      p = r = i;
      while (r < str.length && r <= p * 2 && str[r] === str[2 * p - r]) r++;
      r--;
      res[i] = r - p;
      if (result.radius < r - p) {
        result.index = i;
        result.radius = r - p;
      }
    } else {
      let j = 2 * p - i;
      if (res[j] < r - i) {
        res[i] = res[j];
      } else if (res[j] > r - i) {
        res[i] = r - i;
      } else {
        p = i;
        while (r < str.length && r <= p * 2 && str[r] === str[2 * p - r]) r++;
        r--;
        res[i] = r - p;
        if (result.radius < r - p) {
          result.index = i;
          result.radius = r - p;
        }
      }
    }
  }
  let { index, radius } = result;
  return str.slice(index - radius, index + radius + 1).replaceAll("@", "");
};

두 가지 방식은 공간 복잡도에서 유불리를 다르게 가지고 있으며, 그와 반대인 시간 복잡도 우위를 갖게 된다.