평범한 영어로 된 Ukkonen의 접미사 트리 알고리즘


1101

나는이 시점에서 조금 두껍게 느낀다. 나는 접미사 트리 구조를 머릿속으로 감싸기 위해 며칠을 보냈지 만, 수학적 배경이 없기 때문에 많은 수의 설명이 수학 기호를 과도하게 사용하기 시작하면서 나를 피하게합니다. 내가 찾은 가장 좋은 설명에 가장 가까운 것은 접미사 트리를 사용한 빠른 문자열 검색 이지만 다양한 지점에 대해 글로시하고 알고리즘의 일부 측면이 불분명합니다.

스택 오버플로 에서이 알고리즘에 대한 단계별 설명은 저 외에 다른 많은 사람들에게 귀중한 것이 될 것입니다.

참고로 알고리즘에 대한 Ukkonen의 논문은 다음과 같습니다. http://www.cs.helsinki.fi/u/ukkonen/SuffixT1withFigs.pdf

지금까지 나의 기본 이해 :

  • 주어진 문자열 T의 각 접두사 P를 반복해야합니다.
  • 접두사 P의 각 접미사 S를 반복하고 트리에 추가해야합니다.
  • 접미사 S를 트리에 추가하려면 S의 각 문자를 반복해야합니다. 반복은 S에서 동일한 문자 C로 시작하는 기존 분기를 아래로 걷고 내가있을 때 가장자리를 하위 노드로 분할 할 수 있습니다 접미어에서 다른 문자에 도달하거나 걸을 가장자리가없는 경우. C에 대해 일치하는 모서리가 없으면 C에 대해 새 리프 모서리가 작성됩니다.

기본 알고리즘 은 대부분의 설명에서 지적했듯이 모든 접두사를 단계별로 수행해야하므로 각 접두사에 대한 각 접미사를 단계별로 수행해야 하므로 O (n 2 ) 인 것처럼 보입니다 . Ukkonen의 알고리즘은 그가 사용하는 접미사 포인터 기술 때문에 분명히 독특합니다.하지만 그것이 이해하는 데 어려움이 있다고 생각 합니다 .

또한 이해하는 데 어려움이 있습니다.

  • "액티브 포인트"가 언제 어떻게 할당되고 사용 및 변경되는지
  • 알고리즘의 표준화 측면에서 무슨 일이 일어나고 있습니까?
  • 내가 본 구현이 사용중인 경계 변수를 "수정"해야하는 이유

완성 된 C # 소스 코드 는 다음과 같습니다 . 올바르게 작동 할뿐만 아니라 자동 정식화를 지원하고 출력의 더 멋진 텍스트 그래프를 렌더링합니다. 소스 코드 및 샘플 출력은 다음과 같습니다.

https://gist.github.com/2373868


2017-11-04 업데이트

몇 년 후 접미사 트리에 대한 새로운 용도를 발견하고 JavaScript로 알고리즘을 구현했습니다 . 요점은 아래와 같습니다. 버그가 없어야합니다. npm install chalk같은 위치에서 js 파일로 덤프 한 다음 node.js로 실행하여 다채로운 출력을 확인하십시오. 디버깅 코드없이 동일한 Gist에 제거 된 버전이 있습니다.

https://gist.github.com/axefrog/c347bf0f5e0723cbd09b1aaed6ec6fc6


2
Dan Gusfield의 책에 주어진 설명을 보았습니까 ? 도움이된다는 것을 알았습니다.
jogojapan

4
요점은 라이센스를 지정하지 않습니다-코드를 변경하고 MIT (분명히 귀속)에 따라 다시 게시 할 수 있습니까?
Yurik

2
그렇습니다, 당신의 인생을 위해 가십시오. 그것을 공개 도메인으로 생각하십시오. 이 페이지의 다른 답변에서 언급했듯이 어쨌든 수정 해야하는 버그가 있습니다.
Nathan Ridley

1
아마도이 구현은 다른 사람들에게 도움이 될 것입니다. code.google.com/p/text-indexing
cos

2
"퍼블릭 도메인 고려"는 놀랍게도 매우 도움이되지 않는 대답 일 것입니다. 그 이유는 작업을 공개 도메인에 배치하는 것이 실제로 불가능하기 때문입니다. 따라서 귀하의 "그것을 고려하십시오"라는 의견은 라이센스가 명확하지 않다는 사실을 강조하고 독자에게 작품의 상태가 실제로 명확하다는 것을 의심 할만한 이유를 제공 합니다 . 사람들이 코드를 사용할 수있게하려면 라이센스를 지정하고 원하는 라이센스를 선택하십시오 (그러나 변호사가 아닌 경우 기존 라이센스를 선택하십시오!)
James Youngman

답변:


2377

다음은 문자열이 단순 할 때 (즉, 반복되는 문자가 포함되지 않은 경우) 먼저 수행 한 작업을 표시 한 다음 전체 알고리즘으로 확장하여 Ukkonen 알고리즘을 설명하려는 시도입니다.

먼저, 몇 가지 예비 진술.

  1. 우리가 만들고있는 것은 기본적으로 검색 시도와 같습니다. 따라서 루트 노드, 그로부터 나가는 가장자리, 새로운 노드로 이어지는 가장자리, 그리고 그 밖의 다른 가장자리가 있습니다.

  2. 그러나 검색 트리와 달리 가장자리 레이블은 단일 문자가 아닙니다. 대신, 각 모서리는 정수 쌍을 사용하여 레이블이 지정됩니다 [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로 업데이트합니다. 그러면 트리가 자동으로 다음과 같이 업데이트됩니다.

그리고 있기 때문에 remainder2 , 우리는 현재 위치의 마지막 두 접미사를 삽입해야 ab하고 b. 기본적으로 다음과 같은 이유 때문입니다.

  • a이전 단계 의 접미사가 제대로 삽입되지 않았습니다. 그래서 그것은 남아 있고, 우리가 한 걸음 진전 한 이래로 지금은에서 a로 성장 했습니다 ab.
  • 그리고 우리는 새로운 최종 가장자리를 삽입해야합니다 b.

실제로 이것은 활성 지점 ( a현재 abcab가장자리의 뒤를 가리킴)으로 이동하여 현재 최종 문자를 삽입 함을 의미합니다 b. 그러나 : 다시 말하지만, b이미 같은 가장자리에 존재 한다는 것이 밝혀졌습니다 .

다시, 우리는 나무를 바꾸지 않습니다. 우리는 단순히 :

  • 활성 포인트를로 업데이트합니다 (root,'a',2)(이전과 동일한 노드 및 에지이지만 이제는 뒤에 있음 b)
  • remainder이전 단계에서 최종 모서리를 올바르게 삽입하지 않았고 현재 최종 모서리도 삽입하지 않기 때문에를 3으로 늘 립니다.

명확하게하려면, 우리는 삽입했다 abb현재 단계에 있지만, 때문에 ab이미 발견 된, 우리는 활성 지점을 업데이트하고도 삽입하지 않았다 b. 왜? ab이 트리에 있으면을 포함하여 모든 접미어도 트리에 b있어야합니다. 아마도 묵시적 일 수도 있지만, 지금까지 나무를 건축 한 방식 때문에 반드시 있어야합니다.

을 증가시켜 6 단계로 진행합니다 #. 트리가 자동으로 다음과 같이 업데이트됩니다.

remainder3 이므로 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 에서 remainder4는이므로 먼저 삽입하여 abcd(3 단계 전에 남아 있음) 삽입해야합니다.d 활성 지점 하여 ( 해야합니다.

d활성 지점 에 삽입하려고 하면 O (1) 시간에 에지 분할이 발생합니다.

active_node분할이 시작된로부터는, 적색, 상기에 표시된다. 마지막 규칙은 다음과 같습니다. 규칙 3 :

active_node루트 노드가 아닌 에지에서 에지를 분할 한 후 해당 노드에서 나오는 접미사 링크 (있는 경우) active_node를 따라 지정된 노드로 재설정합니다 . 접미사 링크가 없으면 active_node루트로 설정합니다 . active_edgeactive_length 변경되지 않습니다.

따라서 활성 지점은 이제 (node2,'c',1)이며 node2아래에 빨간색으로 표시됩니다.

삽입 abcd이 완료되었으므로 remainder3으로 감소 하고 현재 단계의 다음 나머지 접미사를 고려합니다 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) 시간 만 걸립니다.

  • 이러한 삽입 중 하나에서 삽입하려는 문자가 이미 존재하는 경우, remainder0 보다 크더라도 아무 것도하지 않고 현재 단계를 종료합니다 . 그 이유는 남아있는 인서트가 방금 만든 인서트의 접미사이기 때문입니다. 그러므로 그것들은 모두 현재 트리에 내재 되어 있습니다. 사실 remainder> 0 만든다 확실히 우리가 나중에 나머지 접미사 처리합니다.

  • 알고리즘의 끝에서 remainder> 0이면 어떻게됩니까? 이것은 텍스트의 끝이 이전 어딘가에서 발생한 부분 문자열 일 때마다 해당됩니다. 이 경우 이전에 발생하지 않은 문자열 끝에 하나의 추가 문자를 추가해야합니다. 문헌에서 일반적으로 달러 기호 $는 그 기호 로 사용됩니다. 왜 중요한가요? -> 나중에 완성 된 접미사 트리를 사용하여 접미사를 검색하는 경우 리프에서 끝나는 경우에만 일치를 수락해야합니다 . 그렇지 않으면 트리에 주 문자열의 실제 접미사가 아닌 많은 문자열이 암시 적으로 포함되어 있기 때문에 많은 가짜 일치 가 발생합니다. 강제remainder마지막에 0이되는 것은 본질적으로 모든 접미사가 리프 노드에서 끝나는 것을 보장하는 방법입니다. 그러나 트리를 사용하여 기본 문자열의 접미사 뿐만 아니라 일반 하위 문자열 을 검색하려는 경우 아래 OP의 의견에서 제안한 것처럼이 마지막 단계는 실제로 필요하지 않습니다.

  • 그렇다면 전체 알고리즘의 복잡성은 무엇입니까? 텍스트 길이가 n 자이면 분명히 n 단계가 있습니다 (또는 달러 기호를 추가하면 n + 1). 각 단계에서 변수를 업데이트하는 것 외에는 아무것도하지 않거나 remainderO (1) 시간이 걸리는 삽입을합니다. 이후는 remainder우리가 이전 단계에서 아무것도하지 횟수를 나타내며, 우리가 지금 만드는 것이 모든 삽입에 대한 감소, 우리가 뭔가를 할 시간의 총 수 (또는 n + 1) n은 정확히이다. 따라서 총 복잡도는 O (n)입니다.

  • 그러나 내가 제대로 설명하지 않은 한 가지 작은 것이 있습니다. 접미사 링크를 따라 가고 활성 지점을 업데이트 한 다음 해당 active_length구성 요소가 new와 제대로 작동하지 않는 것을 발견 할 수 active_node있습니다. 예를 들어 다음과 같은 상황을 고려하십시오.

점선은 나머지 트리를 나타냅니다. 점선은 접미사 링크입니다.

이제 활성 지점을 으로하여 가장자리 (red,'d',3)뒤의 위치를 ​​가리 킵니다 . 이제 필요한 업데이트를하고 접미사 링크를 따라 규칙 3에 따라 활성 지점을 업데이트한다고 가정합니다. 새 활성 지점은 입니다. 그러나 녹색 노드에서 나오는 가장자리는 이므로 2 자만 있습니다. 올바른 활성 지점을 찾으려면 해당 에지를 따라 파란색 노드로 이동하여로 재설정해야 합니다.fdefg(green,'d',3)dde(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)에 의해 제한됩니다.


74
미안 이것은 내가 바라는 것보다 조금 길어졌습니다. 그리고 우리가 아는 많은 사소한 것들을 설명하지만 어려운 부분은 아직 완전히 명확하지 않을 수도 있습니다. 모양을 함께 편집합시다.
jogojapan

68
그리고 이것은 Dan Gusfield의 책에있는 설명에 근거한 것이 아니라고 덧붙여 야합니다. 반복이없는 문자열을 먼저 고려한 다음 반복 처리 방법을 논의하여 알고리즘을 설명하려는 새로운 시도입니다. 나는 그것이 더 직관적이기를 바랐다.
jogojapan

8
@ jogojapan에게 감사드립니다. 귀하의 설명 덕분에 완전한 예제를 작성할 수있었습니다. 나는 소스를 공개 했으므로 다른 누군가가 그것을 사용할 수 있기를 바란다. gist.github.com/2373868
Nathan Ridley

4
@NathanRidley 예 (그런데 마지막 비트는 Ukkonen이 canonicize라고 부르는 것입니다). 이를 트리거하는 한 가지 방법은 세 번 나타나고 다른 컨텍스트에서 한 번 더 나타나는 문자열로 끝나는 하위 문자열이 있는지 확인하는 것입니다. 예 abcdefabxybcdmnabcdex. 초기의 부분 abcd이 반복된다 abxy(이 후 내부 노드를 생성 ab)하고 다시 abcdex, 그리고 만료 bcd에 단지 표시하는 bcdex맥락에서뿐만 아니라, bcdmn콘텍스트. abcdex삽입 한 후 접미사 링크를 따라 삽입 bcdex하면
canonicize

6
내 코드가 완전히 다시 작성되어 이제 자동 정식화를 포함한 모든 경우에 올바르게 작동하고 텍스트 그래프 출력이 훨씬 좋습니다. gist.github.com/2373868
Nathan Ridley

132

jogojapan의 답변에 주어진 접근 방식으로 접미사 트리를 구현하려고 시도했지만 규칙에 사용되는 문구 때문에 일부 경우에는 작동하지 않았습니다. 또한 아무도이 접근법을 사용하여 절대적으로 올바른 접미사 트리를 구현할 수 없다고 언급했습니다. 아래에서는 규칙을 약간 수정하여 jogojapan의 답변에 대한 "개요"를 작성합니다. 또한 중요한 접미사 링크 를 만드는 것을 잊었을 때도 설명하겠습니다 .

사용 된 추가 변수

  1. active point- 새로운 접미사 삽입을 시작 해야하는 위치를 나타내는 트리플 (active_node; active_edge; active_length).
  2. 나머지 - 명시 적으로 추가해야하는 접미사 수를 나타냅니다 . 예를 들어, 단어가 'abcaabca'이고 나머지 = 3이면 마지막 접미사 3 개 ( bca , caa)를 처리해야합니다 .

내부 노드 의 개념을 사용합시다 . 루트리프를 제외한 모든 노드 는 내부 노드 입니다.

관찰 1

삽입해야 할 마지막 접미사가 이미 트리에 존재하는 것으로 밝혀지면 트리 자체는 전혀 변경되지 않습니다 ( active point및 업데이트 만 remainder).

관찰 2

어떤 점에서이 경우 active_length큰 전류 에지 (의 길이와 동일 edge_length), 우리는 우리의 이동 active point때까지를 edge_length보다 확실히 크다 active_length.

이제 규칙을 재정의 해 봅시다 :

규칙 1

로부터 삽입 후의 경우 활성 노드 = 루트활성 길이는 그 후, 0보다 큰 :

  1. 활성 노드 는 변경되지 않습니다
  2. 활성 길이 가 감소합니다
  3. 활성 모서리 가 오른쪽으로 이동합니다 (삽입해야하는 다음 접미사의 첫 문자로)

규칙 2

우리는 새로운 만들 경우 내부 노드 또는 에서 삽입 할 내부 노드를 ,이 첫 번째 아니다 그러한 내부 노드 현재 단계에서, 우리는 이전의 링크 와 같은 과 노드를 스루 하나의 접미사 링크 .

이 정의는 Rule 2jogojapan과 다릅니다. 여기서 새로 작성된 내부 노드뿐만 아니라 삽입하는 내부 노드도 고려합니다.

규칙 3

루트 노드 가 아닌 활성 노드 에서 삽입 한 후에는 접미사 링크를 따라 활성 노드 를 가리키는 노드 로 설정해야합니다 . 접미사 링크가 없으면 활성 노드루트 노드 로 설정하십시오 . 어느 쪽이든, 활성 모서리활성 길이 는 변경되지 않습니다.

이 정의에서 Rule 3우리는 또한 잎 노드 (분할 노드뿐만 아니라)의 삽입을 고려합니다.

그리고 마지막으로, 관찰 3 :

우리는 트리에 추가 할 기호가 가장자리에 이미있는 경우, 우리는 따라하기 Observation 1만 업데이트 active pointremainder변경 트리를 떠나. 그러나 접미사 링크가 필요한 것으로 표시된 내부 노드 가있는 경우 접미사 링크active node 를 통해 해당 노드를 현재 노드에 연결해야합니다 .

이 경우 접미사 링크를 추가하고 그렇지 않은 경우 cdddcdc 에 대한 접미사 트리의 예를 살펴 보겠습니다 .

  1. 접미사 링크를 통해 노드를 연결 하지 않으면 :

    • 마지막 문자 c 를 추가하기 전에 :

    • 마지막 문자 c를 추가 한 후 :

  2. 우리는 경우 DO 접미사 링크를 통해 노드를 연결 :

    • 마지막 문자 c 를 추가하기 전에 :

    • 마지막 문자 c를 추가 한 후 :

큰 차이는없는 것 같습니다. 두 번째 경우에는 두 개의 접미사 링크가 더 있습니다. 그러나 이러한 접미사 링크는 정확 하며 파란색 노드에서 빨간색 링크까지 그 중 하나는 활성 지점을 사용하는 접근 방식에 매우 중요 합니다 . 문제는 여기에 접미사 링크를 넣지 않으면 나중에 새 문자를 트리에 추가 할 때으로 인해 트리에 노드를 추가하는 것을 생략 할 수 Rule 3있다는 것입니다. 접미사 링크, 우리는 active_node루트에 넣어야합니다 .

마지막 글자를 나무에 추가 할 때 파란색 노드에서 삽입하기 전에 빨간색 노드가 이미 존재했습니다 (가장자리가 'c' ). 파란색 노드에서 삽입이 있었으므로 접미사 링크가 필요하다고 표시했습니다 . 그런 다음 활성 포인트 접근 방식 active node에 따라를 빨간색 노드로 설정했습니다. 그러나 문자 'c' 가 이미 가장자리에 있으므로 빨간색 노드에서 삽입하지 않습니다 . 파란색 노드에 접미사 링크가 없어야한다는 의미입니까? 아니요, 접미사 링크를 통해 파란색 노드와 빨간색 노드를 연결해야합니다. 왜 정확한가요? 이 하기 때문에 활성 점접근 방식을 통해 올바른 위치, 즉 더 짧은 접미사를 삽입해야하는 다음 위치로 이동할 수 있습니다.

마지막으로, Suffix Tree의 구현은 다음과 같습니다.

  1. 자바
  2. C ++

이 "개요"가 jogojapan의 자세한 답변과 결합되어 누군가가 자신의 Suffix Tree를 구현하는 데 도움이되기를 바랍니다.


3
노력해 주셔서 감사합니다. 나는 당신이 옳다고 확신합니다. 비록 세부 사항에 대해 바로 생각할 시간은 없지만. 나중에 확인하고 답변을 수정할 수도 있습니다.
jogojapan

정말 고마워 정말 도움이되었습니다. 하지만 관찰 3에 대해 더 구체적으로 설명해 주시겠습니까? 예를 들어, 새로운 접미사 링크를 소개하는 2 단계 다이어그램을 제공합니다. 노드가 활성 노드에 연결되어 있습니까? (실제로 두 번째 노드를 삽입하지 않기 때문에)
염료 염색 :

@makagonov 안녕하세요, 문자열 "cdddcdc"의 접미사 트리를 작성하도록 도와 드릴 수 있습니다 (시작 단계).
tariq zafar

3
규칙 3의 경우 현명한 방법은 루트의 접미사 링크를 루트 자체로 설정하고 (기본적으로) 모든 노드의 접미사 링크를 루트로 설정하는 것입니다. 따라서 우리는 컨디셔닝을 피하고 접미사 링크를 따라갈 수 있습니다.
sqd

1
aabaacaad접미사 링크를 추가하면 트리플 업데이트 시간을 줄일 수있는 경우 중 하나입니다. Jogojapan의 포스트의 마지막 두 단락의 결론은 잘못되었습니다. 이 게시물에서 언급 한 접미사 링크를 추가하지 않으면 평균 시간 복잡도는 O (nlong (n)) 이상이어야합니다. 올바른 것을 찾기 위해 나무를 걷는 데 시간이 더 걸리기 때문 active_node입니다.
IvanaGyro 2016 년

10

@jogojapan 의 잘 설명 된 자습서 덕분 에 Python에서 알고리즘을 구현했습니다.

@jogojapan이 언급 한 몇 가지 사소한 문제 는 내가 예상했던 것보다 더 정교 해졌 으며 매우 신중하게 처리해야합니다. 구현이 충분히 견고 해지 려면 며칠이 걸렸습니다 . 문제와 해결책은 다음과 같습니다.

  1. End withRemainder > 0 It이 상황은 전체 알고리즘의 끝뿐만 아니라 전개 단계 에서도 발생할 수 있습니다 . 이 경우 나머지, actnode, actedge 및 actlength를 변경하지 않고 그대로 두고 현재 전개 단계를 종료하고 원래 문자열의 다음 문자가 현재 경로에 있는지 여부에 따라 접거나 계속 전개하여 다른 단계를 시작할 수 있습니다. 아니.

  2. 노드를 통한 도약 : 접미사 링크를 따라갈 때 활성 지점을 업데이트 한 후 active_length 구성 요소가 새 active_node에서 제대로 작동하지 않는 것을 찾으십시오. 우리는 쪼개지거나 잎을 삽입하기 위해 올바른 장소 로 앞으로 이동 해야합니다 . 이 과정은 그렇게 간단하지 않을 수 있습니다 때문에하여 actlength를 이동하고 다시 이동해야 할 때 actedge 킵은, 모든에게 방법을 변경하는 동안 루트 노드actedgeactlength이 될 수 잘못 때문에 그 움직임. 해당 정보를 유지하려면 추가 변수가 필요합니다.

    여기에 이미지 설명을 입력하십시오

다른 두 가지 문제는 @managonov에 의해 어떻게 든 지적되었습니다.

  1. 분할이 생성수 있음 에지를 분할하려고 할 때 때때로 분할 작업이 노드에서 올바르게 이루어집니다. 이 경우 해당 노드에 새 리프만 추가하고 표준 에지 분할 작업으로 가져옵니다. 즉, 접미사 링크가있는 경우 해당 링크를 적절히 유지 관리해야합니다.

  2. 숨겨진 접미사 링크 문제 1문제 2 에 의해 발생하는 또 다른 특별한 경우가 있습니다 . 때때로 우리는 분할을 위해 여러 노드를 올바른 지점으로 홉해야 합니다. 나머지 문자열과 경로 레이블을 비교하여 이동하면 올바른 지점을 능가 할 수 있습니다 . 이 경우 접미사 링크가 있으면 의도 치 않게 무시됩니다. 앞으로 나아갈 때 올바른 지점기억하면 이를 피할 수 있습니다 . 분할 노드가 이미 존재하거나 접는 단계 중에 문제 1이 발생하는 경우 접미사 링크를 유지해야합니다 .

마지막으로 파이썬 에서의 구현 은 다음과 같습니다.

팁 : 위의 코드 에는 순진한 트리 인쇄 기능 이 포함되어 있으며 디버깅 중에 매우 중요 합니다. 시간을 많이 절약하고 특별한 경우를 찾기에 편리합니다.


10

대답이 중복되어 보이지만 사과하지만 최근 Ukkonen의 알고리즘을 구현하고 며칠 동안 어려움을 겪었습니다. 알고리즘의 핵심 측면의 이유와 방법을 이해하기 위해 주제에 대한 여러 논문을 읽어야했습니다.

이전 답변의 '규칙'접근법이 기본을 이해하는 데 도움이되지 않는다는 것을 알았습니다. 이유 것을 알았습니다. 따라서 실용주의에만 초점을 맞춘 모든 것을 아래에 작성했습니다. 내가했던 것처럼 다른 설명을 따르는 데 어려움을 겪고 있다면 보충 설명이 '클릭'할 수 있습니다.

C # 구현을 여기에 게시했습니다 : https://github.com/baratgabor/SuffixTree

본인은이 주제에 대한 전문가가 아니므로 다음 섹션에 부정확 한 내용이있을 수 있습니다. 문제가 발생하면 자유롭게 편집하십시오.

전제 조건

다음 설명의 시작점은 접미사 트리의 내용과 사용 및 Ukkonen 알고리즘의 특성, 예를 들어 접미사 트리 문자를 문자별로 확장하는 방법에 대해 잘 알고 있다고 가정합니다. 기본적으로 다른 설명을 이미 읽은 것으로 가정합니다.

(그러나 흐름에 대한 기본적인 설명을 추가해야 했으므로 시작 부분이 실제로 중복 될 수 있습니다.)

가장 흥미로운 부분은 접미사 링크 사용과 루트에서 다시 스캔하는 것의 차이점에 설명입니다 . 이것이 구현에 많은 버그와 두통을 줬습니다.

개방형 리프 노드와 그 한계

가장 근본적인 '속임수'는 접미사 끝을 '열린'그대로 두는 것, 즉 끝을 정적 값으로 설정하는 대신 문자열의 현재 길이를 참조 할 수 있다는 것을 이미 알고 있다는 것을 알고 있습니다. 이렇게하면 문자를 추가 할 때 해당 문자를 모두 방문하거나 업데이트하지 않고도 모든 접미사 레이블에 암시 적으로 추가됩니다.

그러나이 접미사의 끝은 명백한 이유로 문자열의 끝을 나타내는 노드, 즉 트리 구조의 리프 노드에만 적용됩니다. 트리에서 실행하는 분기 작업 (새 분기 노드 및 리프 노드 추가)은 필요한 모든 위치에 자동으로 전파되지 않습니다.

반복되는 부분 문자열이 트리에 이미 나타나지 않기 때문에 반복되는 부분 문자열이 트리에 명시 적으로 표시되지 않는다는 것은 아마도 초등 적이며 언급 할 필요는 없습니다. 그러나 반복적 인 하위 문자열이 반복되지 않는 문자를 만나서 끝나는 경우 해당 지점에서 분기점을 만들어 해당 지점에서 분기점을 나타냅니다.

예를 들어 문자열 'ABCXABCY' (아래 참조)의 경우 XY 로 분기 하는 세 가지 다른 접미사 ABC , BCC에 추가해야합니다 . 그렇지 않으면 유효한 접미사 트리가 아니며 루트의 문자를 아래쪽에서 일치시켜 문자열의 모든 하위 문자열을 찾을 수 없습니다.

다시 강조 하자면, 트리에서 접미사에 대해 실행하는 모든 작업은 연속 접미사 (예 : ABC> BC> C)에 의해 반영되어야합니다. 그렇지 않으면 유효한 접미사로 중단됩니다.

접미사에서 분기 반복

그러나 이러한 수동 업데이트를 수행해야한다는 사실을 인정하더라도 얼마나 많은 접미사를 업데이트해야하는지 어떻게 알 수 있습니까? 반복되는 문자 A 와 나머지 반복되는 문자를 연속으로 추가 할 때 접미사를 두 분기로 언제 / 어디서 분리해야하는지 아직 알 수 없습니다. 분할 할 필요성은 우리가 첫 번째 비 반복 문자 ( 이 경우 Y ( 트리에 이미 존재 하는 X 대신))를 만났을 때에 만 확인됩니다 .

우리가 할 수있는 일은 반복 가능한 가장 긴 문자열을 일치시키고 나중에 업데이트해야하는 접미사 수를 세는 것입니다. 이것이 '나머지'입니다 약자입니다.

'remainder'와 'rescanning'의 개념

변수 remainder 는 분기없이 암시 적으로 추가 한 반복 문자 수를 나타냅니다. 즉, 일치 할 수없는 첫 번째 문자를 찾은 후 분기 작업을 반복하기 위해 방문해야하는 접미사 수입니다. 이것은 기본적으로 우리가 루트에서 나무에 얼마나 많은 문자를 '깊게'넣었는지와 같습니다.

따라서 문자열 ABCXABCY 의 이전 예제를 유지하면서 반복 된 ABC 부분을 ​​'암시 적으로' 일치시켜 매번 증가 remainder하여 나머지 3을 만듭니다. 그런 다음 반복되지 않는 문자 'Y' 가 발생합니다 . 여기에서 이전에 추가 된 ABCXABC- > XABC- > Y 로 분할합니다 . 그런 다음 ABC 분기를 remainder이미 처리했기 때문에 3에서 2로 감소 했습니다 . 이제 우리는 마지막 두 문자 ( BC)를 루트에서 일치시켜 분할해야 할 지점에 도달 하여 작업을 반복하고 BCXBC 로 분할합니다. -> XBC -> Y . 다시, 우리 remainder는 1로 감소 하고 작업을 반복합니다. (가) 될 때까지 remainder마지막으로 0입니다, 우리는 (현재의 문자를 추가 할 필요가 Y를 루트뿐만 아니라 자체를).

이 작업은 루트에서 연속 접미사를 따라 단순히 작업을 수행해야하는 지점에 도달하기 때문에 '재검색' 이라고합니다. Ukkonen 알고리즘에서 재 탐색 하며 일반적으로 알고리즘에서 가장 비싼 부분입니다. 수십 개의 노드에서 긴 하위 문자열을 '재검색'해야하는 더 긴 문자열을 상상해보십시오 (나중에 설명 할 것임).

해결책으로 우리는 '접미사 링크' 라고 부르는 것을 소개 합니다. 합니다.

'접미사 링크'의 개념

접미사 링크는 기본적으로 일반적으로 '재검색' 해야하는 위치를 가리 키 므로 고가의 재검색 작업 대신 단순히 링크 된 위치로 이동하고 작업을 수행하고 다음 링크 된 위치로 건너 뛰어 반복 할 수 있습니다. 더 이상 업데이트 할 위치가 없습니다.

물론 하나의 큰 문제는 이러한 링크를 추가하는 방법입니다. 기존 답변은 새 분기 노드를 삽입 할 때 링크를 추가 할 수 있다는 것입니다. 트리의 각 확장에서 분기 노드는 서로 연결해야하는 순서대로 자연스럽게 하나씩 생성된다는 사실을 이용합니다. . 그러나 마지막으로 만든 분기 노드 (가장 긴 접미사)에서 이전에 만든 분기 노드로 연결해야하므로 마지막으로 만든 항목을 캐시하고 다음에 만든 항목을 연결하고 새로 만든 노드를 캐시해야합니다.

한 가지 결과는 주어진 분기 노드가 방금 생성 되었기 때문에 실제로 따라야 할 접미사 링크가 없다는 것입니다. 이 경우 우리는 여전히 루트에서 위에서 언급 한 '재검색'으로 넘어 가야 합니다. 이것이 삽입 후에 접미사 링크를 사용하거나 루트로 이동하라는 지시를받는 이유입니다.

(또는 대안으로, 노드에 부모 포인터를 저장하는 경우 부모를 따르고 링크가 있는지 확인한 후 사용하십시오. 나는 거의 언급되지 않았지만 접미사 링크 사용법은 그렇지 않습니다. 돌에서 설정합니다. 이 여러 가능한 방법이 있고, 당신은 기본 메커니즘을 이해한다면 당신은 당신의 요구에 가장 적합한 하나를 구현할 수 있습니다.)

'액티브 포인트'의 개념

지금까지 우리는 트리를 구축하기위한 여러 가지 효율적인 도구에 대해 논의했으며 여러 가장자리와 노드를 가로 지르는 것을 애매하게 언급했지만 해당 결과와 복잡성을 아직 조사하지 않았습니다.

앞에서 설명한 '리마인더' 개념은 트리의 어디에 있는지 추적하는 데 유용하지만 정보가 충분하지 않다는 것을 알아야합니다.

먼저 우리는 항상 노드의 특정 가장자리에 상주하므로 가장자리 정보를 저장해야합니다. 우리는 이것을 '능동 에지'라고 부를 것이다.

둘째, 엣지 정보를 추가 한 후에도 여전히 트리에서 더 아래쪽에 있고 루트 노드에 직접 연결되지 않은 위치를 식별 할 수있는 방법이 없습니다 . 따라서 노드도 저장해야합니다. 이것을 'active node' 라고합시다 .

마지막으로, 우리는 것을 알 수 있습니다 '나머지' 때문에, 직접 루트에 연결되지 않은 가장자리에 위치를 식별하는 데 부적절하다 ', 나머지는' 전체 경로의 길이가; 그리고 아마도 우리는 이전 가장자리의 길이를 기억하고 빼는 것을 귀찮게하고 싶지 않을 것입니다. 따라서 현재 가장자리나머지 부분 인 표현이 필요합니다 . 이것이 우리가 'active length' 라고 부르는 것 입니다.

이것은 우리가 '액티브 포인트' 라고 부르는 것으로 이어집니다. – 트리에서 우리의 위치에 관해 우리가 유지해야하는 모든 정보를 포함하는 세 개의 변수 패키지입니다 :

Active Point = (Active Node, Active Edge, Active Length)

다음 이미지에서 ABCABD 의 일치 된 라우트가 모서리 AB 의 2 자 ( 루트에서 )와 모서리 CABDABCABD의 4 자 (노드 4에서)로 구성되는 방식을 관찰 하여 6 자의 '나머지' 를 얻을 수 있습니다. 따라서 현재 위치는 Active Node 4, Active Edge C, Active Length 4 로 식별 할 수 있습니다 .

나머지 및 액티브 포인트

'활성 지점' 의 또 다른 중요한 역할은 알고리즘에 추상화 계층을 제공한다는 것입니다. 즉 , 해당 활성 지점이 루트에 있는지 또는 다른 곳에 있는지에 관계없이 알고리즘의 일부가 '활성 지점' 에 대한 작업을 수행 할 수 있습니다. . 이를 통해 알고리즘에서 접미사 링크 사용을 쉽고 간단하게 구현할 수 있습니다.

접미사 링크와 재검색의 차이점

이제 내 경험상 까다로운 부분은 많은 버그와 두통을 유발할 수 있으며 대부분의 출처에서 잘 설명하지 못하는 접미사 연결 사례와 재검색 사례 처리의 차이입니다.

문자열 'AAAABAAAABAAC' 의 다음 예를 고려하십시오 .

여러 모서리에 걸친 나머지

위에서 'remainder'7 이 루트의 총 문자 수에 해당하는 반면, 'active length'4 는 활성 노드의 활성 가장자리에서 일치하는 문자의 합계에 해당합니다.

이제 활성 지점에서 분기 작업을 실행 한 후 활성 노드에 접미사 링크가 포함되거나 포함되지 않을 수 있습니다.

접미사 링크가있는 경우 : '활성 길이' 부분 만 처리하면 됩니다. '나머지' 때문에 무관하다 우리는 접미사 링크를 통해 이동할 노드가 이미 암시 적으로 올바른 '나머지'를 인코딩 단순히이 트리에있는 덕분에,.

접미사 링크가없는 경우 : 0 / 루트에서 '재검색' 해야합니다 . 이는 처음부터 전체 접미사를 처리 함을 의미합니다. 이를 위해 우리는 전체 '리마인더' 를 재검색의 기초로 사용해야합니다 .

접미사 링크 유무 처리의 예제 비교

위 예제의 다음 단계에서 어떤 일이 발생하는지 고려하십시오. 접미사 링크 유무에 관계없이 동일한 결과를 얻는 방법 (즉, 처리 할 다음 접미사로 이동)을 비교해 봅시다.

사용 '접미사 링크를'

접미사 링크를 통해 연속 접미사에 도달

접미사 링크를 사용하면 자동으로 '적절한 위치에 있습니다'. '활성 길이' 가 새로운 위치와 '호환되지 않을'수 있다는 사실 때문에 엄격하게 사실이 아닙니다 .

위의 경우 'active length' 가 4이므로 연결된 노드 4부터 시작하여 ' ABAA' 접미사로 작업하고 있지만 접미사 의 첫 문자 ( 'A'에 해당하는 모서리를 찾은 후 ), 'active length' 가이 가장자리에 3자를 넘습니다. 그래서 우리는 전체 노드를 넘어 다음 노드로 건너 뛰고 점프와 함께 소비 한 문자로 '활성 길이' 를 줄입니다.

그런 다음 감소 된 접미사 'BAA '에 해당하는 다음 모서리 'B'를 찾은 후 마지막으로 모서리 길이가 나머지 '활성 길이' 3 보다 큽니다. 즉, 올바른 위치를 찾았습니다.

길이가 짧고 루트가 아닌 시작점이있는 경우에도이 작업은 일반적으로 '재검색'으로 표시되지 않는 것 같습니다.

사용 '다시 검색'

재검색을 통해 연속 접미사에 도달

전통적인 '재검색'작업을 사용하는 경우 (여기서는 접미사 링크가없는 척) 트리의 맨 위에서 시작하여 루트로 돌아가서 올바른 위치로 다시 내려 가야합니다. 현재 접미사의 전체 길이를 따라.

이 접미사의 길이 는 앞에서 논의한 '나머지' 입니다. 우리는이 잔량을 모두 0이 될 때까지 소비해야합니다. 여기에는 여러 노드를 통해 점프하는 것이 포함될 수 있으며, 각 점프에서 우리가 점프 한 가장자리의 길이만큼 나머지를 줄입니다. 그런 다음 마지막으로 남은 '나머지' 보다 긴 가장자리에 도달합니다 . 여기서 우리는 활성 가장자리를 주어진 가장자리로 설정하고 'active length' 를 나머지 'remainder '로 설정했습니다.

그러나 실제 'remainder' 변수는 유지되어야하며 각 노드 삽입 후에 만 ​​감소해야합니다. 그래서 위에서 설명한 것은 'remainder'로 초기화 된 별도의 변수를 사용한다고 가정했습니다 .

접미사 링크 및 재검색에 대한 참고 사항

1) 두 방법 모두 동일한 결과를 초래합니다. 그러나 접미사 링크 점프는 대부분의 경우 훨씬 빠릅니다. 이것이 접미사 링크의 전체 이론적 근거입니다.

2) 실제 알고리즘 구현은 다를 필요가 없습니다. 위에서 언급했듯이 접미사 링크를 사용하는 경우에도 트리의 해당 분기에 추가 분기가 포함될 수 있으므로 '활성 길이' 는 종종 연결된 위치와 호환되지 않습니다. 따라서 본질적으로 '활성 길이' 를 사용해야합니다. 으로 'remainder 대신 하고 나머지 접미사 길이보다 짧은 가장자리를 찾을 때까지 동일한 재검색 논리를 실행해야합니다.

3) 성능과 관련하여 한 가지 중요한 설명은 다시 스캔하는 동안 각각의 모든 문자를 확인할 필요가 없다는 것입니다. 유효한 접미사 트리가 작성되는 방식으로 인해 문자가 일치한다고 가정 할 수 있습니다. 따라서 대부분 길이를 계산하고 있으며 가장자리가 첫 번째 문자 (주어진 노드의 컨텍스트에서 항상 고유)로 식별되기 때문에 새로운 가장자리로 이동할 때 문자 동등성 검사가 필요합니다. 이것은 '재검색'논리가 전체 문자열 일치 논리와 다릅니다 (즉, 트리에서 하위 문자열 검색).

4) 여기에 설명 된 원래 접미사 연결 은 가능한 방법 중 하나 일뿐 입니다. 예를 들어 NJ Larsson et al. 이 접근법의 이름을 Node-Oriented Top-Down 하고이를 Node-Oriented Bottom-Up 및 2 개의 Edge-Oriented 품종 과 비교합니다 . 서로 다른 접근 방식은 일반적이고 최악의 경우 성능, 요구 사항, 제한 등이 다르지만 일반적으로 Edge 지향 접근 방식은 원본에 대한 전반적인 개선 인 것으로 보입니다 .


8

@ jogojapan 당신은 멋진 설명과 시각화를 가져 왔습니다. 그러나 @makagonov가 언급했듯이 접미사 링크 설정과 관련된 규칙이 누락되었습니다. 단계별로 진행하면 멋지게 보입니다.http://brenden.github.io/ukkonen-animation/ 에서 단어 'aabaaabb'을 통해 . 10 단계에서 11 단계로 이동하면 노드 5에서 노드 2 로의 접미사 링크가 없지만 활성 지점이 갑자기 이동합니다.

@makagonov Java 세계에 거주 한 이래로 ST 구축 워크 플로우를 파악하기 위해 구현을 따르려고했지만 다음과 같은 이유로 인해 힘들었습니다.

  • 가장자리를 노드와 결합
  • 참조 대신 인덱스 포인터 사용
  • 진술을 어 기고;
  • 계속 진술;

그래서 Java에서 이러한 구현을 끝내고 모든 단계를 더 명확하게 반영하고 다른 Java 사람들의 학습 시간을 단축하기를 바랍니다.

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class ST {

  public class Node {
    private final int id;
    private final Map<Character, Edge> edges;
    private Node slink;

    public Node(final int id) {
        this.id = id;
        this.edges = new HashMap<>();
    }

    public void setSlink(final Node slink) {
        this.slink = slink;
    }

    public Map<Character, Edge> getEdges() {
        return this.edges;
    }

    public Node getSlink() {
        return this.slink;
    }

    public String toString(final String word) {
        return new StringBuilder()
                .append("{")
                .append("\"id\"")
                .append(":")
                .append(this.id)
                .append(",")
                .append("\"slink\"")
                .append(":")
                .append(this.slink != null ? this.slink.id : null)
                .append(",")
                .append("\"edges\"")
                .append(":")
                .append(edgesToString(word))
                .append("}")
                .toString();
    }

    private StringBuilder edgesToString(final String word) {
        final StringBuilder edgesStringBuilder = new StringBuilder();
        edgesStringBuilder.append("{");
        for(final Map.Entry<Character, Edge> entry : this.edges.entrySet()) {
            edgesStringBuilder.append("\"")
                    .append(entry.getKey())
                    .append("\"")
                    .append(":")
                    .append(entry.getValue().toString(word))
                    .append(",");
        }
        if(!this.edges.isEmpty()) {
            edgesStringBuilder.deleteCharAt(edgesStringBuilder.length() - 1);
        }
        edgesStringBuilder.append("}");
        return edgesStringBuilder;
    }

    public boolean contains(final String word, final String suffix) {
        return !suffix.isEmpty()
                && this.edges.containsKey(suffix.charAt(0))
                && this.edges.get(suffix.charAt(0)).contains(word, suffix);
    }
  }

  public class Edge {
    private final int from;
    private final int to;
    private final Node next;

    public Edge(final int from, final int to, final Node next) {
        this.from = from;
        this.to = to;
        this.next = next;
    }

    public int getFrom() {
        return this.from;
    }

    public int getTo() {
        return this.to;
    }

    public Node getNext() {
        return this.next;
    }

    public int getLength() {
        return this.to - this.from;
    }

    public String toString(final String word) {
        return new StringBuilder()
                .append("{")
                .append("\"content\"")
                .append(":")
                .append("\"")
                .append(word.substring(this.from, this.to))
                .append("\"")
                .append(",")
                .append("\"next\"")
                .append(":")
                .append(this.next != null ? this.next.toString(word) : null)
                .append("}")
                .toString();
    }

    public boolean contains(final String word, final String suffix) {
        if(this.next == null) {
            return word.substring(this.from, this.to).equals(suffix);
        }
        return suffix.startsWith(word.substring(this.from,
                this.to)) && this.next.contains(word, suffix.substring(this.to - this.from));
    }
  }

  public class ActivePoint {
    private final Node activeNode;
    private final Character activeEdgeFirstCharacter;
    private final int activeLength;

    public ActivePoint(final Node activeNode,
                       final Character activeEdgeFirstCharacter,
                       final int activeLength) {
        this.activeNode = activeNode;
        this.activeEdgeFirstCharacter = activeEdgeFirstCharacter;
        this.activeLength = activeLength;
    }

    private Edge getActiveEdge() {
        return this.activeNode.getEdges().get(this.activeEdgeFirstCharacter);
    }

    public boolean pointsToActiveNode() {
        return this.activeLength == 0;
    }

    public boolean activeNodeIs(final Node node) {
        return this.activeNode == node;
    }

    public boolean activeNodeHasEdgeStartingWith(final char character) {
        return this.activeNode.getEdges().containsKey(character);
    }

    public boolean activeNodeHasSlink() {
        return this.activeNode.getSlink() != null;
    }

    public boolean pointsToOnActiveEdge(final String word, final char character) {
        return word.charAt(this.getActiveEdge().getFrom() + this.activeLength) == character;
    }

    public boolean pointsToTheEndOfActiveEdge() {
        return this.getActiveEdge().getLength() == this.activeLength;
    }

    public boolean pointsAfterTheEndOfActiveEdge() {
        return this.getActiveEdge().getLength() < this.activeLength;
    }

    public ActivePoint moveToEdgeStartingWithAndByOne(final char character) {
        return new ActivePoint(this.activeNode, character, 1);
    }

    public ActivePoint moveToNextNodeOfActiveEdge() {
        return new ActivePoint(this.getActiveEdge().getNext(), null, 0);
    }

    public ActivePoint moveToSlink() {
        return new ActivePoint(this.activeNode.getSlink(),
                this.activeEdgeFirstCharacter,
                this.activeLength);
    }

    public ActivePoint moveTo(final Node node) {
        return new ActivePoint(node, this.activeEdgeFirstCharacter, this.activeLength);
    }

    public ActivePoint moveByOneCharacter() {
        return new ActivePoint(this.activeNode,
                this.activeEdgeFirstCharacter,
                this.activeLength + 1);
    }

    public ActivePoint moveToEdgeStartingWithAndByActiveLengthMinusOne(final Node node,
                                                                       final char character) {
        return new ActivePoint(node, character, this.activeLength - 1);
    }

    public ActivePoint moveToNextNodeOfActiveEdge(final String word, final int index) {
        return new ActivePoint(this.getActiveEdge().getNext(),
                word.charAt(index - this.activeLength + this.getActiveEdge().getLength()),
                this.activeLength - this.getActiveEdge().getLength());
    }

    public void addEdgeToActiveNode(final char character, final Edge edge) {
        this.activeNode.getEdges().put(character, edge);
    }

    public void splitActiveEdge(final String word,
                                final Node nodeToAdd,
                                final int index,
                                final char character) {
        final Edge activeEdgeToSplit = this.getActiveEdge();
        final Edge splittedEdge = new Edge(activeEdgeToSplit.getFrom(),
                activeEdgeToSplit.getFrom() + this.activeLength,
                nodeToAdd);
        nodeToAdd.getEdges().put(word.charAt(activeEdgeToSplit.getFrom() + this.activeLength),
                new Edge(activeEdgeToSplit.getFrom() + this.activeLength,
                        activeEdgeToSplit.getTo(),
                        activeEdgeToSplit.getNext()));
        nodeToAdd.getEdges().put(character, new Edge(index, word.length(), null));
        this.activeNode.getEdges().put(this.activeEdgeFirstCharacter, splittedEdge);
    }

    public Node setSlinkTo(final Node previouslyAddedNodeOrAddedEdgeNode,
                           final Node node) {
        if(previouslyAddedNodeOrAddedEdgeNode != null) {
            previouslyAddedNodeOrAddedEdgeNode.setSlink(node);
        }
        return node;
    }

    public Node setSlinkToActiveNode(final Node previouslyAddedNodeOrAddedEdgeNode) {
        return setSlinkTo(previouslyAddedNodeOrAddedEdgeNode, this.activeNode);
    }
  }

  private static int idGenerator;

  private final String word;
  private final Node root;
  private ActivePoint activePoint;
  private int remainder;

  public ST(final String word) {
    this.word = word;
    this.root = new Node(idGenerator++);
    this.activePoint = new ActivePoint(this.root, null, 0);
    this.remainder = 0;
    build();
  }

  private void build() {
    for(int i = 0; i < this.word.length(); i++) {
        add(i, this.word.charAt(i));
    }
  }

  private void add(final int index, final char character) {
    this.remainder++;
    boolean characterFoundInTheTree = false;
    Node previouslyAddedNodeOrAddedEdgeNode = null;
    while(!characterFoundInTheTree && this.remainder > 0) {
        if(this.activePoint.pointsToActiveNode()) {
            if(this.activePoint.activeNodeHasEdgeStartingWith(character)) {
                activeNodeHasEdgeStartingWithCharacter(character, previouslyAddedNodeOrAddedEdgeNode);
                characterFoundInTheTree = true;
            }
            else {
                if(this.activePoint.activeNodeIs(this.root)) {
                    rootNodeHasNotEdgeStartingWithCharacter(index, character);
                }
                else {
                    previouslyAddedNodeOrAddedEdgeNode = internalNodeHasNotEdgeStartingWithCharacter(index,
                            character, previouslyAddedNodeOrAddedEdgeNode);
                }
            }
        }
        else {
            if(this.activePoint.pointsToOnActiveEdge(this.word, character)) {
                activeEdgeHasCharacter();
                characterFoundInTheTree = true;
            }
            else {
                if(this.activePoint.activeNodeIs(this.root)) {
                    previouslyAddedNodeOrAddedEdgeNode = edgeFromRootNodeHasNotCharacter(index,
                            character,
                            previouslyAddedNodeOrAddedEdgeNode);
                }
                else {
                    previouslyAddedNodeOrAddedEdgeNode = edgeFromInternalNodeHasNotCharacter(index,
                            character,
                            previouslyAddedNodeOrAddedEdgeNode);
                }
            }
        }
    }
  }

  private void activeNodeHasEdgeStartingWithCharacter(final char character,
                                                    final Node previouslyAddedNodeOrAddedEdgeNode) {
    this.activePoint.setSlinkToActiveNode(previouslyAddedNodeOrAddedEdgeNode);
    this.activePoint = this.activePoint.moveToEdgeStartingWithAndByOne(character);
    if(this.activePoint.pointsToTheEndOfActiveEdge()) {
        this.activePoint = this.activePoint.moveToNextNodeOfActiveEdge();
    }
  }

  private void rootNodeHasNotEdgeStartingWithCharacter(final int index, final char character) {
    this.activePoint.addEdgeToActiveNode(character, new Edge(index, this.word.length(), null));
    this.activePoint = this.activePoint.moveTo(this.root);
    this.remainder--;
    assert this.remainder == 0;
  }

  private Node internalNodeHasNotEdgeStartingWithCharacter(final int index,
                                                         final char character,
                                                         Node previouslyAddedNodeOrAddedEdgeNode) {
    this.activePoint.addEdgeToActiveNode(character, new Edge(index, this.word.length(), null));
    previouslyAddedNodeOrAddedEdgeNode = this.activePoint.setSlinkToActiveNode(previouslyAddedNodeOrAddedEdgeNode);
    if(this.activePoint.activeNodeHasSlink()) {
        this.activePoint = this.activePoint.moveToSlink();
    }
    else {
        this.activePoint = this.activePoint.moveTo(this.root);
    }
    this.remainder--;
    return previouslyAddedNodeOrAddedEdgeNode;
  }

  private void activeEdgeHasCharacter() {
    this.activePoint = this.activePoint.moveByOneCharacter();
    if(this.activePoint.pointsToTheEndOfActiveEdge()) {
        this.activePoint = this.activePoint.moveToNextNodeOfActiveEdge();
    }
  }

  private Node edgeFromRootNodeHasNotCharacter(final int index,
                                             final char character,
                                             Node previouslyAddedNodeOrAddedEdgeNode) {
    final Node newNode = new Node(idGenerator++);
    this.activePoint.splitActiveEdge(this.word, newNode, index, character);
    previouslyAddedNodeOrAddedEdgeNode = this.activePoint.setSlinkTo(previouslyAddedNodeOrAddedEdgeNode, newNode);
    this.activePoint = this.activePoint.moveToEdgeStartingWithAndByActiveLengthMinusOne(this.root,
            this.word.charAt(index - this.remainder + 2));
    this.activePoint = walkDown(index);
    this.remainder--;
    return previouslyAddedNodeOrAddedEdgeNode;
  }

  private Node edgeFromInternalNodeHasNotCharacter(final int index,
                                                 final char character,
                                                 Node previouslyAddedNodeOrAddedEdgeNode) {
    final Node newNode = new Node(idGenerator++);
    this.activePoint.splitActiveEdge(this.word, newNode, index, character);
    previouslyAddedNodeOrAddedEdgeNode = this.activePoint.setSlinkTo(previouslyAddedNodeOrAddedEdgeNode, newNode);
    if(this.activePoint.activeNodeHasSlink()) {
        this.activePoint = this.activePoint.moveToSlink();
    }
    else {
        this.activePoint = this.activePoint.moveTo(this.root);
    }
    this.activePoint = walkDown(index);
    this.remainder--;
    return previouslyAddedNodeOrAddedEdgeNode;
  }

  private ActivePoint walkDown(final int index) {
    while(!this.activePoint.pointsToActiveNode()
            && (this.activePoint.pointsToTheEndOfActiveEdge() || this.activePoint.pointsAfterTheEndOfActiveEdge())) {
        if(this.activePoint.pointsAfterTheEndOfActiveEdge()) {
            this.activePoint = this.activePoint.moveToNextNodeOfActiveEdge(this.word, index);
        }
        else {
            this.activePoint = this.activePoint.moveToNextNodeOfActiveEdge();
        }
    }
    return this.activePoint;
  }

  public String toString(final String word) {
    return this.root.toString(word);
  }

  public boolean contains(final String suffix) {
    return this.root.contains(this.word, suffix);
  }

  public static void main(final String[] args) {
    final String[] words = {
            "abcabcabc$",
            "abc$",
            "abcabxabcd$",
            "abcabxabda$",
            "abcabxad$",
            "aabaaabb$",
            "aababcabcd$",
            "ababcabcd$",
            "abccba$",
            "mississipi$",
            "abacabadabacabae$",
            "abcabcd$",
            "00132220$"
    };
    Arrays.stream(words).forEach(word -> {
        System.out.println("Building suffix tree for word: " + word);
        final ST suffixTree = new ST(word);
        System.out.println("Suffix tree: " + suffixTree.toString(word));
        for(int i = 0; i < word.length() - 1; i++) {
            assert suffixTree.contains(word.substring(i)) : word.substring(i);
        }
    });
  }
}

6

내 직감은 다음과 같습니다.

메인 루프의 k 반복 후에는 첫 번째 k 문자로 시작하는 전체 문자열의 모든 접미사를 포함하는 접미사 트리를 구성했습니다.

처음에 이것은 접미사 트리에 전체 문자열을 나타내는 단일 루트 노드가 포함됨을 의미합니다 (이것은 0에서 시작하는 유일한 접미사입니다).

len (string) 반복 후에는 모든 접미사를 포함하는 접미사 트리가 있습니다.

루프 중에 키는 활성 지점입니다. 내 생각에 이것은 문자열의 첫 번째 k 문자의 적절한 접미사에 해당하는 접미사 트리의 가장 깊은 지점을 나타냅니다. (적절한 의미는 접미사가 전체 문자열이 될 수 없음을 의미한다고 생각합니다.)

예를 들어, 'abcabc'문자가 있다고 가정하십시오. 활성 포인트는 접미사 'abc'에 해당하는 트리의 포인트를 나타냅니다.

활성 지점은 (원점, 첫 번째, 마지막)으로 표시됩니다. 이것은 현재 노드 원점에서 시작하여 string [first : last]의 문자를 입력하여 트리의 지점에 있음을 의미합니다.

새 캐릭터를 추가하면 활성 포인트가 여전히 기존 트리에 있는지 확인합니다. 그렇다면 완료된 것입니다. 그렇지 않으면 활성 지점에서 접미사 트리에 새 노드를 추가하고 다음으로 가장 짧은 일치 항목으로 폴백 한 후 다시 확인해야합니다.

참고 1 : 접미사 포인터는 각 노드에 대해 가장 짧은 다음 일치 항목에 대한 링크를 제공합니다.

참고 2 : 새 노드를 추가하고 폴백하면 새 노드에 새 접미사 포인터가 추가됩니다. 이 접미사 포인터의 대상은 단축 된 활성 지점의 노드입니다. 이 노드는 이미 존재하거나 다음 폴백 루프의 다음 반복에서 작성됩니다.

참고 3 : 정규화 부분은 단순히 활성 지점을 확인하는 시간을 절약 해줍니다. 예를 들어, 항상 origin = 0을 사용하고 처음과 마지막 만 변경했다고 가정합니다. 활성 지점을 확인하려면 모든 중간 노드를 따라 매번 접미사 트리를 따라야합니다. 마지막 노드로부터의 거리 만 기록하여이 경로를 따른 결과를 캐시하는 것이 좋습니다.

"수정"경계 변수가 무엇을 의미하는지에 대한 코드 예제를 제공 할 수 있습니까?

건강 경고 : 나는이 알고리즘을 이해하기가 특히 어렵다는 것을 알았 으므로이 직관이 모든 중요한 세부 사항에서 부정확 할 수 있음을 인식하십시오 ...


학술 논문 중 하나는 "적절한"을 문자열의 "적절한 접미사"에 첫 문자가 포함되지 않음을 의미하는 것으로 정의합니다. 때로는 전체 하위 문자열을 "접미사"라고 부르지 만 알고리즘을 정의 할 때 "문자열"및 "하위 문자열"및 "접미사"라는 용어는 자유로이 발생하며 때로는 "접미사"의 의미를 명확하게 설명해야합니다. "적절한 접미사"라는 용어는 모든 것을 접미사로 부르는 것을 배제합니다. 따라서 문자열의 접미사 하위 문자열은 합법적 인 하위 문자열이 될 수 있으며 동일한 접미사가 아닌 적절한 접미사를 가질 수 있습니다. 논리 때문입니다.
블레어 휴턴

3

안녕하세요, 루비에서 위에서 설명한 구현을 구현하려고했습니다. 확인하십시오. 잘 작동하는 것 같습니다.

구현의 유일한 차이점은 기호를 사용하는 대신 가장자리 객체를 사용하려고한다는 것입니다.

https://gist.github.com/suchitpuri/9304856 에도 있습니다.

    require 'pry'


class Edge
    attr_accessor :data , :edges , :suffix_link
    def initialize data
        @data = data
        @edges = []
        @suffix_link = nil
    end

    def find_edge element
        self.edges.each do |edge|
            return edge if edge.data.start_with? element
        end
        return nil
    end
end

class SuffixTrees
    attr_accessor :root , :active_point , :remainder , :pending_prefixes , :last_split_edge , :remainder

    def initialize
        @root = Edge.new nil
        @active_point = { active_node: @root , active_edge: nil , active_length: 0}
        @remainder = 0
        @pending_prefixes = []
        @last_split_edge = nil
        @remainder = 1
    end

    def build string
        string.split("").each_with_index do |element , index|


            add_to_edges @root , element        

            update_pending_prefix element                           
            add_pending_elements_to_tree element
            active_length = @active_point[:active_length]

            # if(@active_point[:active_edge] && @active_point[:active_edge].data && @active_point[:active_edge].data[0..active_length-1] ==  @active_point[:active_edge].data[active_length..@active_point[:active_edge].data.length-1])
            #   @active_point[:active_edge].data = @active_point[:active_edge].data[0..active_length-1]
            #   @active_point[:active_edge].edges << Edge.new(@active_point[:active_edge].data)
            # end

            if(@active_point[:active_edge] && @active_point[:active_edge].data && @active_point[:active_edge].data.length == @active_point[:active_length]  )
                @active_point[:active_node] =  @active_point[:active_edge]
                @active_point[:active_edge] = @active_point[:active_node].find_edge(element[0])
                @active_point[:active_length] = 0
            end
        end
    end

    def add_pending_elements_to_tree element

        to_be_deleted = []
        update_active_length = false
        # binding.pry
        if( @active_point[:active_node].find_edge(element[0]) != nil)
            @active_point[:active_length] = @active_point[:active_length] + 1               
            @active_point[:active_edge] = @active_point[:active_node].find_edge(element[0]) if @active_point[:active_edge] == nil
            @remainder = @remainder + 1
            return
        end



        @pending_prefixes.each_with_index do |pending_prefix , index|

            # binding.pry           

            if @active_point[:active_edge] == nil and @active_point[:active_node].find_edge(element[0]) == nil

                @active_point[:active_node].edges << Edge.new(element)

            else

                @active_point[:active_edge] = node.find_edge(element[0]) if @active_point[:active_edge]  == nil

                data = @active_point[:active_edge].data
                data = data.split("")               

                location = @active_point[:active_length]


                # binding.pry
                if(data[0..location].join == pending_prefix or @active_point[:active_node].find_edge(element) != nil )                  


                else #tree split    
                    split_edge data , index , element
                end

            end
        end 
    end



    def update_pending_prefix element
        if @active_point[:active_edge] == nil
            @pending_prefixes = [element]
            return

        end

        @pending_prefixes = []

        length = @active_point[:active_edge].data.length
        data = @active_point[:active_edge].data
        @remainder.times do |ctr|
                @pending_prefixes << data[-(ctr+1)..data.length-1]
        end

        @pending_prefixes.reverse!

    end

    def split_edge data , index , element
        location = @active_point[:active_length]
        old_edges = []
        internal_node = (@active_point[:active_edge].edges != nil)

        if (internal_node)
            old_edges = @active_point[:active_edge].edges 
            @active_point[:active_edge].edges = []
        end

        @active_point[:active_edge].data = data[0..location-1].join                 
        @active_point[:active_edge].edges << Edge.new(data[location..data.size].join)


        if internal_node
            @active_point[:active_edge].edges << Edge.new(element)
        else
            @active_point[:active_edge].edges << Edge.new(data.last)        
        end

        if internal_node
            @active_point[:active_edge].edges[0].edges = old_edges
        end


        #setup the suffix link
        if @last_split_edge != nil and @last_split_edge.data.end_with?@active_point[:active_edge].data 

            @last_split_edge.suffix_link = @active_point[:active_edge] 
        end

        @last_split_edge = @active_point[:active_edge]

        update_active_point index

    end


    def update_active_point index
        if(@active_point[:active_node] == @root)
            @active_point[:active_length] = @active_point[:active_length] - 1
            @remainder = @remainder - 1
            @active_point[:active_edge] = @active_point[:active_node].find_edge(@pending_prefixes.first[index+1])
        else
            if @active_point[:active_node].suffix_link != nil
                @active_point[:active_node] = @active_point[:active_node].suffix_link               
            else
                @active_point[:active_node] = @root
            end 
            @active_point[:active_edge] = @active_point[:active_node].find_edge(@active_point[:active_edge].data[0])
            @remainder = @remainder - 1     
        end
    end

    def add_to_edges root , element     
        return if root == nil
        root.data = root.data + element if(root.data and root.edges.size == 0)
        root.edges.each do |edge|
            add_to_edges edge , element
        end
    end
end

suffix_tree = SuffixTrees.new
suffix_tree.build("abcabxabcd")
binding.pry
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.