Giáo trình Toán rời rạc
Trang 1Chương II
CÁC KIẾN THỨC CƠ BẢN.
I Thuật toán:
1 Khái niệm thuật toán và đặc trưng của nó:
Giả sử nhằm đảm bảo vệ sinh an toàn thực phẩm chúng ta muốn đóng gói kẹo dừa trên một dây chuyền tự động thay cho gói kẹo bằng tay như hiện nay Để giải quyết bài toán này, chúng ta phải thiết kế và chế tạo ra một thiết bị cơ - điện tử thực hiện các công đoạn từ khâu chiết khối kẹo còn nóng sang thiết bị đóng gói cho đến khâu đưa hộp kẹo ra khỏi băng chuyền Thiết
bị này được điều khiển tự động bằng máy tính và gồm các thao tác:
Bước 0: Khởi động việc đếm số viên kẹo (sovienkeo:=0) Qua bước 1.
Bước 1: Nhận khối kẹo còn nóng từ chảo nấu Nếu khối kẹo vừa nhận có trọng lượng khác 0
thì qua bước 2, bằng không qua bước 11.
Bước 2: Trọng lượng của khối kẹo khác 0? Đúng: Qua bước 3 – Sai: Qua bước 1.
Bước 3: Đùn ép và chia cắt khối kẹo thành viên thích hợp Qua bước 4
Bước 4: Đưa viên kẹo lên khuôn ép định hình Qua bước 5
Bước 5: Lấy viên kẹo ra khỏi khuôn ép và đặt lên băng giấy đúng vị trí qui định Qua bước
6.
Bước 6: Cắt băng giấy vừa viên kẹo và ép gói theo dạng qui định Qua bước 7.
Bước 7: Đẩy ép viên kẹo vào hộp Qua bước 8.
Bước 8: Đếm số viên kẹo đã đặt vào hộp (sovienkeo:=sovienkeo+1) Nếu số viên kẹo còn
nhỏ hơn 100 thì trở về bước 2, ngược lại qua bước 9.
Bước 9: Đưa hộp kẹo ra khỏi băng chuyền Qua bước 10:
Bước 10: Đặt hộp kẹo rổng vào vị trí cũ Khởi động lại việc đếm số viên kẹo đã đặt vào
hộp Trở về bước 2.
Bước 11: Dừng máy.
Qui trình trên cho phép ta/máy thực hiện chính xác, đúng từng bước một
công việc đã giao – từ lúc bắt đầu (nhận khối kẹo nóng/khởi tạo biến đếm) đến lúc kết thúc công việc (dừng máy) Một số công đoạn có thể sẽ phải lập
đi lập lại nhiều lần Với mỗi khối kẹo đã nhận qui trình cho ra kết quả rõ
ràng là các hộp kẹo Qui trình trên có qui định thứ tự bắt buộc đối với các thao tác Thứ tự đó không thể thay đổi được Mỗi công đoạn trong qui trình trên đều có thể thực hiện một cách tương ứng bởi con người (dĩ nhiên là mất thời gian hơn, nguy hiểm hơn, )
Không phải bài toán nào cũng có thể đưa ra một qui trình giải quyết như vậy Tuy nhiên ví dụ trên cũng đủ dẫn chúng ta đến với khái niệm:
Trang 2 Thuật toán:
Một giải thuật (hay thuật toán) là một dãy hữu hạn, có thứ tự các chỉ thị
nhằm giải quyết một bài toán đảm bảo được 6 tính chất đặc trưng cơ bản sau đây:
o Một giải thuật bắt đầu bằng những chỉ thị nhập dữ liệu đầu vào (inputs) Các dữ liệu nhập này sẽ được xử lí bởi những chỉ thị tiếp theo sau của giải thuật.
o Các qui tắc xử lí (processing rules) nêu ra trong giải thuật phải chính xác và không mơ hồ sao cho có thể thực hiện được đúng các qui tắc đó.
o Mỗi chỉ thị phải đủ căn bản sao cho, về nguyên tắc, có thể hoàn thành bởi con người – và bằng tay – trong hữu hạn thời gian.
o Giải thuật phải đủ tổng quát để giải được tất cả các bài toán có dạng như yêu cầu chứ không chỉ cho một tập đặc biêït các giá trị đầu vào.
o Tổng thời gian dùng để thực hiện mọi bước của giải thuật phải hữu hạn Có thể có một số chỉ thị trong các bước giải sẽ được lập đi lập lại nhiều lần nhưng số lần lập phải hữu hạn.
o Giải thuật phải cho ra kết quả (outputs) Nói cách khác giải thuật phải có tính dừng.
2 Các thao tác và cấu trúc điều khiển cơ sở
Để hiện thực được các thuật toán ta cần thiết kế một số thao tác cơ bản cần thiết cho việc thực thi từng bước của thuật toán trên một máy tính giả định
Các thao thác căn bản:
Thao tác nhập input(x) Thao tác này cho phép xem x như một biến chờ
được nhập dữ liệu từ một thiết bị nhập chuẩn nào đó.
Thao tác xuất output(x) Thao tác này cho phép xuất giá trị hiện tại của
x ra một thiết bị xuất chuẩn nào đó.
Thao tác gán trị x:=y Thao tác này được thực hiện từ vế phải qua vế
trái nhằm gán giá trị hiện tại của y cho x.
Thao tác trả về một
giá trị Return x Thao tác này cho phép trả về giá trị hiện tại của
x cho một thiết bị nào đó.
Các thao tác số học
cơ bản hoặc các tác
vụ căn bản
+ - * div
Các cấu trúc điều khiển:
Để biểu diễn quá trình thực hiện một thuật toán, người ta có thể chỉ mô tả tuần
tự các tác vụ của thuật toán kèm theo các chỉ thị so sánh và các lệnh nhảy (jump hoặc
Trang 3goto) là đủ Chẳng hạn như trong ví dụ trên, ở bước 8 ta có một chỉ thị so sánh
(sovienkeo<100 ?) và một lệnh nhảy (Trở về bước 2 | Qua bước 9). Đó là cách mà các ngôn ngữ lập trình cấp thấp như ASSEMBLY (hoặc thậm chí 1985 ANS BASIC –American National Basic) vẫn dùng Tuy nhiên mô tả thuật toán trong một cấu trúc như vậy làm cho thuật toán trở nên khó đọc, khó hiểu và rất dễ trở nên rối rắm, nhiều nhầm lẫn một khi có quá nhiều lệnh nhảy tới lui trong các bước thực hiện Để làm cho việc mô tả thuật toán trở nên trong sáng hơn người ta thấy rằng trong mọi trường hợp hoàn toàn có thể thay lệnh nhảy (jmp hoặc goto) bằng (và chỉ cần như vậy là đủ) ba cấu trúc điều khiển sau đây:
Cấu trúc tuần tự:
Trong cấu trúc điều khiển này các chỉ thị được mô tả thành một dãy tuần tự và được thực thi đúng như theo thứ tự mà chúng được mô tả Nói cách khác lệnh nhảy đến bước kế tiếp liền kề sau bước đóù là không cần thiết phải nêu ra một cách tường minh Nếu một dãy tuần tự các chỉ thị được xem như một khối thống nhất phải thực hiện đầy đủ, hoàn tất thì chúng phải được đánh đấu ở đầu khối và ở cuối khối, chẳng hạn bằng một cặp từ khoá begin / end
Cấu trúc lựa chọn:
Cấu trúc này có cú pháp như sau:
IF <biểu thức lôgic=1> THEN TacVu1 ELSE TacVu2
Cấu trúc này buộc trước tiên phải kiểm tra một điều kiện <biểu thức lôgic> Nếu giá trị của biểu thức này là đúng thì TacVu1 được thực hiện, bằng không TacVu2 sẽ được thực hiện Cú pháp này cho phép “rẽ nhánh” chương trình, cho phép chỉ TacVu1 hoặc (XOR) TacVu2 được thực hiện tuỳ theo giá trị của <biểu thức lôgic> tại thời điểm chương trình đang được thực thi (in run time)
Ví dụ: IF (Sốlượng >0) THEN
LàmTiếp ELSE
Dừng Cấu trúc lặp:
Cấu trúc này có cú pháp như sau:
WHILE <biểu thức lôgic=1> DO TacVu
Cấu trúc này buộc trước tiên phải kiểm tra một điều kiện <biểu thức lôgic> Nếu giá trị của biểu thức này là đúng thì TacVu được thực hiện Sau khi TacVu thực hiện xong thì <biểu thức lôgic> lại được kiểm tra, nếu <biểu thức logic> còn đúng thì TacVu lại được thực hiện, và v.v cho đến khi việc kiểm tra <biểu thức logic> cho thấy <biểu thức lôgic> không còn đúng thì chấm dứt lập lại TacVu Cấu trúc này cho phép “lập lại” TacVu nhiều lần tuỳ theo giá trị của <biểu thức lôgic> Rõ ràng để đảm bảo tính hữu hạn và tính dừng của chương trình thì trong khi thực hiện TacVu phải có cách làm thay đổi giá trị của <biểu thức lôgic> để chương trình có thể thoát ra khỏi vòng lặp sau một số lần lặp nào đó
Trang 4Ví dụ:
I:=2 WHILE (I 5) DO
Begin
Output (i2) i:=i+1 End
Vòng lặp WHILE trong ví dụ này thực hiện khối chỉ thị được bao trong cặp Begin End bốn lần (đưa ra thiết bị xuất chuẩn các số chính phương 4, 9, 16, 25)
3 Các hình thức biểu diễn thuật toán
o Lưu đồ thuật toán (flow chart) : Dùng các sơ đồ dòng chảy trong đó kết hợp các đầu nối, các kí hiệu khối tác vụ và các mũi tên để biểu diễn quá trình thực hiện của thuật toán
Các kí hiệu khối tác vụ được qui định trong bảng sau:
Biểu diễn tác vụ xử lí của máy tính.
Một xử lí được định nghĩa trước
Gọi thường trình xử lí màn hình
Quyết định rẽ nhánh
So <7
Bắt đầu hoặc kết thúc
2
Kí hiệu hướng dòng chảy
Tính giai thừa của N
Trang 5Ví dụ về lưu đồ giải phương trình bậc nhất dạng ax+b=0 BEGIN
True
END
o Mã giả (pseudo code) : Mã giả dùng một số từ khoá, tương tự như các từ khoá dùng trong một ngôn ngữ lập trình nào đó – chẳng hạn ngôn ngữ Pascal – để mô tả quá trình thực hiện giải thuật Ngoài các từ khoá ứng với các cấu trúc vừa nêu trên các từ khoá tựa–Pascal sẽ dùng trong giáo trình này là:1
Procedure Chỉ định tên của thủ tục/ giải thuật.
FOR i:=giá trị đầu TO giá trị cuối DO Công Việc
Lập lại Công Việc mỗi lần i thay đổi giá trị (từ giá
trị đầu đến Giá Trị cuối), mỗi lần thực hiện xong Công Việc giá trị i tự động tăng lên một đơn vị.
Ví dụ 1: Sau đây là đoạn mã giả tương ứng với lưu đồ giải phương trình dạng
ax+b=0 nói trên
Procedure GiaiPhuongTrinhBacNhat
input(a)
input(b)
Output (x= -b/a)
ELSE
IF (b=0) THEN
Output(Phuong trinh co vo so nghiem) ELSE
Output(Phuong trinh vo nghiem)
Ví dụ 2: Thuật toán tìm phần tử lớn nhất trong một dãy hữu hạn.
1 Chú ý rằng mã giả độc lập đối với mọi ngôn ngữ lập trình, do đó không cần tuân thủ cú pháp nghiêm ngặt như của một ngôn ngữ lập trình nào Mọi chỉ thị nếu cần thiết đều có thể dùng được trong
Trang 6Procedure TimMax(a1,a2,a3, ,an: các số nguyên)
{Thuật toán dùng lính canh}
Max:=a1 {Dùng Max để canh chừng giá trị lớn nhất đang được xem xét}
i:=1
While (i<=n) do
Begin
IF (Max<ai) THEN Max:=ai i:=i+1
End
Return(Max)
Cũng có thể mô tả như sau:
Procedure TimMax(a1,a2,a3, ,an: các số nguyên)
Max:=a1
Return(Max)
o Bảng quyết định (Decision table) : Bảng quyết định thường được các nhà phân tích (analyst) và lập trình viên sử dụng như một sơ đồ để mô tả điều gì xảy ra cho hệ thống hoặc cho chương trình khi gặp một dãy trường hợp ứng với nhiều tình huống/chi tiết cần thực hiện khác nhau Bảng quyết định dựa trên logic “IF THEN” Bảng quyết định chia làm nhiều cột và nhiều dòng Cột đầu tiên để ghi các trường hợp khác nhau có thể xảy ra, các cột còn lại để ghi các xử lý đối với mỗi tình huống/chi tiết Ví dụ sau đây sẽ làm rõ ý tưởng đó:
Bảng quyết định dùng để tính lương cho các dạng công nhân viên khác nhau:
Kiểu nhân viên
Qui tắc tính lương
Lương căn bản
Phụ cấp 50%
Phụ cấp 35%
Thừa
Thanh toán theo
Thanh toán cuối
Một bảng quyết định như vậy ứng với một loạt các cấu trúc IF THEN lồng nhau Bảng quyết định ít cho ta một ý niệm đầy đủ về cấu trúc của giải thuật, tuy nhiên lại cho ta một cái nhìn bao quát, không sót các trường hợp có thể xảy ra
Trang 74 Độ phức tạp của thuật toán:
Trong chương I chúng ta đã có nghiên cứu khái niệm thời gian chạy chương trình (hay tốc độ tăng của hàm) Trong nghiên cứu đó chúng ta đã chú ý đến tốc độ thực hiện một chương trình Một chương trình máy tính là một hiện thực cụ thể của một thuật toán nào đó Vấn đề đặt ra khi hiện thực thuật toán thành chương trình là thuật toán đó có hiệu quả không? Và hiệu quả được phân tích như thế nào? Nói chung người ta đánh giá tính hiệu quả của thuật toán dựa trên hai tài nguyên quan trọng nhất của máy tính mà thuật toán yêu cầu được cung cấp để thực hiện bài toán:
a) Thời gian cần thiết để giải một bài toán có một kích thước input cụ thể: vấn
đề này liên quan đến độ phức tạp thời gian của thuật toán.
b) Dung lượng bộ nhớ cần thiết để chương trình (thuật toán) có thể “chạy”
được: vấn đề này liên quan đến độ phức tạp không gian của thuật toán.
Độ phức tạp không gian thường liên quan đến cấu trúc dữ liệu cụ thể dùng để thực hiện thuật toán sẽ được nghiên cứu trong môn Cấu Trúc Dữ Liệu nên không bàn ở đây Chúng ta chỉ tập trung bàn về vấn đề độ phức tạp thời gian.
Độ phức tạp thời gian của một thuật toán có thể đo bằng số lượng phép toán mà thuật toán sử dụng ứng với một qui mô xác định của các giá trị đầu vào Các phép toán này có thể là các phép gán, phép so sánh các số nguyên, các phép cộng, trừ, nhân, chia hoặc bất kì phép tính sơ cấp nào khác Sở
dĩ ta dùng “số lượng các phép toán” thay thế cho “thời gian thực dùng để chạy chương trình” để đo độ
phức tạp thời gian là vì với các máy tính khác nhau có thể có tốc độ xử lí đối với cùng một phép toán là khác nhau.
Để làm rõ hơn vấn đề này ta thử xét bài toán tìm kiếm trên một danh sách (hữu hạn) các số nguyên đã được xếp thứ tự:
Thuật toán tìm kiếm tuyến tính (vét cạn):
Procedure TimKiemTuyenTinh(x: số nguyên ; a1,a2, , an: các số nguyên) i:=1
While (in) and (xai) do i:=i+1
If (in) then VitriTimThay := i else VitriTimThay := 0
return(VitriTimThay)
Thuật toán tìm kiếm nhị phân:
Procedure TimKiemNhiPhan(x: số nguyên; a1,a2, , an: các số nguyên)
i:=1
j:=n
While (i<j) do
begin
m:=(i+j) div 2
if (x>am) then i:=m+1 else j:=m end
If (x=ai) then VitriTimThay := i else VitriTimThay := 0
return(VitriTimThay)
Trong thuật toán vét cạn ở mỗi bước của vòng lặp ta có hai phép so sánh phải thực hiện – một để xem hết danh sách chưa (in) và một để xem coi tìm gặp chưa (x
ai) Cuối cùng phải thực hiện thêm một phép so sánh nữa sau khi thực hiện xong
Trang 8vòng lặp Do đó, nếu tồn tại x=ai thì phải dùng đến 2i+1 phép so sánh Trong trường
hợp xấu nhất là x=an hoặc không tìm thấy sau khi đã duyệt hết danh sách thì số phép
so sánh phải thực hiện là 2n+1 Nếu mỗi phép gán tốn một đơn vị thời gian thuật toán thì thời gian chạy2 của chương trình là F(n)=2n+1 Như vậy F(n) là O(n).
Để xem xét trường hợp thuật toán nhị phân, ta giả sử n=2k với k là số nguyên không âm3 (ie: k=log2 n) Ở mỗi giai đoạn của thuật toán thì i và j là các vị trí đầu và
vị trí cuối của danh sách ở giai đoạn đó Ở mỗi giai đoạn, trừ lần đầu tiên, chiều dài của danh sách chỉ bằng phân nửa chiều dài danh sách của giai đoạn ngay trước đó Ở mỗi giai đoạn có hai phép so sánh được thực hiện Trong tình huống xấu nhất ViTriTimThay chỉ được xác nhận khi chiều dài danh sách bằng 1 (khi i=j) Sau đó một phép so sánh nữa được thực hiện để cho biết có tìm thấy phần tử x trong danh sách tại
vị trí i hoặc không tìm thấy x (ViTriTimThay=0) Trong tình huống xấu nhất việc chia đôi danh sách phải thực hiện tất cả k lần (ứng với các chiều dài danh sách 2k, 2k-1, 2k-2,
…,22, 21) và phải thực hiện tất cả 2k phép so sánh Lần cuối cùng (ứng với chiều dài danh sách 20) phải thực hiện phép so sánh (i<j) để thoát khỏi vòng lặp và phép so sánh (x=ai) để kết xuất ViTriTimThay Tổng cộng có 2k+2 =2 log n + 2 phép so sánh
Thời gian chạy như vậy là F(n)=2 log n + 2 Hay F(n) là O(log n)
Đồ thị so sánh như sau:
Suy từ đồ thị rõ ràng thuật toán nhị phân, ngay cả trong trường hợp xấu nhất, cũng cho một hiệu quả tìm kiếm tốt hơn nhiều so với tìm kiếm kiểu vét cạn
Thuật toán nhị phân có vẻ như được mô tả phức tạp hơn, rắc rối hơn nhưng điều đó không có nghĩa là ít hiệu quả hơn, mà là ngược lại Trong công việc lập trình điều này thường rất hay xảy ra Do đó điều quan trọng khi đánh giá hiệu quả một thuật toán không phải là dựa vào số lượng dòng văn bản đã được dùng để mô tả nó mà là có một sự phân tích toán học thấu đáo
2 Là thời gian thực hiện chương trình trong tình huống xấu nhất (thời gian thuật toán).
3 Nếu n không phải là luỹ thừa của 2, ta có thể xem danh sách đó là danh sách con của danh sách có 2(k+1) phần tử với k là số nguyên nhỏ nhất sao cho: 2 k < n < 2 (k+1)