Loading [MathJax]/jax/output/HTML-CSS/jax.js

개요

Manacher's Algorithm은 주어진 배열에서 배열의 각 entry를 중심으로 하는 가장 큰 회문(palindrome)의 크기를 구하는 알고리즘이다.

 

Brute Force Approach

이를 단순히 Brute Force로 접근해보자.

총 substring의 개수가 O(n2)이며, 각 substring이 회문인지 아닌지 판별하는데 O(n)이 걸린다.

따라서 총 시간복잡도 = O(n2)×O(n)=O(n3) 이다.

 

 

중복 제거

s=bananax가 회문인지 아닌지 판단한다고 하자.

우리는 bananax가 회문이 아니라는 걸 알기 위해 다음과 같은 과정을 거친다.

s[2]=s[4]   ->   s[1]=s[5]   ->   s[0]s[6]

 

우리가 각각의 substring을 모두 회문이 아닌지 체크만 한다면, bananax가 회문이 아니라는 정보만 이용하고, 계산과정에 있었던 s[2]=s[4],s[1]=s[5],s[0]s[6]과 같은 정보들을 버리게 되는 것이다. 따라서 이러한 정보를 이용하여 중복 계산을 줄인다면 시간복잡도의 개선을 노릴 수 있을 것이다.

 

조금 생각을 해본다면 각 entry를 중심으로 하는 가장 큰 회문의 크기를 O(n)만에 구할 수 있다는 것을 알 수 있다.

 

abanana를 예시로 들면 

 

a b a n a n a
    이 부분이 회문이기 때문에    
  이 부분이 회문인지 확인하려면 b와 n이 같은지만 보면 된다.  
또한 위쪽이 회문이 아니기 때문에, 이 부분은 절대 회문이 될수 없다.

 

따라서 각각의 중심을 기준으로 길이를 0부터 하나씩 늘려가면, O(n)만에 특정 index를 중심으로 하는 가장 큰 회문을 구할 수 있다.

 

O(n)의 index가 있고, 각 index에 대해서 가장 큰 회문을 찾는데 O(n)의 시간이 걸린다.

따라서 이 방법의 시간복잡도는 O(n)×O(n)=O(n2)이다.

 

아직도 덜 쓴 정보가 있다?

이 정도면 충분히 버려지는 정보없이 잘 만들었다고 보일 수 있다.

O(n2)이면 많은 PS 문제에서 Acceptable한 시간복잡도이기 때문에 더욱 그렇다.

그럼에도 아직도 버려지는 정보가 있다.

 

우리는 다음과 같은 의문을 가져볼 수 있다.

 

각 중심을 기준으로 하는 회문을 길이 1짜리 회문부터 찾아야하는 것인가?

 

여기까지 왔다면 위의 질문에 대한 답이 'No'라는 것을 대략 추측하셨으리라 믿는다.

 

각 index를 중심으로 하는 최대 회문의 반지름을 A라고 하고 A배열을 채운다고 해보자.

s=banana의 경우

s b a n a n a
A 0 0 1 2 1 0

위와 같은 배열이 채워지게 되는 것이다.

 

만약 s=abacababa에 대해서 A배열을 구하는 과정에서 다음과 같은 상황을 가정해보자.

s a b a c a b a b a
A 0 1 0 3 0        

 

다음의 올 문자 b는 s[4]를 중심으로 하는 길이 A[4]=3인 회문 'abacaba'에 포함되어 있다.

우리는 또한 abacaba가 회문임을 알고, s[6]s[2]s[4]를 기준으로 대칭임을 안다.

따라서 s[0:2]="aba"가 회문이라는 정보를 이용한다면, 추가적인 계산없이 s[5:7]="aba"가 회문이라는 사실을 알 수 있는 것이다.

이 사실을 이용하면, A[4]를 0부터 찾을 필요없이 1부터 찾을 수 있게 된다.

 

좀더 엄밀하게 얘기하자면 A[i]를 계산하고자 할 때, 현재까지 가장 멀리 뻗어있는 회문을 관리하는 것이다.

회문의 index인 ind=argmax(j+A[j]) 와 그 회문이 얼마나 뻗어있는지의 정보인 x=max(j+A[j])를 매번 계산한다.

 

이 회문 안에 들어가는 범위 내에서는 이미 계산한 i의 대칭점인 i=2indi의 정보를 사용하게 된다.

따라서 0부터 보는 것이 아니라 min(A[i],xi)부터 하나씩 늘려가면서 A[i]를 찾는 것이다.

 

만약 i가 범위에 들어가지 않는다면 O(n2) 알고리즘과 동일하게 0부터 계산한다.

 

 

시간복잡도는?

시간복잡도를 계산하기 위해서 A[i]의 초기값이 어디서 부터 시작하는 지를 체크해야한다.

 

case 1) 초기값이 A[i]이고, A[i]<xi일 때,

이미 A[i']를 계산할때, 더 긴 회문이 없다는 계산이 끝났기 때문에, 이 경우에는 바로 A[i]=A[i]을 하고 종료한다.

 

case 2) 초기값이 xi이거나 0일 때

이 경우에는 원래 O(n2) 알고리즘과 똑같이 하나씩 늘리면서 계산하게 된다.

이 때 x의 값이 1 증가하게 되는데, x는 초기값 0부터 감소하지 않고 1씩 증가만 해서 최종적으로 n이 된다.

따라서 case 2는 총 n번 시행되게 된다.

 

따라서 시간복잡도는 O(n)이 된다.

 

 

일반적인 회문은 어떻게 구하는가?

이 알고리즘은 홀수 길이의 회문만 구할 수 있다는 단점이 있다.

짝수 길이의 회문을 구하기 위해서는 string의 중간에 사용하지 않는 문자를 추가해주는 것이다.

예를 들어서 s=banana 라면 s=b@a@n@a@n@a로 변경한다.

이렇게 하면 @를 기준으로 하는 회문은 짝수길이의 회문을 모두 표현하게 된다.

+ Recent posts