You are on page 1of 27

MÔN TIN HỌC - MÃ CHẤM: Ti09a

CHUYÊN ĐỀ : MẢNG BĂM VÀ ỨNG DỤNG


OLIMPIC HÙNG VƯƠNG 2016

MÔN : TIN HỌC


A, Mở đầu.
1, Lý do chọn chuyên đề.
Trong đề thi HSG môn Tin học trong những năm gần đây kiến thức ra rất rộng
ngày càng xuất hiện những thuật toán khó, một trong số đó là kiến thức về mảng
băm cũng đã xuất hiện ví dụ như trong Bài 2 lớp 10 đề thì Olimpic Hùng Vương
năm 2015 cũng đã phải áp dụng bảng băm vào mới xử lí được triệt để bài toán. Vì
vậy trong Hội thảo của trại hè 2016 nhóm Tin học đã chọn chuyên đề mảng băm để
các trường tham dự viết và cùng nhau thảo luận, làm tài liệu giảng dạy.
2, Mục đích của chuyên đề.
Mục đích của chuyên đề làm tài liệu tham khảo cho học sinh và giáo viên trong
quá trình dạy và học đội tuyển HSG của nhà trường.

B, Nội dung chuyên đề.


1, Khái niệm bảng băm.
Nhằm tránh hiện tượng một bảng quá thưa thớt có nhiều vị trí không bao giờ được
dùng đến, chúng ta làm quen với khái niệm băm . Ý tưởng của bảng băm (hình 1) là
cho phép ánh xạ một tập các khóa khác nhau vào các vị trí trong một mảng với kích
thước cho phép. Gọi kích thước mảng này là hash_size, mỗi khóa sẽ được ánh xạ
vào một chỉ số trong khoảng [0, hash_size-1]. Anh xạ này được gọi là hàm băm (hash
function). Một cách lý tưởng, hàm này cần có cách tính đơn giản và phân bổ các khóa
sao cho hai khóa khác nhau luôn vào hai vị trí khác nhau. Nhưng do kích thước mảng
là giới hạn và miền trị của các khóa là rất lớn, điều này là không thể được. Chúng ta
chỉ có thể hy vọng rằng một hàm băm tốt thì sẽ phân bổ được các khóa vào các chỉ
số một cách khá đồng đều và tránh được hiện tượng gom tụ.

Hàm băm nói chung luôn ánh xạ một vài khóa khác nhau vào cùng một chỉ số.
Nếu phần tử cần tìm đang nằm tại chỉ số được ánh xạ đến, vấn đề của chúng ta xem
như đã được giải quyết; ngược lại, chúng ta cần sử dụng một phương pháp nào đó
để giải quyết đụng độ. Việc đụng độ (collision) xảy ra khi hai phần tử cần được chứa
trong cùng một vị trí của bảng.

Trên đây là ý tưởng cơ bản của việc sử dụng bảng băm. Có ba vấn đề chúng ta
cần xem xét khi sử dụng phương pháp băm:

- Tìm hàm băm tốt.


- Xác định phương pháp giải quyết đụng độ.
- Xác định kích thước bảng băm.

Hình 1 – Bảng băm

2, Lựa chọn hàm băm


Hai tiêu chí cơ bản để chọn lựa một hàm băm là:
• Hàm băm cần được tính toán dễ dàng và nhanh chóng.
• Việc phân phối các khóa có thể xuất hiện rải đều trên bảng băm.
Nếu chúng ta biết trước chính xác những khóa nào sẽ xuất hiện, thì chúng ta có
thể xây dựng một hàm băm thật hiệu quả, nhưng nói chung chúng ta thường
không biết trước điều này.
Chúng ta cần lưu ý rằng một hàm băm không hề có tính ngẫu nhiên. Khi tính
nhiều lần cho cùng một khóa, một hàm băm phải cho cùng một trị, có như vậy thì
khóa mới có thể được truy xuất sau khi được lưu trữ.
3, Chia lấy phần dư (modular arithmetic)
Trước hết chúng ta hãy xem xét một trường hợp thật đơn giản. Nếu các khóa là
các số nguyên, hàm băm đơn giản và phổ biến được dùng là phép chia cho hash_size
để lấy phần dư, vì như vậy chúng ta sẽ có các chỉ số thuộc [0, hash_size -1]. Tuy
nhiên cũng cần lưu ý những trường hợp các khóa tập trung vào một số giá trị đặc biệt
nào đó. Chẳng hạn nếu hash_size = 10, mà phần lớn các khóa lại có con số ở hàng
đơn vị là 0. Sự phân tán các khóa phụ thuộc nhiều vào phép chia lấy phần dư, đó
chính là kích thước của bảng băm. Nếu kích thước đó là một bội số của các số nguyên
nhỏ như 2 hoặc 10, thì rất nhiều khóa sẽ cho cùng chỉ số như nhau, trong khi đó có
một số chỉ số rất ít được sử dụng đến. Cách chọn phép chia lấy phần dư tốt nhất
thường là chia cho một số nguyên tố (nhưng không phải là luôn luôn), kết quả sẽ rải
đều các khóa trong bảng băm hơn. Như vậy, thay vì chọn bảng băm kích thước 1000,
chúng ta nên chọn kích thước 997 hoặc 1009; cách chọn 210 = 1024 là một cách chọn
rất dở.
Thông thường, các khóa là các chuỗi ký tự. Một cách tự nhiên, người ta thường
lấy một số nguyên bằng với tổng của các mã ASCII của các ký tự trong khóa
làm đại diện cho nó. Hàm băm với cách viết của C chuẩn sau đây thật đơn giản
và tính cũng rất nhanh:

index Hash(const char *Key, int hash_size)


{
unsigned int HashVal = 0;
while (*Key != ‘\0’)
{
HashVal += *Key;
Key++;
}
return HashVal % hash_size;
}

Tuy nhiên, nếu hash_size lớn, hàm sẽ không phân bổ các khóa tốt. Lấy ví dụ với
hash_size =10007 (một số nguyên tố). Giả sử các khóa có chiều dài 8 ký tự hoặc ít
hơn. Mỗi ký tự có mã ASCII <=127. Giá trị của hàm băm chỉ có thể từ 0 đến 127 x
8 = 1016.

Một cải tiến khác của hàm băm như sau: với giả thiết rằng các khóa đều có ít nhất
3 ký tự, số 27 được dùng vì đó là số ký tự trong bảng chữ cái tiếng Anh (tính cả
khoảng trắng).

index Hash(const char *Key, int hash_size)


{
return (Key[0] + 27*Key[1] + 27*27*Key[2]) %hash_size;
}

Hàm này chỉ quan tâm 3 ký tự đầu của các khóa, nhưng nếu chúng là ngẫu nhiên
và hash_size là 10007 như trên, thì sự phân bổ khá đồng đều. Điều không may ở
đây là các từ trong tiếng Anh không phải là một sự ghép các ký tự một cách ngẫu
nhiên. Mặc dù có đến 263 = 17576 khả năng ghép 3 ký tự, thực tế trong từ điển cho
thấy chỉ có 2851 khả năng xảy ra. Ngay cả khi không có sự đụng độ xảy ra giữa
từng cặp trong các khả năng này, thì cũng chỉ có 28% vị trí trong bảng là được sử
dụng.
Thêm một cải tiến khác như sau đây:

index Hash(const char *Key, int hash_size)


{
unsigned int HashVal = 0;
while (*Key != ‘\0’)
{
HashVal = (HashVal << 5 ) + *Key);
Key++;
}
return HashVal % hash_size;
}

Hàm này quan tâm đến mọi ký tự trong khóa và nói chung có thể phân bổ các
khóa đồng đều trong một bảng kích thước tương đối lớn. Trị của hàm được tính
∑i=0KeySize-1 Key[KeySize-i-1].32i. Đây là đa thức với hệ số là 32 và sử dụng công
thức Horner. Ví dụ, để tính hk = k1 +27k2 +272k3, người ta tính hk = ((k3)*27 +k2)*27
+k1. Việc dùng số 32 thay số 27 là vì với 32 thì không cần làm phép nhân mà chỉ
đơn giản là phép dịch chuyển bit (32 = 25), và thực tế là dùng phép XOR.

Hàm trên đây chưa phải là hàm tốt nhất khi xét đến tiêu chí phân bổ đồng đều,
nhưng nó cho phép việc tính toán được thực hiện rất nhanh chóng. Nếu khóa quá dài
thì nó cũng lộ nhược điểm là phải tính quá lâu. Hơn nữa quá trình dịch bit sẽ làm
mất đi tác dụng của các ký tự đã được xét trước. Thực tế khắc phục điều này bằng
cách không sử dụng tất cả các ký tự có trong khóa.

4, Cách cài đặt.


Hàm hash là một thuật toán được ứng dụng khá nhiều trong các bài toán về xử lí
xâu, dãy, đặc biệt nó còn là một phương pháp mã hóa dữ liệu thông tin trong bảo
mật, điển hình như mã hóa MD5.
Đây được đánh giá là một thuật toán hết sức hiệu quả, đặc biệt là trong thi cử,
bởi các ưu điểm sau:
- Tốc độ thực thi.

- Linh động trong các bài toán.

- Cài đặt thực sự rất đơn giản.

Ở đây ta xét các bài toán có liên quan đến kiểu dữ liệu dạng xâu.
Ý tưởng của thuật toán:
Ta sẽ mã hóa đưa xâu kí tự về một hệ cơ số bất kì, hiểu đơn giản thì nó chỉ là sự
chuyển đổi hệ cơ số trong đó, mỗi một xâu con bất kì trong S đều có duy nhất một
giá trị nhất định trong hệ cơ số thập phân, dễ thấy 2 xâu bằng nhau chỉ khi mã hash
của 2 xâu bằng nhau, tuy nhiên điều ngược lại thì chưa chắc đúng, bởi vậy nhược
điểm duy nhất của thuật toán là tính chính xác của thuật toán, nhưng rất khó để có
thể tìm được trường hợp sai.
Giả sử rằng Σ = {a, b,…, z}, nghĩa là Σ chỉ gồm các chữ cái Latin in thường.
Để biểu diễn một xâu, thay vì dùng chữ cái, chúng ta sẽ chuyển sang biểu diễn số
ở hệ 26.

Ví dụ: xâu ‘abcd’ biểu diễn hệ 26: ‘a’*263+ ‘b’*262+ ‘c’*261 + ‘d’*260 đổi ra số
ở hệ cơ số 10 tương ứng là: 65*263+66*262+67*26+68= 1188866.

Dễ thấy rằng, muốn so sánh 2 xâu bằng nhau khi và chỉ khi biểu diễn của 2 xâu
ở hệ cơ số 10 giống nhau.

Ví dụ xâu A=B ↔ Mã A = Mã B
Tuy nhiên nếu xâu quá dài thì Mã A, Mã B cũng rất lớn. Chính vì thế, ta lấy
mod cho 1 số base nguyên tố rất lớn nào đó ví dụ 109+7, hay 2.109+11…

A=B  Mã A mod base = Mã B mod base

Dễ dàng nhận thấy việc so sánh Mã A mod base với Mã B mod base rồi kết luận
A có bằng với B hay không là sai. Mã A mod base = Mã B mod base chỉ là điều
kiện cần để A bằng B chứ chưa phải điều kiện đủ. Tuy nhiên, chúng ta sẽ chấp
nhận lập luận sai này trong thuật toán Hash. Và coi điều kiện cần như điều kiện
đủ. Trên thực tế, lập luận sai này có những lúc dẫn đến so sánh xâu không chính
xác và chương trình bị chạy ra kết quả sai. Nhưng cũng thực tế cho thấy rằng, khi
chọn base là một số nguyên lớn, số lượng những trường hợp sai rất ít, và ta có
thể coi Hash là một thuật toán chính xác.

Các bài toán chúng ta đặt ra chủ yếu để phục vụ việc so sánh xâu T bất kì với một
xâu con bất kì trong xâu S đã cho.
- Ta có thể đi so sánh lần lượt xâu T với các xâu con của S với đpt( (m – n) * n),
dễ thấy hiệu quả của bài toán không cao về mặt thời gian. Hiển nhiên không
thể xử lí được các bài toán với dữ liệu khoảng 10^6.

- Cách làm mà chúng ta xét đến để thu được hiệu quả hơn ở đây là Hash

- Việc cài đặt được thực hiện như sau:

void Make_hash()
{
Pow[0] = 1
for(int i = 1; i <= m; i ++) Pow[i] = Pow[i – 1] * BASE;
for(int i = 1; i <= m; i ++) h[i] = h[i – 1] * BASE + S[i];
}
int Get(int l, int r)
{
Return h[r] – h[l – 1] * Pow[r – l + 1];
}
Giải thích:
- Make_hash(): hàm khởi tạo Hash.

- Get(l, r): hàm lấy giá trị đại diện của xâu từ vị trí l đến vị trí r.
- h[i]: đại diện cho xâu con từ vị trí 1 đến vị trí thứ i, đại diện cho hàm Hash.

- BASE: một số nguyên tố bất kì lớn hơn số lượng phần tử trong tập hợp kí tự
trong xâu S.

- Pow[i]: lũy thừa bậc i của BASE, nhằm tăng tốc độ tính của hàm get().

B. Bài tập minh họa.


Bài 1: Xâu đối xứng ( Bài 3 lớp10 đề Olimpic trại hè HV2015)
Tham dự thi trại hè lần này, bạn Sơn Tùng đăng ký tham dự trò chơi RCV,
phần thưởng cho người chiến thắng là một chiếc thẻ nhớ 16GB từ ban tổ chức (BTC).
Trò chơi như sau: BCT đưa ra một chuỗi các ký tự chỉ bao gồm các chữ cái in thường
từ a đến z, và một số bộ l, r. Người chơi sẽ trả lời một bộ các câu hỏi của BTC có
dạng: xâu con nhận được kể từ vị trí l đến vị trí r có đối xứng hay không?
Bạn hãy giúp Sơn Tùng dành chiến thắng cuộc thi này.
Dữ liệu:
 Dòng đầu ghi xâu ký tự không cho biết độ dài.
 Dòng tiếp theo ghi số nguyên 𝑘 – số câu hỏi của BTC.
 𝑘 dòng tiếp theo, mỗi dòng ghi hai số nguyên l, r
Kết quả: Gồm 𝑘 dòng, mỗi dòng ghi tương ứng câu trả lời, nếu có ghi 1,
ngược lại ghi -1.
Ví dụ:
Input output
abxbagredcnmooojhggohoreomodioibba 1
5 -1
15 1
10 23 1
25 27 1
29 31
13 15
Ràng buộc:
 30% số test có độ dài xâu ≤ 100
 30% số test có xâu có độ dài ≤ 103
 40% số test độ dài xâu không quá 105 và 𝑘 ≤ 105 , 1 ≤ 𝑙 ≤ 𝑟

Đây là bài cơ bản về tính chất của xâu đối xứng : Xâu đối xứng là xâu mà đọc
xuôi hay đọc ngược đều giống nhau.
Sub 1 và Sub 2 : Ta có thể duyệt mỗi truy vấn kiểm tra xem xâu ngược của xâu
con từ vị trí l đến vị trí r có giống nhau không . Độ phức tạp O(n^2). Hoặc có thể cải
tiến bằng tìm kiếm nhị phân thì độ phức tạp vẫn là O(nlgn).
Sub 3: Ta sử dụng mảng băm để giải quyết.
- Lập hai mảng băm H1 và H2, H1 là mã băm xuôi của xâu ban đầu, H2 là
mã băm ngược của xâu đó.
- Mỗi truy vấn kiểm tra xem mã băm ngược và xuôi từ vị tri l đến r có
giống nhau không.

Độ phức tạp O(n).


Chương trình minh họa duyệt.
using namespace std;
const int maxn = 1e5 + 7;
char s[maxn];
int k;
bool ok(int l, int r) {
for(int i = l; i <= r; i++) {
if(s[i] != s[r - (i - l)]) return 0;
}
return 1;
}
int main() {
//ios_base::sync_with_stdio(0);
freopen("strpalin.inp", "r", stdin);
freopen("strpalin.out", "w", stdout);
scanf("%s ", s + 1);
// n = strlen(s + 1);
scanf("%d ", &k);
for(int i = 1; i <= k; i++) {
int l, r;
scanf("%d %d ", &l, &r);
printf("%d\n", (ok(l, r) ? 1 : -1));
}
}

Chương trình sử dụng Hash.


#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 7;
const int base = 1e9 + 7;
char s[maxn];
ll H1[maxn], H2[maxn], P[maxn];
int k, n;
void make1() //Tạo Hash H1
{
P[0] = ll(1);
for(int i = 1; i <= n; i++)
P[i] = P[i - 1] * ll(base);
H1[0] = ll(17);
for(int i = 1; i <= n; i++)
H1[i] = H1[i - 1] * ll(base) + ll(s[i] - 'a' + 1);
}
ll get1(int l, int r) {
return H1[r] - H1[l - 1] * P[r - l + 1];
}
void make2() //Tạo bảng Hash H2
{
H2[n + 1] = ll(17);
for(int i = n; i >= 1; i--) {
H2[i] = H2[i + 1] * ll(base) + ll(s[i] - 'a' + 1);
}
}
ll get2(int l, int r) {
return H2[l] - H2[r + 1] * P[r - l + 1];
}
int main() {
//ios_base::sync_with_stdio(0);
freopen("strpalin.inp", "r", stdin);
freopen("strpalin.out", "w", stdout);
scanf("%s ", s + 1);
n = strlen(s + 1);
make1();
make2();
scanf("%d ", &k);
for(int i = 1; i <= k; i++) {
int l, r;
scanf("%d %d ", &l, &r);
printf("%d\n", ((get1(l, r) == get2(l, r) ? 1 : -1)));
}
}

Bài 2: SUBSTR- Xâu con


Cho xâu A và xâu B chỉ gồm các chữ cái thường. Xâu B được gọi là xuất hiện tại
vị trí i của xâu A nếu: A[i] = B[1], A[i+1] = B[2], ..., A[i+length(B)-1] =
B[length(B)].

Hãy tìm tất cả các vị trí mà B xuất hiện trong A.

Input

 Dòng 1: xâu A.
 Dòng 2: xâu B.

Độ dài A, B không quá 1000000.

Output

Ghi ra các vị trí tìm được trên 1 dòng (thứ tự tăng dần). Nếu B không xuất hiện
trong A thì bỏ trắng.

Example

input output
aaaaa 1234

aa

Nguồn: vn.spoj.com

Ý tưởng:
Bài toán có thể giải theo nhiều cách( hàm Z, KMP, hash...)
Đây là một cách làm có sử dụng hash:
Khởi tạo hash với xâu A và giá trị hash của xâu B;
Dễ dàng so sánh được xâu B với các xâu con có độ dài m (m là độ dài xâu B) của
xâu A.

Chương trình tham khảo.


#include <bits/stdc++.h>

using namespace std;

const int N = 1000006;


const int BASE = 107;

string s, t;
int n, m, Pow[N], H[N], h;

void make_hash()
{
Pow[0] = 1;
for(int i = 1; i <= n; i ++) {
H[i] = H[i - 1] * BASE + s[i - 1];
Pow[i] = Pow[i - 1] * BASE;
}

for(int i = 0; i < m; i ++) h = h * BASE + t[i];


}

int get(int l, int r)


{
return H[r] - H[l - 1] * Pow[r - l + 1];
}

int main()
{
ios_base::sync_with_stdio(0);

freopen("SUBSTR.inp","r",stdin);
freopen("SUBSTR.out","w",stdout);

cin >> s;
cin >> t;

n = s.size();
m = t.size();

make_hash();
for(int i = 1; i <= n - m + 1; i ++)
if (h == get(i, i + m - 1)) cout << i << " ";
return 0;
}

Bài 3: Mật khẩu


Ngân hàng GreenBank dùng loại mật khẩu sử dụng một lần cho mọi truy nhập
tới các dịch vụ của ngân hàng.
Khi có yêu cầu truy nhập ngân hàng sẽ được cung cấp một từ khóa. Người truy
nhập chỉ phải nhập vào mật khẩu là một xâu ký tự palindrome độ dài ngắn nhất có
chứa từ khóa như một xâu con các ký tự liên tiếp nhau.
Với từ khóa đã cho hãy xác định mật khẩu cần nhập vào. Nếu tồn tại nhiều xâu
khác nhau cùng đáp ứng yêu cầu là mật khẩu thì đưa ra xâu bất kỳ trong số đó.
Dữ liệu: Vào từ file văn bản PAROLE.INP gồm một dòng chứa từ khóa có độ
dài không vượt quá 3×105 và chỉ bao gồm các ký tự la tinh thường.

Kết quả: Đưa ra file văn bản PAROLE.OUT mật khẩu tìm được.
Ví dụ:
PAROLE.INP PAROLE.OUT
ab aba
a a
Ý tưởng:
- Dễ thấy kết quả bài toán sẽ là tS hoặc St với S là xâu đề bài cho còn t là
xâu con ngắn nhất mà ta sẽ thêm vào. Từ đó dẫn với việc cần làm là tìm
xâu con đối xứng dài nhất tại 2 đầu của xâu S. Ta có thể sử dụng hash để
giải quyết vấn đề trên, ta sẽ tạo thêm một xâu T từ xâu S với
T[i] = S[n – i – 1], sau đó khởi tạo 2 mảng hs[] và ht[] từ 2 xâu trên.
- Với mỗi vị trí i ta có thể kiểm tra xâu con từ vị trí 0 -> i (trong C++ vị trí
bắt đầu của xâu mặc định là 0) có phải là xâu đối xứng hay không bằng
cách so sánh giá trị hash từ 0 -> i của xâu S với giá trị hash từ (n – i – 1) -
> (n – 1) của xâu T. Làm hoàn toàn tương tự với xâu con ở đầu còn lại.

Chương trình tham khảo.


#include <bits/stdc++.h>

using namespace std;

const int N = 300005;


const int BASE = 31;

string s, t;
int n, B[N], h[2][N];

void make_hash()
{
cin >> s;
n = s.size();

t = s;
reverse( t.begin(), t.end());
s = ' ' + s + ' ';

B[0] = 1;
for(int i = 1; i <= n; i ++) B[i] = B[i - 1] * BASE;

for(int i = 1; i <= n; i ++) h[0][i] = h[0][i - 1] * BASE + s[i];


reverse( s.begin(), s.end());
for(int i = 1; i <= n; i ++) h[1][i] = h[1][i - 1] * BASE + s[i];
}

int get( int type, int l, int r) { return h[type][r] - h[type][l - 1] * B[r - l + 1]; }

int main()
{
ios_base::sync_with_stdio(0);

freopen("PAROLE.inp","r",stdin);
freopen("PAROLE.out","w",stdout);

make_hash();

int pos = -1;


for(int i = 1; i <= n; i ++)
if ( get( 0, 1, i) == get( 1, n - i + 1, n)) pos = i;

int check = 0;
for(int i = 1; i <= n; i ++)
if ( get( 1, 1, i) == get( 0, n - i + 1, n))
if (i > pos) check = 1, pos = i;
if (check) {
for(int i = n; i > pos; i --) cout << s[i];
cout << t;
return 0;
}

cout << t;
for(int i = n - pos; i >= 1; i --) cout << s[i];

return 0;
}

Bài 4: ADN

Một trong những nhiệm vụ của phân tích gen di truyền là so sánh độ giống nhau
của 2 chuỗi . Chuỗi đó là xâu chỉ chứa các ký tự từ tập . Khi so
sánh người ta có thể đẩy vòng tròn các ký tự trong chuỗi . Mục tiêu của so sánh
là tìm đoạn dài nhất giống nhau ở hai xâu bao gồm các ký tự liên tiếp. Độ dài
đoạn dài nhất giống nhau này được gọi là độ giống nhau của hai chuỗi.

Yêu cầu: Cho hai chuỗi có độ dài giống nhau và không vượt quá . Hãy
viết chương trình xác định độ giống nhau của hai chuỗi và đưa hai chuỗi về dạng sao
cho phần giống nhau là hậu tố ( ) của mỗi chuỗi.
Dữ liệu: Vào từ file văn bản ADN.INP gồm hai dòng, mỗi dòng chứa một chuỗi
ADN.
Kết quả: Đưa ra file văn bản ADN.OUT:

• Dòng đầu tiên chứa một số nguyên – độ giống nhau của hai chuỗi.
• Dòng thứ 2 và dòng thứ 3 chứa các chuỗi đã cho sau khi biến đổi theo yêu cầu.

Ví dụ:
ADN.INP ADN.OUT
ACAGTG 5
AGTGTC ACAGTG
TCAGTG

Chương trình tham khảo.

#include <bits/stdc++.h>
#define ii pair< int, int>
#define x first
#define y second
using namespace std;

const int N = 50006;


const int BASE = 31;

string s[2];
int n, h[2][2 * N], B[N];
ii b[N];

int get( int type, int l, int r) { return h[type][r] - h[type][l - 1] * B[r - l + 1]; }

ii Check( int x)
{
int m = n / 2;
for(int i = 1; i <= m; i ++)
b[i] = ii( get( 0, i, i + x - 1), i);

sort( b + 1, b + m + 1);
for(int i = 1; i <= m; i ++) {
ii H = ii( get( 1, i, i + x - 1), 0);
int pos1 = lower_bound( b + 1, b + m + 1, H) - b;
if (b[pos1].x != H.x) continue;//return ii( b[pos].y, i);
H.y = m;
int pos2 = upper_bound( b + 1, b + m + 1, H) - b;

while(pos1 < pos2) {


int pos = b[pos1].y;
int check = 1;
for(int j = 1; j <= x; j ++)
if (s[0][pos + j - 1] != s[1][i + j - 1]) {check = 0; break;}
if (check) return ii( pos, i);
pos1 ++;
}
}
return ii( 0, 0);
}

int main()
{
ios_base::sync_with_stdio(0);
freopen("ADN.inp","r",stdin);
freopen("ADN.out","w",stdout);

for(int i = 0; i < 2; i ++)


cin >> s[i], s[i] = ' ' + s[i] + s[i];

n = s[0].size() - 1;
B[0] = 1;
for(int i = 1; i <= n / 2; i ++)
B[i] = B[i - 1] * BASE;

for(int t = 0; t < 2; t ++)


for(int i = 1; i <= n; i ++)
h[t][i] = h[t][i - 1] * BASE + s[t][i] - 'A';

int l = 1;
int r = n / 2;
int ans = 0, m;
ii pos = ii( 1, 1);
while(l <= r)
{
int m = (l + r) /2;
ii x = Check(m);
if (x.x) ans = m, pos = x, l = m + 1;
else r = m - 1;
}
cout << ans << endl;
for(int i = pos.x + ans; i < pos.x + n / 2; i ++) cout << s[0][i];
for(int i = pos.x; i < pos.x + ans; i ++) cout << s[0][i];

cout << endl;


for(int i = pos.y + ans; i < pos.y + n / 2; i ++) cout << s[1][i];
for(int i = pos.y; i < pos.y + ans; i ++) cout << s[1][i];
return 0;
}

Bài 5: DTKSUB - Chuỗi con xuất hiện K lần ( Nguồn spoj.com)


Sau những kỳ công trong những cuộc chinh phục các cấu trúc dữ liệu đặc biệt,
tình bạn giữa pirate và duyhung123abc ngày càng trở nên khăng khít. Rồi bỗng một
ngày nọ,duyhung123abc bỗng ra đi không một lời từ biệt, chỉ để lại một mẫu giấy
cho pirate. Mẩu giấy viết rằng : "Em ơi, anh còn nặng nợ toán lý hóa anh, chưa thể
một lòng theo đuổi tin học. Em hãy làm nốt công việc mà anh em ta còn dang dở
!". pirate đọc xong, nước mắt giàn giụa. Nếu khi hai người gặp nhau, vui sướng như
khi Engels gặp Marx, thì trong giây phút chia ly này, lòng pirate đau đớn như khi
Đỗ Phủ tiễn người tri kỉ Lý Bạch lên đường.
Mất đi người anh cả, pirate như con thuyền mất phương hướng. Cuối cùng, sau
những đêm không ngủ, anh quyết định rằng mình sẽ đợi cho đến
khi duyhung123abc trả xong nợ công danh và quay trở về sẽ tiếp tục nghiên cứu các
cấu trúc dữ liệu đặc biệt. Còn bây giờ, anh ta sẽ đi một con đường mới, đi vào một
thế giới mới, thế giới của các THUẬT TOÁN VỀ CHUỖI. Tuy cô độc một mình,
nhưng với niềm tin của mình, pirate đã lên đường ngay mà không có chút do dự.
Nhưng trớ trêu thay, vạn sự khởi đầu nan. Thử thách đầu tiên mà con người trẻ
tuổi này gặp phải thật đau đầu. Anh ta được cho trước một chuỗi S có độ dài N và
một số K. Thử thách được hoàn thành chỉ khi anh ấy đưa ra được độ dài của chuỗi
dài nhất xuất hiện ít nhất K lần trong chuỗi S. Làm sao đây ! Vừa vực dậy sau một
cú sốc lớn, piraterất cần sự giúp đỡ của các bạn để không mất đi sự nhiệt huyết của
mình !
Input
Dữ liệu vào gồm 2 dòng:

 Dòng 1: Hai số nguyên N và K (1 ≤ N ≤ 50000; 1 ≤ K ≤ 200).


 Dòng 2: Chuỗi S có độ dài N (gồm các chữ cái in thương viết liên tiếp nhau).

Output
Dữ liệu ra gồm một dòng duy nhất là độ dài của chuỗi dài nhất xuất hiện ít nhất
K lần trong chuỗi S.

Example
Input:
52
xxxxx

Output:
4
- Lưu ý: Một chuỗi A[1..m] được gọi là xuất hiện trong chuỗi B[1..n] K lần khi
và chỉ khi tồn tại K vị trí i phân biệt sao cho A[1..m] = B[i..i+m-1].
Ý tưởng:
- Chặt nhị phân + hash
- Chặt nhị phân theo chiều dài trong đó hàm kiểm tra thay vì đếm số lần xuất
hiện của dãy con giống nhau ta sẽ đếm số lần xuất hiện của mã hash của
các dãy con đó.
Độ phức tạp : O(N log N);
Chương trình tham khảo.
#include<bits/stdc++.h>
using namespace std;
//typedef long long ll;
const int base = 1e9 + 7;
const int maxn = 5e4 + 7;
char a[maxn];
int n, k, s, ans, OK[maxn];
int POW[maxn], HASH[maxn], q[maxn];
//map<int, int> q;
void make() {
POW[0] = 1;
for(int i = 1; i <= s; i++) POW[i] = POW[i - 1] * base;
HASH[0] = 17;
for(int i = 1; i <= s; i++) HASH[i] = HASH[i - 1] * base + (a[i] - 'a' + 1);
}
int get(int i, int j) {
return HASH[j] - HASH[i - 1] * POW[j - i + 1];
}
bool ok(int leng) {
//q.clear();
int r = 0, t = 0;
if(OK[leng] != -1) return OK[leng];

for(int i = 1; i <= s - leng + 1; i++) {


q[++r] = get(i, i + leng - 1);
}
sort(q + 1, q + r + 1);
int p = 0;
for(int i = 1; i <= r + 1; i++) {
p++;
if(q[i] != q[i - 1]) {
t = max(t, p);
p = 0;
}
}
return OK[leng] = (t >= k);
}

int main() {
freopen("DTKSUB.inp", "r", stdin);
freopen("DTKSUB.out", "w", stdout);
scanf("%d %d", &n, &k);
scanf("%s ", a + 1);
s = strlen(a + 1);
make();
memset(OK, -1, sizeof(OK));
int l = 1, r = s, mid = 0;
while(l <= r) {
mid = (l + r) >> 1;
if(ok(mid)) {
ans = mid;
l = mid + 1;
} else
{
r = mid - 1;
}
}

printf("%d", ans);
return 0;
}
C, Kết luận.

- Ý tưởng thuật toán Hash dựa trên việc đổi từ hệ cơ số lớn sang hệ thập phân, so
sánh hai số thập phân lớn bằng cách so sánh phần dư của chúng với một số đủ lớn.

- Ưu điểm của thuật toán Hash là cài đặt rất dễ dàng.

- Nhược điểm của thuật toán Hash là tính chính xác. Mặc dù rất khó sinh test để có
thể làm cho thuật toán chạy sai, nhưng không phải là không thể. Vì vậy, để nâng cao
tính chính xác của thuật toán, người ta thường dùng nhiều modulo khác nhau để so
sánh mã Hash (ví dụ như dùng 3 modulo một lúc).

Tài liệu tham khảo


1, CTDL và Giải thuật

2, Hash: Thuật toán so khớp xâu - Lê Khắc Minh Tuệ

3, Một số bài trên Spoj.com

4, Một số tài liệu trên internet.

You might also like