Trong luận văn này, tôi sẽ tập trung phân tích việc tổ chức dữ liệu cho lớp thuật toán chia để trị và cách đánh giá độ phức tạp đối với các thuật toán chia để trị.Với mục tiêu chính là á
Trang 1Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
ĐẠI HỌC THÁI NGUYÊN
TRƯỜNG ĐẠI HỌC CÔNG NGHỆ THÔNG TIN&TRUYỀN THÔNG
Trang 2Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
LỜI CAM ĐOAN
Tôi xin cam đoan đây là công trình nghiên cứu của bản thân, được xuất phát từ yêu cầu phát sinh trong công việc để hình thành hướng nghiên cứu Các số liệu có nguồn gốc
rõ ràng tuân thủ đúng nguyên tắc và kết quả trình bày trong luận văn được thu thập được trong quá trình nghiên cứu là trung thực chưa từng được ai công bố trước đây
Trang 3Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
LỜI CẢM ƠN
Đầu tiên, em xin gửi lời cảm ơn sâu sắc nhất đến cán bộ hướng dẫn khoa học, thầy giáo, PGS.TSKH Nguyễn Xuân Huy, người đã truyền cho em nguồn cảm hứng nghiên cứu khoa học, người đã định hướng cho em đến với lĩnh vực nghiên cứu này
Em xin bày tỏ lời cảm ơn tới các thầy giáo, cô giáo đã giảng dạy em trong suốt hai năm học qua Em cũng muốn gửi lời cảm ơn tới những thành viên lớp đã có những góp ý chuyên môn cũng như sự động viên về tinh thần rất đáng trân trọng
Cuối cùng, em xin gửi lời cảm ơn sâu sắc tới tất cả người thân trong gia đình và những bạn bè em với những động viên dành cho em trong công việc và trong cuộc sống
Học viên thực hiện luận văn
Đỗ Tuấn Anh
Trang 4Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
MỤC LỤC
Lời cam đoan
Lời cảm ơn
Mục lục iii
Danh mục các bảng v
Danh mục các hình vẽ v
MỞ ĐẦU 1
CHƯƠNG 1 CÁC CHIẾN LƯỢC THIẾT KẾ THUẬT TOÁN 2
1.1 Các bước cơ bản khi giải bài toán trên máy tính 2
1.2 Phân tích thời gian thực hiện thuật toán 6
1.2.1 Độ phức tạp thuật toán 6
1.2.2 Xác định độ phức tạp của thuật toán 9
1.2.3 Ký hiệu Big-O và biểu diễn thời gian chạy của thuật toán 10
1.2.4 Độ phức tạp thuật toán với tình trạng dữ liệu vào 13
1.2.5 Chi phí thực hiện thuật toán 13
CHƯƠNG 2 TỔ CHỨC DỮ LIỆU CHO LỚP THUẬT TOÁN CHIA ĐỂ TRỊ 14
2.1 Chiến lược chia để trị 14
2.2 Tổ chức dữ liệu cho lớp thuật toán chia để trị 15
2.3 Định lý tổng quát tính độ phức tạp các thuật toán chia để trị 16
2.4 Một số lớp bài toán điển hình 17
2.4.1 Lớp bài toán tìm kiếm 18
2.4.1.1 Thuật toán tìm kiếm nhị phân 18
2.4.1.2 Bài toán tìm Max và min 20
2.4.2 Lớp bài toán sắp xếp 22
2.4.2.1 Thuật toán sắp xếp trộn (Merge Sort) 22
2.4.2.2 Thuật toán sắp xếp nhanh (Quick Sort) 24
2.4.3 Lớp bài toán tối ưu 27
2.4.3.1 Bài toán dãy con dài nhất 27
2.3.3.2 Bài toán tháp Hà Nội 29
2.3.3.5 Bài toán xếp lịch thi đấu 30
CHƯƠNG 3 ỨNG DỤNG THUẬT TOÁN CHIA ĐỂ TRỊ GIẢI BÀI TOÁN NHÂN HAI SỐ NGUYÊN LỚN 32
3.1 Mô tả bài toán 32
3.2 Thuật toán nhân tự nhiên 32
3.3 Thuật toán nhân cơ bản 33
Trang 5Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
3.4 Thuật toán nhân Karatsuba-Ofman 35
3.5 Thuật toán nhân dựa trên biến đổi Fourier nhanh 37
3.6 Thuật toán nhân chia để trị 40
3.6.1 Ý tưởng chung 40
3.6.2 Phân tích thuật toán 41
3.6.3 Mô hình thuật toán chia để trị cho bài toán nhân hai số nguyên lớn 44
3.6.4 So sánh độ phức tạp giữa các thuật toán 46
3.7 Tổ chức dữ liệu cho thuật toán chia để trị 46
3.7.1 Biểu diễn dưới dạng bit 46
3.7.2 Biểu diễn dùng mảng và xâu 47
3.8 Thực nghiệm và đánh giá 51
3.8.1 Cài đặt trên C 51
3.8.2 Cài đặt trên C# 59
KẾT LUẬN VÀ HƯỚNG PHÁT TRIỂN 64
TÀI LIỆU THAM KHẢO 65
Trang 6Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
DANH MỤC CÁC BẢNG
Bảng 1.1 Các lớp độ phức tạp tính toán 11
Bảng 1.2 Thời gian chạy của các lớp thuật toán 12
Bảng 2.1 Độ phức tạp của thuật toán tìm kiếm nhị phân 20
Bảng 2.2 Độ phức tạp của thuật toán sắp xếp nhanh 26
Bảng 3.1 So sánh độ phức tạp tính toán của các thuật toán nhân 46
DANH MỤC CÁC HÌNH Hình 2.1 Thuật toán chia để trị 14
Hình 2.2 Tổ chức dữ liệu cho lớp bài toán chia để trị 15
Hình 2.3 Ví dụ thuật toán sắp xếp trộn 23
Hình 3.1 Thuật toán nhân Brute-force 33
Hình 3.2 Thuật toán nhân chuẩn 34
Hình 3.3 Thuật toán nhân SRMA 35
Hình 3.4 Thuật toán nhân Karatsuba-Ofman 37
Hình 3.5 Thuật toán nhân FFT 39
Hình 3.6 Thuật toán nhân chia để trị 45
Hình 3.7 Phép nhân chia để trị tổ chức dưới dạng bit 46
Hình 3.8 Thuật toán nhân chia để trị biểu diễn bit 47 Hình 3.9 Ví dụ về phép chia Ấn Độ
Trang 7Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
MỞ ĐẦU
.Ngày nay phương pháp này vẫn còn được áp dụng trong nhiều lĩnh vực của đời sống Đặc biệt, phương pháp này rất hiệu quả khi thiết kế thuật toán giải các bài toán lớn, phức tạp Với bài toán đầu vào rất lớn ta chia thành những phần nhỏ hơn và tìm lời giải cho các bài toán nhỏ riêng biệt này, rồi sau đó tổng hợp các nghiệm riêng rẽ thành nghiệm bài toán toàn cục
Trong luận văn này, tôi sẽ tập trung phân tích việc tổ chức dữ liệu cho lớp thuật toán chia để trị và cách đánh giá độ phức tạp đối với các thuật toán chia để trị.Với mục tiêu chính là áp dụng thiết kế thuật toán chia để trị để giải quyết bài toán nhân hai số nguyên lớn, luận văn được trình bày trong 3 chương với bố cục như sau:
Chương 1: Các chiến lược thiết kế thuật toán Giới thiệu tổng quan về các bước
giải bài toán trên máy tính và phân tích đánh giá thời gian thực hiện thuật toán cùng các
chiến lược thiết kế thuật toán cơ bản
Chương 2: Tổ chức dữ liệu cho lớp thuật toán chia để trị.Trình bày ý tưởng, cơ sở
khoa học của thuật toán chia để trị và cách thức tổ chức dữ liệu cho thuật toán chia để trị
với các bài toán kinh điển
Chương 3: Ứng dụng thuật toán chia để trị giải bài toán nhân hai số nguyên lớn
Tập trung phân tích các cách tiếp cận giải bài toán nhân hai số nguyên lớn Từ đó đề xuất thuật toán dựa trên tư tưởng chia để trị để giải quyết và thực nghiệm so sánh với các cách tiếp cận trước đó
Cuối cùng là kết luận và hướng phát triển:Tóm tắtnhững kết quả đạt được, những
hạn chế và nêu lên các hướng phát triển trong tương lai
Trang 8Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
CHƯƠNG 1 CÁC CHIẾN LƯỢC THIẾT KẾ THUẬT TOÁN
1.1 Các bước cơ bản khi giải bài toán trên máy tính
Một thuật toán một thủ tục tính toán được định nghĩa chính xác, mà lấy một giá trị hoặc một tập các giá trị, được gọi là đầu vào hay dữ liệu vào và tạo ra một giá trị, hoặc một tập các giá trị, và gọi là đầu ra Miêu tả một vấn đề thường được xác định nói
chungqua quan hệ đầu vào/đầu ra Một thuật toán là một dãy bước xác định để chuyển đổi
dữ liệu đầu vào thành dữ liệu đầu ra Chúng ta có thể xem một thuật toán như một công cụ
để giải quyết một vấn đề tính toán Việc trình bày rõ ràng một vấn đề nói chung hình
thành mối quan hệ mong muốn đầu vào/đầu ra Một thuật toán mô tả chính xác một thủ tục tính toán để đạt được mối liên hệ giữa dữ liệu đầu vào và dữ liệu đầu ra
1.1.1 Xác định bài toán
Việc xác định bài toán tức là phải xác định xem ta phải giải quyết vấn đề gì? với giả thiết nào đã cho và lời giải cần phải đạt những yêu cầu nào Khác với bài toán thuần tuý toán học chỉ cần xác định rõ giả thiết và kết luận chứ không cần xác định yêu cầu về lời giải, đôi khi những bài toán tin học ứng dụng trong thực tế chỉ cần tìm lời giải tốt tới mức nào đó, thậm chí là tồi ở mức chấp nhận được Bởi lời giải tốt nhất đòi hỏi quá nhiều thời gian và chi phí
Input → Process → Output
(Dữ liệu vào →Xử lý →Kết quả ra)
Ví dụ: Khi cài đặt các hàm số phức tạp trên máy tính Nếu tính bằng cách khai triển chuỗi vô hạn thì độ chính xác cao hơn nhưng thời gian chậm hơn hàng tỉ lần so với phương pháp xấp xỉ Trên thực tế việc tính toán luôn luôn cho phép chấp nhận một sai số nào đó nên các hàm số trong máy tính đều được tính bằng phương pháp xấp xỉ của giải tích số
Xác định đúng yêu cầu bài toán là rất quan trọng bởi nó ảnh hưởng tới cách thức giải quyết và chất lượng của lời giải Một bài toán thực tế thường cho bởi những thông tin khá
mơ hồ và hình thức, ta phải phát biểu lại một cách chính xác và chặt chẽ để hiểu đúng bài toán Trên thực tế, ta nên xét một vài trường hợp cụ thể để thông qua đó hiểu được bài toán rõ hơn và thấy được các thao tác cần phải tiến hành Đối với những bài toán đơn giản, đôi khi chỉ cần qua ví dụ là ta đã có thể đưa về một bài toán quen thuộc để giải
1.1.2 Tìm cấu trúc dữ liệu biểu diễn bài toán
Trang 9Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
Khi giải một bài toán, ta cần phải định nghĩa tập hợp dữ liệu để biểu diễn tình trạng
cụ thể Việc lựa chọn này tuỳ thuộc vào vấn đề cần giải quyết và những thao tác sẽ tiến hành trên dữ liệu vào Có những thuật toán chỉ thích ứng với một cách tổ chức dữ liệu nhất định, đối với những cách tổ chức dữ liệu khác thì sẽ kém hiệu quả hoặc không thể thực hiện được Chính vì vậy nên bước xây dựng cấu trúc dữ liệu không thể tách rời bước tìm kiếm thuật toán giải quyết vấn đề
Các tiêu chuẩn khi lựa chọn cấu trúc dữ liệu:
- Phải biểu diễn được đầy đủ các thông tin nhập và xuất của bài toán
- Phù hợp với các thao tác của thuật toán mà ta lựa chọn để giải quyết bài toán
- Phải cài đặt được trên máy tính với ngôn ngữlập trình đang sửdụng
Đối với một sốbài toán, trước khi tổchức dữliệu ta phải viết một đoạn chương trình nhỏ để khảosátxem dữliệu cần lưu trữlớn tới mức độnào
1.1.3 Xây dựng thuật toán
Thuật toán là một hệ thống chặt chẽ và rõ ràng các quy tắc nhằm xác định một dãy thao tác trên cấu trúc dữ liệu sao cho: Với một bộ dữ liệu vào, sau một số hữu hạn bước thực hiện các thao tác đã chỉ ra, ta đạt được mục tiêu đã định.Các đặc trưng của thuật toán:
1 Tính đơn định: Ở mỗi bước của thuật toán, các thao tác phải hết sức rõ ràng,
không gây nên sự nhập nhằng, lộn xộn, tuỳ tiện, đa nghĩa Thực hiện đúng các bước của thuật toán thì với một dữ liệu vào, chỉ cho duy nhất một kết quả ra
2 Tính dừng: Thuật toán không được rơi vào quá trình vô hạn, phải dừng lại và
cho kết quả sau một số hữu hạn bước
3 Tính đúng: Sau khi thực hiện tất cả các bước của thuật toán theo đúng quá trình
đã định, ta phải được kết quả mong muốn với mọi bộ dữ liệu đầu vào Kết quả
đó được kiểm chứng bằng yêu cầu bài toán
4 Tính phổ dụng: Thuật toán phải dễ sửa đổi để thích ứng được với bất kỳ bài toán
nào trong một lớp các bài toán và có thể làm việc trên các dữ liệu khác nhau
5 Tính khả thi:Đối với một bài toán, có thể có nhiều thuật toán nhưng chúng phải
cho cùng một output đối với một input Tuy nhiên chúng có thể khác nhau về hiệu quả Hiệu quả thời gian là tốc độ xử lý là nhanh hay chậm Ta có thể đánh giá căn cứ vào số bước thực hiện Hiệu quả không gian là không gian lưu trữ theo số các đối tượng dùng để ghi nhớ các kết quả (kể cả kết quả trung gian) Trong Tin học có cả một ngành chuyên đánh giá độ phức tạp của giải thuật, chủ yếu
là đánh giá về hiệu quả thời gian Thực tế sử dụng cho thấy thách thức về không gian lưu trữ có thể giải quyết dễ hơn thách thức về thời gian thực hiện
Trang 10Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
Thông thường, chúng ta không nên cụ thể hoá ngay toàn bộ chương trình mà nên tiến
hành theo phương pháp tinh chế từng bước (Stepwiserefinement):
- Ban đầu, chương trình được thể hiện bằng ngôn ngữ tự nhiên, thể hiện thuật toán với các bước tổng thể, mỗi bước nêu lên một công việc phải thực hiện
- Một công việc đơn giản hoặc là một đoạn chương trình đã được học thuộc thì
ta tiến hành viết mã lệnh ngay bằng ngôn ngữ lập trình
- Một công việc phức tạp thì ta lại chia ra thành những công việc nhỏ hơn để lại tiếp tục với những công việc nhỏ hơn đó
Trong quá trình tinh chế từng bước, ta phải đưa ra những biểu diễn dữ liệu Như vậy cùng với sự tinh chế các công việc, dữ liệu cũng được tinh chế dần, có cấu trúc hơn, thể hiện rõ hơn mối liên hệ giữa các dữ liệu.Phương pháp tinh chếtừng bước là một thểhiện của tưduy giải quyết vấn đềtừtrên xuống, giúpcho người lập trình có được một định hướng thểhiện trong phong cách viết chương trình Tránhviệc mò mẫm, xoá đi viết lại nhiều lần, biến chương trình thành tờgiấy nháp
1.1.5 Chạy và kiểm thử
1.1.5.1 Chạy thử và tìm lỗi
Chương trình là do con người viết ra, mà đã là con người thì ai cũng có thể nhầm lẫn Một chương trình viết xong chưa chắc đã chạy được ngay trên máy tính để cho ra kết quả mong muốn Kỹ năng tìm lỗi, sửa lỗi, điều chỉnh lại chương trình cũng là một kỹ năng quan trọng của người lập trình Kỹ năng này chỉ có được bằng kinh nghiệm tìm và sửa chữa lỗi của chính mình.Có ba loại lỗi:
- Lỗi cú pháp: Lỗi này hay gặp nhất nhưng lại dễ sửa nhất, chỉ cần nắm vững
ngôn ngữ lập trình là đủ Một người được coi là không biết lập trình nếu không biết sửa lỗi cú pháp
- Lỗi cài đặt: Việc cài đặt thể hiện không đúng thuật toán đã định, đối với lỗi này
thì phải xem lại tổng thể chương trình, kết hợp với các chức năng gỡ rối để sửa lại cho đúng
Trang 11Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
- Lỗi thuật toán: Lỗi này ít gặp nhất nhưng nguy hiểm nhất, nếu nhẹ thì phải điều
chỉnh lại thuật toán, nếu nặng thì có khi phải loại bỏ hoàn toàn thuật toán sai và làm lại từ đầu
1.1.5.2 Xây dựng các bộ dữ liệu kiểm tra
Có nhiều chương trình rất khó kiểm tra tính đúng đắn Nhất là khi ta không biết kết quả đúng là thế nào? Vì vậy nếu như chương trình vẫn chạy ra kết quả thì việc tìm lỗi rất khó khăn Khi đó ta nên làm các bộ dữ liệu test để thử chương trình của mình Kinh nghiệm khi xây dựng các bộ dữ liệu test là:
- Bắt đầu với một bộ test nhỏ, đơn giản, làm bằng tay cũng có được đáp số để so sánh với kết quả chương trình chạy ra
- Tiếp theo vẫn là các bộ test nhỏ, nhưng chứa các giá trị đặc biệt hoặc tầm thường Kinh nghiệm cho thấy đây là những test dễ sai nhất
- Các bộ test phải đa dạng, tránh sự lặp đi lặp lại các bộ test tương tự
- Có một vài test lớn chỉ để kiểm tra tính chịu đựng của chương trình mà thôi Kết quả có đúng hay không thì trong đa số trường hợp, ta không thể kiểm chứng được với test này
Lưu ý rằng chương trình chạy qua được hết các test không có nghĩa là chương trình
đó đã đúng Bởi có thể ta chưa xây dựng được bộ test làm cho chương trình chạy sai Vì vậy nếu có thể, ta nên tìm cách chứng minh tính đúng đắn của thuật toán và chương trình, điều này thường rất khó
1.1.6 Tối ưu chương trình
Một chương trình đã chạy đúng không có nghĩa là việc lập trình đã xong, ta phải tiếp tục cải tiến cấu trúc dữ liệu sửa đổi lại một vài chi tiết để có thể chạy nhanh hơn, hiệu quả hơn Thông thường, trước khi kiểm thử thì ta nên đặt mục tiêu viết chương trình sao cho đơn giản, miễn sao chạy ra kết quả đúng là được, sau đó khi tối ưu chương trình, ta xem lại những chỗ nào viết chưa tốt thì tối ưu lại mã lệnh để chương trình ngắn hơn, chạy nhanh hơn Không nên viết tới đâu tối ưu mã đến đó, bởi chương trình có mã lệnh tối ưu thường phức tạp và khó kiểm soát.Ta nên tối ưu chương trình theo các tiêu chuẩn sau:
1 Tính tin cậy: Chương trình phải chạy đúng như dự định, mô tả đúng một giải
thuật đúng Thông thường khi viết chương trình, ta luôn có thói quen kiểm tra tính đúng đắn của các bước mỗi khi có thể
2 Tính uyển chuyển: Chương trình phải dễ sửa đổi Bởi ít có chương trình nào viết
ra đã hoàn hảo ngay được mà vẫn cần phải sửa đổi lại Chương trình viết dễ sửa đổi sẽ làm giảm bớt công sức của lập trình viên khi phát triển chương trình
Trang 12Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
3 Tính trong sáng: Chương trình viết ra phải dễ đọc dễ hiểu, để sau một thời gian
dài, khi đọc lại còn hiểu mình làm cái gì? Để nếu có điều kiện thì còn có thể sửa sai (nếu phát hiện lỗi mới), cải tiến hay biến đổi để được chương trình giải quyết bài toán khác Tính trong sáng của chương trình phụ thuộc rất nhiều vào công cụ lập trình và phong cách lập trình
4 Tính hữu hiệu: Chương trình phải chạy nhanh và ít tốn bộ nhớ, tức là tiết kiệm
được cả về không gian và thời gian Để có một chương trình hữu hiệu, cần phải
có giải thuật tốt và những tiểu xảo khi lập trình Tuy nhiên, việc áp dụng quá nhiều tiểu xảo có thể khiến chương trình trở nên rối rắm, khó hiểu khi sửa đổi Tiêu chuẩn hữu hiệu nên dừng lại ở mức chấp nhận được, không quan trọng bằng ba tiêu chuẩn trên Bởi phần cứng phát triển rất nhanh, yêu cầu hữu hiệu không cần phải đặt ra quá nặng
Từ những phân tích ở trên, chúng ta nhận thấy rằng việc làm ra một chương trình đòi hỏi rất nhiều công đoạn và tiêu tốn khá nhiều công sức Chỉ một công đoạn không hợp lý
sẽ làm tăng chi phí viết chương trình Nghĩ ra cách giải quyết vấn đề đã khó, biến ý tưởng
đó thành hiện thực cũng không dễ chút nào
1.2 Phân tích thời gian thực hiện thuật toán
1.2.1 Độ phức tạp thuật toán
Với một vấn đề đặt ra có thể có nhiều thuật toán giải, chẳng hạn người ta đã tìm ra rất nhiều thuật toán sắp xếp một mảng dữ liệu Trong các trường hợp như thế, khi cần sử dụng thuật toán người ta thường chọn thuật toán có thời gian thực hiện ít hơn các thuật toán khác Mặt khác, khi bạn đưa ra một thuật toán để giải quyết một vấn đề thì một câu hỏi đặt
ra là thuật toán đó có ý nghĩa thực tế không? Nếu thuật toán đó có thời gian thực hiện quá lớn chẳng hạn hàng năm, hàng thế kỉ thì đương nhiên không thể áp dụng thuật toán này trong thực tế Như vậy chúng ta cần đánh giá thời gian thực hiện thuật toán Phân tích thuật toán, đánh giá thời gian chạy của thuật toán là một lĩnh vực nghiên cứu quan trong của khoa học máy tính Trong chương này, chúng ta sẽ nghiên cứu phương pháp đánh giá thời gian chạy của thuật toán bằng cách sử dụng ký hiệu ô lớn, và chỉ ra cách đánh giá thời gian chạy thuật toán bằng ký hiệu ô lớn Trước khi đi tới mục tiêu trên, chúng ta sẽ thảo luận ngắn gọn một số vấn đề liên quan đến thuật toán và tính hiệu quả của thuật toán
1.2.1.1 Tính hiệu quả của thuật toán
Như đã phân tích ở trên, chúng ta thường xem xét thuật toán, lựa chọn thuật toán để
áp dụng dựa vào các tiêu chí sau:
1 Thuật toán đơn giản, dễ hiểu
2 Thuật toán dễ cài đặt (dễ viết chương trình)
3 Thuật toán cần ít bộ nhớ
Trang 13Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
4 Thuật toán chạy nhanh
Khi cài đặt thuật toán chỉ để sử dụng một số ít lần, người ta thường lựa chọn thuật toán theo tiêu chí 1 và 2 Tuy nhiên, có những thuật toán được sử dụng rất nhiều lần, trong nhiều chương trình, chẳng hạn các thuật toán sắp xếp, các thuật toán tìm kiếm, các thuật toán đồ thị…Trong các trường hợp như thế người ta lựa chọn thuật toán để sử dụng theo tiêu chí 3 và 4 Hai tiêu chí này được nói tới như là tính hiệu quả của thuật toán Tính hiệu quả của thuật toán gồm hai yếu tố: dung lượng bộ nhớ mà thuật toán đòi hỏi và thời gian thực hiện thuật toán Dung lượng bộ nhớ gồm bộ nhớ dùng để lưu dữ liệu vào, dữ liệu ra,
và các kết quả trung gian khi thực hiện thuật toán; dung lượng bộ nhớ mà thuật toán đòi hỏi còn được gọi là độ phức tạp không gian của thuật toán Thời gian thực hiện thuật toán
được nói tới như là thời gian chạy (Running time) hoặc độ phức tạp thời gian của thuật
toán Sau này chúng ta chỉ quan tâm tới đánh giá thời gian chạy của thuật toán
Đánh giá thời gian chạy của thuật toán bằng cách nào? Với cách tiếp cận thực nghiệm chúng ta có thể cài đặt thuật toán và cho chạy chương trình trên một máy tính nào
đó với một số dữ liệu vào Thời gian chạy mà ta thu được sẽ phụ thuộc vào nhiều nhân tố:
- Kỹ năng của người lập trình
- Chương trình dịch
- Tốc độ thực hiện các phép toán của máy tính
- Dữ liệu vào
Vì vậy, trong cách tiếp cận thực nghiệm, ta không thể nói thời gian chạy của thuật
toán là bao nhiêu đơn vị thời gian Chẳng hạn câu nói “thời gian chạy của thuật toán là 30 giây” là không thể chấp nhận được Nếu có hai thuật toán A và B giải quyết cùng một vấn
đề, ta cũng không thể dùng phương pháp thực nghiệm để kết luận thuật toán nào chạy nhanh hơn, bởi vì ta mới chỉ chạy chương trình với một số dữ liệu vào.Một cách tiếp cận khác để đánh giá thời gian chạy của thuật toán là phương pháp phân tích sử dụng các công
cụ toán học Chúng ta mong muốn có kết luận về thời gian chạy của một thuật toán mà nó không phụ thuộc vào sự cài đặt của thuật toán, không phụ thuộc vào máy tính mà trên đó thuật toán được thực hiện
Để phân tích thuật toán chúng ta cần sử dụng khái niệm cỡ (size) của dữ liệu vào Cỡ
của dữ liệu vào được xác định phụ thuộc vào từng thuật toán Ví dụ, trong thuật toán tính
định thức của ma trận vuông cấp n, ta có thể chọn cỡ của dữ liệu vào là cấp n của ma trận; còn đối với thuật toán sắp xếp mảng cỡ n thì cỡ của dữ liệu vào chính là cỡ n của mảng
Đương nhiên là có vô số dữ liệu vào cùng một cỡ Nói chung trong phần lớn các thuật
toán, cỡ của dữ liệu vào là một số nguyên dương n Thời gian chạy của thuật toán phụ
thuộc vào cỡ của dữ liệu vào; chẳng hạn tính định thức của ma trận cấp 20 đòi hỏi thời gian chạy nhiều hơn tính định thức của ma trận cấp 10 Nói chung, cỡ của dữ liệu càng lớn
Trang 14Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
thì thời gian thực hiện thuật toán càng lớn Nhưng thời gian thực hiện thuật toán không chỉ phụ thuộc vào cỡ của dữ liệu vào mà còn phụ thuộc vào chính dữ liệu vào
Trong số các dữ liệu vào cùng một cỡ, thời gian chạy của thuật toán cũng thay đổi
Chẳng hạn, xét bài toán tìm xem đối tượng a có mặt trong danh sách (a 1 , … ,a i , … ,a n) hay không Thuật toán được sử dụng là thuật toán tìm kiếm tuần tự: Xem xét lần lượt từng phần tử của danh sách cho tới khi phát hiện ra đối tượng cần tìm thì dừng lại, hoặc đi hết
danh sách mà không gặp phần tử nào bằng a Ở đây cỡ của dữ liệu vào là n, nếu một danh sách với a là phần tử đầu tiên, ta chỉ cần một lần so sánh và đây là trường hợp tốt nhất, nhưng nếu một danh sách mà a xuất hiện ở vị trí cuối cùng hoặc a không có trong danh sách, ta cần n lần so sánh a với từng a i (i=1,2,…,n), trường hợp này là trường hợp xấu
nhất Vì vậy, chúng ta cần đưa vào khái niệm thời gian chạy trong trường hợp xấu nhất và thời gian chạy trung bình
Thời gian chạy trong trường hợp xấu nhất (Worst-case running time) của một thuật
toán là thời gian chạy lớn nhất của thuật toán đó trên tất cả các dữ liệu vào cùng cỡ Chúng
ta sẽ ký hiệu thời gian chạy trong trường hợp xấu nhất là T(n), trong đó n là cỡ của dữ liệu
vào Sau này khi nói tới thời gian chạy của thuật toán chúng ta cần hiểu đó là thời gian chạy trong trường hợp xấu nhất Sử dụng thời gian chạy trong trường hợp xấu nhất để biểu thị thời gian chạy của thuật toán có nhiều ưu điểm Trước hết, nó đảm bảo rằng, thuật toán không khi nào tiêu tốn nhiều thời gian hơn thời gian chạy đó Hơn nữa, trong các áp dụng, trường hợp xấu nhất cũng thường xuyên xảy ra
Chúng ta xác định thời gian chạy trung bình (Average running time) của thuật toán là
số trung bình cộng của thời gian chạy của thuật toán đó trên tất cả các dữ liệu vào cùng cỡ
n Thời gian chạy trung bình của thuật toán sẽ được ký hiệu là T tb (n) Đánh giá thời gian
chạy trung bình của thuật toán là công việc rất khó khăn, cần phải sử dụng các công cụ của xác suất, thống kê và cần phải biết được phân phối xác suất của các dữ liệu vào Rất khó biết được phân phối xác suất của các dữ liệu vào Các phân tích thường phải dựa trên giả thiết các dữ liệu vào có phân phối xác suất đều Do đó, sau này ít khi ta đánh giá thời gian chạy trung bình.Để có thể phân tích đưa ra kết luận về thời gian chạy của thuật toán độc lập với sự cài đặt thuật toán trong một ngôn ngữ lập trình, độc lập với máy tính được sử dụng để thực hiện thuật toán, chúng ta đo thời gian chạy của thuật toán bởi số phép toán sơ cấp cần phải thực hiện khi ta thực hiện thuật toán Cần chú ý rằng, các phép toán sơ cấp là các phép toán số học, các phép toán logic, các phép toán so sánh,…, nói chung, các phép toán sơ cấp cần được hiểu là các phép toán mà khi thực hiện chỉ đòi hỏi một thời gian cố định nào đó (thời gian này nhiều hay ít là phụ thuộc vào tốc độ của máy tính) Như vậy
chúng ta xác định thời gian chạy T(n) là số phép toán sơ cấp mà thuật toán đòi hỏi, khi thực hiện thuật toán trên dữ liệu vào cỡ n Tính ra biểu thức mô tả hàm T(n) được xác định
như trên là không đơn giản, và biểu thức thu được có thể rất phức tạp
Trang 15Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
Do đó, chúng ta sẽ chỉ quan tâm tớitốc độ tăng(Rate of growth) của hàm T(n), tức là
tốc độ tăng của thời gian chạy khi cỡ dữ liệu vào tăng Ví dụ, giả sử thời gian chạy của
thuật toán là T(n) = 3n 2 + 7n + 5 (phép toán sơ cấp) Khi cỡ n tăng, hạng thức 3n2 quyết định tốc độ tăng của hàm T(n), nên ta có thể bỏ qua các hạng thức khác và có thể nói rằng thời gian chạy của thuật toán tỉ lệ với bình phương của cỡ dữ liệu vào
1.2.1.2 Kí pháp để đánh giá độ phức tạp thuật toán
Giả sử T(n) là thời gian thực hiện một thuật toán nào đó và f(n), g(n), h(n) là các hàm xác định dương với mọi n Khi đó ta có độ phức tạp của thuật toán là:
- Hàm Theta lớn:Θ(g(n)) nếu tồn tại các hằng số dương c 1 và c 2 và n 0 sao cho:
c 1 g(n) ≤ T(n) ≤ c 2 g(n) và gọi kí pháp chữ Θ theta lớn, hàm g(n) gọi là giới hạn chặt của hàm T(n)
- Hàm Omega lớn: Ω(g(n)) nếu tồn tại các hàng số c và n 0 sao cho T(n) ≥c.g(n) với mọi n ≥ n 0 và gọi là kí pháp chữ Ω lớn, hàm g(n) được gọi là giới hạn dưới của hàm T(n)
- Hàm O lớn: O(g(n)) nếu tồn tại các hàng số c và n0 sao cho T(n) ≤c.g(n) với
mọi n ≥ n 0 và gọi là kí pháp chữ O lớn, hàm g(n) được gọi là giới hạn trên của hàm T(n)
1.2.1.3 Các tính chất
(i) Tính bắc cầu: Tất cả các kí pháp trên đều có tính bắc cầu Nếu f(n)=O(g(n)) và g(n)= O(h(n)) thì f(n)= O(h(n))
(ii) Tính phản xạ: Tất cả các kí pháp trên đều có tính phản xạf(n)=O(f(n))
1.2.2 Xác định độ phức tạp của thuật toán
Quy tắc hằng số: Nếu đoạn chương trình P có thời gian thực hiện T(n)= O(c 1 f(n)) với c 1 là một hằng số dương thì có thể coi đoạn chương trình P có độ phức tạp tính toán là O(f(n))
Chứng minh: T(n)= O(c 1 f(n)) nên tồn tại c 0 >0 và n 0 >0 để T(n) ≤ c 0 c 1 f(n) với mọi n≥ n 0 Đặt c=c 0 c 1 ta có điều cần chứng minh
Quy tắc lấy Max: Nếu đoạn chương trình P có thời gian thực hiện T(n)=O(f(n)+g(n))
thì có thể coi đoạn chương trình đó có độ phức tạp tính toán là O(max( f(n), g(n)))
Chứng minh: T(n)=O(f(n)+g(n)) nên tồn tại n 0 >0 và c>0 để T(n) ≤cf(n) + cg(n), với mọi n ≥ n 0 vậy T(n) ≤cf(n) +cg(n) ≤ 2cmax (f(n),g(n)) với mọi n ≥ n 0 Từ đó suy điều cần chứng minh
Quy tắc cộng: Nếu P 1 có thời gian thực hiện là T 1 (n)=O(f(n)) và P 2 có thời gian thực
hiện T 2 (n)=O(g(n)), khi đó: T 1 (n) +T 2 (n) = O(f(n) +g(n))
Trang 16Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
Chứng minh: Vì T 1 (n)=O(f(n)) nên tồn tại các hàng số c 1 và n 1 sao cho T(n) ≤ c 1 f(n) với mọi n ≥ n 1 Vì T 2 (n)=O(g(n)) nên tồn tại các hàng số c2 và n2 sao cho T(n) ≤ c 1 g(n) với mọi n ≥ n 2. Chọn c=max (c 1 ,c 2) và n 0 =max (n 1 ,n 2 ) ta có với mọi n ≥ n 0:
T(n)=T 1 (n) + T 2 (n) ≤ c 1 f(n) + c 2 g(n) ≤ cf(n) +cg(n) = c(f(n) +g(n))
Như vậy ta có điều cần chứng minh
Quy tắc nhân:Nếu đoạn chương trình P có thời gian thực hiện T(n)=O(f(n)) Khi đó
nếu thực hiện k(n) lần đoạn chương trình P với k(n)=O(g(n)) thì độ phức tạp tính toán sẽ là: O(f(n) g(n))
Chứng minh: Thời gian thực hiện k(n) lần đoạn chương trình P sẽ là k(n)*T(n), theo
định nghĩa:
- Tồn tại c k ≥ 0 và n k >0 để k(n) ≤ c k( g(n)) với mọi n ≥ n k
- Tồn tại c T ≥ 0 và n T >0 để T(n) ≤ c T f(n) với mọi n ≥ n T
Vậy với mọi n ≥ max(n T, n k) ta có k(n)T(n) ≤ c k c T (f(n)g(n)) Từ đó suy ra điều cần
chứng minh
1.2.3 Ký hiệu Big-O và biểu diễn thời gian chạy của thuật toán
1.2.3.1 Định nghĩa ký hiệu Big-O
Bây giờ chúng ta đưa ra định nghĩa khái niệm một hàm là “ô lớn” của một hàm khác
Định nghĩa Giả sử f(n) và g(n) là các hàm thực không âm của đối số nguyên không
âm n Ta nói “f(n) là ô lớn của g(n)” và viết là f(n)=O( g(n) ) nếu tồn tại các hằng số dương c và n 0 sao cho f(n) ≤ cg(n) với mọi n ≥ n 0 [2]
Như vậy, f(n)=O(g(n)) có nghĩa là hàm f(n) bị chặn trên bởi hàm g(n) với một nhân
tử hằng nào đó khi n đủ lớn Muốn chứng minh được f(n)=O(g(n)), chúng ta cần chỉ ra
nhân tử hằng c , số nguyên dương n0 và chứng minh được f(n) ≤ cg(n) với mọi n ≥ no
âm của đối số nguyên dương)
- Nếu f(n) = g(n) + g 1 (n) + + g k (n), trong đó các hàm g i (n) (i=1, ,k) tăng chậm hơn hàm g(n) (tức là g i (n)/g(n) 0, khi n 0) thì f(n) = O(g(n))
Trang 17Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
- Nếu f(n)=O(g(n)) thì f(n)=O(d.g(n)), trong đó d là hằng số dương bất kỳ
- Nếu f(n)=O(g(n)) và g(n)=O(h(n)) thì f(n)=O(h(n)) (tính bắc cầu)
Các kết luận trên dễ dàng được chứng minh dựa vào định nghĩa của ký hiệu ô lớn
Đến đây, ta thấy rằng, chẳng hạn nếu f(n)=O(n 2
) thì f(n)=O(75n 2 ), f(n)=O(0,01n 2 ), f(n)=O(n 2 + 7n + logn), f(n)=O(n 3 ), , tức là có vô số hàm là cận trên (với một nhân tử hằng nào đó) của hàm f(n)
Một nhận xét quan trọng nữa là, ký hiệu O(g(n)) xác định một tập hợp vô hạn các hàm bị chặn trên bởi hàm g(n), cho nên ta viết f(n)=O(g(n)) chỉ có nghĩa f(n) là một trong
các hàm đó
1.2.3.2 Biểu diễn thời gian chạy của thuật toán
Thời gian chạy của thuật toán là một hàm của cỡ dữ liệu vào: hàm T(n) Chúng ta sẽ biểu diễn thời gian chạy của thuật toán bởi ký hiệu ô lớn: T(n)=O(f(n)), biểu diễn này có nghĩa là thời gian chạy T(n) bị chặn trên bởi hàm f(n) Thế nhưng như ta đã nhận xét, một
hàm có vô số cận trên Trong số các cận trên của thời gian chạy, chúng ta sẽ lấy cận trên
chặt (Tight bound) để biểu diễn thời gian chạy của thuật toán
Định nghĩa Ta nói f(n) là cận trên chặt của T(n) nếu T(n)=O(f(n)), và nếu
T(n)=O(g(n)) thì f(n)=O(g(n))
Nói một cách khác, f(n) là cận trên chặt của T(n) nếu nó là cận trên của T(n) và ta không thể tìm được một hàm g(n) là cận trên của T(n) mà lại tăng chậm hơn hàm f(n) Sau này khi nói thời gian chạy của thuật toán là O(f(n)), chúng ta cần hiểu f(n) là cận trên chặt
của thời gian chạy
Nếu T(n)=O(1) thì điều này có nghĩa là thời gian chạy của thuật toán bị chặn trên bởi một hằng số nào đó, và ta thường nói thuật toán có thời gian chạy hằng Nếu T(n)=O(n),
thì thời gian chạy của thuật toán bị chặn trên bởi hàm tuyến tính, và do đó ta nói thời gian chạy của thuật toán là tuyến tính Các cấp độ thời gian chạy của thuật toán và tên gọi của chúng được liệt kê trong bảng sau:
Bảng 1.3Các lớp độ phức tạp tính toán
O(1) O(logn) O(n) O(nlogn) O(n 2 ) O(n 3 ) O(2 n )
hằng logarit tuyến tính nlogn bình phương lập phương
mũ
Trang 18Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
Hằng số: Hầu hết các chỉ thị của các chương trình đều được thực hiện một lần hay
nhiều nhất chỉ một vài lần Nếu tất cả các chỉ thị của cùng một chương trình có tính chất này thì chúng ta sẽ nói rằng thời gian chạy của nó là hằng số Điều này hiển nhiên là điều
mà ta phấn đấu để đạt được trong việc thiết kế thuật toán
LogN: Khi thời gian chạy của chương trình là logarit tức là thời gian chạy chương
trình tiến chậm khi N lớn dần Thời gian chạy thuộc loại này xuất hiện trong các chương
trình mà giải một bài toán lớn bằng cách chuyển nó thành một bài toán nhỏ hơn, bằng cách cắt bớt kích thước một hằng số nào đó Với mục đích của chúng ta, thời gian chạy có được xem như nhỏ hơn một hằng số “lớn“ Cơ số của logarit làm thay đổi hằng số đó nhưng
không nhiều: Khi N là 1000 thì logN là 3 nếu cơ số là 10, là 10 nếu cơ số là 2; khi N là một triệu, logN được nhân gấp đôi bất cứ khi nào N được nhân đôi, logN tăng lên thêm một
hằng số
N: Khi thời gian chạy của một chương trình là tuyến tính, nói chung đây là trường
hợp mà một số lượng nhỏ các xử lý được làm cho mỗi phần tử dữ liệu nhập Khi N là một
triệu thì thời gian chạy cũng cỡ như vậy Khi N được nhân gấp đôi thì thời gian chạy cũng
được nhân gấp đôi Đây là tình huống tối ưu cho một thuật toán mà phải xử lý N dữ liệu nhập (hay sản sinh ra N dữ liệu xuất)
NlogN: Đây là thời gian chạy tăng dần lên cho các thuật toán mà giải một bài toán
bằng cách tách nó thành các bài toán con nhỏ hơn, kế đến giải quyết chúng một cách độc lập và sau đó tổ hợp các lời giải Chúng ta nói rằng thời gian chạy của thuật toán như thế
là “NlogN”
N 2: Khi thời gian chạy của một thuật toán là bậc hai, trường hợp này chỉ có ý nghĩa thực tế cho các bài toán tương đối nhỏ Thời gian bình phương thường tăng dần lên trong
các thuật toán mà xử lý tất cả các phần tử dữ liệu (có thể là hai vòng lặp lồng nhau) Khi N
là một ngàn thì thời gian chạy là 1 triệu Khi N được nhân đôi thì thời gian chạy tăng lên
gấp 4 lần
N 3: Tương tự, một thuật toán mà xử lý các bộ ba của các phần tử dữ liệu (có thể là 3 vòng lặp lồng nhau) có thời gian chạy bậc ba và cũng chí ý nghĩa thực tế trong các bài
toán nhỏ Khi N là một trăm thì thời gian chạy là một triệu Khi N được nhân đôi thì thời
gian chạy tăng lên gấp 8 lần
2 N: Một số ít thuật toán có thời gian chạy lũy thừa lại thích hợp trong một số trường
hợp thực tế Khi N là hai mươi thì thời gian chạy là 1 triệu Khi N tăng gấp đôi thì thời
gian chạy được nâng lên luỹ thừa hai
Đối với một thuật toán, chúng ta sẽ đánh giá thời gian chạy của nó thuộc cấp độ nào trong các cấp độ đã liệt kê trên Trong bảng trên, chúng ta đã sắp xếp các cấp độ thời gian
chạy theo thứ tự tăng dần, chẳng hạn thuật toán có thời gian chạy là O(logn) chạy nhanh hơn thuật toán có thời gian chạy là O(n), Các thuật toán có thời gian chạy là O(n k
), với
Trang 19Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
k=1,2,3, , được gọi là các thuật toán thời gian chạy đa thức (Polynomial-time algorithm)
Để so sánh thời gian chạy của các thuật toán thời gian đa thức và các thuật toán thời gian
mũ, chúng ta hãy xem xét bảng sau:
Bảng 1.4Thời gian chạy của các lớp thuật toán
0,00003 giây 0,0009 giây 0,027 giây 24,3 giây
0,00004 giây 0,0016 giây 0,064 giây 1,7 phút
0,00005 giây 0,0025 giây 0,125 giây 5,2 phút
0,00006 giây 0,0036 giây 0,216 giây
12,7 ngày
3855 thế kỷ
35,7 năm 2.108 thế kỷ
366 thế kỷ 1,3 1013 thế kỷ Trong bảng trên, giả sử mỗi phép toán sơ cấp cần 1 micro giây để thực hiện Thuật
toán có thời gian chạy n 2 , với cỡ dữ liệu vào n=20 thì thời gian chạy là 202x10-6 =0,004 giây Đối với thuật toán hàm mũ, thời gian chạy là chấp nhận được chỉ với các dữ liệu vào
có cỡ rất khiêm tốn, n < 30; khi cỡ dữ liệu vào tăng, thời gian chạy sẽ tăng lên rất nhanh
và trở thành con số khổng lồ Chẳng hạn, thuật toán với thời gian chạy 3n, với dữ liệu vào
cỡ 60, nó đòi hỏi thời gian là 1,3x1013 thế kỉ! Vì vậy nghiên cứu tìm ra các thuật toán hiệu quả (chạy nhanh) cho các vấn đề có nhiều ứng dụng trong thực tiễn luôn luôn là sự mong muốn của các nhà tin học
1.2.4 Độ phức tạp thuật toán với tình trạng dữ liệu vào
Trong nhiều trường hợp, thời gian thực hiện giải thuật không phải chỉ phụ thuộc vào kích thước dữ liệu mà còn phụ thuộc vào tình trạng của dữ liệu Chẳng hạn thời gian sắp xếp một dãy số theo thứ tự tăng dần mà dãy đưa vào chưa có thứ tự sẽ khác với thời gian sắp xếp một dãy số đã sắp xếp rồi hoặc đã sắp xếp theo thứ tự ngược lại Lúc này, khi phân tích thời gian thực hiện giải thuật ta sẽ phải xét tới trường hợp tốt nhất, trường hợp trung bình và trường hợp xấu nhất Khó khăn trong việc xác định độ phức tạp tính toán
trong trường hợp trung bình (bởi việc xác định T(n) trung bình thường phải dùng tới
những công cụ toán phức tạp), nên ta thường đánh giá độ phức tạp tính toán trong trường hợp xấu nhất
1.2.5 Chi phí thực hiện thuật toán
Độ phức tạp tính toán đặt ra là để đánh giá chi phí thực hiện một giải thuật về mặt thời gian Nhưng chi phí thực hiện giải thuật còn có rất nhiều yếu tố khác nữa: không gian
bộ nhớ phải sử dụng là một ví dụ Tuy nhiên, trên phương diện phân tích lý thuyết, ta chỉ
có thể xét tới vấn đề thời gian bởi việc xác định các chi phí khác nhiều khi rất mơ hồvà phức tạp Đối với người lập trình thì khác, một thuật toán với độ phức tạp dù rất thấp cũng
sẽ là vô dụng nếu như không thể cài đặt được trên máy tính, chính vì vậy khi bắt tay cài đặt một thuật toán, ta phải biết cách tổ chức dữ liệu một cách khoa học, tránh lãng phí bộ
Trang 20Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
nhớ không cần thiết Có một quy luật tương đối khi tổ chức dữ liệu: Tiết kiệm được bộ nhớ thì thời gian thực hiện thường sẽ chậm hơn và ngược lại
Trang 21Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
CHƯƠNG 2 TỔ CHỨC DỮ LIỆU CHO LỚP
THUẬT TOÁN CHIA ĐỂ TRỊ
Trong khoa học máy tính, chia để trị là một mô hình thiết kế thuật toán quan trọng dựa trên đệ quy với nhiều phân nhánh Thuật toán chia để trị hoạt động bằng cách chia bài toán thành nhiều bài toán nhỏ hơn thuộc cùng thể loại, cứ như vậy lặp lại nhiều lần, cho đến khi bài toán thu được đủ đơn giản để có thể giải quyết trực tiếp Sau đó lời giải của các bài toán nhỏ được tổng hợp lại thành lời giải cho bài toán ban đầu Kĩ thuật này là cơ sở cho nhiều thuật toán hiệu quả Tuy nhiên, khả năng hiểu và thiết kế thuật toán chia để trị là một kĩ năng đòi hỏi nhiều thời gian để làm chủ Trong chương này, tôi sẽ trình bày cách tổ chức dữ liệu cho lớp thuật toán chia để trị và các bài toán điển hình được giải quyết theo tiếp cận chia để trị
2.1 Chiến lược chia để trị
Ý tưởng của chiến lược này như sau: Chia vấn đề cần giải thành một số vấn đề con cùng dạng với vấn đề đã cho, chỉ khác là cỡ của chúng nhỏ hơn Mỗi vấn đề con được giải quyết độc lập Sau đó, ta kết hợp nghiệm của các vấn đề con để nhận được nghiệm của vấn đề đã cho Nếu vấn đề con là đủ nhỏ có thể dễ dàng tính được nghiệm, thì ta giải quyết nó, nếu không vấn đề con được giải quyết bằng cách áp dụng đệ quy thủ tục trên (tức là lại tiếp tục chia nó thành các vấn đề con nhỏ hơn,…) [1] Do đó, các thuật toán
được thiết kế bằng chiến lược chia-để-trị sẽ là các thuật toán đệ quy
Sau đây là lược đồ của kỹ thuật chia-để-trị:
DivideConquer (A,x)// Tìm nghiệm x của bài toán A
Kết hợp các nghiệm x i của các bài toán con A i (i=1,…,m)
để nhận được nghiệm x của bài toán A;
Hình 2.3Thuật toán chia để trị
Trang 22Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
“Chia một bài toán thành các bài toán con” cần được hiểu là ta thực hiện các phép
biến đổi, các tính toán cần thiết để đưa việc giải quyết bài toán đã cho về việc giải quyết các bài toán con cỡ nhỏ hơn
2.2 Tổ chức dữ liệu cho lớp thuật toán chia để trị
Chiến lược "chia để trị" được áp dụng cho các thuật toán quy bài toán ban đầu về đúng một bài toán nhỏ hơn, chẳng hạn như thuật toán tìm kiếm nhị phân, dùng cho việc tìm khóa trong một danh sách đã sắp xếp Khi thiết kế thuật toán giải quyết một vấn đề bằng kỹ thuật chia-để-trị thì thuật toán chúng ta thu được là thuật toán đệ quy Thuật toán
đệ quy được biểu diễn trong các ngôn ngữ lập trình bậc cao bởi các hàm đệ quy Đó là các hàm chứa các lời gọi hàm đến chính nó Trong mục này chúng ta sẽ nêu lên các đặc điểm của thuật toán đệ quy và phân tích hiệu quả (về không gian và thời gian) của thuật toán đệ quy là cơ sở cho thuật toán chia đê trị Đệ quy là một kỹ thuật đặc biệt quan trọng để giải quyết vấn đề Có những vấn đề rất phức tạp, nhưng chúng ta có thể đưa ra thuật toán đệ quy rất đơn giản, sáng sủa và dễ hiểu Cần phải hiểu rõ các đặc điểm của thuật toán đệ quy
để có thể đưa ra các thuật toán đệ quy đúng đắn
Khi đó, tổ chức dữ liệu cho lớp bài toán chia để trị được mô tả như sau:
Hình 2.4Tổ chức dữ liệu cho lớp bài toán chia để trị Trong đó:
Bài toán ban đầu được chia thành k bài toán con
Đầu vào của bài toán ban đầu n, m, …được phân nhỏ thành đầu vào lần lượt của các bài toán con là ni, mi (i=1 k)
Đầu ra bài toán ban đầu o được chia thành các đầu ra oi (i=1 k)
Giải thuật đệ quy cho một vấn đề cần phải thoả mãn các đòi hỏi sau:
1 Chứa lời giải cho các trường hợp đơn giản nhất của vấn đề Các trường hợp này được gọi là các trường hợp cơ sở hay các trường hợp dừng
Trang 23Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
2 Chứa các lời gọi đệ quy giải quyết các vấn đề con với cỡ nhỏ hơn
3 Các lời gọi đệ quy sinh ra các lời gọi đệ quy khác và về tiềm năng các lời gọi đệ quy phải dẫn tới các trường hợp cơ sở
Tính chất 3 là đặc biệt quan trọng, nếu không thoả mãn, hàm đệ quy sẽ chạy mãi không dừng Đối với một vấn đề, có thể có hai cách giải: giải thuật đệ quy và giải thuật dùng phép lặp Giải thuật đệ quy được mô tả bởi hàm đệ quy, còn giải thuật dùng phép lặp được mô tả bởi hàm chứa các lệnh lặp, để phân biệt với hàm đệ quy ta sẽ gọi là hàm lặp
Ưu điểm nổi bật của đệ quy so với phép lặp là đệ quy cho phép ta đưa ra giải thuật rất đơn giản, dễ hiểu ngay cả đối với những vấn đề phức tạp Trong khi đó, nếu không sử dụng đệ quy mà dùng phép lặp thì thuật toán thu được thường là phức tạp hơn, khó hiểu hơn
Các nhân tố có thể làm cho thuật toán đệ quy kém hiệu quả Trước hết, ta cần biết
cơ chế máy tính thực hiện một lời gọi hàm Khi gặp một lời gọi hàm, máy tính tạo ra một
bản ghi hoạt động (Activation record) ở ngăn xếp thời gian chạy (Run-time stack) trong bộ
nhớ của máy tính Bản ghi hoạt động chứa vùng nhớ cấp cho các tham biến và các biến địa phương của hàm Ngoài ra, nó còn chứa các thông tin để máy tính trở lại tiếp tục hiện chương trình đúng vị trí sau khi nó đã thực hiện xong lời gọi hàm Khi hoàn thành thực hiện lời gọi hàm thì bản ghi hoạt động sẽ bị loại bỏ khỏi ngăn xếp thời gian chạy Khi thực hiện một hàm đệ quy, một dãy các lời gọi hàm được sinh ra Hậu quả là một dãy bản ghi hoạt động được tạo ra trong ngăn xếp thời gian chạy Cần chú ý rằng, một lời gọi hàm chỉ được thực hiện xong khi mà các lời gọi hàm mà nó sinh ra đã được thực hiện xong và do
đó rất nhiều bản ghi hoạt động đồng thời tồn tại trong ngăn xếp thời gian chạy, chỉ khi một lời gọi hàm được thực hiện xong thì bản ghi hoạt động cấp cho nó mới được loại ngăn xếp thời gian chạy Vì vậy, việc thực hiện hàm đệ quy có thể đòi hỏi rất nhiều không gian nhớ trong ngăn xếp thời gian chạy, thậm chí có thể vượt quá khả năng của ngăn xếp thời gian chạy trong bộ nhớ của máy tính Một nhân tố khác làm cho các thuật toán đệ quy kém hiệu quả là các lời gọi đệ quy có thể dẫn đến phải tính nghiệm của cùng một bài toán con rất nhiều lần
2.3 Định lý tổng quát tính độ phức tạp các thuật toán chia để trị
Các thuật toán chia để trị thường chuyển bài toán lớn về các bài toán nhỏ rồi kết hợp lời giải các bài toán nhỏ để tạo ra kết quả của bài toán ban đầu Để tính độ phức tạp của các thuật toán chia để trị, người ta thường sử dụng định lý tổng quát sau:
Trang 24Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
b n ), đồng thời giả sử ở trường hợp cơ sở n = 1, ta có T(1) = f(1)
Khai triển ra ta được:
2 2
2 2
1 1
k k
Trang 25Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
Một ví dụ lâu đời của thuật toán chia để trị là thuật toán Cooley-Tukey [3] cho biến đổi Fourier rời rạc Thuật toán này được phát hiện bởi Gauss năm 1805 nhưng ông không phân tích số phép tính của thuật toán và thuật toán này chỉ trở nên phổ biến khi được phát hiện lại hơn một thế kỉ sau đó Trong toán học, phép biến đổi Fourier rời rạc, đôi khi còn được gọi là biến đổi Fourier hữu hạn, là một biến đổi trong giải tích Fourier cho các tín hiệu thời gian rời rạc Đầu vào của biến đổi này là một chuỗi hữu hạn các số thực hoặc số phức, làm biến đổi này là một công cụ lý tưởng để xử lý thông tin trên các máy tính Đặc biệt, biến đổi này được sử dụng rộng rãi trong xử lý tín hiệu và các ngành liên quan đến phân tích tần số chứa trong một tín hiệu, để giải phương trình đạo hàm riêng, và để làm các phép như tích chập Biến đổi này có thể được tính nhanh bởi thuật toán biến đổi Fourier nhanh [2, 3, 4]
2.4.1 Lớp bài toán tìm kiếm
2.4.1.1 Thuật toán tìm kiếm nhị phân
a) Ý tưởng: Thuật toán tìm kiếm nhị phân là thuật toán được thiết kế dựa trên chiến
lược chia-để-trị Cho mảng A cỡ n được sắp xếp theo thứ tự tăng dần: A[0] ≤…≤ A[n-1] Với x cho trước, ta cần tìm xem x có chứa trong mảng A hay không, tức là có hay không chỉ số 0 ≤ i ≤ n-1 sao cho A[i] = x[3]
Kỹ thuật chia-để-trị gợi ý ta: Chia mảng A[0…n-1] thành 2 mảng con cỡ n/2 là A[0…k-1] và A[k+1…n-1], trong đó k là chỉ số đứng giữa mảng So sánh x với A[k] Nếu
x = A[k] thì mảng A chứa x và i = k Nếu không, do tính được sắp của mảng A, nếu x A[k] ta tìm x trong mảng A[0…k-1], còn nếu x A[k] ta tìm x trong mảng A[k+1…n-1]
Áp dụng cho bài toán sau:
- Input: Dãy gồm N số nguyên k 1 , k 2 , , k N đôi mộ
Khi đó, chỉ xảy ra một trong ba trường hợp sau:
- Nếu kGiua = x thì Giua là chỉ số cần tìm Việc tìm kiếm kết thúc
- Nếu kGiua> k thì do dãy khoa là dãy đã được sắp xếp nên việc tìm kiếm tiếp theo chỉ xét trên dãy k1, ka2, , kGiua–1 (phạm vi tìm kiếm mới bằng khoảng một nửa phạm vi tìm kiếm cũ)
Trang 26Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
- Nếu aGiua< k thì thực hiện tìm kiếm trên dãy kGiua+1, kGiua+2, , kN
Quá trình trên sẽ được lặp lại một số lần cho đến khi hoặc đã tìm thấy x trong dãy khoa hoặc khẳng định dãy khoa không chứa giá trị bằng x
b) Mô tả thuật toán
- 1.Nhập N, các giá trị k1, k2, , kNvà giá trị khóa x
- 2 Dau 1, Cuoi N
- 4 Nếu kGiua = x thì thông báo chỉ số Giua, rồi kết thúc
- 5 Nếu kGiua> x thì đặt Cuoi = Giua – 1 rồ ớc 7
- 6 Dau Giua + 1
- > hông báo dãy không có số hạng có giá trị trùng với
x, rồi kết thúc
- 8 Quay lại bước 3
Cài đặt thuật toán như sau:
intBINARYSEARCH(int a[max],int x, int l, int r)
if ( x > a[mid] ) return BINARYSEARCH (a, x, mid + 1, r);
returnBINARYSEARCH (a, x, l, mid - 1);
Cuoi 10 10 7 Giua 5 8 6
a Giua 9 30 21 Lượt 0 1 2 Sau hai lượt thì a Giua = k Vậy chỉ số cần tìm là i = Giua = 6
k = 25, N =10
i 1 2 3 4 5 6 7 8 9 10
A 2 4 5 6 9 21 22 30 31 33
Trang 27Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
Dau 1 6 6 7 8 Cuoi 10 10 7 7 7 Giua 5 8 6 7
a Giua 9 30 21 22 Lượt 0 1 2 3 4 Tại lượt thứ tư Dau>Cuoi nên kết luận trong dãy A không có toán hạng nào
có giá trị là 25 cả
d)Phân tích đánh giá
Trường hợp giải thuật tìm nhị phân ta có bảng phân tích sau:
Bảng 2.1Độ phức tạp của thuật toán tìm kiếm nhị phân
Tốt nhất 1 Phần tử giữa của mảng ban đầu có giá trị x
Xấu nhất log2 N Phần tử cần tìm nằm ở cuối mảng
Trung bình log2 N/2 Giả sử xác xuất các phần tử trong mảng nhận
giá trị x là như nhau
Giải thuật tìm nhị phân phụ thuộc vào thứ tự của các phần tử trong mảng để định hướng trong quá trình tìm kiếm, do vậy chỉ áp dụng được cho những dãy đã có thứ tự Thuật toán tìm kiếm nhị phân tiết kiếm thời gian hơn rất nhiều so với giải thuật tìm tuyến tính do Onhịphân(log2n) <Otuyếntính(n) [1] Tuy nhiên khi muốn áp dụng giải thuật tìm nhị phân cần phải xét đến thời gian sắp xếp dãy số để thỏa điều kiện dãy số có thứ tự, thời gian này không nhỏ, và khi dãy số biến động cần phải tiến hành sắp xếp lại,…tất cả các nhu cầu đó tạo ra khuyết điểm chính cho giải thuật tìm nhị phân
2.3.1.2 Bài toán tìm Max và min
a) Ý tưởng: Cho mảng A cỡ n, chúng ta cần tìm giá trị lớn nhất (max) và nhỏ nhất
(min) của mảng này Bài toán đơn giản này có thể giải quyết bằng các thuật toán khác nhau Một thuật toán rất tự nhiên và đơn giản là như nhau Đầu tiên ta lấy max, min là giá trị đầu tiên A[0] của mảng Sau đó so sánh max, min với từng giá trị A[i], 1 ≤ i ≤ n-1, và cập nhật max, min một cách thích ứng [3]
Thuật toán này được mô tả bởi hàm sau:
SiMaxMin (A, max, min)
Trang 28Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
Thời gian thực hiện thuật toán này được quyết định bởi số phép so sánh x với các thành phần A[i] Số lần lặp trong lệnh lặp for là n-1 Trong trường hợp xấu nhất (mảng A được sắp theo thứ tự giảm dần), mỗi lần lặp ta cần thực hiện 2 phép so sánh Như vậy, trong trường hợp xấu nhất, ta cần thực hiện 2(n-1) phép so sánh, tức là thời gian chạy của thuật toán là O(n)
Bây giờ ta áp dụng kỹ thuật chia-để-trị để đưa ra một thuật toán khác Ta chia mảng A[0 n-1] thành các mảng con A[0 k] và A[k+1 n-1] với k = [n/2] Nếu tìm được max, min của các mảng con A[0 k] và A[k+1 n-1], ta dễ dàng xác định được max, min trên mảng A[0 n-1] Để tìm max, min trên mảng con ta tiếp tục chia đôi chúng Quá trình sẽ dừng lại khi ta nhận được mảng con chỉ có một hoặc hai phần tử Trong các trường hợp này ta xác định được dễ dàng max, min
b)Mô tả thuật toán
Do đó, ta có thể đưa ra thuật toán sau:
MaxMin (i, j, max, min)
// Biến max, min ghi lại giá trị lớn nhất, nhỏ nhất trong mảng A[i j]
max = A[i]; min = A[j];
} else {
mid = (i+j) / 2;
MaxMin (i, mid, max1, min1);
MaxMin (mid + 1, j, max2, min2);
if (max 1 max2) max = max2;
else max = max1;
if (min1 min2) min = min1;
else min = min2;
} }
Cài đặt thuật toán trên C như sau:
void MinMax(int a[], int dau, int cuoi, int &min, int &max)
{
int min1,min2,max1,max2;
if (dau==cuoi)
{
Trang 29Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
Áp dụng phương pháp thế lặp, ta tính được T(n) như sau:
2.4.2.1 Thuật toán sắp xếp trộn (Merge Sort)
Một ví dụ lâu đời khác là thuật toán sắp xếp trộn, phát hiện bởi John Von Neumann năm 1945 [3] Ý tưởngthực tế là thông thường dãy dữ liệu đang được lưu trữ là dãy đã sắp xếp, các dữ liệu mới bổ sung thêm vào cuối dãy đã có Xuất hiện nhu cầu cần sắp xếp lại
để dãy dữ liệu sau khi nhập bổ sung thêm các phần tử mới phải được sắp xếp lại
b)Mô tả thuật toán
Trộn (Merge): Cho hai dãy đã sắp xếp B={b1,b2,…bm} và C={ c1,c2,…,cn} cần trộn thành dãy D={d1,d2,…,dm+n ợc sắp
Trang 30Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
i) Lần lượt xác định di ( 1<=i<=n+m) bằng cách chọn phần tử nhỏ hơn trong hai phần tử bj và ck (1<=j<=m; 1<=k<=n) tại mỗi bước
ii) Trong cài đặt thường thêm một phần tử có giá trị lớn hơn giá trị các phần tử trong dãy vào cuối mỗi dãy B và C ( chẳng hạn, bm+1= Maxint và cn+1= Maxint, thường gọi là khóa cầm canh) để khi tất cả các phần tử của một dãy
đã được lựa chọn cho dãy D thì các phần tử còn lại của dãy kia sẽ chuyển thành các phần tử còn lại của dãy D
Cài đặt thuật toán như sau:
Trang 31Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
// Thủ tục trộn hai đoạn đã sắp xếp trong một dãy
voidMERGE(int a[], int k1, int k2, int k3)
{
inti,j,k,t; inttemp[]; i=k1; j=k2; k=k3;
while (i<k2) and (j<=k3)
Trang 32Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
Hình 2.3 Ví dụ thuật toán sắp xếp trộn
d)Phân tích và đánh giá
Sắp xếp trộn là một thuật toán sắp xếp cổ điển nhất nhưng cho tới nay đó là thuật toán được coi là thuật toán sắp xếp ngoài mẫu mực.Phép toán tích cực trong phép trộn là
phép đưa một phần tử khóa vào dãy kết quả nên độ phức tạp của trộn là O(N)
Trong sắp xếp trộn sử dụng không quá [logn] lần trộn nên độ phức tạp của thuật toán
sắp xếp trộn là O(NlgN).Nhược điểm là phải dùng thêm không gian để lưu trữ dãy khóa d
(trong việc trộn)
2.4.2.2 Thuật toán sắp xếp nhanh (Quick Sort)
a) Ý tưởng: Để sắp xếp dãy a1, a2 …an giải thuật Quick Sort dựa trên việc phân hoạch dãy ban đầu thành 2 dãy con khác nhau :
- Dãy con 1: Gồm các phần tử a1 …ai có giá trị không lớn hơn x
- Dãy con 2: Gồm các phần tử ai …an có giá trị không nhỏ hơn x
Với x là giá trị của một phần tử tuỳ ý trong dãy ban đầu Sau khi thực hiện phân hoạch, dãy ban đầu được chia làm 3 phần:
Vấn đề còn lại bây giờ là xây dựng một thủ tục phân hoạch cho dãy ban đầu, điều này phụ thuộc nhiều vào việc xác định phần tử làm mốc ban đầu, mốc được chọn làm sao để tạo ra hai dãy con cân bằng nhau, điều này rất mất thời gian Vì vậy người ta thường chọn phần tử đầu tiên của mảng làm mốc, giả sử dãy ban đầu là mảng gồm có các phần tử
a[i] a[j], tức là lấy p= a[i] làm mốc.Sau đó sử dụng các biến L chạy từ trái sang phải bắt đầu từ vị trí thứ i, biến k chạy từ phải sang trái bắt đầu từ j+1 Biến L được tăng cho tới khi a[L] >p, còn biến k được giảm cho tới khi a[k]<=p, Nếu L<k thì ta đổi giá trị của a[L] và a[k] Quá trình đó lặp đi lặp lại cho đến khi L>k Cuối cùng ta trao đổi vị trí a[i] và a[k] để
đặt mốc vào đúng vị trí của nó.Nhận xét:
Trang 33Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
- Về nguyên tắc, có thể chọn giá trị mốc p là một phần tử tuỳ ý trong dãy, nhưng
để đơn giản, dễ diễn đạt giải thuật, phần tử có vị trí giữa thường được chọn, khi
đó p :=int((i+j)/2)
- Giá trị mốc p được chọn sẽ tác động đến hiệu quả thực hiện thuật toán vì nó quyết định số lần phân hoạch Số lần phân hoạch sẽ ít nhất nếu ta chọn được p là phần tử trung bình (median) của dãy Tuy nhiên, trên thực tế ta khó chọn được phần tử này nên thường chọn phần tử bất kỳ hoặc phần tử nằm chính giữa dãy
làm mốc với hy vọng nó có thể gần với giá trị median (HS viết thủ tục với p nằm giữa dãy)
b) Thuật toán
Có thể phát biểu giải thuật sắp xếp QuickSort một cách đệ qui như sau
Bước 1 :
- Phân hoạch dẫy ai…ajthành các dãy con :
- Dãy con 1 : a[i]…a[L] < x
- Dãy con 2 : a[L]…a[j] >= x
Bước 2 :
- Nếu (i< L) Phân hoạch dãy a[i]…a[L]
- Nếu (L<j) Phân hoạch dãy a[L]…a[j]
Cài đặt thuật toán trên C:
void QUICKSORT(int X[], int n)
pirot=(L+R)div 2; // Luôn lấy chốt ở vị trí gần chính giữa dãy
key = X[pirot]; int i=L;int j= R;
Trang 34Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
Partition(L,j);
Partition(i,R); }
c) Ví dụ
Công việc sắp xếp dãy trên bằng thuật toán QuickSort được tiến hành như sau:
Phân hoạch đoạn l = 0, r = 6, x = a[3] = 7
dãy được phân chia thành 2 phần bằng nhau và chỉ cần log(n) lần phân hoạch thì sắp xếp
xong Nhưng nếu mỗi lần phân hoạch lại chọn phải phần tử có giá trị cực đại hay cực tiểu làm mốc, dãy sẽ bị phân thành 2 phần không đều: một phần chỉ có 1 phần tử, phần còn lại
có (n-1) phần tử, do vậy cần phân hoạch n lần mới sắp xếp xong
ẽ
: 1,2,3,4,…N
( 1,2, N-1, N, N, N-1,… 2.1)
03 ph
ảyra
Trang 35Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
2.4.3 Lớp bài toán tối ƣu
2.4.3.1 Bài toán dãy con dài nhất
Cho mảng A[1 n] Mảng A[p q] được gọi là mảng con của A Trọng lượng mảng
bằng tổng các phần tử Tìm mảng con có trọng lượng lớn nhất (1≤ p ≤ q ≤ n)
Để đơn giản ta chỉ xét bài toán tìm trọng lượng của mảng con lớn nhất còn việc tìm
vị trí thì chỉ là thêm vào bước lưu lại vị trí trong thuật toán Ta có thể dễ dàng đưa ra thuật toán tìm kiếm trực tiếp bằng cách duyệt hết các dãy con có thể của mảng A như sau:
void BruteForceNaice;
{
Max1 = -MaxInt;
for (i = 1; i<= n; i++) // i là điểm bắt đầu của dãy con
for( j =i; j<= n; j++) // j là điểm kết thúc của dãy con
Phân tích độ phức tạp của thuật toán:Lấy s = s + A[k] làm câu lệnh đặc trưng, ta có
số lần thực hiện câu lệnh đặc trưng là 3
Trang 36Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
Lấy s = s + A[j] làm câu lệnh đặc trưng thì ta có số lần thực hiện câu lệnh đặc trưng
Trị: Tính mảng con lớn nhất của mỗi nửa mảng A một cách đệ quy Gọi WL,
WR là trọng lượng của mảng con lớn nhất trong AL, AR
Tổng hợp: Max (WL, WR)
WM = WML + WMR Cài đặt thuật toán:
Sum = Sum + A[k];
MaxSum = MaxSum(Sum, MaxSum);
Trang 37Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
}
Phân tích độ phức tạp: Thời gian chạy thủ tục MaxLeftVector và MaxRightVector
là O(m) (m = j - i + 1) Gọi T(n) là thời gian tính, giả thiết n = 2k Ta có:
Nếu n = 1 thì T(n) = 1
Nếu n > 1 thì việc tính WM đòi hỏi thời gian n/2 + n/2 = n T(n) = 2T(n/2) +
n
Theo định lý thợ ta có: ( )T n O(log )n
2.4.3.2 Bài toán tháp Hà Nội
Có 3 chiếc cọc và một bộ n chiếc đĩa Các đĩa này có kích thước khác nhau và mỗi đĩa đều có 1 lỗ ở giữa để có thể xuyên chúng vào các cọc Ban đầu, tất cả các đĩa đều nằm trên 1 cọc, trong đó, đĩa nhỏ hơn bao giờ cùng nằm trên đĩa lớn hơn Yêu cầu của bài toán
là chuyển bộ n đĩa từ cọc ban đầu A sang cọc đích C (có thể sử dụng cọc trung gian B), với các điều kiện: mỗi lần chuyển 1 đĩa, trong mọi trường hợp, đĩa có kích thước nhỏ hơn bao giờ cũng phải nằm trên đĩa có kích thước lớn hơn [3]
Với n = 1, có thể thực hiện yêu cầu bài toán bằng cách chuyển trực tiếp đĩa 1 từ cọc
A sang cọc C Với n = 2 có thể thực hiện như sau:
- Chuyển đĩa nhỏ từ cọc A sang cọc trung gian B
- Chuyển đĩa lớn từ cọc A sang cọc đích C
- Cuối cùng, chuyển đĩa nhỏ từ cọc trung gian B sang cọc đích C
Như vậy, cả 2 đĩa đã được chuyển sang cọc đích C và không có tình huống nào đĩa lớn nằm trên đĩa nhỏ.Với n > 2, giả sử ta đã có cách chuyển n – 1 đĩa, ta thực hiện như sau:
- Lấy cọc đích C làm cọc trung gian để chuyển n – 1 đĩa bên trên sang cọc trung gian B
- Chuyển cọc dưới cùng (cọc thứ n) sang cọc đích C
- Lấy cọc ban đầu A làm cọc trung gian để chuyển n – 1 đĩa từ cọc trung gian B sang cọc đích C
Như vậy, cả hai đĩa đã được chuyển sang cọc đích C và không có tình huống nào đĩa lớn nằm trên đĩa nhỏ Ta thấy toàn bộ n đĩa đã được chuyển từ cọc A sang cọc C và không
vi phạm bất cứ điều kiện nào của bài toán.Ở đây, ta thấy rằng bài toán chuyển n cọc đã được chuyển về bài toán đơn giản hơn là chuyển n - 1 cọc Điểm dừng của thuật toán đệ qui là khi n = 1 và ta chuyển thẳng cọc này từ cọc ban đầu sang cọc đích Tính chất chia để trị của thuật toán này thể hiện ở chỗ: Bài toán chuyển n đĩa được chia làm 2 bài toán nhỏ
Trang 38Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
hơn là chuyển n - 1 đĩa Lần thứ nhất chuyển n - 1 đĩa từ cọc A sang cọc trung gian B, và lần thứ 2 chuyển n - 1 đĩa từ cọc trung gian B sang cọc đích C
Cài đặt đệ quy cho thuật toán như sau: hàm Chuyển (int n, char a char c) thực hiện việc chuyển đĩa thứ n từ cọc A sang cọc C Hàm Thaphanoi (int n, char a, char c, char b)
là hàm đệ quy thực hiện việc chuyển n đĩa từ cọc A sang cọc C, sử dụng cọc trung gian là cọc B
VoidChuyen (int n, char a, char c)
{
Print (“Chuyendia thu %d tu coc %c sang coc %c\n”, n, a, c);
Return;
}
Trang 39Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
Void ThapHaNoi (int n, char a, char c, char b)
{
If (n = = 1) chuyen (1, a, c);
Else {
Thaphanoi (n-1, a, b, c);
Chuyen (n, a, c);
Thaphanoi (n – 1, b, c, a);
} Return;
}
Hàm Chuyển thực hiện thao tác in ra 1 dòng cho biết chuyển đĩa thứ mấy từ cọc nào sang cọc nào Hàm ThapHaNoi kiểm tra nếu số đĩa bằng 1 thì thực hiện chuyển trực tiếp
đĩa từ cọc A sang cọc C Nếu số đĩa lớn hơn 1, có ba lệnh được thực hiện:
- Lời gọi đệ quy ThapHaNoi(n – 1, a, b, c) để chuyển n – 1 đĩa từ cọc A sang
cọc B, sử dụng cọc C làm trung gian
- Thực hiện chuyển đĩa thứ n từ cọc A sang cọc C
- Lời gọi đệ quy ThapHaNoi(n-1, b, c, a) để chuyển n – 1 đĩa từ cọc B sang cọc
C, sử dụng cọc A làm cọc trung gian
Độ phức tạp của thuật toán là 2n – 1 Nghĩa là để chuyển n cọc thì mất 2n – 1 thao tác chuyển Ta sẽ chứng minh điều này bằng phương pháp quy nạp toán học Với n = 1 thì số lần chuyển là 1 = 2*1 – 1 Giả sử giả thiết đúng với n - 1, tức là để chuyển n - 1 đĩa cần thực hiện 2n – 1 thao tác chuyển Ta sẽ chứng minh rằng để chuyển n đĩa cần 2n - 1 thao tác chuyển Thật vậy, theo phương pháp chuyển của giải thuật thì có 3 bước Bước 1 chuyển n – 1 đĩa từ cọc A sang cọc B mất 2n - 1 thao tác Bước 2 chuyển 1 đĩa từ cọc A sang cọc C mất 1 thao tác Bước 3 chuyển n – 1 đĩa từ cọc B sang cọc C mất 2n - 1 - 1 thao tác Tổng cộng ta mất (2n - 1 - 1) + (2n - 1 - 1) + 1 = 2 * 2n - 1 - 1 = 2n - 1 thao tác chuyển Đó là điều cần chứng minh Như vậy, thuật toán có cấp độ tăng rất lớn Nói về cấp
độ tăng này, có một truyền thuyết vui về bài toán tháp Hà Nội như sau: Ngày tận thế sẽ đến khi các nhà sư ở một ngôi chùa thực hiện xong việc chuyển 40 chiếc đĩa theo quy tắc như bài toán vừa trình bày Với độ phức tạp của bài toán vừa tính được, nếu giả sử mỗi lần chuyển 1 đĩa từ cọc này sang cọc khác mất một giây thì với 240 – 1 lần chuyển, các nhà sư này phải mất ít nhất 34.800 năm thì mới có thể chuyển xong toàn bộ số đĩa này
2.4.3.5 Bài toán xếp lịch thi đấu
Giả sử cần lập một lịch thi đấu Tennis cho n = 2 vận động viên (VĐV) Mỗi vận động viên phải thi đấu với lần lượt n-1 vận động viên khác, mỗi ngày thi đấu 1 trận Như vậy n-1 là số ngày thi đấu tối thiểu phải có Chúng ta cần lập lịch thi đấu bằng cách thiết