Ukkonen'in son ek ağaç algoritması


1101

Bu noktada biraz kalın hissediyorum. Başımı sonek ağacı yapısının etrafına tamamen sarmaya çalışarak günler geçirdim, ancak matematiksel bir arka planım olmadığı için, açıklamaların çoğu, matematiksel sembolojiyi aşırı kullanmaya başladığında beni kaçırıyor. Bulduğum iyi bir açıklamaya en yakın, Sonek Ağaçları ile Hızlı Dize Arama , ancak çeşitli noktalar üzerinde parlıyor ve algoritmanın bazı yönleri belirsizliğini koruyor.

Stack Overflow'daki bu algoritmanın adım adım açıklaması, benim yanı sıra birçok kişi için çok değerli olacaktır, eminim.

Referans için, Ukkonen'in algoritma hakkındaki makalesi: http://www.cs.helsinki.fi/u/ukkonen/SuffixT1withFigs.pdf

Temel anlayışım, şimdiye kadar:

  • Belirli bir dize T her önek P yinelemek gerekir
  • P önekindeki her S sonekini yinelemeli ve ağaca eklemeliyim
  • Ağaca S soneki eklemek için, S'deki her karakter boyunca yinelemem gerekir, yinelemeler ya S'deki aynı C karakter kümesiyle başlayan mevcut bir dalı aşağı doğru yürüdüğüm ve potansiyel olarak bir kenarı azalan düğümlere böldüğümden oluşur. sonekte farklı bir karaktere, VEYA aşağıya doğru eşleşen kenar yoksa. C için aşağı doğru eşleşen kenar bulunamadığında, C için yeni bir yaprak kenarı oluşturulur.

Bu algoritma, O (n görünmektedir 2 biz önekleri arasında durmadan adımına ihtiyaç olarak) gibi pek çok açıklama işaret, daha sonra her bir önek için son ekler her birinden aşama gerekir. Ukkonen algoritması, kullandığı ek işaretçi tekniği nedeniyle görünüşte benzersizdir, ancak bence bu konuda sorun yaşıyorum.

Ayrıca anlamakta güçlük çekiyorum:

  • "aktif nokta" nın tam olarak ne zaman ve nasıl atandığı, kullanıldığı ve değiştirildiği
  • algoritmanın kanonlaşma yönüyle neler oluyor
  • Gördüğüm uygulamalar neden kullandıkları sınırlayıcı değişkenleri "düzeltmeye" ihtiyaç duyuyor?

İşte tamamlanmış C # kaynak kodu. Sadece düzgün çalışmakla kalmaz, aynı zamanda otomatik kanonizasyonu destekler ve çıktının daha hoş görünen bir metin grafiğini oluşturur. Kaynak kodu ve örnek çıktısı:

https://gist.github.com/2373868


Güncelleme 2017-11-04

Yıllar sonra sonek ağaçları için yeni bir kullanım buldum ve algoritmayı JavaScript'te uyguladım . Gist aşağıda. Hatasız olmalı. npm install chalkAynı konumdan bir js dosyasına dökün ve sonra bazı renkli çıktıları görmek için node.js ile çalıştırın. Hata ayıklama kodu olmadan aynı Gist'te soyulmuş bir sürüm var.

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


2
Dan Gusfield'ın kitabında verilen açıklamaya baktınız mı? Yararlı buldum.
jogojapan

4
Gist lisansı belirtmez - kodunuzu değiştirebilir ve MIT altında yeniden yayınlayabilir miyim?
Yurik

2
Evet, hayatına devam et. Bunu kamu malı olarak kabul edin. Bu sayfadaki başka bir cevapta belirtildiği gibi, yine de düzeltilmesi gereken bir hata var.
Nathan Ridley

1
belki bu uygulama başkalarına yardımcı olacaktır, code.google.com/p/text-indexing
cos

2
"Kamu malı olarak düşünün", belki de şaşırtıcı bir şekilde çok yararsız bir cevaptır. Bunun nedeni, çalışmayı kamu malı haline getirmenizin aslında imkansız olmasıdır. Bu nedenle "düşünün ..." yorumunuz, lisansın belirsiz olduğu gerçeğinin altını çiziyor ve okuyucuya çalışmanın durumunun gerçekten sizin için net olduğundan şüphe duyma sebebi veriyor . İnsanların kodunuzu kullanabilmelerini istiyorsanız, lütfen bunun için bir lisans belirtin, istediğiniz herhangi bir lisansı seçin (ancak, bir avukat değilseniz, önceden var olan bir lisansı seçin!)
James Youngman

Yanıtlar:


2377

Aşağıda, Ukkonen algoritmasını önce dize basit olduğunda (yani tekrarlanan karakterler içermediğinde) göstererek ve daha sonra tam algoritmaya genişleterek tarif etme denemesidir.

İlk olarak, birkaç ön ifade.

  1. Yaptığımız şey temelde bir arama üçgeni gibidir. Yani bir kök düğüm var, kenarlar yeni düğümlere yol açıyor ve diğer kenarlar bunlardan çıkıyor, vb.

  2. Ancak : Bir arama satırından farklı olarak, kenar etiketleri tek karakter değildir. Bunun yerine, her kenar bir çift tamsayı kullanılarak etiketlenir [from,to]. Bunlar metne işaretçilerdir. Bu anlamda, her kenar keyfi uzunlukta bir dize etiketi taşır, ancak yalnızca O (1) alan (iki işaretçi) alır.

Temel prensip

Ben ilk başta özellikle basit bir dize, tekrarlanan karakterleri olmayan bir dize sonek ağacının nasıl oluşturulacağını göstermek istiyorum:

abc

Algoritma , soldan sağa doğru adımlarla çalışır . Dizenin her karakteri için bir adım vardır . Her adım birden fazla bireysel operasyon içerebilir, ancak toplam operasyon sayısının O (n) olduğunu göreceğiz (sondaki son gözlemlere bakın).

Bu nedenle, soldan başlıyoruz ve önce akök düğümden (solda) bir yaprağa bir kenar oluşturarak ve bunu şu şekilde etiketleyerek yalnızca tek karakteri ekliyoruz, [0,#]yani kenar 0 konumunda başlayıp biten alt dizeyi temsil ediyor en güncel ucunda . Bu simgeyi , konum 1'deki (hemen sonra ) geçerli uç# anlamında kullanıyorum .a

Yani başlangıçtaki bir ağacımız var, şuna benziyor:

Ve bunun anlamı şu:

Şimdi pozisyon 2'ye geçiyoruz (hemen sonra b). Her adımda Amacımız eklemektir geçerli konuma tüm eklerini kadar . Bunu şu şekilde yapıyoruz:

  • varolan a-gege genişletmekab
  • için yeni bir kenar ekleme b

Temsilimizde bu şöyle görünüyor

resim açıklamasını buraya girin

Ve bunun anlamı:

İki şeyi gözlemliyoruz :

  • İçin kenar gösterimi abolan aynı başlangıçtaki ağaçta olduğu gibi: [0,#]. Geçerli konumu #1'den 2'ye güncellediğimiz için anlamı otomatik olarak değişti .
  • Her kenar O (1) alanını tüketir, çünkü kaç karakter temsil ettiğine bakılmaksızın metne yalnızca iki işaretçi içerir.

Sonra pozisyonu tekrar arttırır cve mevcut her kenara a ekleyerek ve yeni sonek için yeni bir kenar ekleyerek ağacı güncelleriz c.

Temsilimizde bu şöyle görünüyor

Ve bunun anlamı:

Gözlemliyoruz:

  • Ağaç, her adımdan sonra geçerli konuma kadar doğru sonek ağacıdır
  • Metinde karakterler kadar adım var
  • Her adımdaki iş miktarı O (1) 'dir, çünkü mevcut tüm kenarlar #artırılarak otomatik olarak güncellenir ve son karakter için yeni bir kenar eklenmesi O (1) sürede yapılabilir. Bu nedenle, n uzunluğunda bir dize için yalnızca O (n) süresi gereklidir.

İlk uzantı: Basit tekrarlar

Tabii ki bu çok güzel çalışıyor, çünkü dize tekrar yok. Şimdi daha gerçekçi bir dizeye bakıyoruz:

abcabxabcd

abcÖnceki örnekte olduğu gibi başlar , sonra abtekrarlanır ve ardından gelir xve sonra abctekrarlanır d.

Adım 1 ila 3: İlk 3 adımdan sonra önceki örnekteki ağaç var:

Adım 4: Harekete #Bu dolaylı olarak bu mevcut tüm kenarlarını günceller pozisyon 4.:

ve mevcut adımın son ekini akökte eklememiz gerekir.

Bunu yapmadan önce , elbette her zaman orada olan ancak şu ana kadar kullanmadığımız iki değişken (buna ek olarak #) tanıtıyoruz :

  • Aktif noktası üçlü olduğu, (active_node,active_edge,active_length)
  • remainderMalzemeler her yerde ekin gereken kaç yeni ekleri gösteren bir tam sayıdır

Bu ikisinin kesin anlamı yakında netleşecek, ancak şimdilik şunu söyleyelim:

  • Basit abcörnekte, etkin nokta her zaman (root,'\0x',0), yani active_nodekök düğümdü, active_edgeboş karakter olarak belirtildi '\0x've active_lengthsıfırdı. Bunun etkisi, her adımda eklediğimiz yeni kenarın kök düğüme yeni oluşturulan bir kenar olarak eklenmiş olmasıdır. Yakında bu bilgiyi temsil etmek için neden üç katın gerekli olduğunu göreceğiz.
  • remainderHer zaman her adımın başlangıcında 1'e ayarlandı. Bunun anlamı, her adımın sonunda aktif olarak eklememiz gereken eklerin sayısının 1 (her zaman sadece son karakter) olmasıydı.

Şimdi bu değişecek. Geçerli son karakteri aköke eklediğimizde a, özellikle: ile başlayan bir çıkış kenarı olduğunu fark ederiz abca. İşte böyle bir durumda yaptığımız şey:

  • Biz yok taze bir kenar eklemek [4,#]kök düğümde. Bunun yerine, son ekin azaten ağacımızda olduğunu fark ediyoruz. Daha uzun bir kenarın ortasında biter, ancak bundan rahatsız değiliz. Biz sadece şeyleri oldukları gibi bırakıyoruz.
  • Biz aktif noktasını ayarlamak için (root,'a',1). Bu, aktif noktanın artık kök düğümün giden kenarının ortasında a, özellikle bu kenardaki konum 1'den sonra başlayan bir yerde olduğu anlamına gelir . Kenarın sadece ilk karakteriyle belirtildiğini fark ediyoruz a. Bu , herhangi bir belirli karakterle başlayan sadece bir kenar olabileceğinden yeterlidir (tüm açıklamayı okuduktan sonra bunun doğru olduğunu onaylayın).
  • Ayrıca artırıyoruz remainder, bu nedenle bir sonraki adımın başında 2 olacak.

Gözlem: Eklememiz gereken son ekin zaten ağaçta bulunduğu tespit edildiğinde , ağacın kendisi hiç değişmez (sadece aktif noktayı güncelleriz ve remainder). Bu durumda, ağaç artık sonek ağacının geçerli konuma kadar doğru bir temsili değildir , ancak tüm sonekleri içerir (çünkü son ek a, dolaylı olarak bulunur ). Bu nedenle, değişkenlerin güncellenmesi dışında (hepsi sabit uzunluktadır, yani bu O (1)), bu adımda hiçbir iş yapılmamıştır.

Adım 5: Geçerli konumu #5 olarak güncelliyoruz. Bu, ağacı otomatik olarak bu şekilde günceller:

Ve çünkü remainder2'dir , mevcut pozisyonun son iki eklerini eklemek gerekir: abve b. Bunun temel nedeni:

  • aBir önceki adımdaki eki düzgün şekilde yerleştirildiğinden olmamıştı. Bu yüzden etmiştir kalmıştır ve biz bir adım ilerledi beri, artık büyüdü aiçin ab.
  • Ve yeni son kenarı eklememiz gerekiyor b.

Pratikte bu, aktif noktaya ( aşimdi abcabkenar olanın arkasına işaret eden ) gidip geçerli son karakteri eklediğimiz anlamına gelir b. Ancak: Yine, baynı kenarda zaten mevcut olduğu ortaya çıkıyor .

Yani, yine, ağacı değiştirmiyoruz. Biz sadece:

  • Etkin noktayı (root,'a',2)(önceki ile aynı düğüm ve kenar olarak güncelleyin, ancak şimdi arkasına işaret ediyoruz b)
  • Arttırır remainderhala düzgün bir önceki adımda elde edilen nihai kenar takılmamış çünkü 3'e kadar ve her iki akım son kenar uç yoktur.

Açık olmak gerekirse: Eklemek zorundaydık abve bmevcut adımda, ancak abzaten bulunduğundan, etkin noktayı güncelledik ve eklemeye bile çalışmadık b. Neden? Çünkü abağaç içindeyse, her ekinin de ( ağaç dahilinde b) ağaçta olması gerekir. Belki sadece örtülü olarak , ama şimdiye kadar ağacı inşa etme şeklimiz nedeniyle orada olmalı.

Biz geçin 6. adıma artırarak #. Ağaç otomatik olarak şu şekilde güncellenir:

Çünkü remainder3'tür , biz eklemek zorunda abx, bxve x. Aktif nokta bize nerede absona erdiğini söyler , bu yüzden sadece oraya atlamamız ve yerleştirmemiz gerekir x. Gerçekten de xhenüz yok, bu yüzden abcabxkenarı ayırdık ve bir iç düğüm ekledik:

Kenar gösterimleri hala metne işaretçilerdir, bu nedenle iç düğümün bölünmesi ve eklenmesi O (1) zamanında yapılabilir.

Bu yüzden 2 ile başa çıktık abxve azaldık remainder. Şimdi bir sonraki kalan soneki eklememiz gerekiyor bx. Ancak bunu yapmadan önce aktif noktayı güncellememiz gerekiyor. Bunun için, bir kenarı böldükten ve ekledikten sonra, kural aşağıdaki Kural 1 olarak adlandırılacak ve her active_nodekök olduğunda geçerli olacaktır (aşağıdaki diğer durumlar için kural 3'ü öğreneceğiz). Kural 1:

Kök eklendikten sonra,

  • active_node kök kalır
  • active_edge eklememiz gereken yeni son ekin ilk karakterine ayarlanmış, yani b
  • active_length 1 azaltılır

Bu nedenle, yeni aktif nokta üçlü (root,'b',1), bir sonraki ekin bcabxkenarda, 1 karakterin arkasında, yani arkasında yapılması gerektiğini gösterir b. Ekleme noktasını O (1) sürede belirleyebilir ve xmevcut olup olmadığını kontrol edebiliriz . Eğer mevcut olsaydı, mevcut adımı bitirir ve her şeyi olduğu gibi bırakırdık. Ama x mevcut değil, bu yüzden kenarı bölerek yerleştiriyoruz:

Yine, bu O (1) zaman aldı ve biz remainder1'e ve aktif noktayı (root,'x',0)kural 1 durumları olarak güncelliyoruz .

Ama yapmamız gereken bir şey daha var. Bu Kural 2 olarak adlandıracağız:

Bir kenarı bölerek yeni bir düğüm eklersek ve bu geçerli adım sırasında oluşturulan ilk düğüm değilse , önceden eklenen düğümü ve yeni düğümü özel bir işaretçi olan bir sonek bağlantısıyla bağlarız . Daha sonra bunun neden faydalı olduğunu göreceğiz. İşte elde ettiğimiz şey, sonek bağlantısı noktalı bir kenar olarak temsil edilir:

Halen mevcut adımın son ekini eklememiz gerekiyor x. Yana active_lengthaktif düğümün bileşen 0 düşmüş, son uç ile doğrudan kök yapılır. Kök düğümünde başlayan xbir kenar olmadığından yeni bir kenar ekliyoruz:

Gördüğümüz gibi, mevcut adımda kalan tüm kesici uçlar yapıldı.

Her zaman olduğu gibi , bir sonraki karakteri otomatik olarak tüm yaprak kenarlarına ekleyen = 7 ayarlayarak adım 7'ye ilerliyoruz . Sonra yeni son karakteri etkin noktaya (kök) eklemeye ve zaten orada olduğunu bulmaya çalışıyoruz. Bu nedenle, mevcut adımı hiçbir şey eklemeden sonlandırıyoruz ve etkin noktayı güncelliyoruz .#a(root,'a',1)

In 8. adımda , #= 8, biz eklemek bve daha önce görüldüğü gibi, bu tek yolu biz aktif gelin güncellemek (root,'a',2)ve artışı remaindernedeniyle, başka bir şey yapmadan bzaten mevcuttur. Bununla birlikte, aktif noktanın artık bir kenarın sonunda olduğunu (O (1) zamanda) fark ediyoruz. Bunu yeniden ayarlayarak yansıtıyoruz (node1,'\0x',0). Burada, kenarın bittiği node1iç düğüme başvurmak için kullanıyorum ab.

Daha sonra, adım #9'da , 'c' harfini eklememiz gerekir ve bu son numarayı anlamamıza yardımcı olacaktır:

İkinci uzantı: Son ek bağlantılarını kullanma

Her zaman olduğu gibi, #güncelleme cyaprak kenarlarına otomatik olarak eklenir ve 'c' ekleyip ekleyemeyeceğimizi görmek için etkin noktaya gideriz. Görünüşe göre 'c' zaten bu kenarda var, bu yüzden aktif noktayı (node1,'c',1)artırıyoruz remainderve başka bir şey yapmıyoruz.

Şimdi adım #= 10 , remainder4'tür ve bu yüzden abcdönce daktif noktaya ekleyerek (3 adımdan önce kalan) eklememiz gerekir .

dEtkin noktaya ekleme denemesi O (1) zamanında bir kenar bölünmesine neden olur:

active_nodeBölünmüş başlatıldı olan kırmızı, yukarıda işaretlenir. İşte son kural, Kural 3:

active_nodeKök düğüm olmayan bir kenardan bir kenar böldükten sonra , varsa düğümden çıkan sonek bağlantısını izler ve işaret ettiği düğüme sıfırlarız active_node. Sonek bağlantısı yoksa, active_nodekök dizinine ayarlanır . active_edge ve active_lengthdeğişmeden kalır.

Aktif nokta şimdi (node2,'c',1)ve node2aşağıda kırmızı ile işaretlenmiştir:

Ekleme abcdişlemi tamamlandığından, remainder3 değerine düşeriz ve geçerli adımın bir sonraki son ekini ele alırız bcd. Kural 3, etkin noktayı sadece sağ düğüme ve kenara ayarlamıştır, böylece bcdekleme, son karakterini detkin noktaya ekleyerek kolayca yapılabilir .

Bunu yapmak başka bir kenar bölünmesine neden olur ve kural 2 nedeniyle , önceden eklenen düğümden yenisine bir sonek bağlantısı oluşturmalıyız:

Gözlemliyoruz: Son bağlantılar, aktif noktayı sıfırlamamıza olanak tanır, böylece O (1) çabasında bir sonraki eki yapabiliriz . Gerçekten de düğüm etikete onaylamak için yukarıdaki grafikte bak abdüğümün bağlantılıdır b(kendi eki) ve düğümün abcbağlantılıdır bc.

Mevcut adım henüz tamamlanmadı. remainderşimdi 2'dir ve etkin noktayı tekrar sıfırlamak için kural 3'ü izlememiz gerekir. Geçerli active_node(yukarıdaki kırmızı) sonek bağlantısı olmadığından, root olarak sıfırlanır. Aktif nokta şimdi (root,'c',1).

Bu nedenle, bir sonraki ekleme, etiketi ile başlayan kök düğümün bir giden kenarında gerçekleşir c: cabxabcdilk karakterin arkasında, yani arkasında c. Bu başka bir bölünmeye neden olur:

Ve bu yeni bir iç düğümün oluşturulmasını içerdiğinden, kural 2'yi izler ve önceden oluşturulan iç düğümden yeni bir sonek bağlantısı kurarız:

( Bu küçük grafikler için Graphviz Dot kullanıyorum . Yeni sonek bağlantısı, noktanın mevcut kenarları yeniden düzenlemesine neden oldu, bu yüzden yukarıda eklenen tek şeyin yeni bir son ek bağlantısı olduğunu doğrulamak için dikkatlice kontrol edin.)

Bununla, remainder1 olarak ayarlanabilir ve active_nodekök olduğundan, etkin noktayı güncellemek için kural 1'i kullanırız (root,'d',0). Bu, geçerli adımın son ekinin d kökte bir tane eklemek olduğu anlamına gelir :

Bu son adımdı ve işimiz bitti. Bununla birlikte, son gözlem sayısı vardır :

  • Her adımda #1 pozisyon ileri gidiyoruz. Bu, tüm yaprak düğümlerini O (1) sürede otomatik olarak günceller.

  • Ancak a) önceki adımlardan kalan herhangi bir sonekle ve b) mevcut adımın son bir karakteriyle ilgilenmez .

  • remainderbize ne kadar ek kesici uç eklememiz gerektiğini söyler. Bu ekler, dizenin geçerli konumda biten son eklerine bire bir karşılık gelir #. Birbiri ardına düşünüyoruz ve eki yapıyoruz. Önemli: Etkin nokta bize tam olarak nereye gideceğimizi söylediğinden, her ekleme O (1) zamanında yapılır ve etkin noktaya yalnızca tek bir karakter eklememiz gerekir. Neden? Diğer karakterler örtük olarak bulunduğundan (aksi halde etkin nokta olduğu yerde olmaz).

  • Bu tür her bir eklemeden sonra, remaindervarsa son ek bağlantısını azaltır ve takip ederiz . Değilse köke gideriz (kural 3). Zaten kökteysek, kural 1'i kullanarak aktif noktayı değiştiririz. Her durumda, sadece O (1) zaman alır.

  • Bu eklerden biri sırasında, eklemek istediğimiz karakterin zaten orada olduğunu tespit edersek, remainder> 0 olsa bile hiçbir şey yapmıyor ve mevcut adımı sonlandırıyoruz . Bunun nedeni, kalan eklerin henüz yapmaya çalıştığımız eklerin olmasıdır. Dolayısıyla bunlar hepsi örtük akım ağacında. Aslında remainder> 0 markaları emin biz daha sonra kalan ekleri ele.

  • Ya algoritmanın sonunda remainder> 0 olursa ? Bu, metnin sonu daha önce bir yerde meydana gelen bir alt dize olduğunda geçerli olacaktır. Bu durumda, daha önce meydana gelmeyen dizenin sonuna bir karakter daha eklemeliyiz. Literatürde, genellikle dolar işareti $bunun bir sembolü olarak kullanılır. Bu neden önemli? -> Son ekleri aramak için tamamlanmış son ek ağacını kullanırsak, eşleşmeleri yalnızca bir yaprakla biterse kabul etmeliyiz . Çünkü aksi takdirde biz, sahte maçların çok alacağı birçok dizeleri örtük ana dizenin gerçek ekler değildir ağaçta içeriyordu. zorlamakremaindersonunda 0 olması temelde tüm soneklerin bir yaprak düğümünde bitmesini sağlamanın bir yoludur. Ancak, ağacı yalnızca ana dizenin soneklerini değil, genel alt dizeleri aramak için kullanmak istiyorsak, aşağıdaki OP'nin açıklamasında önerildiği gibi, bu son adım gerçekten gerekli değildir.

  • Peki tüm algoritmanın karmaşıklığı nedir? Metin n karakter uzunluğunda ise, açıkça n adım vardır (veya dolar işareti eklersek n + 1). Her adımda (değişkenleri güncellemek dışında) hiçbir şey yapmıyoruz veya remainderher biri O (1) zaman alan kesici uçlar yapıyoruz . Yana remainderbiz önceki adımlarda hiçbir şey yapmamış kaç kere gösterir ve şimdi yapmak her uç için indirildiği, bir şeyler sayısı toplamı (veya n + 1) n tam olarak budur. Dolayısıyla, toplam karmaşıklık O (n) 'dir.

  • Bununla birlikte, düzgün bir şekilde açıklamadığım küçük bir şey var: Bir sonek bağlantısını takip ettiğimiz, aktif noktayı güncellediğimiz ve ardından active_lengthbileşeninin yeni ile iyi çalışmadığını fark edebiliriz active_node. Örneğin, böyle bir durumu düşünün:

(Kesik çizgiler ağacın geri kalanını gösterir. Noktalı çizgi bir sonek bağlantısıdır.)

Şimdi aktif tespittir (red,'d',3)geride yere işaret böylece, füzerine defgkenar. Şimdi gerekli güncellemeleri yaptığımızı ve aktif noktayı kural 3'e göre güncellemek için sonek bağlantısını takip ettiğimizi varsayalım. Yeni aktif nokta (green,'d',3). Ancak, dyeşil düğümden çıkan -edge de, yani sadece 2 karakteri vardır. Doğru aktif noktayı bulmak için, o kenarı mavi düğüme kadar izlememiz ve sıfırlamamız gerekir (blue,'f',1).

Özellikle kötü durumda, active_lengthkadar büyük olabilir remaindern büyüklüğünde olabilen,. Ve doğru aktif noktayı bulmak için, sadece bir iç düğümün üzerine değil, belki de çok sayıda, en kötü durumda n'ye kadar atlamamız gerekir. Algoritma anlamına mu gizli O (n sahiptir 2 her adımda, çünkü) karmaşıklığı remaindergenellikle O (n), ve bir son ek bağlantıyı izledikten sonra aktif düğüme sonrası ayarlamalar da O (n) olabilir?

Hayır. Bunun nedeni, eğer aktif noktayı gerçekten ayarlamamız gerekirse (örneğin yukarıdaki gibi yeşilden maviye), bu da bizi kendi sonek bağlantısı olan ve active_lengthazaltılacak yeni bir düğüme getirir . Son ek bağlantıları zincirini takip ettikçe, kalan ekleri yaparız, active_lengthsadece azaltabilir ve yolda yapabileceğimiz etkin nokta ayarlamalarının sayısı active_lengthherhangi bir zamandan daha büyük olamaz . Yana active_lengthdaha büyük olamaz remainderve remainder sadece her adımda O (n) olmakla birlikte, şimdiye kadar yapılan artışlarla toplamı remaindertüm süreç boyunca O (n) de etkin nokta ayarlamaları olan sayıdır ayrıca O (n) ile sınırlandırılmıştır.


74
Üzgünüm bu umduğumdan biraz daha uzun sürdü. Ve bunun, hepimizin bildiği bir dizi önemsiz şeyi açıkladığını fark ediyorum, ancak zor kısımlar hala net olmayabilir. Haydi birlikte şekillendirelim.
jogojapan

68
Ve bu olduğunu eklemek gerekir değil Dan Gusfield kitabında bulunan açıklamaya dayalı. Öncelikle tekrarlama olmayan bir dizeyi düşünüp ardından tekrarların nasıl ele alınacağını tartışarak algoritmayı tanımlamaya yönelik yeni bir girişimdir. Bunun daha sezgisel olacağını umuyordum.
jogojapan

8
Teşekkürler @jogojapan, açıklamanız sayesinde tamamen çalışan bir örnek yazabildim. Kaynağı umarım bir başkası kullanımda bulabilir: gist.github.com/2373868
Nathan Ridley

4
@NathanRidley Evet (bu arada, Ukkonen'in kanonik olarak adlandırdığı son bit). Bunu tetiklemenin bir yolu, üç kez görünen ve yine farklı bir bağlamda bir kez daha görünen bir dizede biten bir alt dize olduğundan emin olmaktır. Örn abcdefabxybcdmnabcdex. İlk parçası abcdolarak tekrar edilir abxy(bundan sonra bir iç düğüm oluşturur ab) ve tekrar abcdexve biten bcddeğil sadece göründüğü, bcdexbağlam, ama aynı zamanda bcdmnbağlamda. abcdexbcdex
Eklendikten

6
Tamam benim kod tamamen yeniden yazılmıştır ve şimdi otomatik kanonizasyon da dahil olmak üzere tüm durumlar için doğru çalışır, artı çok daha hoş bir metin grafik çıkışı vardır. gist.github.com/2373868
Nathan Ridley

132

Sonek Ağacı'nı jogojapan'ın cevabında verilen yaklaşımla uygulamaya çalıştım, ancak kurallar için kullanılan ifadeler nedeniyle bazı durumlarda işe yaramadı. Dahası, hiç kimsenin bu yaklaşımı kullanarak kesinlikle doğru bir sonek ağacı uygulamayı başaramadığını belirttim. Aşağıda, kurallarda bazı değişikliklerle jogojapan'ın cevabına bir "genel bakış" yazacağım. Önemli sonek bağlantıları oluşturmayı unuttuğumuzda da durumu açıklayacağım .

Kullanılan ek değişkenler

  1. aktif nokta - yeni bir sonek eklemeye başlamamız gereken yerden gösteren üçlü (active_node; active_edge; active_length).
  2. kalan - açıkça eklememiz gereken eklerin sayısını gösterir . Örneğin, kelimemiz 'abcaabca' ve kalan = 3 ise, son 3 son eki işlememiz gerektiği anlamına gelir: bca , ca ve a .

Diyelim bir bir kavram kullanmak iç düğüm dışındaki tüm düğümleri - kökü ve yapraklar vardır iç düğümler .

Gözlem 1

Eklememiz gereken son ekin zaten ağaçta bulunduğu tespit edildiğinde, ağacın kendisi hiç değişmez (sadece active pointve öğesini güncelleriz remainder).

Gözlem 2

Bir noktada active_lengthmevcut kenarın uzunluğuna ( edge_length) eşit veya daha büyükse , active pointaşağıdan edge_lengthkesinlikle daha büyük olana kadar aşağı doğru hareket ederiz active_length.

Şimdi kuralları yeniden tanımlayalım:

Kural 1

Bir ekleme sonrasında ise etkin düğümde = kök , aktif uzunluk , daha sonra daha fazla, 0:

  1. aktif düğüm değiştirilmedi
  2. aktif uzunluk azaltılır
  3. etkin kenar sağa kaydırılır (eklememiz gereken bir sonraki sonekin ilk karakterine)

Kural 2

Yeni bir iç düğüm oluşturursak VEYA bir iç düğümden bir ekleyici yaparsak ve bu geçerli adımdaki ilk SUCH iç düğümü değilse , önceki SUCH düğümünü bir sonek bağlantısıyla BU ile ilişkilendiririz .

Bu tanım Rule 2jogojapan'dan farklıdır ', çünkü burada sadece yeni oluşturulan iç düğümleri değil, aynı zamanda bir ekleme yaptığımız iç düğümleri de dikkate alıyoruz.

Kural 3

Etkin düğümden kök düğüm olmayan bir eklemeden sonra , sonek bağlantısını izlemeli ve etkin düğümü işaret ettiği düğüme ayarlamalıyız . Hayır bir sonek bağlantı varsa, set aktif düğüm için kök düğümü. Her iki durumda da, aktif kenar ve aktif uzunluk değişmeden kalır.

Bu tanımda, Rule 3yaprak düğümlerinin (sadece bölünmüş düğümlerin değil) eklerini de düşünüyoruz.

Ve son olarak, Gözlem 3:

Ağaca eklemek istediğimiz sembol zaten kenardaysa, Observation 1sadece günceller active pointve remainderağacı değiştirmeden bırakırız. ANCAK sonek bağlantısı olarak işaretlenmiş bir iç düğüm varsa, bu düğümü bir sonek bağlantısı aracılığıyla akımımıza bağlamalıyız.active node

Bu durumda bir sonek bağlantısı eklersek ve yapmazsak , cdddcdc için bir sonek ağacı örneğine bakalım :

  1. Biz ise YAPMAYIN bir sonek bağlantı üzerinden düğümleri bağlamak:

    • son harfi eklemeden önce c :

    • c harfini ekledikten sonra :

  2. Biz ise DO bir sonek bağlantı üzerinden düğümleri bağlamak:

    • son harfi eklemeden önce c :

    • c harfini ekledikten sonra :

Önemli bir fark yok gibi görünüyor: ikinci durumda iki tane daha ek bağlantısı var. Ancak bu son ek bağlantıları doğrudur ve bunlardan biri - mavi düğümden kırmızıya - aktif noktaya yaklaşımımız için çok önemlidir . Sorun şu ki, buraya bir sonek bağlantısı koymazsak, ağaca yeni harfler eklediğimizde, ağaç nedeniyle bazı düğümler eklemeyi ihmal edebiliriz , çünkü buna göre, sonek bağlantısı, sonra kök kök koymak gerekir .Rule 3active_node

Ağaca son harfi eklerken, mavi düğümden bir uç yapmadan önce kırmızı düğüm zaten vardı (kenar 'c' olarak etiketlendi ). Mavi düğümden bir uç olduğu için, bir sonek bağlantısına ihtiyaç duyduğunu işaretliyoruz . Daha sonra, aktif nokta yaklaşımına dayanarak active node, kırmızı düğüme ayarlandı. Ancak kırmızı düğümden bir ekleme yapmıyoruz, çünkü 'c' harfi zaten kenarda. Bu, mavi düğümün bir sonek bağlantısı olmadan bırakılması gerektiği anlamına mı geliyor? Hayır, mavi düğümü sonek bağlantısıyla kırmızı düğüme bağlamalıyız. Neden doğru? Çünkü aktif noktayaklaşımı, doğru bir yere, yani daha kısa bir ekin ekini işlememiz gereken bir sonraki yere ulaşmamızı garanti eder .

Son olarak, Sonek Ağacı ile ilgili uygulamalarım:

  1. Java
  2. C ++

Bu "genel bakış" ın jogojapan'ın ayrıntılı cevabı ile birleşmesinin birisinin kendi Sonek Ağacını uygulamasına yardımcı olacağını umuyoruz.


3
Çok teşekkürler ve çaba için +1. Eminim haklısınız .. detayları hemen düşünecek zamanım olmamasına rağmen. Daha sonra kontrol edeceğim ve muhtemelen cevabımı değiştireceğim.
Ocak'ta jogojapan

Çok teşekkürler, gerçekten yardımcı oldu. Yine de, Gözlem 3'e daha açık olabilir misiniz? Örneğin, yeni son ek bağlantısını tanıtan 2 adımın diyagramlarını vermek. Düğüm etkin düğüme bağlı mı? (aslında 2. düğümü
eklemediğimiz için

@makagonov Hey "cdddcdc" dizeniz için bir sonek ağacı oluşturmama yardım edebilir misin Biraz kafam karıştı (başlangıç ​​adımları).
tariq zafar

3
Kural 3'e gelince, akıllı bir yol, kökün sonek bağlantısını kökün kendisine ve (varsayılan olarak) her düğümün sonek bağlantısını köküne ayarlamaktır. Böylece şartlanmayı önleyebilir ve sadece son ek bağlantısını takip edebiliriz.
sqd

1
aabaacaadEk sonek bağlantısı eklemenin üçlü güncelleme zamanını azaltabileceğini gösteren durumlardan biridir. Jogojapan karakolunun son iki paragrafındaki sonuç yanlıştır. Bu gönderinin bahsettiği son ek bağlantılarını eklemezsek, ortalama zaman karmaşıklığı O (nlong (n)) veya daha fazla olmalıdır. Çünkü doğru olanı bulmak için ağaçta yürümek fazladan zaman alır active_node.
IvanaGyro

10

@Jogojapan'ın iyi açıkladığı öğretici için teşekkürler , algoritmayı Python'da uyguladım.

@Jogojapan'ın bahsettiği birkaç küçük sorun beklediğimden daha karmaşık ve çok dikkatli davranılması gerekiyor. Uygulamamın yeterince sağlam hale gelmesi birkaç güne mal oldu (sanırım). Sorunlar ve çözümleri aşağıda listelenmiştir:

  1. Şununla bitir:Remainder > 0 Bu durum, sadece tüm algoritmanın sonu değil , aynı zamanda ortaya çıkan adım sırasında da ortaya çıkabilir . Bu olduğunda, kalan dizgiyi, aktüatörü, oyunculuğu ve hareketliliğini değiştirmeden bırakabilir, mevcut açılma adımını sonlandırabilir ve orijinal dizideki bir sonraki karakterin geçerli yolda olup olmadığına bağlı olarak katlamaya veya açmaya devam ederek başka bir adım başlatabiliriz. değil.

  2. Düğümler Üzerinden Atlama: Bir sonek bağlantısını takip ettiğimizde, etkin noktayı güncelleyin ve ardından active_length bileşeninin yeni active_node ile iyi çalışmadığını bulun. Biz zorundayız ileriye taşımak bölünmeye doğru yere ya da bir yaprak yerleştirin. Bu süreç olabilir o kolay değildir çünkü actlength hareketli ve geri taşımak zorunda actedge tutmak, bütün şeklini değiştirerek sırasında kök düğüm , actedge ve actlength olabilir yanlış çünkü bu hareketleri. Bu bilgileri saklamak için ek değişkenlere ihtiyacımız var.

    resim açıklamasını buraya girin

Diğer iki problem bir şekilde @managonov tarafından belirtildi

  1. Bölme Dejenere Olabilir Bir kenarı bölmeye çalışırken, bölme işleminin bir düğüm üzerinde olduğunu görürsünüz. Bu durumda, yalnızca bu düğüme yeni bir yaprak eklememiz gerekir, bunu standart bir kenar bölme işlemi olarak kabul ederiz, yani varsa sonek bağlantıları buna göre korunmalıdır.

  2. Gizli Sonek Bağlantıları Problem 1 ve problem 2'den kaynaklanan özel bir durum daha vardır . Bazen bölme için birkaç noktayı doğru noktaya atlamamız gerekir, geri kalan dizeyi ve yol etiketlerini karşılaştırarak hareket edersek doğru noktayı aşabiliriz . Bu durumda, sonek bağlantısı istenirse, ihmal edilir. Bu, ilerlerken doğru noktayı hatırlayarak önlenebilir . Bölme düğümü zaten varsa, sonek bağlantısı korunmalıdır, hatta sorun çözme adımı 1, bir açılma adımı sırasında ortaya çıkar.

Son olarak, Python'daki uygulamam aşağıdaki gibidir:

İpuçları: Yukarıdaki kodda hata ayıklama sırasında çok önemli olan saf bir ağaç yazdırma işlevi içerir . Bana çok zaman kazandırdı ve özel vakaları bulmak için uygun.


10

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.

Soneklerde tekrarlanan dallanma

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 remainderbize 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, remainderher 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 remainder3'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, remainder1'e düşeriz ve işlemi tekrar ederiz ; kadar remainderSon 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 .

Kalan ve Aktif Nokta

'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 :

Birden çok kenarda kalan

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

Son ek bağlantıları ile ardışık soneklere ulaşma

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

Yeniden tarama ile ardışık soneklere ulaşma

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.


8

@jogojapan harika bir açıklama ve görselleştirme getirdi. Ancak @makagonov'un belirttiği gibi, son ek bağlantılarını ayarlama ile ilgili bazı kurallar eksik. 'Aabaaabb' kelimesi ile http://brenden.github.io/ukkonen-animation/ üzerinde adım adım ilerlerken hoş bir şekilde görülebilir . Adım 10'dan adım 11'e geçtiğinizde, düğüm 5'ten düğüm 2'ye sonek bağlantısı yoktur, ancak etkin nokta aniden oraya hareket eder.

@makagonov Java dünyasında yaşadığımdan beri ST bina iş akışını kavramak için uygulamanızı takip etmeye çalıştım, çünkü benim için zor oldu:

  • kenarları düğümlerle birleştirme
  • referanslar yerine dizin işaretçileri kullanma
  • ifadeleri keser;
  • ifadelere devam et;

Bu yüzden Java'da böyle bir uygulama ile sonuçlandım, umarım tüm adımları daha net bir şekilde yansıtır ve diğer Java kullanıcıları için öğrenme süresini azaltır:

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

Sezgim şu şekildedir:

Ana döngünün k yinelemelerinden sonra, ilk k karakterleriyle başlayan tüm dizenin tüm soneklerini içeren bir sonek ağacı oluşturdunuz.

Başlangıçta bu, sonek ağacının tüm dizeyi temsil eden tek bir kök düğümü içerdiği anlamına gelir (bu, 0 ile başlayan tek sonektir).

Len (string) yinelemelerinden sonra, tüm sonekleri içeren bir sonek ağacınız olur.

Döngü sırasında anahtar etkin noktadır. Tahminimce bu, sonek ağacında dizenin ilk k karakterinin uygun bir sonekine karşılık gelen en derin noktayı temsil ediyor. (Uygun olduğunu düşünüyorum sonek tüm dize olamaz anlamına gelir.)

Örneğin, 'abcabc' karakterlerini gördüğünüzü varsayalım. Aktif nokta, ağaçtaki 'abc' sonekine karşılık gelen noktayı temsil eder.

Aktif nokta (başlangıç, ilk, son) ile temsil edilir. Bu, şu anda ağaçta düğüm noktasından başlayıp sonra [string: first] dizesindeki karakterleri besleyerek ulaştığınız noktada olduğunuz anlamına gelir.

Yeni bir karakter eklediğinizde, etkin noktanın hala varolan ağaçta olup olmadığını görüyorsunuz. Eğer öyleyse işiniz bitti demektir. Aksi takdirde, etkin noktada sonek ağacına yeni bir düğüm eklemeniz, bir sonraki en kısa eşleşmeye geçmeniz ve tekrar kontrol etmeniz gerekir.

Not 1: Son ek işaretçileri, her düğüm için bir sonraki en kısa eşleşmeye bağlantı verir.

Not 2: Yeni bir düğüm ve yedek eklediğinizde, yeni düğüm için yeni bir sonek işaretçisi eklersiniz. Bu son ek işaretçisinin hedefi, kısaltılmış etkin noktadaki düğüm olacaktır. Bu düğüm ya zaten var olacak ya da bu geri dönüş döngüsünün bir sonraki yinelemesinde oluşturulacak.

Not 3: Standartlaştırma kısmı, etkin noktayı kontrol etmek için zaman kazandırır. Örneğin, her zaman origin = 0 kullandığınızı ve sadece ilk ve son değiştirdiğinizi varsayalım. Aktif noktayı kontrol etmek için tüm ara düğümler boyunca her seferinde ek ağacını takip etmeniz gerekir. Son düğümden yalnızca mesafeyi kaydederek bu yolu izlemenin sonucunu önbelleğe almak mantıklıdır.

Sınırlayıcı değişkenleri "düzeltmek" ile ne demek istediğinize bir kod örneği verebilir misiniz?

Sağlık uyarısı: Ayrıca bu algoritmayı anlamakta özellikle zorlandım, bu nedenle bu sezginin tüm önemli ayrıntılarda yanlış olabileceğini lütfen unutmayın ...


Akademik çalışmalardan biri, "uygun" kelimesini, bir dizenin "uygun soneki" nin ilk karakterini içermediği anlamına gelir. Bazen bütün bir alt dizeye "sonek" adını verirsiniz, ancak algoritmayı tanımlarken "dize" ve "alt dize" ve "sonek" terimleri liberal olarak atılır ve bazen "sonek" ile ne kastettiğinizin çok net olması gerekir. "uygun sonek" terimi, her şeye sonek demeyi içermez. Dolayısıyla, bir dizenin sonek alt dizesi herhangi bir meşru alt dizge olabilir ve aynı sonek olmayan uygun bir sonek olabilir. Çünkü mantık.
Blair Houghton

3

Merhaba ben yakut yukarıda açıklanan uygulamayı uygulamak için denedim, kontrol edin. iyi çalışıyor gibi görünüyor.

uygulamadaki tek fark, sadece sembolleri kullanmak yerine edge nesnesini kullanmaya çalıştım.

https://gist.github.com/suchitpuri/9304856 adresinde de mevcut

    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
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.