โครงสร้างข้อมูลหรืออัลกอริทึมสำหรับค้นหาความแตกต่างระหว่างสตริงได้อย่างรวดเร็ว


19

ฉันมีอาร์เรย์ของ 100,000 สตริงทั้งหมดของความยาวkkฉันต้องการเปรียบเทียบแต่ละสตริงกับสตริงอื่น ๆ เพื่อดูว่ามีสองสตริงที่แตกต่างกัน 1 อักขระหรือไม่ ตอนนี้ที่ผมเพิ่มแต่ละสายไปยังอาร์เรย์ที่ฉันตรวจสอบกับทุกสตริงแล้วในอาร์เรย์ซึ่งมีความซับซ้อนเวลาของการkn(n1)2k

มีโครงสร้างข้อมูลหรืออัลกอริทึมที่สามารถเปรียบเทียบสตริงได้เร็วกว่าสิ่งที่ฉันกำลังทำอยู่หรือไม่?

ข้อมูลเพิ่มเติมบางส่วน:

  • หัวข้อการสั่งซื้อ: abcdeและxbcdeแตกต่างกัน 1 ตัวอักษรในขณะที่abcdeและedcbaแตกต่างกัน 4 ตัว

  • สำหรับแต่ละคู่ของสตริงที่แตกต่างกันโดยตัวละครตัวหนึ่งฉันจะลบหนึ่งในสายเหล่านั้นออกจากอาร์เรย์

  • ตอนนี้ฉันกำลังมองหาสตริงที่แตกต่างกันเพียง 1 ตัวอักษร แต่มันจะดีถ้าความแตกต่างของตัวละคร 1 ตัวนั้นสามารถเพิ่มขึ้นได้เช่น 2, 3 หรือ 4 ตัวอักษร อย่างไรก็ตามในกรณีนี้ฉันคิดว่าประสิทธิภาพสำคัญกว่าความสามารถในการเพิ่มขีดจำกัดความแตกต่างของตัวละคร

  • kมักจะอยู่ในช่วง 20-40


4
ค้นหาพจนานุกรมสตริงที่มีข้อผิดพลาด 1 เป็นปัญหาอย่างเป็นธรรมที่รู้จักกันดีเช่นcs.nyu.edu/~adi/CGL04.pdf
KWillets

1
20-40mers สามารถใช้พื้นที่ว่างพอสมควร คุณอาจดูที่ตัวกรอง Bloom ( en.wikipedia.org/wiki/Bloom_filter ) เพื่อทดสอบว่าสตริงย่อยสลาย - ชุดของเมอร์ทั้งหมดจากการแทนที่หนึ่งหรือสองตัวในการทดสอบ mer - คือ "อาจจะ" หรือ "แน่นอน - ไม่ใช้ "ชุดกิโลเมตร หากคุณได้รับ "อาจจะเป็น" ให้เปรียบเทียบทั้งสองสตริงเพื่อพิจารณาว่าเป็นบวกหรือไม่ กรณี "ไม่แน่นอน" เป็นเชิงลบที่แท้จริงซึ่งจะลดจำนวนการเปรียบเทียบแบบตัวอักษรและตัวอักษรโดยรวมที่คุณต้องทำโดย จำกัด การเปรียบเทียบกับความนิยมที่อาจเกิดขึ้น
Alex Reynolds

หากคุณทำงานกับช่วง k ที่น้อยกว่าคุณสามารถใช้บิตเซ็ตเพื่อเก็บตารางแฮชของ booleans สำหรับสตริงที่เสื่อมลงทั้งหมด (เช่นgithub.com/alexpreynolds/kmer-booleanสำหรับตัวอย่างของเล่น) สำหรับ k = 20-40 ความต้องการพื้นที่สำหรับบิตเซ็ตนั้นมากเกินไป
Alex Reynolds

คำตอบ:


12

เป็นไปได้ที่จะบรรลุเวลาทำงานที่เลวร้ายที่สุดO(nklogk)

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

ใช้สตริงแต่ละเส้นและเก็บไว้ใน hashtable โดยพิมพ์ครึ่งแรกของสตริง จากนั้นทำซ้ำผ่าน hashtable buckets สำหรับแต่ละคู่ของสตริงในที่ฝากข้อมูลเดียวกันตรวจสอบว่าพวกเขาแตกต่างกันใน 1 อักขระ (เช่นตรวจสอบว่าครึ่งหลังของพวกเขาแตกต่างกันใน 1 อักขระ)

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

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

เป็นการเพิ่มประสิทธิภาพประสิทธิภาพหากที่ฝากข้อมูลใด ๆ มีสตริงมากเกินไปคุณสามารถทำซ้ำกระบวนการเดียวกันซ้ำ ๆ เพื่อค้นหาคู่ที่แตกต่างกันตามตัวละครตัวหนึ่ง ภาวนา recursive จะอยู่ในสายของความยาว 2k/2

หากคุณสนใจเวลาที่ใช้งานกรณีที่แย่ที่สุด:

ด้วยการเพิ่มประสิทธิภาพการปฏิบัติดังกล่าวข้างต้นผมเชื่อว่าเลวร้ายที่สุดกรณีเวลาการทำงานเป็น )O(nklogk)


3
หากสตริงแบ่งปันครึ่งแรกเหมือนกันซึ่งอาจเกิดขึ้นได้ในชีวิตจริงคุณจะไม่ได้ปรับปรุงความซับซ้อน Ω(n)
einpoklum - คืนสถานะโมนิก้า

@ einpoklum แน่นอน! นั่นเป็นเหตุผลที่ฉันเขียนคำสั่งในประโยคที่สองของฉันว่ามันกลับไปใช้เวลากำลังสองในกรณีที่เลวร้ายที่สุดเช่นเดียวกับคำสั่งในประโยคสุดท้ายของฉันที่อธิบายถึงวิธีการบรรลุความซับซ้อนที่เลวร้ายที่สุดกรณีเกี่ยวกับกรณีที่เลวร้ายที่สุด แต่ฉันเดาว่าบางทีฉันก็ไม่ได้แสดงออกอย่างชัดเจน - ดังนั้นฉันจึงได้แก้ไขคำตอบของฉัน ตอนนี้มันดีกว่าไหม? O(nklogk)
DW

15

โซลูชันของฉันคล้ายกับ j_random_hacker แต่ใช้ชุดแฮชเพียงชุดเดียว

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

ตัวอย่างที่มีสตริง 'abc', 'adc'

สำหรับ abc เราเพิ่ม '* bc', 'a * c' และ 'ab *'

สำหรับ adc เราเพิ่ม '* dc', 'a * c' และ 'ad *'

เมื่อเราเพิ่ม 'a * c' ครั้งที่สองเราสังเกตเห็นว่ามันมีอยู่แล้วในชุดดังนั้นเราจึงรู้ว่ามีสองสายที่แตกต่างกันเพียงตัวอักษรเดียว

รวมเวลาการทำงานของอัลกอริทึมนี้คือ ) นี่เป็นเพราะเราสร้างสตริงkใหม่สำหรับสตริงnทั้งหมดในอินพุต สำหรับแต่ละสายที่เราต้องคำนวณกัญชาซึ่งปกติจะใช้เวลาO ( k )เวลาO(nk2)knO(k)

การจัดเก็บสตริงทั้งหมดจะใช้เวลาพื้นที่O(nk2)

การปรับปรุงเพิ่มเติม

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

คุณจะต้องใช้ฟังก์ชันแฮชที่กำหนดเองสำหรับวัตถุ เราสามารถใช้การใช้ Java เป็นตัวอย่างให้ดูเอกสารจาวา java hashCode คูณค่ายูนิโค้ดของอักขระแต่ละตัวด้วย (ด้วยkความยาวสตริงและiดัชนีแบบอิงฐานเดียวของอักขระโปรดทราบว่าแต่ละสตริงที่เปลี่ยนแปลงจะแตกต่างกันโดยอักขระหนึ่งตัวจากต้นฉบับเท่านั้นเราสามารถคำนวณได้อย่างง่ายดาย การมีส่วนร่วมของตัวละครนั้นในรหัสแฮชเราสามารถลบมันและเพิ่มตัวละครที่สวมหน้ากากแทนได้ซึ่งจะใช้เวลาO ( 1 )ในการคำนวณสิ่งนี้ช่วยให้เราสามารถนำเวลาการทำงานทั้งหมดลงไปที่O ( n)31kikiO(1)O(nk)


4
@ JollyJoker ใช่แล้วพื้นที่เป็นสิ่งที่น่ากังวลสำหรับวิธีนี้ คุณสามารถลดพื้นที่โดยไม่จัดเก็บสตริงที่ถูกดัดแปลง แต่แทนที่จะเก็บวัตถุที่มีการอ้างอิงถึงสตริงและดัชนีที่ถูกพรางแทน ที่ควรปล่อยให้คุณมีพื้นที่ O (nk)
Simon Prins

ในการคำนวณ hash สำหรับแต่ละสตริงในเวลาO ( k )ฉันคิดว่าคุณจะต้องใช้ฟังก์ชันแฮชแบบโฮมเมดพิเศษ (เช่นคำนวณแฮชของสตริงเดิมในเวลาO ( k )จากนั้น XOR กับแต่ละไฟล์ที่ถูกลบ ตัวละครในkO(k)O(k)แต่ละครั้ง (แม้ว่านี่อาจเป็นฟังก์ชั่นแฮชที่ไม่ดีพอในทางอื่น)) BTW มันค่อนข้างคล้ายกับโซลูชันของฉัน แต่มี hashtable เพียงอันเดียวแทนที่จะแยกเป็น kและแทนที่อักขระด้วย "*" แทนการลบ O(1)k
j_random_hacker

@SimonPrins ด้วยการกำหนดเองequalsและhashCodeวิธีการที่สามารถใช้งานได้ เพียงแค่สร้างสตริง * b-style ในวิธีการเหล่านั้นควรทำให้มันเป็นกระสุน ฉันสงสัยว่าคำตอบอื่น ๆ ที่นี่จะมีปัญหาการชนกันของข้อมูล
JollyJoker

1
@DW ฉันแก้ไขโพสต์ของฉันเพื่อสะท้อนถึงความจริงที่ว่าการคำนวณแฮชจะใช้O(k)เวลาและเพิ่มวิธีการแก้ปัญหาที่จะนำมารวมเวลาทำงานกลับลงไป ) O(nk)
Simon Prins

1
@SimonPrins กรณีที่เลวร้ายที่สุดอาจจะเป็น nk ^ 2 เนื่องจากการตรวจสอบความเท่าเทียมกันของสตริงใน hashset.contain เมื่อแฮชชนกัน แน่นอนกรณีที่เลวร้ายที่สุดคือเมื่อทุกสตริงมีกัญชาที่แน่นอนเดียวกันซึ่งจะต้องมีการตั้งค่าในแบบฉบับที่สวยมากของสตริงโดยเฉพาะอย่างยิ่งที่จะได้รับกัญชาเดียวกัน*bc, ,a*c ab*ฉันสงสัยว่ามันจะเป็นไปไม่ได้หรือไม่?
JollyJoker

7

ฉันจะทำให้ hashtables H 1 , , H k , ซึ่งแต่ละคนมีสตริง( k - 1 ) - ความยาวเป็นกุญแจสำคัญและรายการของตัวเลข (สตริง ID) เป็นค่า hashtable H iจะมีสตริงทั้งหมดที่ถูกประมวลผลจนถึงตอนนี้แต่ด้วยอักขระที่ตำแหน่งที่ฉันลบไป ตัวอย่างเช่นถ้าk = 6ดังนั้นH 3 [ A B D E F ]จะมีรายการของสตริงทั้งหมดที่เห็นจนถึงที่มีรูปแบบAkH1,,Hk(k1)Hiik=6H3[ABDEF]โดยที่หมายถึง "ตัวละครใด ๆ " จากนั้นประมวลผลสตริงอินพุต j -th s j :ABDEFjsj

  1. สำหรับแต่ละในช่วง 1 ถึงk : ik
    • สตริงแบบฟอร์มโดยการลบผมตัวละครจาก -th s Jsjisj
    • เงยหน้าขึ้นมอง ] ทุก ID สตริงที่นี่ระบุสตริงเดิมที่อาจเท่ากับsหรือแตกต่างที่ตำแหน่งHi[sj]sเท่านั้น เอาท์พุทเหล่านี้เป็นแมตช์สำหรับสตริง s J (หากคุณต้องการแยกรายการที่ซ้ำกันให้ทำประเภทค่าของคู่ hashtables (ID สตริง, อักขระที่ถูกลบ) เพื่อให้คุณสามารถทดสอบสำหรับผู้ที่มีตัวอักษรที่ถูกลบเหมือนที่เราเพิ่งลบจาก s j )isjsj
    • ใส่ลงในH iเพื่อใช้แบบสอบถามในอนาคตjHi

หากเราเก็บคีย์แฮชแต่ละคีย์ไว้อย่างชัดเจนเราจะต้องใช้พื้นที่และมีความซับซ้อนของเวลาอย่างน้อยนั้น แต่ตามที่อธิบายไว้โดย Simon Prinsเป็นไปได้ที่จะแสดงชุดของการดัดแปลงสตริง (ในกรณีของเขาอธิบายว่าการเปลี่ยนอักขระเดียวเป็นของฉันเป็นการลบ) โดยปริยายในลักษณะที่k hash keys สำหรับสตริงเฉพาะต้องการเพียงพื้นที่ O ( k )นำไปสู่พื้นที่โดยรวมO ( n k )และเปิดความเป็นไปได้ของO ( n k )O(nk2)*kO(k)O(nk)O(nk)เวลาด้วย เพื่อให้เกิดความซับซ้อนในเวลานี้เราจำเป็นต้องมีวิธีการคำนวณแฮชสำหรับตัวแปรทั้งหมดของความยาวkในเวลาO ( k ) : ตัวอย่างนี้สามารถทำได้โดยใช้แฮชพหุนามตามที่แนะนำโดย DW (และนี่คือ น่าจะดีกว่า XOR เพียงการลบตัวอักษรที่มีแฮชสำหรับสตริงเดิม)kkO(k)

เคล็ดลับการแสดงโดยนัยของ Simon Prins ก็หมายความว่า "การลบ" ของตัวละครแต่ละตัวนั้นไม่ได้ทำจริง ๆ ดังนั้นเราจึงสามารถใช้การแทนสตริงตามปกติโดยไม่ต้องมีการปรับประสิทธิภาพ


2
ทางออกที่ดี ตัวอย่างของฟังก์ชั่นแฮช bespoke ที่เหมาะสมจะเป็นแฮโพลิโนเมียล
DW

ขอบคุณ @DW คุณช่วยอธิบายหน่อยได้ไหมว่าคุณหมายถึงอะไร "polynomial hash" Googling คำไม่ได้รับฉันอะไรที่ดูเหมือนชัดเจน (โปรดแก้ไขโพสต์ของฉันโดยตรงหากคุณต้องการ)
j_random_hacker

1
เพียงแค่อ่านสตริงเป็นฐานจำนวนโมดูโลpโดยที่pมีค่าน้อยกว่าขนาด hashmap ของคุณและqเป็นรากดั้งเดิมของpและqมีขนาดใหญ่กว่าตัวอักษร มันเรียกว่า "แฮชพหุนาม" เพราะมันเป็นเหมือนการประเมินพหุนามที่มีค่าสัมประสิทธิ์จะได้รับจากสตริงที่คิว ฉันจะปล่อยให้มันเป็นสิทธิที่จะคิดออกวิธีการคำนวณ hashes ทั้งหมดที่ต้องการในO ( k )เวลา โปรดทราบว่าวิธีการนี้ไม่ได้รับผลกระทบจากฝ่ายตรงข้ามเว้นแต่คุณจะสุ่มเลือกทั้งคู่qppqpqqO(k)พอใจเงื่อนไขที่ต้องการp,q
user21820

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

1
@MichaelKay: ที่จะไม่ทำงานถ้าคุณต้องการที่จะคำนวณแฮชของการเปลี่ยนแปลงที่เป็นไปได้ของสตริงในO ( k )เวลา คุณยังต้องเก็บไว้ที่ไหนซักแห่ง ดังนั้นหากคุณตรวจสอบตำแหน่งเดียวในแต่ละครั้งคุณจะใช้เวลาkเท่าตราบใดที่คุณตรวจสอบตำแหน่งทั้งหมดพร้อมกันโดยใช้kคูณรายการ hashtable จำนวนมาก kO(k)kk
user21820

2

นี่เป็นวิธีการแฮชที่แข็งแกร่งกว่าวิธีการโพลิโนเมียล - แฮช แรกสร้างจำนวนเต็มบวกสุ่มr 1 .. kที่มี coprime ขนาด Hashtable M คือ0 r ฉัน < M จากนั้นกัญชาแต่ละสายx 1 .. kไป( Σ k ฉัน= 1 x ฉันr ฉัน ) mod M นอกจากนี้เกือบไม่มีอะไรเป็นปรปักษ์สามารถทำได้เพื่อทำให้เกิดการชนกันไม่สม่ำเสมอมากเนื่องจากคุณสร้างR 1 .. kในเวลาทำงานและเพื่อให้เป็นkkr1..kM0ri<Mx1..k(i=1kxiri)modMr1..kkเพิ่มความน่าจะเป็นสูงสุดของการปะทะกันของคู่ใดก็ตามของสตริงที่แตกต่างกันไปอย่างรวดเร็วเพื่อ M นอกจากนี้ยังเห็นได้ชัดว่าวิธีการคำนวณในเวลาO ( k )แฮชที่เป็นไปได้ทั้งหมดสำหรับแต่ละสตริงที่เปลี่ยนไปหนึ่งอักขระ1/MO(k)

หากคุณต้องการรับประกันการ hashing อย่างสม่ำเสมอคุณสามารถสร้างหมายเลขธรรมชาติสุ่มหนึ่งน้อยกว่าMสำหรับแต่ละคู่( i , c )สำหรับiจาก1ถึงkและสำหรับอักขระแต่ละตัวcและ hash แต่ละสตริงx 1 .. kถึง( k i = 1 r ( i , x i ))จากนั้นความน่าจะเป็นที่จะเกิดการชนกันของสายอักขระคู่ใด ๆr(i,c)M(i,c)i1kcx1..k(i=1kr(i,xi))modM M วิธีนี้จะดีกว่าถ้าชุดอักขระของคุณค่อนข้างเล็กเมื่อเทียบกับ n1/Mn n


2

อัลกอริทึมมากมายที่โพสต์ที่นี่ใช้พื้นที่บนโต๊ะแฮชค่อนข้างน้อย นี่คือที่เก็บข้อมูลเสริมO ( ( n lg n ) k 2 )O(1)O((nlgn)k2)อัลกอริทึมแบบง่าย

เคล็ดลับคือการใช้ซึ่งเป็นตัวเปรียบเทียบระหว่างสองค่าและที่ผลตอบแทนจริงถ้า< B (lexicographically) ขณะที่ละเลยkตัวอักษร TH จากนั้นอัลกอริทึมมีดังนี้Ck(a,b)aba<bk

ขั้นแรกให้เรียงลำดับสตริงอย่างสม่ำเสมอและทำการสแกนเชิงเส้นเพื่อลบรายการที่ซ้ำกัน

จากนั้นสำหรับแต่ละ :k

  1. เรียงลำดับสตริงด้วยเป็นตัวเปรียบเทียบCk

  2. เงื่อนไขที่แตกต่างเฉพาะในขณะนี้อยู่ติดกันและสามารถตรวจพบในการสแกนเชิงเส้นk


1

สองสายความยาวkแตกต่างกันในตัวละครตัวหนึ่งแบ่งปันคำนำหน้าของความยาวLและคำต่อท้ายของความยาวเมตรเช่นที่k = L + m + 1

คำตอบโดยไซมอน Prins encodes นี้โดยการจัดเก็บทุกคำนำหน้า / ต่อท้ายอยู่รวมกันอย่างชัดเจนคือabcจะกลายเป็น*bc, และa*c ab*นั่นคือ k = 3, l = 0,1,2 และ m = 2,1,0

เมื่อ valarMorghulis ชี้ให้เห็นคุณสามารถจัดระเบียบคำต่าง ๆ ในแผนผังคำนำหน้า นอกจากนี้ยังมีต้นไม้ต่อท้ายที่คล้ายกันมาก มันค่อนข้างง่ายที่จะเพิ่มต้นไม้ด้วยจำนวนของโหนดใบด้านล่างแต่ละคำนำหน้าหรือคำต่อท้าย; สามารถอัปเดตเป็น O (k) เมื่อแทรกคำใหม่

เหตุผลที่คุณต้องการนับพี่น้องเหล่านี้คือเพื่อให้คุณรู้คำใหม่ไม่ว่าคุณต้องการที่จะระบุสตริงทั้งหมดด้วยคำนำหน้าเดียวกันหรือว่าจะระบุสตริงทั้งหมดด้วยคำต่อท้ายเดียวกัน เช่นสำหรับ "abc" เป็นอินพุตคำนำหน้าที่เป็นไปได้คือ "", "a" และ "ab" ในขณะที่คำต่อท้ายที่สอดคล้องกันคือ "bc", "c" และ "" ตามที่เห็นได้ชัดสำหรับคำต่อท้ายสั้น ๆ ดีกว่าที่จะแจกแจงพี่น้องในต้นไม้คำนำหน้าและในทางกลับกัน

@einpoklum ชี้ให้เห็นว่าเป็นไปได้ที่สตริงทั้งหมดจะมีค่าk / 2เหมือนกันคำนำหน้าเหมือนกัน นั่นไม่ใช่ปัญหาสำหรับวิธีนี้ ต้นไม้คำนำหน้าจะเป็นเชิงเส้นสูงถึงความลึก k / 2 โดยแต่ละโหนดถึงความลึก k / 2 เป็นบรรพบุรุษของโหนดใบ 100.000 เป็นผลให้ต้นไม้ต่อท้ายจะใช้ความลึกสูงสุด (k / 2-1) ซึ่งเป็นสิ่งที่ดีเพราะสตริงจะต้องแตกต่างกันในส่วนต่อท้ายของพวกเขาเนื่องจากพวกเขาแบ่งปันคำนำหน้า

[แก้ไข] ในฐานะของการเพิ่มประสิทธิภาพเมื่อคุณได้กำหนดคำนำหน้าสั้น ๆ ที่ไม่ซ้ำกันของสตริงคุณจะรู้ว่าหากมีอักขระที่แตกต่างกันหนึ่งตัวจะต้องเป็นอักขระตัวสุดท้ายของคำนำหน้าและคุณจะพบสิ่งที่ใกล้เคียงกันเมื่อ ตรวจสอบคำนำหน้าที่สั้นกว่า ดังนั้นถ้า "abcde" มีคำนำหน้าสั้นที่สุดที่ไม่ซ้ำกัน "abc" นั่นหมายความว่ามีสตริงอื่น ๆ ที่ขึ้นต้นด้วย "ab?" แต่ไม่ใช่ด้วย "abc" นั่นคือถ้าพวกเขาต่างกันในตัวละครเดียวนั่นก็คือตัวละครตัวที่สาม คุณไม่จำเป็นต้องตรวจสอบ "abc? e" อีกต่อไป

โดยตรรกะเดียวกันถ้าคุณจะพบว่า "cde" เป็นคำต่อท้ายที่สั้นที่สุดที่ไม่ซ้ำกันแล้วคุณรู้ว่าคุณต้องตรวจสอบเฉพาะคำนำหน้าความยาว 2 "ab" และไม่นำหน้า 1 หรือ 3 ความยาว

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


คุณจะบอกว่าสำหรับแต่ละสายและแต่ละ1 ฉันkเราพบโหนดP [ s 1 , ... , s ฉัน- 1 ]สอดคล้องกับ length- ( ฉัน- 1 )คำนำหน้าในคำนำหน้า Trie และ โหนดS [ s i + 1 , , s k ]ตรงกับความยาว- ( k - i - 1 )s1ikP[s1,,si1](i1)S[si+1,,sk](ki1)คำต่อท้ายในส่วนต่อท้าย trie (แต่ละค่าใช้เวลาตัดจำหน่าย) และเปรียบเทียบจำนวนของลูกหลานของแต่ละเลือกใดก็ตามที่มีลูกหลานน้อยลงแล้ว "ละเอียด" สำหรับส่วนที่เหลือของสตริงในคู่นั้น? O(1)
j_random_hacker

1
เวลาที่คุณเข้าใกล้คืออะไร? ดูเหมือนว่าในกรณีที่เลวร้ายที่สุดมันอาจจะเป็นกำลังสอง: ลองคิดดูสิว่าจะเกิดอะไรขึ้นถ้าสตริงทุกตัวเริ่มต้นและลงท้ายด้วยอักขระเดียวกัน k/4
DW

แนวคิดการปรับให้เหมาะสมนั้นฉลาดและน่าสนใจ คุณมีวิธีในการตรวจสอบ mtaches อยู่หรือไม่? หาก "abcde" มีคำนำหน้าสั้นที่สุดที่ไม่ซ้ำกัน "abc" หมายความว่าเราควรตรวจสอบสตริงอื่น ๆ ของแบบฟอร์ม "ab? de" คุณมีวิธีที่จะทำสิ่งนั้นอย่างมีประสิทธิภาพหรือไม่? เวลาทำงานที่เกิดขึ้นคืออะไร?
DW

@DW: ความคิดคือการหาสตริงในรูปแบบ "ab? de" คุณตรวจสอบต้นไม้คำนำหน้าว่ามีโหนดใบไม้จำนวนเท่าใดที่อยู่ใต้ "ab" และในต้นไม้ต่อท้ายจำนวนโหนดที่มีอยู่ภายใต้ "de" จากนั้นเลือก ที่เล็กที่สุดของทั้งสองที่จะระบุ เมื่อสตริงทั้งหมดเริ่มต้นและสิ้นสุดด้วยอักขระ k / 4 เดียวกัน นั่นหมายถึงโหนด k / 4 แรกในต้นไม้ทั้งสองมีลูกหนึ่งคน และใช่ทุกครั้งที่คุณต้องการต้นไม้เหล่านั้นพวกมันจะต้องถูกสำรวจซึ่งเป็นขั้นตอน O (n * k)
MSalters

To check for a string of the form "ab?de" in the prefix trie, it suffices to get to the node for "ab", then for each of its children v, check whether the path "de" exists below v. That is, don't bother enumerating any other nodes in these subtries. This takes O(ah) time, where a is the alphabet size and h is the height of the initial node in the trie. h is O(k), so if the alphabet size is O(n) then it is indeed O(nk)เวลาโดยรวม แต่ตัวอักษรที่เล็กกว่านั้นเป็นเรื่องปกติ จำนวนเด็ก (ไม่ใช่ลูกหลาน) มีความสำคัญเช่นเดียวกับความสูง
j_random_hacker

1

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

ทางเลือกอื่นอาจเป็นการจัดเก็บสตริงในรายการที่เรียงลำดับ เคล็ดลับคือการเรียงลำดับโดยขั้นตอนวิธี hashing ท้องที่ที่มีความอ่อนไหว นี่เป็นอัลกอริธึมการแฮชซึ่งให้ผลลัพธ์ที่คล้ายกันเมื่ออินพุตคล้ายกัน [1]

ทุกครั้งที่คุณต้องการตรวจสอบสตริงคุณสามารถคำนวณแฮชและค้นหาตำแหน่งของแฮชนั้นในรายการที่เรียงลำดับของคุณ (ใช้สำหรับอาร์เรย์หรือO ( n )สำหรับรายการที่เชื่อมโยง) หากคุณพบว่าเพื่อนบ้าน (พิจารณาเพื่อนบ้านที่ใกล้เคียงทั้งหมดไม่เพียง แต่ผู้ที่มีดัชนี +/- 1) ของตำแหน่งนั้นจะคล้ายกัน (ปิดโดยตัวอักษรหนึ่งตัว) ที่คุณพบคู่ที่ตรงกัน หากไม่มีสตริงที่คล้ายกันคุณสามารถแทรกสตริงใหม่ที่ตำแหน่งที่คุณพบ (ซึ่งใช้O ( 1 )สำหรับรายการที่เชื่อมโยงและO ( n )สำหรับอาร์เรย์)O(log(n))O(n)O(1)O(n)

อัลกอริทึมการแฮชที่มีความอ่อนไหวต่อพื้นที่ที่เป็นไปได้อย่างหนึ่งคือNilsimsa (ด้วยการใช้งานโอเพนซอร์ซที่มีให้ในตัวอย่างในไพ ธ อน )

[1]: โปรดทราบว่าบ่อยครั้งที่อัลกอริธึมการแฮชเช่น SHA1 ได้รับการออกแบบมาสำหรับตรงกันข้าม: ผลิตแฮชที่แตกต่างกันอย่างมากสำหรับสิ่งที่คล้ายกัน แต่ไม่เท่ากับอินพุต

คำแถลงการณ์ปฏิเสธความรับผิดชอบ: ตามจริงแล้วฉันจะใช้โซลูชันฝากข้อมูลแบบซ้อนซ้อน / ทรีที่จัดเรียงแบบต้นไม้เป็นการส่วนตัวสำหรับแอปพลิเคชันการผลิต อย่างไรก็ตามแนวคิดการเรียงลำดับทำให้ฉันเป็นทางเลือกที่น่าสนใจ โปรดทราบว่าอัลกอริทึมนี้ขึ้นอยู่กับอัลกอริทึมแฮชที่เลือก Nilsimsa เป็นอัลกอริทึมที่ฉันพบ - มีอีกหลายอย่าง (ตัวอย่างเช่น TLSH, Ssdeep และ Sdhash) ฉันไม่ได้ตรวจสอบว่า Nilsimsa ทำงานกับอัลกอริทึมที่ระบุไว้ของฉัน


1
แนวคิดที่น่าสนใจ แต่ฉันคิดว่าเราจะต้องมีขอบเขตว่าค่าแฮชสองค่าสามารถแตกต่างกันอย่างไรเมื่ออินพุตของพวกเขาแตกต่างกันเพียง 1 ตัวอักษร - จากนั้นสแกนทุกอย่างภายในช่วงค่าแฮชแทนที่จะเป็นแค่เพื่อนบ้าน (เป็นไปไม่ได้ที่จะมีฟังก์ชั่นแฮชที่สร้างค่าแฮชที่อยู่ติดกันสำหรับคู่ของสตริงที่เป็นไปได้ทั้งหมดที่แตกต่างกันไป 1 อักขระพิจารณาสตริงความยาว 2 ในตัวอักษรไบนารี: 00, 01, 10 และ 11 ถ้า h (00) อยู่ติดกับทั้ง h (10) และ h (01) ดังนั้นมันจะต้องอยู่ระหว่างพวกเขาซึ่งในกรณีนี้ h (11) ไม่สามารถอยู่ติดกับพวกเขาทั้งสองและในทางกลับกันได้)
j_random_hacker

ดูเพื่อนบ้านไม่เพียงพอ พิจารณารายการ abcd, acef, agcd มีคู่ที่ตรงกันอยู่ แต่ขั้นตอนของคุณจะไม่พบเพราะ abcd ไม่ใช่เพื่อนบ้านของ agcd
DW

คุณทั้งคู่พูดถูก! กับเพื่อนบ้านฉันไม่ได้หมายถึงเพียง "เพื่อนบ้านโดยตรง" แต่คิดว่า "เพื่อนบ้าน" ของตำแหน่งใกล้ชิด ฉันไม่ได้ระบุจำนวนเพื่อนบ้านที่ต้องดูตั้งแต่นั้นขึ้นอยู่กับอัลกอริทึมแฮช แต่คุณพูดถูกฉันควรจดคำตอบนี้ไว้ ขอบคุณ :)
tessi

1
"LSH... similar items map to the same “buckets” with high probability" - since it's probability algorithm, result isn't guaranteed. So it depends on TS whether he needs 100% solution or 99.9% is enough.
Bulat

1

One could achieve the solution in O(nk+n2) time and O(nk) space using enhanced suffix arrays (Suffix array along with the LCP array) that allows constant time LCP (Longest Common Prefix) query (i.e. Given two indices of a string, what is the length of the longest prefix of the suffixes starting at those indices). Here, we could take advantage of the fact that all strings are of equal length. Specifically,

  1. Build the enhanced suffix array of all the n strings concatenated together. Let X=x1.x2.x3....xn where xi,1in is a string in the collection. Build the suffix array and LCP array for X.

  2. xi(i1)kxixjj<ixjxi=xjxi[p]xj[p]); in this case take another LCP starting at the corresponding positions following the mismatch. If the second LCP goes beyond the end of xj then xi and xj differ by only one character; otherwise there are more than one mismatches.

    for (i=2; i<= n; ++i){
        i_pos = (i-1)k;
        for (j=1; j < i; ++j){
            j_pos = (j-1)k;
            lcp_len = LCP (i_pos, j_pos);
            if (lcp_len < k) { // mismatch
                if (lcp_len == k-1) { // mismatch at the last position
                // Output the pair (i, j)
                }
                else {
                  second_lcp_len = LCP (i_pos+lcp_len+1, j_pos+lcp_len+1);
                  if (lcp_len+second_lcp_len>=k-1) { // second lcp goes beyond
                    // Output the pair(i, j)
                  }
                }
            }
        }
    }
    

You could use SDSL library to build the suffix array in compressed form and answer the LCP queries.

Analysis: Building the enhanced suffix array is linear in the length of X i.e. O(nk). Each LCP query takes constant time. Thus, querying time is O(n2).

Generalisation: This approach can also be generalised to more than one mismatches. In general, running time is O(nk+qn2) where q is the number of allowed mismatches.

If you wish to remove a string from the collection, instead of checking every j<i, you could keep a list of only 'valid' j.


Can i say that O(kn2) algo is trivial - just compare each string pair and count number of matches? And k in this formula practically can be omitted, since with SSE you can count matching bytes in 2 CPU cycles per 16 symbols (i.e. 6 cycles for k=40).
Bulat

Apologies but I could not understand your query. The above approach is O(nk+n2) and not O(kn2). Also, it is virtually alphabet-size independent. It could be used in conjunction with the hash-table approach -- Once two strings are found to have the same hashes, they could be tested if they contain a single mismatch in O(1) time.
Ritu Kundu

My point is that k=20..40 for the question author and comparing such small strings require only a few CPU cycles, so practical difference between brute force and your approach probably doesn't exist.
Bulat

1

การปรับปรุงหนึ่งเดียวกับโซลูชั่นทั้งหมดที่เสนอ พวกเขาทั้งหมดต้องการO(nk)หน่วยความจำในกรณีที่เลวร้ายที่สุด คุณสามารถลดได้โดยการแฮชของสตริงคอมพิวเตอร์กับ*แทนตัวละครแต่ละตัวเช่น*bcde, a*cde... และการประมวลผลที่ผ่านแต่ละสายพันธุ์เท่านั้นที่มีค่าแฮอยู่ในช่วงจำนวนเต็มบางอย่าง เฟที่มีค่าแฮชแม้แต่คู่ในการผ่านครั้งแรกและค่าการแฮชคี่ในอันที่สอง

คุณยังสามารถใช้วิธีนี้เพื่อแยกการทำงานระหว่างคอร์ CPU / GPU หลายคอร์


Clever suggestion! In this case, the original question says n=100,000 and k40, so O(nk) memory doesn't seem likely to be an issue (that might be something like 4MB). Still a good idea worth knowing if one needs to scale this up, though!
D.W.

0

This is a short version of @SimonPrins' answer not involving hashes.

Assuming none of your strings contain an asterisk:

  1. Create a list of size nk where each of your strings occurs in k variations, each having one letter replaced by an asterisk (runtime O(nk2))
  2. Sort that list (runtime O(nk2lognk))
  3. Check for duplicates by comparing subsequent entries of the sorted list (runtime O(nk2))

An alternative solution with implicit usage of hashes in Python (can't resist the beauty):

def has_almost_repeats(strings,k):
    variations = [s[:i-1]+'*'+s[i+1:] for s in strings for i in range(k)]
    return len(set(variations))==k*len(strings)

Thanks. Please also mention the k copies of exact duplicates, and I'll +1. (Hmm, just noticed I made the same claim about O(nk) time in my own answer... Better fix that...)
j_random_hacker

@j_random_hacker I don't know what exactly the OP wants reported, so I left step 3 vague but I think it is trivial with some extra work to report either (a) a binary any duplicate/no duplicates result or (b) a list of pairs of strings that differ in at most one position, without duplicates. If we take the OP literally ("...to see if any two strings..."), then (a) seems to be desired. Also, if (b) were desired then of course simply creating a list of pairs may take O(n2) if all strings are equal
Bananach

0

Here is my take on 2+ mismatches finder. Note that in this post I consider each string as circular, f.e. substring of length 2 at index k-1 consists of symbol str[k-1] followed by str[0]. And substring of length 2 at index -1 is the same!

If we have M mismatches between two strings of length k, they have matching substring with length at least mlen(k,M)=k/M1 since, in the worst case, mismatched symbols split (circular) string into M equal-sized segments. F.e. with k=20 and M=4 the "worst" match may have the pattern abcd*efgh*ijkl*mnop*.

Now, the algorithm for searching all mismatches up to M symbols among strings of k symbols:

  • for each i from 0 to k-1
    • split all strings into groups by str[i..i+L-1], where L = mlen(k,M). F.e. if L=4 and you have alphabet of only 4 symbols (from DNA), this will make 256 groups.
    • Groups smaller than ~100 strings can be checked with brute-force algorithm
    • For larger groups, we should perform secondary division:
      • Remove from every string in the group L symbols we already matched
      • for each j from i-L+1 to k-L-1
        • split all strings into groups by str[i..i+L1-1], where L1 = mlen(k-L,M). F.e. if k=20, M=4, alphabet of 4 symbols, so L=4 and L1=3, this will make 64 groups.
        • the rest is left as exercise for the reader :D

Why we don't start j from 0? Because we already made these groups with the same value of i, so job with j<=i-L will be exactly equivalent to job with i and j values swapped.

Further optimizations:

  • At every position, also consider strings str[i..i+L-2] & str[i+L]. This only doubles amount of jobs created, but allows to increase L by 1 (if my math is correct). So, f.e. instead of 256 groups, you will split data into 1024 groups.
  • If some L[i] becomes too small, we can always use the * trick: for each i in in 0..k-1, remove i'th symbol from each string and create job searching for M-1 mismatches in those strings of length k-1.

0

ฉันทำงานทุกวันในการประดิษฐ์และปรับปรุง algos ดังนั้นหากคุณต้องการประสิทธิภาพทุกบิตนั่นคือแผน:

  • ตรวจสอบกับ*ในแต่ละตำแหน่งอย่างอิสระเช่นแทนการประมวลผลn*kสตริงชุดงานเดียว- เริ่มkงานอิสระแต่ละชุดการตรวจสอบnสตริง คุณสามารถกระจายkงานเหล่านี้ในหลายคอร์ CPU / GPU นี่เป็นสิ่งสำคัญอย่างยิ่งหากคุณจะตรวจสอบความแตกต่างของถ่าน 2+ ขนาดงานที่เล็กลงจะช่วยปรับปรุงตำแหน่งของแคชด้วยตัวเองซึ่งจะทำให้โปรแกรมเร็วขึ้น 10 เท่า
  • หากคุณกำลังจะใช้ตารางแฮชให้ใช้การดำเนินการของคุณเองโดยใช้การตรวจสอบเชิงเส้นและปัจจัยโหลด ~ 50% มันใช้งานง่ายและรวดเร็ว หรือใช้การใช้งานที่มีอยู่ด้วยการเปิดที่อยู่ ตารางแฮช STL ช้าเนื่องจากใช้การโยงแบบแยกกัน
  • คุณอาจลองกรองข้อมูลล่วงหน้าโดยใช้ตัวกรอง Bloom 3 สถานะ (แยก 0/1/1 + ที่เกิดขึ้น) ตามที่เสนอโดย @AlexReynolds
  • สำหรับแต่ละ i จาก 0 ถึง k-1 ให้รันงานต่อไปนี้:
    • สร้างโครงสร้าง 8 ไบต์ที่ประกอบด้วยแฮช 4-5 ไบต์ของแต่ละสตริง (ด้วย*ตำแหน่ง i-th) และดัชนีสตริงจากนั้นเรียงลำดับหรือสร้างตารางแฮชจากระเบียนเหล่านี้

สำหรับการเรียงลำดับคุณอาจลองใช้คอมโบต่อไปนี้:

  • การส่งครั้งแรกคือการเรียง MSD radix ใน 64-256 วิธีใช้เคล็ดลับ TLB
  • การผ่านครั้งที่สองคือการจัดเรียง MSD radix ใน 256-1024 วิธีที่ไม่มีเคล็ดลับ TLB (รวม 64K วิธี)
  • รอบที่สามคือการจัดเรียงการแทรกเพื่อแก้ไขความไม่สอดคล้องที่เหลืออยู่
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.