a) Thuật toán tạo mã Huffman
5.5. Mô hình từ điển
5.5.1. Tổng quát
Kỹ thuật từ điển là kỹ thuật sử dụng phương pháp phân đoạn văn bản thành các đoạn nhỏ hơn sao cho nó đạt được độ dài nhất có thể được mà nó đã xuất hiện ở trong quá khứ.
Định nghĩa (phân đoạn văn bản)
Phân đoạn văn bản A là chia nó ra thành các đoạn nhỏ hơn. Mỗi đoạn được gọi là một phân đoạn.
Giả sử chúng ta có từ điển D thì tồn tại một ánh xạ f: D -> Ω trong đó Ω là tập các đoạn bit 0/1. Việc nén sẽ tối ưu khi chúng ta phân được đoạn văn bản có
độ dài lớn nhất mà sao cho khi đi tìm trong quá khứ thì chúng ta được đoạn 0/1 trong Ω là nhỏ nhất.
Trong kỹ thuật từ điển, tương tự như kỹ thuật thống kê, ta cũng có hai phương án sau:
+ Mã tĩnh (từ điển tĩnh) + Mã động (từ điển động) 5.5.2. Từ điển tĩnh
Mã có từ điển cố định được gọi là mã tĩnh hay nói cách khác là từ điển tĩnh.
Nhược điểm của từ điển tĩnh:
+ Kích thước của từ điển tĩnh không thể mở rộng ra, để cập nhật các thông tin mới so với các loại dữ liệu khác.
+ Quá trình nghiên cứu thiết kế từ điển đòi hỏi nhiều công sức. + Các thuật toán sử dụng từ điển tĩnh chạy tương đối chậm. + Tốn không gian lưu trữ dữ liệu.
Tuy nhiên bên cạnh những nhược điểm này thì từ điển tĩnh cũng có ưu điểm là: Thiết kế các thuật toán áp dụng cho việc tìm kiếm trên từ điển tĩnh đơn giản tốn ít thời gian.
Do những đặc tính hạn chế của từ điển tĩnh, kết hợp với những đặc điểm nổi bật của thuật toán nén là "sự cứng nhắc" đã làm cho người dùng chương trình để tiết kiệm bộ nhớ nhàm chán không thích sử dụng các chương trình nén nữa. Vậy phải có cách để khắc phục những hạn chế đó và làm cho chương trình nén dữ liệu trở nên mềm dẻo hơn. Chính vì vậy những thuật toán nén chỉ đạt hiệu quả cao nếu chúng ta xử lý tốt "từ điển" được sử dụng trong các thuật toán nói chung.
5.5.3. Từ điển động
Để khắc phục những hạn chế của từ điển tĩnh, biện pháp được xét đến là: Chúng ta không thiết kế một từ điển sẵn mà " từ điển" đó được xây dựng trong quá trình chạy chương trình. Một từ điển như vậy người ta gọi là từ điển động.
Đặc điểm chính của từ điển động:
+ Kích thước của từ điển có thể thay đổi tuỳ theo kích thước của tập tin. + Không tốn thời gian lưu trữ từ điển.
+ Thời gian thực hiên quá trình nén nhanh. + Từ điển không phụ thuộc vào kiểu dữ liệu. + Thời gian thực hiện quá trình giải nén chậm.
5.5.4. LZ77 <Sliding Window>
Ý tưởng là: sử dụng phần đã xét trước đó như là một từ điển. Mã hóa duy trì một cửa số gắn với luồng vào và dịch đầu vào trong cửa sổ từ phải sang trái theo đúng cách
các chuỗi ký tự được mã hóa. Phương thức này dựa trên một cửa sổ trượt. Cửa sổ dưới được chia làm hai phần:
- Phần bên trái gọi là bộ đệm tìm kiếm – search buffer. Đây là từ điển hiện hành và nó bao gồm các ký hiệu mới được duyệt ở đầu vào và được mã hóa.
- Phần bên phải là bộ đệm nhìn trước - look-ahead buffer, chứa văn bản chưa được mã hóa.
Thực tế, bộ đệm search dài hàng nghìn byte trong khi bộ đệm look-ahead chỉ dài khoảng 10 byte. Thanh dọc ở giữa t và e bên dưới biểu diễn dòng phân chia hiện hành giữa hai bộ đệm. Bởi vậy ta thấy văn bản “sir sid eastman easily t” đã được nén, trong khi văn bản “eases sea sick seals” cần được nén.
--> văn bản đã mã hóa…
search buffer look-ahead buffer
sir sid eastman easily t eases sea sick seals
Mã hóa quét quay lui bộ đệm search (từ phải sang trái) để tìm ký tự trùng với ký tự e đầu tiên trong bộ đệm look-ahead. Nó tìm thấy một ký tự e trong từ easily. Từ e này nằm tại offset 8 tính từ phần cuối của bộ đệm search. Sau đó, mã hóa tìm ký tự e tiếp theo có thể có. Ba ký tự eas trùng nhau trong trường hợp này, vì vậy độ dài của chuỗi hợp là 3. Mã hóa tiếp tục quay lui quét, cố gắng tìm thấy các chuỗi hợp dài hơn. Trong trường hợp này chỉ có ký tự e trong từ eastman, tại offset 16 và có độ dài như vậy. Mã hóa chọn chuỗi hợp dài nhất hoặc nếu chúng có độ dài như nhau, chuỗi hợp cuối cùng được tìm thấy và chuẩn bị xâu (16,3, “e”).
Việc chọn chuỗi hợp cuối cùng hơn là chuỗi hợp đầu tiên làm đơn giản hóa bộ mã hóa, khi nó chỉ phải theo dõi chuỗi hợp cuối cùng được tìm thấy. Thú vị để ghi nhớ việc chọn lựa chuỗi hợp đầu tiên trong khi làm chương trình có phần phức tạp hơn cũng là một lợi thế. Nó chọn lựa offset nhỏ nhất. Dường như điều này không lợi thế khi một xâu nên có chỗ cho offset có thể lớn nhất. Tuy nhiên, đi theo LZ77 với Huffman, hoặc mã thống kê khác của các xâu mà các offset nhỏ được gán các mã ngắn hơn. Phương thức này, được đề xướng bởi Bernd Herd, gọi là LZH.
Một câu hỏi mà có thể xảy ra tại điểm này là việc giải mã bằng cách nào trong khi mã hoá chọn chuỗi hợp đầu tiên hoặc cuối cùng? Câu trả lời dễ hiểu là việc giải mã không được biết nhưng nó không cần để biết. Giải mã đơn giản là đọc các xâu và sử dụng mỗi offset để tìm chuỗi văn bản mà không cần biết chuỗi là chuỗi hợp đầu tiên hay cuối cùng.
Cấu tạo một xâu LZ77: gồm có 3 phần: + offset: vị trí của xâu trong bộ đệm search. + length: độ dài xâu.
Trong ví dụ trên, xâu được ghi là (16,3, “e”) (e – ký tự e thứ hai trong từ teases). Xâu này được ghi trên luồng ra và cửa sổ được dịch sang phải (hay nói cách khác, luồng vào được dịch sang trái) bốn vị trí: ba vị trí cho chuỗi hợp và một vị trí cho ký tự kế tiếp.
Nếu sự tìm kiếm quay lui không tìm được ký tự trùng, thì một xâu LZ77 có trường offset và length là bằng 0 và trường thứ ba là ký tự không được hợp. Đây cũng là nguyên nhân một xâu phải có ba thành phần. Các xâu với trường offset và length bằng 0 phổ biến tại phần bắt đầu công việc nén, khi mà bộ đệm search là rỗng hoặc hầu như rỗng. Ví dụ: => (0,0, “s”) => (0,0, “i”) => (0,0, “r”) => (0,0, “ ”) =>(4,2,“d”) a) Thuật toán nén
Giả sử cửa sổ có: bộ đệm search có kích thước N byte và bộ đệm look-ahead có kích thước là F byte.
Bước 1: Đọc 1 ký tự đầu tiên giữ nguyên.
Bước 2: Cho cửa sổ dịch sang phải. Mỗi lần đếm số ký tự trùng nhau liên tiếp kể từ đầu của bộ đệm look-ahead về bộ đệm search và ghi nhớ con số tương ứng với vị trí offset của nó. Tìm số lớn nhất trong tất cả các con số ấy.
Bước 3: Ghi ra mã [i, j, w] trong đó:
- i: khoảng cách từ vị trí hiện tại đến vị trí tương ứng với số lớn nhất vừa tìm được.
- j: số các ký tự trùng nhau hay chính là số lớn nhất vừa tìm được. - w: ký tự ở vị trí thứ j + 1 trong bộ đệm look-ahead.
Bước 4: Lặp lại bước 2 cho đến khi hết văn bản.
Bước 5: Kết thúc
b) Ví dụ
Cho văn bản sau: “sire sire eastman easily” với N = 24, F =16. (_ : thay cho dấu cách) Quá trình nén
STT Search buffer vị trí hiện tại Look-ahead buffer Ghi ra
1 sire sire eastma (0,0, “s”)
2 s 2 ire sire eastman (0,0, “i”)
3 si 3 re sire eastman_ (0,0, “r”) 4 sir 4 e sire eastman e (0,0, “e”) 5 sire 5 _sire eastman ea (0,0, “_”)
sir sid eastman s ir sid eastman e si r sid eastman ea sir sid eastman eas sir sid eastman easi
6 sire_ 6 sire eastman eas (5,5, “e”) 7 sire sire_ 11 eastman easily (2,1, “a”) 8 sire sire ea 13 stman easily (7,1, “t”) 9 sire sire east 15 man easily (0, 0, “m”) 10 sire sire eastm 16 An easily (4, 1, “n”) 11 sire sire eastman 18 _easily (8,4, “i”) 12 sire sire eastman easi 23 ly (0,0, “l”) 13 sire sire eastman easil 24 y (0,0, “y”)
Quá trình nén văn bản “sire sire eastman easily” dùng LZ77
Vậy bản nén là:
(0,0,“s”)(0,0,“i”)(0,0,“r”)(0,0,“e”)(0,0,“_”)(5,5,”e”)(2,1,”a”)(7,1,”t”)(0,0,”m”) (4,1,“n”)(8,4,“i”)(0,0, “l”)(0,0,“y”)
c) Thuật toán giải nén
Bước 1: Đọc bộ đệm look-ahead, 1 ký tự đầu tiên của bản mã ghi ra bản giải mã.
Bước 2: Đọc một bộ mã [i, j, w]. Lùi về đoạn văn bản đã giải mã i vị trí và copy j ký tự từ vị trí thứ i đó để ghi ra bản giải mã, sau đó ghi tiếp w ra.
Bước 3: Nếu còn bộ mã thì quay lại bước 2. Nếu không còn thì sang bước 4
Bước 4: Kết thúc
d) Ví dụ:
STT Bộ mã đọc vào Ghi ra Bản giải mã
1 (0,0,“s”) s s
2 (0,0,“i”) i si
3 (0,0,“r”) r sir
4 (0,0,“e”) e sire
5 (0,0,“_”) _ sire_
6 (5,5,”e”) sire_ sire sire_
7 (2,1,”a”) ea sire sire ea
8 (7,1,”t”) st sire sire east
9 (0,0,”m”) m sire sire eastm
10 (4,1,“n”) an sire sire eastman 11 (8,4,“i”) _easi sire sire eastman easi 12 (0,0, “l”) l sire sire eastman easil 13 (0,0,“y”) y sire sire eastman easily
Quá trình giải nén văn bản dùng LZ77
Văn bản đã giải nén: “sire sire eastman easily”.
5.5.5. LZ78
LZ78 được phát minh và sử dụng trong những năm 1978. LZ78 sử dụng một từ điển gồm các chuỗi đã xem trước đó. Từ điển này được khởi tạo là rỗng và kích thước của nó chỉ bị giới hạn bởi số lượng bộ nhớ sẵn có.
Một xâu LZ78 được tạo ra gồm hai trường có dạng (W,C): - W: chỉ số của chuỗi ký hiệu trong từ điển.
- C: mã ký hiệu kế tiếp.
Xâu LZ78 không chứa kích thước của chuỗi khi nó đã được bao hàm trong từ điển. Mỗi xâu tương ứng với một chuỗi các ký hiệu vào và chuỗi đó được thêm vào từ điển sau khi xâu được ghi vào luồng nén. Không có xâu nào bị xóa khỏi từ điển, đây là cả lợi thế (các chuỗi tương lai có thể được nén bởi chính những chuỗi đã được xét từ lâu) và bất lợi (từ điển phát triển nhanh và đầy bộ nhớ sẵn có) so với LZ77.
Từ điển được khởi tạo với một chuỗi rỗng bắt đầu tại vị trí 0. Khi các ký tự được nhập vào và được mã hóa thì các chuỗi được thêm vào tại vị trí 1, 2, …
Khi một ký tự tiếp được đọc từ luồng vào, từ điển tìm kiếm một mục với chuỗi đơn x. Nếu không tìm thấy, x được thêm vào từ điển tại vị trí kế tiếp, và xâu (0, x) được ghi ra. Xâu này cho biết chuỗi “null x” ( chuỗi rỗng kết hợp với x). Nếu một mục với x được tìm thấy (ví dụ tại vị trí 37), ký tự kế tiếp y được đọc và từ điển tìm kiếm mục chứa chuỗi hai ký tự xy. Nếu không có trong từ điển, thì chuỗi xy được thêm vào tại vị trí tiếp theo trong từ điển, và xâu (37, y) được ghi ra. Xâu này cho biết chuỗi xy với 37 là vị trí của chuỗi x trong từ điển. Quá trình tiếp tục đến khi gặp ký tự kết thúc luồng vào.
Từ điển được xây dựng trong quá trình nén và giải nén là hoàn toàn động, do đó không cần lưu từ điển cùng với file nén. Điều này tạo kích thước file nén giảm đáng kể.
a, Thuật toán
Input: Tệp tin nguồn f
Output: Tệp tin nén fn
Bước 1: + Khởi tạo từ điển = xâu rỗng có chỉ số 0. + Đặt xâu trung gian P = rỗng.
Bước 2: Đọc ký tự tiếp theo trong tệp văn bản nguồn f gán vào C.
Bước 3: Kiểm tra: Nếu xâu (P + C) đã có trong từ điển thì + Đặt P = P + C
+ Quay lại bước 2; Ngược lại thì:
+ Cập nhật P + C vào trong từ điển; + Gán W = P chỉ số trong từ điển; + Ghi cặp (W,C) ra tệp tin nén fn. + Đặt lại P = rỗng
Bước 4: Kiểm tra: Nếu còn ký tự trong văn bản thì quay lại bước 2. Ngược lại thì:
+ Ghi ra cặp (W, C) với C là rỗng ra tệp nén fn; + Sang bước 5;
Bước 5:Kết thúc
b, Ví dụ:
Nén văn bản “sir sid eastman”
STT P C Từ điển W Ghi ra 1 s 1 – s 0 (0, “s”) 2 i 2 – i 0 (0, “i”) 3 r 3 – r 0 (0, “r”) 4 _ 4 - _ 0 (0, “_”) 5 s 6 s i 5 – si 1 (1, “i”) 7 d 6 – d 0 (0, “d”) 8 _ 9 _ e 7 - _e 4 (4, “e”) 10 a 8 – a 0 (0, “a”) 11 s 12 s t 9 - st 1 (1, “t”) 13 m 10 - m 0 (0, “m”) 14 a 15 a n 11 - an 8 (8, “n”) Vậy bản nén: (0,“s”)(0,“i”)(0,“r”)(0,“_”)(1,“i”)(0,“d”)(4,“e”)(0,“a”)(1,“t”)(0,“m”) (8,“n”)
c) Thuật toán giải nén
Với mỗi cặp (W,C) ta ký hiệu .
W nội dung của đoạn copy thứ W trong từ điển. Nếu W=0 thì W là rỗng.
Input: Tệp tin nguồn nén fn
Output: Tệp tin giải nén fg;
Bước 1: + Mở tệp tin nguồn fn (để đọc) + Mở tệp tin giải nén fg (để ghi)
Bước 2: Khởi tạo từ điển = xâu rỗng có chỉ số 0
Bước 3: Đọc một cặp (W,C).
Bước 4: + Ghi xâu (W +C) ra file giải nén fg. + Cập nhật xâu này vào từ điển
Bước 5: Kiểm tra: nếu còn ký tự trong file mã fn thì quay lại bước 3. Ngược lại sang bước 6;
Bước 6: Kết thúc .
b, Ví dụ: Giải nén
STT Cặp mã đọc vào Ghi ra Từ điển
1 (0,“s”) s 1 – s
2 (0,“i”) i 2 – i
4 (0,“_”) _ 4 - _ 5 (1,“i”) si 5 – si 6 (0,“d”) d 6 – d 7 (4,“e”) _e 7 - _e 8 (0,“a”) a 8 – a 9 (1,“t”) st 9 – st 10 (0,“m”) m 10 – m 11 (8,“n”) an 11 – an
BÀI 6: HỆ MÃ HÓA ĐỐI XỨNG 6.1. Các khái niệm
Mã khối (block) xử lý bản tin theo từng khối, lần lượt mỗi khối được mã hoặc giải mã. Có thể xem giống như phép thế với các ký tự lớn – mỗi khối gồm 64 bít hoặc nhiều hơn.
Mã dòng xử lý bản tin theo từng bít hoặc bite, lần lượt mỗi bít hoặc bite được mã hoá hoặc giải mã. Chẳng hạn như mã khoá tự động Vigenere.
Rất nhiều mã hiện nay là mã khối. Chúng có khả năng ứng dụng rộng rãi hơn. Rất nhiều ứng dụng mã đối xứng trên mạng sử dụng mã khối.
Các nguyên lý mã khối
Hầu hết các mã khối đối xứng dựa trên cấu trúc mã Fiestel, do nhà bac học Fiestel đề xuất năm 1973. Đây là điều cần thiết, vì cần phải có khả năng giải mã các bản mã một cách có hiệu quả.
Mã khối được coi giống như phép thế cực lớn. Cần bảng có 264 đầu vào cho mã khối 64 bít, bảng như vậy là rất lớn. Do đó có thể thay thế bằng cách tạo các khối nhỏ hơn. Sử dụng ý tưởng dùng mã tích. Ở đây sẽ kết hợp giữa mã thay thế và mã hoán vị, đồng thời sử dụng nhiều vòng lặp như vậy.
Hình 6.1 - Hộp S có thuận nghịch
Mạng S-P dựa trên hai thao tác mã cơ bản mà ta đã biết: phép thế (S-box) và hoán vị (P-box). Chúng sẽ tạo nên độ rối loạn và khuếch tán của bản tin.
Hình 6.2 - Hộp P có thuận nghịch
Rối loạn và khuếch tán (Diffusion and Confusion)
Một tính chất quan trọng của mã tốt là mã cần phải che dấu hoàn toàn các tính chất thống kê của bản tin gốc. Như ta đã thấy mã bộ đệm một lần có thể làm được điều đó, do tính ngẫu nhiên của khoá đệm và độ dài bằng bản tin của nó.
Shannon nghiên cứu và đề xuất phương pháp thực tế hơn là kết hợp các thành phần khác nhau của bản rõ để xử lý qua nhiều lần và nhận được bản mã.
Khuếch tán (Diffusion Confusion): là làm tan biến cấu trúc thống kê của bản rõ trên bản mã. Điều đó đạt được nếu mỗi bit của bản rõ tác động đến giá trị của rất nhiều bit trên bản mã hay mỗi bit của bản mã chịu tác động của nhiều bit bản rõ.
Rối loạn (Confusion): là làm cho quan hệ giữa bản mã và khoá càng phức tạp càng tốt. Bản mã có tính rối loạn cao sẽ làm cho việc tìm mò khoá trở nên rất khó khăn, ngay cả khi kẻ tấn công có các đặc trưng thống kê của bản mã và biết cách khoá tác động đến bản mã.
6.2. Mã Fiestel
Horst Fiestel sáng tạo nên mã Fiestel dựa trên mã tích nghịch đảo được, tức là kết hợp mã thế với mã hoán vị và qui trình giải mã là giống với mã hoá, chỉ cần thay đổi vai trò khối bản mã với khối bản rõ và thứ tự các khoá con được dùng. Từ khoá