Người viết: Phạm Hoàng Hiệp - University of Georgia
Review bởi:
Cho xâu có độ dài . Hãy tìm tất cả các xâu con đối xứng (palindrome) trong xâu .
Thuật toán Manacher sẽ xử lý bài toán trên với độ phức tạp thời gian .
Lưu ý, vì có thể có xâu con đối xứng trong một xâu nên có thể dễ dàng lầm tưởng rằng không thể tạo ra thuật toán có độ phức tạp tốt hơn cho bài toán này. Tuy nhiên, với một xâu đối xứng, chúng ta có các xâu con ở phía trong cũng là đối xứng.
Cụ thể, giả sử xâu con là một xâu đối xứng. Nếu thì ta có cũng là một xâu đối xứng. Ví dụ, xâu đối xứng thì có thể dễ dàng thấy được rằng các xâu , hay đều đối xứng.
Như vậy, với mỗi điểm chính giữa của một xâu con đối xứng , chúng ta có thể lưu lại độ dài dài nhất sao cho là một xâu đối xứng.
Điểm chính giữa là điểm mà khi đảo ngược trật tự của xâu thì vị trí của điểm này trong xâu không thay đổi. Điểm chính giữa này có thể là một chữ cái hoặc một khoảng trống, tương ứng với xâu đối xứng có độ dài lẻ hoặc chẵn.
Như vậy, từ quan sát trên, chúng ta có thể ngay lập tức đưa ra thuật toán có độ phức tạp . Xét tất cả các vị trí trong xâu (một chữ cái hoặc một khoảng trống), và chạy về hai phía đến khi nào xâu không còn đối xứng nữa.
Cũng như rất nhiều thuật toán liên quan đến xâu khác như Z hay KMP, khi xử lý ở từng vị trí trên xâu, kết quả ở những vị trí trước cho ta rất nhiều dữ liệu để xử lý ở vị trí tiếp theo. Trong bài toán này, chúng ta có thể tận dụng dữ kiện xâu đối xứng như hình vẽ dưới đây.
Giả sử chúng ta có một xâu đối xứng qua
Do
#include<bits/stdc++.h>
using namespace std;
const int MaxN = 5e3 + 5;
int D_odd[MaxN];
int D_even[MaxN];
string S;
int N;
void Calc_D_odd() {
for(int i = 1 ; i <= N ; i++) {
D_odd[i] = 0;
while(i - D_odd[i] - 1 > 0 && i + D_odd[i] + 1 <= N && S[i - D_odd[i] - 1] == S[i + D_odd[i] + 1]) {
D_odd[i]++;
}
}
}
void Calc_D_even() {
int L = 1;
int R = 0;
for(int i = 1 ; i < N ; i++) {
int j = i + 1;
D_even[i] = 0;
while(i - D_even[i] > 0 && j + D_even[i] <= N && S[i - D_even[i]] == S[j + D_even[i]]) {
D_even[i]++;
}
}
}
signed main() {
ios_base::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> S;
N = S.length();
S = ' ' + S; // for 1-base
Calc_D_odd();
Calc_D_even();
for(int i = 1 ; i < N ; i++) {
cout << D_odd[i] * 2 + 1 << ' ' << D_even[i] * 2 << ' ';
}
cout << D_odd[N] * 2 + 1;
}
Do xâu đối xứng độ dài lẻ có điểm chính giữa là một chữ cái, việc xử lý và tính toán sẽ dễ dàng hơn so với trường hợp xâu đối xứng có độ dài chẵn (điểm chính giữa là khoảng trống giữa hai chữ cái). Vì vậy, chúng ta sẽ giải quyết trường hợp này trước.
Chúng ta sẽ tính giá trị
Hoàn toàn tương tự với việc tìm xâu có độ dài lẻ, chúng ta xét các khoảng ở giữa hai ký tự (có thể coi khoảng này là một ký tự). Tuy nhiên, chi tiết cài đặt cần cẩn thận để có được kết quả chính xác.
Để tránh phải xử lý cẩn thận trong trường hợp xâu đối xứng độ dài chẵn, bạn đọc có thể cân nhắc sử dụng cách làm như sau.
Thêm các ký tự đặc biệt giữa hai ký tự liên tiếp trong xâu, khi đó các xâu đối xứng độ dài chẵn sẽ là các xâu đối xứng độ dài lẻ với điểm chính giữa là một ký tự đặc biệt. Các xâu đối xứng độ dài lẻ vẫn là các xâu đối xứng độ dài lẻ với điểm chính giữa là các chữ cái ban đầu trong xâu.
Ký tự đặc biệt được thêm cần phải giống nhau (để đảm bảo tính đối xứng) và khác với tất cả các ký tự được dùng trong xâu. Ví dụ,
Ở mỗi lần tính
Dưới đây là code mẫu của thuật toán Manacher cho bài toán được nêu trong phần Giới thiệu. Các bạn có thể tự cài đặt và nộp ở link sau: Library checker - Enumerate Palindromes
#include<bits/stdc++.h>
using namespace std;
const int MaxN = 5e5 + 5;
int D_odd[MaxN];
int D_even[MaxN];
string S;
int N;
void Calc_D_odd() {
int L = 1;
int R = 0;
for(int i = 1 ; i <= N ; i++) {
if(i > R) D_odd[i] = 0;
else D_odd[i] = min(R - i, D_odd[L + (R - i)]);
while(i - D_odd[i] - 1 > 0 && i + D_odd[i] + 1 <= N && S[i - D_odd[i] - 1] == S[i + D_odd[i] + 1]) {
D_odd[i]++;
}
if(i + D_odd[i] > R) {
R = i + D_odd[i];
L = i - D_odd[i];
}
}
}
void Calc_D_even() {
int L = 1;
int R = 0;
for(int i = 1 ; i < N ; i++) {
int j = i + 1;
if(j > R) D_even[i] = 0;
else D_even[i] = min(R - j + 1, D_even[L + (R - j)]);
while(i - D_even[i] > 0 && j + D_even[i] <= N && S[i - D_even[i]] == S[j + D_even[i]]) {
D_even[i]++;
}
if(i + D_even[i] > R) {
R = i + D_even[i];
L = j - D_even[i];
}
}
}
signed main() {
ios_base::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> S;
N = S.length();
S = ' ' + S; // for 1-base
Calc_D_odd();
Calc_D_even();
for(int i = 1 ; i < N ; i++) {
cout << D_odd[i] * 2 + 1 << ' ' << D_even[i] * 2 << ' ';
}
cout << D_odd[N] * 2 + 1;
}
Cho hai số nguyên dương
Một ma trận con
Một ma trận con được định nghĩa là đẹp nếu có cách để thay đổi thứ tự các chữ cái trên các hàng (không thay đổi thứ tự trên các cột) sao cho tất cả các hàng và cột của ma trận đều con đó đều là xâu đối xứng.
Xác định số ma trận con đẹp trong ma trận đã cho.
#include<bits/stdc++.h>
using namespace std;
const int MaxN = 255;
int Cnt[MaxN][26];
int Cnt_Odd[MaxN];
int D_odd[MaxN];
int D_even[MaxN];
char c[MaxN][MaxN];
bool Ok[MaxN];
int N,M;
int Ans = 0;
bool Equal(int Row1, int Row2) {
if(!Ok[Row1]) return false;
if(!Ok[Row2]) return false;
for(int j = 0 ; j < 26 ; j++) {
if(Cnt[Row1][j] != Cnt[Row2][j]) return false;
}
return true;
}
void Calc_D_odd() {
int L = 1;
int R = 0;
for(int i = 1 ; i <= N ; i++) {
if(i > R) D_odd[i] = 0;
else D_odd[i] = min(R - i, D_odd[L + (R - i)]);
if (Ok[i]) while(i - D_odd[i] - 1 > 0 && i + D_odd[i] + 1 <= N && Equal(i - D_odd[i] - 1, i + D_odd[i] + 1)) {
D_odd[i]++;
}
Ans += (D_odd[i] + Ok[i]);
if(i + D_odd[i] > R) {
R = i + D_odd[i];
L = i - D_odd[i];
}
}
}
void Calc_D_even() {
// D_even[i] is the value for the space between i and i + 1
int L = 1;
int R = 0;
for(int i = 1 ; i < N ; i++) {
int j = i + 1;
if(j > R) D_even[i] = 0;
else D_even[i] = min(R - j + 1, D_even[L + (R - j)]);
while(i - D_even[i] > 0 && j + D_even[i] <= N && Equal(i - D_even[i], j + D_even[i])) {
D_even[i]++;
}
Ans += D_even[i];
if(i + D_even[i] > R) {
R = i + D_even[i];
L = j - D_even[i];
}
}
}
signed main() {
ios_base::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> N >> M;
for(int i = 1 ; i <= N ; i++) {
cin >> c[i] + 1;
}
for(int c1 = 1 ; c1 <= M ; c1++) {
for(int c2 = c1 ; c2 <= M ; c2++) {
bool ok = true;
for(int i = 1 ; i <= N ; i++) {
int t = c[i][c2] - 'a';
Cnt[i][t]++;
if(Cnt[i][t] & 1) Cnt_Odd[i]++;
else Cnt_Odd[i]--;
if(Cnt_Odd[i] > ((c2 - c1 + 1) & 1)) Ok[i] = false;
else Ok[i] = true;
}
Calc_D_odd();
Calc_D_even();
}
for(int i = 1 ; i <= N ; i++) {
for(int j = 0 ; j < 26 ; j++) {
Cnt[i][j] = 0;
}
Cnt_Odd[i] = 0;
}
}
cout << Ans;
}