Một trong những bài toán cơ bản và có ứng dụng rộng rãi là bài toán đối sánh mẫu nhằm tìm kiếm tất cả các vị trí xuấthiện của một xâu mẫu trong một văn bản lớn.. Chương 2: Tìm kiếm mẫu
TỔNG QUAN
Giới thiệu vấn đề
Đối sánh mẫu là một chủ đề quan trọng trong lĩnh vực xử lý văn bản Các thuật toán đối sánh mẫu được xem là những thành phần cơ sở được cài đặt cho các hệ thống thực tế đang tồn tại trong hầu hết các hệ điều hành Hơn thế nữa, các thuật toán đối sánh mẫu cung cấp các mô hình cho nhiều lĩnh vực khác nhau của khoa học máy tính: xử lý ảnh, xử lý ngôn ngữ tự nhiên, tin sinh học và thiết kế phần mềm.
String-matching được hiểu là việc tìm một hoặc nhiều xâu mẫu (pattern) xuất hiện trong một văn bản (có thể là rất dài) Ký hiệu xâu mẫu hay xâu cần tìm là X =(x0, x1, ,xm-1) có độ dài m Văn bản Y =(y0, y1, ,yn-1) có độ dài n Cả hai xâu được xây dựng từ một tập hữu hạn các ký tự Alphabet ký hiệu là với kích cỡ là Như vậy một xâu nhị phân có độ dài n ứng dụng trong mật mã học cũng được xem là một mẫu Một chuỗi các ký tự ABD độ dài m biểu diễn các chuỗi AND cũng là một mẫu
- Xâu mẫu X =(x0, x1, , xm), độ dài m.
- Văn bản Y =(y0, x1, , yn), độ dài n.
- Tất cả vị trí xuất hiện của X trong Y.
Phân loại các thuật toán đối sánh mẫu
Tìm kiếm mẫu là một bài toán quan trọng trong khoa học máy tính, với mục tiêu tìm tất cả các vị trí xuất hiện của xâu mẫu XXX (pattern) trong văn bản YYY (text) Các thuật toán tìm kiếm mẫu được phát triển với nhiều chiến lược khác nhau nhằm tối ưu hóa hiệu suất, đặc biệt khi xử lý văn bản lớn hoặc mẫu phức tạp Dựa trên cách thuật toán duyệt và so sánh các ký tự trong văn bản, có thể phân loại các thuật toán tìm kiếm mẫu thành bốn nhóm chính:
Tìm kiếm mẫu từ trái sang phải: (Chương 2) o Các thuật toán thuộc nhóm này so sánh các ký tự từ trái sang phải trong cửa sổ tìm kiếm, nghĩa là bắt đầu từ ký tự đầu tiên của mẫu và tiến dần về phía cuối Nếu không khớp, cửa sổ thường được dịch chuyển một khoảng dựa trên thông tin đã so sánh o Harrison Algorithm, Karp-Rabin Algorithm, Morris-Pratt Algorithm, Knuth- Morris-Pratt Algorithm, Forward Dawg Matching algorithm , Apostolico-Crochemore algorithm, Naive algorithm
Tìm kiếm mẫu từ phải sang trái: (Chương 3) o Nhóm này thực hiện so sánh từ ký tự cuối cùng của mẫu (bên phải) về phía đầu (bên trái) Khi không khớp, thuật toán sử dụng các heuristic
(như Bad Character hoặc Good Suffix) để nhảy xa hơn, tận dụng thông tin từ các ký tự không khớp. o Boyer-Moore Algorithm , Turbo BM Algorithm, Colussi Algorithm, Sunday Algorithm, Reverse Factorand Algorithm, Turbo Reverse Factor, Zhu and Takaoka and Berry-Ravindran Algorithms
Tìm kiếm mẫu từ vị trí xác định: (Chương 4) o Các thuật toán này sử dụng một vị trí cố định (thường là một ký tự hoặc khối ký tự đặc biệt trong mẫu) làm "mỏ neo" (anchor) để bắt đầu so sánh Sau đó, chúng mở rộng so sánh ra hai phía (trái và phải) từ vị trí này. o Two Way Algorithm, Colussi Algorithm , Galil-Giancarlo Algorithm, Sunday's Optimal Mismatch Algorithm, Maximal Shift Algorithm, Skip Search, KMP Skip Search and Alpha Skip Search Algorithms.
Tìm kiếm mẫu từ vị trí bất kỳ: (Chương 5) o Nhóm này không cố định hướng so sánh (trái sang phải hoặc phải sang trái), mà có thể bắt đầu từ bất kỳ vị trí nào trong mẫu, thường dựa trên các cấu trúc dữ liệu như đồ thị hoặc bảng băm để tối ưu hóa việc nhảy và so sánh. o Horspool Algorithm, Smith Algorithm , Raita Algorithm.
Phân loại này không chỉ giúp hiểu rõ cách hoạt động của từng thuật toán mà còn hỗ trợ việc lựa chọn thuật toán phù hợp cho các bài toán thực tế, tùy thuộc vào đặc điểm của văn bản và mẫu cần tìm kiếm.
TÌM KIẾM MẪU TỪ TRÁI SANG PHẢI
Thuật toán Knuth-Morris-Pratt (KMP)
2.1.1 Mô tả thuật toán a Ý tưởng chính:
KMP là một cải tiến của thuật toán Morris-Pratt, so sánh từ trái sang phải và sử dụng bảng kmpNext để giảm số lần so sánh ký tự.
Bảng kmpNext được xây dựng trong giai đoạn tiền xử lý, lưu trữ độ dài của biên (border) dài nhất của tiền tố X[0 i−1] sao cho biên này được theo sau bởi một ký tự khác X[i].
Khi có sự không khớp, thay vì dịch cửa sổ từng bước như Brute Force, KMP sử dụng bảng kmpNext để nhảy đến vị trí phù hợp, tránh so sánh lại các ký tự đã khớp. b Bước thực hiện:
Xâu mẫu X=(x0,x1, ,xm−1), độ dài m.
Văn bản Y=(y0,y1, ,yn−1), độ dài n.
Tất cả vị trí xuất hiện của X trong Y.
// Tiền xử lý: Tạo bảng kmpNext kmpNext = preprocessKMP(X, m); // Bảng kmpNext i = 0; // Vị trí hiện tại trong Y j = 0; // Vị trí hiện tại trong X while (i < n) { // Lặp tìm kiếm cho đến khi duyệt hết Y while (j >= 0 && X[j] != Y[i]) { // Nếu không khớp j = kmpNext[j]; // Nhảy đến vị trí biên tiếp theo
} i++; // Tiến con trỏ trên Y j++; // Tiến con trỏ trên X if (j == m) { // Nếu khớp toàn bộ X
OUTPUT(i - j); // Xuất vị trí bắt đầu j = kmpNext[j]; // Nhảy để tìm tiếp
2.1.2 Đánh giá độ phức tạp
Thời gian tiền xử lý: O(m), cần xây dựng bảng kmpNext.
Thời gian tìm kiếm: O(n), mỗi ký tự trong Y được so sánh tối đa một lần nhờ bảng kmpNext.
Tổng thời gian: O(m+n), tuyến tính với độ dài của mẫu và văn bản.
Số lần so sánh ký tự: Tối đa 2n−1 trong trường hợp xấu nhất.
Không gian (Bộ nhớ): O(m), cần lưu bảng kmpNext.
Tập dữ liệu kiểm thử:
Bảng kmpNext (theo tài liệu trang 48 và 202): i 0 1 2 3 4 5 6 7 8
Trạng thái (state) Chuyển tiếp
Kết quả Ghi chú j=0 G 0 → 1 Tiếp tục - Khớp 'G'. j=1 C 1 → 2 Tiếp tục - Khớp 'GC'. j=2 A 2 → 3 Tiếp tục - Khớp 'GCA'. j=3 T 3 → (không khớp) Chuyển về -
1 → 0 - Không khớp 'T', nhảy về trạng thái 0. j=6 C 0 → 1 → 2 Tiếp tục - Khớp 'GC' lại. j=7 A 2 → 3 → 4 → 5
→ 6 → 7 Tiếp tục - Tiếp tục khớp
Trạng thái (state) Chuyển tiếp
Kết quả Ghi chú j=8 G 7 → 8 (khớp toàn bộ) Đạt trạng thái chấp nhận
"GCAGAGAG" tại j=5. j=9 A 8 → 1 Tiếp tục - Nhảy về trạng thái 1, tiếp tục. j A
Tiếp tục - Khớp một phần, không đạt trạng thái chấp nhận. j$ (kết thúc) - - - Kết thúc, không tìm thêm khớp.
# Lớp Suffix Automaton class SuffixAutomaton: def init (self, m):
# Khởi tạo trạng thái: 0 là trạng thái ban đầu self.states = [{}] # Danh sách các trạng thái và chuyển tiếp self.suffix_link = [-1] # Liên kết suffix self.length = [0] # Độ dài chuỗi của trạng thái
# Kích thước tối đa của suffix automaton là 2*m - 1 trạng thái self.terminal = [False] * (2 * m) # Trạng thái chấp nhận self.size = 1 # Số lượng trạng thái self.last = 0 # Trạng thái cuối cùng khi xây dựng def add_char(self, c):
# Tạo trạng thái mới new_state = self.size self.states.append({}) self.length.append(self.length[self.last] + 1) self.suffix_link.append(0) # Khởi tạo suffix link tạm thời self.size += 1
# Thêm chuyển tiếp từ trạng thái cuối cùng p = self.last while p != -1 and c not in self.states[p]: self.states[p][c] = new_state p = self.suffix_link[p] if p == -1: self.suffix_link[new_state] = 0 else: q = self.states[p][c] if self.length[p] + 1 == self.length[q]: self.suffix_link[new_state] = q else:
# Phân tách trạng thái (split state) clone = self.size self.size += 1 self.states.append(self.states[q].copy()) self.length.append(self.length[p] + 1) self.suffix_link.append(self.suffix_link[q]) while p != -1 and self.states[p][c] == q: self.states[p][c] = clone p = self.suffix_link[p] self.suffix_link[q] = clone self.suffix_link[new_state] = clone self.last = new_state def build(self, X, m):
# Reset cấu trúc cho chuỗi mới self.states = [{}] self.suffix_link = [-1] self.length = [0] self.terminal = [False] * (2 * m) self.size = 1 self.last = 0 for c in X: self.add_char(c)
# Đặt trạng thái terminal: Các trạng thái có độ dài bằng độ dài mẫu p = self.last while p != -1: self.terminal[p] = True p = self.suffix_link[p] return self
# Hàm tìm kiếm Forward DAWG Matching def forwardDAWGMatching(X, m, Y, n): automaton = SuffixAutomaton(m).build(X, m) j = 0 # Vị trí hiện tại trong Y state = 0 # Trạng thái hiện tại trong automaton positions = [] # Lưu các vị trí xuất hiện while j < n:
# Tìm trạng thái tiếp theo theo ký tự Y[j] while state != -1 and Y[j] not in automaton.states[state]: state = automaton.suffix_link[state] if state == -1: state = 0 # Quay về trạng thái ban đầu nếu không có suffix link else: state = automaton.states[state][Y[j]] # Chuyển sang trạng thái tiếp theo
# Nếu trạng thái hiện tại là trạng thái chấp nhận và độ dài khớp bằng độ dài mẫu
# Thêm vị trí khớp vào danh sách if automaton.terminal[state] and automaton.length[state] == m: positions.append(j - m + 1) j += 1 # Di chuyển sang ký tự tiếp theo trong Y return positions
Y = "GCATCGCAGAGAGTATACAGTACG" positions = forwardDAWGMatching(X, len(X), Y, len(Y)) print("Vị trí xuất hiện:", positions)
Thuật toán Not So Naive
2.2.1 Mô tả thuật toán a Ý tưởng chính
Thuật toán Not So Naive là một cải tiến của thuật toán Brute Force, giảm số lần so sánh bằng cách tận dụng thông tin từ hai ký tự đầu tiên của xâu mẫu X
Nếu X[0]==X[1], thuật toán dịch cửa sổ 2 vị trí khi không khớp tại Y[j]; nếu X[0]≠X[1], thuật toán kiểm tra Y[j] và Y[j+1] để quyết định dịch 1 hoặc 2 vị trí
So sánh được thực hiện từ trái sang phải. b Bước thực hiện:
Xâu mẫu X=(x0,x1, ,xm−1), độ dài m.
Văn bản Y=(y0,y1, ,yn−1), độ dài n.
Tất cả vị trí xuất hiện của X trong Y.
NotSoNaive(X, m, Y, n): j = 0; // Khởi tạo vị trí bắt đầu của cửa sổ trong Y while (j = 0 and X[i] != X[j]: j = kmpNext[j] i += 1 j += 1 kmpNext[i] = j return kmpNext def apostolico_crochemore(X, Y): m = len(X) n = len(Y) kmpNext = preprocess_kmp(X, m) j = 0 k = 0 while j = m: print(f"Match at position: {j}") k = i while k > 0 and (j + k >= n or X[k] != Y[j + k]): k = kmpNext[k] j += max(1, k - kmpNext[k])
TÌM KIẾM MẪU TỪ PHẢI SANG TRÁI
Thuật toán Boyer-Moore
3.1.1 Mô tả thuật toán a Ý tưởng chính:
So sánh từ phải sang trái trong cửa sổ, và dịch cửa sổ sang phải dựa trên hai heuristic: o Bad Character: Nếu không khớp tại ký tự Y[i], dịch cửa sổ sao cho ký tự không khớp trong Y căn chỉnh với lần xuất hiện cuối cùng của nó trong X (trừ ký tự cuối). o Good Suffix: Nếu có một hậu tố của X khớp với một phần của Y, dịch sao cho hậu tố này căn chỉnh với lần xuất hiện trước đó trong X.
Kết hợp cả hai heuristic, chọn bước dịch lớn nhất để tối ưu hóa hiệu suất. b Bước thực hiện:
Xâu mẫu X=(x0,x1, ,xm−1), độ dài m.
Văn bản Y=(y0,y1, ,yn−1), độ dài n.
Tất cả vị trí xuất hiện của X trong Y.
// Tiền xử lý: Tạo bảng Bad Character và Good Suffix badChar = preprocessBadCharacter(X, m, Σ); // Bảng Bad Character goodSuffix = preprocessGoodSuffix(X, m); // Bảng Good Suffix j = 0; // Vị trí bắt đầu của cửa sổ trong Y while (j = 0 && X[i] == Y[i + j]) { // So sánh từ phải sang trá i i ;
} if (i < 0) { // Nếu khớp toàn bộ
OUTPUT(j); // Xuất vị trí j j += goodSuffix[0]; // Dịch theo Good Suffix
} else { // Nếu không khớp tại vị trí i shiftBad = badChar.get(Y[j + i], m) - (m - 1 - i); // Bước d ịch Bad Character shiftGood = goodSuffix[i + 1]; // Bước dịch Good Suffix j += max(shiftBad, shiftGood); // Chọn bước dịch lớn nhất
3.1.2 Đánh giá độ phức tạp
Thời gian tiền xử lý: O(m+|Σ|), để tạo bảng Bad Character và Good Suffix.
Thời gian tìm kiếm (Best case): O(n/m), khi các ký tự trong Y không khớp với X, và nhảy với bước lớn nhất.
Thời gian tìm kiếm (Average case): O(n/m), rất hiệu quả nhờ hai heuristic.
Thời gian tìm kiếm (Worst case): O(m×n), ví dụ khi tìm X="aaa" trong Y="aaaaaa".
Không gian (Bộ nhớ): O(m+|Σ|), cần lưu bảng Bad Character và Good Suffix.
Tập dữ liệu kiểm thử:
Bảng Bad Character (dựa trên ký tự cuối cùng trong X): c A C G T badChar[c] 2 1 0 8
Y[j] Trạng thái (state) Chuyển tiếp Kết quả Ghi chú j=0 G 7 → 6 (không khớp)
Y[j] Trạng thái (state) Chuyển tiếp Kết quả Ghi chú tại j=5. j=9 A 7 → 6 (không khớp)
- Không khớp 'G' với 'T', nhảy 2. j A 7 → 6 (không khớp)
- Không khớp 'G' với 'A', nhảy 2. j T 7 → 6 (không khớp)
- Không khớp 'G' với 'T', nhảy 8. j! A 7 → 6 (không khớp)
- Không khớp 'G' với 'A', nhảy 2. j# G 7 → 6 (không khớp)
- Kết thúc, không khớp tiếp.
3.1.4 Cài đặt thuật toán def preprocessBadCharacter(X, m, alphabet_size%6):
# Tạo bảng Bad Character với kích thước alphabet badChar = [-1] * alphabet_size for i in range(m - 1): # Không tính ký tự cuối cùng badChar[ord(X[i])] = m - 1 - i return badChar def preprocessGoodSuffix(X, m):
# Tạo bảng Good Suffix goodSuffix = [m] * (m + 1) # Mặc định nhảy toàn bộ độ dài mẫu borderPos = [0] * (m + 1) # Lưu vị trí biên dài nhất borderPos[m] = m + 1 # Biên cuối
# Tính biên từ phải sang trái i = m j = m + 1 while i > 0: while j n: return []
# Tiền xử lý badChar = preprocessBadCharacter(X, m) goodSuffix = preprocessGoodSuffix(X, m) positions = [] j = 0 while j = 0 and X[i] == Y[i + j]: i -= 1 if i < 0: positions.append(j) j += goodSuffix[0] if j + m < n else 1 else:
# Tính bước nhảy Bad Character shiftBad = max(1, i - badChar[ord(Y[i + j])])
# Tính bước nhảy Good Suffix shiftGood = goodSuffix[i + 1] j += max(shiftBad, shiftGood) return positions
Y = "GCATCGCAGAGAGTATACAGTACG" positions = BoyerMoore(X, Y) print("Vị trí xuất hiện:", positions) # Kết quả: [5]
Thuật toán Turbo BM
3.2.1 Mô tả thuật toán a Ý tưởng chính
Turbo BM là một cải tiến của thuật toán Boyer-Moore (BM), vốn đã nổi tiếng với hiệu suất cao trong việc tìm kiếm mẫu nhờ so sánh từ phải sang trái và sử dụng hai heuristic: Bad Character và Good Suffix.
Turbo BM cải tiến Boyer-Moore bằng cách giảm số lần so sánh trong trường hợp xấu nhất, đặc biệt khi mẫu có các đoạn lặp lại, thông qua việc sử dụng một cơ chế "turbo-shift" để nhảy xa hơn khi có sự không khớp.
Thuật toán tận dụng bộ nhớ để lưu lại thông tin về các lần so sánh trước đó, từ đó tránh lặp lại các so sánh không cần thiết. b Bước thực hiện
Xâu mẫu X=(x0,x1, ,xm−1), độ dài m.
Văn bản Y=(y0,y1, ,yn−1), độ dài n.
Tất cả vị trí xuất hiện của X trong Y.
// Tiền xử lý: Tạo bảng Bad Character và Good Suffix badChar = preprocessBadCharacter(X, m); // Bảng Bad Character goodSuffix = preprocessGoodSuffix(X, m); // Bảng Good Suffix j = 0; // Vị trí bắt đầu của cửa sổ trong Y turboShift = 0; // Khoảng cách nhảy turbo lastMatch = m; // Vị trí ký tự cuối cùng khớp trong lần so sánh trướ c while (j = 0 && X[i] == Y[i + j]) { // So sánh từ phải sang trá i i ;
} if (i < 0) { // Nếu khớp toàn bộ (i < 0)
OUTPUT(j); // Xuất vị trí j j = j + goodSuffix[0]; // Dịch cửa sổ theo Good Suffix turboShift = 0; // Reset turboShift lastMatch = m; // Reset lastMatch
} else { // Nếu không khớp tại vị trí i
// Tính khoảng cách nhảy bằng Bad Character badCharShift = (i - badChar[Y[i + j]]); if (badCharShift < 0) badCharShift = 1; // Đảm bảo nhảy ít n hất 1 vị trí
// Tính khoảng cách nhảy bằng Good Suffix goodSuffixShift = goodSuffix[i];
// Tính turboShift (tránh so sánh lại các đoạn đã khớp) if (lastMatch < m && i + j < n) { if (i >= lastMatch) { turboShift = lastMatch; // Nhảy xa hơn nếu có thể } else { turboShift = 0;
// Lấy khoảng cách nhảy lớn nhất shift = max(badCharShift, goodSuffixShift, turboShift); j = j + shift; // Dịch cửa sổ lastMatch = m - i - 1; // Cập nhật lastMatch
3.2.2 Đánh giá độ phức tạp
Thời gian tốt nhất (Best case): O(n/m), khi các ký tự trong Y không khớp với X, và thuật toán nhảy với bước lớn nhất.
Thời gian trung bình (Average case): O(n/m), thường rất nhanh nhờ các heuristic Bad Character, Good Suffix, và Turbo-Shift.
Thời gian tệ nhất (Worst case): O(m×n), ví dụ khi tìm X="aaa" trong Y="aaaaaa", nhưng Turbo BM cải thiện đáng kể so với Boyer-Moore thông thường bằng cách giảm số lần so sánh lặp lại.
Không gian (Bộ nhớ): O(|Σ|+m), cần lưu bảng Bad Character (O(|Σ|)) và Good
Hiệu quả cao trong thực tế, đặc biệt với các văn bản dài và mẫu không lặp lại.
Giảm số lần so sánh so với Boyer-Moore nhờ cơ chế Turbo-Shift.
Tận dụng thông tin từ các lần so sánh trước để nhảy xa hơn.
Yêu cầu pha tiền xử lý, tốn bộ nhớ để lưu các bảng Bad Character và Good Suffix.
Độ phức tạp trong trường hợp xấu nhất vẫn là O(m×n), dù hiếm gặp.
Phức tạp hơn Not So Naive và Brute Force về mặt cài đặt.
Tập dữ liệu kiểm thử:
So sánh Kết quả Dịch cửa sổ (j) Ghi chú j=0 X[3 0]="AABA"==Y[
Good Suffix nhảy 4 vị trí.
So sánh Kết quả Dịch cửa sổ (j) Ghi chú j=4 X[3 0]="AABA"==Y[
Good Suffix nhảy 4 vị trí. j=8 X[3]=′A′≠Y[11]=′B′ Không khớp j=8+3 Bad Character nhảy
Good Suffix nhảy 4 vị trí, kết thúc.
3.2.4 Cài đặt thuật toán def preprocessBadCharacter(X, m):
# Khởi tạo bảng Bad Character badChar = {} for i in range(m - 1, -1, -1): if X[i] not in badChar: badChar[X[i]] = i return badChar def preprocessGoodSuffix(X, m):
# Khởi tạo mảng border với kích thước m+1 để chứa chỉ số m border = [0] * (m + 1d` goodSuffix = [0] * m
# Tìm biên (border) của mẫu i = m j = m + 1 border[i] = j while i > 0: while j = lastMatch: turboShift = lastMatch else: turboShift = 0
# Lấy khoảng nhảy lớn nhất shift = max(badCharShift, goodSuffixShift, turboShift) j += shift # Dịch cửa sổ lastMatch = m - i - 1 # Cập nhật lastMatch return positions
Y = "AABAAABAAAAAABAAAABAAA" positions = TurboBM(X, len(X), Y, len(Y)) print("Vị trí xuất hiện:", positions) # Kết quả mong đợi: [0, 4, 11]
Thuật toán Reverse Factor
3.3.1 Mô tả thuật toán a Ý tưởng chính:
Đây là một biến thể của thuật toán tìm kiếm dựa trên yếu tố (factor) ngược, so sánh từ phải sang trái trong cửa sổ hiện tại.
Thuật toán sử dụng chiến lược dịch dựa trên yếu tố lớn nhất (factor) phù hợp với phần hậu tố của mẫu không khớp, giúp tối ưu hóa bước nhảy.
Tiền xử lý tạo ra bảng yếu tố ngược để xác định các vị trí dịch dựa trên ký tự không khớp. b Bước thực hiện:
Tất cả vị trí xuất hiện của X trong Y.
// Tiền xử lý: Tạo bảng yếu tố ngược factorTable = preprocessReverseFactor(X, m, Sigma); j = 0; // Vị trí bắt đầu của cửa sổ trong Y while (j = 0 && X[i] == Y[i + j]) { i ;
} if (i < 0) { // Nếu khớp toàn bộ
} else { // Không khớp, dịch cửa sổ j += factorTable[i + 1]; // Dịch dựa trên yếu tố ngược }
3.3.2 Đánh giá độ phức tạp
Thời gian tiền xử lý: O(m), để tạo bảng yếu tố ngược.
Thời gian tìm kiếm (Best case): O(n/m), khi các yếu tố ngược cho phép nhảy lớn.
Thời gian tìm kiếm (Average case): O(n), hiệu quả nhờ chiến lược yếu tố ngược.
Thời gian tìm kiếm (Worst case): O(mn), xảy ra khi mẫu và văn bản có nhiều ký tự lặp lại (ví dụ X = "aaa", Y = "aaaaaa").
Không gian (Bộ nhớ): O(m), để lưu bảng yếu tố ngược.
Tập dữ liệu kiểm thử:
Bảng yếu tố ngược (ví dụ): o i=0: 1, i=1: 2, i=2: 3, i=3: 4, i=4: 5, i=5: 6, i=6: 7, i=7: 8 (dựa trên biên).
(j) So sánh Kết quả Dịch cửa sổ (j) Ghi chú j=0 X[7 5] != Y[7 5]
Không khớp j=0+2=2 Dịch dựa trên yếu tố tại i=5. j=2 X[7 5] != Y[9 7]
Không khớp j=2+2=4 Dịch dựa trên yếu tố tại i=5. j=4 X[7 5] != Y[11 9]
Không khớp j=4+2=6 Dịch dựa trên yếu tố tại i=5. j=6 X[7 0] == Y[13 6]
Khớp tại j=5, dịch 1 vị trí. j=7 X[7 6] != Y[14 13]
Không khớp j=7+2=9 Dịch dựa trên yếu tố tại i=6. j=9 X[7 6] != Y[16 15]
Không khớp j=9+2 Dịch dựa trên yếu tố tại i=6, kết thúc.
3.3.4 Cài đặt thuật toán def preprocessReverseFactor(X, m):
# Tạo bảng yếu tố ngược factorTable = [1] * (m + 1) for i in range(m - 1, 0, -1): factorTable[i] = m - i return factorTable def ReverseFactor(X, Y): m = len(X) n = len(Y) if m == 0 or n == 0 or m > n: return []
# Tiền xử lý factorTable = preprocessReverseFactor(X, m) positions = [] j = 0 while j = 0 and X[i] == Y[i + j]: i -= 1 if i < 0: positions.append(j) j += 1 else: j += factorTable[i + 1] # Dịch dựa trên yếu tố ngược return positions
Y = "GCATCGCAGAGAGTATACAGTACG" positions = ReverseFactor(X, Y) print("Vị trí xuất hiện:", positions) # Kết quả: [5]
Thuật toán Turbo Reverse Factor
3.4.1 Mô tả thuật toán a Ý tưởng chính:
Đây là một cải tiến của thuật toán Reverse Factor, sử dụng chiến lược "turbo" để tăng tốc độ so sánh bằng cách kết hợp so sánh từ phải sang trái với các bước nhảy lớn hơn dựa trên yếu tố ngược.
Thuật toán tận dụng bảng yếu tố ngược mở rộng, cho phép nhảy xa hơn khi phát hiện không khớp, đồng thời tối ưu hóa việc xử lý các trường hợp lặp lại trong mẫu.
So sánh được thực hiện từ cuối mẫu, với các kiểm tra bổ sung để xác định yếu tố phù hợp nhất. b Bước thực hiện:
Tất cả vị trí xuất hiện của X trong Y.
// Tiền xử lý: Tạo bảng yếu tố ngược mở rộng factorTable = preprocessTurboReverseFactor(X, m, Sigma); j = 0; // Vị trí bắt đầu của cửa sổ trong Y while (j = 0 && X[i] == Y[i + j]) { i ;
} if (i < 0) { // Nếu khớp toàn bộ
} else { // Không khớp, dịch cửa sổ j += factorTable[i + 1]; // Dịch dựa trên yếu tố ngược mở rộ ng
3.4.2 Đánh giá độ phức tạp
Thời gian tiền xử lý: O(m), để tạo bảng yếu tố ngược mở rộng.
Thời gian tìm kiếm (Best case): O(n/m), khi các yếu tố ngược mở rộng cho phép nhảy lớn.
Thời gian tìm kiếm (Average case): O(n), hiệu quả nhờ chiến lược turbo.
Thời gian tìm kiếm (Worst case): O(mn), xảy ra khi mẫu và văn bản có nhiều ký tự lặp lại (ví dụ X = "aaa", Y = "aaaaaa").
Không gian (Bộ nhớ): O(m), để lưu bảng yếu tố ngược mở rộng.
Tập dữ liệu kiểm thử:
Bảng yếu tố ngược mở rộng (ví dụ): o i=0: 1, i=1: 2, i=2: 3, i=3: 4, i=4: 5, i=5: 6, i=6: 7, i=7: 8 (dựa trên biên và yếu tố turbo).
(j) So sánh Kết quả Dịch cửa sổ (j) Ghi chú j=0 X[7 5] != Y[7 5]
Không khớp j=0+3=3 Dịch dựa trên yếu tố tại i=4. j=3 X[7 5] != Y[10 8]
Không khớp j=3+3=6 Dịch dựa trên yếu tố tại i=4. j=6 X[7 0] == Y[13 6]
Khớp tại j=5, dịch 1 vị trí. j=7 X[7 6] != Y[14 13]
Không khớp j=7+2=9 Dịch dựa trên yếu tố tại i=6. j=9 X[7 6] != Y[16 15]
Không khớp j=9+2 Dịch dựa trên yếu tố tại i=6, kết thúc.
3.4.4 Cài đặt thuật toán def preprocessTurboReverseFactor(X, m):
# Tạo bảng yếu tố ngược mở rộng dựa trên hậu tố factorTable = [1] * (m + 1) for i in range(m - 1, -1, -1):
# Tìm yếu tố lớn nhất có thể dịch j = i while j < m - 1 and X[j] == X[j - i]: j += 1 factorTable[i] = j - i + 1 if j < m else m - i return factorTable def TurboReverseFactor(X, Y): m = len(X) n = len(Y) if m == 0 or n == 0 or m > n: return []
# Tiền xử lý factorTable = preprocessTurboReverseFactor(X, m) positions = [] j = 0 while j = 0 and X[i] == Y[i + j]: i -= 1 if i < 0: positions.append(j) j += 1 else: j += factorTable[i + 1] # Dịch dựa trên yếu tố ngược mở rộng return positions
Y = "GCATCGCAGAGAGTATACAGTACG" positions = TurboReverseFactor(X, Y) print("Vị trí xuất hiện:", positions) # Kết quả mong đợi: [5]
TÌM KIẾM MẪU TỪ MỘT VỊ TRÍ CỤ THỂ
Thuật toán Alpha Skip Search
4.1.1 Mô tả thuật toán a Ý tưởng chính
Alpha Skip Search là một cải tiến của thuật toán Skip Search.
Thuật toán sử dụng các buckets chứa danh sách vị trí của các factor (khối) có độ dài ℓ=logσm trong xâu mẫu X.
Trong pha tiền xử lý, một trie T(X) được xây dựng để lưu trữ tất cả các factor dài ℓ của X Mỗi lá của trie tương ứng với một factor và chứa một bucket lưu danh sách các vị trí mà factor đó xuất hiện trong X.
Trong pha tìm kiếm, thuật toán duyệt các factor dài ℓ trong văn bản Y tại các vị trí j=k×(m−ℓ+1)−1, với k là số nguyên trong khoảng [1,⌊(n−ℓ)/m⌋] Nếu factor Y[j…j+ℓ−1] tồn tại trong T(X), thuật toán kiểm tra toàn bộ mẫu tại các vị trí tương ứng.
So sánh được thực hiện từ trái sang phải. b Bước thực hiện:
Xâu mẫu X=(x0,x1, ,xm−1), độ dài m.
Văn bản Y=(y0,y1, ,yn−1), độ dài n.
Tất cả vị trí xuất hiện của X trong Y.
// Tiền xử lý: Xây dựng trie T(X) và buckets ell = ceiling(log_sigma(m)) // Độ dài factor: ell = log_sigma(m)
T = buildTrie(X, m, ell) // Xây dựng trie cho các factor dài ell buckets = [] // Buckets: factor -> danh sách vị trí for i from 0 to m-ell: factor = X[i i+ell-1] addToBucket(T, factor, i) // Thêm vị trí i vào bucket của facto r
// Tìm kiếm shift = m - ell + 1 for j from ell-1 to n-ell step shift: factor = Y[j j+ell-1] // Lấy factor từ Y if factor in buckets: // Nếu factor tồn tại trong T(X) for pos in buckets[factor]: // Với mỗi vị trí pos của facto r trong X start = j - pos // Tính vị trí bắt đầu của mẫu tr ong Y if start >= 0: i = 0 while i < m and X[i] == Y[start + i]: // So sánh to àn bộ mẫu i = i + 1 if i == m: // Nếu khớp toàn bộ
OUTPUT(start) // Xuất vị trí khớp
4.1.2 Đánh giá độ phức tạp
Thời gian tiền xử lý: O(m), vì việc xây dựng trie và buckets duyệt qua xâu mẫu một lần (giả định σ là hằng số).
Thời gian tìm kiếm tệ nhất: O(m×n), khi tất cả các factor trong Y đều có trong X, dẫn đến kiểm tra toàn bộ mẫu tại mỗi vị trí.
Số lần so sánh ký tự trung bình: O(logσm×(n/(m−logσm))), nhờ nhảy dựa trên các factor không khớp.
Không gian (Bộ nhớ): O(m), để lưu trie và buckets (kích thước alphabet được coi là hằng số).
Tập dữ liệu kiểm thử:
Giả sử σ=4 (alphabet {A, C, G, T}), ta chọn ℓ=⌈logσm⌉=⌈log48⌉=2.
Bảng buckets: u (factor) z[u] (danh sách vị trí)
Fact or Tra cứu Kết quả Dịch cửa sổ (j) Ghi chú j=1 GC
Không khớp j=7 start=1, không khớp tại
Fact or Tra cứu Kết quả Dịch cửa sổ (j) Ghi chú j=7 CA
CAG có tại (1) Khớp tại 6 j start=6, khớp toàn bộ mẫu. j AGT Không có Không kiểm tra j Nhảy qua vì AGT không có trong X. j AGT Không có Không kiểm tra j% Nhảy qua, kết thúc vì j>n.
4.1.4 Cài đặt thuật toán import math from collections import defaultdict
# Hàm tiền xử lý: Xây dựng buckets cho các factor dài ell def preprocess(X, m, sigma): ell = math.ceil(math.log(m, sigma)) # Độ dài factor buckets = defaultdict(list) for i in range(m - ell + 1): factor = X[i:i+ell] # Lấy factor dài ell buckets[factor].append(i) # Thêm vị trí vào bucket return buckets, ell
# Hàm tìm kiếm Alpha Skip Search def alphaSkipSearch(X, m, Y, n, sigma): buckets, ell = preprocess(X, m, sigma) # Tiền xử lý positions = [] # Lưu các vị trí xuất hiện shift = m - ell + 1 # Bước nhảy j = ell - 1 # Vị trí bắt đầu while j = 0: i = 0 while i < m and X[i] == Y[start + i]: # So sánh toàn bộ mẫu i += 1 if i == m: # Nếu khớp positions.append(start) j += shift # Dịch cửa sổ return positions
Y = "GCATCGCAGAGAGTATACAGTACG" sigma = 4 # Kích thước alphabet {A, C, G, T} positions = alphaSkipSearch(X, len(X), Y, len(Y), sigma) print("Vị trí xuất hiện:", positions)
Thuật toán Colussi Algorithm
4.2.1 Mô tả thuật toán a Ý tưởng chính:
Colussi là một cải tiến của thuật toán Knuth-Morris-Pratt (KMP), sử dụng chiến lược so sánh ký tự theo thứ tự cụ thể (phân chia các vị trí mẫu thành hai tập hợp: "noholes" và "holes").
So sánh được thực hiện từ trái sang phải trong các tập hợp này, giúp giảm số lần so sánh ký tự trong trường hợp xấu nhất.
Thuật toán sử dụng hai bảng tiền xử lý: một bảng cho các "noholes" (vị trí không bị gián đoạn) và một bảng cho các "holes" (vị trí gián đoạn), kết hợp với kỹ thuật biên (border) để dịch cửa sổ. b Bước thực hiện:
Tất cả vị trí xuất hiện của X trong Y.
// Tiền xử lý: Tạo bảng noholes và holes
(noholes, holes) = preprocessColussi(X, m, Sigma); j = 0; // Vị trí bắt đầu của cửa sổ trong Y while (j i] or [m])) def Colussi(X, Y): m = len(X) n = len(Y) if m == 0 or n == 0 or m > n: return []
# Tiền xử lý noholes, holes = preprocessColussi(X, m) positions = [] j = 0 while j