Tải bản đầy đủ (.pdf) (91 trang)

Bài giảng Thiết kế và đánh giá thuật toán: Phần 1 - ĐH Sư Phạm Kỹ Thuật Nam Định

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (937.13 KB, 91 trang )

BỘ LAO ĐỘNG - THƯƠNG BINH VÀ XÃ HỘI
TRƯỜNG ĐẠI HC S PHM K THUT NAM NH

tập bài giảng

thiết kế và đánh giá
thuật toán
M S TB2012-02-06

BIấN SON: PHM CAO HO

NAM ĐỊNH 2012


Lời nói đầu
Xây dựng một thuật tốn tốt để giải bài bài toán đã cho là bước quan trọng nhất
trong việc giải bài tốn đó trên máy tính điện tử. Để có được một thuật một thuật tốn
tốt cần phải nắm vững các kỹ thuật thiết kế, phân tích, đánh giá thuật toán cùng các
thuật toán cơ bản cho một số lớp bài bài tốn điển hình.
Tài liệu Thiết kế và đánh giá thuật toán được biên soạn nhằm phục vụ công việc
giảng dạy và học tập môn học Thiết kế và đánh giá thuật toán của ngành học Khoa học
máy tính thuộc khoa Cơng nghệ thơng tin trường Đại học sư phạm kỹ thuật Nam Định.
Tài liệu cũng rất cần thiết cho tất cả các ngành học thuộc khoa Cơng nghệ thơng tin.
Nội dung của tài liệu trình bày các kỹ thuật thiết kế thuật tốn thơng dụng và cơ
sở phân tích, đánh giá độ phức tạp của thuật toán. Tài liệu gồm 6 chương:
Chương 1: Tổng quan về thiết kế và đánh giá thuật toán
Chương 2: Kỹ thuật chia để trị
Chương 3: Kỹ thuật tham lam
Chương 4: Kỹ thuật quay lui
Chương 5: Kỹ thuật nhánh và cận
Chương 6: Kỹ thuật quy hoạch động


Trong từng chương các vấn đề đưa ra đều được minh họa bằng các ví dụ. Cuối
mỗi chương đều có một hệ thống các bài tập nhằm giúp người học củng cố các kiến
thức đã được học đồng thời rèn luyện khả năng vận dụng các kiến thức để giải quyết
một số bài toán trong thực tế. Với các bài tập khó tài liệu đã đưa ra hướng dẫn giải để
giúp người học thuận lợi trong qua trình nghiên cứu và giải quyết các bài tập. Cuối tài
liệu là phần cài đặt một số thuật toán đã được thiết kế nhằm giúp người học thuận lợi
hơn trong việc nắm bắt và vận dụng các kỹ thuật thiết kế thuật toán.
Tài liệu được biên soạn theo chương trình mơn học Thiết kế và đánh giá thuật
tốn của ngành học Khoa học máy tính thuộc khoa Cơng nghệ thông tin trường Đại học
sư phạm kỹ thuật Nam Định. Nội dung tài liệu được biên soạn dựa trên cơ sở nội dung
các bài giảng của tác giả trong một số năm qua tại khoa Công nghệ thông tin trường
Đại học sư phạm kỹ thuật Nam Định.
Trong quá trình biên soạn, tác giả đã nhận được nhiều ý kiến đóng góp cùng với
sự động viên, khích lệ của bạn bè đồng nghiệp trong khoa và trong trường. Tác giả xin
được tỏ lịng cảm ơn với những ý kiến đóng góp và động viên khích lệ này.
i


Với lần biên soạn đầu tiên, mặc dù đã hết sức cố gắng song chắc chắn tài liệu
không thể tránh khỏi những thiếu sót. Rất mong nhận được các ý kiến đóng góp để tài
liệu ngày càng hồn thiện hơn.
Phạm Cao Hào

ii


MỤC LỤC
Chương 1. Tổng quan về thiết kế và đánh giá thuật toán

1


1.1. Thuật toán

1

1.1.1. Khái niệm thuật toán

1

1.1.2. Các đặc trưng cơ bản của thuật toán

1

1.2. Sự cần thiết của thiết kế và đánh giá thuât toán

2

1.3. Diễn tả thuật toán

3

1.4. Thiết kế thuật toán

7

1.4.1. Modul hoá và thiết kế từ trên xuống

7

1.4.2. Phương pháp là mịn dần (tinh chỉnh từng bước)


7

1.4.3. Một số kỹ thuật thiết kế

8

1.5. Phân tích thuật tốn

9

1.5.1. Thời gian thực hiên thuật tốn

9

1.5.2. Độ phức tạp tính tốn của thuật tốn

10

1.5.3. Ðộ phức tạp của chương trình có gọi chương trình con khơng đệ qui

16

1.5.4. Phân tích các thuật tốn đệ quy

17

1) Thành lập phương trình truy hồi

18


2) Giải phương trình truy hồi

19

Bài tập chương 1.

31

Chương 2. Kỹ thuật chia để trị

37

2.1 Nội dung kỹ thuật

37

2.2. Các ví dụ áp dụng

37

2.2.1. Tìm min và max

37

2.2.2. Một số thuật toán sắp xếp

40

1) Sắp xếp nhanh


40

2) Sắp xếp trộn

44

2.2.3. Tìm kiếm nhị phân

51

2.2.4. Nhân các số nguyên lớn

53

Bài tập chương 2.

57

Chương 3. Kỹ thuật tham lam

62

3.1. Nội dung kỹ thuật

62

3.1.1. Bài toán tối ưu tổ hợp

62


3.1.2. Nội dung kỹ thuật tham lam

62

3.2. Các ví dụ áp dụng

62
iii


3.2.1. Bài toán người giao hàng

62

3.2.2. Bài toán chiếc ba lơ

65

3.2.3. Bài tốn tơ màu bản đồ

70

3.2.4. Tìm cây khung nhỏ nhất

74

3.2.5. Tìm đường đi ngắn nhất

77


3.2.6. Bài tốn phân công công việc

79

Bài tập chương 3.

84

Chương 4. Kỹ thuật quay lui

86

4.1. Nội dung kỹ thuật

86

4.2. Các ví dụ áp dụng

87

4.2.1. Đưa ra các dãy nhị phân độ dài n

87

4.2.2. Đưa ra các hoán vị của n số nguyên

88

4.2.3. Đưa ra các tập con của tập gồm n số nguyên


90

4.2.4. Bài tốn xếp hậu

92

4.2.5. Tìm đường đi trên đồ thị

94

4.2.6. Bài toán ngựa đi tuần

99

Bài tập chương 4

104

Chương 5. Kỹ thuật nhánh và cận

111

5.1. Nội dung kỹ thuật

111

5.2. Các ví dụ áp dụng

114


5.2.1. Bài toán người du lịch

114

5.2.2. Bài toán chiếc ba lô

128

Bài tập chương 5.

133

Chương 6. Kỹ thuật quy hoạch động

137

6.1. Nội dung kỹ thuật

137

6.2. Các ví dụ áp dụng

140

6.2.1. Tính số tổ hợp

140

6.2.2. Bài tốn nhân nhiều ma trận


143

6.2.3. Bài tốn chiếc ba lơ

149

6.2.4. Xâu con chung dài nhất

154

Bài tập chương 6.

164

Phụ lục

171

Tài liệu tham khảo

195

iv


v


Chƣơng 1

TỔNG QUAN VỀ THIẾT KẾ VÀ ĐÁNH GIÁ THUẬT TỐN
1.1. Thuật tốn
1.1.1. Khái niệm thuật tốn
Thuật tốn (Algorithm) đã được biết đến từ rất lâu. Đầu tiên thuật toán được
hiểu như là các qui tắc thực hiện các phép tính số học với các con số được viết trong
hệ thập phân. Cùng với sự phát triển của máy tính, khái niệm thuật toán được hiểu
theo nghĩa rộng hơn. Khái niệm thuật tốn được định nghĩa một cách hình thức
chính xác thông qua máy Turing. ở đây chúng ta sẽ xem xét khái niệm thuật toán
một cách trực quan.
Thuật toán (hay giải thuật, thuật giải) là một khái niệm cơ sở của tin học.
Mỗi bài toán trong thực tế bao gồm hai phần:
- Input: Các đại lượng cho trước (đại lượng vào)
- Output: Các đại lượng cần tìm (đại lượng ra)
Như vậy việc giải bài toán là việc xác định tường minh output theo input
bằng một q trình có thể thực hiện một cách hiệu quả. Đó chính là nội dung cơ bản
của lý thuyết tính tốn. Khi cho bài tốn, ta cần tìm ra một dãy hữu hạn các thao tác
đơn giản được sắp xếp theo một trình tự xác định sao cho theo đó, từ input ta sẽ tìm
ra được output theo yêu cầu.
Một cách trực quan thuật toán giải một bài toán là một dãy hữu hạn các chỉ
dẫn (quy tắc, thao tác hay phép toán) hết sức rõ ràng và chính xác được sắp xếp theo
một trình tự xác định để sao cho sau một số hữu hạn lần thực hiên các chỉ dẫn đó thì
biến đổi được input thành output.
1.1.2. Các đặc trƣng cơ bản của thuật tốn
1) Dữ liệu vào
Mỗi thuật tốn có thể có một hoặc nhiều đại lượng vào mà ta thường gọi là
dữ liệu vào
2) Dữ liệu ra
Sau khi thực hiên xong thuật toán, tuỳ theo chức năng mà thuật toán đảm
nhiệm ta có thể thu được một số đại luợng ra mà ta gọi là dữ liệu ra.
3) Tính xác định

Tính xác định của thuật tốn địi hỏi ở chỗ ở mỗi bước các thao tác phải hết
sức rõ ràng, không thể gây nên sự nhập nhằng, lẫn lộn, tuỳ tiện. Nói cách khác trong
cùng một điều kiện hai bộ xử lý (người hoặc máy) thực hiện cùng một bước của
thuật tốn thì phải cho cùng một kết quả.
1


4) Tính dừng
Thuật tốn phải dừng và cho kết quả sau một số hữu hạn bước thực hiện.
5) Tính hiệu quả
Yêu cầu của tính hiệu quả là trong số các thuật toán thực hiện cùng một chức
năng ta cần chọn thuật toán tốt nhất. Tiêu chuẩn tốt ở đây được hiểu là: thuật tốn
thực hiện nhanh, tốn ít thời gian nhất, dùng ít giấy hoặc từ nhớ để lưu trữ các kết
quả trung gian.
6) Tính phổ dụng
Một thuật tốn được xem là có tính phổ dụng cao nếu nó có thể dùng để giải
bất cứ bài toán nào trong một lớp các bài tốn chứ khơng phải là một bài toán cụ
thể.
1.2. Sự cần thiết của thiết kế và đánh giá thuật toán
Xây dựng một thuật toán tốt để giải bài toán đã cho là bước quan trọng nhất
trong việc giải bài tốn đó trên máy tính điện tử. Để có được một thuật tốn tốt cần
phải nắm vững các kỹ thuật thiết kế, phân tích, đánh giá thuật tốn cùng các thuật
toán cơ bản cho một số lớp bài tốn điển hình.
Trong khi giải một bài tốn chúng ta có thể có một số thuật tốn khác nhau,
vấn đề là cần phải đánh giá các thuật tốn đó để lựa chọn một thuật tốn tốt (nhất).
Thơng thường thì ta sẽ căn cứ vào các tiêu chuẩn sau:
(1) Thuật toán đúng đắn.
(2) Thuật toán đơn giản.
(3) Thuật toán thực hiện nhanh.
Với yêu cầu (1), để kiểm tra tính đúng đắn của thuật tốn chúng ta có thể cài

đặt thuật tốn đó và cho thực hiện trên máy với một số bộ dữ liệu mẫu rồi lấy kết
quả thu được so sánh với kết quả đã biết. Thực ra thì cách làm này khơng chắc chắn
bởi vì có thể thuật tốn đúng với tất cả các bộ dữ liệu chúng ta đã thử nhưng lại sai
với một bộ dữ liệu nào đó. Vả lại cách làm này chỉ phát hiện ra thuật tốn sai chứ
chưa chứng minh được là nó đúng. Tính đúng đắn của thuật tốn cần phải được
chứng minh bằng tốn học. Điều này khơng đơn giản và do vậy chúng ta sẽ không
đề cập đến ở đây.
Khi chúng ta viết một chương trình để sử dụng một vài lần thì yêu cầu (2) là
quan trọng nhất. Chúng ta cần một giải thuật dễ viết chương trình để nhanh chóng
có được kết quả , thời gian thực hiện chương trình khơng được đề cao vì dù sao thì
chương trình đó cũng chỉ sử dụng một vài lần mà thơi. Tuy nhiên khi một chương
trình được sử dụng nhiều lần thì thì yêu cầu tiết kiệm thời gian thực hiện chương
2


trình lại rất quan trọng đặc biệt đối với những chương trình mà khi thực hiện cần dữ
liệu nhập lớn do đó yêu cầu (3) sẽ được xem xét một cách kĩ càng. Ta gọi nó là hiệu
quả thời gian thực hiện của thuật tốn.
1.3. Diễn tả thuật tốn
Có nhiều cách diễn tả thuật toán. Người ta thường diễn tả thuật toán bằng
một trong các cách sau:
1) Liệt kê từng buớc
Thuật tốn có thể trình bày dưới dạng ngơn ngữ tự nhiện theo trình tự các
bước thực hiện trong thuật tốn
2) Sơ đồ khối (Lưu đồ)
Dùng các hình vẽ (có qui ước) để diễn tả thuật toán. Lưu đồ cho hình ảnh
trực quan và tổng thể của thuật tốn nên thường được sử dụng.
3) Ngơn ngữ lập trình
Dùng cấu trúc lệnh, dữ liệu của một ngơn ngữ lập trình nào đó để mơ tả.
4) Dạng giả mã

Thuật tốn trình bày dưới dạng văn bản bằng ngôn ngữ tự nhiên tuy dễ hiểu
nhưng khó cài đặt. Dùng một ngơn ngữ lập trình nào đó để diễn tả thì phức tạp, khó
hiểu. Thơng thường thuật tốn cũng được trình bày dưới dạng văn bản và không
ràng buộc nhiều vào cú pháp qui định của ngơn ngữ lập trình, nhưng cũng tn theo
một số qui ước ban đầu- Ta gọi dạng này là dạng giả mã. Tuỳ theo việc định hướng
cài đặt thuật tốn theo ngơn ngữ lập trình nào mà tả fiễn đạt thuật tốn gần với ngơn
ngữ ấy. Trong tài liệu naứy ta trình bày các thuật tốn dưới dạng giả mã của ngơn
ngữ lập trình C. Dưới đây là một số quy ước của ngơn ngữ lập trình C:
* Các ký tự
- Bộ chữ cái: 26 chữ cái
- 10 chữ số thập phân.
- Các dấu phép toán số học.
- Các dấu phép toán quan hệ.
...
* Các phép toán số học và logic
Các từ sau xem như là các từ khoá : if, else, case, for, while , do while
...
3


* Các phép toán số học và logic
- Các phép toán số học : +, -, *, /, %.
- Các phép toán Logic : &&, ||, !
* Lệnh gán: biến=biểu thức;
* Khối lệnh:
{
A1;
A2;
...
An;

}
* Cấu trúc rẽ nhánh if
Toán tử if cho phép lựa chọn chạy theo một trong hai nhánh tuỳ thuộc vào sự
bằng không và khác không của biểu thức. Nó có hai cách viết sau :
if (biểu thức)

if (biểu thức)

khối lệnh 1

khối lệnh 1

/* Dạng một */

else
khối lệnh 2
/* Dạng hai */

Sự lồng nhau của các toán tử if :
C cho phép sử dụng các toán tử if lồng nhau có nghĩa là trong các khối lệnh
(1 và 2) ở trên có thể chứa các tốn tử if - else khác. Trong trường hợp này, nếu
không sử dụng các dấu đóng mở ngoặc cho các khối thì sẽ có thể nhầm lẫn giữa các
if-else. Chú ý là máy sẽ gắn tốn tử else với tốn tử if khơng có else gần nhất.
* Cấu trúc rẽ nhánh - tốn tử switch:
switch (biểu thức nguyên)
{
case n1
khối lệnh 1
case n2
4



khối lệnh 2
.......
case nk
khối lệnh k
[ default
khối lệnh k+1]
}
Với ni là các số nguyên, hằng ký tự hoặc biểu thức hằng. Các ni cần có giá
trị khác nhau. Đoạn chương trình nằm giữa các dấu { } gọi là thân của tốn tử
switch.
default là một thành phần khơng bắt buộc phải có trong thân của switch.
* Cấu trúc lặp với toán tử while :
Toán tử while dùng để xây dựng chu trình lặp dạng :
while (biểu thức)
Lệnh hoặc khối lệnh;
Như vậy toán tử while gồm một biểu thức và thân chu trình. Thân chu trình
có thể là một lệnh hoặc một khối lệnh.
Hoạt động của chu trình như sau :
Máy xác định giá trị của biểu thức, tuỳ thuộc giá trị của nó máy sẽ chọn cách
thực hiện như sau :
Nếu biểu thức có giá trị 0 (biểu thức sai), máy sẽ ra khỏi chu trình và chuyển
tới thực hiện câu lệnh tiếp sau chu trình trong chương trình.
Nếu biểu thức có giá trị khác khơng (biểu thức đúng), máy sẽ thực hiện lệnh
hoặc khối lệnh trong thân của while. Khi máy thực hiện xong khối lệnh này nó lại
thực hiện xác định lại giá trị biểu thức rồi làm tiếp các bước như trên.
* Cấu trúc lặp với toán tử for :
Toán tử for dùng để xây dựng cấu trúc lặp có dạng sau :
for (biểu thức 1; biểu thức 2; biểu thức 3)

Lệnh hoặc khối lệnh ;
Toán tử for gồm ba biểu thức và thân for. Thân for là một câu lệnh hoặc một
khối lệnh viết sau từ khoá for. Bất kỳ biểu thức nào trong ba biểu thức trên có thể
5


vắng mặt nhưng phải giữ dấu ;
Thông thường biểu thức 1 là toán tử gán để tạo giá trị ban đầu cho biến điều
khiển, biểu thức 2 là một quan hệ logic biểu thị điều kiện để tiếp tục chu trình, biểu
thức ba là một tốn tử gán dùng để thay đổi giá trị biến điều khiển.
Hoạt động của toán tử for :
Toán tử for hoạt động theo các bước sau :
Xác định biểu thức 1
Xác định biểu thức 2
Tuỳ thuộc vào tính đúng sai của biểu thức 2 để máy lựa chọn một trong hai
nhánh :
Nếu biểu thức 2 có giá trị 0 (sai), máy sẽ ra khỏi for và chuyển tới câu lệnh
sau thân for.
Nếu biểu thức 2 có giá trị khác 0 (đúng), máy sẽ thực hiện các câu lệnh trong
thân for.
Tính biểu thức 3, sau đó quay lại bước 2 để bắt đầu một vòng mới của chu
trình.
* Cấu trúc do-while
Khác với các tốn tử while và for, việc kiểm tra điều kiện kết thúc đặt ở đầu
chu trình, trong chu trình do while việc kiểm tra điều kiện kết thúc đặt cuối chu
trình. Như vậy thân của chu trình bao giờ cũng được thực hiện ít nhất một lần.
do
Lệnh hoặc khối lệnh;
while (biểu thức) ;
Hoạt động của chu trình như sau :

Máy thực hiện các lệnh trong thân chu trình.
Khi thực hiện xong tất cả các lệnh trong thân của chu trình, máy sẽ xác định
giá trị của biểu thức sau từ khoá while rồi quyết định thực hiện như sau :
Nếu biểu thức đúng (khác 0) máy sẽ thực hiện lặp lại khối lệnh của chu trình
lần thứ hai rồi thực hiện kiểm tra lại biểu thức như trên.
Nếu biểu thức sai (bằng 0) máy sẽ kết thúc chu trình và chuyển tới thực hiện
lệnh đứng sau toán tử while.
6


Những điều lưu ý với toán tử while ở trên hoàn toàn đúng với do while.
* Câu lệnh break
Câu lệnh break cho phép ra khỏi các chu trình với các tốn tử for, while, do
while và switch. Khi có nhiều chu trình lồng nhau, câu lệnh break sẽ đưa máy ra
khỏi chu trình bên trong nhất chứa nó khơng cần điều kiện gì.
* Câu lệnh continue :
Trái với câu lệnh break, lệnh continue dùng để bắt đầu một vòng mới của
chu trình chứa nó. Trong while và do while, lệnh continue chuyển điều khiển về
thực hiện ngay phần kiểm tra, còn trong for điều khiển được chuyển về bước khởi
đầu lại (tức là bước : tính biểu thức 3, sau đó quay lại bước 2 để bắt đầu một vịng
mới của chu trình). Lệnh continue chỉ áp dụng cho chu trình chứ khơng áp dụng cho
switch.
1.4. Thiết kế thuật tốn
1.4.1. Modul hoá và thiết kế từ trên xuống
Các bài toán giải được trên máy tính ngày càng phức tạp và đa dạng. Các
thuật tốn giải chúng ngày càng có quy mơ lớn địi hỏi nhiều thời gian và cơng sức
của nhiều người. Tuy nhiên công việc sẽ đơn giản hơn nếu như ta chia bài toán ra
thành các bài toán nhỏ. Điều đó cũng có nghĩa là nếu coi bài tốn là modul chính thì
cần chia thành các modul con. Đến lượt mình các modul con lại phân rã thành các
modul con thích hợp...

Như vậy việc tổ chức lời giải thể hiện theo một cấu trúc phân cấp. Chiến thuật
giải bài toán như vậy là “chia để trị”, thể hiện chiến thuật đó ta
dùng thiết kế từ trên xuống. Đó là cách nhìn nhận vấn đề một cách tổng quát, đề cập
đến các cơng việc chính, sau đó mới bổ sung dần các chi tiết.
1.4.2. Phƣơng pháp làm mịn dần (tinh chỉnh từng bƣớc)
Đầu tiên thuật tốn được trình bày dưới dạng ngơn ngữ tự nhiên thể hiện ý
chính cơng việc. Các bước sau sẽ chi tiết hóa dần tương ứng với các cơng việc nhỏ
hơn. Đó là các bước làm mịn dần đặc tả thuật toán và hướng về ngơn ngữ lập trình
mà ta dự định cài đặt.
Q trình thiết kế và phát triển thuật toán sẽ thể hiện dần từ ngôn ngữ tự
nhiên, sang ngôn ngữ mã giả rồi đến ngơn ngữ lập trình, và đi từ mức “làm cái
gì“đến “làm như thế nào”.
7


1.4.3. Một số kỹ thuật thiết kế
Trên cơ sở lý thuyết máy Turing, người ta chia được các bài toán thành 2 lớp
không giao nhau : Lớp giải được bằng thuật tốn, và lớp khơng giải được bằng thuật
tốn.
Đối với lớp các bài toán giải được bằng thuật toán, dựa vào các đặc trưng của
quá trình thiết kế của thuật tốn, ta có thể chỉ ra một số các kỹ thuật thiết kế thuật
toán cơ bản sau đây :
1) Kỹ thuật chia để trị
Chia bài toán thành các bài toán đủ nhỏ, giải các bài toán nhỏ rồi tổng hợp kết
quả lại .
2) Kỹ thuật quay lui
Tìm kiếm theo ưu tiên.
Đối với mỗi bước thuật toán, ưu tiên theo độ rộng hay chiều sâu để tìm kiếm.
Chẳng hạn thuật tốn giải bài toán 8 hậu.
3) Kỹ thuật tham lam

Ý tưởng là : Xác định trật tự xử lý để có lợi nhất, Sắp xếp dữ liệu theo trật tự
đó, rồi xử lý dữ liệu theo trật tự đã nêu. Công sức bỏ ra là tìm ra trật tự đó. Chẳng
hạn thuật tốn tìm cây khung nhỏ nhất.
4) Kỹ thuật nhánh và cận
Trong quá trình tìm kiếm lời giải, ta phân hoạch tập các phương án của bài
toán ra thành hai hay nhiều tập con được biểu diễn như là các nút của cây tìm kiếm
và cố gắng bằng phép đánh giá cận cho các nút, tìm cách loại bỏ các nhánh của cây
mà ta biết chắc không chứa phương án tối ưu.
5) Kỹ thuật Quy hoạch động
Kỹ thuật quy hoạch động dựa vào một nguyên lý, gọi là nguyên lý tối ưu của
Bellman :
“ Nếu lời giải của bài toán là tối ưu thì lời giải của các bài tốn con cũng tối
ưu ”.
Kỹ thuật này tổ chức tìm kiếm lời giải theo kiểu từ dưới lên. Xuất phát từ các
bài toán con nhỏ và đơn giản nhất, tổ hợp các lời giải của chúng để có lời giải của
bài toán con lớn hơn...và cứ như thế cuối cùng được lời giải của bài toán ban đầu.
8


1.5. Phân tích tht tốn
Trong khi giải một bài tốn chúng ta có thể có một số thuật tốn khác nhau,
vấn đề là cần phải đánh giá các thuật toán đó để lựa chọn một thuật tốn tốt (nhất).
Thơng thường thì ta sẽ căn cứ vào các tiêu chuẩn sau:
- Thuật toán đơn giản
- Thuật toán thực hiện nhanh
Khi chúng ta viết một chương trình để sử dụng một vài lần thì u cầu thuật
tốn đơn giản là quan trọng. Chúng ta cần một giải thuật dễ viết chương trình để
nhanh chóng có được kết quả, thời gian thực hiện chương trình khơng được đề cao
vì dù sao thì chương trình đó cũng chỉ sử dụng một vài lần mà thơi.
Tuy nhiên khi một chương trình được sử dụng nhiều lần thì yêu cầu tiết kiệm

thời gian thực hiện chương trình lại rất quan trọng đặc biệt đối với những chương
trình mà khi thực hiện cần dữ liệu nhập lớn do đó u cầu thuật tốn thực hiện
nhanh sẽ được xem xét một cách kĩ càng. Ta gọi nó là hiệu quả thời gian thực hiện
của thuật toán. Hơn nữa khối lượng dữ liệu lớn mà dung lượng bộ nhớ lại có giới
hạn thì khơng thể bỏ qua u cầu về tiết kiệm bộ nhớ được. Tuy nhiên cân đối giữa
yêu cầu về thời gian và không gian không mấy khi có được một giải phấp trọn vẹn.
Sau đây ta sẽ chỉ chú ý đến việc phân tích thời gian thực hiện thuật tốn.
1.5.1. Thêi gian thùc hiƯn tht tốn
Một phương pháp để xác định hiệu quả thời gian thực hiện của một thuật
tốn là lập trình nó và đo lường thời gian thực hiện của hoạt động trên một máy
tính xác định đối với tập hợp được chọn lọc các dữ liệu vào.
Thời gian thực hiện không chỉ phụ thuộc vào thuật tốn mà cịn phụ thuộc
vào tập các chỉ thị của máy tính, chất lượng của máy tính và kĩ xảo của người lập
trình. Sự thi hành cũng có thể điều chỉnh để thực hiện tốt trên tập đặc biệt các dữ
liệu vào được chọn. Ðể vượt qua các trở ngại này, các nhà khoa học máy tính đã
chấp nhận tính phức tạp của thời gian được tiếp cận như một sự đo lường cơ bản sự
thực thi của thuật tốn. Thuật ngữ tính hiệu quả sẽ đề cập đến sự đo lường này và
đặc biệt đối với sự phức tạp thời gian trong trường hợp xấu nhất.
Nói chung thì thời gian thực hiện thuật tốn khơng chỉ phụ thuộc vào kích
thước mà cịn phụ thuộc vào tính chất của dữ liệu vào. Nghĩa là dữ liệu vào có cùng
kích thước nhưng thời gian thực hiện giải thuật có thể khác nhau. Chẳng hạn
chương trình sắp xếp dãy số nguyên tăng dần, khi ta cho vào dãy có thứ tự thì thời
gian thực hiện khác với khi ta cho vào dãy chưa có thứ tự, hoặc khi ta cho vào một
9


dãy đã có thứ tự tăng thì thời gian thực hiện cũng khác so với khi ta cho vào một
dãy đã có thứ tự giảm.
Vì vậy thường ta coi T(n) là thời gian thực hiện chương trình trong trường
hợp xấu nhất trên dữ liệu vào có kích thước n, tức là: T(n) là thời gian lớn nhất để

thực hiện chương trình đối với mọi dữ liệu vào có cùng kích thước n.
Để đánh giá thời gian thực hiện thuật toán người ta tìm cách đánh giá độc lập
với các yếu tố bên ngồi như máy tính hay các yếu tố liên quan đến máy tính. Cách
đánh giá như vậy dẫn tới khái niệm về cấp độ lớn của thời gian thực hiện thuật tốn
hay độ phức tạp tính tốn của thuật tốn.
1.5.2. Độ phức tạp tính tốn của thuật tốn
Nếu thời gian thực hiện một thuật toán là T(n) =cn2 (với c là hằng số, n là
kích thước dữ liệu đầu vào) thì ta nói: Độ phức tạp tính tốn của thuật tốn này có
cấp là n2 (hay cấp độ lớn của thời gian thực hiện thuật toán là n2) và ta ký hiệu:
T(n) = O(n2) (ký hiệu chữ O lớn)
Một cách tổng quát có thể định nghĩa:
Một hàm f(n) được xác định là O(g(n)) và viết là f(n) =O(g(n)) và được gọi
là cấp g(n) nếu tồn tại các hằng số c và n0 sao cho:
f(n) ≤ cg(n) khi n ≥ n0
nghĩa là f(n) bị chặn trên bởi một hằng số nhân với g(n), với mọi giá trị của n tăng
từ một điểm nào đó. Thơng thường các hàm thể hiện độ phức tạp tính tốn của thuật
tốn có dạng :
log2n, n, nlog2, n2, n3, 2n, n!, nn
Sau đây là bảng giá trị của một số hàm đó
Log2n

n

nlog2n

n2

n3

2n


0

1

0

1

1

2

1

2

2

4

8

4

2

4

8


16

64

16

3

8

24

64

512

256

4

16

64

256

40963

65536


5

32

160

1024

32768

2.147.483.648

Hình 1.1. Bảng giá trị của một số hàm số
Các hàm như 2n , n!, nn được gọi là hàm loại mũ. Một thuật toán mà thời gian
10


thực hiện của nó có cấp là các hàm loại mũ thì tốc độ rất chậm. Các hàm như n3 , n2,
nlog2n, n, log2n được gọi là các hàm loại đa thức. Một thuật tốn mà thời gian thực
hiện có độ phức tạp là một hàm đa thức thì chấp nhận được tức là có thể cài đặt để
thực hiện, cịn các thuật tốn có độ phức tạp hàm mũ thì phải tìm cách cải tiến thuật
tốn.
Các quy tắc xác định độ phức tạp của thuật toán:
Xác định độ phức tạp tính tốn của một thuật tốn bất kỳ có thể dẫn tới
những bài toán phức tạp. Tuy nhiên, trong thực tế, đối với một số thuật tốn ta cũng
có thể phân tích được bằng một số quy tắc đơn giản.
* Quy tắc tổng:
Giả sử T1(n) và T2(n) là độ phức tạp tính tốn của hai đoạn chương trình P1
và P2 mà T1(n) = O(f(n)); T2(n) = O(g(n)) thì độ phức tạp tính tốn khi thực hiện P1

và tiếp theo là P2 sẽ là: T1(n) + T2(n) = O(max (f(n),g(n))
Chứng minh:
Vì T1(n) = O(f(n)); T2(n) = O(g(n)) nên theo định nghĩa tồn tại các hằng số
dương c1 , n1 và c2 , n2 sao cho:
T1(n) ≤ c1  f(n), với mọi n > n1
T2(n) ≤ c2  g(n) với mọi n > n2.
Chọn c = c1 + c2; n0 = max {n1, n2}.
Khi đó: T1(n) + T2(n)  c1  f(n) + c2  g(n)  c  max(f(n), g(n)) , với mọi n
> n0 .
Do vậy: O(f(n)) + O(g(n)) = O(max(f(n), g(n))).
Ví dụ 1.1.
Trong một chương trình có 3 bước thực hiện mà độ phức tạp tính tốn từng
bước lần lượt là O(n2), O(n3) và O(nlog2n) thì độ phức tạp tính tốn hai bước đầu là
O(max (n2, n3) = O(n3). Độ phức tạp tính tốn của chương trình sẽ là: O(max(n3,
nlog2n)) = O(n3).
Nhận xét:
Từ quy tắc này có thể nhận thấy rằng: nếu g(n) ≤ f(n) với mọi n ≥ n0 thì:
O(f(n)+g(n)) = O(f(n)).
Chẳng hạn: O(n4+n2) = O(n4)
O(n + log2n) = O(n).
11


* Quy tắc nhân:
Giả sử T1(n) và T2(n) là độ phức tạp tính tốn của hai đoạn chương trình P1
và P2 mà T1(n) = O(f(n)); T2(n) = O(g(n)) thì độ phức tạp tính tốn khi P1 và P2 lồng
nhau sẽ là: T1(n).T2(n) = O(f(n).g(n))
Chứng minh:
Ta có: T1(n) = O(f(n)), T2(n) = O(g(n) theo định nghĩa tồn tại các hằng số
dương c1 , n1và c2 , n2 sao cho:

T1(n) ≤ c1  f(n), với mọi n > n1
T2(n) ≤ c2  g(n) với mọi n > n2.
Chọn c = c1 * c2; n0 = max {n1, n2}.
Khi đó: T1(n).T2(n)  c1  f(n)  c2  g(n) = c  (f(n) g(n)).
Do vậy: T1(n).T2(n) = O(f(n).g(n)).
Ví dụ 1.2.
Câu lệnh gán : x = x+1 có thời gian thực hiện bằng c (hằng số) nên được
đánh giá là O(1).
Câu lệnh: for ( i=1; i<=n; i++) x =x+1;
Có độ phức tạp tính tốn O(n.1) = O(n)
Câu lệnh : for ( i= 1; i<=n; i++)
for ( j= 1; j<=n; j++) x =x+1;
có độ phức tạp tính tốn được đánh giá là: O(n.n)=O(n2)
Nhận xét:
Từ quy tắc nhân ta sẽ có:
O(cf(n)) = O(f(n))
Chẳng hạn O(n2/2) = O(n2)
Chú ý :
Dựa vào những nhận xét đã nêu ở trên về quy tắc khi đánh giá độ phức tạp
tính toán của thuật toán ta chỉ cần chú ý tới các bước tương tự với một phép toán mà
ta gọi là phép tốn tích cực. Đó là một phép tốn thuộc thuật tốn mà thời gian thực
hiện nó khơng ít hơn thời gian thực hiện các phép toán khác (tất nhiên phép tốn
tích cực khơng phải là duy nhất) hay nói một cách khác: số lần thực hiện nó khơng
kém gì các phép tốn khác.
12


Ví dụ 1.3.
Giải thuật tính giá trị của ex theo công thức gần đúng:
ex = 1 + x/1! +x2/2! + …+ xn/n! với x và n cho trước

EXP1();
{
scanf(x) ;
S =1;
for (i=1;i<=n;i++)
{
p =1;
for(j=1;j<=i;j++) p=p*x/j;
S = S +p;
}
}
Ta có thể coi phép tốn tích cực ở đây là phép: p = p*x/j
Ta thấy nó được thực hiện: 1 +2+…+ n = n(n+1)/2 lần
Vậy độ phức tạp tính tốn của thuật tốn này được đánh giá là T(n) = O(n2)
Thuật tốn có thể được viết theo một cách khác:
EXP2()
{
scanf(x);
S =1;
P =1;
for (i=1;i<=n;i++)
{
p =p*x/i;
S =S + p;
}
}

13



Bây giờ độ phức tạp tính tốn lại là: T(n) = O(n). Vì phép gán p=p*x/i chỉ thực
hiện n lần.
Ví dụ 1.4.
Thuật toán sắp xếp kiểu nổi bọt
void Bubble(a)
{
for(i=1;i<=n-1; i++)
for(j=n; j>=i+1; j--)
if (a[j-1]>a[j])
{
tg:= a[j-1];
a[j-1] := a[j];
a[j]:= tg;
}
}
Trong thuật toán ta coi phép so sánh (a[j-1]>a[j]) là phép tốn tích cực. Phép
tốn này nằm trong vịng lặp for(j=n; j>=i+1; j--) nên nó được thực hiện (n-i) lần.
Vịng lặp for(j=n; j>=i+1; j--) nằm trong vòng lặp for(i=1;i<=n-1; i++) thực hiện
(n-1) lần. Do vậy số lần thực hiện phép tốn tích cực (a[j-1]>a[j]) sẽ là:
n 1

( n  i ) 
i 1

n( n  1 )
2

Nên độ phức tạp tính tốn của thuật toán là O(n2).
Chú ý:
Ta biết rằng thời gian thực hiện thuật tốn 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 dữ liệu nhập nữa. Chẳng hạn,
khi xếp tăng dần một dãy các số nguyên mà dãy các so nguyên đĩ đã có sẵn thứ tự
tăng dần, hoặc ngược lại, hoặc ngẫu nhiên. Lúc đó khi phân tích thời gian thực hiện
thuật tốn ta sẽ phải xét tới: đối với mọi dữ liệu vào có kích thước n thì T(n) trong
trường hợp tốt nhất, xấu nhất là như thế nào? T(n) trung bình? Việc xác định T(n)
trung bình thường khó và phức tạp địi hỏi những cơng cụ tốn học đặc biệt, hơn
nữa việc tính trung bình có thể có nhiều cách quan niệm khác nhau. Trong trường
hợp T(n) khó xác định người ta thường đánh giá thuật toán qua giá trị xấu nhất của
T(n)
14


VÝ dơ 1.5.
timkiem(v)
/*Cho vectơ V có n phần tử, thuật tốn này thực hiện tìm trong V một phần tử có
giá trị bằng X cho trước. Nếu tìm thấy trả về chỉ số của phần tử đó, nếu khơng tìm
thấy trả về giá trị 0*/
i =1;
while ((V[i] !=X)&& (i<= n)) i= i+1;
if (i<=n) return(i);
else return(0);
Coi phép tốn tích cực ở đây là phép so sánh V[i] với X. Có thể thấy số lần phép
toán thực hiện phụ thuộc vào chỉ số i mà V[i] = X. Trường hợp thuận lợi nhất xảy ra
khi X bằng V[1]: một lần thực hiện.
Trường hợp xấu nhất khi X bằng V[n] hoặc khơng tìm thấy: n lần thực hiện
Vậy: Ttốt = O(1) và Txấu = O(n)
Thì ta xác định độ phức tạp tính tốn của thuật toán là O(n)
* Qui tắc tổng quát để phân tích một chương trình:
Giả sử rằng, các lệnh gán khơng chứa các lời gọi hàm. Khi đó để đánh giá
thời gian thực hiện một chương trình, ta có thể áp dụng một số quy tắc sau

1. Thời gian thực hiện các lệnh đơn: gán, đọc, viết là O(1)
2. Lệnh hợp thành (khối lệnh) : thời gian thực hiện lệnh hợp thành được xác
định bởi luật tổng.
3. Lệnh if : Giả sử thời gian thực hiện các lệnh S1, S2 là O(f(n)) và O(g(n))
tương ứng. Khi đó thời gian thực hiện lệnh if là O(max (f(n), g(n)))
4. Lệnh witch: Lệnh này được đánh giá như lệnh if
5. Lệnh while : Giả sử thời gian thực hiện lệnh S (thân của while) là O(f(n)).
Giả sử g(n) là số tối đa các lần thực hiện lệnh S. Khi đó thời gian thực hiện
lệnh while là O(f(n)g(n)).
6. Lệnh do ...while :Giả sử thời gian thực hiện khối lệnh trong thân do ...
while là O(f(n)). Giả sử g(n) là số lần tối đa các lần thực hiện khối lệnh trong thân
do ... while . Khi đó thời gian thực hiện lệnh do ... while là O(f(n)g(n)).
7. Lệnh for : Lệnh này được đánh giá tương tự như lệnh while.
1.5.3. Ðộ phức tạp của chƣơng trình có gọi chƣơng trình con khơng đệ qui
15


Nếu chúng ta có một chương trình với các chương trình con khơng đệ quy,
để tính thời gian thực hiện của chương trình, trước hết chúng ta tính thời gian thực
hiện của các chương trình con khơng gọi các chương trình con khác. Sau đó chúng
ta tính thời gian thực hiện của các chương trình con chỉ gọi các chương trình con mà
thời gian thực hiện của chúng đã được tính. Chúng ta tiếp tục q trình đánh giá
thời gian thực hiện của mỗi chương trình con sau khi thời gian thực hiện của tất cả
các chương trình con mà nó gọi đã được đánh giá. Cuối cùng ta tính thời gian cho
chương trình chính.
Giả sử ta có một hệ thống các chương trình gọi nhau theo sơ đồ sau:

Hình 1.2. Chương trình gọi chương trình con khơng đẹ quy
Chương trình A gọi hai chương trình con là B và C, chương trình B gọi hai
chương trình con là B1 và B2, chương trình B1 gọi hai chương trình con là B11 và

B12.
Ðể xác định độ phức tạp tính tốn của A, ta thực hiện theo các bước sau:
1. Xác định độ phức tạp tính tốn của C, B2, B11 và B12. Vì các chương trình
con này khơng gọi chương trình con nào cả.
2. Xác định độ phức tạp tính tốn của B1. Vì B1 gọi B11 và B12 mà độ phức
tạp tính tốn của B11 và B12 đã được tính ở bước 1.
3. Xác định độ phức tạp tính tốn của B. Vì B gọi B1 và B2 mà độ phức tạp
tính tốn của B1 đã được tính ở bước 2 và độ phức tạp tính tốn của B2 đã được
tính ở bước 1.
4. Xác định độ phức tạp tính tốn của A. Vì A gọi B và C mà độ phức tạp tính
tốn của B đã được tính ở bước 3 và độ phức tạp tính tốn của C đã được tính ở
bước 1.
Ví dụ 1.6.
Thuật tốn sắp xếp nổi bọt.
Trước hết viết thủ tục Swap để thực hiện việc hồn đổi hai phần tử cho nhau,
sau đó trong thủ tục Bubble, khi cần sẽ gọi đến thủ tục Swap này.
void Swap (x, y)
16


{
temp = x;
x = y;
y = temp;
}
void Bubble (a)
{
for(i=1;i<=n-1; i++)
for(j=n; j>=i+1; j--)
if (a[j-1]>a[j])

Swap(a[j-1], a[j]);
}
Trong cách viết trên, hàm Bubble gọi hàm Swap, do đó để tính độ phức tạp
tính tốn của Bubble, trước hết ta cần tính độ phức tạp tính tốn của Swap. Dễ thấy
độ phức tạp tính tốn của Swap là O(1) vì nó chỉ bao gồm 3 lệnh gán. Do vậy ta có
thể coi phép tốn tích cực là phép so sánh (a[j-1]>a[j]) và khi đó dễ thấy độ phức
tạp tính tốn của thuật tốn là:

1.5.4. Phân tích các thuật tốn đệ quy
Nhiều thuật tốn dựa trên sự phân rã đệ qui một bài toán lớn thành các bài
toán nhỏ, rồi dùng lời giải các bài toán nhỏ để giải bài toán ban đầu. Thời gian chạy
của thuật tốn như thế được xác định bởi kích thước và số lượng các bài toán con và
giá phải trả của sự phân rã. Nên các thuật toán đệ qui có thời gian chạy phụ thuộc
vào thời gian chạy cho các dữ liệu nhập có kích thước nhỏ hơn, điều này được diễn
dịch thành một cơng thức tốn học gọi là cơng thức truy hồi hay phương trình truy
hồi, hệ thức truy hồi. Do đó, để tính độ phức tạp của thuật tốn, ta thường phải giải
các phương trình truy hồi.
Với các thuật tốn có các lời gọi đệ quy, ta khơng thể áp dụng cách tính như vừa
trình bày trong mục 1.3.3 bởi vì một chương trình đệ quy sẽ gọi chính bản thân nó.
Có thể thấy hình ảnh chương trình đệ quy A như sau:

17


Hình 1.3. Chương trình đệ quy A
Với các chương trình đệ quy, trước hết ta cần thành lập các phương trình truy
hồi, sau đó giải phương trình truy hồi, nghiệm của phương trình truy hồi sẽ là thời
gian thực hiện của chương trình đệ quy.
1) Thành lập phƣơng trình truy hồi
Phương trình truy hồi là một phương trình biểu diễn mối liên hệ giữa T(n) và

T(k), trong đó T(n) là thời gian thực hiện chương trình với kích thước dữ liệu nhập
là n, T(k) thời gian thực hiện chương trình với kích thước dữ liệu nhập là k, với k <
n. Ðể thành lập được phương trình truy hồi, ta phải căn cứ vào chương trình đệ quy.
Thơng thường một chương trình đệ quy để giải bài tốn kích thước n, phải có
ít nhất một trường hợp dừng ứng với một n cụ thể và lời gọi đệ quy để giải bài tốn
kích thước k (kĐể thành lập phương trình truy hồi, ta gọi T(n) là thời gian để giải bài tốn
kích thước n, ta có T(k) là thời gian để giải bài tốn kích thước k. Khi dừng, ta phải
xem xét khi đó chương trình làm gì và tốn hết bao nhiêu thời gian, chẳng hạn thời
gian này là c(n). Khi đệ quy chưa dừng thì phải xét xem có bao nhiêu lời gọi đệ quy
với kích thước k ta sẽ có bấy nhiêu T(k). Ngồi ra ta cịn phải xem xét đến thời gian
để phân chia bài toán và tổng hợp các lời giải, chẳng hạn thời gian này là d(n).
Dạng tổng quát của một phương trình truy hồi sẽ là:

Trong đó C(n) là thời gian thực hiện chương trình ứng với trường hợp đệ quy
dừng. F(T(k)) là một đa thức của các T(k), d(n) là thời gian để phân chia bài toán và
tổng hợp các kết quả.
Chú ý:
Trong khi đánh giá độ phức tạp tính tốn của thuật tốn thì với T(
hiểu

n
n
n
là   hoặc  
2
2
2

(Với x là một số thực thì:

18

n
) ta sẽ
2


x là số nguyên lớn nhất nhỏ hơn hoặc bằng x
x là số nguyên nhỏ nhất lớn hơn hoặc bằng x)
Ví dụ 1.7.
Xét hàm tính giai thừa viết bằng thuật toán đệ quy như sau:
int Giai_thua(n)
{
if (n<=1) gt=1;
else gt=n* Giai_thua(n-1);
return(gt);
}
Gọi T(n) là thời gian thực hiện việc tính n giai thừa, thì T(n-1) là thời gian
thực hiện việc tính n-1 giai thừa. Trong trường hợp n <=1 thì chương trình chỉ thực
hiện một lệnh gán gt=1, nên tốn O(1), do đó ta có T(1) = C1. Trong trường hợp n>1
chương trình phải gọi đệ quy Giai_thua(n-1), việc gọi đệ quy này tốn T(n-1), sau
khi có kết quả của việc gọi đệ quy, chương trình phải nhân kết quả đó với n và gán
cho gt. Thời gian để thực hiện phép nhân và phép gán là một hằng C2. Vậy ta có:
T(n) =

nếu n 1
nếu n >1

C1
T(n-1)+C2


Ðây là phương trình truy hồi để tính thời gian thực hiện của chương trình đệ
quy Giai_thua.
2) Giải phƣơng trình truy hồi
Một số phương pháp giải phương trình truy hồi:
* Phương pháp thay thế
Dùng đệ quy để thay thế bất kỳ T(m) với m < n vào phía phải của phương
trình cho đến khi tất cả T(m) với m > 1 được thay thế bởi biểu thức của các T(1)
hoặc T(0). Vì T(1) và T(0) ln là hằng số nên chúng ta có cơng thức của T(n) chứa
các số hạng chỉ liên quan đến n và các hằng số. Từ cơng thức đó ta suy ra T(n).
VÝ dơ 1.8.
Hàm tính n! trong ví dụ 1.7.
int Giai_thua(n)
{
19


×