सादे अंग्रेजी में उकोकोन के प्रत्यय ट्री एल्गोरिदम


1100

मैं इस बिंदु पर थोड़ा मोटा महसूस करता हूं। मैंने प्रत्यय के पेड़ के निर्माण के चारों ओर पूरी तरह से अपना सिर लपेटने की कोशिश में दिन बिताए हैं, लेकिन क्योंकि मेरे पास एक गणितीय पृष्ठभूमि नहीं है, कई स्पष्टीकरण मुझे हटा देते हैं क्योंकि वे गणितीय सहजीवन का अत्यधिक उपयोग करना शुरू करते हैं। एक अच्छी व्याख्या के निकटतम जो मैंने पाया है कि प्रत्यय पेड़ों के साथ फास्ट स्ट्रिंग सर्चिंग है , लेकिन वह विभिन्न बिंदुओं पर चमकता है और एल्गोरिथ्म के कुछ पहलू अस्पष्ट रहते हैं।

स्टैक ओवरफ्लो पर यहाँ इस एल्गोरिथ्म का एक चरण-दर-चरण स्पष्टीकरण मेरे अलावा कई अन्य लोगों के लिए अमूल्य होगा, मुझे यकीन है।

संदर्भ के लिए, यहाँ एल्गोरिथ्म पर उककोनन का पेपर है: http://www.cs.helsinki.fi/u/ukkonen/SuffixT1withFigs.pdf

मेरी बुनियादी समझ, अब तक:

  • मुझे दिए गए स्ट्रिंग T के प्रत्येक उपसर्ग P के माध्यम से पुनरावृति करने की आवश्यकता है
  • मुझे उपसर्ग P में प्रत्येक प्रत्यय S के माध्यम से पुनरावृति करना और उस पेड़ में जोड़ना होगा
  • पेड़ में प्रत्यय एस को जोड़ने के लिए, मुझे एस में प्रत्येक चरित्र के माध्यम से पुनरावृति करने की आवश्यकता है, पुनरावृत्तियों के साथ या तो एक मौजूदा शाखा को चलना है जो कि सी में वर्णों के एक ही सेट से शुरू होता है और संभवतः एक छोर को वंशज नोड में विभाजित करता है। प्रत्यय में एक अलग चरित्र तक पहुँचने, या नीचे चलने के लिए कोई मेल खाने वाला किनारा नहीं था। जब सी के लिए नीचे चलने के लिए कोई मिलान किनारे नहीं पाया जाता है, तो सी के लिए एक नया पत्ता किनारे बनाया जाता है।

मूल एल्गोरिथ्म O (n 2 ) प्रतीत होता है , जैसा कि अधिकांश स्पष्टीकरणों में बताया गया है, क्योंकि हमें सभी उपसर्गों के माध्यम से कदम बढ़ाने की आवश्यकता है, फिर हमें प्रत्येक उपसर्ग के लिए प्रत्ययों के माध्यम से कदम बढ़ाने की आवश्यकता है। Ukkonen एल्गोरिथ्म हालांकि मुझे लगता है कि, प्रत्यय सूचक तकनीक वह का उपयोग करता है की वजह से जाहिरा तौर पर अद्वितीय है कि क्या मुझे समझने में कठिनाई हो रही है।

मुझे समझने में भी परेशानी हो रही है:

  • "सक्रिय बिंदु" को कब और कैसे सौंपा गया है, इसका उपयोग और परिवर्तन किया गया है
  • एल्गोरिथ्म के canonization पहलू के साथ क्या हो रहा है
  • जिन क्रियान्वयनों को मैंने देखा है, उनका उपयोग करने वाले चर को "ठीक" करने की आवश्यकता है

यहाँ पूरा C # सोर्स कोड दिया गया है। यह न केवल सही ढंग से काम करता है, बल्कि स्वचालित कैनोनेज़ेशन का समर्थन करता है और आउटपुट के एक अच्छे दिखने वाले टेक्स्ट ग्राफ को प्रस्तुत करता है। स्रोत कोड और नमूना आउटपुट यहां है:

https://gist.github.com/2373868


अद्यतन 2017-11-04

कई वर्षों के बाद मुझे प्रत्यय के पेड़ों के लिए एक नया उपयोग मिला है, और जावास्क्रिप्ट में एल्गोरिदम को लागू किया है । गिस्ट नीचे है। यह बग-रहित होना चाहिए। इसे एक npm install chalkही स्थान से एक js फ़ाइल में डंप करें , और फिर कुछ रंगीन आउटपुट देखने के लिए नोड के साथ चलाएं। किसी भी डिबगिंग कोड के बिना, एक ही Gist में एक स्ट्रिप डाउन संस्करण है।

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


2
क्या आपने डैन गुसफील्ड की पुस्तक में दिए गए विवरण पर एक नज़र डाली ? मैंने पाया कि मददगार बनना है।
जोगोजपन

4
जीआईएस लाइसेंस को निर्दिष्ट नहीं करता है - क्या मैं आपका कोड बदल सकता हूं और एमआईटी के तहत पुनर्प्रकाशित कर सकता हूं (स्पष्ट रूप से एट्रिब्यूशन के साथ)?
यूरीक

2
हाँ, अपने जीवन के लिए जाओ। इसे पब्लिक डोमेन मानें। जैसा कि इस पृष्ठ पर एक अन्य उत्तर द्वारा उल्लेख किया गया है, एक बग है जिसे वैसे भी फिक्सिंग की आवश्यकता है।
नाथन रिडले

1
शायद यह कार्यान्वयन दूसरों की मदद करेगा, गोटो कोड. google.com/p/text-indexing
cos

2
"इसे सार्वजनिक डोमेन पर विचार करें", शायद आश्चर्यजनक रूप से एक बहुत ही अनपेक्षित उत्तर है। कारण यह है कि आपके लिए सार्वजनिक डोमेन में काम करना वास्तव में असंभव है। इसलिए आपका "इस पर विचार करें ..." टिप्पणी इस तथ्य को रेखांकित करती है कि लाइसेंस अस्पष्ट है और पाठक को संदेह का कारण देता है कि कार्य की स्थिति वास्तव में आपके लिए स्पष्ट है । यदि आप चाहते हैं कि लोग आपके कोड का उपयोग करने में सक्षम हों, तो कृपया इसके लिए एक लाइसेंस निर्दिष्ट करें, अपने पसंद का कोई भी लाइसेंस चुनें (लेकिन, जब तक आप एक वकील नहीं हैं, पहले से मौजूद लाइसेंस चुनें!)
जेम्स यंगमैन

जवाबों:


2377

निम्नलिखित उककोन एल्गोरिथम का वर्णन करने का एक प्रयास है, जो यह दर्शाता है कि जब स्ट्रिंग सरल होती है (अर्थात कोई दोहराए गए वर्ण नहीं होते हैं), और फिर इसे पूर्ण एल्गोरिथ्म में विस्तारित किया जाता है।

सबसे पहले, कुछ प्रारंभिक बयान।

  1. हम जो निर्माण कर रहे हैं, वह मूल रूप से एक खोज तिकड़ी की तरह है। तो एक रूट नोड है, इससे निकलने वाले किनारों को नए नोड्स तक ले जाना है, और आगे के किनारों को उन लोगों से बाहर जाना है, और आगे

  2. लेकिन : एक खोज तिकड़ी के विपरीत, किनारे लेबल एकल वर्ण नहीं हैं। इसके बजाय, प्रत्येक किनारे को पूर्णांक की एक जोड़ी का उपयोग करके लेबल किया जाता है [from,to]। ये पाठ में संकेत हैं। इस अर्थ में, प्रत्येक किनारे पर मनमाना लंबाई का एक स्ट्रिंग लेबल होता है, लेकिन केवल O (1) स्थान (दो पॉइंटर्स) लेता है।

मूल सिद्धांत

मैं पहले प्रदर्शित करना चाहता हूं कि विशेष रूप से सरल स्ट्रिंग के प्रत्यय के पेड़ को कैसे बनाया जाए, एक स्ट्रिंग जिसे कोई दोहराया वर्ण नहीं है:

abc

एल्गोरिथ्म चरणों में काम करता है, बाएं से दाएं । नहीं है स्ट्रिंग के हर किरदार के लिए एक कदम । प्रत्येक चरण में एक से अधिक व्यक्तिगत ऑपरेशन शामिल हो सकते हैं, लेकिन हम देखेंगे (अंत में अंतिम टिप्पणियों को देखें) कि संचालन की कुल संख्या ओ (एन) है।

इसलिए, हम बाईं ओर से शुरू करते हैं , और पहले aरूट नोड (बाईं ओर) से एक पत्ती तक एक बढ़त बनाकर केवल एक ही चरित्र सम्मिलित करते हैं , और इसे लेबल करते हैं [0,#], जिसका अर्थ है कि किनारे 0 की स्थिति में शुरू होने वाले प्रतिस्थापन का प्रतिनिधित्व करता है और समाप्त होता है पर वर्तमान अंत । मैं प्रतीक #का उपयोग वर्तमान छोर का अर्थ करने के लिए करता हूं , जो कि स्थिति 1 पर है (दाएं बाद में a)।

तो हमारे पास एक प्रारंभिक पेड़ है, जो इस तरह दिखता है:

और इसका मतलब यह है:

अब हम स्थिति 2 पर (ठीक बाद b) प्रगति करते हैं । प्रत्येक चरण में हमारा लक्ष्य सभी प्रत्ययों को वर्तमान स्थिति तक सम्मिलित करना है । हम इसके द्वारा करते हैं

  • मौजूदा-विस्तार का विस्तार करने के aलिएab
  • के लिए एक नया किनारा डालने b

हमारे प्रतिनिधित्व में यह ऐसा दिखता है

यहां छवि विवरण दर्ज करें

और इसका क्या मतलब है:

हम दो चीजों का पालन ​​करते हैं:

  • के लिए किनारे प्रतिनिधित्व abहै एक ही रूप में यह प्रारंभिक पेड़ में हुआ करता था: [0,#]। इसका अर्थ अपने आप बदल गया है क्योंकि हमने वर्तमान स्थिति #को 1 से 2 तक अद्यतन किया है ।
  • प्रत्येक किनारे O (1) स्थान का उपभोग करता है, क्योंकि इसमें पाठ में केवल दो बिंदु होते हैं, भले ही यह कितने वर्णों का प्रतिनिधित्व करता हो।

अगला हम स्थिति को फिर से बढ़ाते हैं और cप्रत्येक मौजूदा किनारे पर एक जोड़कर और नए प्रत्यय के लिए एक नया किनारा डालकर पेड़ को अपडेट करते हैं c

हमारे प्रतिनिधित्व में यह ऐसा दिखता है

और इसका क्या मतलब है:

हम निरीक्षण करते हैं:

  • पेड़ प्रत्येक चरण के बाद वर्तमान स्थिति तक सही प्रत्यय वृक्ष है
  • पाठ में जितने अक्षर हैं उतने ही चरण हैं
  • प्रत्येक चरण में काम की मात्रा ओ (1) है, क्योंकि सभी मौजूदा किनारों को बढ़ाकर स्वचालित रूप से अपडेट किया जाता है #, और अंतिम चरित्र के लिए एक नया किनारा डालने का काम ओ (1) समय में किया जा सकता है। इसलिए लंबाई n की एक स्ट्रिंग के लिए, केवल O (n) समय की आवश्यकता होती है।

पहला विस्तार: सरल दोहराव

बेशक यह केवल इतना अच्छा काम करता है क्योंकि हमारे स्ट्रिंग में कोई पुनरावृत्ति नहीं होती है। अब हम एक और अधिक यथार्थवादी स्ट्रिंग देखें:

abcabxabcd

यह abcपिछले उदाहरण के साथ शुरू होता है , फिर abदोहराया जाता है और उसके बाद x, और फिर abcदोहराया जाता है d

चरण 3 से 3: पहले 3 चरणों के बाद हमारे पास पिछले उदाहरण से पेड़ है:

चरण 4: हम #स्थिति 4 में चले जाते हैं । यह अंतर्निहित रूप से सभी मौजूदा किनारों को अपडेट करता है:

और हमें वर्तमान चरण के अंतिम प्रत्यय aको रूट पर सम्मिलित करने की आवश्यकता है ।

ऐसा करने से पहले, हम दो और चर (इसके अलावा #) पेश करते हैं, जो निश्चित रूप से हर समय होते हैं, लेकिन हमने अभी तक उनका उपयोग नहीं किया है:

  • सक्रिय बिंदु है, जो एक ट्रिपल है (active_node,active_edge,active_length)
  • remainderहै, जो एक पूर्णांक का संकेत कितने नए प्रत्यय हम डालने की जरूरत है

इन दोनों का सटीक अर्थ जल्द ही स्पष्ट हो जाएगा, लेकिन अभी के लिए आइए बताते हैं:

  • सरल abcउदाहरण में, सक्रिय बिंदु हमेशा था (root,'\0x',0), यानी active_nodeरूट नोड active_edgeथा , इसे शून्य वर्ण के रूप में निर्दिष्ट किया गया था '\0x', और active_lengthशून्य था। इसका प्रभाव यह था कि हर चरण में हमने जो एक नया किनारा डाला था, उसे नए सिरे से बनाए गए किनारे के रूप में रूट नोड में डाला गया था। हम जल्द ही देखेंगे कि इस जानकारी का प्रतिनिधित्व करने के लिए एक ट्रिपल क्यों आवश्यक है।
  • remainderहमेशा हर कदम की शुरुआत में 1 पर सेट किया गया था। इसका अर्थ यह था कि प्रत्येक चरण के अंत में हमें सक्रिय रूप से सम्मिलित होने वाले प्रत्ययों की संख्या 1 थी (हमेशा अंतिम वर्ण)।

अब यह बदलने जा रहा है। जब हम वर्तमान अंतिम चरित्र aको रूट पर सम्मिलित करते हैं , तो हम ध्यान देते हैं कि पहले से ही एक आउटगोइंग एज है a, विशेष रूप से abca:। यहाँ हम ऐसे मामले में क्या करते हैं:

  • हम रूट नोड पर एक नया किनारा नहीं[4,#] डालते हैं। इसके बजाय हम बस नोटिस करते हैं कि प्रत्यय aपहले से ही हमारे पेड़ में है। यह एक लंबी धार के बीच में समाप्त होता है, लेकिन हम इससे परेशान नहीं हैं। हम चीजों को वैसे ही छोड़ देते हैं जैसे वे हैं।
  • हम सक्रिय बिंदु निर्धारित करने के लिए (root,'a',1)। इसका मतलब है कि सक्रिय बिंदु अब रूट नोड के आउटगोइंग एज के बीच में है aजो उस किनारे पर स्थिति 1 के बाद विशेष रूप से शुरू होता है । हम देखते हैं कि किनारे को इसके पहले चरित्र द्वारा निर्दिष्ट किया गया है a। यह पर्याप्त है क्योंकि किसी भी विशेष चरित्र से शुरू होने वाला केवल एक ही किनारा हो सकता है (पुष्टि करें कि यह पूरे विवरण के माध्यम से पढ़ने के बाद सच है)।
  • हम भी वृद्धि करते हैं remainder, इसलिए अगले चरण की शुरुआत में यह 2 होगा।

अवलोकन: जब अंतिम प्रत्यय हमें सम्मिलित करने की आवश्यकता होती है , तो पहले से ही पेड़ में मौजूद पाया जाता है , पेड़ ही नहीं बदला जाता है (हम केवल सक्रिय बिंदु को अपडेट करते हैं और remainder)। पेड़ तो प्रत्यय पेड़ का सही प्रतिनिधित्व नहीं है वर्तमान स्थिति के लिए ऊपर किसी भी अधिक है, लेकिन यह होता है (क्योंकि अंतिम प्रत्यय सभी प्रत्यय aनिहित है परोक्ष )। इसलिए, चर को अपडेट करने के अलावा (जो सभी निश्चित लंबाई के हैं, इसलिए यह O (1) है), इस चरण में कोई काम नहीं किया गया था ।

चरण 5: हम वर्तमान स्थिति #को 5 पर अपडेट करते हैं। यह स्वचालित रूप से पेड़ को इस पर अपडेट करता है:

और क्योंकि remainder2 है , हमें वर्तमान स्थिति के दो अंतिम प्रत्ययों को सम्मिलित करने की आवश्यकता है: abऔर b। यह मूल रूप से है क्योंकि:

  • aपिछले चरण से प्रत्यय ठीक से कभी नहीं डाला गया है। तो यह बना हुआ है , और जब से हमने एक कदम आगे बढ़ाया है, यह अब से बढ़ गया aहै ab
  • और हमें नया अंतिम छोर डालने की आवश्यकता है b

व्यवहार में इसका मतलब यह है कि हम सक्रिय बिंदु पर जाते हैं (जो aकि अब abcabकिनारे के पीछे की ओर इशारा करता है), और वर्तमान अंतिम चरित्र डालें bलेकिन: फिर से, यह पता चला है कि bपहले से ही उसी किनारे पर मौजूद है।

तो, फिर से, हम पेड़ को नहीं बदलते हैं। हम बस:

  • सक्रिय बिंदु को अपडेट करें (root,'a',2)(पहले नोड और किनारे पहले की तरह, लेकिन अब हम पीछे की ओर इशारा करते हैं b)
  • remainder3 से वृद्धि क्योंकि हमने अभी भी पिछले चरण से अंतिम छोर को ठीक से नहीं डाला है, और हम वर्तमान अंतिम छोर को भी सम्मिलित नहीं करते हैं।

स्पष्ट होने के लिए: हमें सम्मिलित करना था abऔर bवर्तमान चरण में, लेकिन क्योंकि abपहले से ही पाया गया था, हमने सक्रिय बिंदु को अपडेट किया और सम्मिलित करने का प्रयास भी नहीं किया b। क्यों? क्योंकि अगर abपेड़ में है, तो हर प्रत्यय (सहित b) पेड़ में भी होना चाहिए। शायद केवल स्पष्ट रूप से , लेकिन यह वहाँ होना चाहिए, जिस तरह से हमने अब तक पेड़ का निर्माण किया है।

हम वेतन वृद्धि से 6 कदम आगे बढ़ते हैं #। पेड़ अपने आप अपडेट हो जाता है:

क्योंकि remainder3 है , हमें सम्मिलित करना है abx, bxऔर x। सक्रिय बिंदु हमें बताता है कि कहां abसमाप्त होता है, इसलिए हमें केवल वहां कूदने और सम्मिलित करने की आवश्यकता है x। वास्तव में, xअभी तक वहाँ नहीं है, इसलिए हम abcabxकिनारे को विभाजित करते हैं और एक आंतरिक नोड सम्मिलित करते हैं:

किनारे के निरूपण अभी भी पाठ में संकेत हैं, इसलिए आंतरिक नोड को विभाजित करना और सम्मिलित करना ओ (1) समय में किया जा सकता है।

इसलिए हमने 2 के साथ abxनिस्तारण किया है और remainderअब हमें अगले शेष प्रत्यय को सम्मिलित करने की आवश्यकता है bx। लेकिन इससे पहले कि हम ऐसा करें कि हमें सक्रिय बिंदु को अपडेट करने की आवश्यकता है। इसके लिए नियम, एक किनारे को विभाजित करने और सम्मिलित करने के बाद, नीचे नियम 1 कहा जाएगा , और यह जब भी active_nodeरूट होता है (हम आगे के अन्य मामलों के लिए नियम 3 सीखेंगे) लागू होता है। यहाँ नियम 1 है:

जड़ से सम्मिलन के बाद,

  • active_node जड़ बना हुआ है
  • active_edge नए प्रत्यय के पहले चरित्र के लिए सेट है जिसे हमें सम्मिलित करने की आवश्यकता है, अर्थात b
  • active_length 1 से कम हो गया है

इसलिए, नया सक्रिय-बिंदु ट्रिपल (root,'b',1)इंगित करता है कि अगला इंसर्ट को bcabxकिनारे पर बनाया जाना चाहिए , 1 वर्ण के पीछे, यानी पीछे b। हम ओ (1) समय में सम्मिलन बिंदु की पहचान कर सकते हैं और जांच सकते हैं कि xपहले से मौजूद है या नहीं। यदि यह मौजूद था, तो हम वर्तमान कदम को समाप्त कर देंगे और सब कुछ उसी तरह छोड़ देंगे। लेकिन x मौजूद नहीं है, इसलिए हम किनारे को विभाजित करके इसे सम्मिलित करते हैं:

फिर, इसमें O (1) का समय लगा और हम नियम 1 के रूप में remainder1 और सक्रिय बिंदु को अपडेट करते (root,'x',0)हैं।

लेकिन एक और चीज है जो हमें करने की जरूरत है। हम इस नियम 2 को कॉल करेंगे :

यदि हम एक किनारे को विभाजित करते हैं और एक नया नोड सम्मिलित करते हैं, और यदि वह वर्तमान चरण के दौरान बनाया गया पहला नोड नहीं है , तो हम पहले से डाले गए नोड और नए नोड को एक विशेष सूचक, एक प्रत्यय लिंक के माध्यम से जोड़ते हैं । हम बाद में देखेंगे कि यह क्यों उपयोगी है। यहाँ हमें जो मिलता है, प्रत्यय लिंक को डॉटेड एज के रूप में दर्शाया गया है:

हमें अभी भी वर्तमान चरण के अंतिम प्रत्यय को सम्मिलित करने की आवश्यकता है x। चूंकि active_lengthसक्रिय नोड का घटक 0 पर गिर गया है, अंतिम प्रविष्टि सीधे रूट पर बनाई गई है। चूंकि रूट नोड पर कोई आउटगोइंग एज नहीं है x, इसलिए हम एक नया किनारा डालें:

जैसा कि हम देख सकते हैं, वर्तमान चरण में सभी शेष आवेषण किए गए थे।

हम = 7 सेट करके चरण 7 पर आगे बढ़ते हैं #, जो aहमेशा की तरह सभी पत्ती किनारों पर, अगले चरित्र को स्वचालित रूप से जोड़ता है । फिर हम नए अंतिम चरित्र को सक्रिय बिंदु (रूट) में सम्मिलित करने का प्रयास करते हैं, और पाते हैं कि यह पहले से ही है। इसलिए हम कुछ भी सम्मिलित किए बिना वर्तमान चरण को समाप्त करते हैं और सक्रिय बिंदु को अपडेट करते हैं (root,'a',1)

में चरण 8 , #= 8, हम संलग्न b, और जैसा कि पहले देखा, यह केवल इसका मतलब है कि हम करने के लिए सक्रिय बिंदु को अद्यतन (root,'a',2)और वेतन वृद्धि remainderकुछ और करने के बिना, क्योंकि bपहले से ही मौजूद है। हालांकि, हम नोटिस करते हैं (ओ (1) समय) कि सक्रिय बिंदु अब एक छोर के अंत में है। हम इसे फिर से सेट करके इसे दर्शाते हैं (node1,'\0x',0)। यहाँ, मैं node1आंतरिक नोड का उल्लेख करने के लिए उपयोग करता हूँ जो abकिनारे पर समाप्त होता है।

फिर, चरण #= 9 में , हमें '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पूरा हो गया है, हम घटती remainderसे 3 और वर्तमान कदम के अगले शेष प्रत्यय पर विचार, bcd। नियम 3 ने सक्रिय बिंदु को सिर्फ सही नोड और किनारे पर सेट किया है ताकि डालने को सक्रिय बिंदु पर bcdबस अपने अंतिम चरित्र dको सम्मिलित करके किया जा सके ।

ऐसा करने से एक और बढ़त विभाजित हो जाती है, और नियम 2 के कारण , हमें पहले से डाले गए नोड से नए तक एक प्रत्यय लिंक बनाना होगा:

हम निरीक्षण करते हैं: प्रत्यय लिंक हमें सक्रिय बिंदु को रीसेट करने में सक्षम बनाता है ताकि हम O (1) प्रयास में अगला शेष सम्मिलित कर सकें । पुष्टि करने के लिए ऊपर ग्राफ कि वास्तव में नोड लेबल पर पर देखो abपर नोड से जुड़ा हुआ है b(अपने प्रत्यय), और कम से नोड abcसे जुड़ा हुआ है bc

वर्तमान चरण अभी समाप्त नहीं हुआ है। remainderअब 2 है, और हमें सक्रिय बिंदु को फिर से रीसेट करने के लिए नियम 3 का पालन करने की आवश्यकता है। चूंकि वर्तमान active_node(ऊपर लाल) में कोई प्रत्यय लिंक नहीं है, इसलिए हम रूट पर रीसेट करते हैं। सक्रिय बिंदु अब है (root,'c',1)

इसलिए अगला इंसर्ट रूट नोड के एक आउटगोइंग किनारे पर होता है जिसका लेबल इसके साथ शुरू होता है c: cabxabcdपहले वर्ण के पीछे, यानी पीछे c। यह एक और विभाजन का कारण बनता है:

और चूंकि इसमें एक नए आंतरिक नोड का निर्माण शामिल है, हम नियम 2 का पालन करते हैं और पहले बनाए गए आंतरिक नोड से एक नया प्रत्यय लिंक सेट करते हैं:

(मैं इन छोटे रेखांकन के लिए ग्राफविज़ डॉट का उपयोग कर रहा हूं । नए प्रत्यय लिंक के कारण मौजूदा किनारों को फिर से व्यवस्थित करना है, इसलिए यह पुष्टि करने के लिए ध्यान से देखें कि ऊपर डाली गई एकमात्र चीज़ एक नया प्रत्यय लिंक है।)

इसके साथ, remainder1 पर सेट किया जा सकता है और चूंकि active_nodeरूट है, हम सक्रिय बिंदु को अपडेट करने के लिए नियम 1 का उपयोग करते हैं (root,'d',0)। इसका मतलब है कि वर्तमान चरण का अंतिम इंसर्ट d जड़ में एक एकल सम्मिलित करने के लिए है :

वह अंतिम चरण था और हम कर रहे हैं। हालांकि अंतिम टिप्पणियों की संख्या है :

  • प्रत्येक चरण में हम #1 स्थिति से आगे बढ़ते हैं। यह स्वचालित रूप से O (1) समय में सभी लीफ नोड्स को अपडेट करता है।

  • लेकिन यह पिछले चरणों से शेष किसी भी प्रत्यय ) और वर्तमान चरण के एक अंतिम चरित्र के साथ बी) से संबंधित नहीं है।

  • remainderहमें बताता है कि हमें कितने अतिरिक्त आवेषण बनाने हैं। ये आवेषण मौजूदा स्थिति में समाप्त होने वाले स्ट्रिंग के अंतिम प्रत्ययों के अनुरूप एक-से-एक होते हैं #। हम एक के बाद एक विचार करते हैं और सम्मिलित करते हैं। महत्वपूर्ण: प्रत्येक इंसर्ट O (1) समय में किया जाता है क्योंकि सक्रिय बिंदु हमें बताता है कि कहाँ जाना है, और हमें सक्रिय बिंदु पर केवल एक ही वर्ण जोड़ने की आवश्यकता है। क्यों? क्योंकि अन्य वर्ण निहित हैं (अन्यथा सक्रिय बिंदु वह जगह नहीं होगी जहां यह है)।

  • इस तरह के प्रत्येक डालने के बाद, remainderयदि कोई हो , तो हम प्रत्यय लिंक को घटाते हैं और उसका पालन करते हैं। यदि हम रूट (नियम 3) पर नहीं जाते हैं। यदि हम पहले से ही रूट पर हैं, तो हम नियम 1 का उपयोग करके सक्रिय बिंदु को संशोधित करते हैं। किसी भी मामले में, यह केवल O (1) समय लेता है।

  • यदि, इन आवेषणों में से एक के दौरान, हम पाते हैं कि जिस चरित्र को हम सम्मिलित करना चाहते हैं वह पहले से ही है, हम कुछ भी नहीं करते हैं और वर्तमान चरण को समाप्त करते हैं, भले ही remainder> 0। कारण यह है कि जो भी आवेषण बने रहेंगे, वे उस प्रत्यय के प्रत्यय होंगे जो हमने अभी बनाने की कोशिश की थी। इसलिए वे सभी वर्तमान पेड़ में निहित हैं । यह तथ्य कि remainder> 0 सुनिश्चित करता है कि हम बाद में शेष प्रत्ययों से निपटते हैं।

  • क्या होगा अगर एल्गोरिथ्म के अंत में remainder> 0? जब भी पाठ का अंत एक विकल्प है जो पहले कहीं हुआ था, तो यह मामला होगा। उस स्थिति में हमें स्ट्रिंग के अंत में एक अतिरिक्त चरित्र जोड़ना होगा जो पहले नहीं हुआ है। साहित्य में, आमतौर पर डॉलर चिन्ह $का उपयोग उसके लिए प्रतीक के रूप में किया जाता है। यह तथ्य इतना मायने क्यों रखता हे? -> अगर बाद में हम प्रत्ययों की खोज के लिए पूर्ण प्रत्यय के पेड़ का उपयोग करते हैं, तो हमें केवल तभी मैच स्वीकार करना चाहिए जब वे एक पत्ते पर समाप्त हो जाएं । अन्यथा हमें बहुत सारे संयमी मैच मिलेंगे, क्योंकि पेड़ में निहित कई तार हैं जो मुख्य स्ट्रिंग के वास्तविक प्रत्यय नहीं हैं। जबरदस्तीremainderअंत में 0 होना अनिवार्य रूप से यह सुनिश्चित करने का एक तरीका है कि सभी प्रत्यय एक पत्ती नोड पर समाप्त होते हैं। हालांकि, अगर हम सामान्य उपद्रवों की खोज करने के लिए पेड़ का उपयोग करना चाहते हैं , न केवल मुख्य स्ट्रिंग के प्रत्यय , यह अंतिम चरण वास्तव में आवश्यक नहीं है, जैसा कि नीचे ओपी की टिप्पणी द्वारा सुझाया गया है।

  • तो संपूर्ण एल्गोरिथ्म की जटिलता क्या है? यदि पाठ लंबाई में n वर्ण है, तो स्पष्ट रूप से n चरण हैं (या यदि डॉलर चिह्न जोड़ते हैं तो n + 1)। प्रत्येक चरण में हम या तो कुछ भी नहीं करते हैं (चर को अपडेट करने के अलावा), या हम remainderआवेषण बनाते हैं , प्रत्येक ओ (1) समय लेते हैं। चूंकि remainderयह इंगित करता है कि हमने पिछले चरणों में कितनी बार कुछ भी नहीं किया है, और अब हम जो कुछ भी करते हैं, उसके लिए घटाया जाता है, कुल बार हम कुछ करते हैं n (या n + 1)। इसलिए, कुल जटिलता O (n) है।

  • हालांकि, एक छोटी सी बात है जिसे मैंने ठीक से नहीं समझाया: ऐसा हो सकता है कि हम एक प्रत्यय लिंक का अनुसरण करें, सक्रिय बिंदु को अपडेट करें, और फिर पता लगाएं कि इसका active_lengthघटक नए के साथ अच्छी तरह से काम नहीं करता है active_node। उदाहरण के लिए, इस तरह की स्थिति पर विचार करें:

(धराशायी लाइनें बाकी पेड़ों को दर्शाती हैं। बिंदीदार रेखा एक प्रत्यय कड़ी है।)

अब सक्रिय बिंदु होने दें (red,'d',3), इसलिए यह किनारे fपर पीछे की जगह को इंगित करता defgहै। अब मान लें कि हमने आवश्यक अपडेट किए हैं और अब नियम 3 के अनुसार सक्रिय बिंदु को अपडेट करने के लिए प्रत्यय लिंक का पालन करें। नया सक्रिय बिंदु है (green,'d',3)। हालांकि, dहरे रंग के नोड से बाहर निकलने का संकेत है de, इसलिए इसमें केवल 2 वर्ण हैं। सही सक्रिय बिंदु खोजने के लिए, हमें स्पष्ट रूप से उस किनारे को नीली नोड पर चलना होगा और रीसेट करना होगा (blue,'f',1)

विशेष रूप से खराब स्थिति में, active_lengthजितना बड़ा हो सकता है remainder, उतना बड़ा 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
खेद है कि मुझे उम्मीद से थोड़ा अधिक समय समाप्त हो गया। और मुझे पता है कि यह बहुत सी तुच्छ बातें हैं जो हम सभी जानते हैं, जबकि कठिन भागों को अभी भी पूरी तरह से स्पष्ट नहीं किया जा सकता है। आइए इसे एक साथ आकार में संपादित करें।
जोगोजपन

68
और मुझे यह जोड़ना चाहिए कि यह डैन गुसफील्ड की पुस्तक में पाए गए विवरण पर आधारित नहीं है । यह पहली बार एल्गोरिथ्म का वर्णन करने का एक नया प्रयास है जिसमें बिना किसी पुनरावृत्ति के साथ एक स्ट्रिंग पर विचार किया जाता है और फिर चर्चा की जाती है कि कैसे पुनरावृत्ति को संभाला जाता है। मुझे उम्मीद थी कि यह अधिक सहज होगा।
जोगोजपन

8
धन्यवाद @ जोगोजपन, मैं आपके स्पष्टीकरण के लिए पूरी तरह से काम करने वाला उदाहरण लिखने में सक्षम था। मैंने स्रोत को इतनी उम्मीद से प्रकाशित किया है कि कोई और इसका उपयोग कर सकता है: gist.github.com/2373868
नाथन रिडले

4
@NathanRidley हां (वैसे, अंतिम बिट वही है जो उककोनेन कॉलोनीज़ कर सकता है)। इसे ट्रिगर करने का एक तरीका यह सुनिश्चित करना है कि एक सबस्ट्रिंग है जो तीन बार दिखाई देता है और एक स्ट्रिंग में समाप्त होता है जो एक और बार एक अलग संदर्भ में प्रकट होता है। जैसे abcdefabxybcdmnabcdex। के प्रारंभिक भाग में abcdदोहराया जाता है abxy(यह बाद में एक आंतरिक नोड बनाता है ab) और फिर से abcdex, और इसमें समाप्त होता है bcd, जो न केवल bcdexसंदर्भ में, बल्कि bcdmnसंदर्भ में भी प्रकट होता है। डालने के बाद abcdex, हम सम्मिलित करने के लिए प्रत्यय लिंक का पालन करते हैं bcdex, और वहाँ
canonicize

6
ठीक है, मेरा कोड पूरी तरह से फिर से लिखा गया है और अब सभी मामलों के लिए सही ढंग से काम करता है, जिसमें स्वचालित कैनोनेज़ेशन भी शामिल है, साथ ही इसमें एक बहुत अच्छा टेक्स्ट ग्राफ आउटपुट भी है। gist.github.com/2373868
नाथन रिडले

132

मैंने जोगोजपन के उत्तर में दिए गए दृष्टिकोण के साथ प्रत्यय ट्री को लागू करने की कोशिश की, लेकिन नियमों के लिए इस्तेमाल किए जाने वाले शब्दों के कारण यह कुछ मामलों में काम नहीं आया। इसके अलावा, मैंने उल्लेख किया है कि कोई भी इस दृष्टिकोण का उपयोग करके एक बिल्कुल सही प्रत्यय पेड़ को लागू करने में कामयाब नहीं हुआ है। नीचे मैं नियमों में कुछ संशोधनों के साथ जोगोजपन के उत्तर का "अवलोकन" लिखूंगा। मैं उस मामले का भी वर्णन करूंगा जब हम महत्वपूर्ण प्रत्यय लिंक बनाना भूल जाते हैं ।

अतिरिक्त चर का इस्तेमाल किया

  1. सक्रिय बिंदु - एक ट्रिपल (active_node; active_edge; active_length), जहां से हमें एक नया प्रत्यय डालना शुरू करना होगा।
  2. शेष - उन प्रत्ययों की संख्या दर्शाता है जिन्हें हमें स्पष्ट रूप से जोड़ना चाहिए । उदाहरण के लिए, यदि हमारा शब्द 'अबकाबा' है, और शेष = 3 है, तो इसका मतलब है कि हमें 3 अंतिम प्रत्ययों को संसाधित करना चाहिए: bca , ca और a

चलो एक की अवधारणा का उपयोग आंतरिक नोड को छोड़कर सभी नोड्स, - जड़ और लीफ़्स हैं आंतरिक नोड्स

अवलोकन १

जब अंतिम प्रत्यय हमें सम्मिलित करने की आवश्यकता होती है, तो पहले से ही पेड़ में मौजूद पाया जाता है, पेड़ ही नहीं बदला जाता है (हम केवल अपडेट करते हैं active pointऔर remainder)।

अवलोकन २

यदि किसी बिंदु active_lengthपर वर्तमान धार ( edge_length) की लंबाई के बराबर या अधिक है , तो हम अपने active pointनीचे ले जाते हैं जब तक edge_lengthकि कड़ाई से अधिक न हो active_length

अब, नियमों को फिर से परिभाषित करें:

नियम 1

यदि सक्रिय नोड = रूट से सम्मिलन के बाद , सक्रिय लंबाई 0 से अधिक है, तो:

  1. सक्रिय नोड नहीं बदला गया है
  2. सक्रिय लंबाई घट जाती है
  3. सक्रिय बढ़त को सही स्थानांतरित किया गया है (अगले प्रत्यय के पहले चरित्र में हमें सम्मिलित करना होगा)

नियम २

हम एक नया बनाते हैं तो आंतरिक नोड या एक से एक Inserter बनाने के आंतरिक नोड , और यह पहली बार नहीं है इस तरह के आंतरिक नोड वर्तमान कदम पर है, तो हम पिछले लिंक ऐसी साथ नोड इस एक के माध्यम से एक प्रत्यय लिंक

की यह परिभाषा Rule 2'जोगोजपन' से अलग है, क्योंकि यहाँ हम न केवल नए बनाए गए आंतरिक नोड्स, बल्कि आंतरिक नोड्स को भी ध्यान में रखते हैं, जिनसे हम एक सम्मिलन बनाते हैं।

नियम ३

सक्रिय नोड से एक डालने के बाद जो रूट नोड नहीं है, हमें प्रत्यय लिंक का पालन करना चाहिए और नोड को सक्रिय नोड सेट करना होगा जो इसे इंगित करता है। यदि कोई प्रत्यय लिंक नहीं है, तो सक्रिय नोड को रूट नोड पर सेट करें । किसी भी तरह से, सक्रिय बढ़त और सक्रिय लंबाई अपरिवर्तित रहती है।

इस परिभाषा में Rule 3हम लीफ नोड्स (केवल स्प्लिट-नोड्स) के आवेषण पर भी विचार करते हैं।

और अंत में, अवलोकन 3:

जब हम जिस पेड़ से जोड़ना चाहते हैं वह प्रतीक पहले से ही किनारे पर है, तो, हम Observation 1केवल अपडेट करते हैं , active pointऔर remainderपेड़ को अपरिवर्तित छोड़ देते हैं। लेकिन अगर वहाँ एक आंतरिक नोड को प्रत्यय लिंक के रूप में चिह्नित किया गया है , तो हमें active nodeएक प्रत्यय लिंक के माध्यम से उस नोड को अपने वर्तमान से जोड़ना होगा।

आइए cdddcdc के लिए एक प्रत्यय के पेड़ के उदाहरण को देखें अगर हम ऐसे मामले में प्रत्यय जोड़ते हैं और यदि हम नहीं करते हैं:

  1. यदि हम प्रत्यय लिंक के माध्यम से नोड्स कनेक्ट नहीं करते हैं :

    • अंतिम पत्र जोड़ने से पहले c :

    • अंतिम अक्षर c जोड़ने के बाद :

  2. यदि हम प्रत्यय लिंक के माध्यम से नोड्स कनेक्ट करते हैं:

    • अंतिम पत्र जोड़ने से पहले c :

    • अंतिम अक्षर c जोड़ने के बाद :

ऐसा लगता है कि कोई महत्वपूर्ण अंतर नहीं है: दूसरे मामले में दो और अधिक प्रत्यय लिंक हैं। लेकिन ये प्रत्यय लिंक सही हैं , और उनमें से एक - नीले नोड से लाल एक तक - सक्रिय बिंदु के साथ हमारे दृष्टिकोण के लिए बहुत महत्वपूर्ण है । समस्या यह है कि अगर हम यहां एक प्रत्यय लिंक नहीं डालते हैं, तो बाद में, जब हम पेड़ में कुछ नए अक्षर जोड़ते हैं, तो हम पेड़ के कारण कुछ नोड्स को जोड़ने में चूक कर सकते हैं , क्योंकि, इसके अनुसार, यदि कोई नहीं है प्रत्यय लिंक, तो हमें रूट पर रखना होगा ।Rule 3active_node

जब हम पेड़ से आखिरी अक्षर जोड़ रहे थे, तो लाल नोड पहले से ही मौजूद था जब हमने नीले नोड (किनारे से जुड़े 'सी' ) से एक सम्मिलित किया । जैसा कि नीले नोड से एक सम्मिलित था, हम इसे एक प्रत्यय लिंक की आवश्यकता के रूप में चिह्नित करते हैं । फिर, सक्रिय बिंदु दृष्टिकोण पर भरोसा करते हुए , active nodeलाल नोड पर सेट किया गया था। लेकिन हम लाल नोड से एक सम्मिलित नहीं करते हैं, क्योंकि पत्र 'सी' पहले से ही किनारे पर है। क्या इसका मतलब है कि नीले नोड को प्रत्यय लिंक के बिना छोड़ दिया जाना चाहिए? नहीं, हमें प्रत्यय लिंक के माध्यम से नीले नोड को लाल के साथ जोड़ना होगा। क्यों सही है? क्योंकि सक्रिय बिंदुदृष्टिकोण की गारंटी है कि हम एक सही जगह पर पहुंचते हैं, अर्थात, अगले स्थान पर जहां हमें एक छोटे प्रत्यय के डालने की प्रक्रिया करनी चाहिए ।

अंत में, यहाँ मेरे प्रत्यय ट्री के कार्यान्वयन हैं:

  1. जावा
  2. सी ++

आशा है कि जोगोजापान के विस्तृत जवाब के साथ संयुक्त "अवलोकन" से किसी को अपने स्वयं के सफ़िक्स ट्री को लागू करने में मदद मिलेगी।


3
बहुत बहुत धन्यवाद और आप प्रयास के लिए +1। मुझे यकीन है कि आप सही हैं .. हालाँकि मेरे पास अभी विवरण के बारे में सोचने का समय नहीं है। मैं बाद में जाँच करूँगा और संभवतः अपना उत्तर भी संशोधित करूँगा।
जोगोजपन

बहुत बहुत धन्यवाद, यह वास्तव में मदद की। हालाँकि, क्या आप अवलोकन ३ पर अधिक विशिष्ट हो सकते हैं? उदाहरण के लिए, नए प्रत्यय लिंक को पेश करने वाले 2 चरणों के आरेख देना। क्या नोड सक्रिय नोड से जुड़ा हुआ है? (जैसा कि हम वास्तव में दूसरा नोड नहीं
डालते हैं

@makagonov अरे क्या आप मुझे अपनी स्ट्रिंग "cdddcdc" के लिए प्रत्यय का पेड़ बनाने में मदद कर सकते हैं मैं ऐसा करने में थोड़ा उलझन में हूं (शुरुआती चरण)।
तारिक ज़फ़र

3
नियम 3 के अनुसार, एक स्मार्ट तरीका रूट की प्रत्यय लिंक को रूट करने के लिए सेट करना है, और (डिफ़ॉल्ट रूप से) प्रत्येक नोड के रूट का प्रत्यय लिंक सेट करें। इस प्रकार हम कंडीशनिंग से बच सकते हैं और बस प्रत्यय लिंक का अनुसरण कर सकते हैं।
sqd

1
aabaacaadउन मामलों में से एक है जो अतिरिक्त प्रत्यय लिंक जोड़ने से पता चलता है कि यह ट्रिपल को अपडेट करने के समय को कम कर सकता है। जोगोजापान के पद के अंतिम दो पैराग्राफ में निष्कर्ष गलत है। यदि हम इस पोस्ट में दिए गए प्रत्यय लिंक को नहीं जोड़ते हैं, तो औसत समय जटिलता O (nlong (n)) या अधिक होनी चाहिए। क्योंकि सही को खोजने के लिए पेड़ को चलने में अतिरिक्त समय लगता है active_node
इवानागिरो

10

@Jogojapan द्वारा अच्छी तरह से समझाया ट्यूटोरियल के लिए धन्यवाद , मैंने पायथन में एल्गोरिथ्म को लागू किया।

@ जोगोजापान द्वारा बताई गई कुछ छोटी-छोटी समस्याएं मेरे अनुमान से कहीं अधिक परिष्कृत हैं , और बहुत सावधानी से व्यवहार किए जाने की आवश्यकता है। मेरे कार्यान्वयन को पर्याप्त रूप से मजबूत करने के लिए मुझे कई दिनों तक खर्च करना पड़ा (मुझे लगता है)। समस्याओं और समाधान नीचे सूचीबद्ध हैं:

  1. अंत के साथRemainder > 0 यह पता चलता है कि यह स्थिति अनफॉल्डिंग चरण के दौरान भी हो सकती है , न कि पूरे एल्गोरिथ्म के अंत में। जब ऐसा होता है, तो हम शेष को छोड़ सकते हैं, एक्टनोड, एक्टेज, और एक्टलेंग्थ को अपरिवर्तित कर सकते हैं , मौजूदा खुलासा कदम को समाप्त कर सकते हैं, और या तो एक और कदम शुरू कर सकते हैं या तो तह या अनफ्लो रख सकते हैं, जो इस बात पर निर्भर करता है कि मूल स्ट्रिंग में अगला चार्ट चालू पथ पर है या नहीं।

  2. लीप ओवर नोड्स: जब हम एक प्रत्यय लिंक का पालन करते हैं, तो सक्रिय बिंदु को अपडेट करें, और फिर पता लगाएं कि इसका सक्रिय_लगावेंट घटक नए सक्रिय_नोड के साथ अच्छी तरह से काम नहीं करता है। हमें एक पत्ती को विभाजित करने, या डालने के लिए सही जगह पर आगे बढ़ना है। यह प्रक्रिया शायद सीधी नहीं हो सकती है क्योंकि गति के दौरान एक्टलेंग्थ और एक्टेज सभी तरह से बदलते रहते हैं, जब आपको रूट नोड में वापस जाना होता है , तो एक्टेज और एक्टलेंग्थ उन चालों के कारण गलत हो सकता है । हमें उस जानकारी को रखने के लिए अतिरिक्त चर की आवश्यकता है।

    यहां छवि विवरण दर्ज करें

अन्य दो समस्याओं को किसी तरह @managonov ने इंगित किया है

  1. स्प्लिट डिगनेटेरेट कर सकता है जब एक किनारे को विभाजित करने की कोशिश कर रहा है, तो कभी-कभी आप पाएंगे कि विभाजन ऑपरेशन एक नोड पर सही है। उस मामले में हमें केवल उस नोड में एक नया पत्ता जोड़ने की जरूरत है, इसे एक मानक एज स्प्लिट ऑपरेशन के रूप में लें, जिसका अर्थ है कि यदि कोई है तो प्रत्यय लिंक, इसी तरह बनाए रखा जाना चाहिए।

  2. छिपे हुए प्रत्यय लिंक एक और विशेष मामला है जो समस्या 1 और समस्या 2 से उत्पन्न होता है । कभी-कभी हमें विभाजन के लिए सही बिंदु पर कई नोड्स पर हॉप करने की आवश्यकता होती है, यदि हम शेष स्ट्रिंग और पथ लेबल की तुलना करके स्थानांतरित करते हैं, तो हम सही बिंदु को पार कर सकते हैं। यदि कोई ऐसा होना चाहिए, तो उस मामले की प्रत्यय लिंक को अनायास ही उपेक्षित कर दिया जाएगा। आगे बढ़ने पर सही बिंदु को याद करके इससे बचा जा सकता था। प्रत्यय लिंक बनाए रखा जाना चाहिए यदि विभाजित नोड पहले से मौजूद है, या यहां तक ​​कि समस्या 1 अनफॉलो कदम के दौरान भी होती है।

अंत में, पायथन में मेरा कार्यान्वयन इस प्रकार है:

टिप्स: इसमें ऊपर के कोड में एक भोले वृक्ष का मुद्रण कार्य शामिल है , जो डिबगिंग करते समय बहुत महत्वपूर्ण है । इसने मुझे बहुत समय बचाया और विशेष मामलों का पता लगाने के लिए सुविधाजनक है।


10

माफ़ी अगर मेरा जवाब बेमानी लगता है, लेकिन मैंने हाल ही में उकोकोन के एल्गोरिथ्म को लागू किया, और खुद को दिनों के लिए संघर्ष कर पाया; एल्गोरिथ्म के कुछ मुख्य पहलुओं को क्यों और कैसे समझा जाए, इस विषय पर मुझे कई पत्रों के माध्यम से पढ़ना था।

मुझे अंतर्निहित कारणों को समझने के लिए पिछले जवाबों के 'नियम' दृष्टिकोण का पता चला , इसलिए मैंने व्यावहारिक रूप से पूरी तरह से ध्यान केंद्रित करने के नीचे सब कुछ लिखा है। यदि आपने अन्य स्पष्टीकरणों के साथ संघर्ष किया है, जैसे मैंने किया, तो शायद मेरी पूरक व्याख्या आपके लिए इसे 'क्लिक' कर देगी।

मैंने अपना C # कार्यान्वयन यहाँ प्रकाशित किया: https://github.com/baratgabor/SuffixTree

कृपया ध्यान दें कि मैं इस विषय का विशेषज्ञ नहीं हूं, इसलिए निम्नलिखित अनुभागों में अशुद्धियाँ (या बदतर) हो सकती हैं। यदि आप किसी भी मुठभेड़, संपादित करने के लिए स्वतंत्र महसूस हो रहा है।

आवश्यक शर्तें

निम्नलिखित विवरण का शुरुआती बिंदु मानता है कि आप प्रत्यय वृक्षों की सामग्री और उपयोग से परिचित हैं, और उकोकोन के एल्गोरिथ्म की विशेषताओं, जैसे कि आप वर्ण से प्रत्यय वृक्ष के चरित्र को कैसे शुरू कर रहे हैं, शुरू से अंत तक। असल में, मुझे लगता है कि आप पहले से ही कुछ अन्य स्पष्टीकरण पढ़ चुके हैं।

(हालांकि, मुझे प्रवाह के लिए कुछ मूल आख्यान जोड़ना था, इसलिए शुरुआत वास्तव में बेमानी लग सकती है।)

सबसे दिलचस्प हिस्सा प्रत्यय लिंक का उपयोग करने और रूट से पुनरुत्थान के बीच अंतर पर स्पष्टीकरण है । यह वही है जिसने मुझे अपने कार्यान्वयन में बहुत सारे कीड़े और सिरदर्द दिए हैं।

ओपन-एंडेड लीफ नोड्स और उनकी सीमाएं

मुझे यकीन है कि आप पहले से ही जानते हैं कि सबसे मौलिक 'ट्रिक' यह महसूस करना है कि हम प्रत्ययों के अंत को 'खुला' छोड़ सकते हैं, यानी अंत को स्थैतिक मूल्य पर सेट करने के बजाय स्ट्रिंग की वर्तमान लंबाई को संदर्भित कर सकते हैं। इस तरह जब हम अतिरिक्त वर्ण जोड़ते हैं, तो उन पात्रों को सभी प्रत्यय लेबल में जोड़ दिया जाएगा, बिना उन सभी को देखे और अपडेट किए बिना।

लेकिन प्रत्ययों का यह खुला अंत - स्पष्ट कारणों के लिए - केवल नोड्स के लिए काम करता है जो स्ट्रिंग के अंत का प्रतिनिधित्व करता है, अर्थात पेड़ की संरचना में पत्ती नोड्स। पेड़ (नई शाखा नोड्स और पत्ती नोड्स के अलावा) पर हम जिस ब्रांचिंग ऑपरेशन को अंजाम देते हैं, वह हर उस जगह पर अपने आप नहीं फैलता, जिसकी उन्हें जरूरत होती है।

यह शायद प्राथमिक है, और उल्लेख की आवश्यकता नहीं होगी, कि बार-बार होने वाले पदार्थ पेड़ में स्पष्ट रूप से दिखाई नहीं देते हैं, क्योंकि पेड़ में पहले से ही इनमें पुनरावृत्ति होने का गुण होता है; हालाँकि, जब दोहराए जाने वाले विकल्प एक गैर-दोहराए जाने वाले चरित्र का सामना करके समाप्त होते हैं, तो हमें उस बिंदु से विचलन का प्रतिनिधित्व करने के लिए उस बिंदु पर एक शाखा बनाना होगा।

उदाहरण के लिए, स्ट्रिंग 'एबीसीएक्सएबीवाईवाई' (नीचे देखें) के मामले में, एक्स और वाई के लिए एक शाखा में तीन अलग-अलग प्रत्ययों, एबीसी , बीसी और सी को जोड़ने की आवश्यकता होती है ; अन्यथा यह एक मान्य प्रत्यय पेड़ नहीं होगा, और हम स्ट्रिंग के सभी सबस्ट्रिंग को रूट से नीचे की ओर वर्णों का मिलान नहीं कर सकते हैं।

एक बार फिर, जोर देने के लिए - पेड़ में एक प्रत्यय पर किए गए किसी भी ऑपरेशन को उसके लगातार प्रत्ययों (जैसे एबीसी> बीसी> सी) द्वारा परिलक्षित किया जाना चाहिए, अन्यथा वे केवल वैध प्रत्यय होने का दावा करते हैं।

प्रत्ययों में बार-बार शाखा लगाना

लेकिन फिर भी अगर हम स्वीकार करते हैं कि हमें ये मैनुअल अपडेट करने हैं, तो हमें कैसे पता चलेगा कि कितने प्रत्ययों को अपडेट करने की आवश्यकता है? चूंकि, जब हम दोहराए गए वर्ण A (और उत्तराधिकार में बाकी दोहराए गए वर्ण) को जोड़ते हैं, तो हमें अभी तक पता नहीं है कि कब / कहां हमें प्रत्यय को दो शाखाओं में विभाजित करने की आवश्यकता है। विभाजित करने की आवश्यकता केवल तब पता चलती है जब हम पहले गैर-दोहराए जाने वाले चरित्र का सामना करते हैं, इस मामले में वाई ( एक्स के बजाय जो पहले से ही पेड़ में मौजूद है)।

हम जो कर सकते हैं वह है सबसे लंबे समय तक दोहराए जाने वाले स्ट्रिंग का मिलान करना, और गिनना कि इसके कितने प्रत्ययों को हमें बाद में अपडेट करने की आवश्यकता है। यही 'शेष' है।

'शेष' और 'पुनरुत्थान' की अवधारणा

चर remainderहमें बताता है कि हमने कितने दोहराया वर्णों को जोड़ दिया, बिना शाखा के; एक बार जब हमें पहला चरित्र नहीं मिला जिसे हम मैच नहीं कर सकते हैं, तो कितने ब्रिक्स ऑपरेशन को दोहराने के लिए हमें कितने प्रत्ययों की आवश्यकता होगी। यह अनिवार्य रूप से इसके मूल से पेड़ में कितने वर्ण 'गहरे' के बराबर है।

इसलिए, स्ट्रिंग एबीसीएक्सएबीसीवाई के पिछले उदाहरण के साथ रहकर , हम प्रत्येक बार बढ़ने वाले दोहराए गए एबीसी भाग 'अंतर्निहित' से मेल खाते हैं remainder, जिसके परिणामस्वरूप 3. शेष है और फिर हम गैर-दोहराए जाने वाले चरित्र 'वाई' का सामना करते हैं । यहां हमने पहले जोड़े गए ABCX को ABC -> X और ABC -> Y में विभाजित किया है । फिर हम remainder3 से 2 तक की कमी करते हैं , क्योंकि हम पहले से ही एबीसी ब्रांचिंग का ध्यान रखते हैं। अब हम अंतिम 2 अक्षरों - बीसी - को मूल से उस बिंदु तक पहुंचाने के लिए ऑपरेशन को दोहराते हैं, जहां हमें विभाजित होने की आवश्यकता होती है, और हम बीसीएक्स को भी बीसी में विभाजित करते हैं।-> एक्स और ईसा पूर्व -> वाई । फिर से, हम remainder1 में कमी करते हैं, और ऑपरेशन को दोहराते हैं; जब तक remainder0. नहीं है, तब तक , हमें वर्तमान वर्ण ( Y ) को मूल रूप में ही जोड़ना होगा ।

यह ऑपरेशन, रूट से लगातार प्रत्ययों के बाद केवल उस बिंदु तक पहुंचने के लिए जहां हमें एक ऑपरेशन करने की आवश्यकता होती है जिसे उकोकोन के एल्गोरिदम में 'रिसकेनिंग' कहा जाता है, और आमतौर पर यह एल्गोरिथ्म का सबसे महंगा हिस्सा है। एक लंबी स्ट्रिंग की कल्पना करें, जहां आपको कई दर्जनों नोड्स (हम बाद में इस पर चर्चा करेंगे) में संभावित रूप से लंबे समय तक सब्सट्रिंग करने की आवश्यकता होती है।

समाधान के रूप में, हम परिचय देते हैं जिसे हम 'प्रत्यय लिंक' कहते हैं

'प्रत्यय लिंक' की अवधारणा

प्रत्यय लिंक मूल रूप से उन पदों की ओर इशारा करते हैं जिन्हें हम सामान्य रूप से 'rescan' करना चाहते हैं, इसलिए महंगे rescan ऑपरेशन के बजाय हम बस लिंक की स्थिति में जा सकते हैं, अपना काम कर सकते हैं, अगली लिंक की गई स्थिति पर जा सकते हैं, और दोहरा सकते हैं - जब तक कि अपडेट करने के लिए कोई और स्थिति नहीं है।

बेशक एक बड़ा सवाल यह है कि इन कड़ियों को कैसे जोड़ा जाए। मौजूदा उत्तर यह है कि जब हम नई शाखा नोड्स डालते हैं तो हम लिंक जोड़ सकते हैं, इस तथ्य का उपयोग करते हुए कि, पेड़ के प्रत्येक विस्तार में, शाखा नोड्स स्वाभाविक रूप से एक के बाद एक सटीक क्रम में बनाए जाते हैं, जिन्हें हमें एक साथ जोड़ना होगा। । हालाँकि, हमें पिछली बनाई गई शाखा नोड (सबसे लंबे समय तक प्रत्यय) से पहले के बनाए गए लिंक से लिंक करना होगा, इसलिए हमें जो अंतिम बनाना है उसे कैश करना होगा, जो हम बनाएंगे उसे अगले लिंक करें, और नए बनाए गए कैश को कैश करें।

एक परिणाम यह है कि हम वास्तव में अक्सर लिंक का अनुसरण करने के लिए प्रत्यय नहीं रखते हैं, क्योंकि दिए गए शाखा नोड को अभी बनाया गया था। इन मामलों में हमें अभी भी जड़ से 'पुनर्जीवन' की ओर जाना है। यही कारण है कि, एक प्रविष्टि के बाद, आपको या तो प्रत्यय लिंक का उपयोग करने का निर्देश दिया जाता है, या रूट करने के लिए कूदते हैं।

(या वैकल्पिक रूप से, यदि आप मूल बिंदुओं को नोड्स में संग्रहीत कर रहे हैं, तो आप माता-पिता का अनुसरण करने की कोशिश कर सकते हैं, जांचें कि क्या उनके पास लिंक है, और इसका उपयोग करें। मैंने पाया कि यह बहुत ही कम उल्लेख किया गया है, लेकिन प्रत्यय लिंक का उपयोग नहीं है। पत्थरों में सेट करें। कई संभावित दृष्टिकोण हैं, और यदि आप अंतर्निहित तंत्र को समझते हैं तो आप एक को लागू कर सकते हैं जो आपकी आवश्यकताओं को सबसे अच्छा लगता है।)

'सक्रिय बिंदु' की अवधारणा

अब तक हमने पेड़ के निर्माण के लिए कई कुशल उपकरणों पर चर्चा की, और अस्पष्ट रूप से कई किनारों और नोड्स पर ट्रैवर्सिंग का उल्लेख किया, लेकिन अभी तक संबंधित परिणामों और जटिलताओं का पता नहीं लगाया है।

'शेष' की पहले से बताई गई अवधारणा ट्रैक रखने के लिए उपयोगी है जहां हम पेड़ में हैं, लेकिन हमें यह महसूस करना होगा कि यह पर्याप्त जानकारी संग्रहीत नहीं करता है।

सबसे पहले, हम हमेशा नोड के एक विशिष्ट किनारे पर रहते हैं, इसलिए हमें किनारे की जानकारी संग्रहीत करने की आवश्यकता है। हम इसे 'सक्रिय बढ़त' कहेंगे ।

दूसरे, किनारे की जानकारी जोड़ने के बाद भी, हमारे पास अभी भी ऐसी स्थिति की पहचान करने का कोई तरीका नहीं है जो पेड़ में नीचे की ओर है, और सीधे रूट नोड से जुड़ा नहीं है। इसलिए हमें नोड को भी स्टोर करना होगा। इसे 'सक्रिय नोड' कहते हैं

अंत में, हम देख सकते हैं कि 'शेष' एक किनारे पर स्थिति की पहचान करने के लिए अपर्याप्त है जो सीधे रूट से जुड़ा नहीं है, क्योंकि 'शेष' पूरे मार्ग की लंबाई है; और हम शायद पिछले किनारों की लंबाई को याद रखने और घटाने के साथ परेशान नहीं करना चाहते हैं। इसलिए हमें एक प्रतिनिधित्व की आवश्यकता है जो मूल रूप से वर्तमान किनारे पर शेष है । इसे ही हम 'सक्रिय लंबाई' कहते हैं

यह उस चीज़ की ओर ले जाता है जिसे हम 'सक्रिय बिंदु' कहते हैं - तीन वैरिएबल्स का एक पैकेज जिसमें वे सभी जानकारी होती हैं जिनकी हमें पेड़ में अपनी स्थिति बनाए रखने की आवश्यकता होती है:

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

आप निम्न चित्र पर देख सकते हैं कि ABCABD के मिलान वाले मार्ग में किनारे पर 2 अक्षर AB ( रूट से ), प्लस 4 अक्षर CABDABCABD (नोड 4 से) - 6 वर्णों के 'शेष' के परिणामस्वरूप हैं । तो, हमारी वर्तमान स्थिति को सक्रिय नोड 4, सक्रिय एज सी, सक्रिय लंबाई 4 के रूप में पहचाना जा सकता है ।

शेष और सक्रिय बिंदु

'सक्रिय बिंदु' की एक अन्य महत्वपूर्ण भूमिका यह है कि यह हमारे एल्गोरिथ्म के लिए एक अमूर्त परत प्रदान करता है, जिसका अर्थ है कि हमारे एल्गोरिथ्म के कुछ हिस्सों को 'सक्रिय बिंदु' पर अपना काम कर सकते हैं , चाहे वह सक्रिय बिंदु जड़ में हो या कहीं और हो । यह हमारे एल्गोरिथ्म में स्वच्छ और सीधे-आगे तरीके से प्रत्यय लिंक के उपयोग को लागू करना आसान बनाता है।

प्रत्यय के अंतर बनाम प्रत्यय लिंक का उपयोग करना

अब, मुश्किल हिस्सा, कुछ ऐसा जो - मेरे अनुभव में - बहुत सारे कीड़े और सिरदर्द पैदा कर सकता है, और ज्यादातर स्रोतों में खराब तरीके से समझाया गया है, प्रत्यय लिंक मामलों बनाम रेसकान मामलों के प्रसंस्करण में अंतर है।

स्ट्रिंग 'AAAABAAAABAAC' के निम्नलिखित उदाहरण पर विचार करें :

कई किनारों पर अवशेष

आप ऊपर देख सकते हैं कि 7 का 'शेष' कैसे रूट से वर्णों के कुल योग से मेल खाता है, जबकि 4 की 'सक्रिय लंबाई' सक्रिय नोड के सक्रिय किनारे से मिलान किए गए वर्णों के योग से मेल खाती है।

अब, सक्रिय बिंदु पर एक ब्रांचिंग ऑपरेशन को अंजाम देने के बाद, हमारे सक्रिय नोड में एक प्रत्यय लिंक नहीं हो सकता है।

यदि कोई प्रत्यय लिंक मौजूद है: हमें केवल 'सक्रिय लंबाई' भाग को संसाधित करने की आवश्यकता है । 'शेष' क्योंकि, अप्रासंगिक है नोड जहाँ हम प्रत्यय लिंक के माध्यम से करने के लिए कूद पहले से ही सही 'शेष' परोक्ष encodes , बस पेड़ यह वह जगह है जहाँ में किया जा रहा के आधार पर।

यदि कोई प्रत्यय लिंक मौजूद नहीं है: हमें शून्य / रूट से 'rescan' करने की आवश्यकता है , जिसका अर्थ है कि शुरुआत से पूरे प्रत्यय को संसाधित करना। इसके अंत तक हमें पूरे 'शेष' का उपयोग पुनरुत्थान के आधार के रूप में करना होगा।

प्रत्यय लिंक के साथ और बिना प्रसंस्करण की उदाहरण तुलना

ऊपर दिए गए उदाहरण के अगले चरण में क्या होता है, इस पर विचार करें। आइए तुलना करते हैं कि एक ही परिणाम कैसे प्राप्त करें - यानी अगले प्रत्यय के लिए आगे बढ़ना - एक प्रत्यय लिंक के साथ और बिना।

'प्रत्यय लिंक' का उपयोग करना

प्रत्यय लिंक के माध्यम से लगातार प्रत्ययों तक पहुंचना

ध्यान दें कि यदि हम एक प्रत्यय लिंक का उपयोग करते हैं, तो हम स्वचालित रूप से 'सही जगह पर' हैं। जो अक्सर इस तथ्य के कारण कड़ाई से सच नहीं है कि नई स्थिति के साथ 'सक्रिय लंबाई' 'असंगत' हो सकती है।

उपरोक्त मामले में, चूंकि 'सक्रिय लंबाई' 4 है, हम प्रत्यय ' ABAA' के साथ काम कर रहे हैं , जो लिंक किए गए नोड 4 से शुरू होता है। लेकिन यह पता लगाने के बाद कि प्रत्यय के पहले वर्ण से मेल खाता है ( 'ए') ), हम देखते हैं कि हमारी 'सक्रिय लंबाई' इस बढ़त को 3 वर्णों से अधिक है। तो हम अगले किनारे पर, अगले नोड तक, और छलांग के साथ सेवन किए गए पात्रों द्वारा 'सक्रिय लंबाई' घटाते हैं।

इसके बाद, जब हमें अगली बढ़त 'बी' मिली, तो घटे हुए प्रत्यय 'बीएए ' से संबंधित, हम अंत में ध्यान दें कि किनारे की लंबाई 3 की शेष 'सक्रिय लंबाई' से बड़ी है , जिसका मतलब है कि हमें सही जगह मिली है।

कृपया ध्यान दें कि ऐसा लगता है कि इस ऑपरेशन को आमतौर पर 'रिसकेनिंग' के रूप में संदर्भित नहीं किया जाता है, भले ही मुझे लगता है कि यह rescanning के सीधे बराबर है, बस एक छोटी लंबाई और एक गैर-रूट शुरुआती बिंदु के साथ।

'Rescan' का प्रयोग

पुनरावर्तन के माध्यम से लगातार प्रत्ययों तक पहुंचना

ध्यान दें कि यदि हम एक पारंपरिक 'रेस्कैन' ऑपरेशन का उपयोग करते हैं (यहाँ दिखावा किया गया है कि हमारा प्रत्यय लिंक नहीं है), हम पेड़ के शीर्ष पर, रूट पर शुरू करते हैं, और हमें अपना रास्ता फिर से सही जगह पर काम करना होगा, वर्तमान प्रत्यय की संपूर्ण लंबाई के साथ।

इस प्रत्यय की लंबाई 'शेष' है जिसकी हमने पहले चर्चा की थी। हमें इस शेष की संपूर्णता का उपभोग करना होगा, जब तक कि यह शून्य तक न पहुंच जाए। यह (और अक्सर करता है) कई नोड्स के माध्यम से कूदना शामिल है, प्रत्येक कूद में हम जिस किनारे से कूदते हैं उसकी लंबाई तक शेष घट जाती है। फिर अंत में, हम एक छोर पर पहुंचते हैं जो हमारे शेष 'शेष' से लंबा है ; यहाँ हम दिए गए किनारे पर सक्रिय बढ़त सेट करते हैं, शेष 'शेष ' के लिए 'सक्रिय लंबाई' सेट करते हैं, और हम कर रहे हैं।

हालांकि, ध्यान दें कि वास्तविक 'शेष' चर को संरक्षित करने की आवश्यकता है, और प्रत्येक नोड प्रविष्टि के बाद केवल डीक्रिएट किया गया है। इसलिए मैंने ऊपर वर्णित एक अलग चर का उपयोग करते हुए मान लिया कि 'शेष'

प्रत्यय लिंक और बचाव पर नोट्स

1) ध्यान दें कि दोनों विधियाँ समान परिणाम की ओर ले जाती हैं। हालांकि, प्रत्यय लिंक जंपिंग ज्यादातर मामलों में काफी तेज है; प्रत्यय लिंक के पीछे पूरा तर्क है।

2) वास्तविक एल्गोरिथम कार्यान्वयन को अलग करने की आवश्यकता नहीं है। जैसा कि मैंने ऊपर उल्लेख किया है, यहां तक ​​कि प्रत्यय लिंक का उपयोग करने के मामले में, 'सक्रिय लंबाई' अक्सर लिंक की गई स्थिति के साथ संगत नहीं है, क्योंकि पेड़ की उस शाखा में अतिरिक्त शाखाएं हो सकती हैं। तो अनिवार्य रूप से आपको बस 'शेष ' के बजाय 'सक्रिय लंबाई' का उपयोग करना होगा , और उसी पुनरुत्थान तर्क को निष्पादित करना होगा जब तक कि आपको एक बढ़त नहीं मिलती जो आपकी शेष प्रत्यय लंबाई से कम है।

3) प्रदर्शन से संबंधित एक महत्वपूर्ण टिप्पणी यह ​​है कि पुनरुत्थान के दौरान प्रत्येक चरित्र की जांच करने की कोई आवश्यकता नहीं है। जिस तरह से एक वैध प्रत्यय पेड़ बनाया गया है, उसके कारण हम सुरक्षित रूप से मान सकते हैं कि अक्षर मेल खाते हैं। इसलिए आप ज्यादातर लंबाई की गिनती कर रहे हैं, और चरित्र समतुल्यता जाँच की आवश्यकता तब होती है जब हम एक नए किनारे पर जाते हैं, क्योंकि किनारों की पहचान उनके पहले चरित्र द्वारा की जाती है (जो किसी दिए गए नोड के संदर्भ में हमेशा अद्वितीय होता है)। इसका मतलब यह है कि 'रिसकेनिंग' तर्क पूर्ण स्ट्रिंग मिलान तर्क (यानी पेड़ में एक विकल्प के लिए खोज) से अलग है।

4) यहाँ वर्णित मूल प्रत्यय लिंकिंग संभव दृष्टिकोणों में से एक है । उदाहरण के लिए एनजे लार्सन एट अल। इस दृष्टिकोण को नोड-ओरिएंटेड टॉप-डाउन के नाम से जाना जाता है , और इसकी तुलना नोड-ओरिएंटेड बॉटम-अप और दो एज-ओरिएंटेड किस्मों से की जाती है। अलग-अलग दृष्टिकोणों में अलग-अलग विशिष्ट और सबसे खराब स्थिति प्रदर्शन, आवश्यकताएं, सीमाएं आदि हैं, लेकिन आमतौर पर ऐसा लगता है कि एज-ओरिएंटेड दृष्टिकोण मूल में एक समग्र सुधार है।


8

@ जोगोजपन आप भयानक व्याख्या और दृश्य लाए। लेकिन जैसा @magonagonov ने उल्लेख किया है कि यह प्रत्यय लिंक स्थापित करने के बारे में कुछ नियमों को याद कर रहा है। Http://brenden.github.io/ukkonen-animation/ पर 'aabaaabb' शब्द के जरिए कदम से कदम मिलाते हुए यह अच्छे तरीके से दिखाई दे रहा है । जब आप चरण 10 से चरण 11 तक जाते हैं, तो नोड 5 से नोड 2 तक कोई प्रत्यय लिंक नहीं होता है, लेकिन सक्रिय बिंदु अचानक वहां आ जाता है।

@makagonov जब से मैं जावा दुनिया में रहता हूं मैंने भी एसटी बिल्डिंग वर्कफ़्लो को समझने के लिए आपके कार्यान्वयन का पालन करने की कोशिश की, लेकिन यह मेरे लिए कठिन था:

  • नोड्स के साथ किनारों का संयोजन
  • संदर्भ के बजाय इंडेक्स पॉइंटर्स का उपयोग करना
  • बयानों को तोड़ता है;
  • बयान जारी रखें;

इसलिए मैंने जावा में ऐसे कार्यान्वयन को समाप्त कर दिया है, जो मुझे उम्मीद है कि सभी चरणों को स्पष्ट तरीके से दर्शाएगा और अन्य जावा लोगों के लिए सीखने का समय कम कर देगा:

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 पुनरावृत्तियों के बाद आपने एक प्रत्यय वृक्ष का निर्माण किया है जिसमें पूर्ण स्ट्रिंग के सभी प्रत्यय हैं जो पहले वर्णों में शुरू होते हैं।

शुरुआत में, इसका मतलब है कि प्रत्यय के पेड़ में एक एकल रूट नोड होता है जो पूरे स्ट्रिंग का प्रतिनिधित्व करता है (यह एकमात्र प्रत्यय है जो 0 से शुरू होता है)।

लेन (स्ट्रिंग) पुनरावृत्तियों के बाद आपके पास एक प्रत्यय वृक्ष होता है जिसमें सभी प्रत्यय होते हैं।

लूप के दौरान कुंजी सक्रिय बिंदु है। मेरा अनुमान है कि यह प्रत्यय वृक्ष में सबसे गहरे बिंदु का प्रतिनिधित्व करता है जो स्ट्रिंग के पहले k अक्षर के उचित प्रत्यय से मेल खाता है। (मुझे लगता है कि उचित का मतलब है कि प्रत्यय संपूर्ण स्ट्रिंग नहीं हो सकता है।)

उदाहरण के लिए, मान लीजिए कि आपने पात्रों को 'एबीसीबी' देखा है। सक्रिय बिंदु प्रत्यय 'एबीसी' के अनुरूप पेड़ में बिंदु का प्रतिनिधित्व करेगा।

सक्रिय बिंदु (मूल, पहले, अंतिम) द्वारा दर्शाया गया है। इसका मतलब है कि आप वर्तमान में उस पेड़ के बिंदु पर हैं जो आपको नोड मूल पर शुरू करके मिलता है और फिर स्ट्रिंग में पात्रों को खिलाना [पहला: अंतिम]

जब आप एक नया वर्ण जोड़ते हैं तो आप यह देखना चाहते हैं कि क्या सक्रिय बिंदु अभी भी मौजूदा पेड़ में है। अगर ऐसा है तो आप कर रहे हैं। अन्यथा आपको सक्रिय बिंदु पर प्रत्यय के पेड़ पर एक नया नोड जोड़ने की जरूरत है, अगले कम से कम मैच में वापसी करें, और फिर से जांच करें।

नोट 1: प्रत्यय सूचक प्रत्येक नोड के लिए अगले सबसे छोटे मैच के लिए एक लिंक देते हैं।

नोट 2: जब आप एक नया नोड जोड़ते हैं और फ़ॉलबैक करते हैं तो आप नए नोड के लिए एक नया प्रत्यय सूचक जोड़ते हैं। इस प्रत्यय सूचक के लिए गंतव्य छोटा सक्रिय बिंदु पर नोड होगा। यह नोड या तो पहले से मौजूद है, या इस फ़ॉलबैक लूप के अगले पुनरावृत्ति पर बनाया जाएगा।

नोट 3: कैनोनेज़ेशन भाग सक्रिय बिंदु की जाँच में समय बचाता है। उदाहरण के लिए, मान लीजिए कि आपने हमेशा उत्पत्ति = 0 का उपयोग किया है, और बस पहले और अंतिम को बदल दिया है। सक्रिय बिंदु की जांच करने के लिए आपको हर बार सभी मध्यवर्ती नोड्स के साथ प्रत्यय के पेड़ का पालन करना होगा। यह अंतिम नोड से दूरी दर्ज करके इस पथ का अनुसरण करने के परिणाम को कैश करने के लिए समझ में आता है।

क्या आप "फिक्स" बाउंडिंग वैरिएबल का मतलब क्या है इसका एक कोड उदाहरण दे सकते हैं?

स्वास्थ्य चेतावनी: मुझे यह एल्गोरिदम विशेष रूप से समझने में कठिन लगा, इसलिए कृपया महसूस करें कि यह अंतर्ज्ञान सभी महत्वपूर्ण विवरणों में गलत होने की संभावना है ...


अकादमिक कागजात में से एक "उचित" को परिभाषित करता है, जिसका अर्थ है एक स्ट्रिंग के "उचित प्रत्यय" में इसका पहला चरित्र नहीं है। कभी-कभी आप एक पूरे विकल्प को "प्रत्यय" कहते हैं, लेकिन एल्गोरिथ्म को "स्ट्रिंग" और "सबस्ट्रिंग" शब्दों को परिभाषित करते समय और "प्रत्यय" को उदारतापूर्वक इधर-उधर फेंक दिया जाता है और कभी-कभी आपको "प्रत्यय" से बहुत स्पष्ट होने की आवश्यकता होती है, इसलिए शब्द "उचित प्रत्यय" पूरी बात को एक प्रत्यय कहते हैं। तो एक स्ट्रिंग का एक प्रत्यय किसी भी वैध विकल्प हो सकता है और एक उचित प्रत्यय हो सकता है जो एक ही प्रत्यय नहीं है। क्योंकि तर्क।
ब्लेयर ह्यूटन

3

हाय मैंने रूबी में ऊपर वर्णित कार्यान्वयन को लागू करने की कोशिश की है, कृपया इसे देखें। यह काम तो ठीक करता है।

कार्यान्वयन में एकमात्र अंतर यह है कि, मैंने केवल प्रतीकों का उपयोग करने के बजाय किनारे की वस्तु का उपयोग करने की कोशिश की है।

यह भी https://gist.github.com/suchitpuri/9304856 पर मौजूद है

    require 'pry'


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

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

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

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

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


            add_to_edges @root , element        

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

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

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

    def add_pending_elements_to_tree element

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



        @pending_prefixes.each_with_index do |pending_prefix , index|

            # binding.pry           

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

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

            else

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

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

                location = @active_point[:active_length]


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


                else #tree split    
                    split_edge data , index , element
                end

            end
        end 
    end



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

        end

        @pending_prefixes = []

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

        @pending_prefixes.reverse!

    end

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

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

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


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

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


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

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

        @last_split_edge = @active_point[:active_edge]

        update_active_point index

    end


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

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

suffix_tree = SuffixTrees.new
suffix_tree.build("abcabxabcd")
binding.pry
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.