MÃ HOÁ BURROWS WHEELER
Ngày nay, có nhiều phương pháp mã hoá thông tin được sử dụng rộng
rãi để đảm bảo tính chất an toàn, bảo mật dữ liệu. Bài viết này trình bày lời
giải cho một đề thi tin học Quốc tế, nhưng nội dung của nó lại đề cập tới một
cách tiếp cận mới trong kỹ thuật mã hóa và giải mã.
Burrows Wheeler đề xuất phương pháp mã hoá thông tin như sau: ví
dụ ta cần mã hoá từ BANANA, các bước tiến hành là:
Bước 1: Từ cần mã hoá được dịch chuyển vòng tròn và tạo thành một ma trận
L*L ký tự, trong đó L là độ dài của từ. Ta có:
BANANA
ANANAB
NANABA
ANABAN
NABANA
ABANAN
Bước 2: Sắp xếp lại các dòng của ma trận theo thứ tự từ điển:
ABANAN
ANABAN
ANANAB
BANANA
NABANA
NANABA
Bước 3: Trích xâu từ các ký tự cuối ở mỗi dòng, thông báo xâu này và cho
biết từ gốc là từ thứ mấy trong ma trận nhận được ở bước 2. Ta có
(NNBAAA,4)
Yêu cầu: Hãy viết chương trình mã hoá và giải mã.
Dữ liệu: Vào từ file CODE.INP, dòng đầu là một số 0 hoặc 1:
- Nếu là số 0 thì ta phải thực hiện thao tác mã hoá; và tiếp theo là một hoặc
nhiều nhóm dòng, mỗi dòng là một xâu ký tự cần mã hóa.
Dữ liệu ra bao gồm một hoặc nhiều nhóm hai dòng, dòng đầu là xâu ký tự đã
mã hoá, dòng tiếp theo là số nguyên dương cho biết vị trí từ gốc.
- Nếu là số 1 thì ta phải thực hiện thao tác giải mã; và tiếp theo là một hoặc
nhiều nhóm hai dòng, dòng đầu là xâu ký tự đã mã hoá, dòng tiếp theo là số
nguyên dương cho biết vị trí từ gốc.
Dữ liệu ra bao gồm một hoặc nhiều nhóm dòng, mỗi dòng là một xâu ký tự
đã giải mã.
Ví dụ 1:
CODE.INP
0
BANANA
COGUMELO
RONALDO
CODE.OUT
NNBAAA
4
OMOEULCG
1
NLAORDO
7
Ví dụ 2:
CODE.INP
1
NNBAAA
4
OMOEULCG
1
NLAORDO
7
CODE.OUT
BANANA
COGUMELO
RONALDO
Cho biết các xâu có thể có độ dài tới 1000 ký tự (L≤1000).
Dễ thấy rằng, với bài toán này chúng ta cần tìm ra một thuật giải và
cấu trúc dữ liệu phù hợp nếu không sẽ không thể giải được với dữ liệu đầu
bài cho khá lớn như vậy. Chẳng hạn, ta không thể khai báo một mảng
A:array[1 1000,1 1000] để chứa mảng ký tự do hạn chế về miền nhớ dành
cho biến tĩnh của ngôn ngữ lập trình Pascal. Chúng ta sẽ cùng phân tích để
tìm lời giải tối ưu cho bài toán.
Để có thể mã hoá, ta cần phải biết được các ký tự cuối của các từ (và
đương nhiên là phải biết vị trí từ gốc); vì các từ được dịch chuyển vòng tròn
nên nếu ta có thể có một "mảng vòng tròn" để lưu các xâu và một mảng chỉ
số để lưu vị trí bắt đầu của các xâu thì sẽ tiết kiệm đáng kể chi phí về miền
nhớ. Để tạo ra mảng vòng tròn như vậy, ta coi một mảng một chiều như là
mảng vòng tròn với lưu ý là phần tử ở cuối mảng được coi như đi trước phần
tử đầu tiên. Với ví dụ đầu bài ta có mảng chứa xâu và mảng chỉ số của các
xâu được chuyển vòng tròn là:
B A N A N A
1 2 3 4 5 6
Dựa vào hai mảng này ta dễ dàng xác định được các xâu sau khi dịch
chuyển với lưu ý là ký tự ở vị trí cuối cùng đi trước ký tự đầu tiên. Ví dụ: xâu
thứ 2 (bắt đầu từ vị trí thứ hai trong mảng chỉ số) là: ANANAB, xâu thứ 5
(bắt đầu từ vị trí thứ 5 trong mảng chỉ số) là: NABANA Việc sắp xếp các
xâu thực chất ta chỉ làm việc trên hai mảng này. Sau khi sắp xếp các xâu theo
thứ tự từ điển, mảng chỉ số có dạng:
6 4 2 1 5 3
Mảng này cho ta biết vị trí các xâu sau khi sắp xếp, xâu thứ nhất (xâu
BANANA) ở vị 4, xâu xâu thứ 2 (xâu ANANAB) ở vị trí 3…Từ đó, chúng ta
dễ dàng suy ra xâu mã hoá bằng cách trích ra các ký tự ở vị trí ngay trước của
xâu tương ứng. Cụ thể như sau: Lấy ký tự trước vị trí đầu tiên của xâu 6: do
xâu 6 bắt đầu từ vị trí 6 nên ta lấy ký tự ở vị trí 5 của mảng ban đầu, là ký tự
N. Lấy ký tự trước vị trí đầu tiên của xâu 4: do xâu 4 bắt đầu từ vị trí 4 nên ta
lấy ký tự ở vị trí 3 của mảng ban đầu, là ký tự N. Cứ như vậy, ta thu được
xâu kết quả là: NNBAAA.
Công việc còn lại là đi tìm thuật toán để giải mã hiệu quả. Để có thể
giải mã, ta cần biết xâu mã hoá s và vị trí n của xâu gốc. Thuật toán được đưa
ra như sau:
- Sắp xếp các từ trong s theo thứ tự từ điển để thu được xâu s', phải đảm bảo
giữ nguyên thứ tự của các từ giống nhau trong xâu s (nên dùng phương pháp
sắp xếp nổi bọt). Ví dụ với s=NNBAAA thì s'=AAABNN.
-Bắt đầu từ vị trí thứ n trong xâu s' và tiến hành duyệt lần lượt trên hai xâu để
thu được kết quả. Cụ thể: ký tự đầu tiên ở vị trí n=4 của s', là B; tiếp theo vì
B ở vị trí thứ 3 trong s nên ký tự thứ hai ở vị trí 3 trong s', là A; tiếp theo vì A
ở vị trí thứ 6 trong s nên ký tự thứ ba ở vị trí 6 trong s', là N…cứ như vậy ta
thu được kết quả là BANANA.
Lưu ý: nếu phương pháp sắp xếp không đảm bảo giữ nguyên thứ tự của các
từ trong xâu gốc thì có thể dẫn đến một số test bị sai. Chẳng hạn, bạn đọc có
thể tự kiểm tra khẳng định này để thấy rằng nếu giải mã xâu (NPMAAA, 6)
thì có thể sẽ cho PAMANA nhưng đáp án đúng phải là PANAMA. Trong ví
dụ minh hoạ ở trên, các ký tự A tại vị trí 4, 5 và 6 của xâu s=NNBAAA phải
tương ứng với các ký tự A tại các vị trí 1, 2 và 3.
Toàn văn chương trình như sau:
program bw;{chuong trinh ma hoa va giai ma Burrows Wheeler}
var i,j,n:integer;
fi,fo:text;
a,c:array[1 1000] of char;
b:array[1 1000] of integer;
function lessthan(i,j,n:integer):boolean;
{kiem tra xau a tai vi tri i co lon hon taij vi tri j ko
do dai xau la n=length(a), tinh theo modulo}
var ii:integer;
begin
ii:=0;
while (ii<n)and (a[i]=a[j]) do
begin
inc(ii);
if i=n then i:=1 else inc(i);
if j=n then j:=1 else inc(j);
end;
lessthan:= a[i]<a[j];
end;
function eq(i,j,n:integer):boolean;
{kiem tra xau a tai vi tri i co bang xau tai vi tri j ko
do dai xau la n=length(a), tinh theo modulo}
var ok:boolean;ii:integer;
begin
ok:=true;
ii:=0;
while (ii<n)and(ok) do
begin
inc(ii);
if a[i]<>a[j] then ok:=false;
if i=n then i:=1 else inc(i);
if j=n then j:=1 else inc(j);
end;
eq:=ok
end;
procedure code(n:integer;var m:integer);
var k,i,tg,j:integer;
begin
m:=0;
for i:=1 to n do b[i]:=i;
for i:=1 to n-1 do
for j:=1+i to n do
if lessthan(b[j],b[i],n)then
begin
tg:=b[i];
b[i]:=b[j];
b[j]:=tg;
end;
for j:=1 to n do
if m=0 then if eq(1,b[j],n)then m:=j;
end;
procedure decode(n,m:integer);
{N:vi tri cua xau ban dau trong day da xs
M:chieu dai xau}
var i,j,k:integer;tg:char;
begin c:=a;
for i:=1 to m do b[i]:=i;
for i:=1 to m-1 do
for j:=m downto i+1 do
if c[j]<c[j-1] then
begin
tg:=c[j-1];
c[J-1]:=c[j];
c[j]:=tg;
k:=b[j-1];
b[j-1]:=b[j];
b[j]:=k;
end;
i:=1;k:=n;write(fo,c[k]);
while i<m do
begin
k:=b[k];
write(fo,c[k]);
inc(i);
end;
writeln(fo);
end;
procedure solve;
var I,c,j:integer;
begin
assign(fi,'code.inp');
reset(fi);
assign(fo,'code.out');
rewrite(fo);
readln(fi,c);{c=0:ma hoa; c=1:giai ma}
if c=0 then
while not eof(fi) do
begin
i:=0;
while not eoln(fi) do
begin
inc(i);
read(fi,a[i]);
end;
readln(fi);
code(i,n);
for j:=1 to i do
if b[j]=1 then write(fo,a[i])
else write(fo,a[b[j]-1]);
writeln(fo);
writeln(fo,n);
end
else
while not eof(fi) do
begin
i:=0;
while not eoln(fi) do
begin
inc(i);
read(fi,a[i]);
end;
readln(fi);
readln(fi,n);
decode(n,i);
end;
close(fi);close(fo);
end;
begin
solve;
end.