माफ़ी अगर मेरा जवाब बेमानी लगता है, लेकिन मैंने हाल ही में उकोकोन के एल्गोरिथ्म को लागू किया, और खुद को दिनों के लिए संघर्ष कर पाया; एल्गोरिथ्म के कुछ मुख्य पहलुओं को क्यों और कैसे समझा जाए, इस विषय पर मुझे कई पत्रों के माध्यम से पढ़ना था।
मुझे अंतर्निहित कारणों को समझने के लिए पिछले जवाबों के 'नियम' दृष्टिकोण का पता चला , इसलिए मैंने व्यावहारिक रूप से पूरी तरह से ध्यान केंद्रित करने के नीचे सब कुछ लिखा है। यदि आपने अन्य स्पष्टीकरणों के साथ संघर्ष किया है, जैसे मैंने किया, तो शायद मेरी पूरक व्याख्या आपके लिए इसे 'क्लिक' कर देगी।
मैंने अपना C # कार्यान्वयन यहाँ प्रकाशित किया: https://github.com/baratgabor/SuffixTree
कृपया ध्यान दें कि मैं इस विषय का विशेषज्ञ नहीं हूं, इसलिए निम्नलिखित अनुभागों में अशुद्धियाँ (या बदतर) हो सकती हैं। यदि आप किसी भी मुठभेड़, संपादित करने के लिए स्वतंत्र महसूस हो रहा है।
आवश्यक शर्तें
निम्नलिखित विवरण का शुरुआती बिंदु मानता है कि आप प्रत्यय वृक्षों की सामग्री और उपयोग से परिचित हैं, और उकोकोन के एल्गोरिथ्म की विशेषताओं, जैसे कि आप वर्ण से प्रत्यय वृक्ष के चरित्र को कैसे शुरू कर रहे हैं, शुरू से अंत तक। असल में, मुझे लगता है कि आप पहले से ही कुछ अन्य स्पष्टीकरण पढ़ चुके हैं।
(हालांकि, मुझे प्रवाह के लिए कुछ मूल आख्यान जोड़ना था, इसलिए शुरुआत वास्तव में बेमानी लग सकती है।)
सबसे दिलचस्प हिस्सा प्रत्यय लिंक का उपयोग करने और रूट से पुनरुत्थान के बीच अंतर पर स्पष्टीकरण है । यह वही है जिसने मुझे अपने कार्यान्वयन में बहुत सारे कीड़े और सिरदर्द दिए हैं।
ओपन-एंडेड लीफ नोड्स और उनकी सीमाएं
मुझे यकीन है कि आप पहले से ही जानते हैं कि सबसे मौलिक 'ट्रिक' यह महसूस करना है कि हम प्रत्ययों के अंत को 'खुला' छोड़ सकते हैं, यानी अंत को स्थैतिक मूल्य पर सेट करने के बजाय स्ट्रिंग की वर्तमान लंबाई को संदर्भित कर सकते हैं। इस तरह जब हम अतिरिक्त वर्ण जोड़ते हैं, तो उन पात्रों को सभी प्रत्यय लेबल में जोड़ दिया जाएगा, बिना उन सभी को देखे और अपडेट किए बिना।
लेकिन प्रत्ययों का यह खुला अंत - स्पष्ट कारणों के लिए - केवल नोड्स के लिए काम करता है जो स्ट्रिंग के अंत का प्रतिनिधित्व करता है, अर्थात पेड़ की संरचना में पत्ती नोड्स। पेड़ (नई शाखा नोड्स और पत्ती नोड्स के अलावा) पर हम जिस ब्रांचिंग ऑपरेशन को अंजाम देते हैं, वह हर उस जगह पर अपने आप नहीं फैलता, जिसकी उन्हें जरूरत होती है।
यह शायद प्राथमिक है, और उल्लेख की आवश्यकता नहीं होगी, कि बार-बार होने वाले पदार्थ पेड़ में स्पष्ट रूप से दिखाई नहीं देते हैं, क्योंकि पेड़ में पहले से ही इनमें पुनरावृत्ति होने का गुण होता है; हालाँकि, जब दोहराए जाने वाले विकल्प एक गैर-दोहराए जाने वाले चरित्र का सामना करके समाप्त होते हैं, तो हमें उस बिंदु से विचलन का प्रतिनिधित्व करने के लिए उस बिंदु पर एक शाखा बनाना होगा।
उदाहरण के लिए, स्ट्रिंग 'एबीसीएक्सएबीवाईवाई' (नीचे देखें) के मामले में, एक्स और वाई के लिए एक शाखा में तीन अलग-अलग प्रत्ययों, एबीसी , बीसी और सी को जोड़ने की आवश्यकता होती है ; अन्यथा यह एक मान्य प्रत्यय पेड़ नहीं होगा, और हम स्ट्रिंग के सभी सबस्ट्रिंग को रूट से नीचे की ओर वर्णों का मिलान नहीं कर सकते हैं।
एक बार फिर, जोर देने के लिए - पेड़ में एक प्रत्यय पर किए गए किसी भी ऑपरेशन को उसके लगातार प्रत्ययों (जैसे एबीसी> बीसी> सी) द्वारा परिलक्षित किया जाना चाहिए, अन्यथा वे केवल वैध प्रत्यय होने का दावा करते हैं।
लेकिन फिर भी अगर हम स्वीकार करते हैं कि हमें ये मैनुअल अपडेट करने हैं, तो हमें कैसे पता चलेगा कि कितने प्रत्ययों को अपडेट करने की आवश्यकता है? चूंकि, जब हम दोहराए गए वर्ण A (और उत्तराधिकार में बाकी दोहराए गए वर्ण) को जोड़ते हैं, तो हमें अभी तक पता नहीं है कि कब / कहां हमें प्रत्यय को दो शाखाओं में विभाजित करने की आवश्यकता है। विभाजित करने की आवश्यकता केवल तब पता चलती है जब हम पहले गैर-दोहराए जाने वाले चरित्र का सामना करते हैं, इस मामले में वाई ( एक्स के बजाय जो पहले से ही पेड़ में मौजूद है)।
हम जो कर सकते हैं वह है सबसे लंबे समय तक दोहराए जाने वाले स्ट्रिंग का मिलान करना, और गिनना कि इसके कितने प्रत्ययों को हमें बाद में अपडेट करने की आवश्यकता है। यही 'शेष' है।
'शेष' और 'पुनरुत्थान' की अवधारणा
चर remainder
हमें बताता है कि हमने कितने दोहराया वर्णों को जोड़ दिया, बिना शाखा के; एक बार जब हमें पहला चरित्र नहीं मिला जिसे हम मैच नहीं कर सकते हैं, तो कितने ब्रिक्स ऑपरेशन को दोहराने के लिए हमें कितने प्रत्ययों की आवश्यकता होगी। यह अनिवार्य रूप से इसके मूल से पेड़ में कितने वर्ण 'गहरे' के बराबर है।
इसलिए, स्ट्रिंग एबीसीएक्सएबीसीवाई के पिछले उदाहरण के साथ रहकर , हम प्रत्येक बार बढ़ने वाले दोहराए गए एबीसी भाग 'अंतर्निहित' से मेल खाते हैं remainder
, जिसके परिणामस्वरूप 3. शेष है और फिर हम गैर-दोहराए जाने वाले चरित्र 'वाई' का सामना करते हैं । यहां हमने पहले जोड़े गए ABCX को ABC -> X और ABC -> Y में विभाजित किया है । फिर हम remainder
3 से 2 तक की कमी करते हैं , क्योंकि हम पहले से ही एबीसी ब्रांचिंग का ध्यान रखते हैं। अब हम अंतिम 2 अक्षरों - बीसी - को मूल से उस बिंदु तक पहुंचाने के लिए ऑपरेशन को दोहराते हैं, जहां हमें विभाजित होने की आवश्यकता होती है, और हम बीसीएक्स को भी बीसी में विभाजित करते हैं।-> एक्स और ईसा पूर्व -> वाई । फिर से, हम remainder
1 में कमी करते हैं, और ऑपरेशन को दोहराते हैं; जब तक remainder
0. नहीं है, तब तक , हमें वर्तमान वर्ण ( 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) यहाँ वर्णित मूल प्रत्यय लिंकिंग संभव दृष्टिकोणों में से एक है । उदाहरण के लिए एनजे लार्सन एट अल। इस दृष्टिकोण को नोड-ओरिएंटेड टॉप-डाउन के नाम से जाना जाता है , और इसकी तुलना नोड-ओरिएंटेड बॉटम-अप और दो एज-ओरिएंटेड किस्मों से की जाती है। अलग-अलग दृष्टिकोणों में अलग-अलग विशिष्ट और सबसे खराब स्थिति प्रदर्शन, आवश्यकताएं, सीमाएं आदि हैं, लेकिन आमतौर पर ऐसा लगता है कि एज-ओरिएंटेड दृष्टिकोण मूल में एक समग्र सुधार है।