diff --git a/_posts/2024-04-25-suffix-automaton-instead-of-suffix-array-1.md b/_posts/2024-04-25-suffix-automaton-instead-of-suffix-array-1.md index daac4345..ab31fd1c 100644 --- a/_posts/2024-04-25-suffix-automaton-instead-of-suffix-array-1.md +++ b/_posts/2024-04-25-suffix-automaton-instead-of-suffix-array-1.md @@ -10,18 +10,18 @@ tags: [string, data-structure] 이 글에서는 Suffix Automaton 자체에 대한 상세한 설명보다는 문제 풀이에 필요한 개념 위주로 간략하게 설명합니다. 아래에 Suffix Automaton에 대해 자세히 설명된 글이 있으니, 참고하시면 좋을 것 같습니다. -[https://koosaga.com/314](https://koosaga.com/314) -[https://cp-algorithms.com/string/suffix-automaton.html](https://cp-algorithms.com/string/suffix-automaton.html) +- [https://koosaga.com/314](https://koosaga.com/314) +- [https://cp-algorithms.com/string/suffix-automaton.html](https://cp-algorithms.com/string/suffix-automaton.html) -## Suffix Automaton이란? +# Suffix Automaton이란? Suffix Automaton을 간단히 설명하면 Suffix Trie의 효율적인 구현이라고 할 수 있습니다. 문자열 $S$의 모든 Suffix를 Trie에 넣는다고 생각해 봅시다. Trie가 무엇인지에 대해서는 이 글에서 다루지 않으므로, 다른 글을 참고하시기 바랍니다. -Trie를 이용하면 어떤 문자열 집합 $\{S_1, S_2, \cdots, S_n\}$에서 어떤 문자열 $P$를 Prefix로 가지는 원소가 존재하는지 $O(|P|)$에 검사할 수 있습니다. 그렇다면 Trie에 들어있는 것이 $S$의 모든 Suffix라면 어떻게 될까요? +Trie를 이용하면 어떤 문자열 집합 $\{S_1, S_2, \cdots, S_n\}$에서 어떤 문자열 $P$를 Prefix로 가지는 원소가 존재하는지 $O(\vert P\vert )$에 검사할 수 있습니다. 그렇다면 Trie에 들어있는 것이 $S$의 모든 Suffix라면 어떻게 될까요? -어떤 문자열 $S$의 모든 부분 문자열은 $S$의 Suffix의 Prefix이므로, 어떤 문자열 $P$가 $S$의 부분 문자열인지 $O(|P|)$에 검사할 수 있을 것입니다. 또한, Trie에 있는 각각의 노드는 정확히 하나의 부분 문자열에 대응될 것입니다. +어떤 문자열 $S$의 모든 부분 문자열은 $S$의 Suffix의 Prefix이므로, 어떤 문자열 $P$가 $S$의 부분 문자열인지 $O(\vert P\vert )$에 검사할 수 있을 것입니다. 또한, Trie에 있는 각각의 노드는 정확히 하나의 부분 문자열에 대응될 것입니다. $S=\rm{abcbc}$일 때 $S$의 Suffix들인 $\{\rm{abcbc}, \rm{bcbc}, \rm{cbc}, \rm{bc}, \rm{c}\}$로 Suffix Trie를 구성하면 아래와 같습니다. @@ -31,9 +31,9 @@ $S=\rm{abcbc}$일 때 $S$의 Suffix들인 $\{\rm{abcbc}, \rm{bcbc}, \rm{cbc}, \r ![](/assets/images/suffix-automaton-psb0623/trie_run.png) -결국 어떤 문자열 $S$에서 부분 문자열 $P$가 등장하는지를 $O(|P|)$ 시간에 알 수 있게 되며, 이러한 Suffix Trie를 구성할 수만 있다면 문자열 $S$의 부분 문자열을 다루는 데에 있어 강력한 도구가 될 것입니다. 그러나 구성에 걸리는 시간이 $O(|S|^2)$이고, 노드의 개수 역시 $O(|S|^2)$이기에 실제로 활용하기는 사실상 불가능합니다. 따라서, 다른 방법이 필요합니다. +결국 어떤 문자열 $S$에서 부분 문자열 $P$가 등장하는지를 $O(\vert P\vert )$ 시간에 알 수 있게 되며, 이러한 Suffix Trie를 구성할 수만 있다면 문자열 $S$의 부분 문자열을 다루는 데에 있어 강력한 도구가 될 것입니다. 그러나 구성에 걸리는 시간이 $O(\vert S\vert ^2)$이고, 노드의 개수 역시 $O(\vert S\vert ^2)$이기에 실제로 활용하기는 사실상 불가능합니다. 따라서, 다른 방법이 필요합니다. -Suffix Automaton은 **하나의 노드에 여러 개의 문자열이 대응**될 수 있게 함으로써 $O(|S|)$개의 노드만으로 Suffix Trie의 기능을 유지하는 자료구조입니다. 또한, Suffix Automaton을 구성하는 과정에서 Suffix Link 등의 유용한 추가 정보도 얻을 수 있습니다. 다음은 위와 동일하게 $S=\rm{abcbc}$로 Suffix Automaton을 만든 결과입니다. +Suffix Automaton은 **하나의 노드에 여러 개의 문자열이 대응**될 수 있게 함으로써 $O(\vert S\vert )$개의 노드만으로 Suffix Trie의 기능을 유지하는 자료구조입니다. 또한, Suffix Automaton을 구성하는 과정에서 Suffix Link 등의 유용한 추가 정보도 얻을 수 있습니다. 다음은 위와 동일하게 $S=\rm{abcbc}$로 Suffix Automaton을 만든 결과입니다. ![](/assets/images/suffix-automaton-psb0623/automaton.png) @@ -41,7 +41,7 @@ Suffix Trie와 동일하게, 루트 노드에서부터 글자를 하나씩 따 ![](/assets/images/suffix-automaton-psb0623/automaton_run.png) -### 상태 +## 상태 위에서 Suffix Automaton의 한 노드(상태, state라고도 불리움)에는 여러 개의 문자열이 대응된다고 언급했습니다. 한 노드에는 구체적으로 어떤 문자열들이 대응될까요? @@ -156,7 +156,7 @@ $P$의 왼쪽 끝점을 바깥쪽으로 점점 늘리다 보면 어느 순간 $e 이 글에 증명이 단 하나도 없는 것은 직관적인 이해를 위해 의도된 것이며, $len$과 $link$가 어떻게 엄밀하게 정의되는지는 Suffix Automaton을 설명한 다른 글들을 참고하는 것을 추천드립니다. -### DAG +## DAG 위에서 Suffix Automaton이 어떻게 상태(혹은 노드)를 정의하는지 알아보았는데요, 놀라운 점은 위처럼 상태 정의를 하더라도 노드끼리 잘만 연결해주면 문자열의 모든 부분 문자열을 찾을 수 있는 Suffix Trie의 성질을 유지할 수 있다는 것입니다. @@ -182,7 +182,7 @@ Suffix Automaton의 DAG는 Suffix Trie의 성질을 유지하고 있지만, 한 또한 이러한 연결 관계가 DAG의 성질을 가지고 있기 때문에, Suffix Automaton의 DAG 상에서 DP(Dynamic Programming)를 진행해 줄 수 있습니다. 어떤 문제에서 DP를 활용할 수 있는지는 다음 포스트에서 다루도록 하겠습니다. -### Suffix Link +## Suffix Link Suffix Automaton의 DAG도 유용한 성질이지만, Suffix Automaton을 문제풀이에 활용할 수 있는 핵심은 단연 Suffix Link라고 해도 과언이 아닙니다. @@ -201,7 +201,7 @@ Suffix Link로 트리를 만들면 어떤 문자열의 Suffix에 관한 정보 이러한 성질은 위의 그림에서도 쉽게 확인할 수 있습니다. 또한 Suffix Link가 트리 구조를 가지기 때문에 DP, Sparse Table, Lowest Common Ancestor, Heavy Light Decomposition 등의 다양한 테크닉과 같이 활용할 수 있습니다. 이를 활용해서 풀 수 있는 문제 역시 다음 포스트에서 다루도록 하겠습니다. -### 알고리즘 +## 알고리즘 Suffix Automaton을 만드는 $O(n)$ 알고리즘은 문자를 하나씩 넣으며, 새로 발생하는 문자열들에 해당하는 노드를 만들어준 후 기존의 DAG와 Suffix Link에 올바르게 연결해주는 방식으로 구현됩니다. @@ -211,7 +211,7 @@ Suffix Automaton을 만드는 $O(n)$ 알고리즘은 문자를 하나씩 넣으 -### 시간/공간 복잡도 +## 시간/공간 복잡도 Suffix Automaton을 만드는 알고리즘의 시간/공간복잡도는 기본적으로 둘 다 $O(n)$으로 보아도 무방하지만, 각 노드의 DAG 상의 다음 노드들을 저장할 때 어떤 자료구조를 사용하냐에 따라 디테일한 차이가 존재할 수 있습니다. 문자열에 존재하는 서로 다른 알파벳 종류의 개수를 $k$라고 합시다. @@ -242,7 +242,7 @@ struct Node { 대부분의 상황에서는 $k$가 상수로 주어지므로 어느 쪽이든 시간/공간 복잡도가 $O(n)$이라고 간주할 수 있습니다. 그러나 각각의 방식에 분명한 장/단점이 있으므로, 문제에 따라 적합한 자료구조를 선택하시면 되겠습니디. -또한, 위의 알고리즘대로 문자열 $S$에 대해 Suffix Automaton을 구성하면 최대 $2|S|-1$개의 노드를 가지고, DAG는 최대 $3|S|-4$개의 간선을 가짐이 증명되어 있습니다. 증명이 궁금하신 분들은 아래 링크를 참고하시기 바랍니다. +또한, 위의 알고리즘대로 문자열 $S$에 대해 Suffix Automaton을 구성하면 최대 $2\vert S\vert -1$개의 노드를 가지고, DAG는 최대 $3\vert S\vert -4$개의 간선을 가짐이 증명되어 있습니다. 증명이 궁금하신 분들은 아래 링크를 참고하시기 바랍니다. [https://cp-algorithms.com/string/suffix-automaton.html#additional-properties](https://cp-algorithms.com/string/suffix-automaton.html#additional-properties) @@ -320,19 +320,19 @@ Suffix Array의 경우 잘 알려진 구현은 $O(n \log n)$의 시간 복잡도 또한, Suffix Array와는 Suffix Automaton은 다르게 뒤에 문자를 하나씩 넣으면서 구축할 수 있기 때문에(즉, Incremental하기 때문에), 특정 쿼리 문제를 Online으로 풀 수 있다는 장점 역시 존재합니다. -## Suffix Automaton 자체만을 이용하는 테크닉 +# Suffix Automaton 자체만을 이용하는 테크닉 이 섹션에서는 다른 추가적인 알고리즘 없이 Suffix Automaton만을 활용하여 풀 수 있는 문제들을 소개합니다. -### Suffix Automaton + 모든 상태 순회 +## Suffix Automaton + 모든 상태 순회 어떤 문자열 $S$의 모든 부분 문자열에 대한 어떤 값을 구해야 할 때, 문자열 $S$에 대해 Suffix Automaton을 구축한 다음 존재하는 (루트가 아닌) 모든 노드를 순회하면서 답을 구하는 테크닉입니다. -Suffix Automaton에 존재하는 노드는 최대 $2|S|-1$개이기 때문에, 전부 순회하더라도 $O(n)$에 문제를 풀 수 있습니다. +Suffix Automaton에 존재하는 노드는 최대 $2\vert S\vert -1$개이기 때문에, 전부 순회하더라도 $O(n)$에 문제를 풀 수 있습니다. 그냥 Suffix Automaton만 이용하면 각 노드마다 알 수 있는 정보가 한정적이기 때문에, DP 등의 전처리를 진행한 이후 활용하는 경우가 대부분입니다. 이와 관련해서는 다음 포스트에서 다루도록 하겠습니다. -#### [서로 다른 부분 문자열의 개수 2 (BOJ 11479)](https://www.acmicpc.net/problem/11479) +### [[연습 문제] 서로 다른 부분 문자열의 개수 2 (BOJ 11479)](https://www.acmicpc.net/problem/11479) 문자열 $S$가 주어질 때 $S$의 서로 다른 부분 문자열이 몇개 있는지 세는 문제입니다. 이 문제는 Suffix Array와 Longest Common Prefix 배열을 이용해서 풀 수 있는 대표적인 문제입니다. @@ -342,7 +342,7 @@ Suffix Automaton에 존재하는 노드는 최대 $2|S|-1$개이기 때문에, 따라서, Suffix Automaton에 존재하는 모든 노드 $v$를 순회하며 $v.len - v.link.len$을 전부 더해주면 위 문제를 풀 수 있습니다. 시간 복잡도는 $O(n)$입니다. -### Suffix Automaton + Incremental Update +## Suffix Automaton + Incremental Update 아래 두 종류의 쿼리가 주어질 때 사용할 수 있는 테크닉입니다. @@ -351,7 +351,7 @@ Suffix Automaton에 존재하는 노드는 최대 $2|S|-1$개이기 때문에, Suffix Automaton의 구축 과정에서 문자 하나를 넣을 때 마다, 구해야 하는 특정한 값을 $O(1)$ 내지는 $O(\log n)$으로 올바르게 업데이트해줄 수 있는 경우에 매우 유용합니다. Suffix Automaton에 문자 하나를 넣을 때 마다 쉽게 업데이트를 관리해줄 수 있는 값인 경우, 특별한 알고리즘 없이도 Online으로 문제를 풀 수 있습니다. -#### [서로 다른 부분 문자열 쿼리 2 (BOJ 16907)](https://www.acmicpc.net/problem/16907) +### [[연습 문제] 서로 다른 부분 문자열 쿼리 2 (BOJ 16907)](https://www.acmicpc.net/problem/16907) [서로 다른 부분 문자열의 개수 2 (BOJ 11479)](https://www.acmicpc.net/problem/11479) 문제의 변형입니다. 뒤에 문자를 하나씩 추가하며, 쿼리가 주어질 때마다 서로 다른 문자열의 개수를 출력하는 문제입니다. @@ -368,7 +368,7 @@ Suffix Automaton 알고리즘의 특성 상, 노드 $v$의 다른 속성들과 새로 생성되는 노드의 개수와 Suffix Link의 연결이 바뀌는 곳 모두 $O(1)$이기 때문에 문자 하나를 추가할 때 $O(1)$의 작업으로 서로 다른 부분 문자열의 개수를 올바르게 관리할 수 있습니다. 따라서 문제를 Online으로 풀 수 있으며, 시간 복잡도는 $O(n)$입니다. -### Suffix Automaton + Incremental Update + Revert +## Suffix Automaton + Incremental Update + Revert 아래 세 종류의 쿼리가 주어질 때 사용할 수 있는 테크닉입니다. @@ -378,7 +378,7 @@ Suffix Automaton 알고리즘의 특성 상, 노드 $v$의 다른 속성들과 Suffix Automaton에서 문자 하나를 추가하는데 걸리는 시간이 $O(1)$이기 때문에, 문자 하나를 추가할 때 생긴 변경사항을 $O(1)$의 정보로 관리할 수 있습니다. 따라서, 문자가 추가될 때마다 변경사항을 스택에 추가하고, 제거 쿼리가 들어올 때 마다 스택의 가장 위 정보를 빼서 직전 상태로 복구해주면 공간복잡도 $O(n)$, 시간복잡도 $O(1)$에 제거 쿼리를 처리할 수 있습니다. -#### [May I Add a Letter? (BOJ 22276)](https://www.acmicpc.net/problem/22276) +### [[연습 문제] May I Add a Letter? (BOJ 22276)](https://www.acmicpc.net/problem/22276) - 문자열의 맨 뒤에 문자 추가 - 문자열의 맨 뒤에서 문자 제거 @@ -411,7 +411,7 @@ Suffix Automaton에서 문자 하나를 추가하는데 걸리는 시간이 $O(1 이 문제는 특히나 Suffix Array로 푸는 것보다 Suffix Automaton으로 푸는 것이 훨씬 쉬운 것 같습니다. -## 마치며 +# 마치며 분명 Suffix Automaton에 대해서는 최소한의 내용만 설명하고 다양한 문제 풀이 테크닉에 관한 내용을 메인으로 다루고 싶었는데, Suffix Automaton 설명이 너무 길어진 나머지 분량 조절에 실패한 것 같습니다. @@ -429,5 +429,5 @@ Suffix Automaton에서 문자 하나를 추가하는데 걸리는 시간이 $O(1 ## References -[https://koosaga.com/314](https://koosaga.com/314) -[https://cp-algorithms.com/string/suffix-automaton.html](https://cp-algorithms.com/string/suffix-automaton.html) \ No newline at end of file +- [https://koosaga.com/314](https://koosaga.com/314) +- [https://cp-algorithms.com/string/suffix-automaton.html](https://cp-algorithms.com/string/suffix-automaton.html) \ No newline at end of file