Đưa ra hai chuỗi, tìm sự trùng lặp tối đa giữa kết thúc của một và bắt đầu khác


11

Tôi cần tìm một mã hiệu quả (giả) để giải quyết vấn đề sau:

Cho hai chuỗi các số nguyên (không nhất thiết phải phân biệt) (a[1], a[2], ..., a[n])(b[1], b[2], ..., b[n]), tìm tối đa dnhư vậy a[n-d+1] == b[1], a[n-d+2] == b[2], ..., và a[n] == b[d].

Đây không phải là bài tập về nhà, tôi thực sự đã nghĩ ra điều này khi cố gắng hợp đồng hai chất căng dọc theo càng nhiều chiều càng tốt. Tôi nghi ngờ một thuật toán hiệu quả tồn tại (có thể O(n)?), Nhưng tôi không thể nghĩ ra thứ gì đó không có O(n^2). Cách O(n^2)tiếp cận sẽ là vòng lặp rõ ràng dvà sau đó là vòng lặp bên trong trên các mục để kiểm tra điều kiện cần thiết cho đến khi đạt mức tối đa d. Nhưng tôi nghi ngờ một cái gì đó tốt hơn điều này là có thể.


Nếu một hàm băm lăn có thể được tính toán cho nhóm các đối tượng trong mảng của bạn, tôi nghĩ rằng điều này có thể được thực hiện hiệu quả hơn. Tính toán hàm băm cho các phần tử b[1] to b[d]và sau đó chuyển sang atính toán băm mảng a[1] to a[d]nếu phù hợp với câu trả lời của bạn, nếu không tính toán hàm băm a[2] to a[d+1]bằng cách sử dụng lại hàm băm được tính toán a[1] to a[d]. Nhưng tôi không biết liệu các đối tượng trong mảng có thể chấp nhận được một hàm băm lăn được tính toán trên chúng hay không.
SomeDude

2
@becko Xin lỗi, tôi nghĩ cuối cùng tôi cũng hiểu những gì bạn đang cố gắng thực hiện. Đó là tìm sự trùng lặp tối đa giữa điểm cuối avới điểm bắt đầu b. Như thế này .
dùng3386109

1
Dường như với tôi rằng vấn đề là một biến thể của khớp chuỗi, có thể được giải quyết bằng một biến thể trên thuật toán Knuth mật Morris Morris Pratt . Thời gian chạy sẽ là O (m + n) trong đó msố lượng phần tử trong anlà số phần tử trong b. Thật không may, tôi không có đủ kinh nghiệm với KMP để cho bạn biết cách điều chỉnh nó.
dùng3386109

1
@ user3386109 giải pháp của tôi cũng là một biến thể của thuật toán khớp chuỗi có tên là Rabin-Karp , sử dụng phương thức của Horner làm hàm băm.
Daniel

1
@Daniel Ah, tôi biết rằng tôi đã thấy một hàm băm lăn được sử dụng ở đâu đó, nhưng không thể nhớ ở đâu :)
user3386109

Câu trả lời:


5

Bạn có thể sử dụng thuật toán z , thuật toán thời gian tuyến tính ( O (n) ):

Cho một chuỗi S có độ dài n, Thuật toán Z tạo ra một mảng Z trong đó Z [i] là độ dài của chuỗi con dài nhất bắt đầu từ S [i] cũng là tiền tố của S

Bạn cần nối các mảng của bạn ( b + a ) và chạy thuật toán trên mảng được xây dựng kết quả cho đến i đầu tiên sao cho Z [i] + i == m + n .

Ví dụ: với a = [1, 2, 3, 6, 2, 3] & b = [2, 3, 6, 2, 1, 0], phép nối sẽ là [2, 3, 6, 2, 1 , 0, 1, 2, 3, 6, 2, 3] sẽ mang lại Z [10] = 2 hoàn thành Z [i] + i = 12 = m + n .


Xinh đẹp! Cảm ơn.
vẫy gọi

3

Đối với độ phức tạp thời gian / không gian O (n), mẹo là đánh giá giá trị băm cho mỗi lần tiếp theo. Hãy xem xét mảng b:

[b1 b2 b3 ... bn]

Sử dụng phương pháp của Horner , bạn có thể đánh giá tất cả các giá trị băm có thể cho mỗi lần tiếp theo. Chọn một giá trị cơ sở B(lớn hơn bất kỳ giá trị nào trong cả hai mảng của bạn):

from b1 to b1 = b1 * B^1
from b1 to b2 = b1 * B^1 + b2 * B^2
from b1 to b3 = b1 * B^1 + b2 * B^2 + b3 * B^3
...
from b1 to bn = b1 * B^1 + b2 * B^2 + b3 * B^3 + ... + bn * B^n

Lưu ý rằng bạn có thể đánh giá từng chuỗi trong thời gian O (1), bằng cách sử dụng kết quả của chuỗi trước đó, do đó tất cả các chi phí công việc O (n).

Bây giờ bạn có một mảng Hb = [h(b1), h(b2), ... , h(bn)], Hb[i]băm từ đâu b1đến bi.

Làm điều tương tự cho mảng a, nhưng với một mẹo nhỏ:

from an to an   =  (an   * B^1)
from an-1 to an =  (an-1 * B^1) + (an * B^2)
from an-2 to an =  (an-2 * B^1) + (an-1 * B^2) + (an * B^3)
...
from a1 to an   =  (a1   * B^1) + (a2 * B^2)   + (a3 * B^3) + ... + (an * B^n)

Bạn phải lưu ý rằng, khi bạn chuyển từ chuỗi này sang chuỗi khác, bạn nhân toàn bộ chuỗi trước đó với B và thêm giá trị mới nhân với B. Ví dụ:

from an to an =    (an   * B^1)

for the next sequence, multiply the previous by B: (an * B^1) * B = (an * B^2)
now sum with the new value multiplied by B: (an-1 * B^1) + (an * B^2) 
hence:

from an-1 to an =  (an-1 * B^1) + (an * B^2)

Bây giờ bạn có một mảng Ha = [h(an), h(an-1), ... , h(a1)], Ha[i]băm từ đâu aiđến an.

Bây giờ, bạn có thể so sánh Ha[d] == Hb[d]cho tất cả các dgiá trị từ n đến 1, nếu chúng khớp, bạn có câu trả lời của mình.


CHÚ Ý : đây là phương pháp băm, các giá trị có thể lớn và bạn có thể phải sử dụng phương pháp lũy thừa nhanh và các phép đo mô đun , có thể (hầu như) không cho bạn va chạm , khiến phương pháp này không hoàn toàn an toàn. Một thực hành tốt là chọn một cơ sở Blàm số nguyên tố thực sự lớn (ít nhất là lớn hơn giá trị lớn nhất trong mảng của bạn). Bạn cũng nên cẩn thận vì các giới hạn của các con số có thể tràn qua mỗi bước, vì vậy bạn sẽ phải sử dụng (modulo K) trong mỗi thao tác (trong đó Kcó thể là số nguyên tố lớn hơn B).

Điều này có nghĩa là hai chuỗi khác nhau có thể có cùng một hàm băm, nhưng hai chuỗi bằng nhau sẽ luôn có cùng một hàm băm.


Bạn có thể vui lòng bắt đầu câu trả lời này với một đánh giá về các yêu cầu tài nguyên?
greybeard

2

Điều này thực sự có thể được thực hiện trong thời gian tuyến tính, không gian thêm O (n)O (n) . Tôi sẽ giả sử các mảng đầu vào là các chuỗi ký tự, nhưng điều này không cần thiết.

Một phương thức ngây thơ sẽ - sau khi khớp các ký tự k bằng nhau - tìm một ký tự không khớp và quay lại các đơn vị k-1 trong a , đặt lại chỉ mục trong b , rồi bắt đầu quá trình khớp từ đó. Điều này rõ ràng đại diện cho một trường hợp xấu nhất O (n²) .

Để tránh quá trình quay lui này, chúng ta có thể quan sát rằng việc quay lại không hữu ích nếu chúng ta không gặp phải ký tự b [0] trong khi quét các ký tự k-1 cuối cùng . Nếu chúng ta đã tìm thấy ký tự đó, thì việc quay lại vị trí đó sẽ chỉ hữu ích, nếu trong chuỗi con có kích thước k đó, chúng ta có sự lặp lại định kỳ.

Chẳng hạn, nếu chúng ta xem chuỗi con "abcabc" ở đâu đó trong ab là "abcabd" và chúng ta thấy rằng ký tự cuối cùng của b không khớp, chúng ta phải xem xét rằng một trận đấu thành công có thể bắt đầu từ "a" thứ hai trong chuỗi con và chúng ta nên di chuyển chỉ số hiện tại của mình trở lại b trước khi tiếp tục so sánh.

Ý tưởng là sau đó thực hiện một số tiền xử lý dựa trên chuỗi b để ghi lại các tham chiếu ngược trong b rất hữu ích để kiểm tra khi có sự không phù hợp. Vì vậy, ví dụ, nếu b là "acaacaacd", chúng ta có thể xác định các phản hồi dựa trên 0 này (đặt bên dưới mỗi ký tự):

index: 0 1 2 3 4 5 6 7 8
b:     a c a a c a a c d
ref:   0 0 0 1 0 0 1 0 5

Ví dụ: nếu chúng ta có một "acaacaaca" thì sự không khớp đầu tiên xảy ra với ký tự cuối cùng. Các thông tin trên sau đó cho biết thuật toán quay trở lại b đến chỉ số 5, vì "acaac" là phổ biến. Và sau đó, chỉ với việc thay đổi chỉ mục hiện tại trong b, chúng ta có thể tiếp tục khớp ở chỉ số hiện tại của a . Trong ví dụ này, trận đấu của nhân vật cuối cùng sẽ thành công.

Với điều này, chúng tôi có thể tối ưu hóa tìm kiếm và đảm bảo rằng chỉ mục trong một luôn có thể tiến lên phía trước.

Đây là cách triển khai ý tưởng đó trong JavaScript, chỉ sử dụng cú pháp cơ bản nhất của ngôn ngữ đó:

function overlapCount(a, b) {
    // Deal with cases where the strings differ in length
    let startA = 0;
    if (a.length > b.length) startA = a.length - b.length;
    let endB = b.length;
    if (a.length < b.length) endB = a.length;
    // Create a back-reference for each index
    //   that should be followed in case of a mismatch.
    //   We only need B to make these references:
    let map = Array(endB);
    let k = 0; // Index that lags behind j
    map[0] = 0;
    for (let j = 1; j < endB; j++) {
        if (b[j] == b[k]) {
            map[j] = map[k]; // skip over the same character (optional optimisation)
        } else {
            map[j] = k;
        }
        while (k > 0 && b[j] != b[k]) k = map[k]; 
        if (b[j] == b[k]) k++;
    }
    // Phase 2: use these references while iterating over A
    k = 0;
    for (let i = startA; i < a.length; i++) {
        while (k > 0 && a[i] != b[k]) k = map[k];
        if (a[i] == b[k]) k++;
    }
    return k;
}

console.log(overlapCount("ababaaaabaabab", "abaababaaz")); // 7

Mặc dù có các whilevòng lặp lồng nhau , chúng không có tổng số lần lặp nhiều hơn n . Điều này là do giá trị của k giảm nghiêm trọng trong whilecơ thể và không thể trở thành âm. Điều này chỉ có thể xảy ra khi k++được thực hiện nhiều lần để có đủ chỗ cho những lần giảm như vậy. Vì vậy, tất cả trong tất cả, không thể có nhiều vụ hành quyết của whilecơ thể hơn là các k++vụ hành quyết, và sau đó rõ ràng là O (n).

Để hoàn thành, ở đây bạn có thể tìm thấy mã giống như trên, nhưng trong một đoạn tương tác: bạn có thể nhập chuỗi của riêng mình và xem kết quả tương tác:

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.