Thuật toán Knuth Morris Pratt

Một phần của tài liệu Đánh giá và thu thập thông tin tự động trên internet sử dụng dịch vụ tìm kiếm (Trang 40 - 49)

Thuật toán KMP đƣợc Knuth, Morris và Pratt phát triển vào năm 1977 là thuật toán tìm kiếm có thời gian tuyến tính O(n+m). Thuật toán này chạy khá tốt trong thực tế, tuy nhiên cũng khá khó hiểu, vì thế khó gỡ lỗi. Trƣớc hết trình bày một biến thể của thuật toán với thời gian tính cỡ O(n+|Σ|m). Thuật toán này có hai điểm mạnh:

- Thuật toán này đơn giản hơn thuật toán gốc và bao gồm ý tƣởng chính của thuật toán gốc.

- Nếu kích thƣớc của bảng chữ cái không quá lớn (256 với kí tự ASCII), thuật toán này có thể coi là tuyến tính O(n+m) .

Thuật toán với thời gian tính cỡ O(n+|Σ|m)

Ý tƣởng của thuật toán KMP dựa trên quan sát rằng trong thuật toán trâu bò có nhiều bƣớc so sánh dƣ thừa. Cụ thể, khi so sánh phần tử T[i] với P[j], nếu T[i]≠P[j], ta đã có thông tin:

T[i−1]=P[j−1],T[i−2]=P[j−1],…,T[i−j+1]=P[1](1)

Thuật toán trâu bò dịch sang P lên 1 đơn vị bằng cách và thiết lập biến chạy trên xâu mẫu j=0 mà không tận dụng thông tin này. Trong phần này, thay vì nói "dịch xâu mẫu lên x đơn vị" ta sẽ nói "lùi biến chạy j về vị trí s". Hai cách nói này về cơ bản là tƣơng đƣơng.

Nhƣ vậy tại sao thông tin này lại có ích? Xét văn bản T và xâu mẫu P trong ví dụ đầu tiên.

Khi so sánh T[5,6,…,11] với P[1,2,…,7], chúng ta thấy hai xâu này không khớp tại vị trí T[10]≠P[6] (i=10,j=6). Ta thiết lập j=4 và tiếp tục so sánh T[i+1]=T[11] với T[j+1]=T[5]. Cứ làm nhƣ vậy, thì mỗi vòng lặp biến chạy trên văn bản i luôn tăng và do đó thời gian tính toán sẽ là O(n) nếu nhƣ ta có thể tính đƣợc vị trí cần lùi lại của biến chạy j trong thời gian O(1).

Tại sao trong ví dụ trên ta lại lùi biến chạy j về vị trí 4. Quan sát kĩ ta thấy chúng ta lùi j về vị trí 4 là do T[10,9,8,7]=P[4,3,2,1], tức là tiền tố (prefix) kết thúc tại j=4 của P khớp với hậu tố (suffix) bắt đầu từ 7 của T[1,2,…,10]. Tổng kết lại, quy luật lùi biến chạy j nhƣ sau:

Quy luật 1: Lùi j về vị trí s sao cho tiền tố kết thúc tại s của P[1,2,…,j]

(chính là P[1,2,…,s]) khớp với hậu tố bắt đầu tại i−s của T[1,2,…,i](chính là T[i−s,i−s+1,…,i]) và độ dài của đoạn khớp này (chính là s) lớn nhất. Ta gọi kí tự T[i] là kí tự chốt (pivot). Trong ví dụ trên, nếu ta lùi lại j=5 thì P[1,2,…,5] không khớp khới với T[6,7,8,9,10]. Nếu ta lùi lại j=4 thì P[1,2,3,4] khớp khới với T[7,8,9,10] và độ dài đoạn khớp này là 4. Tƣơng tự, nếu ta lùi j=2 thì P[1,2] khớp khới với T[9,10] và độ dài đoạn khớp là 2.

Theo luật trên, ta lùi j về vị trí sao cho độ dài đoạn khớp là lớn nhất, do đó, ta lùi j=4.

Nhận xét thấy vị trí lùi lại j=s phụ thuộc vào hai yếu tố: - Kí tự chốt T[i].

- Vị trí xảy ra không khớp trong P (chính là j).

Trƣờng hợp kí tự chốt T[i] không xuất hiện trong P, ta lùi lại j=0 (hay nói cách khác ta dịch P lên j đơn vị). Ví dụ nếu T[10]=f thì ta có thể dịch P lên 6 đơn vị. Dựa trên nhận xét trên, ta sẽ tính trƣớc mảng S[1,2,…,|Σ|][1,2,…,m] trong đó S[p,q] là giá trị ta sẽ lùi lại nếu T[i] là kí tự thứ p trong bảng chữ cái và vị trí xảy ra không khớp là q trong xâu mẫu P. Ví dụ trong ví dụ trên T[i]=b (tƣơng đƣơng với p=2 nếu Σ={a,b,c}) và q=j=6, thì S[2,6]=4. Giả sử ta đã tính đƣợc bảng S bằng thủ tục COMPUTEBACKUP (ở dƣới), thuật toán tìm xâu sẽ nhƣ sau:

C Code: 1 2 3 4 5 6 7 8

// assume both string T and P are indexed from 1

// you can force this condition by adding a sentinel character at the beginning of both strings.

charAlph[28] = " abcdefghijklmnopqrstuvwxyz"; // the alphabet

intKMPmatcher(intn, intm){

compute_backup(m);

KMPMATCHER(T[1,2,…,n],P[1,2,…,m]): COMPUTEBACKUP(Σ,P[1,2,…,m])

j←1

for i←1 to n

if T[i]≠P[j] [[found a mismatched]] p← index of T[i] in Σ

j←S[p,j] [[the restarting value]] j←j+1

if j=m+1

return i−m+1. return NONE

9 10 11 12 13 14 15 16 17 18 19 inti = 1, j = 1, p = 0; for(i = 1; i <=n; i++){ if(T[i] != P[j]){

p = T[i]-96; the index of T[i] in the alphabet

j = S[p][j]; } elsej++; if(j == m+1){ returni-m+1; } } return-1; }

Với mỗi vòng lặp ta mất thời gian O(1), do đó, tổng thời gian là O(n+T(|Σ|,m))trong đó T(|Σ|,m) là thời gian tính bảng S[1,2,…,|Σ|][1,2,…,m]. Để tính bảng S[1,2,…,|Σ|][1,2,…,m], ta sử dụng chính ý tƣởng lùi biến chạy ở trên kết hợp với quy hoạch động.

Gọi X[1,2,…,m] là mảng trong đó X[j] là chỉ số s<j−1 lớn nhất sao cho P[1,2,…,s] khớp với P[j−s+1,j−s+2,…,j]. Gọi p là chỉ số của P[j] trong bảng chữ cái Σ. S[k,j] và X[j] có thể đƣợc tính thông qua X[j−1] dựa trên công thức quy hoạch động sau:

S[k,j] = S[k,X[j−1]+1] S[p,j] = j+1

X[j] = S[p,X[j−1]+1]−1 why -1?

C Code: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 voidcompute_backup(intm){ intX = -1; intj = 0 ,k = 0, p = 0;

for(k = 0; k < 27; k++){ // 26 is the size of the alphabet

memset(S[k], 0, sizeof(S[k])); // set

everything to 0 } for(k = 0; k < 27 ; k ++){ S[k][0] = 1; } for(j = 1; j <=m ; j++){ for(k = 1; k < 27; k++){ S[k][j] = S[k][X+1]; }

p = P[j]-96; // the index of P[j] in the

alphabet

S[p][j] = j+1;

X = S[p][X+1]-1;

}

}

Do X[j] và S[k][j] chỉ phụ thuộc vào X[j−1], ta có thể tiết kiệm một chút bộ nhớ bằng cách chỉ dùng một biến X thay vì dùng cả mảng X[1,2,…,m]. Dễ thấy thuật toán trên mất thời gian O(|Σ|m) để cập nhật bảng S, do đó:

COMPUTEBACKUP(Σ,P[1,2,…,m]): X[0]←−1 S[1,2,…,|Σ|][0]←1 for j←1 to m for k←1 to |Σ| S[k,j]←S[k,X[j−1]+1] p← index of P[j] in Σ S[p,j]=j+1 X[j]=S[p,X[j−1]+1]−1

Lemma 2: Thời gian tìm xâu mẫu P[1,2,…,m] trong văn bản T[1,2,…,n]của

Thuật toán tìm kiếm có thời gian tuyến tính O(n+m)

Trong thuật toán trƣớc, sự phụ thuộc thời gian vào kích thƣớc bảng chữ cái dƣờng nhƣ dễ hiểu vì kí tự chốt T[i] có thể là bất kì kí tự nào trong bảng chữ cái. Để xóa bỏ sự phụ thuộc này, ta chọn kí tự chốt là T[i−1]. Do T[i−1]=P[j−1], kí tự này luôn xuất hiện trong P. Quy luật lùi biến chạy có thể phát biểu lại nhƣ sau:

Quy luật 3/2: Lùi j về vị trí s sao cho tiền tố kết thúc tại s−1 của P[1,2,…,j−1] (chính là P[1,2,…,s−1]) khớp với hậu tố bắt đầu tại i−s của T[1,2,…,i−1] (chính là T[i−s,i−s+1,…,i−1]) và độ dài của đoạn khớp này

(chính là s) lớn nhất.

Sự khác biệt cơ bản giữa quy luật 1 và quy luật 3/2 đó là: sau mỗi lần lùi biến chạy j, quy luật 1 luôn đảm bảo T[i]=P[j] và do đó ta tiếp tục xét kí tự tiếp theo i+1. Còn quy luật 3/2 không đảm bảo điều này, do đó, sau mỗi lần lùi biến chạy j, nếu T[i]≠P[j], ta phải tiếp tục lùi biến chạy j một lần nữa (nghe hơi giống đệ quy). Dƣờng nhƣ thuật toán này mất nhiều phép so sánh hơn thuật toán trƣớc. Tuy nhiên, phân tích dƣới đây chỉ ra rằng số lần lùi lại nhƣ vậy không nhiều.

Do T[i−j+1,i−j+2,…,i−1]=P[1,2,…,j−1], quy luật trên có thể đƣợc phát biểu lại nhƣ sau:

Quy luật 2: Lùi j về vị trí s sao cho tiền tố kết thúc tại s−1 của

P[1,2,…,j−1] (chính là P[1,2,…,s−1]) khớp với hậu tố bắt đầu tại j−s

của P[1,2,…,j−1] (chính là P[j−s,j−s+1,…,j−1]) và độ dài của đoạn khớp này (chính là s) lớn nhất.

Trong ví dụ trên, nếu ta lùi lại j=5 thì tiền tố P[1,2,…,4] = abab

của P[1,2,…,6] không khớp khới với hậu tố P[2,3,4,5]=baba. Nếu ta lùi lại j=4thì tiền tố P[1,2,3]=aba của P[1,2,…,6] khớp với hậu tố P[3,4,5]= aba của P[1,2,…,6] và độ dài đoạn khớp này là 3. Tƣơng tự, nếu ta lùi j=2 thì P[1]

khớp với P[5] và độ dài đoạn khớp là 1. Theo luật trên, ta lùi j về vị trí sao cho độ

Theo quy luật 2, ta có thể thấy vị trí lùi lại j=s chỉ phụ thuộc vào vị trí xảy ra không khớp trong P (chính là j), không phụ thuộc vào T. Gọi F[j] là vị trí j phải lùi về khi T[j]≠P[i]. Giả sử ta đã tính đƣợc F[1,2,…,m] bằng thủ tục COMPUTEFAILURE (ở dƣới), thuật toán tìm xâu sẽ nhƣ sau:

C Code: 1 2 3 4 5 6 7 8 9 10 11 12 13 14

intfastKMPmatcher(intn, intm){

compute_failure(m);

inti = 1, j = 1;

for(i = 1; i <=n ; i++){

while(T[i] != P[j] && j > 0){ // found a

mismatch

j = F[j]; // backup the pattern

} if(j == m){ returni-m+1; } j++; FASTKMPMATCHER(T[1,2,…,n],P[1,2,…,m]): COMPUTEFAILURE(P[1,2,…,m]) j←1 for i←1 to n

while T[i]≠P[j] and j>0

j←F[j] [[backing up the pattern]] if j=m

return i−m+1 j←j+1

15 }

return-1;

}

Giả sử thủ tục COMPUTEFAILURE(P[1,2,…,m]) sử dụng T(m) phép so sánh, ta sẽ chứng minh rằng số phép so sánh của thuật toán FASTKMPMATCHER

(T[1,2,…,n],P[1,2,…,m]) là O(n+T(m)). Không dễ để có thể thấy đƣợc điều này vì sự xuất hiện vòng lặp while bên trong vòng lặp for. Trƣớc hết ta thấy sau mỗi phép so sánh bằng (T[i]=P[j]), cả i và j đều tăng lên 1. Do đó, số phép so sánh bằng tối đa là n−1. Vòng lặp while giảm giá trị của j mỗi khi ta bắt gặp một phép so sánh không bằng (T[i]≠T[j]). Tuy nhiên, số lƣợng giảm của j không thể vƣợt quá giá trị mà j đã tăng lên (sau mỗi phép so sánh bằng) qua các vòng lặp trƣớc đó. Điều đó có nghĩa là tổng số phép so sánh không bằng không thể vƣợt quá tổng số phép so sánh bằng, do đó, tối đa là n−1. Nhƣ vậy, tổng số phép so sánh là 2(n−1)+T(m)=O(n+T(m)).

F[1,2,…,m] có thể đƣợc tính dựa vào định nghĩa của F[j] và quy luật 2 trong

thời gian O(m3). Cụ thể, để tính F[j], ta sẽ kiểm tra tất cả các chỉ số s từ 1 đến

j−2 sao cho P[1,2,…,s−1] khớp với P[j−s,j−s+1,…,j−1] và chọn ra chỉ số s lớn nhất

thỏa mãn điều kiện này. Tuy nhiên, Knuth-Morris-Pratt chỉ ra rằng bảng F[1,2,…,m] có thể đƣợc tính trong thời gian O(m) bằng cách sửa đổi thuật toán FASTKMPMATCHER đối sánh P với chính nó.

Code bằng C của giả mã:

COMPUTEFAILURE(P[1,2,…,m]): k←0 F[1]←0 for j←1 to m−1 while P[j]≠P[k] and k>0 k←F[k] (*) k←k+1 F[j+1]←k

C Code: 1 2 3 4 5 6 7 8 9 10 11 voidcompute_failure(intm){ intk = 0, j = 1; F[1] = 0; for(j=1 ; j < m; j++){ while(P[j] != P[k] && k > 0){ k = F[k]; } k++; F[j+1] = k; } }

Tại sao đối sánh P với chính nó lại cho ra kết quả ta mong muốn. Về mặt trực quan, để ý quy luật 3/2, nếu thay T trong quy luật này bởi P, và j bởi k, giá trị s trong quy luật 3/2 chính là F[j]. Thủ tục FASTKMPMATCHER về cơ bản thực thi quy luật này, do đó, ta có thể sử dụng chính thủ tục này để tính F[j].

Một cách hiểu khác của thủ tục này là nhƣ sau: giả sử F[j]=s, ta biết rằng P[1,2,…,s−1]=P[j−s,…,j−1]. Do đó, ta tìm các ứng cử viên k, bắt đầu từ ứng viên lớn nhất (chính là F[j−1]), sao cho:

P[1,2,…,k−1]=P[j−k,…,j−2]

Nếu P[k]=P[j−1] thì k+1 chính là F[j]. Nếu P[k]≠P[j−1], ứng viên tiếp theo sẽ là F[k] (theo định nghĩa của bảng F), do đó ở dòng (*), ta gán k←F[k]. Do ta xét các ứng cử viên từ lớn đến nhỏ, giá trị tìm đƣợc sẽ tƣơng ứng với đoạn khớp lớn nhất. Phân tích tƣơng tự nhƣ trên, số phép so sánh trong thủ tục COMPUTEFAILURE là 2(m−1)=O(m). Do đó,

Ví dụ: bảng F[1,2,…,7] ứng với mẫu P=ababaca nhƣ sau:

P[i] a b a b a c

F[i] 0 1 1 2 3 4

Lemma 3: Thời gian tìm xâu mẫu P[1,2,…,m] trong văn bản T[1,2,…,n ]

Một phần của tài liệu Đánh giá và thu thập thông tin tự động trên internet sử dụng dịch vụ tìm kiếm (Trang 40 - 49)

Tải bản đầy đủ (PDF)

(80 trang)