Professional Documents
Culture Documents
2
Phần thứ hai
NỘI DUNG CHUYÊN ĐỀ
I. SƠ LƢỢC VỀ HÀM BĂM
I.1. Khái niệm
Các phép toán trên các cấu trúc dữ liệu nhƣ danh sách, cây nhị phân,…
phần lớn đƣợc thực hiện bằng cách so sánh các phần tử của cấu trúc, do vậy thời
gian truy xuất không nhanh và phụ thuộc vào kích thƣớc của cấu trúc.
Bảng băm giúp hạn chế số lần so sánh, do đó giảm thiểu đƣợc thời gian
truy xuất. Độ phức tạp của các phép toán trên bảng băm thƣờng có bậc là O(1)
và không phụ thuộc vào kích thƣớc của bảng băm.
Các phép toán chính thƣờng dùng trên cấu trúc bảng băm:
Phép băm hay hàm băm (hash function)
Tập khoá của các phần tử trên bảng băm
Tập địa chỉ trên bảng băm
Phép toán thêm phần tử vào bảng băm
Phép toán xoá một phần tử trên bảng băm
Phép toán tìm kiếm trên bảng băm
Thông thƣờng bảng băm đƣợc sử dụng khi cần giải quyết những bài toán
có các cấu trúc dữ liệu lớn và đƣợc lƣu trữ ở bộ nhớ ngoài.
Phép băm (Hash Function): Trong hầu hết các ứng dụng, khoá đƣợc dùng
nhƣ một phƣơng thức để truy xuất dữ liệu một cách gián tiếp. Hàm đƣợc dùng
để ánh xạ một khoá vào một dãy các số nguyên và dùng các giá trị nguyên này
để truy xuất dữ liệu đƣợc gọi là hàm băm.
Vấn đề: Cho trƣớc một tập S gồm các phần tử đƣợc đặc trƣng bởi giá trị
khóa. Trên giá trị các khóa này có quan hệ thứ tự. Tổ chức S nhƣ thế nào để tìm
kiếm 1 phần tử có khóa k cho trƣớc có độ phức tạp ít nhất trong giới hạn bộ nhớ
cho phép?
Ý tưởng: Biến đổi khóa k thành một số (bằng hàm hash) và sử dụng số
này nhƣ là địa chỉ để tìm kiếm trên bảng dữ liệu.
3
Hình vẽ dƣới đây minh họa cho ý tƣởng này:
Nhƣ vậy, hàm băm là hàm biến đổi khóa của phần tử thành địa chỉ trên
bảng băm.
Khóa có thể là dạng số hay số dạng chuỗi. Giải quyết vấn đề băm với các
khoá không phải là số nguyên:
Tìm cách biến đổi khoá thành số nguyên
Ví dụ loại bỏ dấu „-‟ trong mã số 9635-8904 đƣa về số nguyên 96358904
Đối với chuỗi, sử dụng giá trị các ký tự trong bảng mã ASCCI
Sau đó sử dụng các hàm băm chuẩn trên số nguyên.
I.2. Hàm Băm sử dụng Phƣơng pháp chia
Dùng số dƣ:
o h(k) = k mod m
o k là khoá, m là kích thƣớc của bảng.
Vấn đề chọn giá trị m
o m = 2n (không tốt)
o nếu chọn m= 2n thông thƣờng không tốt h(k) = k mod 2n sẽ chọn
cùng n bits cuối của k
o m là nguyên tố (tốt). Thông thƣờng m đƣợc chọn là số nguyên tố
n
gần với 2 . Chẳng hạn bảng ~4000 mục, chọn m = 4093
I.3. Hàm Băm sử dụng Phƣơng pháp nhân
Sử dụng
o h(k) = m (k A mod 1)
o k là khóa, m là kích thƣớc bảng, A là hằng số: 0 < A < 1
Chọn m và A
o m thƣờng chọn m = 2p
4
o Sự tối ƣu trong việc chọn A phụ thuộc vào đặc trƣng của dữ liệu.
o Theo Knuth chọn A = 1/2( 5 -1) 0.618033987 đƣợc xem là tốt.
I.4. Phép băm phổ quát
Việc chọn hàm băm m không tốt có thể dẫn đến xác suất đụng độ lớn.
Giải pháp:
o Lựa chọn hàm băm h ngẫu nhiên.
o Chọn hàm băm độc lập với khóa.
o Khởi tạo một tập các hàm băm H phổ quát và từ đó h đƣợc chọn
ngẫu nhiên.
o Một tập các hàm băm H là phổ quát (universal ) nếu với
mọi f,k H và 2 khoá k, l ta có xác suất: Pr{f(k) = f(l)} <= 1/m
Ví dụ: Giả sử nếu khoá là một số nguyên, dƣơng và HK(key) là một số
nguyên với một digit từ 0..9, Thế thì, hàm băm sẽ dùng toán tử modulo-10 để trả
về giá trị tƣơng ứng của một khoá. Chẳng hạn: nếu khoá=49 thì HF(49)=9.
Một cách tổng quát, với một hàm băm, nhiều khoá khác nhau có thể cho
cùng một giá trị băm. Trong tình huống này xảy ra sự xung đột (collision) và cần
thiết phải giải quyết sự đụng độ này. Một trong những phƣơng pháp giải quyết
sự xung đột với thời gian nhanh là sử dụng các cấu trúc danh sách đặc, hay danh
sách kề có kích thƣớc cố định.
Các cấu trúc bảng băm đơn giản, thƣờng đƣợc cài đặt bằng các danh sách
kề. Do vậy, để truy xuất một phần tử trên các bảng băm thuộc loại này, chỉ cần
hai khóa tƣơng ứng với hàng thứ i và cột thứ j để định vị một phần tử trên bảng.
Mở rộng: Có nhiều bảng băm khác nhƣ: Bảng băm chữ nhật (m hàng, n
cột),Bảng băm tam giác dƣới (m hàng) và bảng băm tam giác trên (n cột), Bảng
băm đƣờng chéo (n cột), Bảng băm ADT,…
Với mỗi loại bảng băm cần thiết phải xác định tập khóa K, xác định tập địa
chỉ M và xây dựng hàm băm HF (Hash Function) cho phù hợp.
Mặt khác, khi xây dựng hàm băm cũng cần thiết phải tìm kiếm các giải
pháp để giải quyết sự xung đột, nghĩa là giảm thiểu sự ánh xạ của nhiều khoá
khác nhau vào cùng một địa chỉ (ánh xạ nhiều-một).
5
II. MỘT SỐ BÀI TẬP VẬN DỤNG
Input:
aaaaa
aa
Output:
1234
Thuật toán:
Mảng a[],b[] chứa các kí tự xâu a,b.
Mảng A[] là mã hash của xâu a[1..i] . Nghĩa là xâu tạo thành từ các kí tự từ
1..i của mảng a biểu diễn mã là A[i];
Tƣơng tự với mảng B[] là mã hash của xâu b[1..i] . Nghĩa là xâu tạo thành
từ các kí tự từ 1..i của mảng b biểu diễn mã là B[i];
Ta xây dựng 1 hàm get(long long h[],int l,int r) để lấy mã hash của mảng h
trong đoạn từ l đến r trong xâu;
Giờ ta chỉ phải kiểm tra từng mã hash độ dài n của xâu A có bằng mã hash
độ dài n của xâu B không.
6
Code Pascal:
const
fi='';
fo='';
maxn=trunc(1e6);
base=trunc(1e9)+7;
pp=307;
var
f : array[0..maxn] of int64;
i,j,n,m : longint;
hb : int64;
a,b : array[1..maxn] of byte;
ha,pow : array[0..maxn] of int64;
procedure enter;
var x : ansistring;
begin
assign(input,fi);reset(input);
readln(x);
m := length(x);
for i := 1 to m do a[i] := ord(x[i])-48;
readln(x);
n := length(x);
for i := 1 to n do b[i] := ord(x[i])-48;
close(input);
end;
function gethash(l,r : longint) : int64;
begin
gethash := (ha[r]-ha[l-1]*pow[r-l+1] + base*base) mod base;
end;
procedure process;
begin
assign(output,fo);rewrite(output);
ha[0] := 0;
for i:=1 to m do ha[i] := (ha[i-1]*pp + a[i]) mod base;
pow[0] := 1;
for i:=1 to m do pow[i] := pow[i-1]*pp mod base;
hb := 0;
for i:=1 to n do hb := (hb*pp + b[i]) mod base;
for i:=1 to m-n+1 do
if hb = gethash(i,i+n-1) then
write(i,' ');
close(output);
end;
begin
enter;
7
process;
end.
Codes Blocks :
#include <bits/stdc++.h>
#define FORE(i, a, b) for(int i = a; i <= b; i++)
#define FORD(i, a, b) for(int i = a; i >= b; i--)
#define FOR(i, a, b) for(int i = a; i < b; i++)
const int MAXN = 1e6 +10;
const int INF = 1e9 + 7;
using namespace std;
//string a,b;
int m,n;
char a[MAXN],b[MAXN];
long long A[MAXN],B[MAXN],M[MAXN];
int get(long long h[],int l,int r)
{
return (h[r] - h[l-1]*M[r-l+1] + 1LL*INF*INF)%INF;
}
int main()
{
ios::sync_with_stdio(0); cin.tie(0);
#ifndef ONLINE_JUDGE
freopen("substr.inp", "r", stdin);
freopen("substr.out", "w", stdout);
#endif
M[0]=1; M[1]=2309;
FOR(i,2,MAXN) M[i]=M[i-1]*M[1] %INF;
cin>>a+1; m=strlen(a+1);
cin>>b+1; n=strlen(b+1);
//FORE(i,1,m) cout<<a[i]<<endl;
FORE(i,1,m) A[i]=(A[i-1]*M[1]+a[i])%INF;
FORE(i,1,n) B[i]=(B[i-1]*M[1]+b[i])%INF;
//FORE(i,1,m) cout<<A[i]<<endl;
FORE(i,1,m-n+1)
{
if (get(A,i,i+n-1)==B[n]) cout<<i<<' ';
}
return 0;
}
8
Bài 2: Tiền tố và hậu tố http://vn.spoj.com/problems/C11STR2/)
Xâu a đƣợc gọi là TIềN Tố của xâu b nếu xâu a trùng với phần đầu của xâu
b. Ví dụ pre là tiền tố của prefix
Xâu a đƣợc gọi là HậU Tố của xâu b nếu xâu a trùng với phần cuối của xâu
b. Ví dụ fix là hậu tố của suffix
YENTHANH132 vừa mới học về tiền tố và hậu tố nên hôm nay anh ta sẽ
đố các bạn một bài toán đơn giản về tiền tố và hậu tố nhƣ sau:
Cho 2 xâu a,b gồm các kí tự latin thƣờng ('a' đến 'z')
Tìm 1 xâu c thỏa mãng:
1. Xâu a là tiền tố của xâu c
2. Xâu b là hậu tố của xâu c
3. Độ xài xâu c là ngắn nhất.
Input
Dòng 1: Xâu a
Dòng 2: Xâu b
Output
Một dòng duy nhất là xâu c.
Giới hạn:
40% số test có độ dài 2 xâu a,b <= 1000 kí tự
Trong toàn bộ test, độ dài 2 xâu a,b <= 105 kí tự
2 xâu a,b không nhất thiết phải khác nhau
Ví dụ:
Thuật toán:Để thỏa mãn xâu c có độ dài ngắn nhất thì phần hậu tố xâu a
phải trùng càng nhiều với phần tiền tố xâu b.
Chúng ta kiểm tra việc hậu tố xâu a có giống tiền tố xâu b không bằng cách
xử dụng hash để giảm độ phức tạp thuật toán.
9
Gọi c = a + b;
Ta sẽ duyệt số ký tự cần xóa để xâu c thỏa mãn điều kiện đề bài.
Theo bài ra điều kiện để xâu c chấp nhận b[1,i] = a[m-i+1,m].
Để thuật toán so sánh xâu giảm độ phức tạp, ta sử dụng Hash.
Code Pascal:
{$H+}
uses math;
const
fi='';
fo='';
base=trunc(1e9);
pp=307;
maxn=trunc(1e5);
var
a,b,c : string;
i,j,n,m : longint;
ha,hb : array[0..maxn] of int64;
pow : array[0..maxn] of int64;
procedure enter;
begin
assign(input,fi);reset(input);
readln(a);
readln(b);
close(input);
end;
procedure swap( var m,n : longint; var a,b : string);
var tg1 : longint;
tg2 : string;
begin
tg1 := m ; m := n ; n:= tg1;
tg2 := a; a :=b ; b:= tg2;
end;
function getha(l,r : longint) : int64;
begin
getha := (ha[r]-ha[l-1]*pow[r-l+1] + base*base) mod base;
end;
function gethb(l,r : longint) : int64;
begin
gethb := (hb[r]-hb[l-1]*pow[r-l+1] + base*base) mod base;
end;
10
procedure process;
begin
m := length(a); n := length(b);
c := a + b;
//if m<n then swap(m,n,a,b);
pow[0] := 1;
for i:=1 to max(m,n) do pow[i] := pow[i-1]*pp mod base;
ha[0] := 0; hb[0] := 0;
for i:=1 to m do ha[i] := (ha[i-1]*pp + ord(a[i])-48) mod base;
for i:=1 to n do hb[i] := (hb[i-1]*pp + ord(b[i])-48) mod base;
for i:=min(m,n) downto 1 do
if gethb(1,i) = getha(m-i+1,m) then
begin
delete(c,m-i+1,i);
break;
end;
end;
procedure print;
begin
assign(output,fo);rewrite(output);
writeln(c);
close(output);
end;
begin
enter;
process;
print;
end.
Codes Blocks:
#include <bits/stdc++.h>
#define FORE(i, a, b) for(int i = a; i <= b; ++i)
#define FORD(i, a, b) for(int i = a; i >= b; --i)
#define FOR(i, a, b) for(int i = a; i < b; ++i)
#define X first
#define Y second
#define long long long
const int MAXN = 1e5 +10;
const long INF = 1e9 + 9;
const int N = 5000;
11
{
return (A[r]-A[l-1]*M[r-l+1]+INF*INF)%INF;
}
int main()
{
ios::sync_with_stdio(0); cin.tie(0);
#ifndef ONLINE_JUDGE
freopen("c11str2.inp", "r", stdin);
freopen("c11str2.out", "w", stdout);
#endif
cin>>a; n=a.size();
cin>>b; m=b.size();
M[1]=2802;
FORE(i,2,MAXN-5) M[i]=M[i-1]*M[1]%INF;
FORE(i,1,n) A[i]=(A[i-1]*M[1]+a[i-1])%INF;
FORE(i,1,m) B[i]=(B[i-1]*M[1]+b[i-1])%INF;
int ll=min(m,n),k=-1;
FORD(i,ll,1)
if (get(n-i+1,n)==B[i])
{
k=i-1;
break;
}
cout<<a;
FORE(i,k+1,m-1) cout<<b[i];
return 0;
}
Bài 3: ESIGN (Bài của thầy Nguyễn Thanh Tùng)
Trong chữ ký điện tử có phần nhận dạng ngƣời gửi là dãy số nguyên P = (p1, p2,
. . ., pn). P đƣợc gọi là Mã nhận dạng tên. Ở công ty Aurora mã nhận dạng tên
của mỗi ngƣời đƣợc xây dựng theo quy tắc sau:
Trích một dãy con độ dài n các phần tử liên tiếp nhau của dãy số nguyên
A = (a1, a2, . . ., am),
Hoán vị một cách ngẫu nhiên các vị trí trong dãy con trích đƣợc.
Ví dụ, với m= 6 và A = (2, 1, 4, 6, 3, 9) và n = 4, mã nhận dạng tên có thể là (1,
4, 6, 3), (3, 4, 6, 1), . . . nhƣng (1, 2, 4, 9), (2, 5, 6, 4) không phải là mã nhận
dạng tên của ngƣời trong Công ty.
Cấp bậc của nhân viên trong Công ty càng cao thì vị trí đầu của dãy con đƣợc
trích ra trong A càng nhỏ. Ở ví dụ trên nhân viên với mã nhận dạng (3, 4, 6, 1)
có cấp bậc 2.
12
Cho P và A của một văn bản điện tử. Hãy xác định xem tác giả của văn bản có
phải là ngƣời của Công ty hay không và đƣa ra thông báo tƣơng ứng “YES”
hoặc “NO”. Nếu là ngƣời của Công ty, hãy chỉ ra cấp bậc của tác giả.
Dữ liệu: Vào từ file văn bản ESIGN.INP:
Dòng đầu tiên chứa một số nguyên n (1 ≤ n ≤ 105),
Dòng thứ 2 chứa n số nguyên p1, p2, . . ., pn (1 ≤ pi ≤ 105, i = 1 ÷ n),
Dòng thứ 3 chứa số nguyên m (n ≤ m ≤ 105),
Dòng thứ 2 chứa m số nguyên a1, a2, . . ., am (1 ≤ aj ≤ 105, j = 1 ÷ m).
Kết quả: Đƣa ra file văn bản ESIGN.OUT thông báo “YES” hoặc “NO”. Nếu là
ngƣời của Công ty thì ở dòng tiếp theo đƣa ra một số nguyên – cấp bậc của
ngƣời đó.
Ví dụ:
ESIGN.INP ESIGN.OUT
3 YES
234 2
4
1423
Thuật toán: Bài toán trên đƣa về bài toán kiểm tra 2 xâu có là hoán vị của
nhau.Để kiểm tra nhanh ta vẫn áp dụng tƣ tƣởng thuật toán Hash. Nhƣng khác ở
chỗ tính mảng băm bằng công thức: h[i] := h[i-1] + pow[a[i]];
Code Pascal:
const fi='esign.inp';
fo='esign.out';
maxn=trunc(1e5);
hb=307;
pp=trunc(1e9)+7;
var h :array[0..maxn] of int64;
i,j,n,m :longint;
a :array[0..maxn] of longint;
hp,hq,ha :int64;
ans :longint;
procedure enter;
var t :longint;
begin
h[0]:=1;
for i:=1 to maxn do h[i]:=(h[i-1]*hb) mod pp;
read(n);
for i:=1 to n do
13
begin
read(t);
hp := (hp+h[t]) mod pp;
end;
end;
procedure process;
begin
readln(m);
for i:=1 to m do
begin
read(a[i]);
end;
ha := 1;
for i:=1 to n-1 do ha := (ha+h[a[i]]) mod pp;
a[0] :=0;
for i:=1 to m-n+1 do
begin
ha := (ha+h[a[i+n-1]]-h[a[i-1]]) mod pp;
if ha=hp then
begin
ans := i;
break;
end;
end;
end;
procedure print;
begin
if ans=0 then
begin
writeln('NO');
end
else
begin
writeln('YES');
writeln(ans);
end;
end;
begin
assign(input,fi);reset(input);
assign(output,fo);rewrite(output);
enter;
process;
print;
close(input);close(output);
end.
14
Bài 4: Writing (http://vn.spoj.com/problems/PBCWRI/)
Cho 2 chuỗi A,B chứa các chữ cái trong bảng chữ tiếng Anh (có cả chữ hoa
và chữ thƣờng). Chuỗi A có độ dài n, chuỗi B có độ dài m.
Yêu cầu: Đếm số lần xuất hiện của các hoán vị của chuỗi A trong chuỗi B.
Dữ liệu:
Dòng đầu tiên chứa 2 số nguyên n và m.
Dòng thứ 2 chứa n kí tự của chuỗi A.
Dòng thứ 3 chứa m kí tự của chuỗi B.
Kết qủa:
Một số duy nhất là kết quả của bài toán.
Giới hạn: n ≤ 3000; m ≤ 3 000 000;
Ví dụ:
cAda
AbrAcadAbRa
15
i,j,n,m,dem : longint;
aa : array[0..maxn] of longint;
bb : array[0..maxm] of longint;
a,b : string;
begin
assign(input,fi);reset(input);
assign(output,fo);rewrite(output);
readln(n,m);
readln(a);
readln(b);
for i:=1 to n do aa[i] := ord(a[i])-ord('A');
for i:=1 to m do bb[i] := ord(b[i])-ord('A');
pow[0] := 1;
for i:=1 to 100 do pow[i] := pow[i-1]*pp mod base;
for i:=1 to n do ha := (ha + pow[aa[i]]) mod base;
for i:=1 to m do hb[i] := (hb[i-1] + pow[bb[i]]) mod base;
//tg := hb[n-2];
//for i:=1 to n-1 do tg := tg + pow[bb[i]]; tg := tg +1;
for i:=1 to m-n+1 do
begin
tg := (hb[i+n-1] - hb[i-1] + base*base) mod base;
if tg = ha then inc(dem);
end;
writeln(dem);
close(input);close(output);
end.
16
Phần thứ ba
KẾT LUẬN
Các bài toán vận dụng hàm băm vào giải quyết thật tuyệt vời. Chƣơng trình
chạy nhanh hơn nên luôn đáp ứng đƣợc về mặt thời gian cho phép. Để có thể
nghiên cứu kĩ và sâu hơn về hàm băm, tác giả xin dành để một dịp khác nhé.
Cảm ơn các bạn đã đọc tài liệu!
Tác giả
Nguyễn Thị Hợp
17
TÀI LIỆU THAM KHẢO
1. Trần Đỗ Hùng, Chuyên đề bồi dưỡng HSG Tin học THPT,NXB GD 2007
2. Tài liệu sách giáo khoa chuyên tin
3. Nguyễn Xuân My, Một số vấnđề chọn lọc trong môn Tin học, NXB GD
2002
4. Trang web: http://www.vn.spoj.com
18