Cấu trúc dữ liệu hoặc thuật toán để nhanh chóng tìm thấy sự khác biệt giữa các chuỗi


19

Tôi có một mảng gồm 100.000 chuỗi, tất cả độ dài . Tôi muốn so sánh từng chuỗi với mọi chuỗi khác để xem liệu hai chuỗi có khác nhau 1 ký tự không. Ngay bây giờ, khi tôi thêm từng chuỗi vào mảng, tôi đang kiểm tra chuỗi đó với mọi chuỗi đã có trong mảng, có độ phức tạp về thời gian là .n ( n - 1 )kn(n1)2k

Có cấu trúc dữ liệu hoặc thuật toán nào có thể so sánh các chuỗi với nhau nhanh hơn những gì tôi đang làm không?

Một số thông tin bổ sung:

  • Vấn đề thứ tự: abcdexbcdekhác nhau bởi 1 ký tự, trong khi abcdeedcbakhác nhau bởi 4 ký tự.

  • Đối với mỗi cặp chuỗi khác nhau bởi một ký tự, tôi sẽ xóa một trong các chuỗi đó khỏi mảng.

  • Ngay bây giờ, tôi đang tìm kiếm các chuỗi chỉ khác nhau 1 ký tự, nhưng thật tuyệt nếu sự khác biệt 1 ký tự đó có thể được tăng lên thành 2, 3 hoặc 4 ký tự. Tuy nhiên, trong trường hợp này, tôi nghĩ hiệu quả quan trọng hơn khả năng tăng giới hạn chênh lệch ký tự.

  • k thường nằm trong khoảng 20-40.


4
Tìm kiếm một từ điển chuỗi có 1 lỗi là một vấn đề khá nổi tiếng, ví dụ: cs.nyu.edu/~adi/CGL04.pdf
KWillets

1
20-40 người có thể sử dụng một chút không gian. Bạn có thể xem bộ lọc Bloom ( en.wikipedia.org/wiki/Bloom_filter ) để kiểm tra xem các chuỗi suy biến - tập hợp tất cả các mers từ một, hai hoặc nhiều thay thế trên một thử nghiệm - là "có thể trong" hoặc "chắc chắn -không-in "một bộ kmer. Nếu bạn nhận được "có thể trong", thì hãy so sánh thêm hai chuỗi để xác định xem đó có phải là dương tính giả hay không. Các trường hợp "chắc chắn không có" là những tiêu cực thực sự sẽ làm giảm tổng số so sánh từng chữ cái bạn phải làm, bằng cách hạn chế so sánh với các lần truy cập "có thể vào" tiềm năng.
Alex Reynold

Nếu bạn đang làm việc với phạm vi k nhỏ hơn, bạn có thể sử dụng bitet để lưu trữ bảng booleans cho tất cả các chuỗi suy biến (ví dụ: github.com/alapidreynold/kmer-boolean cho ví dụ về đồ chơi). Tuy nhiên, đối với k = 20-40, yêu cầu không gian cho một bit đơn giản là quá nhiều.
Alex Reynold

Câu trả lời:


12

Có thể đạt được thời gian chạy trường hợp xấu nhất .O(nklogk)

Hãy bắt đầu đơn giản. Nếu bạn quan tâm đến một giải pháp dễ thực hiện sẽ hiệu quả trên nhiều đầu vào, nhưng không phải tất cả, thì đây là một giải pháp đơn giản, thực dụng, dễ thực hiện mà nhiều điều kiện thực tế cho nhiều tình huống. Tuy nhiên, nó lại rơi vào thời gian chạy bậc hai trong trường hợp xấu nhất.

Lấy từng chuỗi và lưu trữ nó trong một hashtable, được khóa ở nửa đầu của chuỗi. Sau đó, lặp đi lặp lại trong các thùng băm. Đối với mỗi cặp chuỗi trong cùng một nhóm, hãy kiểm tra xem chúng có khác nhau ở 1 ký tự không (nghĩa là kiểm tra xem nửa thứ hai của chúng có khác nhau trong 1 ký tự không).

Sau đó, dành mỗi chuỗi và lưu nó trong một Hashtable, lần này keyed trên thứ hai nửa của chuỗi. Một lần nữa kiểm tra từng cặp dây trong cùng một thùng.

Giả sử các chuỗi được phân phối tốt, thời gian chạy có thể sẽ là khoảng . Ngoài ra, nếu tồn tại một cặp chuỗi khác nhau 1 ký tự, nó sẽ được tìm thấy trong một trong hai lần đi qua (vì chúng chỉ khác nhau 1 ký tự, ký tự khác nhau đó phải ở nửa đầu hoặc nửa sau của chuỗi, vì vậy nửa thứ hai hoặc nửa đầu của chuỗi phải giống nhau). Tuy nhiên, trong trường hợp xấu nhất (ví dụ: nếu tất cả các chuỗi bắt đầu hoặc kết thúc với cùng một ký tự ), thì điều này sẽ giảm xuống thời gian chạy , vì vậy thời gian chạy trong trường hợp xấu nhất của nó không phải là sự cải thiện về vũ phu lực lượng.k / 2 O ( n 2 k )O(nk)k/2O(n2k)

Là một tối ưu hóa hiệu suất, nếu bất kỳ nhóm nào có quá nhiều chuỗi trong đó, bạn có thể lặp lại quy trình tương tự theo cách đệ quy để tìm kiếm một cặp khác nhau bởi một ký tự. Lệnh gọi đệ quy sẽ nằm trên chuỗi có độ dài .k/2

Nếu bạn quan tâm đến thời gian chạy trường hợp xấu nhất:

Với tối ưu hóa hiệu suất ở trên, tôi tin rằng thời gian chạy trong trường hợp xấu nhất là .O(nklogk)


3
Nếu các chia sẻ cùng một nửa đầu, điều này rất có thể xảy ra trong cuộc sống thực, thì bạn đã không cải thiện sự phức tạp. Ω(n)
einpoklum - phục hồi Monica

@einpoklum, chắc chắn rồi! Đó là lý do tại sao tôi đã viết câu nói trong câu thứ hai của mình rằng nó rơi vào thời gian chạy bậc hai trong trường hợp xấu nhất, cũng như câu trong câu cuối cùng của tôi mô tả cách đạt được độ phức tạp trong trường hợp xấu nhất của nếu bạn quan tâm về trường hợp xấu nhất Nhưng tôi đoán có lẽ tôi đã không thể hiện điều đó rất rõ ràng - vì vậy tôi đã chỉnh sửa câu trả lời của mình cho phù hợp. Bây giờ có tốt hơn không? O(nklogk)
DW

15

Giải pháp của tôi tương tự như j_random_hacker's nhưng chỉ sử dụng một bộ băm duy nhất.

Tôi sẽ tạo ra một chuỗi băm. Đối với mỗi chuỗi trong đầu vào, thêm vào chuỗi đặt . Trong mỗi chuỗi này thay thế một trong các chữ cái bằng một ký tự đặc biệt, không tìm thấy trong bất kỳ chuỗi nào. Trong khi bạn thêm chúng, hãy kiểm tra xem chúng chưa có trong bộ. Nếu có thì bạn có hai chuỗi chỉ khác nhau bởi (nhiều nhất) một ký tự.k

Một ví dụ với các chuỗi 'abc', 'adc'

Đối với abc, chúng tôi thêm '* bc', 'a * c' và 'ab *'

Đối với adc, chúng tôi thêm '* dc', 'a * c' và 'ad *'

Khi chúng tôi thêm 'a * c' lần thứ hai chúng tôi nhận thấy nó đã có trong tập hợp, vì vậy chúng tôi biết rằng có hai chuỗi chỉ khác nhau bởi một chữ cái.

Tổng thời gian chạy của thuật toán này là . Điều này là do chúng ta tạo chuỗi mới cho tất cả chuỗi trong đầu vào. Đối với mỗi chuỗi đó, chúng ta cần tính toán hàm băm, thường mất thời gian .k n O ( k )O(nk2)knO(k)

Lưu trữ tất cả các chuỗi chiếm không gian .O(nk2)

Cải tiến hơn nữa

Chúng ta có thể cải thiện thuật toán hơn nữa bằng cách không lưu trữ các chuỗi đã sửa đổi trực tiếp mà thay vào đó lưu trữ một đối tượng có tham chiếu đến chuỗi gốc và chỉ mục của ký tự được che. Bằng cách này, chúng ta không cần tạo tất cả các chuỗi và chúng ta chỉ cần không gian để lưu trữ tất cả các đối tượng.O(nk)

Bạn sẽ cần phải thực hiện một hàm băm tùy chỉnh cho các đối tượng. Chúng ta có thể lấy việc triển khai Java làm ví dụ, xem tài liệu java . Mã băm java nhân giá trị unicode của mỗi ký tự với (với độ dài chuỗi và chỉ mục một ký tự của ký tự. Lưu ý rằng mỗi chuỗi thay đổi chỉ khác nhau bởi một ký tự so với bản gốc. Chúng ta có thể dễ dàng Tính toán sự đóng góp của ký tự đó vào mã băm. Chúng ta có thể trừ đi và thêm ký tự mặt nạ của chúng ta. Điều này làm cho tính toán. Điều này cho phép chúng ta giảm tổng thời gian chạy xuống k i O ( 1 ) O ( n k )31kikiO(1)O(nk)


4
@JollyJoker Vâng, không gian là một điều đáng quan tâm với phương pháp này. Bạn có thể giảm dung lượng bằng cách không lưu trữ các chuỗi đã sửa đổi mà thay vào đó lưu trữ một đối tượng có tham chiếu đến chuỗi và chỉ mục bị che. Điều đó sẽ để lại cho bạn không gian O (nk).
Simon bắt đầu

Để tính băm cho mỗi chuỗi trong thời gian , tôi nghĩ bạn sẽ cần một hàm băm tự chế đặc biệt (ví dụ: tính băm của chuỗi gốc trong thời gian , sau đó XOR nó với từng chuỗi bị xóa mỗi ký tự trong lần (mặc dù đây có thể là hàm băm khá tệ theo những cách khác)). BTW, điều này khá giống với giải pháp của tôi, nhưng với một hàm băm duy nhất thay vì riêng biệt và thay thế một ký tự bằng "*" thay vì xóa nó. O ( k ) O ( k ) O ( 1 ) kkO(k)O(k)O(1)k
j_random_hacker

@SimonPrins Với tùy chỉnh equalshashCodephương pháp có thể hoạt động. Chỉ cần tạo chuỗi kiểu * b trong các phương thức đó sẽ làm cho nó chống đạn; Tôi nghi ngờ một số câu trả lời khác ở đây sẽ có vấn đề va chạm băm.
JollyJoker

1
@DW Tôi đã sửa đổi bài đăng của mình để phản ánh thực tế rằng việc tính toán các giá trị băm mất thời gian và thêm một giải pháp để đưa tổng thời gian chạy trở lại xuống . O ( n k )O(k)O(nk)
Simon bắt đầu

1
@SimonPrins Trường hợp xấu nhất có thể là nk ^ 2 do kiểm tra tính bằng chuỗi trong hashset.contains khi băm va chạm. Tất nhiên, trường hợp xấu nhất là khi tất cả các chuỗi có băm cùng chính xác, mà sẽ đòi hỏi một tập thủ công khá nhiều các chuỗi, đặc biệt là để có được cùng bảng băm cho *bc, a*c, ab*. Tôi tự hỏi nếu nó có thể được hiển thị không thể?
JollyJoker

7

Tôi sẽ tạo hashtables , mỗi chuỗi có chuỗi -thngngth làm khóa và danh sách các số (ID chuỗi) làm giá trị. Hashtable sẽ chứa tất cả các chuỗi được xử lý cho đến nay nhưng với ký tự ở vị trí đã xóa . Ví dụ: nếu , thì sẽ chứa danh sách tất cả các chuỗi được nhìn thấy cho đến nay có mẫu , trong đó có nghĩa là "bất kỳ ký tự nào". Sau đó, để xử lý chuỗi đầu vào thứ :H 1 , Lôi , H k ( k - 1 ) H i i k = 6 H 3 [ A B D E F ] A B D E F j s jkH1,,Hk(k1)Hiik=6H3[ABDEF]ABDEFjsj

  1. Đối với mỗi trong phạm vi từ 1 đến : kik
    • Biểu mẫu chuỗi bằng cách xóa ký tự thứ khỏi . i s jsjisj
    • Tra cứu . Mỗi ID chuỗi ở đây xác định một chuỗi gốc bằng hoặc chỉ khác nhau ở vị trí . Xuất ra những kết quả này cho phù hợp với chuỗi . (Nếu bạn muốn loại trừ các trùng lặp chính xác, hãy tạo loại giá trị của các hashtables thành một cặp (ID chuỗi, ký tự đã xóa) để bạn có thể kiểm tra những cái có cùng ký tự đã bị xóa khi chúng tôi vừa xóa khỏi .)s i s j s jHi[sj]sisjsj
    • Chèn vào để sử dụng các truy vấn trong tương lai.H ijHi

Nếu chúng ta lưu trữ từng khóa băm một cách rõ ràng, thì chúng ta phải sử dụng không gian và do đó có độ phức tạp về thời gian ít nhất là như vậy. Nhưng như mô tả của Simon Prins , nó có thể đại diện cho một loạt các thay đổi thành một chuỗi (trong trường hợp của ông được mô tả như thay đổi nhân vật duy nhất để , trong tôi như xóa) ngầm trong một cách mà tất cả phím băm cho một nhu cầu chuỗi đặc biệt chỉ Không gian , dẫn đến tổng thể không gian và cũng mở ra khả năng thời gian . Để đạt được độ phức tạp thời gian này, chúng ta cần một cách tính băm cho tất cả các biến thể của chuỗi dài trongk O ( k ) O ( n k ) O ( n k ) k k O ( k )O(nk2)*kO(k)O(nk)O(nk)kkO(k) thời gian: ví dụ, điều này có thể được thực hiện bằng cách sử dụng băm đa thức, như được đề xuất bởi DW (và điều này có thể tốt hơn nhiều so với chỉ đơn giản là XOR ký tự bị xóa bằng hàm băm cho chuỗi gốc).

Thủ thuật đại diện ngầm của Simon Prins cũng có nghĩa là việc "xóa" từng ký tự không thực sự được thực hiện, vì vậy chúng ta có thể sử dụng biểu diễn dựa trên mảng thông thường của chuỗi mà không bị phạt hiệu suất (thay vì danh sách được liên kết như tôi đã đề xuất ban đầu).


2
Giải pháp tốt đẹp. Một ví dụ về hàm băm bespoke phù hợp sẽ là hàm băm đa thức.
DW

Cảm ơn @DW Có lẽ bạn có thể làm rõ một chút ý của bạn về "hàm đa thức"? Googling thuật ngữ đã không cho tôi bất cứ điều gì có vẻ dứt khoát. (Xin vui lòng chỉnh sửa bài viết của tôi trực tiếp nếu bạn muốn.)
j_random_hacker

1
Chỉ cần đọc chuỗi dưới dạng cơ sở số modulo , trong đó là số nguyên tố nhỏ hơn kích thước hashmap của bạn và là gốc nguyên thủy của và nhiều hơn kích thước bảng chữ cái. Nó được gọi là "băm đa thức" bởi vì nó giống như đánh giá đa thức có hệ số được cho bởi chuỗi tại . Tôi sẽ để nó như một bài tập để tìm ra cách tính tất cả các giá trị băm mong muốn trong thời gian . Lưu ý rằng phương pháp này không tránh khỏi đối thủ, trừ khi bạn chọn ngẫu nhiên cả thỏa mãn các điều kiện mong muốn. p p q p q q O ( k ) p , qqppqpqqO(k)p,q
dùng21820

1
Tôi nghĩ rằng giải pháp này có thể được tinh chế bằng cách quan sát rằng chỉ có một trong những k nhu cầu bảng băm để tồn tại ở bất kỳ thời điểm một, do đó làm giảm yêu cầu bộ nhớ.
Michael Kay

1
@MichaelKay: Điều đó sẽ không hiệu quả nếu bạn muốn tính băm của các thay đổi có thể có của một chuỗi trong thời gian . Bạn vẫn cần lưu trữ chúng ở đâu đó. Vì vậy, nếu bạn chỉ kiểm tra một vị trí tại một thời điểm, bạn sẽ mất lần miễn là nếu bạn kiểm tra tất cả các vị trí với nhau bằng cách sử dụng lần nhiều lần mục có thể băm. O ( k ) k kkO(k)kk
dùng21820

2

Đây là một cách tiếp cận hashtable mạnh mẽ hơn so với phương pháp băm đa thức. Đầu tiên tạo ra ngẫu nhiên số nguyên dương nguyên tố cùng nhau với kích thước Hashtable . Cụ thể, . Sau đó băm mỗi chuỗi để . Hầu như không có gì mà một kẻ thù có thể làm để gây ra các va chạm rất không đồng đều, vì bạn tạo trong thời gian chạy và do đó tăng xác suất va chạm tối đa của bất kỳ cặp chuỗi riêng biệt nào sẽ nhanh chóng lên . Rõ ràng là làm thế nào để tính toán trongr 1 .. k M 0 r i < M x 1 .. k ( k i = 1 x i r i ) mod M r 1 .. k k 1 / M O ( k )kr1..kM0ri<Mx1..k(i=1kxiri)modMr1..kk1/MO(k) thời gian tất cả các giá trị băm có thể cho mỗi chuỗi với một ký tự được thay đổi.

Nếu bạn thực sự muốn đảm bảo băm đồng nhất, bạn có thể tạo một số tự nhiên ngẫu nhiên nhỏ hơn cho mỗi cặp cho từ đến và cho mỗi ký tự , sau đó băm từng chuỗi để . Sau đó, xác suất va chạm của bất kỳ cặp cho các chuỗi biệt là chính xác . Cách tiếp cận này tốt hơn nếu bộ ký tự của bạn tương đối nhỏ so với .M ( i , c ) i 1 k c x 1 .. k ( k i = 1 r ( i , x i ) ) mod M 1 / M nr(i,c)M(i,c)i1kcx1..k(i=1kr(i,xi))modM1/Mn


2

Rất nhiều thuật toán được đăng ở đây sử dụng khá nhiều khoảng trống trên các bảng băm. Dưới đây là một phụ trợ lưu trữ thời gian chạy thuật toán đơn giản.O ( ( n lg n ) k 2 )O(1)O((nlgn)k2)

Thủ thuật là sử dụng , là công cụ so sánh giữa hai giá trị và trả về giá trị true nếu (từ vựng) trong khi bỏ qua ký tự thứ . Sau đó, thuật toán như sau.a b a < b kCk(a,b)aba<bk

Đầu tiên, chỉ cần sắp xếp các chuỗi thường xuyên và thực hiện quét tuyến tính để loại bỏ bất kỳ trùng lặp.

Sau đó, với mỗi :k

  1. Sắp xếp các chuỗi với làm bộ so sánh.Ck

  2. Các chuỗi chỉ khác nhau ở hiện liền kề và có thể được phát hiện trong quá trình quét tuyến tính.k


1

Hai chuỗi có độ dài k , khác nhau trong một ký tự, chia sẻ tiền tố có độ dài l và hậu tố có độ dài m sao cho k = l + m + 1 .

Câu trả lời của Simon Prins mã hóa này bằng cách lưu trữ tất cả các tiền tố / hậu tố kết hợp một cách rõ ràng, tức là abctrở thành *bc, a*cab*. Đó là k = 3, l = 0,1,2 và m = 2,1,0.

Như valarMorghulis chỉ ra, bạn có thể sắp xếp các từ trong cây tiền tố. Cũng có cây hậu tố rất giống nhau. Khá dễ dàng để tăng cây với số lượng nút lá bên dưới mỗi tiền tố hoặc hậu tố; điều này có thể được cập nhật trong O (k) khi chèn một từ mới.

Lý do bạn muốn các số anh chị em này là để bạn biết, đưa ra một từ mới, cho dù bạn muốn liệt kê tất cả các chuỗi có cùng một tiền tố hay liệu có thể liệt kê tất cả các chuỗi có cùng hậu tố hay không. Ví dụ: "abc" là đầu vào, các tiền tố có thể là "", "a" và "ab", trong khi các hậu tố tương ứng là "bc", "c" và "". Rõ ràng, đối với các hậu tố ngắn, tốt hơn là liệt kê anh chị em trong cây tiền tố và ngược lại.

Như @einpoklum chỉ ra, chắc chắn tất cả các chuỗi đều có chung tiền tố k / 2 . Đó không phải là vấn đề đối với phương pháp này; cây tiền tố sẽ được tuyến tính lên đến độ sâu k / 2 với mỗi nút có độ sâu tới k / 2 là tổ tiên của 100.000 nút lá. Do đó, cây hậu tố sẽ được sử dụng tới độ sâu (k / 2-1), điều này rất tốt vì các chuỗi phải khác nhau trong các hậu tố của chúng do chúng có chung tiền tố.

[sửa] Để tối ưu hóa, khi bạn đã xác định tiền tố duy nhất ngắn nhất của chuỗi, bạn biết rằng nếu có một ký tự khác, thì đó phải là ký tự cuối cùng của tiền tố và bạn đã tìm thấy tiền tố gần như trùng lặp khi kiểm tra một tiền tố ngắn hơn một. Vì vậy, nếu "abcde" có tiền tố duy nhất ngắn nhất "abc", điều đó có nghĩa là có các chuỗi khác bắt đầu bằng "ab?" nhưng không phải với "abc". Tức là nếu họ chỉ khác nhau ở một nhân vật, đó sẽ là nhân vật thứ ba. Bạn không cần phải kiểm tra "abc? E" nữa.

Theo cùng một logic, nếu bạn thấy rằng "cde" là một hậu tố ngắn nhất duy nhất, thì bạn biết rằng bạn chỉ cần kiểm tra tiền tố độ dài 2 "ab" chứ không phải tiền tố dài 1 hoặc 3.

Lưu ý rằng phương pháp này chỉ hoạt động với chính xác một sự khác biệt của một ký tự và không khái quát thành 2 sự khác biệt về ký tự, nó dựa vào một ký tự là sự tách biệt giữa các tiền tố giống hệt nhau và các hậu tố giống hệt nhau.


Bạn có gợi ý rằng với mỗi chuỗi và mỗi , chúng ta sẽ tìm thấy nút tương ứng với tiền tố length- trong tiền tố trie và nút tương ứng với trong hậu tố trie (mỗi lần mất thời gian được khấu hao ) và so sánh số lượng Con cháu của mỗi người, chọn con nào có ít con cháu hơn, và sau đó "thăm dò" cho phần còn lại của chuỗi trong trie đó? 1 i k P [ s 1 , ... , s i - 1 ] ( i - 1 ) S [ s i + 1 , ... , s k ] ( k - i - 1 ) O ( 1 )s1ikP[s1,,si1](i1)S[si+1,,sk](ki1)O(1)
j_random_hacker

1
Thời gian chạy của phương pháp của bạn là gì? Đối với tôi, trong trường hợp xấu nhất, nó có thể là bậc hai: xem xét điều gì xảy ra nếu mọi chuỗi bắt đầu và kết thúc với cùng một ký tự . k/4
DW

Ý tưởng tối ưu hóa là thông minh và thú vị. Bạn đã có một cách đặc biệt để kiểm tra mtaches chưa? Nếu "abcde" có tiền tố duy nhất ngắn nhất "abc", điều đó có nghĩa là chúng ta nên kiểm tra một số chuỗi khác có dạng "ab? De". Bạn đã có trong đầu một cách đặc biệt để làm điều đó, điều đó sẽ hiệu quả? Thời gian chạy kết quả là gì?
DW

@DW: Ý tưởng là để tìm các chuỗi ở dạng "ab? De", bạn kiểm tra cây tiền tố có bao nhiêu nút lá tồn tại bên dưới "ab" và trong cây hậu tố có bao nhiêu nút tồn tại dưới "de", sau đó chọn nhỏ nhất trong hai để liệt kê. Khi tất cả các chuỗi bắt đầu và kết thúc với cùng k / 4 ký tự; điều đó có nghĩa là các nút k / 4 đầu tiên trong cả hai cây đều có một con. Và vâng, mỗi khi bạn cần những cái cây đó, chúng phải được chuyển qua bước O (n * k).
MSalters

Để kiểm tra một chuỗi có dạng "ab? De" trong bộ ba tiền tố, nó đủ để đến nút cho "ab", sau đó cho mỗi con của nó , kiểm tra xem đường dẫn "de" có tồn tại bên dưới . Đó là, đừng bận tâm đến việc liệt kê bất kỳ nút nào khác trong các mục phụ này. Điều này làm mất thời gian , trong đó là kích thước bảng chữ cái và là chiều cao của nút ban đầu trong trie. là , vì vậy nếu kích thước bảng chữ cái là thì đó thực sự là thời gian , nhưng bảng chữ cái nhỏ hơn là phổ biến. Số lượng trẻ em (không phải con cháu) rất quan trọng, cũng như chiều cao. v O ( a h ) a h h O ( k ) O ( n ) O ( n k )vvO(ah)ahhO(k)O(n)O(nk)
j_random_hacker

1

Lưu trữ các chuỗi trong xô là một cách tốt (đã có những câu trả lời khác nhau phác thảo điều này).

Một giải pháp thay thế có thể là lưu trữ các chuỗi trong danh sách được sắp xếp . Bí quyết là sắp xếp theo thuật toán băm nhạy cảm cục bộ . Đây là một thuật toán băm mang lại kết quả tương tự khi đầu vào tương tự [1].

Mỗi lần bạn muốn điều tra một chuỗi, bạn có thể tính toán hàm băm của chuỗi đó và tra cứu vị trí của hàm băm đó trong danh sách được sắp xếp của bạn (lấy cho mảng hoặc cho danh sách được liên kết). Nếu bạn thấy rằng những người hàng xóm (xem xét tất cả những người hàng xóm thân thiết, không chỉ những người có chỉ số +/- 1) của vị trí đó cũng tương tự (tắt bởi một ký tự), bạn đã tìm thấy kết quả khớp của mình. Nếu không có chuỗi tương tự, bạn có thể chèn chuỗi mới vào vị trí bạn tìm thấy (lấy cho danh sách được liên kết và cho mảng).O ( n ) O ( 1 ) O ( n )O(log(n))O(n)O(1)O(n)

Một thuật toán băm nhạy cảm cục bộ có thể là Nilsimsa (với triển khai nguồn mở có sẵn, ví dụ như trong python ).

[1]: Lưu ý rằng các thuật toán băm thường, như SHA1, được thiết kế ngược lại: tạo ra các giá trị băm khác nhau rất lớn cho các đầu vào tương tự, nhưng không bằng nhau.

Tuyên bố miễn trừ trách nhiệm: Thành thật mà nói, cá nhân tôi sẽ triển khai một trong những giải pháp xô tổ chức / cây được tổ chức cho một ứng dụng sản xuất. Tuy nhiên, ý tưởng danh sách được sắp xếp đánh tôi như một sự thay thế thú vị. Lưu ý rằng thuật toán này phụ thuộc nhiều vào thuật toán băm chọn. Nilsimsa là một thuật toán tôi tìm thấy - còn nhiều thuật toán nữa (ví dụ TLSH, Ssdeep và Sdhash). Tôi chưa xác minh rằng Nilsimsa hoạt động với thuật toán được phác thảo của tôi.


1
Ý tưởng thú vị, nhưng tôi nghĩ rằng chúng ta sẽ cần có một số giới hạn về việc hai giá trị băm có thể cách nhau bao xa khi đầu vào của chúng chỉ khác nhau 1 ký tự - sau đó quét mọi thứ trong phạm vi giá trị băm đó, thay vì chỉ là hàng xóm. (Không thể có hàm băm tạo ra các giá trị băm liền kề cho tất cả các cặp chuỗi có thể khác nhau 1 ký tự. Hãy xem xét các chuỗi có độ dài 2 trong bảng chữ cái nhị phân: 00, 01, 10 và 11. Nếu h (00) là liền kề với cả h (10) và h (01) thì phải nằm giữa chúng, trong trường hợp h (11) không thể liền kề với cả hai và ngược lại.)
j_random_hacker

Nhìn vào hàng xóm là không đủ. Hãy xem xét danh sách abcd, acef, agcd. Có tồn tại một cặp phù hợp, nhưng thủ tục của bạn sẽ không tìm thấy nó, vì abcd không phải là hàng xóm của agcd.
DW

Cả hai bạn đều đúng! Với hàng xóm tôi không chỉ có nghĩa là "hàng xóm trực tiếp" mà nghĩ đến "một khu phố" của những vị trí gần gũi. Tôi đã không xác định có bao nhiêu hàng xóm cần được xem xét vì điều đó phụ thuộc vào thuật toán băm. Nhưng bạn nói đúng, có lẽ tôi nên ghi lại điều này trong câu trả lời của tôi. cảm ơn :)
tessi

1
"LSH ... ánh xạ các mặt hàng tương tự vào cùng một xô xô có xác suất cao" - vì thuật toán xác suất của nó, kết quả không được đảm bảo. Vì vậy, nó phụ thuộc vào TS cho dù anh ta cần 100% giải pháp hay 99,9% là đủ.
Bulat

1

Người ta có thể đạt được giải pháp trong không gian thời gian và không gian bằng cách sử dụng mảng hậu tố nâng cao ( mảng Suffix cùng với mảng LCP ) cho phép truy vấn LCP (Tiền tố chung dài nhất) không đổi (tức là Cho hai chỉ số của một chuỗi, độ dài của tiền tố dài nhất của các hậu tố bắt đầu từ các chỉ số đó). Ở đây, chúng ta có thể tận dụng thực tế là tất cả các chuỗi có độ dài bằng nhau. Đặc biệt,O ( n k )O(nk+n2)O(nk)

  1. Xây dựng mảng hậu tố nâng cao của tất cả các chuỗi nối với nhau. Đặt trong đó là một chuỗi trong bộ sưu tập. Xây dựng mảng hậu tố và mảng LCP cho .X = x 1 . x 2 . x 3 . . . . x n x i , 1 i n XnX=x1.x2.x3....xnxi,1inX

  2. Bây giờ mỗi bắt đầu ở vị trí trong lập chỉ mục dựa trên zero. Đối với mỗi chuỗi , hãy lấy LCP với mỗi chuỗi sao cho . Nếu LCP vượt quá cuối thì . Mặt khác, có một sự không phù hợp (giả sử ); trong trường hợp này, hãy lấy một LCP khác bắt đầu tại các vị trí tương ứng sau sự không phù hợp. Nếu LCP thứ hai vượt quá cuối thì và khác nhau bởi một ký tự; mặt khác, có nhiều hơn một sự không phù hợp ( i - 1 ) k x i x j j < i x j x i = x j x i [ p ] x j [ p ] x j x i x jxi(i1)kxixjj<ixjxi=xjxi[p]xj[p]xjxixj

    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)
                  }
                }
            }
        }
    }
    

Bạn có thể sử dụng thư viện SDSL để xây dựng mảng hậu tố ở dạng nén và trả lời các truy vấn LCP.

Phân tích: Xây dựng mảng hậu tố nâng cao là tuyến tính theo chiều dài củatức là. Mỗi truy vấn LCP mất thời gian không đổi. Do đó, thời gian truy vấn là.XO(nk)O(n2)

Khái quát hóa: Cách tiếp cận này cũng có thể được khái quát thành nhiều hơn một sự không phù hợp. Nói chung, thời gian chạy làtrong đólà số lượng không phù hợp được phép.O(nk+qn2)q

Nếu bạn muốn xóa một chuỗi khỏi bộ sưu tập, thay vì kiểm tra mọi , bạn có thể giữ một danh sách chỉ có 'hợp lệ' .j<ij


Tôi có thể nói rằng algo là tầm thường không - chỉ cần so sánh từng cặp chuỗi và số lượng trận đấu? Và trong công thức này thực tế có thể được bỏ qua, vì với SSE, bạn có thể đếm các byte phù hợp trong 2 chu kỳ CPU trên 16 ký hiệu (tức là 6 chu kỳ cho k = 40). O(kn2)k
Bulat

Xin lỗi nhưng tôi không thể hiểu truy vấn của bạn. Cách tiếp cận trên là chứ không phải . Ngoài ra, nó là độc lập kích thước bảng chữ cái. Nó có thể được sử dụng cùng với cách tiếp cận bảng băm - Một khi hai chuỗi được tìm thấy có cùng giá trị băm, chúng có thể được kiểm tra nếu chúng có chứa một sự không khớp duy nhất trong thời gian . O(nk+n2)O(kn2)O(1)
Ritu Kundu

Quan điểm của tôi là k = 20..40 đối với tác giả câu hỏi và so sánh các chuỗi nhỏ như vậy chỉ cần một vài chu kỳ CPU, vì vậy sự khác biệt thực tế giữa lực lượng vũ phu và cách tiếp cận của bạn có lẽ không tồn tại.
Bulat

1

Một cải tiến cho tất cả các giải pháp được đề xuất. Tất cả đều yêu cầu bộ nhớ trong trường hợp xấu nhất. Bạn có thể giảm nó bằng cách tính toán băm đầu dây thay vì mỗi nhân vật, tức là , ... và xử lý tại mỗi đường chuyền chỉ biến thể với giá trị băm trong phạm vi số nguyên nào đó. Fe với các giá trị băm chẵn trong lần đầu tiên và giá trị băm lẻ trong lần thứ hai.O(nk)**bcdea*cde

Bạn cũng có thể sử dụng phương pháp này để phân chia công việc giữa nhiều lõi CPU / GPU.


Khéo léo gợi ý! Trong trường hợp này, câu hỏi ban đầu cho biết và , vì vậy bộ nhớ dường như không phải là vấn đề (có thể giống như 4MB). Tuy nhiên, vẫn là một ý tưởng tốt đáng để biết nếu một người cần phải mở rộng quy mô này! n=100,000k40O(nk)
DW

0

Đây là phiên bản ngắn của câu trả lời của @SimonPrins không liên quan đến băm.

Giả sử không có chuỗi nào của bạn chứa dấu hoa thị:

  1. Tạo một danh sách kích thước trong đó mỗi chuỗi của bạn xuất hiện trong biến thể, mỗi chuỗi có một chữ cái được thay thế bằng dấu hoa thị (runtime )k O ( n k 2 )nkkO(nk2)
  2. Sắp xếp danh sách đó (runtime )O(nk2lognk)
  3. Kiểm tra trùng lặp bằng cách so sánh các mục tiếp theo của danh sách được sắp xếp (runtime )O(nk2)

Một giải pháp thay thế với việc sử dụng hàm băm trong Python (không thể cưỡng lại vẻ đẹp):

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)

kO(nk)

O(n2)

0

Đây là mất của tôi trên 2+ công cụ tìm không khớp. Lưu ý rằng trong bài đăng này, tôi coi mỗi chuỗi là hình tròn, chuỗi con có độ dài 2 tại chỉ mục k-1bao gồm ký hiệu str[k-1]theo sau str[0]. Và chuỗi con có độ dài 2 tại chỉ số -1là như nhau!

Mkmlen(k,M)=k/M1Mk=20M=4abcd*efgh*ijkl*mnop*

Bây giờ, thuật toán tìm kiếm tất cả sự không phù hợp với Mcác ký hiệu giữa các chuỗi kký hiệu:

  • cho mỗi i từ 0 đến k-1
    • chia tất cả các chuỗi thành các nhóm bởi str[i..i+L-1], ở đâu L = mlen(k,M). Fe nếu L=4và bạn có bảng chữ cái chỉ có 4 ký hiệu (từ DNA), điều này sẽ tạo thành 256 nhóm.
    • Có thể kiểm tra các nhóm nhỏ hơn ~ 100 chuỗi bằng thuật toán brute-force
    • Đối với các nhóm lớn hơn, chúng ta nên thực hiện phân chia thứ cấp:
      • Xóa khỏi mọi chuỗi trong các Lký hiệu nhóm mà chúng ta đã khớp
      • cho mỗi j từ i-L + 1 đến kL-1
        • chia tất cả các chuỗi thành các nhóm bởi str[i..i+L1-1], ở đâu L1 = mlen(k-L,M). Fe nếu k=20, M=4, alphabet of 4 symbols, vì vậy L=4L1=3, điều này sẽ tạo ra 64 nhóm.
        • phần còn lại là bài tập cho người đọc: D

Tại sao chúng ta không bắt đầu jtừ 0? Bởi vì chúng tôi đã tạo các nhóm này có cùng giá trị i, do đó, công việc với j<=i-Lsẽ hoàn toàn tương đương với công việc với các giá trị i và j được hoán đổi.

Tối ưu hóa hơn nữa:

  • Tại mọi vị trí, cũng xem xét các chuỗi str[i..i+L-2] & str[i+L]. Điều này chỉ tăng gấp đôi số lượng công việc được tạo, nhưng cho phép tăng thêm L1 (nếu toán của tôi đúng). Vì vậy, thay vì 256 nhóm, bạn sẽ chia dữ liệu thành 1024 nhóm.
  • L[i]*0..k-1M-1k-1

0

Tôi làm việc hàng ngày về phát minh và tối ưu hóa thuật toán, vì vậy nếu bạn cần từng chút hiệu suất cuối cùng, đó là kế hoạch:

  • Kiểm tra *độc lập ở từng vị trí, tức là thay vì các biến n*kthể chuỗi xử lý công việc đơn lẻ - bắt đầu kcác công việc độc lập mỗi nchuỗi kiểm tra . Bạn có thể trải rộng các kcông việc này giữa nhiều lõi CPU / GPU. Điều này đặc biệt quan trọng nếu bạn định kiểm tra 2+ char diffs. Quy mô công việc nhỏ hơn cũng sẽ cải thiện cục bộ bộ đệm, điều này có thể làm cho chương trình nhanh hơn gấp 10 lần.
  • Nếu bạn định sử dụng bảng băm, hãy sử dụng triển khai riêng của bạn bằng cách sử dụng thăm dò tuyến tính và hệ số tải ~ 50%. Nó nhanh và khá dễ thực hiện. Hoặc sử dụng một triển khai hiện có với địa chỉ mở. Bảng băm STL chậm do sử dụng chuỗi riêng biệt.
  • Bạn có thể thử lọc trước dữ liệu bằng bộ lọc Bloom 3 trạng thái (phân biệt các lần xuất hiện 0/1/1 +) theo đề xuất của @AlexReynold.
  • Đối với mỗi i từ 0 đến k-1 chạy công việc sau:
    • Tạo các cấu trúc 8 byte chứa hàm băm 4-5 byte của mỗi chuỗi (với *vị trí thứ i) và chỉ mục chuỗi, sau đó sắp xếp chúng hoặc xây dựng bảng băm từ các bản ghi này.

Để sắp xếp, bạn có thể thử kết hợp sau:

  • Vượt qua đầu tiên là phân loại radix MSD theo 64-256 cách sử dụng thủ thuật TLB
  • đường chuyền thứ hai là sắp xếp cơ số MSD theo 256-1024 cách với thủ thuật TLB (tổng cộng 64K cách)
  • vượt qua thứ ba là sắp xếp chèn để khắc phục sự không nhất quán còn lại
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.