BIT: สัญชาตญาณที่อยู่เบื้องหลังต้นไม้ดัชนีแบบไบนารีคืออะไรและคิดอย่างไร?


99

ต้นไม้ดัชนีแบบไบนารีมีวรรณกรรมน้อยมากหรือค่อนข้างน้อยเมื่อเทียบกับโครงสร้างข้อมูลอื่น ๆ สถานที่เดียวซึ่งจะมีการเรียนการสอนเป็นTopCoder กวดวิชา แม้ว่าการสอนจะเสร็จสมบูรณ์ในคำอธิบายทั้งหมด แต่ฉันไม่สามารถเข้าใจสัญชาตญาณที่อยู่เบื้องหลังต้นไม้เช่นนั้นได้หรือไม่ มันถูกคิดค้นขึ้นมาได้อย่างไร? หลักฐานที่แท้จริงของความถูกต้องคืออะไร?


4
บทความเกี่ยวกับวิกิพีเดียบอกว่าเหล่านี้เรียกว่าต้นไม้เฟนวิค
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
ฉันรู้ว่าคุณรู้สึกอย่างไรครั้งแรกที่ฉันอ่านบทความ topcoder ดูเหมือนว่าจะเป็นเรื่องมหัศจรรย์
Rockstar5645

คำตอบ:


168

โดยสังเขปคุณสามารถนึกถึงต้นไม้ที่มีการทำดัชนีแบบไบนารีเป็นตัวแทนที่ถูกบีบอัดของต้นไม้แบบไบนารีที่เป็นการเพิ่มประสิทธิภาพของการเป็นตัวแทนอาร์เรย์มาตรฐานเอง คำตอบนี้จะเป็นไปได้หนึ่งที่มา

ตัวอย่างเช่นสมมติว่าคุณต้องการเก็บความถี่สะสมสำหรับองค์ประกอบต่าง ๆ ทั้งหมด 7 รายการ คุณสามารถเริ่มต้นด้วยการเขียนเจ็ดถังลงในหมายเลขที่จะกระจาย:

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

ทีนี้สมมติว่าความถี่สะสมมีลักษณะดังนี้:

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

การใช้อาเรย์รุ่นนี้คุณสามารถเพิ่มความถี่สะสมขององค์ประกอบใด ๆ โดยการเพิ่มมูลค่าของจำนวนที่เก็บไว้ ณ จุดนั้นจากนั้นเพิ่มความถี่ของทุกสิ่งที่เกิดขึ้นภายหลัง ตัวอย่างเช่นในการเพิ่มความถี่สะสมของ 3 ด้วย 7 เราสามารถเพิ่ม 7 ให้กับแต่ละองค์ประกอบในอาร์เรย์ที่หรือหลังตำแหน่ง 3 ดังที่แสดงที่นี่:

[ 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]

ด้วยโครงสร้างต้นไม้นี้มันง่ายที่จะหาผลรวมสะสมจนถึงจุด แนวคิดมีดังต่อไปนี้: เรารักษาตัวนับ 0 เริ่มแรกจากนั้นทำการค้นหาไบนารีตามปกติจนกระทั่งเราพบโหนดที่มีปัญหา ในขณะที่เราทำเช่นนั้นเราก็มีสิ่งต่อไปนี้เช่นกันทุกครั้งที่เราย้ายไปทางขวาเรายังเพิ่มมูลค่าปัจจุบันไปยังตัวนับ

ตัวอย่างเช่นสมมติว่าเราต้องการค้นหาผลรวมของ 3. เมื่อต้องการทำเช่นนั้นเราจะทำสิ่งต่อไปนี้:

  • เริ่มต้นที่รูท (4) ตัวนับคือ 0
  • ไปทางซ้ายเพื่อโหนด (2) ตัวนับคือ 0
  • ไปทางขวาเพื่อโหนด (3) ตัวนับคือ 0 + 6 = 6
  • ค้นหาโหนด (3) ตัวนับคือ 6 + 15 = 21

คุณอาจจินตนาการได้ว่ากำลังรันกระบวนการนี้ในสิ่งที่ตรงกันข้าม: เริ่มต้นที่โหนดที่กำหนดเริ่มต้นตัวนับให้เป็นค่าของโหนดนั้นจากนั้นเดินขึ้นต้นไม้ไปที่รูท เมื่อใดก็ตามที่คุณติดตามลิงก์ลูกที่เหมาะสมขึ้นให้เพิ่มมูลค่าที่โหนดที่คุณไปถึง ตัวอย่างเช่นเพื่อค้นหาความถี่สำหรับ 3 เราสามารถทำสิ่งต่อไปนี้:

  • เริ่มต้นที่โหนด (3) ตัวนับคือ 15
  • ขึ้นไปที่โหนด (2) ตัวนับคือ 15 + 6 = 21
  • ขึ้นไปที่โหนด (4) ตัวนับคือ 21

ในการเพิ่มความถี่ของโหนด (และโดยปริยายความถี่ของโหนดทั้งหมดที่มาหลังจากนั้น) เราจำเป็นต้องอัปเดตชุดของโหนดในต้นไม้ที่รวมโหนดนั้นไว้ในทรีย่อยทางซ้าย ในการทำเช่นนี้เราทำสิ่งต่อไปนี้: เพิ่มความถี่สำหรับโหนดนั้นจากนั้นเริ่มเดินขึ้นไปที่รากของต้นไม้ เมื่อใดก็ตามที่คุณติดตามลิงก์จะนำคุณไปสู่การเป็นลูกซ้ายเพิ่มความถี่ของโหนดที่คุณพบโดยการเพิ่มค่าปัจจุบัน

ตัวอย่างเช่นหากต้องการเพิ่มความถี่ของโหนด 1 ถึงห้าเราจะทำสิ่งต่อไปนี้:

                 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 ในไบนารี
  • ตั้งค่าตัวนับเป็น 0
  • ทำซ้ำต่อไปนี้ในขณะที่ n ≠ 0:
    • เพิ่มค่าที่โหนด n
    • ล้างขวาสุด 1 บิตจาก n

ลองคิดดูว่าเราจะทำขั้นตอนการอัพเดทอย่างไร ในการทำเช่นนี้เราจะต้องติดตามเส้นทางการเข้าถึงกลับไปที่รูทอัปเดตโหนดทั้งหมดที่เราติดตามลิงค์ด้านซ้ายขึ้น เราสามารถทำได้โดยการทำอัลกอริทึมด้านบน แต่การสลับ 1 ทั้งหมดเป็น 0 และ 0 เป็น 1

ขั้นตอนสุดท้ายในทรีดัชนีแบบไบนารีคือให้สังเกตว่าเนื่องจากกลอุบาย bitwise นี้เราไม่จำเป็นต้องมีต้นไม้ที่เก็บไว้อย่างชัดเจนอีกต่อไป เราสามารถเก็บโหนดทั้งหมดไว้ในอาร์เรย์ที่มีความยาว n จากนั้นใช้เทคนิคการบิดสองบิตเพื่อนำทางต้นไม้โดยปริยาย ในความเป็นจริงนั่นคือสิ่งที่ต้นไม้ทำดัชนี bitwise - มันเก็บโหนดในอาร์เรย์จากนั้นใช้เทคนิค bitwise เหล่านี้เพื่อจำลองการเดินขึ้นบนต้นไม้นี้ได้อย่างมีประสิทธิภาพ

หวังว่านี่จะช่วยได้!


ขอให้เรายังคงอภิปรายนี้ในการแชท
Hjulle

คุณสูญเสียฉันในวรรคสอง คุณหมายถึงความถี่สะสมของ 7 องค์ประกอบที่แตกต่างกันอย่างไร
Jason Goemaat

20
นี่คือคำอธิบายที่ดีที่สุดที่ฉันได้อ่านในหัวข้อที่ผ่านมาในบรรดาแหล่งข้อมูลทั้งหมดที่ฉันพบบนอินเทอร์เน็ต ทำได้ดี !
Anmol Singh Jaggi

2
เฟนวิคได้สิ่งนี้มาอย่างชาญฉลาดอย่างไร?
Rockstar5645

1
นี่เป็นคำอธิบายที่ดีมาก แต่ได้รับความทุกข์จากปัญหาเดียวกันกับคำอธิบายอื่น ๆ เช่นเดียวกับกระดาษของเฟนวิคไม่มีหลักฐานพิสูจน์!
DarthPaghius

3

ฉันคิดว่าเอกสารต้นฉบับของ Fenwick ชัดเจนกว่ามาก คำตอบข้างต้นโดย @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.