BIT: Trực giác đằng sau một cây được lập chỉ mục nhị phân là gì và nó được nghĩ như thế nào?


99

Một cây được lập chỉ mục nhị phân có rất ít hoặc tương đối không có tài liệu so với các cấu trúc dữ liệu khác. Nơi duy nhất mà nó được dạy là hướng dẫn topcoder . Mặc dù hướng dẫn đã hoàn thành trong tất cả các giải thích, tôi không thể hiểu được trực giác đằng sau một cái cây như vậy? Làm thế nào nó được phát minh? Bằng chứng thực tế về tính đúng đắn của nó là gì?


4
Một bài viết trên Wikipedia tuyên bố rằng chúng được gọi là cây Fenwick .
David Harkness 16/03/13

2
@ DavidHarkness- Peter Fenwick đã phát minh ra cấu trúc dữ liệu, vì vậy đôi khi chúng được gọi là cây Fenwick. Trong bài báo gốc của mình (được tìm thấy tại citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.14.891717 ), ông gọi chúng là cây được lập chỉ mục nhị phân. Hai thuật ngữ thường được sử dụng thay thế cho nhau.
templatetypedef

1
Câu trả lời sau đây truyền tải một trực giác "trực quan" rất đẹp của các cây được lập chỉ mục nhị phân cs.stackexchange.com/questions/42811/ .
Rabih Kodeih

1
Tôi biết cảm giác của bạn, lần đầu tiên tôi đọc bài viết topcoder, nó trông giống như ma thuật.
Rockstar5645

Câu trả lời:


168

Theo trực giác, bạn có thể nghĩ về một cây được lập chỉ mục nhị phân như là một biểu diễn nén của cây nhị phân mà chính nó là một sự tối ưu hóa của một biểu diễn mảng tiêu chuẩn. Câu trả lời này đi vào một dẫn xuất có thể.

Ví dụ, giả sử bạn muốn lưu trữ tần số tích lũy cho tổng cộng 7 yếu tố khác nhau. Bạn có thể bắt đầu bằng cách viết ra bảy nhóm trong đó các số sẽ được phân phối:

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

Bây giờ, giả sử rằng các tần số tích lũy trông giống như thế này:

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

Sử dụng phiên bản này của mảng, bạn có thể tăng tần số tích lũy của bất kỳ phần tử nào bằng cách tăng giá trị của số được lưu trữ tại điểm đó, sau đó tăng tần số của mọi thứ sau đó. Ví dụ: để tăng tần số tích lũy lên 3, chúng ta có thể thêm 7 vào mỗi phần tử trong mảng tại hoặc sau vị trí 3, như được hiển thị ở đây:

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

Vấn đề với điều này là phải mất O (n) thời gian để làm điều này, điều này khá chậm nếu n lớn.

Một cách mà chúng ta có thể nghĩ về việc cải thiện hoạt động này sẽ là thay đổi những gì chúng ta lưu trữ trong các thùng. Thay vì lưu trữ tần số tích lũy đến điểm đã cho, thay vào đó bạn có thể nghĩ đến việc chỉ lưu trữ số lượng mà tần số hiện tại đã tăng lên so với nhóm trước đó. Ví dụ, trong trường hợp của chúng tôi, chúng tôi sẽ viết lại các nhóm trên như sau:

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

Bây giờ, chúng ta có thể tăng tần số trong một nhóm trong thời gian O (1) bằng cách thêm số lượng thích hợp vào nhóm đó. Tuy nhiên, tổng chi phí thực hiện tra cứu giờ trở thành O (n), vì chúng ta phải tính toán lại tổng số trong nhóm bằng cách tổng hợp các giá trị trong tất cả các nhóm nhỏ hơn.

Cái nhìn sâu sắc lớn đầu tiên chúng ta cần có từ đây đến cây được lập chỉ mục nhị phân là như sau: thay vì liên tục tính lại tổng của các phần tử mảng trước một phần tử cụ thể, nếu chúng ta tính trước tổng của tất cả các phần tử trước khi cụ thể điểm trong dãy? Nếu chúng ta có thể làm điều đó, thì chúng ta có thể tìm ra tổng tích lũy tại một điểm bằng cách chỉ cần tổng hợp đúng các khoản tiền được tính toán trước này.

Một cách để làm điều này là thay đổi biểu diễn từ một mảng các nhóm thành một cây nhị phân của các nút. Mỗi nút sẽ được chú thích với một giá trị đại diện cho tổng tích lũy của tất cả các nút ở bên trái của nút đã cho. Ví dụ: giả sử chúng ta xây dựng cây nhị phân sau từ các nút này:

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

Bây giờ, chúng ta có thể tăng từng nút bằng cách lưu trữ tổng tích lũy của tất cả các giá trị bao gồm nút đó và cây con bên trái của nó. Ví dụ: với các giá trị của chúng tôi, chúng tôi sẽ lưu trữ như sau:

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]

Với cấu trúc cây này, thật dễ dàng để xác định tổng tích lũy đến một điểm. Ý tưởng là như sau: chúng tôi duy trì một bộ đếm, ban đầu là 0, sau đó thực hiện tìm kiếm nhị phân bình thường cho đến khi chúng tôi tìm thấy nút được đề cập. Khi chúng tôi làm như vậy, chúng tôi cũng như sau: bất cứ khi nào chúng tôi di chuyển đúng, chúng tôi cũng thêm giá trị hiện tại vào bộ đếm.

Ví dụ: giả sử chúng tôi muốn tra cứu tổng cho 3. Để làm như vậy, chúng tôi thực hiện như sau:

  • Bắt đầu từ gốc (4). Số lượt truy cập là 0.
  • Đi bên trái đến nút (2). Số lượt truy cập là 0.
  • Đi thẳng đến nút (3). Bộ đếm là 0 + 6 = 6.
  • Tìm nút (3). Số lượt truy cập là 6 + 15 = 21.

Bạn có thể tưởng tượng cũng đang chạy quá trình này theo chiều ngược lại: bắt đầu từ một nút nhất định, khởi tạo bộ đếm với giá trị của nút đó, sau đó đi lên cây đến gốc. Bất cứ khi nào bạn theo một liên kết con phải lên trên, hãy thêm giá trị tại nút bạn đến. Ví dụ: để tìm tần số cho 3, chúng ta có thể làm như sau:

  • Bắt đầu tại nút (3). Số lượt truy cập là 15.
  • Đi lên đến nút (2). Số lượt truy cập là 15 + 6 = 21.
  • Đi lên đến nút (4). Số lượt truy cập là 21.

Để tăng tần số của một nút (và, mặc nhiên, tần số của tất cả các nút đi sau nó), chúng ta cần cập nhật tập hợp các nút trong cây bao gồm nút đó trong cây con bên trái của nó. Để làm điều này, chúng tôi làm như sau: tăng tần số cho nút đó, sau đó bắt đầu đi lên đến gốc của cây. Bất cứ khi nào bạn theo một liên kết đưa bạn lên như một đứa trẻ bên trái, hãy tăng tần số của nút bạn gặp bằng cách thêm vào giá trị hiện tại.

Ví dụ: để tăng tần số của nút 1 lên năm, chúng tôi sẽ làm như sau:

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

Bắt đầu từ nút 1, tăng tần số của nó lên 5 để có được

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

Bây giờ, đi đến cha mẹ của nó:

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

Chúng tôi đã theo một liên kết con bên trái lên trên, vì vậy chúng tôi cũng tăng tần số của nút này:

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

Bây giờ chúng ta đi đến cha mẹ của nó:

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

Đó là một liên kết con bên trái, vì vậy chúng tôi cũng tăng nút này:

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

Và bây giờ chúng ta đã hoàn thành!

Bước cuối cùng là chuyển đổi từ cây này sang cây được lập chỉ mục nhị phân và đây là nơi chúng ta có thể làm một số điều thú vị với số nhị phân. Hãy viết lại từng chỉ số xô trong cây này dưới dạng nhị phân:

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

Ở đây, chúng ta có thể thực hiện một quan sát rất, rất mát mẻ. Lấy bất kỳ số nhị phân nào trong số này và tìm số 1 cuối cùng được đặt trong số đó, sau đó bỏ bit đó đi, cùng với tất cả các bit xuất hiện sau nó. Bây giờ bạn còn lại với những điều sau đây:

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

Đây là một quan sát thực sự, thực sự hay: nếu bạn coi 0 có nghĩa là "trái" và 1 có nghĩa là "phải", các bit còn lại trên mỗi số đánh vần chính xác cách bắt đầu từ gốc và sau đó đi xuống số đó. Ví dụ, nút 5 có mẫu nhị phân 101. 1 cuối cùng là bit cuối cùng, vì vậy chúng tôi bỏ nó xuống để lấy 10. Thật vậy, nếu bạn bắt đầu ở gốc, hãy sang phải (1), sau đó rẽ trái (0), bạn kết thúc tại nút 5!

Lý do điều này rất có ý nghĩa là các hoạt động tra cứu và cập nhật của chúng tôi phụ thuộc vào đường dẫn truy cập từ nút sao lưu đến gốc và liệu chúng tôi có theo dõi các liên kết con trái hay phải. Ví dụ, trong quá trình tra cứu, chúng tôi chỉ quan tâm đến các liên kết phù hợp mà chúng tôi theo dõi. Trong quá trình cập nhật, chúng tôi chỉ quan tâm đến các liên kết bên trái mà chúng tôi theo dõi. Cây được lập chỉ mục nhị phân này thực hiện tất cả các siêu hiệu quả này chỉ bằng cách sử dụng các bit trong chỉ mục.

Thủ thuật chính là thuộc tính sau của cây nhị phân hoàn hảo này:

Cho nút n, nút tiếp theo trên đường dẫn truy cập trở lại gốc mà chúng ta đi đúng được đưa ra bằng cách lấy biểu diễn nhị phân của n và loại bỏ 1 cuối cùng.

Ví dụ, hãy xem đường dẫn truy cập cho nút 7, là 111. Các nút trên đường dẫn truy cập đến thư mục gốc mà chúng ta có liên quan đến việc đi theo một con trỏ phải lên trên là

  • Nút 7: 111
  • Nút 6: 110
  • Nút 4: 100

Tất cả những điều này là liên kết đúng. Nếu chúng ta chọn đường dẫn truy cập cho nút 3, là 011 và nhìn vào các nút nơi chúng ta đi đúng, chúng ta sẽ nhận được

  • Nút 3: 011
  • Nút 2: 010
  • (Nút 4: 100, theo liên kết bên trái)

Điều này có nghĩa là chúng ta có thể tính toán rất, rất hiệu quả tổng tích lũy cho một nút như sau:

  • Viết ra nút n trong nhị phân.
  • Đặt bộ đếm thành 0.
  • Lặp lại như sau trong khi n ≠ 0:
    • Thêm vào giá trị tại nút n.
    • Xóa 1 bit ngoài cùng bên phải từ n.

Tương tự, hãy nghĩ về cách chúng tôi sẽ thực hiện một bước cập nhật. Để làm điều này, chúng tôi muốn theo đường dẫn truy cập trở lại thư mục gốc, cập nhật tất cả các nút nơi chúng tôi theo một liên kết bên trái lên trên. Chúng ta có thể làm điều này bằng cách thực hiện thuật toán trên, nhưng chuyển đổi tất cả 1 thành 0 và 0 thành 1.

Bước cuối cùng trong cây được lập chỉ mục nhị phân là lưu ý rằng vì thủ thuật bitwise này, chúng ta thậm chí không cần phải lưu trữ cây một cách rõ ràng nữa. Chúng ta chỉ có thể lưu trữ tất cả các nút trong một mảng có độ dài n, sau đó sử dụng các kỹ thuật xoay vòng theo bit để điều hướng cây ngầm. Trên thực tế, đó chính xác là những gì mà cây được lập chỉ mục bitwise làm - nó lưu trữ các nút trong một mảng, sau đó sử dụng các thủ thuật bitwise này để mô phỏng hiệu quả việc đi lên trên cây này.

Hi vọng điêu nay co ich!



Bạn mất tôi trong đoạn thứ hai. Bạn có ý nghĩa gì về tần số tích lũy của 7 yếu tố khác nhau?
Jason Goemaat

20
Đây là lời giải thích tốt nhất mà tôi đã đọc về chủ đề cho đến nay, trong số tất cả các nguồn tôi tìm thấy trên Internet. Làm tốt !
Anmol Singh Jaggi

2
Làm thế nào mà Fenwick có được thông minh này?
Rockstar5645

1
Đây là một lời giải thích rất hay nhưng gặp phải vấn đề tương tự như mọi lời giải thích khác cũng như bài viết của chính Fenwick, không cung cấp bằng chứng!
DarthPaghius

3

Tôi nghĩ rằng bài báo gốc của Fenwick rõ ràng hơn nhiều. Câu trả lời ở trên của @templatetypedef yêu cầu một số "quan sát rất hay" về việc lập chỉ mục của một cây nhị phân hoàn hảo, gây nhầm lẫn và kỳ diệu đối với tôi.

Fenwick nói đơn giản rằng phạm vi trách nhiệm của mọi nút trong cây thẩm vấn sẽ theo bit được đặt cuối cùng của nó:

Trách nhiệm nút cây Fenwick

Ví dụ, bit thiết lập cuối cùng của 6== 00110là "2 bit", nó sẽ chịu trách nhiệm cho phạm vi 2 nút. Với 12== 01100, nó là "4 bit", do đó, nó sẽ chịu trách nhiệm cho một phạm vi gồm 4 nút.

Vì vậy, khi truy vấn F(12)== F(01100), chúng tôi tách từng bit một, nhận được F(9:12) + F(1:8). Đây không phải là một bằng chứng nghiêm ngặt, nhưng tôi nghĩ điều đó rõ ràng hơn khi đặt đơn giản vào trục số và không phải trên cây nhị phân hoàn hảo, trách nhiệm của mỗi nút là gì và tại sao chi phí truy vấn bằng với số đặt bit.

Nếu điều này vẫn chưa rõ ràng thì giấy rất được khuyến khích.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.