1. Trang chủ
  2. » Luận Văn - Báo Cáo

Tổ chức dữ liệu cho lớp thuật toán chia để trị và ứng dụng

80 162 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 80
Dung lượng 4,43 MB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

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ánchia để 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êuchính là áp

Trang 1

ĐẠI HỌC THÁI NGUYÊN

TRƯỜNG ĐẠI HỌC CÔNG NGHỆ THÔNG TIN&TRUYỀN THÔNG

Trang 2

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êucầ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 đượctrong quá trình nghiên cứu là trung thực chưa từng được ai công bố trước đây

Thái Nguyên, ngày 19 tháng 5 năm 2014

Học viên thực hiên

Đỗ Tuấn Anh

Trang 3

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 haină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 4

http://w ww .l r c-

tn u edu vn/

Số hóa bởi Trung tâm Học liệu 4 MỤC LỤC Lời cam đoan Lời cảm ơn Trang 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

Trang 6

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 7

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 8

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 đờisố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ảicho 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ệmbà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ánchia để 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êuchí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ênlớ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ácchiế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ấtthuậ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áchtiế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 9

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ụctí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ờigiả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ứcnà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ờigian 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ểnchuỗ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ớiphươ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ảití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ảiquyế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àitoá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àitoá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 10

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ếnhà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ựchiệ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ìmkiế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ìnhnhỏ để 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ãythao 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ướcthự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ácbướ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ể đánhgiá 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ưutrữ có thể giải quyết dễ hơn thách thức về thời gian thực hiện

Trang 11

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ậttoá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ậycù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ệncủ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ướngthể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ăngquan 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ửachữ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ôngbiế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ửalại cho đúng

Trang 12

- 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ếtquả đú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ấtkhó khăn Khi đó ta nên làm các bộ dữ liệu test để thử chương trình của mình Kinhnghiệ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ố để sosá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ầmthườ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ếtquả 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ếptụ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 xemlạ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ạynhanh 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 ưuthườ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 tratí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 13

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ửasai (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ếtbà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ọngbằ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ệukhô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 đòihỏ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ấtnhiề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ụngthuậ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ánkhá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àytrong 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íchthuậ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 trongcủ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ờigian 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ảoluậ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 14

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ậttoá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, trongnhiề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ậttoá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 theotiê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ệuquả 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 gianthự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 đòihỏ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ựcnghiệ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ạynhanh 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ậnkhá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ờigian 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 15

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) haykhô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ừngphầ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 gianchạ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ểuthị 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ánkhô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ủaxá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 gianchạ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 độclậ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éptoá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 16

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ằngthờ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ầnchứ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 17

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 18

- 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 gianchạ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ủachú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ằnglogarit tuyến tính nlognbình phương lập phương mũ

Trang 19

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ấtnà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áchcắ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ó đượcxem 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 độclậ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ĩathự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à 3vò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àotrong 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 20

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,004giâ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ệuquả (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ự mongmuố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àokí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ắpxế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 giansắ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, khiphâ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ợptrung 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ườnghợ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ặtthờ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 21

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 22

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ọngdự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àitoá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ácbà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 theotiế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 xi của các bài toán con Ai (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 23

“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ếtcá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 n h ị p h â n , dùng cho việctì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áchà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ểmcủ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ảiquyế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:

Trong đó:

Hình 2.4Tổ chức dữ liệu cho lớp bài toán chia để trị

 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 24

aT f n b

n

cf n

logb a

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ãikhô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ậtdù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 đơngiả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 địaphươ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ệnchươ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ựchiệ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ựchiệ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 ghihoạ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ộtlờ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ếpthờ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 gianchạ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ệuquả 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ấtnhiề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ợplờ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ủacác thuật toán chia để trị, người ta thường sử dụng định lý tổng quát sau:

Định lý (Mastertheorem): Cho T n , ta có

1 Nếu af với c> 1 thì T n

Trang 25

f n aT n b f n af n b a2T n b2

f n af n b a2 f n b2 a k f n b k n

cf n b

1 1

f n

n

cf n b

giải rồi dùng f(n) phép tính để kết hợp lời giải các bài toán con lại.

Chứng minh:Giả sử n (không mất tính tổng quát do ta có thể chọn k nhỏ nhấtsao cho b k ), đồ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:

a k f n b k

nhất là Như vậy, ta có: T n

Trường hợp 2: Nếu af với c< 1 tương tự ta cũng có tổng dãy cấp số

nhân với hệ số lũy tiến c< 1 và số hạng lớn nhất là f(n) Như vậy, ta có: T n

Trường hợp 3: Nếu af , các số hạng của tổng trên đều bằng nhau và

bằng f(n), vậy : T

2.4 Một số lớp bài toán điển hình

Trang 26

Một ví dụ lâu đời của thuật toán chia để trị là th u ật to á n Coole y - Tuk e y [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ôngphâ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áthiệ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à b iến đ ổ i Fourier h ữ u h ạ n , là một biến đổi trong g iải t í ch Fourier cho các tínhiệ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ố t h ực hoặc số

biệt, biến đổi này được sử dụng rộng rãi trong x ử l ý t í n h i ệu và các ngành liên quan đếnphân tích tần số chứa trong một tín hiệu, để giải phươ n g tr ì nh đ ạ o hàm ri ê n g , và để làmcá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 b iến đ ổ iFourier n h a nh [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ôngchỉ 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ếptheo chỉ xét trên dãy k1, ka2, , kGiua–1 (phạm vi tìm kiếm mới bằng khoảngmột nửa phạm vi tìm kiếm cũ)

Trang 27

2

- 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

- 3 Giua

- 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);

Trang 28

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

Trung bình log2 N/2 Giả sử xác xuất các phần tử trong mảng nhậngiá 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 để địnhhướ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ếntí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ờigian 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ácnhu 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ácnhau 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 29

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ácthà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ủathuậ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ảngA[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ênmả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ợpnà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]

} mid = (i+j) / 2;

max = A[j];min = A[i];

max = A[i]; min = A[j];

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 30

min=a[dau]; max=a[dau];

} else { minmax(a,dau,(dau+cuoi)/2,min1,max1);

c) Đánh giá độ phức tạp

Bây giờ ta đánh giá thời gian chạy của thuật toán này Gọi T(n) là số phép so sánh cần thực hiện Không khó khăn thấy rằng, T(n) được xác định bởi quan hệ đệ quy sau.

T(1) = 0, T(2) = 1 T(n) = 2T(n/2) + 2 với n 2

Á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 Neumannnă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ắpxế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

a) Ý tưởng: ần sắp xế ộn” hai dãy đã được sắp thành

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ộnthành dãy D={d1,d2,…,dm+n ợc sắp

Trang 31

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 haiphầ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ểnthà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 32

for(t=i;t<=k2,t++) temp[k+t-i]=a[t]; for(k=k1;t<=k3,k++)a[k]=temp[k];

}

c) Ví dụ

Trang 33

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ậttoá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ânhoạ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ânhoạ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àyphụ 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ọnphầ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 34

- 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 đượcphầ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 35

Partition(i,R); }

c) Ví dụ

Cho dãy số a: 4 9 3 7 5 3 8

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ểulà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 36

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ậttoá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 {

s= 0;

for ( k = i; k<= j; k++) // Tính trọng lượng của dãy

s = s + A[k]

if (s > Max1) Max1 = S }

}

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à

i Do đó, thời gian T(n) = O(n3) Nếu để ý, ta có thể giảm độ phức tạp của thuật toán bằng cách giảm bớt vòng lặptrong cùng (vòng lặp theo k):

k

một cách tóm tắt như sau:

for ( i = 1; i<= n; i++)

for ( j = i; j<= n; j++) {

.Khi đó thuật toán có thể được viết

s = s + A[j]; //Câu lệnh đặc trưng

if (s > max1) max1 = s;

}

Trang 37

1 j i

O(n2 )

n n

j

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

 Thời gian của thuật toán T(n) = O(n 2 ).

i

Cách tiếp cận chia để trị

 Chia: Chia mảng A ra thành hai mảng con với chênh lệch độ dài ít nhất, kí hiệu là AL, AR

 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

}

Các hàm MaxLeftVector, Max RightVector được cài đặt như sau:

void MaxLeftVector(a, i, j);

{ MaxSum = -Maxint;

Sum = 0;

for( k = j;k>= i;k ) {

Sum = Sum + A[k];

MaxSum = Max(Sum,MaxSum);

} Return MaxSum;

}

Tương tự với hàm MaxRightVector là

for (k = i;k<= j;k++)

{ Sum = Sum + A[k];

MaxSum = MaxSum(Sum, MaxSum);

Trang 38

O(log n)

}

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)

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ằmtrê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ơnbao 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 đĩalớ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 Bsang 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 đĩalớ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 39

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 40

Void ThapHaNoi (int n, char a, char c, char b)

{

If (n = = 1) chuyen (1, a, c);

Else {

} Return;

đĩ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ácchuyể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ầnthự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 thaotá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 1chuyể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 Asang 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 - 1thao tác Tổng cộng ta mất (2n - 1 - 1) + (2n - 1 - 1) + 1 = 2 * 2n - 1 - 1 = 2n - 1 thao tácchuyể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ắcnhư 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ầnchuyể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

Ngày đăng: 21/02/2019, 09:30

Nguồn tham khảo

Tài liệu tham khảo Loại Chi tiết
[1] Nguyễn Xuân Huy, Sáng tạo trong thuật toán và lập trình, NXB Thông tin và truyền thông 2011 Sách, tạp chí
Tiêu đề: Sáng tạo trong thuật toán và lập trình
Nhà XB: NXB Thông tin và truyền thông 2011
[2] Lê Minh Hoàng, Giải thuật &amp; lập trình, Đại học sư Phạm Hà Nội, 1999 - 2002 Sách, tạp chí
Tiêu đề: Giải thuật & lập trình
[3] Đỗ Xuân Lôi, Cấu trúc dữ liệu và giải thuật, NXB Thống kê, 1999.Tiếng Anh Sách, tạp chí
Tiêu đề: Cấu trúc dữ liệu và giải thuật
Nhà XB: NXB Thống kê
[4] Anany Levitin, Introduction to the Design and Analysis of Algorithms, Villanova University, 2002 Sách, tạp chí
Tiêu đề: Introduction to the Design and Analysis of Algorithms
[5] G. Brassard, Paul Bratley, Fundamental of Algorithmics, Prentice-Hall, 1996 Sách, tạp chí
Tiêu đề: Fundamental of Algorithmics

TỪ KHÓA LIÊN QUAN

TÀI LIỆU CÙNG NGƯỜI DÙNG

TÀI LIỆU LIÊN QUAN

🧩 Sản phẩm bạn có thể quan tâm

w