Thuật toán Z
Bài dịch từ Codeforces blog.
Z là một phương pháp xử lý xâu khá hay, ứng dụng phổ biến nhất của Z là dùng để tìm kiếm xâu (tìm các vị trí mà xâu A xuất hiện trong xâu B).
Thuật toán Z có đầu vào là một xâu, kết quả sau khi thực hiện là một mảng \(Z\) (được định nghĩa cụ thể ở dưới), từ mảng này có thể ứng dụng cho nhiều bài toán trên xâu khác.
Mô tả thuật toán
Cho xâu \(S\) với độ dài \(n\), thuật toán Z sẽ tạo ra mảng \(Z\) với ý nghĩa \(Z[i]\) là độ dài xâu con dài nhất bắt đầu từ \(i\) và là tiền tố của S. Định nghĩa một cách bài bản hơn, ta có \(Z[i]\) là số lớn nhất sao cho \(S[j] = S[i+j]\) với \(0 \leq j \leq Z[i]\). Ta gọi đoạn con như vậy là prefix-substring.
Duyệt \(i\) từ \(1\) đến \(n-1\) (mảng bắt đầu từ 0), ở mỗi vị trí ta quản lí một đoạn \([l, r]\), \(r\) lớn nhất có thể sao cho xâu con từ \(l\) tới \(r\) là tiền tố của xâu \(S\) (prefix) (Lưu ý là chỉ yêu cầu \(r\) lớn nhất chứ cả đoạn \([l, r]\) không cần phải lớn nhất). Ta sẽ tính \(Z[i]\) chung trong vòng lặp trên. Ban đầu \(l = r = 0\).
Giả sử ở i ta đã có đoạn \([l, r]\) cho vị trí i-1 và tính được tất cả các giá trị Z trước đó. Chia trường hợp để cập nhật l, r, và Z[i] như sau:
- Nếu i > r, trong trường hợp này không có tiền tố nào bắt đầu trước i và kết thúc sau i, vì vậy ta gán lại \(l=i\), và cho \(r\) chạy từ \(i\) trở lên để tìm đoạn \([l,r]\) mới. Sau đó tính \(Z[i]=r-l+1\).
- Trường hợp ngược lại, \(i \leq r\). Đặt \(k = i - l\), ta thấy xâu \(S[k...]\) và xâu \(S[i...]\) giống nhau ở
ít nhất \(r - i + 1\) phần tử đầu, vì vậy có thể tận dụng \(Z[k]\) để tính \(Z[i]\). Ta có: \(Z[i] \geq min(Z[k],r-i+1)\).
- Nếu \(Z[k]<r-i+1\), ta có \(Z[i]=Z[k]\) vì nếu \(Z[i]\) lớn hơn thì \(Z[k]\) đã phải có giá trị lớn hơn giá trị hiện tại.
- Nếu \(Z[k] \geq r-i+1\), trong trường hợp này tiền tố bắt đầu từ \(i\) có thể vượt quá \(r\) nên ta gán lại \(l=i\) và cho \(r\) tiếp tục tăng để tìm đoạn \([l,r]\) mới. Sau đó cũng có \(Z[i]=r-l+1\) như trên.
Một số lưu ý:
- Độ phức tạp của thuật toán là \(O(N)\), vì \(i\) và \(r\) luôn tăng trong quá trình chạy.
- Không cần quan tâm tới giá trị của \(Z[0]\) (theo định nghĩa thì \(Z[0]=n\)).
Cài đặt
vector<int> z_algo(const string &s) {
int n(s.size());
vector<int> z(n);
int l(0), r(0);
for (int i=1; i<n; i++) {
if (i > r) {
l = r = i;
while (r<n && s[r-l]==s[r]) r += 1;
z[i] = r - l;
r -= 1;
} else if (z[i-l] < r-i+1) {
z[i] = z[i-l];
} else {
l = i;
while (r<n && s[r-l]==s[r]) r += 1;
z[i] = r - l;
r -= 1;
}
}
return z;
}
Ứng dụng xâu khớp chuỗi
Bài toán: SUBSTR
Để tìm các vị trí chuỗi B xuất hiện trong chuỗi A, ta tạo một chuỗi S mới bằng cách nối đuôi B và A, có kèm theo một kí tự xen giữa:
\[S = B + "." + A\]Có thể thay “.” bằng một kí tự bất kì, miễn là nó không xuất hiện trong A và B.
Sau đó dùng thuật toán bên trên để tính mảng \(Z\) cho xâu \(S\), các vị trí có chuỗi \(B\) xuất hiện thì sẽ có \(Z[i] = length(B)\).
Code: /code/80/
Các ứng dụng khác
- Đề bài: C11STR2, lời giải: /code/133/