문제풀이

팰린드롬을 효율적으로 찾는 매내처 알고리즘

pwerty 2025. 5. 22. 11:25

흔하게 보이는 유형은 아니라고 생각했는데, 의외로 백준에 제법 분량이 있다고 한다. 한번쯤 짚고 가면 좋을 것 같아서 찾고 정리하려고 한다. 알고리즘은 어하지 않아도 되는 계산을 적절한 논리를 통해 넘길 수 있다면 속도적 개선점을 찾을 수 있다. 그에 대해 써 내려가보겠다.


 

Manacher's Algorithm은 회문을 효율적으로 찾는 알고리즘이다. 우선, 회문의 정의을 아주 간단하게 짚고 가보자.

회문은 팰린드롬과 동의어이다. 앞에서 읽으나 뒤에서 읽으나 같은 문자열이다.

  • 기러기 역삼역 우영우 같은 단어들은 회문의 대표적인 예제이다.

 

핵심

어떤 전체 문자열에서 일부의 팰린드롬을 찾는데는 O(N^2)가 소요될 것이다. 앞 뒷 비교.. 하다보면 제일 빨리 떠올릴 수 있는 방법이 그 정도 시간이 걸린다. 하지만 매내처 알고리즘은 O(N)으로 빠르다. 이에 대한 개선한 원리는 이미 계산된 정보를 통해 중복 계산을 피하는 것.

 

 

작동 원리

  • 문자열 전처리
    • 짝수 길이 팰린드롬을 쉽게 찾기 위해 각 문자 사이에 Dummy를 삽입한다.
    • 이것은 실제 코드가 시작하기전 이뤄저야 하는 작업이기 때문에 전처리 작업으로써 필요하다.
for (char cc : s) // 각 문자 하나에 대해
{
  ss.push_back('#'); // Dummy 역할을 할 한 글자를 넣고
  ss.push_back(cc); // 기존에 입력받았던 s에서의 한글자 단위인 cc를 삽입 시도 하는 것이다.
}

 

  • 반지름 개념을 활용
    • 각 위치에서 가능한 팰린드롬의 최대 반지름을 계산한다.
    • 이게 기하학적 지식을 쓰는건 아니고, 중심에서 팰린드롬 끝까지의 거리를 사용하는 것이다.
    • 중복 계산을 피하기 위한 목적이다. aba 팰린드롬의 반지름은 1이다.

 

하단의 코드는 방금 전처리 해서 ss에 담긴 모든 문자열에 대해 for문을 돌면서 이루어진다. 즉, 작동 대상은 문자열 내 모든 내용이다.

for(int i = 0; i < ss.size(); i++)
{
 // start

  • 핵심 최적화 기법
    • 이전에 발견한 팰린드롬 내에 있는 문자의 경우, 그 문자를 중심으로 하는 팰린드롬은 이미 계산된 대칭 위치의 팰린드롬 정보를 활용한다.
    • 예를 들어 "abacaba"에서, c를 중심으로 하는 팰린드롬 안에 있는 두 번째 a를 탐색하기 전 첫 번째의 a의 정보를 활용한다.
if (i <= r) // i는 for문을 돌면서 지정되는 인덱스이다. 이전에 발견된 팰린드롬 내부에 있는지 부터 본다.
{
  // 해당 된다면 i <= r, 대칭 위치의 반지름 정보를 활용한다.
  mana[i] = min(r - i, mana[c + (c - i)]);
  // min을 사용해서 대칭점의 반지름이 현재 알려진 팰린드롬 경계를 넘지 않도록 보정한다.
}

 

  • 탐색 범위 확장
    • 이미 알려진 반지름부터 시작해 양쪽으로 문자를 비교하며 팰린드롬 길이를 확장한다.
    • 새로 발견한 더 큰 팰린드롬이 있으면 중심점과 반지름 정보를 갱신한다.
while (i + mana[i] + 1 < ss.size() and i - mana[i] - 1 >= 0 and ss[i + mana[i] + 1] == ss[i - mana[i] - 1])
{
  ++mana[i];
}
// i + mana[i] + 1과 i - mana[i] - 1은 다음에 비교할 문자 위치를 가리킨다.
// 문자열 범위를 벗어나지 않고 AND 양쪽 문자가 동일 할 때 반지름, mana[i]를 증가시킨다.
// 이 과정을 통해 각 위치에서 간으한 최대 팰린드롬 길이를 찾을 수 있게 되었다.

 

그리고 필요하다면 갱신해줘야한다!

if (i + mana[i] > r) // 현재 위치에서 발견한 팰린드롬이 이전에 알려진 경계보다 더 우측으로 확장 시..
{
  c = i; // 중심점 c를 현재 위치 i로 갱신한다.
  r = i + mana[i]; // 오른쪽 경계 r을 새롭게 발견된 팰린드롬의 오른쪽 끝으로 갱신한다.
} // 이 정보는 다음 위치 처리 시 최적화에 활용 할 수 있다.

} // 반복문 종료

 

 

 

https://www.acmicpc.net/problem/16163

이 문제를 해결 할 수 있게 되었다. 축가한다!

#include <iostream>
#include <string>

using namespace std;

string s;
string ss;
int mana[4000005];
int r = -1;
int c = -1;

void solve()
{
  cin >> s;
  for (char cc : s)
  {
    ss.push_back('#');
    ss.push_back(cc);
  }
  ss.push_back('#');

  for (int i = 0; i < ss.size(); i++)
  {
    if (i <= r)
    {
      mana[i] = min(r - i, mana[c + (c - i)]);
    }

    while (i + mana[i] + 1 < ss.size() and i - mana[i] - 1 >= 0 and ss[i + mana[i] + 1] == ss[i - mana[i] - 1])
    {
      ++mana[i];
    }

    if (i + mana[i] > r)
    {
      c = i;
      r = i + mana[i];
    }
  }
}


int main() {
    long long result = 0;
    solve();
    
    for(int i = 0; i < 4000005; i++)
    {
        result += (mana[i] + 1) / 2;
    }
    cout << result << '\n';

    return 0;
}

 

내용 출처 : https://nicotina04.tistory.com/250