Algorithm/내용 정리

Manacher 알고리즘

gilbert9172 2024. 12. 15. 22:11

 

❐ Description


[5. Longest Palindromic Substring] 문제를 풀면서 알게 된 Manacher 알고리즘 정리. 

 

 

 

 

 

 

❐ Manacher 알고리즘 이해하기


Manacher 알고리즘은 문자열에서 가장 긴 팰린드롬 부분 문자열을 효율적으로 찾는 알고리즘이다.

일반적인 방법으로 풀 경우 O(n²)의 시간 복잡도가 걸리지만, Manacher 알고리즘은 이를 O(n)으로 줄여준다.

이 알고리즘은 팰린드롬의 대칭성을 이용하여 중복 계산을 줄이는 방식(동적 프로그래밍)으로 동작한다.

 

 

코드로 이해해 보았다. 

다른 블로그들은 변수명이 엉망 그 자체다...
변수명 의미
radiuses 반지름 배열
currentCenterIdx 탐색 과정에서 수시로 변경되는 중심 인덱스
centerIdxOfMaxPalindrome 현재까지 찾은 가장 긴 팰린드롬의 중심 인덱스
rightBoundary 현재까지 찾은 가장 긴 팰린드롬의 오른쪽 가장 끝 인덱스
mirrorIdx currentCenterIdx를 기준으로 현재 탐색하고 있는 요소(i)의 대칭되는 요소의 인덱스

 

더보기
전체 코드
public String longestPalindrome(String s) {
    // 1. 문자열 변환
    StringBuilder builder = new StringBuilder();
    builder.append("#");
    for (char c : s.toCharArray()) {
        builder.append(c).append("#");
    }
    String transformedInput = builder.toString();

    // 2. 변수 초기화
    int inputLen = transformedInput.length();
    int[] radiuses = new int[inputLen]; // 팰린드롬 반지름 배열
    int currentCenterIdx = 0, rightBoundary = 0; // 중심과 오른쪽 경계
    int maxLen = 0, centerIdxOfMaxPalindrome = 0;

    // 3. radiuses 배열 계산
    for (int i = 0; i < inputLen; i++) {
        int mirrorIdx = 2 * currentCenterIdx - i; // i의 거울 위치
        if (i < rightBoundary) {
            radiuses[i] = Math.min(rightBoundary - i, radiuses[mirrorIdx]);
        }

        // 4. 팰린드롬 확장
        while (i + radiuses[i] + 1 < inputLen && i - (radiuses[i] + 1) >= 0 &&
                transformedInput.charAt(i + radiuses[i] + 1) == transformedInput.charAt(i - (radiuses[i] + 1))) {
            radiuses[i]++;
        }

        // 5. 중심과 오른쪽 경계 업데이트
        if (i + radiuses[i] > rightBoundary) {
            currentCenterIdx = i;
            rightBoundary = i + radiuses[i];
        }

        // 6. 최대 길이 갱신
        if (radiuses[i] > maxLen) {
            maxLen = radiuses[i];
            centerIdxOfMaxPalindrome = i;
        }
    }

    // 7. 결과 추출
    int start = (centerIdxOfMaxPalindrome - maxLen) / 2; // 원본 문자열에서 시작 위치
    return s.substring(start, start + maxLen);
}

 

1. 홀수 길이와 짝수 길이를 동일하게 다룰 수 있도록 하기 위해서 임의의 문자열(#) 추가

StringBuilder builder = new StringBuilder();
builder.append("#");
for (char c : s.toCharArray()) {
    builder.append(c).append("#");
}
String transformedInput = builder.toString();

 

 

2. 변수 초기화

int inputLen = transformedInput.length();
int[] radiuses = new int[inputLen];
int currentCenterIdx = 0, rightBoundary = 0;
int maxLen = 0, centerIdxOfMaxPalindrome = 0;

 

 

3. radiuses 배열 계산

3번 과정에서 많은 subtask들이 수행된다.

 

3-1. 현재 탐색 중인 요소(i)의 대칭 인덱스 찾기

int mirrorIdx = 2 * currentCenterIdx - i;

 

 

3-2. 현재 탐색 중인 요소(i)가 rightBoundary 내에 있다면 대칭성을 이용하기

if (i < rightBoundary) {
    radiuses[i] = Math.min(rightBoundary - i, radiuses[mirrorIdx]);
}
  • `rightBoundary - 1` : 현재까지 찾은 가장 긴 palindrome의 오른쪽 끝까지의 거리

 

3-3. Palindrome 확장

// i + radiuses[i] + 1 : 현재 중심 기준 왼쪽 문자 idx
// i - (radiuses[i] + 1) : 현재 중심 기준 왼쪽 문자 idx
while (i + radiuses[i] + 1 < inputLen && i - radiuses[i] - 1 >= 0 &&
        transformedInput.charAt(i + radiuses[i] + 1) == transformedInput.charAt(i - radiuses[i] - 1)) {
    radiuses[i]++;
}

 

 

3-4. 현재 중심과 rightBoundary(오른쪽 경계) 갱신

if (i + radiuses[i] > rightBoundary) {
    currentCenterIdx = i;
    rightBoundary = i + radiuses[i];
}

 

 

3-5. 최대 길이의 Palindrome 길이 갱신

if (radiuses[i] > maxLen) {
    maxLen = radiuses[i];
    centerIdxOfMaxPalindrome = i;
}

 

 

3-6. 결과 추출

int start = (centerIdxOfMaxPalindrome - maxLen) / 2;
return s.substring(start, start + maxLen);
  • `(centerIdxOfMaxPalindrome - maxLen) / 2` : 원본 문자열에서 시작 위치를 구하기 위함.