다음은 문자열이 단순 할 때 (즉, 반복되는 문자가 포함되지 않은 경우) 먼저 수행 한 작업을 표시 한 다음 전체 알고리즘으로 확장하여 Ukkonen 알고리즘을 설명하려는 시도입니다.
먼저, 몇 가지 예비 진술.
우리가 만들고있는 것은 기본적으로 검색 시도와 같습니다. 따라서 루트 노드, 그로부터 나가는 가장자리, 새로운 노드로 이어지는 가장자리, 그리고 그 밖의 다른 가장자리가 있습니다.
그러나 검색 트리와 달리 가장자리 레이블은 단일 문자가 아닙니다. 대신, 각 모서리는 정수 쌍을 사용하여 레이블이 지정됩니다
[from,to]
. 이것들은 텍스트에 대한 포인터입니다. 이러한 의미에서 각 모서리는 임의 길이의 문자열 레이블을 전달하지만 O (1) 공간 (두 개의 포인터) 만 사용합니다.
기초 원리
먼저 간단한 문자열, 반복되는 문자가없는 문자열의 접미사 트리를 만드는 방법을 먼저 보여 드리고자합니다.
abc
알고리즘 은 왼쪽에서 오른쪽으로 단계적으로 작동합니다 . 문자열의 모든 문자마다 한 단계 가 있습니다 . 각 단계에는 둘 이상의 개별 작업이 포함될 수 있지만 총 작업 수는 O (n)임을 알 수 있습니다 (마지막 최종 관찰 참조).
따라서 왼쪽 부터 시작하여 먼저 a
루트 노드 (왼쪽)에서 잎까지 [0,#]
의 가장자리를 만들고이 레이블을로 지정 하여 단일 문자 만 삽입합니다.
즉, 가장자리는 위치 0에서 시작하여 끝나는 하위 문자열을 나타냅니다. 에서 현재 끝 . 기호 #
를 사용하여 현재 끝 을 의미 하며 위치 1에 있습니다 (바로 뒤 a
).
초기 트리는 다음과 같습니다.
그리고 이것이 의미하는 바는 다음과 같습니다.
이제 위치 2로 진행합니다 (바로 후 b
). 각 단계의 목표는 모든 접미사를 현재 위치까지
삽입 하는 것 입니다. 우리는 이것을
- 기존
a
에지 확장ab
- 하나의 새로운 가장자리를 삽입
b
우리의 표현에서 이것은
그리고 그것이 의미하는 것은 :
우리는 두 가지를 관찰합니다 .
- 위한 에지 표현
ab
이다 동일한 는 초기 트리 예전 같이 [0,#]
. 현재 위치 #
를 1에서 2로 업데이트했기 때문에 그 의미가 자동으로 변경되었습니다 .
- 각 모서리는 표시되는 문자 수에 관계없이 텍스트에 두 개의 포인터로만 구성되므로 O (1) 공간을 사용합니다.
다음으로 위치를 다시 늘리고 c
기존의 모든 모서리에 a 를 추가 하고 새 접미사에 대해 새 모서리를 하나 삽입 하여 트리를 업데이트합니다 c
.
우리의 표현에서 이것은
그리고 그것이 의미하는 것은 :
우리는 관찰한다 :
- 트리는
각 단계 후 현재 위치까지 올바른 접미사 트리입니다.
- 텍스트에 문자 수만큼 많은 단계가 있습니다.
- 각 단계의 작업량은 O (1)입니다. 모든 기존 모서리는 증분
#
에 의해 자동으로 업데이트 되므로 최종 문자에 대한 새 모서리 하나를 삽입하는 데 O (1) 시간이 걸릴 수 있습니다. 따라서 길이가 n 인 문자열의 경우 O (n) 시간 만 필요합니다.
첫 번째 확장 : 간단한 반복
물론 이것은 문자열에 반복이 없기 때문에 아주 잘 작동합니다. 보다 현실적인 문자열을 살펴 보겠습니다.
abcabxabcd
abc
이전 예제에서 와 같이 시작한 다음 ab
을 반복 x
한 다음 abc
을 반복합니다 d
.
1 단계 ~ 3 단계 : 처음 3 단계 후 이전 예의 트리가 있습니다.
4 단계 :#
위치 4 로 이동 합니다. 이렇게하면 기존의 모든 가장자리가 암시 적으로 다음과 같이 업데이트됩니다.
a
루트에 현재 단계의 마지막 접미사를 삽입해야 합니다.
이 작업을 수행하기 전에 두 가지 변수 ()를 추가로
도입했습니다. #
물론 변수 는 항상 있었지만 지금까지는 사용하지 않았습니다.
- 활성 점 트리플이며
(active_node,active_edge,active_length)
- 는
remainder
, 이는 우리가 삽입 할 필요가 얼마나 많은 새로운 접미사를 나타내는 정수
이 두 가지의 정확한 의미는 곧 명확 해 지겠지만 지금은 다음과 같이 말하겠습니다.
- 간단한에서
abc
예를 들어, 활성 점은 언제나
(root,'\0x',0)
즉 active_node
, 루트 노드이었다 active_edge
널 문자로 지정 '\0x'
하고, active_length
제로였다. 그 결과 모든 단계에서 삽입 한 새로운 모서리 하나가 새로 생성 된 모서리로 루트 노드에 삽입되었습니다. 이 정보를 나타 내기 위해 왜 트리플이 필요한지 곧 알게 될 것입니다.
- 는
remainder
항상 각 단계의 시작 부분에 1로 설정했다. 이것의 의미는 각 단계의 끝에 적극적으로 삽입해야하는 접미사 수가 1 (항상 최종 문자 임)이라는 것입니다.
이제 이것은 변할 것입니다. 우리가 현재의 마지막 문자를 삽입 할 때 a
루트에, 우리는 이미 시작으로 나가는 에지가있는 것을 알 a
특히, : abca
. 이러한 경우에 우리가하는 일은 다음과 같습니다.
- 우리 는
[4,#]
루트 노드에 새로운 가장자리 를 삽입 하지 않습니다 . 대신 접미사 a
가 이미 트리에 있음을 알 수 있습니다. 그것은 더 긴 가장자리의 중간에서 끝나지 만 우리는 그것에 의해 방해받지 않습니다. 우리는 상황을 그대로 둡니다.
- 우리는 활성 점 설정 에를
(root,'a',1)
. 즉, 활성 지점이 루트 노드의 나가는 가장자리 중간에 a
, 특히 해당 가장자리의 위치 1 뒤에 시작합니다 . 가장자리는 단순히 첫 번째 문자로 지정됩니다 a
. 특정 문자로 시작하는 모서리 가 하나만 있을 수 있기 때문에 충분 합니다 (전체 설명을 읽은 후 이것이 사실인지 확인).
- 또한 증가
remainder
하므로 다음 단계 시작시 2가됩니다.
관찰 : 삽입해야 할 마지막 접미사가 이미 트리에 존재하는 것으로 밝혀 지면 트리 자체는 전혀 변경되지 않습니다 (활성 지점 만 업데이트하고 및 remainder
). 그러면 트리는 더 이상 현재 위치까지 접미사 트리를 정확하게 표현하지 않지만 모든 접미사를 포함 합니다 (최종 접미사 a
가 암시 적 으로 포함 되기 때문에 ). 따라서 변수를 수정하는 것 (모두 고정 길이이므로 O (1) 임) 외에는 이 단계에서 수행 된 작업 이
없습니다 .
5 단계 : 현재 위치 #
를 5로 업데이트합니다. 그러면 트리가 자동으로 다음과 같이 업데이트됩니다.
그리고 있기 때문에 remainder
2 , 우리는 현재 위치의 마지막 두 접미사를 삽입해야 ab
하고 b
. 기본적으로 다음과 같은 이유 때문입니다.
a
이전 단계 의 접미사가 제대로 삽입되지 않았습니다. 그래서 그것은 남아 있고, 우리가 한 걸음 진전 한 이래로 지금은에서 a
로 성장 했습니다 ab
.
- 그리고 우리는 새로운 최종 가장자리를 삽입해야합니다
b
.
실제로 이것은 활성 지점 ( a
현재 abcab
가장자리의 뒤를 가리킴)으로 이동하여 현재 최종 문자를 삽입 함을 의미합니다 b
. 그러나 : 다시 말하지만, b
이미 같은 가장자리에 존재 한다는 것이 밝혀졌습니다 .
다시, 우리는 나무를 바꾸지 않습니다. 우리는 단순히 :
- 활성 포인트를로 업데이트합니다
(root,'a',2)
(이전과 동일한 노드 및 에지이지만 이제는 뒤에 있음 b
)
remainder
이전 단계에서 최종 모서리를 올바르게 삽입하지 않았고 현재 최종 모서리도 삽입하지 않기 때문에를 3으로 늘 립니다.
명확하게하려면, 우리는 삽입했다 ab
및 b
현재 단계에 있지만, 때문에 ab
이미 발견 된, 우리는 활성 지점을 업데이트하고도 삽입하지 않았다 b
. 왜? ab
이 트리에 있으면을 포함하여 모든 접미어도 트리에
b
있어야합니다. 아마도 묵시적 일 수도 있지만, 지금까지 나무를 건축 한 방식 때문에 반드시 있어야합니다.
을 증가시켜 6 단계로 진행합니다 #
. 트리가 자동으로 다음과 같이 업데이트됩니다.
remainder
3 이므로 abx
, bx
및
을 삽입해야합니다 x
. 활성 지점은 ab
끝이 어디인지 알려주 므로 여기서 점프하고 삽입하면 x
됩니다. 실제로 x
아직 존재하지 않으므로 abcabx
가장자리를 분할하고 내부 노드를 삽입합니다.
가장자리 표현은 여전히 텍스트에 대한 포인터이므로 내부 노드를 분할하고 삽입하는 것은 O (1) 시간 안에 수행 할 수 있습니다.
우리가 처리 한 그래서 abx
및 감소 remainder
(2)에 이제 우리는 다음 나머지 접미사를 삽입해야합니다 bx
. 그러나이를 수행하기 전에 활성 지점을 업데이트해야합니다. 가장자리를 나누고 삽입 한 후의 규칙을 아래 규칙 1 이라고 하고 active_node
루트가 될 때마다 적용됩니다
(다른 경우에는 규칙 3을 더 배우게됩니다). 다음은 규칙 1입니다.
루트에서 삽입 한 후
active_node
뿌리를 유지
active_edge
삽입해야 할 새 접미사의 첫 문자로 설정됩니다. b
active_length
1 씩 감소
따라서 새로운 활성 포인트 트리플 (root,'b',1)
은 다음 인서트가 bcabx
모서리, 1 문자 뒤, 즉 뒤에서 이루어져야 함을 나타냅니다 b
. O (1) 시간에 삽입 점을 식별하고 x
이미 존재 하는지 여부를 확인할 수 있습니다. 그것이 존재한다면, 우리는 현재 단계를 끝내고 모든 것을 그대로 둡니다. 그러나 x
존재하지 않으므로 가장자리를 분할하여 삽입합니다.
다시, 이것은 O (1) 시간이 걸렸고 우리 remainder
는 1로 업데이트 하고 활성 포인트 (root,'x',0)
는 규칙 1 상태로 업데이트 합니다 .
그러나 우리가해야 할 것이 하나 더 있습니다. 이 규칙을 2로 부릅니다 .
가장자리를 분할하고 새 노드를 삽입 하고 현재 단계에서 작성된 첫 번째 노드 가 아닌 경우 , 이전에 삽입 된 노드와 새 노드를 특수 포인터 인 접미사 링크를 통해 연결 합니다. 나중에 이것이 왜 유용한 지 알게 될 것입니다. 우리가 얻는 것은 다음과 같습니다. 접미사 링크는 점선으로 표시됩니다.
현재 단계의 최종 접미사를 계속 삽입해야합니다
x
. active_length
활성 노드 의 구성 요소가 0으로 떨어지기 때문에 최종 삽입은 루트에서 직접 이루어집니다. 로 시작하는 루트 노드에는 나가는 가장자리가 없으므로 x
새로운 가장자리를 삽입합니다.
보시다시피, 현재 단계에서 나머지 모든 인서트가 만들어졌습니다.
= 7 을 설정 하여 7 단계 로 진행합니다.이 #
문자 a
는 항상 다음과 같이 모든 문자에 다음 문자를 자동으로 추가합니다
. 그런 다음 새로운 최종 문자를 활성 지점 (루트)에 삽입하고 이미있는 것을 찾습니다. 따라서 아무 것도 삽입하지 않고 현재 단계를 종료하고 활성 지점을로 업데이트합니다 (root,'a',1)
.
에서 8 단계 , #
= 8, 우리는 추가 b
하고, 이전에 볼 수 있듯이,이 유일한 수단은 우리가 활성을 가리킨 업데이트 (root,'a',2)
및 증가 remainder
하기 때문에, 다른 아무것도하지 않고이 b
이미 존재합니다. 그러나 (O (1) 시간) 활성 지점이 이제 가장자리 끝에 있음을 알 수 있습니다. 이를로 재설정하여 반영합니다
(node1,'\0x',0)
. 여기서는 가장자리가 끝나는 node1
내부 노드를 나타 ab
냅니다.
그런 다음 = 9 단계#
에서 'c'를 삽입하면 최종 트릭을 이해하는 데 도움이됩니다.
두 번째 확장 : 접미사 링크 사용
항상 그렇듯이 #
업데이트는 c
리프 가장자리 에 자동으로 추가 되고 활성 지점으로 이동하여 'c'를 삽입 할 수 있는지 확인합니다. 'c'는 이미 그 가장자리에 존재하므로 활성 점을
(node1,'c',1)
증가 remainder
시키고 다른 작업을 수행하지 않습니다.
이제 단계 #
= 10 에서 remainder
4는이므로 먼저 삽입하여
abcd
(3 단계 전에 남아 있음) 삽입해야합니다.d
활성 지점 하여 ( 해야합니다.
d
활성 지점 에 삽입하려고 하면 O (1) 시간에 에지 분할이 발생합니다.
active_node
분할이 시작된로부터는, 적색, 상기에 표시된다. 마지막 규칙은 다음과 같습니다. 규칙 3 :
active_node
루트 노드가 아닌 에지에서 에지를 분할 한 후 해당 노드에서 나오는 접미사 링크 (있는 경우) active_node
를 따라 지정된 노드로 재설정합니다 . 접미사 링크가 없으면 active_node
루트로 설정합니다 . active_edge
과active_length
변경되지 않습니다.
따라서 활성 지점은 이제 (node2,'c',1)
이며 node2
아래에 빨간색으로 표시됩니다.
삽입 abcd
이 완료되었으므로 remainder
3으로 감소 하고 현재 단계의 다음 나머지 접미사를 고려합니다
bcd
. 규칙 3은 활성 지점을 올바른 노드와 가장자리로 설정 bcd
하여 간단히 최종 문자를 삽입하여 삽입을 수행 할 수 있습니다.
d
활성 지점에 를 .
이렇게하면 또 다른 에지 분할이 발생하고 규칙 2 때문에 이전에 삽입 된 노드에서 새 노드로 접미사 링크를 작성해야합니다.
우리는 관찰 : 우리가 다음 할 수 있도록 접미사 링크는 활성 점을 다시 할 수있게 나머지 삽입 O (1)의 노력에. 위의 그래프를보고 실제로 레이블 ab
의 노드가 노드 b
의 접미사 abc
에 연결되어 있고 노드 가에 연결되어 있는지 확인하십시오.
bc
.
현재 단계가 아직 완료되지 않았습니다. remainder
이제 2이고 규칙 3을 따라 활성 포인트를 다시 설정해야합니다. 현재 active_node
(위의 빨간색) 접미사 링크가 없으므로 루트로 재설정합니다. 현재 포인트는(root,'c',1)
입니다.
따라서 다음 삽입 레이블을 시작으로 루트 노드의 나가는 가장자리에 발생합니다 c
: cabxabcd
첫 번째 문자, 즉 뒤에 뒤에 c
. 이로 인해 또 다른 분할이 발생합니다.
여기에는 새로운 내부 노드 생성이 포함되므로 규칙 2를 따르고 이전에 만든 내부 노드에서 새 접미사 링크를 설정합니다.
( 이 작은 그래프에 Graphviz Dot 을 사용하고 있습니다. 새로운 접미사 링크로 인해 점이 기존 가장자리를 다시 정렬하게되었으므로 위에 삽입 된 유일한 것이 새로운 접미사 링크인지 확인하십시오.)
이것 remainder
으로 1로 설정 될 수 있고 , active_node
루트 이기 때문에 규칙 1을 사용하여 활성 포인트를로 업데이트합니다 (root,'d',0)
. 이것은 현재 단계의 마지막 삽입이 d
루트에서 하나를 삽입하는 것을 의미합니다 .
그게 마지막 단계 였고 우리는 끝났습니다. 그러나 여러 가지 최종 관측치 가 있습니다 .
각 단계에서 우리 #
는 1 위치 앞으로 이동 합니다. O (1) 시간에 모든 리프 노드를 자동으로 업데이트합니다.
그러나 a) 이전 단계에서 남은 접미사 및 b) 현재 단계의 마지막 문자 하나를 다루지 않습니다 .
remainder
몇 개의 추가 인서트가 필요한지 알려줍니다. 이 인서트는 현재 위치에서 끝나는 문자열의 마지막 접미사에 일대일로 대응합니다 #
. 우리는 차례로 고려하고 삽입합니다. 중요 : 활성 지점은 정확히 어디로 가야하는지 알려주기 때문에 각 삽입은 O (1) 시간 안에 이루어지며 활성 지점에는 하나의 문자 만 추가하면됩니다. 왜? 다른 문자는 암시 적 으로 포함 되기 때문에
(그렇지 않으면 활성 지점이 원래 위치에 있지 않습니다).
이러한 각 삽입 후에 remainder
접미사 링크가 있으면 줄이십시오. 그렇지 않다면 우리는 루트로 간다 (규칙 3). 루트에 이미있는 경우 규칙 1을 사용하여 활성 지점을 수정합니다. 어쨌든 O (1) 시간 만 걸립니다.
이러한 삽입 중 하나에서 삽입하려는 문자가 이미 존재하는 경우, remainder
0 보다 크더라도 아무 것도하지 않고 현재 단계를 종료합니다 . 그 이유는 남아있는 인서트가 방금 만든 인서트의 접미사이기 때문입니다. 그러므로 그것들은 모두 현재 트리에 내재 되어 있습니다. 사실 remainder
> 0 만든다 확실히 우리가 나중에 나머지 접미사 처리합니다.
알고리즘의 끝에서 remainder
> 0이면 어떻게됩니까? 이것은 텍스트의 끝이 이전 어딘가에서 발생한 부분 문자열 일 때마다 해당됩니다. 이 경우 이전에 발생하지 않은 문자열 끝에 하나의 추가 문자를 추가해야합니다. 문헌에서 일반적으로 달러 기호 $
는 그 기호 로 사용됩니다. 왜 중요한가요? -> 나중에 완성 된 접미사 트리를 사용하여 접미사를 검색하는 경우 리프에서 끝나는 경우에만 일치를 수락해야합니다 . 그렇지 않으면 트리에 주 문자열의 실제 접미사가 아닌 많은 문자열이 암시 적으로 포함되어 있기 때문에 많은 가짜 일치 가 발생합니다. 강제remainder
마지막에 0이되는 것은 본질적으로 모든 접미사가 리프 노드에서 끝나는 것을 보장하는 방법입니다. 그러나 트리를 사용하여 기본 문자열의 접미사 뿐만 아니라 일반 하위 문자열 을 검색하려는 경우 아래 OP의 의견에서 제안한 것처럼이 마지막 단계는 실제로 필요하지 않습니다.
그렇다면 전체 알고리즘의 복잡성은 무엇입니까? 텍스트 길이가 n 자이면 분명히 n 단계가 있습니다 (또는 달러 기호를 추가하면 n + 1). 각 단계에서 변수를 업데이트하는 것 외에는 아무것도하지 않거나 remainder
O (1) 시간이 걸리는 삽입을합니다. 이후는 remainder
우리가 이전 단계에서 아무것도하지 횟수를 나타내며, 우리가 지금 만드는 것이 모든 삽입에 대한 감소, 우리가 뭔가를 할 시간의 총 수 (또는 n + 1) n은 정확히이다. 따라서 총 복잡도는 O (n)입니다.
그러나 내가 제대로 설명하지 않은 한 가지 작은 것이 있습니다. 접미사 링크를 따라 가고 활성 지점을 업데이트 한 다음 해당 active_length
구성 요소가 new와 제대로 작동하지 않는 것을 발견 할 수 active_node
있습니다. 예를 들어 다음과 같은 상황을 고려하십시오.
점선은 나머지 트리를 나타냅니다. 점선은 접미사 링크입니다.
이제 활성 지점을 으로하여 가장자리 (red,'d',3)
뒤의 위치를 가리 킵니다 . 이제 필요한 업데이트를하고 접미사 링크를 따라 규칙 3에 따라 활성 지점을 업데이트한다고 가정합니다. 새 활성 지점은 입니다. 그러나 녹색 노드에서 나오는 가장자리는 이므로 2 자만 있습니다. 올바른 활성 지점을 찾으려면 해당 에지를 따라 파란색 노드로 이동하여로 재설정해야 합니다.f
defg
(green,'d',3)
d
de
(blue,'f',1)
특히 나쁜 경우는 active_length
한 크게 할 수
remainder
없음 한 크게 할 수있다. 올바른 활성 지점을 찾으려면 하나의 내부 노드를 뛰어 넘을 필요가있을뿐만 아니라 최악의 경우 최대 n 개까지 점프해야 할 수도 있습니다. 이는 각 단계 에서 일반적으로 O (n)이고 접미사 링크를 따른 후 활성 노드에 대한 사후 조정도 O (n) 일 수 있기 때문에 알고리즘에 숨겨진 O (n 2 ) 복잡성 이 있음을 의미합니까 remainder
?
그 이유는 실제로 활성 포인트를 조정해야 할 경우 (예 : 위와 같이 녹색에서 파란색으로) 자체 접미사 링크가있는 새로운 노드로 연결되어 active_length
줄어들 것입니다. 접미사 링크 체인을 따라 가면서 나머지 인서트를 만들고 active_length
감소시킬 수 있으며 도중에 수행 할 수있는 활성 점 조정의 수는 active_length
주어진 시간 보다 클 수 없습니다 . 이후는
active_length
보다 클 수 없다 remainder
, 그리고 remainder
뿐만 아니라 매 단계에서 O (n)은, 그러나 이제까지 만든 단위의 총 합이 remainder
전체 프로세스의 과정은 O (N)가 너무 활성 점 조정의 수입니다 또한 O (n)에 의해 제한됩니다.