Professional Documents
Culture Documents
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 - Mã Chấm: Ti09A: Chuyên Đề: Mảng Băm Và Ứng Dụng Olimpic Hùng Vương 2016
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:
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).
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:
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.
Ở đâ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…
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
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().
Đâ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.
Input
Dòng 1: xâu A.
Dòng 2: xâu B.
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.
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;
}
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;
}
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.
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;
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 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
#include <bits/stdc++.h>
#define ii pair< int, int>
#define x first
#define y second
using namespace std;
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;
int main()
{
ios_base::sync_with_stdio(0);
freopen("ADN.inp","r",stdin);
freopen("ADN.out","w",stdout);
n = s[0].size() - 1;
B[0] = 1;
for(int i = 1; i <= n / 2; i ++)
B[i] = B[i - 1] * BASE;
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];
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];
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.
- 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).