Cevabım gereksiz görünüyorsa özür dilerim, ama son zamanlarda Ukkonen algoritmasını uyguladım ve kendimi günlerce bununla mücadele ederken buldum; Algoritmanın bazı temel yönlerinin nedenini ve nasıl olduğunu anlamak için konuyla ilgili birden fazla makale okumak zorunda kaldım.
Önceki cevapların 'kurallar' yaklaşımını altta yatan nedenleri anlamak için yararsız buldum, bu yüzden aşağıda her şeyi sadece pragmatiklere odaklanarak yazdım. Tıpkı benim yaptığım gibi diğer açıklamaları takip etmekle uğraştıysanız, belki de tamamlayıcı açıklamam sizin için 'tıklayacaktır'.
C # uygulamamı burada yayınladım: https://github.com/baratgabor/SuffixTree
Lütfen bu konuda uzman olmadığımı unutmayın, bu nedenle aşağıdaki bölümlerde yanlışlıklar (veya daha kötüsü) bulunabilir. Herhangi biriyle karşılaşırsanız, düzenlemekten çekinmeyin.
Ön şartlar
Aşağıdaki açıklamanın başlangıç noktası, sonek ağaçlarının içeriği ve kullanımı ile Ukkonen'in algoritmasının özelliklerini (örneğin, son ek ağaç karakterini baştan sona nasıl genişlettiğinizi) bildiğinizi varsayar. Temel olarak, diğer açıklamaların bazılarını zaten okuduğunuzu varsayıyorum.
(Ancak, akış için bazı temel anlatılar eklemek zorunda kaldım, bu yüzden başlangıç gerçekten gereksiz hissedebilir.)
En ilginç kısım, sonek bağlantılarını kullanma ve kökten yeniden tarama arasındaki farktır . Bu benim uygulamada bana çok fazla hata ve baş ağrısı verdi.
Açık uçlu yaprak düğümleri ve sınırlamaları
Eminim ki en temel 'numara' soneklerin sonunu 'açık' bırakabileceğimizi, yani sonun statik bir değere ayarlanması yerine dizenin mevcut uzunluğuna atıfta bulunabileceğimizi bilmektir. Bu şekilde ek karakterler eklediğimizde, bu karakterler hepsini ziyaret etmek ve güncellemek zorunda kalmadan tüm sonek etiketlerine örtülü olarak eklenir.
Ancak son eklerin bu açık sona ermesi - bariz nedenlerle - sadece dizenin sonunu temsil eden düğümler, yani ağaç yapısındaki yaprak düğümleri için çalışır. Ağaç üzerinde yürüttüğümüz dallanma işlemleri (yeni dal düğümleri ve yaprak düğümlerinin eklenmesi) ihtiyaç duydukları her yerde otomatik olarak yayılmaz.
Muhtemelen temeldir ve yinelenen alt dizelerin ağaçta açıkça görünmediğinden bahsetmek gerekmez, çünkü ağaç bunları tekrar olması nedeniyle zaten içerir; ancak, tekrarlayan alt dize, tekrar etmeyen bir karakterle karşılaşarak sona erdiğinde, o noktadan itibaren sapmayı temsil etmek için o noktada bir dallanma yaratmamız gerekir.
Örneğin, 'ABCXABCY' dizesi durumunda (aşağıya bakın) , ABC ve BC ve C olmak üzere üç farklı son eke X ve Y'ye bir dal eklenmesi gerekir ; aksi takdirde geçerli bir sonek ağacı olmazdı ve dizeden tüm alt dizeleri kökten karakterleri aşağıya doğru eşleştirerek bulamadık.
Bir kez daha vurgulamak için - ağaçtaki bir sonek üzerinde yaptığımız herhangi bir işlemin ardışık ekleriyle de (örn. ABC> BC> C) yansıtılması gerekir, aksi takdirde geçerli sonekler olmaktan çıkar.
Ancak bu manuel güncellemeleri yapmak zorunda olduğumuzu kabul etsek bile, kaç ekin güncellenmesi gerektiğini nasıl bilebiliriz? Tekrarlanan A karakterini (ve art arda tekrarlanan karakterlerin geri kalanını) eklediğimizde, son eki ne zaman / nerede ikiye bölmemiz gerektiği hakkında henüz bir fikrimiz yok. Bölme ihtiyacı, yalnızca ilk tekrarlanmayan karakterle karşılaştığımızda, bu durumda Y'de ( ağaçta zaten var olan X yerine ) tespit edilir.
Yapabileceğimiz, yapabileceğimiz en uzun tekrarlanan dizeyle eşleşmek ve daha sonrasının kaç tanesini güncellememiz gerektiğini saymak. Bu nedir 'kalan' anlamına gelir.
'Kalan' ve 'yeniden tarama' kavramı
Değişken remainder
bize kaç tane tekrarlanan karakterin dallanmadan örtük olarak eklendiğini söyler; yani, eşleştiremediğimiz ilk karakteri bulduğumuzda dallanma işlemini tekrarlamak için kaç eke ihtiyacımız var. Bu aslında ağaç kökünden kaç karakter 'derin' olduğumuza eşittir.
Böylece, ABCXABCY dizesinin önceki örneğiyle kaldığımızda , yinelenen ABC bölümünü 'örtük olarak' eşleştiriyoruz, remainder
her seferinde artar , bu da 3'ün kalanıyla sonuçlanır. Sonra tekrarlamayan 'Y' karakteriyle karşılaşırız . Burada daha önce eklenen bölünmüş ABCX içine ABC > - X ve ABC -> Y . Daha sonra remainder
3'ten 2'ye düşeriz , çünkü zaten ABC dallamasına dikkat ettik . Şimdi , bölünmemiz gereken noktaya ulaşmak için kökten son 2 karakteri - BC - eşleştirerek işlemi tekrarlıyoruz ve BCX'i de BC'ye bölüyoruz-> X ve M.Ö. -> Y . Yine, remainder
1'e düşeriz ve işlemi tekrar ederiz ; kadar remainder
Son olarak 0., biz (şimdiki karakter eklemek gerekir Y köküne yanı kendisini).
Bir işlem yapmamız gereken noktaya ulaşmak için kökten ardışık ekleri takip eden bu işlem, Ukkonen algoritmasında 'yeniden tarama ' olarak adlandırılan şeydir ve genellikle bu algoritmanın en pahalı kısmıdır. Potansiyel olarak binlerce kez, düzinelerce düğümde (bunu daha sonra tartışacağız) uzun alt dizeleri 'yeniden taramanız' gereken daha uzun bir dize düşünün.
Bir çözüm olarak, 'son ek bağlantıları' dediğimiz şeyi sunuyoruz .
'Son ek bağlantıları' kavramı
Sonek bağlantıları temelde normalde 'yeniden taramamız' gereken konumlara işaret eder , bu nedenle pahalı yeniden tarama işlemi yerine sadece bağlantılı konuma atlayabilir, işimizi yapabilir, bir sonraki bağlantılı konuma atlayabilir ve tekrar - oraya kadar güncellenecek konum yok.
Tabii ki büyük bir soru bu bağlantıların nasıl ekleneceğidir. Mevcut yanıt, ağacın her uzantısında, dal düğümlerinin doğal olarak bunları birbirine bağlamak zorunda olduğumuz sırayla oluşturdukları gerçeğini kullanarak yeni dal düğümleri eklediğimizde bağlantıları ekleyebilmemizdir. . Yine de, son oluşturulan dal düğümünden (en uzun sonek) daha önce oluşturulmuş olana bağlamamız gerekir, bu yüzden oluşturduğumuz son öğeyi önbelleğe almamız, bunu oluşturduğumuz bir sonrakine bağlamamız ve yeni oluşturulan önbelleğe almamız gerekir.
Bunun bir sonucu, aslında takip edilecek ek bağlantılarına sahip olmamamızdır, çünkü verilen dal düğümü yeni oluşturulmuştur. Bu gibi durumlarda yine de kökten yukarıda bahsedilen 'yeniden taranmaya' geri dönmeliyiz . Bu nedenle, bir eklemeden sonra, sonek bağlantısını kullanmanız veya kök dizinine atlamanız istenir.
(Veya alternatif olarak, düğümlerde üst işaretçiler saklıyorsanız, ebeveynleri takip etmeye çalışabilir, bağlantılarının olup olmadığını kontrol edebilir ve kullanabilirsiniz. Bunun çok nadiren belirtildiğini, ancak son ek bağlantı kullanımının taşlar seti. Orada birden olası yaklaşımlar vardır ve altta yatan mekanizma anlarsanız ihtiyaçlarını en uygun olanı uygulayabilirsiniz.)
'Aktif nokta' kavramı
Şimdiye kadar, ağacı inşa etmek için çok verimli araçları tartıştık ve belirsiz bir şekilde birden fazla kenar ve düğüm üzerinde geçişe atıfta bulunduk, ancak henüz ilgili sonuçları ve karmaşıklıkları araştırmadık.
Daha önce açıklanan 'kalan' kavramı , ağacın neresinde olduğumuzu takip etmek için yararlıdır, ancak bunun yeterli bilgi depolamadığını fark etmeliyiz.
İlk olarak, her zaman bir düğümün belirli bir kenarında bulunuruz, bu nedenle kenar bilgilerini depolamamız gerekir. Buna 'aktif kenar' adını vereceğiz .
İkincisi, kenar bilgisini ekledikten sonra bile, ağaçta daha aşağıda olan ve doğrudan kök düğüme bağlı olmayan bir konumu tanımlamanın bir yolu yoktur . Bu yüzden düğümü de depolamamız gerekiyor. Buna 'aktif düğüm' diyelim .
Son olarak, 'kalanın' doğrudan bir kenara bağlı olmayan bir kenardaki bir konumu tanımlamak için yetersiz olduğunu fark edebiliriz , çünkü 'kalan' tüm rotanın uzunluğudur; ve muhtemelen önceki kenarların uzunluğunu hatırlamak ve çıkarmakla uğraşmak istemiyoruz. Bu yüzden, esasen mevcut kenarda kalan bir temsile ihtiyacımız var . Buna 'aktif uzunluk' diyoruz .
Bu, 'aktif nokta' dediğimiz şeye yol açar - ağaçtaki konumumuz hakkında tutmamız gereken tüm bilgileri içeren üç değişkenli bir paket:
Active Point = (Active Node, Active Edge, Active Length)
Aşağıdaki görüntüde, ABCABD'nin eşleşen yolunun AB kenarındaki ( kökten ) 2 karakterden ve CABDABCABD kenarındaki (4 düğümden) 4 karakterden nasıl oluştuğunu gözlemleyebilirsiniz - sonuçta 6 karakterlik bir 'kalan' elde edilir. Böylece, mevcut konumumuz Aktif Düğüm 4, Aktif Kenar C, Aktif Uzunluk 4 olarak tanımlanabilir .
'Aktif noktanın' bir diğer önemli rolü, algoritmamız için bir soyutlama katmanı sağlamasıdır, yani algoritmamızın bazı bölümleri, bu aktif noktanın kökte veya başka bir yerde olmasına bakılmaksızın, çalışmalarını 'aktif nokta' üzerinde yapabilirler. . Bu, algoritmamızda son ek bağlantılarının kullanımını temiz ve basit bir şekilde uygulamayı kolaylaştırır.
Son ek bağlantıları kullanma ile yeniden tarama arasındaki farklar
Şimdi, zor kısım, benim deneyimime göre, bol miktarda hata ve baş ağrısına neden olabilen ve çoğu kaynakta zayıf bir şekilde açıklanabilecek bir şey, son ek bağlantı vakalarının yeniden tarama vakalarına karşı işlenmesindeki farktır.
Aşağıdaki 'AAAABAAAABAAC' dizesinin örneğini ele alalım :
Yukarıda ' 7'nin ' kalanının ' kökten toplam karakter toplamına nasıl karşılık geldiğini gözlemlerken , 4'ün ' aktif uzunluğu ' aktif düğümün aktif kenarından eşleşen karakterlerin toplamına karşılık gelir.
Şimdi, aktif noktada bir dallanma işlemi gerçekleştirdikten sonra, aktif düğümümüz bir sonek bağlantısı içerebilir veya içermeyebilir.
Bir sonek bağlantısı varsa: Yalnızca 'aktif uzunluk' kısmını işlememiz gerekir. 'Kalan' Çünkü, alakasız biz eki bağlantı yoluyla atlamak düğüm zaten örtük doğru 'kalan'' kodlar sadece o ağaçta olmanın gereği.
Bir sonek bağlantısı yoksa: Sıfırdan / kökten 'yeniden taramamız' gerekir , bu da tüm soneki baştan işlemek demektir. Bu amaçla yeniden taramayı temel almak için 'geri kalanın' tamamını kullanmalıyız .
Sonek bağlantısıyla ve sonek bağlantısı olmadan işlemenin örnek karşılaştırması
Yukarıdaki örneğin bir sonraki adımında neler olacağını düşünün. Aynı sonuca nasıl ulaşacağını (yani işlemek için bir sonraki son eke geçmeyi) bir sonek bağlantısıyla ve soneksiz olarak karşılaştıralım.
'Son ek bağlantısı' kullanma
Bir sonek bağlantısı kullanırsak, otomatik olarak 'doğru yerde' olduğumuza dikkat edin. Bu, 'aktif uzunluğun' yeni pozisyonla 'uyumsuz' olması nedeniyle genellikle kesin olarak doğru değildir .
Yukarıdaki durumda, 'aktif uzunluk' 4 olduğu için, bağlantılı Düğüm 4'ten başlayarak ' ABAA' son ekiyle çalışıyoruz . Ancak son ekin ilk karakterine ( 'A') karşılık gelen kenarı bulduktan sonra ), 'etkin uzunluğumuzun' bu kenarı 3 karakter aştığını fark ediyoruz . Bu yüzden tam kenardan bir sonraki düğüme atlıyoruz ve atlama ile tükettiğimiz karakterlerle 'aktif uzunluğu' azaltıyoruz.
Daha sonra , azaltılmış 'BAA ' son ekine karşılık gelen bir sonraki kenarı 'B' bulduktan sonra , sonunda kenar uzunluğunun 3'ün kalan 'aktif uzunluğundan' daha büyük olduğunu, yani doğru yeri bulduğumuz anlamına gelir.
Lütfen bu işlemin genellikle 'yeniden tarama' olarak adlandırılmadığını, bana göre sadece yeniden kısaltmanın doğrudan eşdeğeri gibi görünse de, sadece kısaltılmış bir uzunluk ve kök olmayan bir başlangıç noktasıyla birlikte olduğunu unutmayın.
' Yeniden tarama'yı kullanma
Geleneksel bir 'yeniden tarama' işlemi kullanırsak (burada bir sonek bağlantımız yokmuş gibi), ağacın tepesinden, kökten başlıyoruz ve tekrar doğru yere doğru yol almamız gerekiyor, geçerli son ekin tüm uzunluğu boyunca.
Bu son ekin uzunluğu, daha önce tartıştığımız 'kalan' dır . Bu kalanın tamamını, sıfıra ulaşıncaya kadar tüketmeliyiz. Bu, (ve çoğu zaman) birden fazla düğümden atlamayı içerebilir, her atlayışta, geri kalanını atladığımız kenarın uzunluğuna indirir. Sonunda kalan 'kalanımızdan daha uzun bir kenara ulaşıyoruz ; burada aktif kenarı verilen kenara, 'aktif uzunluğu' kalan 'kalan ' olarak ayarladık ve işimiz bitti.
Bununla birlikte, gerçek 'kalan' değişkeninin korunması ve yalnızca her düğümün eklenmesinden sonra azaltılması gerektiğini unutmayın. Bu yüzden yukarıda tarif ettiğim şey, 'kalan' olarak başlatılan ayrı bir değişken kullanılarak varsayılmıştır .
Son ek bağlantıları ve yeniden taramalarla ilgili notlar
1) Her iki yöntemin de aynı sonuca yol açtığına dikkat edin. Ancak sonek bağlantı atlama çoğu durumda önemli ölçüde daha hızlıdır; ek bağlantılarının ardındaki mantık budur.
2) Gerçek algoritmik uygulamaların farklı olması gerekmez. Yukarıda bahsettiğim gibi, sonek bağlantısı kullanıldığında bile, 'aktif uzunluk' genellikle bağlı konumla uyumlu değildir, çünkü ağacın bu dalı ek dallanma içerebilir. Yani esasen , 'kalan ' yerine 'aktif uzunluk' kullanmanız ve kalan sonek uzunluğunuzdan daha kısa bir kenar bulana kadar aynı yeniden tarama mantığını yürütmeniz gerekir.
3) Performansla ilgili önemli bir nokta, yeniden tarama sırasında her bir karakteri kontrol etmeye gerek olmamasıdır. Geçerli bir sonek ağacının oluşturulma şekli nedeniyle, karakterlerin eşleştiğini güvenle varsayabiliriz. Yani çoğunlukla uzunlukları sayıyorsunuz ve karakter denkliği kontrolüne olan tek ihtiyaç, yeni bir kenara atladığımızda ortaya çıkıyor, çünkü kenarlar ilk karakterleriyle tanımlanıyor (bu, belirli bir düğüm bağlamında her zaman benzersizdir). Bu, 'yeniden tarama' mantığının tam dize eşleme mantığından farklı olduğu anlamına gelir (yani ağaçta bir alt dize aramak).
4) Burada açıklanan orijinal son ek , olası yaklaşımlardan sadece biridir . Örneğin NJ Larsson ve diğ. bu yaklaşımı Düğüm Odaklı Yukarıdan Aşağı olarak adlandırır ve Düğüm Odaklı Aşağıdan Yukarı ve iki Kenar Odaklı çeşitle karşılaştırır. Farklı yaklaşımların farklı tipik ve en kötü durum performansları, gereksinimleri, sınırlamaları vb. Vardır, ancak genellikle Kenar Odaklı yaklaşımların orijinal için genel bir iyileştirme olduğu görülmektedir.