อัลกอริทึมทรีต่อท้ายของ Ukkonen เป็นภาษาอังกฤษธรรมดา


1101

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

คำอธิบายทีละขั้นตอนของอัลกอริทึมที่นี่ใน Stack Overflow จะมีค่าสำหรับคนอื่น ๆ นอกเหนือจากฉันฉันแน่ใจ

สำหรับการอ้างอิงนี่เป็นบทความของ Ukkonen เกี่ยวกับอัลกอริทึม: http://www.cs.helsinki.fi/u/ukkonen/SuffixT1withFigs.pdf

ความเข้าใจพื้นฐานของฉันจนถึงตอนนี้:

  • ฉันต้องวนซ้ำคำนำหน้า P แต่ละตัวของสตริง T ที่กำหนด
  • ฉันต้องวนซ้ำคำต่อท้ายแต่ละ S ในคำนำหน้า P และเพิ่มเข้าไปในต้นไม้
  • ในการเพิ่มคำต่อท้าย S ลงบนต้นไม้ฉันจำเป็นต้องวนซ้ำอักขระแต่ละตัวใน S ด้วยการวนซ้ำซึ่งประกอบด้วยการเดินลงสาขาที่มีอยู่ซึ่งเริ่มต้นด้วยชุดอักขระ C เดียวกันใน S และอาจแบ่งขอบเป็นโหนดสืบทอดเมื่อฉัน ถึงตัวละครที่แตกต่างกันในคำต่อท้ายหรือถ้าไม่มีขอบที่ตรงกันที่จะเดินลง เมื่อไม่พบการจับคู่ขอบเพื่อเดินลงสำหรับ C ขอบใบใหม่จะถูกสร้างขึ้นสำหรับ C

อัลกอริธึมพื้นฐานดูเหมือนจะเป็น O (n 2 ) ดังที่อธิบายไว้ในคำอธิบายส่วนใหญ่เนื่องจากเราจำเป็นต้องก้าวผ่านคำนำหน้าทั้งหมดจากนั้นเราต้องก้าวผ่านคำต่อท้ายสำหรับคำนำหน้าแต่ละคำ อัลกอริธึมของ Ukkonen นั้นมีความพิเศษเพราะเทคนิคตัวชี้ส่วนต่อท้ายที่เขาใช้แม้ว่าฉันคิดว่านั่นเป็นสิ่งที่ฉันมีปัญหาในการทำความเข้าใจ

ฉันยังมีปัญหาในการทำความเข้าใจ:

  • เมื่อใดและอย่างไรที่ "กำหนดจุดใช้งาน" ถูกกำหนดใช้และเปลี่ยนแปลง
  • สิ่งที่เกิดขึ้นกับด้าน canonization ของอัลกอริทึม
  • ทำไมการปรับใช้ที่ฉันเห็นจำเป็นต้อง "แก้ไข" ตัวแปรขอบเขตที่ใช้

นี่คือซอร์สโค้ดC #ที่สมบูรณ์ มันไม่เพียงทำงานได้อย่างถูกต้อง แต่รองรับ canonization อัตโนมัติและแสดงกราฟข้อความที่ดูดีกว่าของเอาต์พุต ซอร์สโค้ดและเอาต์พุตตัวอย่างอยู่ที่:

https://gist.github.com/2373868


อัปเดต 2017-11-04

หลังจากหลายปีที่ฉันได้พบการใช้งานใหม่สำหรับต้นไม้ต่อท้ายและได้ดำเนินการขั้นตอนวิธีการในJavaScript สรุปสาระสำคัญอยู่ด้านล่าง ควรปราศจากข้อบกพร่อง ดัมพ์เป็นไฟล์ js npm install chalkจากตำแหน่งเดียวกันจากนั้นรันด้วย node.js เพื่อดูเอาต์พุตที่มีสีสัน มีเวอร์ชันที่แยกส่วนใน Gist เดียวกันโดยไม่มีรหัสการดีบักใด ๆ

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


2
คุณดูคำอธิบายที่ให้ไว้ในหนังสือของ Dan Gusfieldหรือไม่? ฉันพบว่าเป็นประโยชน์
jogojapan

4
สรุปสาระสำคัญไม่ได้ระบุใบอนุญาต - ฉันสามารถเปลี่ยนรหัสของคุณและเผยแพร่ภายใต้ MIT (เห็นได้ชัดว่ามีการอ้างเหตุผล)
Yurik

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

1
บางทีการใช้งานนี้จะช่วยผู้อื่นให้ไปที่code.google.com/p/text-indexing
cos

2
"คิดว่ามันเป็นสาธารณสมบัติ" บางทีอาจเป็นคำตอบที่ไร้ประโยชน์อย่างน่าประหลาดใจ เหตุผลก็คือเป็นไปไม่ได้ที่คุณจะวางงานในโดเมนสาธารณะ เพราะฉะนั้น "คิดว่ามัน ..." ของคุณความคิดเห็นที่ขีดเส้นใต้ความจริงที่ว่าใบอนุญาตมีความชัดเจนและให้เหตุผลที่ผู้อ่านที่จะมีข้อสงสัยว่าสถานะของการทำงานที่เป็นจริงที่ชัดเจนให้กับคุณ หากคุณต้องการให้ผู้คนสามารถใช้รหัสของคุณได้โปรดระบุใบอนุญาตสำหรับมันเลือกใบอนุญาตใด ๆ ที่คุณต้องการ (แต่ถ้าคุณไม่ใช่ทนายความให้เลือกใบอนุญาตที่มีอยู่ก่อน!)
James Youngman

คำตอบ:


2377

ต่อไปนี้เป็นความพยายามที่จะอธิบายอัลกอริธึม Ukkonen โดยการแสดงสิ่งที่ทำก่อนเมื่อสตริงนั้นง่าย (เช่นไม่มีตัวอักษรซ้ำ ๆ ) แล้วขยายไปยังอัลกอริธึมเต็ม

ก่อนแถลงการณ์เบื้องต้นบางประการ

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

  2. แต่ : แตกต่างจากใน Trie การค้นหาเลเบลขอบไม่ใช่อักขระเดี่ยว [from,to]แต่ขอบแต่ละจะมีป้ายโดยใช้คู่ของจำนวนเต็ม นี่คือพอยน์เตอร์ในข้อความ ในแง่นี้แต่ละขอบมีฉลากสตริงที่มีความยาวตามอำเภอใจ แต่ใช้พื้นที่ O (1) (สองพอยน์เตอร์) เท่านั้น

หลักการพื้นฐาน

ฉันต้องการแสดงให้เห็นถึงวิธีการสร้างทรีต่อท้ายของสตริงที่ง่ายโดยเฉพาะอย่างยิ่งสตริงที่ไม่มีอักขระซ้ำ:

abc

ขั้นตอนวิธีการทำงานในขั้นตอนจากซ้ายไปขวา มีขั้นตอนหนึ่งสำหรับตัวอักษรของสตริงทุก แต่ละขั้นตอนอาจเกี่ยวข้องกับการดำเนินการมากกว่าหนึ่งอย่าง แต่เราจะเห็น (ดูการสังเกตสุดท้ายในตอนท้าย) ว่าจำนวนการดำเนินการทั้งหมดคือ O (n)

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

ดังนั้นเราจึงมีต้นไม้เริ่มต้นซึ่งมีลักษณะดังนี้:

และมันหมายถึงอะไร:

ตอนนี้เรากำลังไปสู่ตำแหน่งที่ 2 (หลังจากนั้นb) เป้าหมายของเราในแต่ละขั้นตอน คือการแทรกคำต่อท้ายทั้งหมดขึ้นอยู่กับตำแหน่งปัจจุบัน เราทำสิ่งนี้โดย

  • ขยายขอบเขตที่มีอยู่aไปเป็นab
  • แทรกหนึ่งขอบใหม่สำหรับ b

ในการเป็นตัวแทนของเราดูเหมือนว่า

ป้อนคำอธิบายรูปภาพที่นี่

และมันหมายถึงอะไร:

เราสังเกตสองสิ่ง:

  • การแทนค่าขอบabนั้นเหมือนกับที่เคยมีในแผนผังเริ่มต้น: [0,#]. ความหมายเปลี่ยนไปโดยอัตโนมัติเนื่องจากเราอัปเดตตำแหน่งปัจจุบัน#จาก 1 เป็น 2
  • ขอบแต่ละอันใช้พื้นที่ O (1) เนื่องจากมันประกอบด้วยพอยน์เตอร์เพียงสองตัวเท่านั้นลงในข้อความโดยไม่คำนึงถึงจำนวนตัวอักษรที่แสดง

ต่อไปเราจะเพิ่มตำแหน่งอีกครั้งและอัปเดตต้นไม้โดยผนวกทุกขอบที่มีอยู่และการใส่ขอบใหม่สำหรับปัจจัยใหม่cc

ในการเป็นตัวแทนของเราดูเหมือนว่า

และมันหมายถึงอะไร:

เราสังเกต:

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

ส่วนขยายแรก: ทำซ้ำง่าย ๆ

แน่นอนว่ามันใช้งานได้ดีเพราะสตริงของเราไม่มีการทำซ้ำ ตอนนี้เราดูสตริงที่เหมือนจริงมากขึ้น:

abcabxabcd

มันเริ่มต้นด้วยabcเช่นในตัวอย่างก่อนหน้านี้แล้วabซ้ำแล้วซ้ำอีกและตามมาด้วยxและจากนั้นจะถูกทำซ้ำตามมาด้วยabcd

ขั้นตอนที่ 1 ถึง 3:หลังจาก 3 ขั้นตอนแรกเรามีต้นไม้จากตัวอย่างก่อนหน้านี้:

ขั้นตอนที่ 4:เราย้าย#ไปที่ตำแหน่งที่ 4 ซึ่งจะเป็นการอัปเดตขอบที่มีอยู่ทั้งหมดเป็นแบบนี้:

และเราจำเป็นต้องแทรกคำต่อท้ายสุดท้ายของขั้นตอนปัจจุบันaที่ราก

ก่อนที่เราจะทำสิ่งนี้เราแนะนำตัวแปรอีกสองตัว (นอกเหนือจาก #) ซึ่งแน่นอนอยู่ที่นั่นตลอดเวลา แต่เรายังไม่ได้ใช้มัน:

  • จุดที่ใช้งานซึ่งเป็นสาม (active_node,active_edge,active_length)
  • The remainderซึ่งเป็นจำนวนเต็มที่ระบุจำนวนคำต่อท้ายใหม่ที่เราต้องการแทรก

ความหมายที่แท้จริงของทั้งสองนี้จะชัดเจนในไม่ช้า แต่สำหรับตอนนี้ขอพูดว่า:

  • ในabcตัวอย่างง่าย ๆจุดที่ใช้งานอยู่เสมอ (root,'\0x',0)คือactive_nodeโหนดรูตactive_edgeถูกระบุเป็นอักขระ null '\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ต่อท้ายจากขั้นตอนก่อนหน้านี้ไม่เคยมีการใส่อย่างถูกต้อง ดังนั้นจึงยังคงอยู่และเนื่องจากเรามีความก้าวหน้าในขั้นตอนเดียวจะได้เติบโตขึ้นในขณะนี้จากการaab
  • 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 , remainder4 และเพื่อให้เราต้องแทรก abcd(ซึ่งยังคงอยู่จาก 3 ขั้นตอนที่แล้ว) โดยการใส่dที่จุดที่ใช้งานอยู่

การพยายามแทรกdที่จุดที่ใช้งานอยู่จะทำให้เกิดการแตกขอบในเวลา O (1):

การactive_nodeเริ่มต้นการแยกนั้นทำเครื่องหมายด้วยสีแดงด้านบน นี่คือกฎสุดท้ายกฎข้อ 3:

หลังจากแยกขอบจากactive_nodeที่ไม่ใช่โหนดรูทแล้วเราจะตามลิงก์ต่อท้ายที่ออกจากโหนดนั้นหากมีและตั้งค่าactive_nodeเป็นโหนดที่ชี้ไป หากไม่มีลิงก์คำต่อท้ายเราจะตั้งค่าactive_nodeเป็นรูท active_edge และactive_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)

  • แต่จะไม่จัดการกับ) ส่วนต่อท้ายที่เหลือจากขั้นตอนก่อนหน้าและ 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 (หรือ n + 1) ดังนั้นความซับซ้อนทั้งหมดคือ O (n)

  • แต่มีสิ่งหนึ่งที่เล็ก ๆ ที่ผมไม่ต้องอธิบาย: มันสามารถเกิดขึ้นว่าเราทำตามลิงค์ต่อท้ายปรับปรุงจุดที่ใช้งานแล้วพบว่าองค์ประกอบที่ไม่ทำงานได้ดีกับใหม่active_length active_nodeตัวอย่างเช่นพิจารณาสถานการณ์เช่นนี้:

(เส้นประบ่งบอกถึงส่วนที่เหลือของต้นไม้เส้นประเป็นลิงค์ต่อท้าย)

ตอนนี้ให้จุดที่ใช้งานอยู่เป็น(red,'d',3)เพื่อให้มันชี้ไปยังสถานที่ที่อยู่เบื้องหลังfในdefgขอบ ตอนนี้ถือว่าเราทำปรับปรุงที่จำเป็นและตอนนี้ตามลิงค์ต่อท้ายเพื่อปรับปรุงจุดที่ใช้งานให้เป็นไปตามกฎ 3. (green,'d',3)จุดที่ใช้งานใหม่คือ อย่างไรก็ตามd-edge ที่ออกจากโหนดสีเขียวdeจึงมีเพียง 2 ตัวอักษร (blue,'f',1)เพื่อที่จะหาจุดที่ใช้งานที่ถูกต้องที่เราเห็นได้ชัดว่าต้องทำตามขอบที่โหนดสีฟ้าและรีเซ็ต

ในกรณีที่ไม่ดีโดยเฉพาะอย่างยิ่งที่active_lengthจะมีขนาดใหญ่เท่า remainderซึ่งสามารถเป็นใหญ่เป็น n และอาจเป็นไปได้ว่าการหาจุดแอคทีฟที่ถูกต้องนั้นไม่เพียง แต่ต้องข้ามโหนดภายในหนึ่งโหนดเท่านั้น แต่อาจจะมากถึง n ในกรณีที่แย่ที่สุด นั่นหมายความว่าอัลกอริทึมนั้นมีความซับซ้อนO (n 2 ) ที่ซ่อนอยู่เพราะในแต่ละขั้นตอนremainderมักจะเป็น O (n) และการปรับแต่งภายหลังให้กับโหนดที่แอ็คทีฟหลังจากติดตามลิงก์ต่อท้ายอาจเป็น O (n) เช่นกัน

ไม่เหตุผลคือถ้าหากเราต้องปรับจุดที่ใช้งาน (เช่นจากสีเขียวเป็นสีน้ำเงินดังกล่าวข้างต้น) นั่นจะนำเราไปสู่โหนดใหม่ที่มีลิงค์ต่อท้ายของตัวเองและ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 จะเตะใน.
jogojapan

6
ตกลงรหัสของฉันได้รับการเขียนใหม่อย่างสมบูรณ์และตอนนี้ทำงานได้อย่างถูกต้องสำหรับทุกกรณีรวมถึง canonization อัตโนมัติและมีเอาต์พุตกราฟข้อความที่ดีกว่า gist.github.com/2373868
Nathan Ridley

132

ฉันพยายามใช้ Suffix Tree ด้วยวิธีการที่ให้ไว้ในคำตอบของ jogojapan แต่มันไม่ได้ผลในบางกรณีเนื่องจากถ้อยคำที่ใช้สำหรับกฎ นอกจากนี้ผมได้กล่าวไม่มีใครที่มีการจัดการที่จะใช้ต้นไม้ต่อท้ายอย่างถูกต้องโดยใช้วิธีการนี้ ด้านล่างฉันจะเขียน "ภาพรวม" ของคำตอบของ jogojapan พร้อมการแก้ไขกฎบางอย่าง ฉันจะอธิบายกรณีเมื่อเราลืมที่จะสร้างลิงค์ต่อท้ายที่สำคัญ

ใช้ตัวแปรเพิ่มเติม

  1. จุดที่ใช้งาน - สาม (active_node; active_edge; active_length) แสดงจากที่เราจะต้องเริ่มแทรกคำต่อท้ายใหม่
  2. ที่เหลือ - การแสดงจำนวนคำต่อท้ายที่เราต้องเพิ่มอย่างชัดเจน ตัวอย่างเช่นถ้าคำพูดของเราคือ 'abcaabca' และส่วนที่เหลือ = 3 ก็หมายความว่าเราจะต้องดำเนินการต่อท้าย 3 ล่าสุด: BCA , CAและ

ลองใช้แนวคิดของโหนดภายใน - โหนดทั้งหมดยกเว้นรากและใบมีต่อมน้ำภายใน

สังเกต 1

เมื่อปัจจัยสุดท้ายที่เราต้องแทรกพบว่ามีอยู่ในต้นไม้แล้วต้นไม้ที่ตัวเองไม่ได้เปลี่ยนเลย (เราจะปรับปรุงactive pointและremainder)

การสังเกต 2

ถ้าในบางจุดactive_lengthมากกว่าหรือเท่ากับความยาวของขอบปัจจุบัน (คนedge_length) เราย้ายของเราactive pointลงไปจนเป็นอย่างเคร่งครัดมากกว่าedge_lengthactive_length

ทีนี้มาทบทวนกฎกันใหม่:

กฎข้อที่ 1

ถ้าหลังจากแทรกจากโหนดที่ใช้งาน = รากที่ยาวที่ใช้งานมากกว่า 0 แล้ว:

  1. โหนดที่ใช้งานจะไม่มีการเปลี่ยนแปลง
  2. ความยาวที่ใช้งานจะลดลง
  3. ขอบที่ใช้งานถูกเลื่อนไปทางขวา (ไปที่อักขระตัวแรกของคำต่อไปที่เราต้องแทรก)

กฎข้อที่ 2

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

คำจำกัดความของคำRule 2นี้แตกต่างจาก jogojapan 'เนื่องจากที่นี่เราคำนึงถึงไม่เพียง แต่โหนดภายในที่สร้างขึ้นใหม่แต่ยังรวมถึงโหนดภายในซึ่งเราทำการแทรก

กฎข้อ 3

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

ในคำจำกัดความนี้Rule 3เรายังพิจารณาการแทรกของโหนดปม

และในที่สุดการสังเกต 3:

เมื่อสัญลักษณ์ที่เราต้องการที่จะเพิ่มให้กับต้นไม้ที่มีอยู่แล้วบนขอบที่เราตามที่Observation 1อัปเดตเท่านั้นactive pointและremainderออกจากต้นไม้เดิม แต่ถ้ามีโหนดภายในทำเครื่องหมายเป็นต้องเชื่อมโยงต่อท้ายเราจะต้องเชื่อมต่อโหนดที่มีในปัจจุบันของเราactive nodeผ่านการเชื่อมโยงต่อท้าย

ดู Let 's ตัวอย่างของต้นไม้ต่อท้ายสำหรับcdddcdcถ้าเราเพิ่มการเชื่อมโยงต่อท้ายในกรณีดังกล่าวและถ้าเราทำไม่ได้:

  1. หากเราไม่เชื่อมต่อโหนดผ่านลิงก์ต่อท้าย:

    • ก่อนที่จะเพิ่มตัวอักษรตัวสุดท้าย :

    • หลังจากเพิ่มตัวอักษรตัวสุดท้ายc :

  2. ถ้าเราทำเชื่อมต่อโหนดผ่านการเชื่อมโยงต่อท้าย:

    • ก่อนที่จะเพิ่มตัวอักษรตัวสุดท้าย :

    • หลังจากเพิ่มตัวอักษรตัวสุดท้ายc :

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

เมื่อเราได้เพิ่มตัวอักษรตัวสุดท้ายกับต้นไม้โหนดสีแดงได้อยู่แล้วก่อนที่เราจะทำแทรกจากโหนดสีฟ้า (ขอบ labled 'C' ) ขณะที่มีการแทรกจากโหนดสีฟ้าที่เราทำเครื่องหมายว่าจำเป็นต้องเชื่อมโยงต่อท้าย จากนั้นขึ้นอยู่กับวิธีการใช้งานจุดที่active nodeถูกตั้งค่าให้โหนดสีแดง แต่เราไม่ได้ทำการแทรกจากโหนดสีแดงเนื่องจากตัวอักษร'c'อยู่บนขอบแล้ว หมายความว่าต้องทิ้งโหนดสีน้ำเงินโดยไม่มีลิงก์ต่อท้ายหรือไม่ ไม่เราต้องเชื่อมต่อโหนดสีฟ้ากับสีแดงผ่านลิงค์ต่อท้าย ทำไมมันถูกต้อง? เพราะจุดที่ใช้งานวิธีการค้ำประกันที่เราได้รับไปยังสถานที่ที่เหมาะสมเช่นการไปยังสถานที่ต่อไปที่เราจะต้องดำเนินการแทรกของสั้นต่อท้าย

ในที่สุดนี่คือการใช้งานของ Suffix Tree ของฉัน:

  1. ชวา
  2. C ++

หวังว่า "ภาพรวม" นี้รวมกับคำตอบโดยละเอียดของ jogojapan จะช่วยให้ใครบางคนสามารถใช้ Suffix Tree ของตัวเองได้


3
ขอบคุณมากและ +1 สำหรับคุณความพยายาม ฉันแน่ใจว่าคุณพูดถูก .. ถึงแม้ว่าฉันจะไม่มีเวลาคิดถึงรายละเอียดเลย ฉันจะตรวจสอบในภายหลังและอาจแก้ไขคำตอบของฉันเช่นกัน
jogojapan

ขอบคุณมากมันช่วยได้จริงๆ แม้ว่าคุณจะเจาะจงมากขึ้นใน Observation 3 หรือไม่? ตัวอย่างเช่นการให้ไดอะแกรมของ 2 ขั้นตอนที่แนะนำลิงค์ต่อท้ายใหม่ โหนดเชื่อมโยงโหนดที่ใช้งานอยู่หรือไม่ (เนื่องจากเราไม่ได้แทรกโหนดที่ 2)
dyesdyes

@ Makagonov เฮ้คุณช่วยฉันสร้าง tree ต่อท้ายสำหรับสตริงของคุณได้ไหม "cdddcdc" ฉันสับสนเล็กน้อย (ขั้นตอนเริ่มต้น)
tariq zafar

3
สำหรับกฎที่ 3 วิธีที่ชาญฉลาดคือการตั้งค่าลิงค์ส่วนต่อท้ายของรูทเป็นรูทเองและ (โดยค่าเริ่มต้น) จะตั้งค่าลิงค์ส่วนต่อท้ายของทุกโหนดให้เป็นรูท ดังนั้นเราสามารถหลีกเลี่ยงการปรับสภาพและเพียงไปตามลิงค์ต่อท้าย
sqd

1
aabaacaadเป็นหนึ่งในกรณีที่แสดงการเพิ่มลิงก์ต่อท้ายพิเศษสามารถลดเวลาในการอัปเดตสามครั้ง ข้อสรุปในสองย่อหน้าสุดท้ายของโพสต์ของ jogojapan ผิด หากเราไม่ได้เพิ่มลิงค์ต่อท้ายโพสต์ที่กล่าวถึงความซับซ้อนของเวลาโดยเฉลี่ยควรเป็น O (nlong (n)) หรือมากกว่า active_nodeเพราะมันต้องใช้เวลาพิเศษที่จะเดินต้นไม้เพื่อหาสิ่งที่ถูกต้อง
IvanaGyro

10

ขอบคุณสำหรับการกวดวิชาที่อธิบายอย่างดีโดย@jogojapanฉันใช้อัลกอริทึมใน Python

ปัญหาเล็กน้อยที่ @jogojapan กล่าวถึงนั้นซับซ้อนกว่าที่ฉันคาดไว้และต้องได้รับการปฏิบัติอย่างระมัดระวัง ฉันต้องใช้เวลาหลายวันกว่าจะทำให้การติดตั้งของฉันแข็งแกร่งพอ (ฉันคิดว่า) ปัญหาและแนวทางแก้ไขมีดังนี้:

  1. จบลงด้วยการRemainder > 0ปรากฎสถานการณ์นี้ยังสามารถเกิดขึ้นได้ในระหว่างขั้นตอนการแฉไม่เพียง แต่ในตอนท้ายของขั้นตอนวิธีการทั้งหมด เมื่อสิ่งนั้นเกิดขึ้นเราสามารถปล่อยให้ส่วนที่เหลือ, actnode, actedge และ actlength ไม่เปลี่ยนแปลงสิ้นสุดขั้นตอนการแฉปัจจุบันและเริ่มต้นขั้นตอนอื่นโดยคงการพับหรือคลี่คลายขึ้นอยู่กับว่าอักขระตัวถัดไปในสตริงเดิมอยู่บนเส้นทางปัจจุบันหรือ ไม่.

  2. Leap Over Nodes:เมื่อเราติดตามลิงก์ต่อท้ายให้อัปเดตจุดใช้งานแล้วพบว่าส่วนประกอบ active_length นั้นทำงานได้ไม่ดีกับ active_node ใหม่ เราต้องเดินหน้าต่อไปยังสถานที่ที่เหมาะสมเพื่อแยกหรือใส่ใบไม้ กระบวนการนี้อาจไม่ตรงไปตรงมาเพราะในระหว่างการเคลื่อนย้าย actlength และ actedge จะเปลี่ยนไปตลอดทางเมื่อคุณต้องย้ายกลับไปที่โหนดรูactedgeและactlengthอาจผิดเพราะการเคลื่อนไหวเหล่านั้น เราต้องการตัวแปรเพิ่มเติมเพื่อเก็บข้อมูลนั้น

    ป้อนคำอธิบายรูปภาพที่นี่

อีกสองปัญหาถูกชี้ให้เห็นโดย@managonov

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

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

ในที่สุดการนำไปใช้ของฉันในPythonมีดังนี้:

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


10

ขอโทษถ้าคำตอบของฉันดูเหมือนซ้ำซ้อน แต่ผมดำเนินการขั้นตอนวิธีการ Ukkonen เมื่อเร็ว ๆ นี้และพบว่าตัวเองดิ้นรนกับมันวัน; ฉันต้องอ่านบทความหลายเรื่องในเรื่องที่จะเข้าใจสาเหตุและวิธีของประเด็นหลักของอัลกอริทึม

ฉันพบวิธีการ 'กฎ' ของคำตอบก่อนหน้านี้ไม่เป็นประโยชน์สำหรับการทำความเข้าใจเหตุผลพื้นฐานดังนั้นฉันจึงเขียนทุกอย่างด้านล่างโดยมุ่งเน้นไปที่การปฏิบัติ หากคุณพยายามอธิบายคำอธิบายอื่น ๆ เช่นเดียวกับฉันบางทีคำอธิบายเพิ่มเติมของฉันอาจทำให้ 'คลิก' สำหรับคุณ

ฉันเผยแพร่การติดตั้ง C # ของฉันที่นี่: https://github.com/baratgabor/SuffixTree

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

ข้อกำหนดเบื้องต้น

จุดเริ่มต้นของคำอธิบายต่อไปนี้จะถือว่าคุณคุ้นเคยกับเนื้อหาและการใช้ต้นไม้ต่อท้ายและคุณลักษณะของอัลกอริทึมของ Ukkonen เช่นวิธีที่คุณขยายอักขระต้นไม้ต่อท้ายแบบตัวอักษรตั้งแต่ต้นจนจบ โดยทั่วไปฉันคิดว่าคุณได้อ่านคำอธิบายอื่น ๆ แล้ว

(อย่างไรก็ตามฉันต้องเพิ่มการบรรยายพื้นฐานบางอย่างสำหรับการไหลดังนั้นจุดเริ่มต้นอาจรู้สึกซ้ำซ้อนจริง ๆ )

ส่วนที่น่าสนใจที่สุดคือคำอธิบายเกี่ยวกับความแตกต่างระหว่างการใช้ลิงค์ต่อท้ายและการสแกนซ้ำจากรูท นี่คือสิ่งที่ทำให้ฉันมีข้อบกพร่องและปวดหัวมากในการใช้งานของฉัน

โหนดปลายใบไม้แบบปลายเปิดและข้อ จำกัด

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

แต่การเปิดท้ายของคำต่อท้าย - ด้วยเหตุผลที่ชัดเจน - ใช้งานได้เฉพาะกับโหนดที่แสดงถึงจุดสิ้นสุดของสตริงเช่นโหนดใบไม้ในโครงสร้างต้นไม้ การดำเนินการแยกย่อยที่เราเรียกใช้บนต้นไม้ (การเพิ่มโหนดโหนดใหม่และโหนดใบไม้) จะไม่เผยแพร่โดยอัตโนมัติทุกที่ที่ต้องการ

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

ตัวอย่างเช่นในกรณีของสตริง'ABCXABCY' (ดูด้านล่าง) จำเป็นต้องเพิ่มการแยกไปที่XและYในส่วนต่อท้ายสามแบบคือABC , BCและC ; มิฉะนั้นจะไม่เป็นต้นไม้ต่อท้ายที่ถูกต้องและเราไม่พบสตริงย่อยทั้งหมดโดยการจับคู่อักขระจากรูตด้านล่าง

อีกครั้งเพื่อเน้น - การดำเนินการใด ๆ ที่เราดำเนินการในส่วนต่อท้ายในต้นไม้จะต้องสะท้อนให้เห็นด้วยคำต่อท้ายเช่นกัน (เช่น ABC> BC> C) มิฉะนั้นพวกเขาก็จะกลายเป็นคำต่อท้ายที่ถูกต้อง

ทำซ้ำการแยกย่อยในส่วนต่อท้าย

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

สิ่งที่เราสามารถทำได้คือเพื่อให้ตรงกับสายที่ยาวที่สุดที่เราสามารถทำซ้ำและนับจำนวนของคำต่อท้ายของเราต้องปรับปรุงในภายหลัง นี่คือสิ่งที่'เหลือ'หมายถึง

แนวคิดของ 'เหลือ' และ 'สแกนใหม่'

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

ดังนั้นอยู่กับตัวอย่างก่อนหน้านี้ของสตริงABCXABCYเราจะจับคู่ซ้ำABCส่วน 'โดยปริยาย' ที่เพิ่มขึ้นremainderในแต่ละครั้งซึ่งผลในส่วนที่เหลือของ 3. จากนั้นเราพบไม่ซ้ำตัวอักษร'Y' ที่นี่เราแยกเพิ่มไว้ก่อนหน้าABCXเข้าไปในเอบีซี -> Xและเอบีซี-> Y จากนั้นเราลดลงremainderจาก 3 เป็น 2 เพราะเราดูแลการแยกสาขาABCแล้ว ตอนนี้เราทำการดำเนินการซ้ำโดยจับคู่อักขระ 2 ตัวสุดท้าย - BC - จากรูทถึงจุดที่เราต้องแบ่งและเราแยกBCXเป็นBC-> XและBC -> Y อีกครั้งเราลดลงremainderเป็น 1 และทำซ้ำการดำเนินการ; จนกระทั่งมีค่าremainderเป็น 0 สุดท้ายเราต้องเพิ่มอักขระปัจจุบัน ( Y ) ลงในรูทเช่นกัน

การดำเนินการนี้ตามคำต่อท้ายจากรากเพียงเพื่อไปยังจุดที่เราต้องทำการดำเนินการคือสิ่งที่เรียกว่า'rescanning'ในอัลกอริทึมของ Ukkonen และโดยทั่วไปแล้วนี่เป็นส่วนที่แพงที่สุดของอัลกอริทึม ลองนึกภาพสตริงที่ยาวขึ้นซึ่งคุณต้องการ 'rescan' substrings ยาวในหลาย ๆ โหนด (เราจะพูดถึงเรื่องนี้ในภายหลัง) อาจเป็นพัน ๆ ครั้ง

เป็นวิธีแก้ปัญหาที่เราแนะนำสิ่งที่เราเรียก'การเชื่อมโยงต่อท้าย'

แนวคิดของ 'ลิงค์ต่อท้าย'

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

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

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

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

แนวคิดของ 'จุดที่ใช้งาน'

จนถึงตอนนี้เราได้พูดถึงเครื่องมือที่มีประสิทธิภาพหลายอย่างสำหรับการสร้างต้นไม้และอ้างถึงการข้ามผ่านหลาย ๆ ขอบและโหนด แต่ยังไม่ได้สำรวจผลที่ตามมาและความซับซ้อนที่เกี่ยวข้อง

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

ประการแรกเรามักจะอาศัยอยู่บนขอบเฉพาะของโหนดดังนั้นเราจำเป็นต้องเก็บข้อมูลขอบ เราจะเรียกสิ่งนี้ว่า'ขอบที่ใช้งาน'

ประการที่สองแม้หลังจากเพิ่มข้อมูลขอบเรายังไม่มีวิธีระบุตำแหน่งที่อยู่ไกลกว่าต้นไม้และไม่เชื่อมต่อโดยตรงกับโหนดรูท ดังนั้นเราต้องเก็บโหนดด้วย ขอเรียกนี้'โหนดที่ใช้งาน'

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

สิ่งนี้นำไปสู่สิ่งที่เราเรียกว่า'จุดที่ใช้งาน' - แพคเกจของตัวแปรสามตัวที่มีข้อมูลทั้งหมดที่เราต้องการเพื่อรักษาตำแหน่งของเราในต้นไม้

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

คุณสามารถสังเกตได้จากภาพต่อไปนี้ว่าเส้นทางที่ตรงกันของABCABDประกอบด้วยอักขระ 2 ตัวที่ขอบAB (จากรูท ), บวก 4 ตัวอักษรบนขอบCABDABCABD (จากโหนด 4) - ส่งผลให้'เหลือ' 6 ตัวอักษร ดังนั้นตำแหน่งปัจจุบันของเราสามารถระบุว่าเป็นที่ใช้งานโหนดที่ 4 ที่ใช้งานอยู่ขอบ C, ใช้งานความยาว 4

ที่เหลือและ Active จุด

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

ความแตกต่างของการสแกนซ้ำโดยใช้ลิงก์ต่อท้าย

ตอนนี้ส่วนที่ยุ่งยากสิ่งที่ - จากประสบการณ์ของฉัน - อาจทำให้เกิดข้อบกพร่องและปวดหัวมากมายและได้รับการอธิบายไม่ดีในแหล่งข้อมูลส่วนใหญ่คือความแตกต่างในการประมวลผลกรณีลิงก์ต่อท้ายเทียบกับกรณีที่สแกนใหม่

พิจารณาตัวอย่างต่อไปนี้ของสตริง'AAAABAAAABAAC' :

เหลืออีกหลายขอบ

คุณสามารถสังเกตข้างต้นว่า'ส่วนที่เหลือ'ของ 7 สอดคล้องกับผลรวมของอักขระจากรูทอย่างไรขณะที่'ความยาวที่ใช้งาน'ของ 4 สอดคล้องกับผลรวมของอักขระที่ตรงกันจากขอบที่ใช้งานของโหนดที่ใช้งานอยู่

ตอนนี้หลังจากดำเนินการแยกสาขาที่จุดที่ใช้งานอยู่โหนดที่ใช้งานของเราอาจมีหรือไม่มีลิงค์ต่อท้าย

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

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

ตัวอย่างการเปรียบเทียบการประมวลผลด้วยและไม่มีลิงค์ต่อท้าย

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

ใช้'ลิงค์ต่อท้าย'

การเข้าถึงคำต่อท้ายติดต่อกันผ่านลิงก์ต่อท้าย

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

ในกรณีข้างต้นเนื่องจาก'ความยาวที่แอ็คทีฟ'คือ 4 เรากำลังทำงานกับคำต่อท้าย ' ABAA'เริ่มต้นที่โหนดที่เชื่อมโยง 4 แต่หลังจากค้นหาขอบที่สอดคล้องกับอักขระตัวแรกของคำต่อท้าย ( 'A') ) เราสังเกตว่า'ความยาวที่ใช้งาน'ของเราล้นขอบนี้ 3 ตัวอักษร ดังนั้นเราจึงกระโดดข้ามขอบเต็มไปยังโหนดถัดไปและลด'ความยาวที่ใช้งาน'โดยตัวละครที่เราบริโภคด้วยการกระโดด

จากนั้นหลังจากที่เราพบขอบถัดไป'B'ซึ่งตรงกับคำต่อท้ายที่ลดลง'BAA ' ในที่สุดเราก็ทราบว่าความยาวของขอบนั้นใหญ่กว่า'ความยาวที่ใช้งาน'ที่เหลืออยู่ 3 ซึ่งหมายความว่าเราพบที่ที่ถูกต้อง

โปรดทราบว่าดูเหมือนว่าการดำเนินการนี้มักจะไม่เรียกว่า 'การสแกนอีกครั้ง' แม้ว่าฉันจะดูเหมือนว่ามันเป็นการเทียบเท่าโดยตรงกับการสแกนซ้ำเพียงแค่ความยาวที่สั้นลงและเป็นจุดเริ่มต้นที่ไม่ใช่รูท

ใช้'rescan'

การเข้าถึงคำต่อท้ายติดต่อกันผ่านการสแกนซ้ำ

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

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

อย่างไรก็ตามโปรดทราบว่าตัวแปร'ส่วนที่เหลือ' ที่แท้จริงจะต้องได้รับการเก็บรักษาและลดลงหลังจากการแทรกแต่ละโหนดเท่านั้น ดังนั้นสิ่งที่ฉันอธิบายไว้ข้างต้นสันนิษฐานว่าใช้ตัวแปรแยกต่างหากเริ่มต้นได้ที่'ที่เหลือ'

หมายเหตุเกี่ยวกับลิงก์ส่วนต่อท้าย & ตัวแก้ไขซ้ำ

1) สังเกตว่าทั้งสองวิธีนำไปสู่ผลลัพธ์เดียวกัน อย่างไรก็ตามการข้ามลิงค์ของ Suffix นั้นเร็วกว่ามากในกรณีส่วนใหญ่ นั่นคือเหตุผลทั้งหมดที่อยู่เบื้องหลังลิงก์ที่ต่อท้าย

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

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

4) ต่อท้ายเดิมการเชื่อมโยงอธิบายที่นี่เป็นเพียงหนึ่งในแนวทางที่เป็นไปได้ ตัวอย่างเช่นNJ Larsson และคณะ ตั้งชื่อวิธีการนี้เป็นTop-Down ที่เน้นโหนดและเปรียบเทียบกับNode-Oriented Bottom-Upและพันธุ์Edge-Orientedสองชนิด วิธีการที่แตกต่างกันมีการแสดงกรณีทั่วไปความต้องการข้อ จำกัด ฯลฯ ที่แตกต่างกันและแย่ที่สุด แต่โดยทั่วไปแล้วดูเหมือนว่าวิธีการที่เน้นแนวขอบคือการปรับปรุงโดยรวมของต้นฉบับ


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 (สตริง) คุณจะมีแผนผังคำต่อท้ายที่มีคำต่อท้ายทั้งหมด

ในระหว่างการวนรอบที่สำคัญคือจุดที่ใช้งาน ฉันเดาว่านี่หมายถึงจุดที่ลึกที่สุดในทรีต่อท้ายที่สอดคล้องกับคำต่อท้ายที่เหมาะสมของอักขระ k ตัวแรกของสตริง (ฉันคิดว่าเหมาะสมหมายความว่าคำต่อท้ายไม่สามารถเป็นทั้งสตริงได้)

ตัวอย่างเช่นสมมติว่าคุณเห็นตัวอักษร 'abcabc' จุดที่ใช้งานจะเป็นตัวแทนของจุดในต้นไม้ที่สอดคล้องกับคำต่อท้าย 'abc'

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

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

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

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

หมายเหตุ 3: ส่วน canonization เพียงประหยัดเวลาในการตรวจสอบจุดที่ใช้งานอยู่ ตัวอย่างเช่นสมมติว่าคุณใช้ Origin = 0 เสมอและเพิ่งเปลี่ยนเป็นครั้งแรกและครั้งสุดท้าย ในการตรวจสอบจุดที่ใช้งานอยู่คุณจะต้องติดตามทรีต่อท้ายทุกครั้งตามโหนดกลางทั้งหมด มันสมเหตุสมผลที่จะแคชผลลัพธ์ของการติดตามเส้นทางนี้โดยการบันทึกระยะทางจากโหนดสุดท้าย

คุณสามารถยกตัวอย่างรหัสของสิ่งที่คุณหมายถึงโดยตัวแปรขอบเขต "แก้ไข"?

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


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

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.