Bài toán tìm kiếm xâu mẫu
Đề bài: Cho xâu T độ dài n (gọi là văn bản_text). Cho P độ dài m
(gọi là xâu mẫu_pattern). Tìm tất cả các vị trí khớp của P trong T.
Giải: Có 4 thuật toán sau:
1. Tìm kiếm trực tiếp:
Ý tưởng:Dịch từng vị trí s=0,1,...n-m, với mỗi vị trí xem xâu mẫu có xuất hiện ở
vị trí đó không.
Code:
void NaiveSM(char* P,int m,char* T,int n){
int i,j;
for(j=0;j<=n-m;j++){
for (i=0;i<m&&P[i]==T[i+j];i++)
if (i>=m) OUTPUT(j);
}
}
Độ phức tạp: O(nm).
2. Thuật toán Boyer-Moore:
Ý tưởng:
Hàm int Last(char c,char* P): Trả vị trí cuối cùng của c trong xâu mẫu P.
Nếu c không xuất hiện trong P thì giá trị trả lại là -1.
VD: P= ”abcabdacgj”
Vị trí 0123456789
=> Last(‘a’,P)=6 Last(‘d’,P)=5 Last(‘p’,P)=-1
Dựa trên cơ sở hàm Last, ta sẽ xây dựng các bước nhảy để tăng tính tốc độ
duyệt.Thuật toán như sau:
+ Gọi s là vị trí cần khảo sát. Ban đầu s=0.
P a c a b a c
T a a b a c b d c a c a b a c
s=0
+ Lặp chừng nào s<=n-m:
So sánh 2 xâu P và T, lần lượt từ vị trí cuối cùng, cho tới khi gặp các kí tự
khác nhau.Gọi đó là kí tự thứ j trong xâu P, tương ứng vị trí s+j trong T:
VD1:
0 1 2 3 4 5
P a c a b a c
T a a b a c b d c a c a b a c
s=0 1 2 3 4 5 6 7 8 9 10 11 12 13
Ta thấy c<>b nên j=5.Tương ứng kí tự P[5] và T[5].
VD2:
0 1 2 3 4 5
P a c a b a c
T a a b a c b d c a c a b a c
s=4 5 6 7 8 9
j=3. Kí tự P[3] vàT[7].
Nếu j=-1 => Đây là vị trí khớp, xuất s.
Sau đó dịch phải bình thường(s++)
Trái lại, gọi c=T[s+j].Xét Last(c,P):
• TH1: Last(c,P)<j. Ta dịch P để vị trí Last[c,P] trùng với vị trí s+j của
xâu T:
VD:
0 1 2 3 4 j=5
P: a c a b a c -> j=5 , c=’b’
T: a a b a c b d c a a c last(c,P)=3
s=0 1 2 3 4 5
Sau khi dịch:
0 1 2 3 4 5
P: a c a b a c
T: a a b a c b d c a a c
s=2 3 4 5 6 7
Dễ thấy thao tác dịch là s=s+j- last(c,P).
Ở VD trên ta dịch được 2 vị trí->tốt hơn dịch tuần tự.
• TH2: Last(c,P)=-1. Kí tự c không xuất hiện trong P.Dịch toàn bộ P ra
sau vị trí s+j của T:
VD:
0 1 2 3 4 j=5
P: a c a b a c -> j=5 , c=’d’
T: a a b a c b d c a a c e f last(c,P)=-1
s=1 2 3 4 5 6
Sau khi dịch:
0 1 2 3 4 5
P: a c a b a c
T: a a b a c b d c a a c e f
s=7 8 9 10 11 12
Dễ thấy thao tác dịch vẫn là s=s+j- last(c,P).
• TH3: Nếu Last(c,P)>j. Ta chỉ dịch phải 1 vị trí (s++)
VD:
0 1 2 j=3 4 5
P: a c a b a c -> j=3 , c=’c’
T: a a b c a c d c a a c last(c,P)=5
s=0 1 2 3 4 5
Sau khi dịch:
0 1 2 3 4 5
P: a c a b a c
T: a a b c a c d c a a c
s=1 2 3 4 5 6
VD tổng hợp:
Để hiểu hơn,bạn hãy thử với P= abcab và T=acbabbdababcabcabb
Giải:
0 1 j=2 3 4
P: a b c a b
T: a c b a b b d a b a b c a b c a b b
s=0 1 2 3 4
0 1 2 j=3 4
P: a b c a b
T: a c b a b b d a b a b c a b c a b b
s=1 2 3 4 5
0 1 2 3 j=4
P: a b c a b
T: a c b a b b d a b a b c a b c a b b
s=2 3 4 5 6
0 1 2 3 j=4
P: a b c a b
T: a c b a b b d a b a b c a b c a b b
s=7 8 9 10 11
j=-1 0 1 2 3 4
P: a b c a b
T: a c b a b b d a b a b c a b c a b b
s=9 10 11 12 13
->xuất s=9.
0 1 2 3 j=4
P: a b c a b
T: a c b a b b d a b a b c a b c a b b
s=10 11 12 13 14
j=-1 0 1 2 3 4
P: a b c a b
T: a c b a b b d a b a b c a b c a b b
s=12 13 14 15 16
->xuất s=12.
0 1 2 3 4
P: a b c a b
T: a c b a b b d a b a b c a b c a b b
s=13 14 15 16 17
Như vậy có 2 vị trí khớp : s=9 và s=12.
Code:
s=0;
while (s<=n-m) {
j= m-1;
while ((j>=0)&&(T[j+s]==P[j])) j--;
if (j==0) {
OUTPUT(s);
s++;
}
else {
k=last(T[j+s],P);
s=s+ max( j-k,1);
}
}
Độ phức tạp:
Hàm Last: O(m+<kích th ước bảng chữ cái>)
Chương trình:Tình huống tồi nhất O(mn+ +<kích thước bảng chữ>)
VD: P=ba
m-1
T=a
n
Kém hiệu quả với bảng chữ nhỏ.
3. Thuật toán Rabin Karp:
Ý tưởng: Chuyển đổi P và các xâu con độ dài m của T sang số nguyên (n-m+1
số). Bài toán quy về tìm 1 số trong dãy n-m+1 số đã cho.
Gọi kích thước bảng chữ là k.
P sẽ chuyển thành:
p= k
m-1
P[0] + k
m-2
P[1] + ... +P[m-1]
=(...(P[0] * k + P[1])*k + P[2])...)*k + P[m-1] (Sơ đồ Horne)
Độ phức tạp O(m).
Với các xâu con của T, nếu tính trực tiếp như trên phải có độ phức tạp:
(n-m+1) * O(m)=O((n-m+1)m)
-> Tốn kém.
Tuy nhiên, ta có thể tính số sau theo số trước:
VD: Số trước: a1a2a3a4 ->t1
Số sau : a2a3a4a5 ->t2
-> t2= (t1 % k
m-1
)*k + a5
Cách tính này chỉ có độ phức tạp O(n):
t[0]=0; offset=1;
for (i=0;i<m-1) offset* = k; //offset = k
m-1
for (i=0;i<m;i++) t[0]=2*t[0]+T[i];
for(s=1;s<=n-m;s++)
t[s]=(t[s-1] % offset) *k + T[s+m-1];
Tóm lại thuật toán có độ phức tạp O(m+n)
Nhược điểm:
Các số p,t có thể rất lớn ,vượt quá các kiểu dữ liệu cơ bản->Các phép toán
không còn là O(1) nữa.
Khắc phục: tính toán theo modul (p,t tính theo số dư khi chia cho 1 số q nào
đó).Tuy nhiên như vậy dẫn đến 1 số xâu khác nhau vẫn có thể cho các số giống
nhau.Vì vậy khi tìm được số t=p, ta phải kiểm tra xem vị trí đó có thật sự là
khớp hay không.
Nên chọn q đủ lớn,<= Max_nguyên / k
4. Thuật toán Knuth-Morris-Pratt:
Ý tưởng:
Để dễ mô tả,ta coi các xâu đánh số từ 1.
Xâu W gọi là tiền tố(prefix) của xâu X nếu X có dạng WY (Y là 1 xâu nào đó)
VD: X=”qetyughjk” W=”qety”
Xâu W gọi là hậu tố(suffix) của xâu X nếu X có dạng YW (Y là 1 xâu nào đó)
VD: X=”qetyughjk” W=”yughjk”
Nếu có thêm W<> X thì W gọi là prefix(hay suffic) thực sự của X.
Hàm int Prefix(int q):
Hàm trả độ dài của prefix dài nhất của P[1..m] đồng thời là suffix thực sự của
P[1..q].
VD: P=”abcabcd”
P=”abcabcd” -> Prefix(1)=0
P=”abcabcd” -> Prefix(2)=0
P=”abcabcd” -> Prefix(3)=0
P=”a bca bcd” -> Prefix(4)=1
P=”ab cab cd” -> Prefix(5)=2
P=”abc abc d” -> Prefix(6)=3
P=”abcabcd” -> Prefix(7)=0
Ta xây dựng PI(k)=Prefix(k) với k=1->m:
+ Dễ thấy PI[1]=0.
+ Giả sử đã có các PI(k) với mọi k<q.
Ta sẽ tính PI(q).
VD1: P=”abcabc” q=6
P=”ab cab c” -> PI(5)=2
Khi bổ sung kí tự P[3], ta thấy nó khớp với “ab” thành “abc” là suffic của
xâu P[1..6]: P=”abc abc ”
Vậy PI(6)=PI(5)+1=3.
VD2: P=”abcababcabc” q=11
P=”abcab abcab c” -> PI(10)=5
Khi bổ sung kí tự P[6], ta thấy nó ghép với “abcab” thành “abcaba” không
phải là suffic của xâu P[1..11].
Nhưng xâu Prefix của “abcab” (tức “ab”) thì khớp với kí tự tiếp theo(P[3]
=”c”) tạo thành xâu “abc” chính là suffic của P[1..11]
P=”abc ababcabc ” -> PI(11)=3
VD3: P=”abcabcabcaa” q=11
P=” abcabca bca a” -> PI(10)=7
Khi bổ sung kí tự P[8] ,ta thấy nó ghép với “abcabca” thành “abcabcab”
không phải là suffic của xâu P[1..11].
Xét xâu Prefix của “abcabca” (tức “abca”).Nó ghép với kí tự tiếp theo(P[5]
=”b”) tạo thành xâu “abcab” vẫn không là suffic của P [1..11]