BIT : 이진 인덱스 트리의 직관은 무엇이며 어떻게 생각 되었습니까?


99

이진 색인 트리는 다른 데이터 구조와 비교할 때 문헌이 적거나 거의 없습니다. 그것이 가르치는 유일한 곳 은 topcoder tutorial 입니다. 튜토리얼이 모든 설명에서 완료되었지만 그러한 나무의 직관을 이해할 수 없습니까? 어떻게 발명 되었습니까? 정확성에 대한 실제 증거는 무엇입니까?


4
Wikipedia에 관한 기사는 이것을 Fenwick tree 라고 합니다.
David Harkness

2
@ DavidHarkness- Peter Fenwick은 데이터 구조를 발명하여 Fenwick 트리라고도합니다. 그의 원본 논문 ( citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.14.8917 )에서 이진 색인 트리로 불렀습니다. 이 두 용어는 종종 같은 의미로 사용됩니다.
templatetypedef

1
다음 답변은 이진 인덱스 트리 cs.stackexchange.com/questions/42811/… 의 아주 "시각적"직관을 전달합니다 .
Rabih Kodeih

1
톱 코더 기사를 처음 읽었을 때 기분이 어떤지 잘 알고 있습니다.
Rockstar5645

답변:


168

직관적으로 이진 색인 트리는 표준 배열 표현의 최적화 인 이진 트리의 압축 표현으로 생각할 수 있습니다. 이 답변은 하나의 가능한 파생으로 들어갑니다.

예를 들어 총 7 개의 다른 요소에 대해 누적 빈도를 저장한다고 가정 해 봅시다. 숫자가 배포 될 7 개의 버킷을 작성하여 시작할 수 있습니다.

[   ] [   ] [   ] [   ] [   ] [   ] [   ]
  1     2     3     4     5     6     7

이제 누적 빈도가 다음과 같다고 가정 해 봅시다.

[ 5 ] [ 6 ] [14 ] [25 ] [77 ] [105] [105]
  1     2     3     4     5     6     7

이 버전의 배열을 사용하면 해당 지점에 저장된 숫자 값을 증가시킨 다음 이후에 나오는 모든 항목의 주파수를 증가시켜 요소의 누적 주파수를 증가시킬 수 있습니다. 예를 들어 누적 빈도 3을 7 씩 늘리려면 다음과 같이 위치 3 또는 그 이후에 배열의 각 요소에 7을 추가 할 수 있습니다.

[ 5 ] [ 6 ] [21 ] [32 ] [84 ] [112] [112]
  1     2     3     4     5     6     7

이것의 문제는 이것을하기 위해 O (n) 시간이 걸린다는 것인데, n이 크면 꽤 느립니다.

이 작업 개선에 대해 생각할 수있는 한 가지 방법은 버킷에 저장하는 내용을 변경하는 것입니다. 주어진 지점까지 누적 주파수를 저장하는 대신 현재 버킷이 이전 버킷에 비해 증가한 양을 저장하는 것을 생각할 수 있습니다. 예를 들어,이 경우 위의 버킷을 다음과 같이 다시 작성합니다.

Before:
[ 5 ] [ 6 ] [21 ] [32 ] [84 ] [112] [112]
  1     2     3     4     5     6     7

After:
[ +5] [ +1] [+15] [+11] [+52] [+28] [ +0]
  1     2     3     4     5     6     7

이제 버킷에 적절한 양을 추가하여 시간 O (1)에서 버킷 내 빈도를 증가시킬 수 있습니다. 그러나 이제 모든 작은 버킷의 값을 합산하여 버킷의 총계를 재 계산해야하므로 조회를 수행하는 데 드는 총 비용이 O (n)이됩니다.

여기에서 이진 인덱스 트리로 가져와야하는 첫 번째 주요 통찰력은 다음과 같습니다. 특정 요소 앞에 오는 배열 요소의 합계를 지속적으로 재 계산하지 않고 특정 요소 이전에 모든 요소의 총계를 사전 계산하는 경우 시퀀스의 포인트? 그렇게 할 수 있다면, 사전 계산 된 합의 올바른 조합을 요약하여 특정 시점에서 누적 합을 알아낼 수 있습니다.

이를 수행하는 한 가지 방법은 표현을 버킷 배열에서 이진 노드 트리로 변경하는 것입니다. 각 노드에는 해당 노드의 왼쪽에있는 모든 노드의 누적 합계를 나타내는 값이 주석으로 표시됩니다. 예를 들어,이 노드에서 다음 이진 트리를 구성한다고 가정 해보십시오.

             4
          /     \
         2       6
        / \     / \
       1   3   5   7

이제 해당 노드와 왼쪽 하위 트리를 포함한 모든 값의 누적 합계를 저장하여 각 노드를 보강 할 수 있습니다. 예를 들어, 값이 주어지면 다음을 저장합니다.

Before:
[ +5] [ +1] [+15] [+11] [+52] [+28] [ +0]
  1     2     3     4     5     6     7

After:
                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
        1     3     5     7
      [ +5] [+15] [+52] [ +0]

이 트리 구조가 주어지면 누적 합계를 한 지점까지 쉽게 결정할 수 있습니다. 아이디어는 다음과 같습니다. 처음에 카운터를 유지 한 다음 문제가있는 노드를 찾을 때까지 정상적인 이진 검색을 수행합니다. 우리가 그렇게 할 때, 우리는 또한 다음을 수행합니다 : 우리가 올바르게 움직일 때마다, 우리는 또한 현재 값을 카운터에 추가합니다.

예를 들어, 3에 대한 합계를 찾으려고 가정하면 다음을 수행합니다.

  • 루트에서 시작하십시오 (4). 카운터는 0입니다.
  • 노드 (2)로 왼쪽으로 이동하십시오. 카운터는 0입니다.
  • 노드 (3)로 오른쪽으로 이동하십시오. 카운터는 0 + 6 = 6입니다.
  • 노드 (3)을 찾으십시오. 카운터는 6 + 15 = 21입니다.

주어진 노드에서 시작하여 카운터를 해당 노드의 값으로 초기화 한 다음 트리를 루트까지 걸어 올리십시오. 오른쪽 하위 링크를 위쪽으로 따라갈 때마다 도착한 노드의 값을 추가하십시오. 예를 들어 3의 빈도를 찾으려면 다음을 수행 할 수 있습니다.

  • 노드 (3)에서 시작하십시오. 카운터는 15입니다.
  • 노드 (2)로 이동하십시오. 카운터는 15 + 6 = 21입니다.
  • 노드 (4)로 이동하십시오. 카운터는 21입니다.

노드의 빈도 (그리고 그 뒤에 오는 모든 노드의 빈도)를 증가 시키려면 왼쪽 하위 트리에 해당 노드를 포함하는 트리의 노드 세트를 업데이트해야합니다. 이렇게하려면 다음을 수행하십시오. 해당 노드의 주파수를 증가시킨 다음 트리의 루트까지 올라가십시오. 왼쪽 자식으로 연결되는 링크를 따라갈 때마다 현재 값을 추가하여 발생하는 노드의 빈도를 늘리십시오.

예를 들어, 노드 1의 빈도를 5 씩 늘리려면 다음을 수행하십시오.

                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
      > 1     3     5     7
      [ +5] [+15] [+52] [ +0]

노드 1에서 시작하여 주파수를 5 씩 증가시켜

                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
      > 1     3     5     7
      [+10] [+15] [+52] [ +0]

이제 부모에게 가십시오.

                 4
               [+32]
              /     \
         > 2           6
         [ +6]       [+80]
         /   \       /   \
        1     3     5     7
      [+10] [+15] [+52] [ +0]

우리는 왼쪽 자식 링크를 위쪽으로 따라 갔으므로이 노드의 주파수도 증가시킵니다.

                 4
               [+32]
              /     \
         > 2           6
         [+11]       [+80]
         /   \       /   \
        1     3     5     7
      [+10] [+15] [+52] [ +0]

이제 부모에게갑니다 :

               > 4
               [+32]
              /     \
           2           6
         [+11]       [+80]
         /   \       /   \
        1     3     5     7
      [+10] [+15] [+52] [ +0]

이것은 왼쪽 자식 링크이므로이 노드도 증가시킵니다.

                 4
               [+37]
              /     \
           2           6
         [+11]       [+80]
         /   \       /   \
        1     3     5     7
      [+10] [+15] [+52] [ +0]

이제 끝났습니다!

마지막 단계는 이것을 이진 색인 트리로 변환하는 것이며, 이진 숫자로 재미있는 일을 할 수있는 곳입니다. 이 트리의 각 버킷 인덱스를 바이너리로 다시 작성해 보겠습니다.

                100
               [+37]
              /     \
          010         110
         [+11]       [+80]
         /   \       /   \
       001   011   101   111
      [+10] [+15] [+52] [ +0]

여기서 우리는 매우 멋진 관찰을 할 수 있습니다. 이 이진수 중 하나를 가져와 숫자에 설정된 마지막 1을 찾은 다음 그 뒤에 오는 모든 비트와 함께 해당 비트를 제거하십시오. 이제 다음과 같이 남아 있습니다.

              (empty)
               [+37]
              /     \
           0           1
         [+11]       [+80]
         /   \       /   \
        00   01     10   11
      [+10] [+15] [+52] [ +0]

다음은 정말 멋진 결과입니다. 0을 "왼쪽"으로, 1을 "오른쪽"으로 취급하면 각 숫자의 나머지 비트는 루트에서 시작하여 해당 숫자로 내려가는 방법을 정확하게 설명합니다. 예를 들어, 노드 5에는 이진 패턴 101이 있습니다. 마지막 1은 마지막 비트이므로 10을 얻기 위해 드롭합니다. 실제로 루트에서 시작하면 오른쪽 (1)으로 이동 한 다음 왼쪽 (0)으로 이동하면 끝납니다. 노드 5에서 위로!

이것이 중요한 이유는 조회 및 업데이트 작업이 노드에서 루트까지의 액세스 경로와 왼쪽 또는 오른쪽 하위 링크를 따르는 지 여부에 달려 있기 때문입니다. 예를 들어, 조회하는 동안 우리는 우리가 따르는 올바른 링크에 관심이 있습니다. 업데이트하는 동안 우리는 우리가 따르는 왼쪽 링크에 관심이 있습니다. 이 이진 색인 트리는 색인의 비트를 사용하여이 모든 것을 효율적으로 수행합니다.

핵심 트릭은이 완벽한 이진 트리의 다음 속성입니다.

노드 n이 주어지면 액세스 경로의 다음 노드는 루트로 돌아가서 n의 이진 표현을 취하고 마지막 1을 제거하여 제공됩니다.

예를 들어, 노드 7의 액세스 경로 인 111을 살펴보십시오. 루트에 대한 액세스 경로에서 오른쪽 포인터를 따라가는 노드는 다음과 같습니다.

  • 노드 7 : 111
  • 노드 6 : 110
  • 노드 4 : 100

이 모든 것이 올바른 링크입니다. 노드 3의 액세스 경로 인 011을 가져 와서 오른쪽으로 이동하는 노드를 보면

  • 노드 3 : 011
  • 노드 2 : 010
  • (노드 4 : 100, 왼쪽 링크를 따릅니다)

이는 다음과 같이 노드까지 누적 합계를 매우 효율적으로 계산할 수 있음을 의미합니다.

  • 노드 n을 2 진으로 작성하십시오.
  • 카운터를 0으로 설정하십시오.
  • n ≠ 0 인 동안 다음을 반복하십시오.
    • 노드 n의 값을 더하십시오.
    • n에서 가장 오른쪽 1 비트를 지 웁니다.

마찬가지로 업데이트 단계를 수행하는 방법에 대해 생각해 봅시다. 이를 위해 루트까지 액세스 경로를 따라 가면서 왼쪽 링크를 따라 올라간 모든 노드를 위쪽으로 업데이트하려고합니다. 기본적으로 위의 알고리즘을 수행하여 1을 0으로, 0을 1로 전환하여이 작업을 수행 할 수 있습니다.

이진 인덱스 트리의 마지막 단계는이 비트 단위의 속임수로 인해 더 이상 트리를 명시 적으로 저장하지 않아도된다는 것입니다. 모든 노드를 길이 n의 배열에 저장 한 다음 비트 별 꼬기 기법을 사용하여 트리를 암시 적으로 탐색 할 수 있습니다. 실제로 비트 단위 인덱스 트리는 정확히 노드를 배열에 저장 한 다음 비트 단위 트릭을 사용하여이 트리에서 위쪽으로 걷는 것을 효율적으로 시뮬레이션합니다.

도움이 되었기를 바랍니다!



당신은 두 번째 단락에서 나를 잃었습니다. 7 가지 요소의 누적 빈도는 무엇을 의미합니까?
Jason Goemaat

20
이것은 내가 인터넷에서 찾은 모든 소스 중 지금까지 내가 읽은 주제에 대한 최고의 설명입니다. 잘 했어 !
Anmol Singh Jaggi

2
펜윅은 어떻게 이것을 똑똑하게 얻었습니까?
Rockstar5645

1
이것은 매우 훌륭한 설명이지만 펜윅 자신의 논문뿐만 아니라 다른 모든 설명과 같은 문제를 겪고 있으며 증거를 제공하지 않습니다!
DarthPaghius

3

펜윅의 논문 이 훨씬 더 명확 하다고 생각합니다 . @templatetypedef의 위의 대답은 혼란스럽고 마술 같은 완벽한 이진 트리의 인덱싱에 대한 "매우 멋진 관찰"이 필요합니다.

펜윅은 심문 트리에있는 모든 노드의 책임 범위는 마지막 설정 비트에 따른다고 간단히 말했다.

펜윅 트리 노드 책임

예를 들어 6== 의 마지막 세트 비트 00110가 "2 비트"이므로 2 노드의 범위를 담당합니다. 들어 12== 01100는 4 개 노드의 범위에 대한 책임을 질 것입니다, 그래서 그것은 "4 비트"입니다.

따라서 F(12)== 를 쿼리 할 때 F(01100)비트를 하나씩 제거 하여을 얻습니다 F(9:12) + F(1:8). 이것은 거의 엄격한 증거는 아니지만 완벽한 이진 트리가 아닌 숫자 축에 간단히 배치하면 각 노드의 책임은 무엇이며 쿼리 비용은 다음과 같은 이유가 더 분명하다고 생각합니다. 비트를 설정합니다.

여전히 확실하지 않은 경우 용지를 사용하는 것이 좋습니다.

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