chđề bd hs giỏi tin 12
DESCRIPTION
Tai lieu hay cho HS chuyen TinTRANSCRIPT
1
Chuyên Đề:
PHÂN TÍCH THIẾT KẾ THUẬT TOÁN
PHẦN 1: THUẬT TOÁN VÀ PHÂN TÍCH THUẬT TOÁN
I. KHÁI NIỆM BÀI TOÁN VÀ THUẬT TOÁN
1. Khái niệm bài toán
Trong phạm vi tin học, ta có thể quan niệm bài toán là một việc nào đó ta muốn máy tính
thực hiện.
Một bài toán được mô tả bởi hai thành phần: tập dữ liệu đầu vào cho trước (Input) và tập
kết quả ra cần nhận được (Output).
Thuật ngữ “một” bài toán được hiểu theo nghĩa phổ dụng, bài toán có nhiều bộ dữ liệu khác
nhau.
Ví dụ: giải bài toán tìm ước chung lớn nhất (UCLN) của hai số nguyên dương M, N. Thuật
toán để giải bài toán này phải đáp ứng với bất kì bộ giá trị cụ thể của bộ dữ liệu vào M, N.
2. Khái niệm thuật toán:
Việc cho một bài toán là mô tả rõ Input và Output. Vấn đề là làm thế nào để tìm ra Ouput?.
Đó chính là lời giải của bài toán
Có nhiều xu hướng khác nhau trong việc nghiên cứu lời giải của các bài toán. Trong toán
học có xu hướng nghiên cứu định tính, chứng minh sự tồn tại lời giải bài toán mà không cần
thiết chỉ ra một cách tường minh cách tìm lời giải đó.Trong việc nghiên cứu thuật toán ở đây,
chúng ta quan tâm tới cách tường minh để tìm Output từ Input của bài toán.
Có nhiều định nghĩa khác nhau về thuật toán, dưới đây là một định nghĩa thường dùng
Thuật toán để giải một bài toán là một dãy hữu hạn các thao tác được sắp xếp theo một
trình tự xác định sao cho sau khi thực hiện dãy thao tác ấy, từ Input của bài toán, ta nhận được
Output cần tìm
3. Các bƣớc giải bài toán trên máy tính:
a. Xác định bài toán:
Việc xác định bài toán chính là xác định rõ hai thành phần Input, Output và mối liên hệ giữa
chúng.
Ví dụ: giải bài toán tìm ước chung lớn nhất (UCLN) của hai số nguyên dương M, N.
Input: hai số nguyên dương M, N
Output: UCLN của M và N
2
b. Thiết kế hoặc lựa chọn thuật toán:
Một bài toán có thể có nhiều thuật toán để giải. Nhưng một thuật toán thì chỉ có thể dùng để
giải quyết cho một bài toán cụ thể mà thôi. Vấn đề đặt ra ở đây nếu có nhiều thuật toán để giải
một bài toán thì ta nên chọn thuật toán nào? Dĩ nhiên ta sẽ chọn thuật toán nào tốt nhất. Nhưng
một thuật toán thế nào được cho là tốt nhất, nó được đánh giá dựa trên những tiêu chí nào?
Tùy theo bài toán mà ta sẽ lựa chọn thuật toán theo các tiêu chí sau đây:
(1) Tính hiệu quả về thời gian: là thời gian để thực hiện xong một bài toán
(2) Tính hiệu quả về không gian: là dung lượng cần thiết để lưu trữ các dữ liệu cần thiết khi
xử lí bài toán
(3) Tính khả thi khi cài đặt thuật toán: là thuật toán có dễ cài đặt hay không, có tốn nhiều
công sức của người lập trình không.
Ví dụ: Đối với một bài toán lớn và số lần giải bài toán là nhiều lần thì tiêu chí (1) sẽ được
ưu tiên hàng đầu. Nhưng đối với bài toán nhỏ và số lần giải bài toán đó không nhiều thì thông
thường ta sẽ chọn tiêu chí (3) làm tiêu chí được chọn đầu tiên
c. Viết chương trình:
Viết chương trình là tổng hợp giữa việc lựa chọn cách tổ chức dữ liệu và sử dụng ngôn ngữ
lập trình để diễn đạt đúng thuật toán
Cấu trúc dữ liệu (Data Structures): là các đơn vị cấu trúc (construct) của ngôn ngữ lập
trình dùng để biểu diễn các mô hình dữ liệu, ví dụ như mảng (array), bản ghi (record), tệp
(file), xâu (string), danh sách liên kết (list linked),…
Các cấu trúc dữ liệu được lựa chọn không những cần có khả năng biểu diễn được dữ liệu
vào Input và dữ liệu ra Output của bài toán cần giải mà còn cần phải phù hợp với các thao tác
của thuật toán và cài đặt được bằng ngôn ngữ lập trình đã được lựa chọn.
Hai bước b và c thường được thực hiện không tách rời và gắn kết chặt chẽ với nhau rất chặt
chẽ. Vì có những thuật toán chỉ thích ứng và hiệu quả với một cách tổ chức dữ liệu nhất định.
Như vậy có thuật toán tốt chưa đủ mà phải chọn được cấu trúc dữ liệu thích hợp, đồng thời phải
có kĩ thuật cài đặt chương trình tốt.
d. Kiểm thử và hiệu chỉnh:
Sau khi được viết xong, chương trình vẫn còn có thể có nhiều lỗi khác chưa phát hiện được
nên có thể không có kết quả đúng. Vì vậy, cần phải kiểm thử tính đúng đắn của chương trình.
Có nhiều cách để thực hiện được điều đó, cách thường dùng là thực hiện chương trình với một
số bộ Input tiêu biểu phù thuộc vào đặc thù của bài toán mà bằng cách nào đó ta đã biết trước
được bộ kết quả Output tương ứng. Các bộ Input và Output tương ứng này gọi là các Test.
Trong quá trình kiểm thử nếu phát hiện có sai sót, ta phải hiệu chỉnh lại chương trình rồi thử
lại. Trên cơ sở đánh giá các lỗi của chương trình qua kiểm thử có thể xác định được hoặc ngôn
ngữ lập trình, hoặc cấu trúc dữ liệu thậm chí cả thuật toán lựa chọn là không phù hợp, cần thiết
quay lại các công việc của bước trước đó.
3
e. Viết tài liệu
Tài liệu trình bày các nội dung liên quan đến mô tả bài toán, thuật toán, cấu trúc dữ liệu,
thiết kế chương trình và hướng dẫn sử dụng. Tài liệu (bao gồm cả các chú thích trong chương
trình) là cần thiết không những cho người dùng để có thể khai thác tốt chương trình, đề xuất
phương án để hoàn thiện chương trình mà còn cho chính người lập trình hoặc người đọc
chương trình dễ dàng chỉnh sửa, nâng cấp chương trình khi cần thiết
II. PHÂN TÍCH THUẬT TOÁN
1. Độ phức tạp của thuật toán:
Một trong những tiêu chí thường được lấy để đánh giá thuật toán là thời gian thực hiện thuật
toán. Vậy, làm thế nào để “đo được” thời gian thực hiện của một chương trình (mô tả một thuật
toán)?
a. Dùng mẫu chuẩn
Đây là cách rất tự nhiên và thường được sử dụng. Dựa vào thời gian thực tế thực hiện các
chương trình viết bằng cùng một ngôn ngữ lập trình cho các thuật toán khác nhau với cùng các
bộ dữ liệu vào Input như nhau, trên cùng một hệ thống máy tính để kết luận thuật toán nào
nhanh, thuật toán nào chậm. Tuy nhiên việc này cũng dễ mắc lỗi vì khi thực hiện chương trình,
ngoài yếu tố thuật toán còn các yếu tố khác của hệ thống, đặc trưng dữ liệu chạy thử cũng có
ảnh hưởng đến thời gian chạy của chương trình. Vì thế để so sánh chính xác hơn cần xây dựng
các bộ dữ liệu vào Input theo các mẫu chuẩn (benchmark) sao cho có thể thừa nhận một
chương trình thực hiện tốt trên mẫu chuẩn thì được coi là tốt trên mọi dữ liệu vào.
b. Phân tích thuật toán
Một cách khác để đánh giá chương trình (thuật toán) là dựa vào mỗi câu lệnh của chương
trình nguồn sẽ thực hiện bao nhiêu lần trên một tập dữ liệu vào. Phần lớn các môi trường lập
trình đều có bộ đếm lệnh (statement couter) là công cụ để thực hiện phép đo đó.
Mục tiêu của phân tích thuật toán không chỉ là để so sánh, đánh giá giúp cho việc lựa chọn
thuật toán tốt mà còn dựa vào kết quả phân tích đánh giá đó để hiệu chỉnh, cải tiến thuật toán
đã có được tốt hơn. Nhiều chương trình cho thấy, tổng thời gian thực hiện một chương trình lớn
thường tiêu phí cho những phần “nhỏ” của chương trình. Về nguyên tắc, số các thao tác trừu
tượng trong thuật toán là rất lớn. Tuy nhiên thường thì tính năng của thuật toán chỉ phục thuộc
vào một vài đại lượng. Khi đánh giá thời gian thực hiện thuật toán ta chú ý đặc biệt đến các
phép toán mà số lần thực hiện không ít hơn các phép toán khác (chứa trong các phần “nhỏ” của
chương trình).
Việc chú ý đến các phép toán được thực hiện nhiều lần là điều cần thiết khi cài đặt thuật
toán bằng chương trình. Ví dụ, người lập trình cần quan tâm lớn để cải tiến các “vòng lặp
trong” vì các phép toán trong đó là các phép toán được thực hiện lặp lại nhiều lần nhất.
Cách đánh giá thời gian thực hiện thuật toán độc lập với hệ thống máy tính dẫn tới khái
niệm độ phức tạp của thuật toán. Thời gian để thực hiện thuật toán phụ thuộc rất nhiều yếu tố.
Một yếu tố rất quan trọng là kích thước của dữ liệu vào. Dữ liệu càng lớn thì thời gian thực
hiện thuật toán càng lớn. Ta kí hiệu T(n) là hàm “đo” số lượng các phép toán cơ bản xuất hiện
khi thực hiện thuật toán và đố số nguyễn phúc thảo ngọc là kích thước dữ liệu vào
4
2. Kí pháp độ phức tạp của thuật toán
Ta có thể gọi hàm T(n) là thời gian thực hiện một thuật toán nào đó. Giải sử, f(n) và g(n) là
các hàm xác định dương với mọi n. Khi đó ta nói độ phức tạp tính toán của thuật toán có thời
gian thực hiện là T(n) là:
Hàm O lớn (đọc là ô lớn): O(g(n)) nếu tồn tại các hằng số phan văn mạnh và n0 sao cho
T(n) ≤ c.g(n) với mọi n ≥ n0 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).
Ví dụ: nếu T(n) = n2 + 1 thì T(n) = O(n
2).
Thật vậy, chọn c = 2 và n0 = 1, khi đó mọi n ≥ 1, ta có:
T(n) = n2 + 1 ≤ 2n
2 = 2g(n)
3. Các cách xác định độ phức tạp của thuật toán
a. Quy tắc hằng số
Nếu một thuật toán T có thời gian thực hiện T(n) = O (c1f(n)) với c1 là một hằng số dương
thì có thể coi thuật toán T đó có độ phức tạp tính toán là O (f(n)).
Thật vậy, vì T(n) = O(c1f(n)) nên tồn tại c0 > 0 và n0 >0 để T(n) ≤ c0.c1f(n) với mọi n ≥ n0.
Chọn c = c0.c1, với mọi n > n0 ta có T(n) ≤ cf(n). Vậy quy tắc hằng số là đúng.
Như vậy, hằng số ở đây không quan trọng.
b. Quy tắc cộng
Giả thiết, thuật toán gồm hai phần liên tiếp T1 và T2. Khi đó nếu phần T1 của thuật toán có
thời gian thực hiện là T1(n) = O(f(n)) và phần T2 có thời gian thực hiện là T2(n) = O(g(n)), khi
đó thời gian thực hiện thuật toán sẽ là
T1(n) + T2(n) = O(f(n) + g(n))
Chứng minh: Vì T1(n) = O(f(n)) nên tồn tại các hằng số c1 và n1 sao cho
T1(n) ≤ c1.f(n) với mợi n ≥ n1
Vì T2(n) = O(g(n)) nên tồn tại các hằng số c2 và n2 sao cho
T2(n) ≤ c2.f(n) với mợi n ≥ n2
Chọn c = max(c1, c2) và n0 = max(n1, n2) ta có với mọi n ≥ n0:
T(n) = T1(n) + T2(n) ≤ c1f(n) + c2g(n) ≤ cf(n) + cg(n) = c(f(n) + g(n))
Đó là điều phải chứng minh
c. Quy tắc lấy max
Nếu thuật toán T có thời gian thực hiện T(n) = O(f(n) + g(n)) thì có thể coi thuật toán T có
độ phức tạp tính toán là O(max(f(n),g(n))).
Thật vậy, vì T(n) = O(f(n)+g(n)) nên tồn tại n0 > 0 và c > 0 để T(n) ≤ cf(n) + cg(n), với mọi
n ≥ n0
Vậy T(n) ≤ cf(n) + cg(n) ≤ 2c.max(f(n), g(n)) với mọi n ≥ n0
5
Từ đó suy ra điều cần chứng minh
d. Quy tắc nhân
Nếu đoạn thuật toán T 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 thuật toán T 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 thuật toán T sẽ là k(n)T(n), theo định nghĩa:
- Tồn tại ck≥0 và nk>0 để k(n)≤ckg(n) với mợi n≥nk
- Tồn tại cT≥0 và nT>0 để T(n)≤cTf(n) với mọi n≥nT
Vậy với mọi n ≥ max(nT,nk) ta có k(n)T(n) ≤ ckcT(f(n)g(n)). Từ đó suy ra điều cần chứng
minh.
4. Các thuật ngữ thƣờng dùng cho độ phức tạp thuật toán
Độ phức tạp Thuật ngữ
O(1) Độ phức tạp hằng số
O(logn) Độ phức tạp lôgarit
O(n) Độ phức tạp tuyến tính
O(nlogn) Độ phức tạp nlogn
O(nb) Độ phức tạp đa thức
O(bn) (b>1) Độ phức tạp hàm mũ
O(n!) Độ phức tạp giai thừa
5. Đánh giá thời gian thực hiện chƣơng trình
Trước hết, ta phân loại câu lệnh trong ngôn ngữ lập trình bậc cao. Có các loại câu lệnh
thường dùng sau:
- Câu lệnh đơn thực hiện một thao tác, ví dụ câu lệnh gán đơn giản (không chứa lời gọi
hàm trong biểu thức), đọc/ghi đơn giản, câu lệnh chuyển điều khiển đơn giản (break, goto,
continue, return)
- Câu lệnh hợp thành là dãy các câu lệnh tạo thành một khối độc lập
- Câu lệnh rẽ nhánh dạng IF…THEN (còn gọi là câu lệnh điều kiện IF)
- Các câu lệnh lặp
Để đánh giá thời gian thực hiện chương trình, cần thiết phải biết đánh giá thời gian thực hiện
các câu lệnh. Để làm điều đó ta áp dụng các quy tắc tính độ phức tạp thuật toán đã trình bày ở
mục trên, cụ thể:
- Thời gian thực hiện một lệnh đơn không phụ thuộc vào kích thước dữ liệu nên sẽ là O(1)
- Thời gian thực hiện một câu lệnh hợp thành sẽ được tính theo quy tắc cộng và quy tắc
max
- Thời gian thực hiện câu lệnh IF: giả sử thời gian thực hiện hai câu lệnh thành phần dạng
đủ là f(n) và g(n) thì thời gian thực hiện của câu lệnh IF sẽ được tính theo quy tắc max nên sẽ là
O(max(f(n),g(n))).
6
- Thời gian thực hiện câu lệnh lặp sẽ áp dụng theo quy tắc nhân, nghĩa là O(k(n)f(n)),
trong đó k(n) là số lần lặp và f(n) là thời gian thực hiện câu lện bên trong vòng lặp
6. Một số ví dụ
Phân tích thời gian thực hiện của các đoạn chương trình sau:
Ví dụ 1:
VAR i, j, n: longint;
s1, s2: longint;
BEGIN
{1} Readln(n);
{2} s1:=0;
{3} FOR i:=1 TO n DO
{4} s1:=s1+i;
{5} s2:=0;
{6} FOR j:=1 TO n DO
{7} s2:=s2+j*j;
{8} writeln(‘1+2+…+’,n,’=’,s1);
{9} writeln(‘1^2+2^2+..+’,n,’^2=’,s2);
END.
Thời gian thực hiện chương trình phụ thuộc vào n
Các câu lệnh {1}, {2}, {4}, {5}, {7}, {8}, {9} có thời gian thực hiện là O(1)
Câu lệnh {3} thời gian thực hiện là O(n). Câu lệnh {6} thời gian thực hiện là O(n)
Vậy thời gian thực hiện của chương trình là
Max(O(1), O(1), O(n), O(1), O(n),O(1), O(1)) = O(n)
Ví dụ 2:
{1} c:=0;
{2} FOR i:=1 TO 2*n DO
{3} c:=c+1
{4} FOR i:=1 TO n DO
{5} FOR j:=1 TO n DO
{6} c:=c+1
Câu lệnh {1}. {3}, {6} có thời gian thực hiện là O(1). Lệnh lặp FOR {2} có số lần lặp là 2n
nên lệnh {2} có thời gian thực hiện là O(n). Lệnh lặp 5 có số lần lặp là n nên lệnh {5} có thời
gian thực hiện là O(n). Lệnh lặp {4} có số lần lặp là n và nó chứ câu lệnh lặp {5} nên thời có
thời gian thực hiện là O(n2).
Vậy thời gian thực hiện của đoạn chương trình trên là
Max(O(1), O(n), O(n2)) = O(n
2)
7
Ví dụ 3:
{1} FOR i:=1 TO n DO
{2} FOR j:=1 TO i DO
{3} c:=c+1
Câu lệnh {3} có thời gian thực hiện là O(1)
Khi i=1, j chạy từ 1 đến 1 nên lệnh lặp For {2} lặp 1 lần
Khi i:=2, j chạy từ 1 đến 2 nên lệnh lặp For {2} lặp 2 lần
…
Khi i:=n, j chạy từ 1 đến n nên lệnh lặp For {2} lặp n lần
Như vậy lệnh {3} được lặp 1+2+3…+n=n(n+1)/2 lần, do đó lệnh {1} có thời gian thực hiện
là O(n2)
Vậy thời gian thực hiện đoạn chương trình trên là O(n2)
BÀI TẬP
Phân tích thời gian thực hiện của các đoạn chƣơng trình sau:
Câu 1.
Readln(n);
S:=0;
i:=1;
WHILE i<n DO
BEGIN
Readln(X);
S:= S + X;
Inc(i);
END;
M:= S/n;
Writeln(M);
Câu 2.
FOR i := 1 TO n DO
FOR j := 1 TO n DO
BEGIN
c[i,j] := 0;
FOR k := 1 TO n DO
c[i,j] := c[i,j] + a[i,k] * b[k,j];
END;
8
Câu 3.
j:=n;
d:=0;
REPEAT
j:=j div 2;
d:=d+1;
UNTIL j<1
Câu 4.
FOR i:=1 TO n DO
IF i mod 2 =0 THEN c:=c+1
ELSE c:=c+2;
Câu 5.
FOR i:=1 TO n DO
IF i mod 2 = 0 THEN
FOR j:=1 TO n DO DO c:=c+1;
Câu 6.
D:=0
FOR i:=1 TO n-2 DO
FOR j:=i+1 TO n-1 DO
FOR k:=j+1 TO n DO d:=d+1;
9
PHẦN 2: THIẾT KẾ THUẬT TOÁN
I. THUẬT TOÁN DUYỆT
Như chúng ta đã biết các thuật toán được xây dựng để giải quyết vấn đề nhờ vào một quy
tắc nào đó. Tuy nhiên có những vấn đề không theo quy tắc, và khi đó phải dùng phương pháp
vét cạn.
Vét cạn, duyệt, quay lui… là một số tên gọi tuy không đồng nghĩa nhưng cùng chỉ một
phương pháp rất đơn giản trong tin học: tìm nghiệm của một bài toán bằng cách xem xét tất cả
các phương án có thể. Đối với con người phương pháp này thường là không khả thi vì số
phương án cần kiểm tra quá lớn. Tuy nhiên đối với máy tính, nhờ tốc độ xử lí nhanh, máy tính
có thể giải rất nhiều bài toán bằng phương pháp vét cạn.
Ưu điểm lớn nhất của phương pháp vét cạn là luôn đảm bảo tìm ra nghiệm chính xác. Ngoài
ra phương pháp vét cạn còn có một số ưu điểm so với các phương pháp khác là đòi hỏi rất ít bộ
nhớ và cài đặt đơn giản. Hạn chế duy nhất của phương pháp này là thời gian thực thi rất lớn, độ
phức tạp thường ở bậc mũ. Do đó vét cạn thường chỉ áp dụng tốt với các bài toán có kích thước
nhỏ.
Mặc dù vậy, không nên coi thường phương pháp này. Rất nhiều bài toán chỉ có thuật toán
duy nhất là vét cạn. Trong một số tình huống khác, chẳng hạn như thời gian lập trình hạn chế
thì vét cạn có thể coi như một giải pháp tình thế. Rất nhiều trường hợp ta có thể sử dụng vét
cạn theo phương châm: thà mất 1tiếng để viết một chương trình vét cạn chạy trong trong 4
tiếng, còn hơn mất 4 ngày tìm thuật toán hiệu qủa để chương trình chạy trong 1 phút.
Chúng ta không đề cập kĩ về việc áp dụng phương pháp vét cạn đối với các bài toán đơn
giản như tìm giá trị nhỏ nhất, lớn nhất hay tìm tất cả các số nguyên tố của một tập hợp. Chúng
ta sẽ xem xét thuật toán vét cạn đối với các bài toán tìm cấu hình tổ hợp và bài toán tối ưu tổ
hợp, là lớp các bài toán rất tổng quát và phổ biến trong tin học
1. Phƣơng pháp:
Trong nhiều bài toán, việc tìm nghiệm có thể quy về việc tìm vectơ hữu hạn (x1,x2,…,xn,
…), độ dài vectơ có thể xác định trước hoặc không. Vectơ này cần phải thỏa mãn một số điều
kiện tùy thuộc vào yêu cầu của bài toán. Các thành phần xi được chọn ra từ tập hữu hạn Ai.
Tùy từng trường hợp mà bài toán có thể yêu cầu: tìm một nghiệm, tìm tất cả các nghiệm
hoặc đếm số nghiệm.
Tư tưởng của thuật toán quay lui vét cạn như sau: Ta xây dựng vectơ nghiệm từng bước, bắt
đầu từ vecto không (). Thành phần đầu tiên x1 được chọn ra từ tập S1=A1. Giả sử đã chọn được
các thành phần x1, x2, …, xi. Lặp lại quá trình trên để tiếp tục mở rộng nghiệm. Nếu không thể
chọn được thành phần xi+1 (Si+1 rỗng) thì ta quay lại chọn một phần tử khác của Si cho xi. Nếu
không còn một phần tử nào khác của Si ta quay lại chọn một phần tử khác của Si-1 làm xi-1 và
cứ thế tiếp tục. Trong quá trình mở rộng nghiệm, ta phải kiểm tra nghiệm đang xây dựng đã là
nghiệm của bài toán chưa. Nếu chỉ cần tìm một nghiệm thì khi gặp nghiệm ta dừng lại. Còn nếu
cần tìm tất cả các nghiệm thì quá trình đó chỉ dừng lại khi tất cả các khả năng lựa chọn của
thành phần của vectơ nghiệm đã được vét cạn.
Thuật toán duyệt thường được dùng mô hình đệ quy sau:
10
PROCEDURE Try(i)
BEGIN
<xác định Si>
FOR xi Si DO BEGIN
<ghi nhận thành phần thứ i>
IF (tìm thấy nghiệm) THEN <đưa ra nghiệm>
ELSE Try(i+1)
<loại phần tử thứ i>
END;
END;
Hoặc ta có thể sử dụng mô hình đệ quy quay lui như sau:
PROCEDURE Try(i)
BEGIN
FOR (mỗi phương án chọn) DO
IF (chọn được) THEN
BEGIN
<Thực hiện bước đi thứ i>
IF (thành công) THEN (thông báo kết quả)
ELSE Try(i+1)
<Hủy bước đi thứ i>
END;
END;
Với mô hình tổng quát trên ta có ba vấn đề trọng tâm cần làm là:
- Tìm cách biểu diễn nghiệm của bài toán dưới dạng một dãy các đối tượng được chọn
dần từng bước;
- Xác định tập Si các ứng cử viên được chọn làm thành phần tứ i của nghiệm. Chọn cách
thích hợp để biễu diễn Si;
- Tìm các điều kiện để một vectơ đã được chọn làm nghiệm bài toán.
2. Giải các bài toán cấu hình tổ hợp bằng thuật toán duyệt
a. Tổ hợp:
Một tổ hợp chập k của n là một tập con k phần tử của tập n phần tử.
Số lượng tổ hợp chập k của n được tính theo công thức sau:
k)!(nk!
n!Ck
n
Chẳng hạn tập {1,2,3,4} có các tổ hợp chập 2 là: {1,2}, {1,3, {1,4, {2,3}, {2,4}, {3,4}. Vì
trong tập hợp các phần tử không phân biệt thứ tự nên tập {1,2} cũng là tập {2,1} và do đó, ta
coi chúng chỉ là một tổ hợp.
Bài toán đặt ra cho chúng ta là hãy xác định tất cả các tổ hợp châp k của tập n phần tử. Để
đơn giản ta chỉ xét bài toán tìm các tổ hợp của tập các số nguyên từ 1 đến n. Đối với một tập
11
hữu hạn bất kì, bằng cách đánh số thứ tự của các phần tử, ta cũng đưa được về bài toán đối với
tập các số nguyên từ 1 đến n.
Nghiệm cần tìm của bài toán tìm các tổ hợp chập k của n phần tử phải thoả mãn các điều
kiện sau:
- Là một vector x =(x1,x2,…xk)
- xi lấy giá trị trong tập {1,2,…n}
- Ràng buộc: xi<xi+1 với mọi giá trị i từ 1 đến k-1.
Có ràng buộc 3 là vì tập hợp không phân biệt thứ tự phần tử nên ta sắp xếp các phần tử theo
thứ tự tăng dần.
Để giải quyết bài toán này ta cần xác định 3 vấn đề quan trọng
- Biểu diễn nghiệm: xây dựng một mảng x để biểu diễn tổ hợp
- Xác định tập Si: là từ xi-1+1 đến (n-k+i). Để điều này đúng cho cả trường hợp i = 1 ta
thêm vào x0=0
- Điều kiện để một vectơ được chọn là: x[i1]< x[i] nki.
Dưới đây là toàn bộ chương trình giải bài toán trên. Để đơn giản, các giá trị n,k được nhập
từ bàn phím và các tổ hợp được in ra màn hình. Người đọc có thể cải tiến chương trình để
nhập/xuất ra file.
PROGRAM tohop;
USES crt;
CONST
max = 20;
VAR
n,k : integer;
x : array[0..max] of integer;
{===============================}
PROCEDURE Input;
BEGIN
clrscr;
write('n,k = '); readln(n,k);
writeln('Cac TO hop chap ',k,' cua ',n);
END;
{===============================}
PROCEDURE print;
VAR
i : integer;
BEGIN
FOR i := 1 TO k DO write(' ',x[i]);
writeln;
END;
{===============================}
PROCEDURE try(i:integer);
VAR j : integer;
BEGIN
12
FOR j := x[i-1]+1 TO n-k+i DO
BEGIN
x[i] := j;
IF i = k THEN Print ELSE try(i+1);
END;
END;
{===============================}
PROCEDURE solve;
BEGIN
x[0] := 0;
try(1);
END;
{===============================}
BEGIN
Input;
solve;
END.
b. Chỉnh hợp lặp:
Chỉnh hợp lặp chập k của n là một dãy k thành phần, mỗi thành phần là một phần tử của tập n
phần tử, có xét đến thứ tự và không yêu cầu các thành phần khác nhau.
Số lượng chỉnh hợp lặp chập k của n được tính theo công thức sau:
kk
n nA
Một ví dụ dễ thấy nhất của chỉnh hợp lặp là các dãy nhị phân. Một dãy nhị phân độ dài m là
một chỉnh hợp lặp chập m của tập 2 phần tử {0,1}. Chẳng hạn 101 là một dãy nhị phân độ dài
3. Ngoài ra ta còn có 7 dãy nhị phân độ dài 3 nữa là 000, 001, 010, 011, 100, 110, 111. Vì có
xét thứ tự nên dãy 101 và dãy 011 là 2 dãy khác nhau.
Như vậy, bài toán xác định tất cả các chỉnh hợp lặp chập k của tập n phần tử yêu cầu tìm
các nghiệm như sau:
- Là một vector x =(x1,x2,…xk)
- xi lấy giá trị trong tập {1,2,…n}
- Không có ràng buộc nào giữa các thành phần.
Chú ý là cũng như bài toán tìm tổ hợp, ta chỉ xét đối với tập n số nguyên từ 1 đến n. Nếu tập
hợp cần tìm chỉnh hợp không phải là tập các số nguyên từ 1 đến n thì ta có thể đánh số các
phần tử của tập đó để đưa về tập các số nguyên từ 1 đến n
Do không có ràng buộc nào giữa các thành phần nên đối với bài này chỉ cần dùng một mảng
x để lưu nghiệm
Dưới đây là chương trình
PROGRAM Chinhhoplap;
USES crt;
CONST max = 20;
VAR
13
n : integer;
x : array[1..max] of integer;
{===============================}
PROCEDURE Input;
BEGIN
clrscr;
write('n = '); readln(n);
writeln('Cac day nhi phan DO dai ',n);
END;
{===============================}
PROCEDURE print;
VAR i : integer;
BEGIN
FOR i := 1 TO n DO write(' ',x[i]);
writeln;
END;
{===============================}
PROCEDURE try(i:integer);
VAR j : integer;
BEGIN
FOR j := 0 TO 1 DO
BEGIN
x[i] := j;
IF i = n THEN Print ELSE try(i+1);
END;
END;
{===============================}
PROCEDURE solve;
BEGIN
try(1);
END;
{===============================}
BEGIN
Input;
solve;
END.
c. Chỉnh hợp không lặp:
Khác với chỉnh hợp lặp là các thành phần được phép lặp lại, tức là có thể giống nhau, chỉnh
hợp không lặp chập k của tập n phần tử cũng là một dãy k thành phần lấy từ tập n phần tử có
xét thứ tự nhưng các thành phần không được phép giống nhau.
Số lượng chỉnh hợp không lặp chập k của n được tính theo công thức sau:
k)!(n
n!A
k
n
Chẳng hạn có n người, một cách chọn ra k người để xếp thành một hàng là một chỉnh hợp
không lặp chập k của n.
14
Một trường hợp đặc biệt của chỉnh hợp không lặp là hoán vị. Hoán vị của một tập n phần tử
là một chỉnh hợp không lặp chập n. Nói một cách trực quan thì hoán vị của tập n phần tử là
phép thay đổi vị trí của các phần tử (do đó mới gọi là hoán vị).
Nghiệm của bài toán tìm các chỉnh hợp không lặp chập k của tập n số nguyên từ 1 đến n là
các vector x thoả mãn các điều kiện:
- x có k thành phần: x = (x1,x2,…xk)
- Các giá trị xi lấy trong tập {1,2,..n}
- Ràng buộc: các giá trị xi đôi một khác nhau, tức là xixj với mọi ij.
Chỉnh hợp không lặp yêu cầu các phần tử phải khác nhau. Để đảm bảo điều đó, ngoài mảng
x, ta sẽ dùng thêm một cấu trúc dữ liệu nữa là mảng d để đánh dấu. Khi một giá trị được chọn,
ta đánh dấu giá trị đó, và khi chọn, ta chỉ chọn các giá trị chưa đánh dấu. Mảng d sẽ là "trạng
thái" của thuật toán.
Dưới đây là chương trình tìm toàn bộ hoán vị của n số nguyên từ 1 đến n
PROGRAM Hoanvi;
USES crt;
CONST max = 20;
VAR n : integer;
x,d : array[1..max] of integer;
{===============================}
PROCEDURE Input;
BEGIN
clrscr;
write('n = '); readln(n);
writeln('Cac hoan vi cua day ',n);
END;
{===============================}
PROCEDURE print;
VAR i : integer;
BEGIN
FOR i := 1 TO n DO write(' ',x[i]);
writeln;
END;
{===============================}
PROCEDURE try(i:integer);
VAR j : integer;
BEGIN
FOR j := 1 TO n DO
IF d[j] = 0 THEN
BEGIN
x[i] := j; d[j] := 1;
IF i = n THEN Print ELSE try(i+1);
d[j] := 0;
END;
END;
{===============================}
15
PROCEDURE solve;
BEGIN
try(1);
END;
{===============================}
BEGIN
Input;
solve;
END.
3. Bài toán 8 quân hậu
Yêu cầu bài toán: Cho bàn cờ vua nxn. Hãy xếp n con hậu lên bàn cờ sao cho không con
nào khống chế con nào. Hai 2 con hậu khống chế nhau nếu chúng ở trên cùng một hàng, một
cột hoặc một đường chéo.
Chẳng hạn khi n =8 ta có một cách đặt sau, các ô đen là các vị trí đặt hậu:
Để chuyển bài toán này về dạng chuẩn của bài toán tìm cấu hình tổ hợp, ta có có nhận xét:
mỗi con hậu phải ở trên một hàng và một cột. Do đó ta coi con hậu thứ i ở hàng i và nếu biết
x[i] là cột đặt con hậu thứ i thì ta suy ra được lời giải. Vậy nghiệm của bài toán có thể coi là
một vector x gồm n thành phần với ý nghĩa:
- Con hậu thứ i được đặt ở hàng i và cột x[i].
- x[i] lấy giá trị trong tập {1,2…n}
- Ràng buộc: các giá trị x[i] khác nhau từng đôi một và không có 2 con hậu ở trên cùng
một đường chéo.
Để cài đặt bài toán này, chúng ta sẽ phân tích chi tiết về các ràng buộc trên.
Ràng buộc thứ nhất là các giá trị x[i] phải khác nhau. Ta có thể dùng một mảng đánh dấu
như ở thuật toán hoán vị để đảm bảo điều này.
Ràng buộc thứ 2 là các con hậu không được nằm trên cùng một đường chéo chính và phụ.
Ta dễ dàng nhận ra rằng 2 vị trí (x1,y1) và (x2,y2) nằm trên cùng đường chéo chính nếu:
x1y1=x2y2=const.
Tương tự, 2 vị trí (x1,y1) và (x2,y2) nằm trên cùng đường chéo phụ nếu:
x1y1=x2y2=const
16
Do đó, con hậu i đặt tại vị trí (i,x[i]) và con hậu j đặt tại vị trí (j,x[j]) phải thoả mãn ràng
buộc:
ix[i] jx[j] và i+x[i] j+x[j] với mọi ij
Ta có thể viết riêng một hàm Ok để kiểm tra các ràng buộc đó. Nhưng giải pháp tốt hơn là
dùng thêm các mảng đánh dấu để mô tả rằng một đường chéo chính và phụ đã có một con hậu
khống chế. Tức là khi ta đặt con hậu i ở vị trí (i,j), ta sẽ đánh dấu đường chéo chính i-j và
đường chéo phụ i+j.
Như vậy về cấu trúc dữ liệu, ta dùng 4 mảng:
- Mảng x với ý nghĩa: x[i] là cột ta sẽ đặt con hậu ở hàng thứ i.
- Mảng cot với ý nghĩa: cot[j]=1 nếu cột j đã có một con hậu được đặt, ngược lại thì
cot[j]=0.
- Mảng dcc với ý nghĩa: dcc[k]=1 nếu đường chéo chính thứ k đã có một con hậu được
đặt, tức là ta đã đặt một con hậu tại vị trí (i,j) mà ij=k; ngược lại thì dcc[k]=0.
- Tương tự ta dùng mảng dcp với ý nghĩa: dcp[k]=1 nếu đường chéo phụ thứ k đã có một
con hậu được đặt.
Dưới đây là chương trình
CONST n=8;
TYPE vector = array[1..n] of longint;
VAR cot:array[1..n] of longint;
Dcc:array[1-n..n-1] of longint;
Dcp:array[1+1..n+n] of longint;
X:vector;
PROCEDURE print;
VAR i:longint;
BEGIN
FOR i:=1 TO n DO write(x[i],’’);
Writeln;
END;
PROCEDURE xephau(i:longint);
VAR j:longint;
BEGIN
FOR j:=1 TO n DO
IF (cot[j]=0) and (dcc[i-j]=0) and (dcp[i+j]=0) THEN
BEGIN
X[i]:=j;
Cot[j]:=1;
Dcc[i-j]=1;
Dcp[i+j]:=1;
IF j=n THEN print ELSE xephau(j+1);
Cot[j]:=0;
Dcc[i-j]:=0;
Dcp[i+j]:=0;
END;
END;
17
BEGIN
Fillchar(cot,sizeof(cot),0);
Fillchar(dcc,sizeof(dcc),0);
Fillchar(dcp,sizeof(dcp),0);
Xephau(1);
Readln;
END;
Bài này có tất cả 92 nghiệm. Một trong những nghiệm đó là (1, 5, 8, 6, 3, 7, 2, 4)
BÀI TẬP
Câu 1. Bài toán từ đẹp
Tìm tất cả các từ đẹp độ dài n
Một từ đẹp là một xâu độ dài n chỉ gồm các kí tự A,B,C mà không có 2 xâu con liên tiếp
nào giống nhau.
Chẳng hạn ABAC là một từ đẹp độ dài 4, BABCA là một từ đẹp độ dài 5.
Câu 2. Bài toán rút tiền tự động
Một máy ATM hiện có n (n<=20) tờ tiền có mệnh giá t1, t2, .., tn. Hãy đưa ra một cách trả
với số tiền đúng bằng S, nếu không thể trả đúng bằng S thì thông báo không chọn được
Câu 3. Bài toán xếp ba lô
Có một balô có tải trọng m và n đồ vật, đồ vật i có trọng lượng wi và có giá trị vi. Hãy lựa
chọn các vật để cho vào balô sao cho tổng trọng lượng của chúng không quá M và tổng giá trị
của chúng là lớn nhất.
Câu 4. Bài toán ngƣời du lịch
Có n thành phố, d[i,j] là chi phí để di chuyển từ thành phố i đến thành phố j. (Nếu không có
đường đi thì d[i,j] = ). Một người muốn đi du lịch qua tất cả các thành phố, mỗi thành phố
một lần rồi trở về nơi xuất phát sao cho tổng chi phí là nhỏ nhất. Hãy xác định một đường đi
như vậy.
Câu 5. Bài toán mã đi tuần
Cho một bàn cờ có kích thước nxn (n>=3). Một con mã di chuyển theo luật cờ vua được đặt
trong 1 ô với tọa độ đầu là (x1, y1). Hãy tìm một đường đi với n2-1 bước đi sao cho trên mọi ô
trên bàn cờ đều được mã nhảy đến đúng 1 lần
Câu 6. Viết chương trình liệt kê tất cả các xâu nhị phân có đúng n chữ số (n>=3) sao cho
không có xâu con 101
Câu 7. Có n cặp vợ chồng. Hãy xếp họ vào 1 bàn tròn gồm 2n chiếc ghế sao cho
- Không có ghế thừa
- Nam nữ ngồi xen kẻ
- Vợ chồng không được ngồi cạnh nhau
18
II. THUẬT TOÁN SẮP XẾP
1. Tầm quan trọng của bài toán sắp xếp:
Sắp xếp một danh sách các đối tượng theo một thứ tự nào đó là một bài toán thường được
vận dụng trong các ứng dụng tin học. Ví dụ ta cần sắp xếp danh sách thí sinh theo tên với thứ
tự Alphabet, hoặc sắp xếp danh sách sinh viên theo điểm trung bình với thứ tự từ cao đến thấp.
Một ví dụ khác là khi cần tìm kiếm một đối tượng trong một danh sách các đối tượng bằng giải
thuật tìm kiếm nhị phân thì danh sách các đối tượng này phải được sắp xếp trước đó.
Tóm lại sắp xếp là một yêu cầu không thể thiếu trong khi thiết kế các phần mềm. Do đó
việc nghiên cứu các phương pháp sắp xếp là rất cần thiết để vận dụng trong khi lập trình.
2. Sắp xếp trong và sắp xếp ngoài
Sắp xếp trong là sự sắp xếp dữ liệu được tổ chức trong bộ nhớ trong của máy tính, ở đó ta
có thể sử dụng khả năng truy nhập ngẫu nhiên của bộ nhớ và do vậy nó thực hiện rất nhanh.
Sắp xếp ngoài là sự sắp xếp được sử dụng khi số lượng đối tượng cần sắp xếp lớn không thể
lưu trữ trong bộ nhớ trong mà phải lưu trữ trên bộ nhớ ngoài. Cụ thể là ta sẽ sắp xếp dữ liệu
được lưu trữ trong các tập tin.
3. Phát biểu bài toán:
Giả sử các đối tượng cần được sắp xếp là các mẩu tin gồm một hoặc nhiều trường. Một
trong các trường được gọi là khóa sắp xếp (key), kiểu của nó là một kiểu có quan hệ thứ tự
(như các kiểu số nguyên, số thực, chuỗi ký tự...).
Danh sách các đối tượng cần sắp xếp sẽ là một mảng của các mẩu tin vừa nói ở trên. Mục
đích của việc sắp xếp là tổ chức lại các mẩu tin sao cho các khóa của chúng được sắp thứ tự
tương ứng với quy luật sắp xếp.
Ví dụ: Cho mảng a các đối tượng, cần sắp xếp lại các thành phần (phần tử) của mảng a để
nhận được mảng (dãy) a mới với các phần tử có giá trị khóa tăng dần:
a[1].key ≤ a[2].key ≤ … ≤ a[n].key
Ðể trình bày các ví dụ minh họa chúng ta sẽ dùng PASCAL làm ngôn ngữ thể hiện và sử
dụng khai báo sau:
CONST Max = 1000;
TYPE Object = Record
Key : KeyTYPE;
OtherFields : OtherTYPE;
END
Tarray = aray [1..Max]of Object
VAR a: Tarray;
n:longint;
PROCEDURE Swap(VAR x,y:Object);
VAR temp : Object;
BEGIN
temp := x; x := y; y := temp;
END;
Cần thấy rằng thủ tục Swap có thời gian thực hiện là O(1)
19
4. Các thuật toán sắp xếp đơn giản:
a. Sắp xếp nổi bọt (sắp xếp tráo đổi - Bubble Sort):
* Ý tưởng: Chúng ta tưởng tượng rằng các mẩu tin được lưu trong một mảng dọc, qua quá
trình sắp xếp, mẫu tin nào có khóa “nhẹ” sẽ được nổi lên trên. Chúng ta duyệt tòan mảng, từ
dưới lên trên. Nếu hai phần tử ở cạnh nhau mà không đúng thứ tự tức là nếu phần tử “nhẹ hơn”
lại nằm dưới thì phải cho nó “nổi lên” bằng cách đổi chỗ hai phần tử này cho nhau. Việc này
lặp lại cho đến khi không còn phần tử nào đứng sai thứ tự.
* Mô tả cụ thể:
- Lượt thứ nhất: Xét các phần tử từ a[n] đến a[2], với mỗi phần tử a[j], so sánh khoá của nó
với khoá của phần tử a[j-1] đứng ngay trước nó. Nếu khoá của a[j] nhỏ hơn khoá của a[j-1] thì
hoán đổi a[j] và a[j-1] cho nhau. Sau luợt thứ nhất phần tử nhỏ có khóa nhỏ nhất sẽ “nổi lên” vị
trí thứ nhất
- Lượt thứ 2: Xét các phần tử từ a[n] đến a[3], và làm tương tự như trên. Sau lượt thứ 2
phần tử có khóa nhỏ thứ 2 sẽ “nổi lên” vị trí thứ 2
…
- Lượt thứ n-1: thì dãy đã được sắp xếp xong
Ví dụ: Sắp xếp mảng gồm 10 mẩu tin có khóa là các số nguyên: 5, 6, 2, 2, 10, 12, 9, 10, 9
và 3
Bảng sau ghi lại các giá trị khoá tương ứng với từng lượt thực hiện thuật toán trên.
Khóa
Lượt a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9] a[10]
Ban đầu 5 6 2 2 10 12 9 10 9 3
Lượt 1 2 5 6 2 3 10 12 9 10 9
Lượt 2 2 5 6 3 9 10 12 9 10
Lượt 3 3 5 6 9 9 10 12 10
Lượt 4 5 6 9 9 10 10 12
Lượt 5 6 9 9 10 10 12
Lượt 6 9 9 10 10 12
Lượt 7 9 10 10 12
Lượt 8 10 10 12
Lượt 9 10 12
Kết quả 2 2 3 5 6 9 9 10 10 12
20
* Chương trình:
PROCEDURE BubbleSort;
VAR i,j: integer;
BEGIN
FOR i := 1 TO n-1 DO
FOR j := n DOWNTO i+1 DO
IF a[j].key < a[j-1].key THEN
Swap(a[j],a[j-1]);
END;
* Đánh giá độ phức tạp:
Số phép toán so sánh a[j].key > a[j-1].key được dùng để đánh giá hiệu suất về mặt thời gian
cho thuật toán sắp xếp nổi bọt. Tại lượt thứ i cần n – i phép so sánh. Như vậy tổng số phép so
sánh cần thiết là:
)(2
1)n(n1...2)(n1)(ni)(nT(n) 2
1n
1i
nO
Vậy thuật toán có độ phức tạp là T(n) = )( 2nO
* Một cách cài đặt khác của thuật toán trên cũng cho độ phức tạp O(n2)
FOR i := 1 TO n-1 DO
FOR j := i+1 TO n DO
IF a[j].key < a[i].key THEN
Swap(a[j],a[i]);
b. Sắp xếp chọn (Selection Sort):
* Ý tưởng: Tìm phần tử có khóa nhỏ nhất trong dãy sau đó tráo đổi nó với phần tử đầu tiên
của dãy. Sau đó lại tiếp tục tìm phần tử có khóa nhỏ nhất (trừ phần tử đầu tiên) rồi tráo đổi với
phần tử thứ 2. Việc này lặp lại cho đến khi không còn phần tử để tráo đổi.
* Mô tả cụ thể:
- Ðầu tiên chọn phần tử có khóa nhỏ nhất trong n phần tử từ a[1] đến a[n] và hoán vị nó với
phần tử a[1].
- Chọn phần tử có khóa nhỏ nhất trong n-1phần tử từ a[2] đến a[n] và hoán vị nó với a[2].
- Tổng quát ở bước thứ i, chọn phần tử có khoá nhỏ nhất trong n-i+1 phần tử từ a[i] đến a[n]
và hoán vị nó với a[i].
- Sau n-1 bước này thì mảng đã được sắp xếp.
Ví dụ: Sắp xếp mảng gồm 10 mẩu tin có khóa là các số nguyên: 5, 6, 2, 2, 10, 12, 9, 10, 9
và 3
21
Bảng sau ghi lại các giá trị khoá tương ứng với từng lượt thực hiện thuật toán trên.
Khóa
Lượt a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9] a[10]
Ban đầu 5 6 2 2 10 12 9 10 9 3
Lượt 1 2 6 5 2 10 12 9 10 9 3
Lượt 2 2 5 6 10 12 9 10 9 3
Lượt 3 3 6 10 12 9 10 9 5
Lượt 4 5 10 12 9 10 9 6
Lượt 5 6 12 9 10 9 10
Lượt 6 9 12 10 9 10
Lượt 7 9 10 12 10
Lượt 8 10 12 10
Lượt 9 10 12
Kết quả 2 2 3 5 6 9 9 10 10 12
* Chương trình:
PROCEDURE SelectionSort;
VAR i,j,LowIndex: integer;
LowKey: KeyTYPE;
BEGIN
{1} FOR i := 1 TO n-1 DO
BEGIN
{2} LowIndex := i;
{3} LowKey := a[i].key;
{4} FOR j := i+1 TO n DO
{5} IF a[j].key < LowKey THEN
BEGIN
{6} LowKey := a[j].key;
{7} LowIndex := j;
END;
{8} Swap(a[i],a[LowIndex]);
END;
END;
* Đánh giá độ phức tạp:
Các lệnh: {2}, {3}có thời gian thực hiện là O(1).
Vòng lặp FOR {4} – {7} thực hiện n-i lần vì j chạy từ i+1 đến n, mỗi lần lặp thực hiện hết
O(1) thời gian nên nó tốn O (n-i) thời gian. Mặc khác vòng lặp {1} thực hiện n-1 lần nên thời
gian tổng cộng để thực hiện chương trình là
22
)(2
1)n(n1...2)(n1)(ni)(nT(n) 2
1n
1i
nO
c. Sắp xếp thêm dần (sắp xếp xen - Insertion Sort):
* Ý tưởng: Lần lượt xét các phần tử trong dãy, tại mỗi phần tử đang xét ta sẽ chèn vào vị trí
thích hợp trong danh sách các phần tử được xét trước đó.
* Mô tả cụ thể:
Trước hết ta xem phần tử a[1] là một dãy đã có thứ tự.
- Lượt 1, xen phần tử a[2] vào danh sách đã có thứ tự a[1] sao cho a[1], a[2] là một danh
sách có thứ tự.
- Lượt 2, xen phần tử a[3] vào danh sách đã có thứ tự a[1], a[2] sao cho a[1], a[2], a[3] là
một danh sách có thứ tự.
- Tổng quát, lượt i, xen phần tử a[i+1] vào danh sách đã có thứ tự a[1],a[2],..a[i] sao cho
a[1], a[2],.. a[i+1] là một danh sách có thứ tự.
- Phần tử đang xét a[j] sẽ được xen vào vị trí thích hợp trong danh sách các phần tử đã được
sắp trước đó a[1],a[2],..a[j-1] bằng cách so sánh khoá của a[j] với khoá của a[j-1] đứng ngay
trước nó. Nếu khoá của a[j] nhỏ hơn khoá của a[j-1] thì hoán đổi a[j-1] và a[j] cho nhau và tiếp
tục so sánh khoá của a[j-1] (lúc này a[j-1] chứa nội dung của a[j]) với khoá của a[j-2] đứng
ngay trước nó...
- Sau n-1 lượt thực hiện việc chèn thì mảng đã được sắp xếp.
Ví dụ: Sắp xếp mảng gồm 10 mẩu tin có khóa là các số nguyên: 5, 6, 2, 2, 10, 12, 9, 10, 9
và 3
Bảng sau ghi lại các giá trị khoá tương ứng với từng lượt thực hiện thuật toán trên.
Khóa
Lượt a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9] a[10]
Ban đầu 5 6 2 2 10 12 9 10 9 3
Lượt 1 5 6
Lượt 2 2 5 6
Lượt 3 2 2 5 6
Lượt 4 2 2 5 6 10
Lượt 5 2 2 5 6 10 12
Lượt 6 2 2 5 6 9 10 12
Lượt 7 2 2 5 6 9 10 10 12
Lượt 8 2 2 5 6 9 9 10 10 12
Lượt 9 2 2 3 5 6 9 9 10 10 12
23
* Chương trình:
PROCEDURE InsertionSort;
VAR i,j: integer;
BEGIN
{1} FOR i := 2 TO n DO
BEGIN
{2} J := i;
{3} WHILE (j>1) AND (a[j].key < a[j-1].key) DO
BEGIN
{4} swap(a[j], a[j-1]);
{5} j := j-1;
END;
END;
END;
* Đánh giá độ phức tạp:
Vòng lặp {1} có i chạy từ 2 đến n nên nếu gọi T(n) là thời gian để sắp n phần tử thì ta có
)(2
1)n(ni)(nT(n) 2
1n
1i
nO
5. Thuật toán sắp xếp nhanh (QuickSort):
Chúng ta vẫn xét mảng a các phần tử a[1]..a[n]. Giả sử x là 1 giá trị khóa mà ta gọi là
“chốt” . Ta phân hoạch dãy a[1]..a[n] thành hai mảng con "bên trái" và "bên phải". Mảng con
"bên trái" bao gồm các phần tử có khóa nhỏ hơn chốt, mảng con "bên phải" bao gồm các phần
tử có khóa lớn hơn hoặc bằng chốt.
Sắp xếp mảng con “bên trái” và mảng con “bên phải” thì mảng đã cho sẽ được sắp bởi vì tất
cả các khóa trong mảng con “bên trái“ đều nhỏ hơn các khóa trong mảng con “bên phải”.
Việc sắp xếp các mảng con “bên trái” và “bên phải” cũng được tiến hành bằng phương pháp
nói trên.
Một mảng chỉ gồm một phần tử hoặc gồm nhiều phần tử có khóa bằng nhau thì đã có thứ tự.
Để sắp xếp một dãy có chỉ số từ L đến H ta tiến hành các bước như sau:
- Chọn chốt x (có thể chọn ngẫu nhiên hoặc định sẵn thường là chọn x=a[(L+H)div 2])
- Phân hoạch mảng đã cho thành hai mảng con a[L]..a[x] và a[x+1]..a[H]. So sánh các phẩn
tử thuộc mảng bên trái và bên phải với chốt. Cho i chạy từ L sang phải, j chạy từ H sang trái;
nếu phát hiện một cặp ngược thứ tự: i<=j và a[i].key>=a[j].key thì phải đổi chổ hai phần tử đó
cho đến khi i>j. Lúc đó dãy ở trình trạng: khóa các phần tử đoạn L..i < khóa của x; khóa của
các phần tử đoạn j..H >= khóa của x.
- Sắp xếp mảng a[L]..a[j]
- Sắp xếp mảng a[i]..a[H]
Thủ tục QuickSort(L, H) sau dùng để sắp xếp đoạn từ L tới H. Để sắp xếp dãy số, ta gọi
QuickSort(1,n).
24
PROCEDURE QuickSort(L, H: longint);
VAR i,j : longint;
x, tmp: object;
BEGIN
i:=L;
j:=H;
x:=a[(L+H) div 2];
REPEAT
WHILE a[i].key < x.key DO inc(i);
WHILE a[j].key > x.key DO inc(j);
IF i<=j THEN
BEGIN
tmp:a[i];
a[i]:=a[j];
a[j]:=tmp;
inc(i);
dec(j);
END;
UNTIL i>j;
IF L<j THEN QuickSort(L,j);
IF i<H THEN QickSort(i, H);
END;
* Đánh giá độ phức tạp
QuickSort sẽ hoạt động rất kém trong trường hợp dãy đã được sắp sẵn. Khi đó phải mất đến
N lần gọi đệ quy và mỗi lần chỉ loại được 1 phần tử. Thời gian thực hiện trong trường hợp xấu
nhất này là khoảng N2/2 có nghĩa là O(N
2)
Trong trường hợp tốt nhất phân chia sẽ được hai nữa dãy bằng nhau. Khi đó thời gian thực
hiện của thuật toán T(N) sẽ được tính là
T(N) = 2T(N/2) + N
Hay T(N) ≈ NlogN nghĩa là O(NlogN)
Trong trường hợp trung bình thuật toán cũng có độ phức tạp khoảng 2NlogN = O(NlogN).
6. Thuật toán sắp xếp hòa nhập (trộn) hai đƣờng trực tiếp (MergeSort):
Ý tưởng của thuật toán này bắt nguồn từ việc trộn hai dãy đã được sắp xếp thành một dãy
mới cũng được sắp xếp.
a. Trộn hai dãy đã sắp xếp:
Bài toán: Cho hai mảng A và B đã được sắp xếp theo chiều không giảm. Từ hai mảng đã
cho hãy đưa các giá trị của A và B vào mảng C mà vẫn đảm bảo trật trự sắp xếp.
Giả sử ta có hai dãy đã sắp xếp như sau:
17 32 49 89 06 25 53
25
Để giải quyết bài này ta dùng hai biến duyệt từ đầu mỗi dãy. Tại mỗi bước, tiến hành so
sánh giá trị của hai phần tử tại vị trí của hai biến duyệt. Nếu phần tử nào nhỏ hơn ta đưa
phần tử đó xuống dưới dãy mới và tăng biến duyệt tương ứng lên 1. Quá trình được lặp lại
cho tới khi tất cả các phần tử của hai dãy đã được duyệt và xét
Mô phỏng thuật toán của ví dụ trên:
Ban đầu:
Bước 1:
Bước 2:
Bước 3:
Bước 4:
Bước 5:
Bước 6:
26
Bước 7:
Thủ tục trộn hai dãy đã sắp xếp trên HS tự viết.
Ta thấy rằng để thực hiện việc sắp xếp trộn thì hai dãy được trộn không phải là hai dãy
riêng biệt mà nằm trên cũng 1 dãy. Hãy nói cách khác ta trộn hai phần của một dãy chứ
không phải hai dãy riêng biệt. Ngoài ra kết quả trộn lại được lưu ngay tại dãy ban đầu chứ
không lưu ra một dãy khác.
Do vậy ta phải cải tiến thủ tục để trộn hai phần của một dãy A (với phần đầu có chỉ số từ
al đến am, phần sau có chỉ số từ am+1 đến ar). Dùng một mảng tạm C để lưu kết quả trộn
sau đó sao chép vào mảng ban đầu a.
Thủ tục này được viết như sau:
PROCEDURE Merger(VAR A: object; al,am,ar:longint);
VAR i, j, k: longint;
C:object;
BEGIN
i := al; j := am+1; k:= al;
WHILE (i<=am) and (j<=ar) DO
BEGIN
IF A[i]>A[j] THEN BEGIN C[k]:=A[j]; inc(j); END
ELSE BEGIN C[k]:=A[i]; inc(i); END;
inc(k);
END;
WHILE i<=am DO BEGIN C[k]:=A[i]; inc(i); inc(k); END;
WHILE j<=ar DO BEGIN C[k]:=A[j]; inc(j); inc(k); END;
FOR i:=al TO am DO A[i]:=C[i];
END;
b. Hòa nhập hai đường trược tiếp
Ý tưởng: Để thực hiện việc sắp xếp hòa nhập hai đường trực tiếp đầu tiên ta coi mỗi
phần tử của dãy là 1 danh sách con gồm 1 phần tử đã được sắp xếp. Tiếp theo trộn từng 2
cặp dãy con 1 phần tử kề nhau để tạo thành các dãy con 2 phần tử được sắp. Các dãy con 2
phần tử được sắp này lại được trộn với nhau tạo thành dãy con 4 phần tử được sắp. Quá
trình trên tiếp tục cho đến khi còn duy nhất 1 dãy con được sắp, đó chính là dãy ban đầu.
27
Ví dụ mô phỏng việc sắp xếp 1 dãy bằng thuật toán trên
Ta sử dụng đệ quy để viết thủ tục sắp xếp trên
PROCEDURE MegerSort(VAR A:object; L, R:
longint);
VAR mid: longint;
BEGIN
IF R>L THEN
BEGIN
mid:= (L+R) div 2;
MegerSort(A, L, Mid);
MegerSort(A, Mid+1, R);
MegerSort(A, L, Mid, R);
END;
END;
BÀI TẬP
Câu 1. Cho mảng A[1..N, 1..M] chứa các số nguyên. Hãy sắp xếp lại các giá trị của các ô
trong A sao cho
* A[i,1] ≤ A[i,2] ≤ …≤ A[i,M]
* A[1,j] ≤ A[2,j] ≤…≤ A[N,j]
Dữ liệu: cho file MANG.INP gồm N+1 dòng
+ Dòng 1: chứa hai số N và M
+ Dòng i+1 (1 ≤ i ≤ N): ghi M số A[i,1], A[i,2], ..., A[i,N]
Các số ghi trên cùng một dòng cách nhau ít nhất một dấu cách.
Kết quả ghi vào file có tên MANG.OUT
28
Ví dụ:
MANG.INP MANG.OUT
5 8
1 3 9 8 3 2 4 5
5 2 4 1 6 1 7 9
4 3 3 4 1 0 3 2
5 3 8 1 6 3 5 4
8 2 1 2 1 1 3 4
1 1 1 1 1 1 1 1
2 2 2 2 2 2 3 3
3 3 3 3 3 3 4 4
4 4 4 4 5 5 5 5
6 6 7 8 8 8 9 9
Câu 2. Xét tập F(N) tất cả các số hữu tỉ trong đoạn [0,1] với mẫu số không vượt quá N
(1<N<=100).
Ví dụ, tập F(5): 1
1
5
4
4
3
3
2
5
3
2
1
3
1
4
1
5
1
1
0
Cho trước một số nguyên dương N, viết chương trình in ra mọi phân số tối giản thuộc
F(N) theo thứ tự tăng dần của giá trị.
Câu 3. Cho N (N<=300) đoạn số nguyên [ai, bi] hãy tìm một số mà số đó thuộc nhiều đoạn
số nguyên nhất.
Ví dụ, có 5 đoạn [0,10], [2,3], [4,7], [3,5], [5,8], ta chọn số 5 thuộc 4 đoạn [0,10], [4,7],
[3,5], [5,8].
Câu 4. Cho 2 dãy số: A có n phần tử, B có m phần tử, các phần tử là các số nguyên x
Tìm số lượng giá trị trong A mà có mặt trong B
Yêu cầu: Viết số lượng tìm ra được
Dữ liệu vào: file AB.IN có dòng đầu là hai số n, m.
Từ dòng thứ 2 lần lượt là các số của A sau đó là các số của B. (0<n,m<10^6, |x|<10^9)
Kết quả ra: File AB.OUT gồm 1 dòng ghi số lượng tìm được
Ví dụ:
AB.IN AB.OUT
3 4
2 5 6 7 5 5 6
2
Câu 5. Để quản lí nhân sự ở một tỉnh nọ (có khoảng 1 triệu người), hãy sắp xếp tuổi của dân
cư ở đây theo thứ tự từ nhỏ đến lớn. Biết rằng tuổi dân cư chỉ nằm trong khoảng từ 1 đến
100.
Tuổi của dân cư được cho vào file T_IN.TXT viết liên tiếp nhau cách nhau ít nhất một
dấu cách hoặc một dấu xuống dòng. Kết quả đưa ra file T_OUT.TXT cấu trúc file
vào nhưng đã được sắp xếp.
29
Ví dụ:
T_IN.TXT T_OUT.TXT
1 2 20 4 45 62 3
2 4 55 2 4 5 100
1 2 2 2 3 4 4 4 5
20 45 55 62 100
Câu 6. Người ta có N đoạn dây xích (N<=20000), mỗi đoạn dây xích là một chuỗi các mắt
xích được nối với nhau. Các đoạn dây xích này tách rời nhau. Mỗi đoạn có không quá 20000
mắt xích.
Bằng cách cắt ra một mắt xích, sau đó hàn lại, ta có thể nối hai dây xích thành một đoạn.
Thời gian để cắt và hàn mỗi mắt xích là 1 đơn vị thời gian và được xem là bằng nhau với
mọi mắt xích.
Nhiệm vụ của bạn là phải nối chúng lại thành một đoạn xích duy nhất với thời gian ít
nhất (hay số mắt xích bị cắt và hàn lại là ít nhất).
Input:
Dữ liệu cho trong file NOIXICH.IN có cấu trúc như sau:
Dòng đầu tiên là số N, số đoạn xích.
Những dòng tiếp theo ghi N số nguyên dương, số thứ i là số mắt xích
có trong đoạn xích thứ i (1<= i <= N)
Hai số cạnh nhau trên một dòng cách nhau ít nhất là một dấu cách.
Output:
Kết quả ghi vào file NOIXICH.OUT một số duy nhất là số đơn vị thời gian mà bạn cần
nối N đoạn xích đã cho.
Ví dụ:
NOIXICH.IN NOIXICH.OUT NOIXICH.IN NOIXICH.OUT
3
4 7
6
2 4
5 7 8
9
3
Câu 7. Cho đoạn thẳng [a0,b0] và một dãy các đoạn thẳng [a1,b1], [a2,b2], [a3,b3], …,
[an,bn]. Với 0<n<106, ai, bi (i= n0, ) là số nguyên và -10
9 <ai, bi <10
9. Tìm phần đoạn thẳng
riêng của đoạn thẳng [a0,b0] tức là các điểm thuộc [a,b] nhưng không thuộc bất cứ đoạn
[ai,bi] nào với i= n1,
Yêu cầu: Chỉ ra các phần đoạn thẳng riêng tìm được. Nếu không thông báo không tìm ra.
Dữ liệu vào: File DOAN.INP có các thông tin
- Dòng đầu tiên chứa hai số nguyên a0, b0 và số n
- Dòng thứ 2 chứa các cặp số nguyên dương ai, bi (i= n1, )
30
Dữ liệu ra: File DOAN.OUT có các thông tin
- Dòng đầu tiên: ghi từ Yes hoặc No tùy theo có tồn tại đoạn riêng hay không
- Từ dòng thứ 2 trở đi ghi các đoạn riêng nếu có (mỗi đoạn riêng trên một dòng)
Ví dụ:
DOAN.INP DOAN.OUT
-3 5 4
-8 -2 8 20 -1 3 4 6
YES
-2 -1
3 4
31
Chuyên đề:
PHƢƠNG PHÁP QUY HOẠCH ĐỘNG
I. GIỚI THIỆU
Trong chương trình Tin học chuyên ngành ở các trường đại học sư phạm, phương pháp
quy hoạch động hầu như không được nhắc đến hoặc chỉ giới thiệu sơ qua nên nhiều sinh
viên mới ra trường tỏ ra lúng túng khi gặp những bài toán dạng này trong việc giảng dạy
môn Tin học dành cho các lớp chuyên Tin ở phổ thông. Hơn nữa, tài liệu, sách báo viết về
phương pháp quy hoạch động rất ít, hầu hết giáo viên đều tự tìm tòi, nghiên cứu, tự biên
soạn chương trình để giảng dạy. Mấy năm gần đây Bộ Giáo dục và Đào tạo mới chính thức
đưa ra chương trình giảng dạy dành cho các lớp chuyên Tin ở các trường phổ thông, trong
đó quy hoạch động là một chuyên đề bắt buộc trong chương trình tin học chuyên của lớp 11
chiếm một thời lượng lớn (15 tiết). Những bài toán áp dụng phương pháp quy hoạch động
cũng thường xuyên xuất hiện trong các đề thi học sinh giỏi Tin học cấp tỉnh, cấp quốc gia,
Olympic cho nên việc nắm được kiến thức, kỹ năng giải bài toán quy hoạch động của giáo
viên dạy chuyên Tin cũng như học sinh trong các đội tuyển Tin học là vấn đề rất cần thiết.
Bản thân tôi là một giáo viên giảng dạy môn Tin học trong trường THPT chuyên Lê Quý
Đôn – Khánh Hoà, đã qua nhiều năm tham gia bồi dưỡng đội tuyển học sinh giỏi môn Tin
học ở cấp trường, cấp tỉnh và qua hai năm giảng dạy lớp chuyên Tin, tôi đã rút ra được một
số kinh nghiệm trong việc giảng dạy chuyên đề quy hoạch động. Sau đây tôi xin được giới
thiệu một số kinh nghiệm trong việc sử dụng phương pháp quy hoạch động để giải một số
bài toán trong tin học.
Quy hoạch động là một phương pháp nhằm giảm thời gian chạy của các thuật toán thể
hiện các tính chất của các bài toán con gối đầu nhau (overlapping subproblem) và cấu trúc
con tối ưu (optimal substructure). Phương pháp quy hoạch động do nhà toán học Richard
Bellman pháp minh vào năm 1953.
Phương pháp quy hoạch động (dynamic programming) là một kỹ thuật được áp dụng để
giải nhiều lớp bài toán, đặc biệt là các bài toán tối ưu. Đây là phương pháp có ý nghĩa trong
việc giải quyết các bài toán thực tế.
Phương pháp này dựa trên nguyên lý tối ưu của Bellman: Nếu một dãy các lựa chọn là
tối ưu thì mọi dãy con của nó cũng là tối ưu.
Ý tưởng của phương pháp: Để không phải tính lại kết quả bài toán con ta lưu các kết quả
đã tính vào một bảng và chỉ việc sử dụng lại giá trị của nó khi cần thiết.
Khác với phương pháp chia để trị là kỹ thuật tiếp cận TOP - DOWN (đi từ trên xuống),
phương pháp quy hoạch động dùng kỹ thuật BOTTOM - UP (đi từ dưới lên):
Xuất phát từ các trường hợp riêng đơn giản nhất, có thể tìm ngay ra nghiệm. Bằng cách
kết hợp nghiệm của chúng, ta nhận được nghiệm của bài toán cỡ lớn hơn. Cứ thế tiếp tục,
chúng ta sẽ nhận được nghiệm của bài toán ban đầu.
32
Trong quá trình tìm nghiệm của bài toán từ dưới lên chúng ta sẽ sử dụng bảng để lưu giữ
kết quả của các bài toán con đã giải trước đó. Kết quả của những bài toán con này có thể là
cơ sở cho việc giải bài toán lớn hơn.
Khi giải một bài toán con, cần đến nghiệm của bài toán con nhỏ hơn, ta chỉ cần lấy kết
quả từ bảng, không cần phải giải lại. Chính vì thế mà giải thuật nhận được bằng phương
pháp này rất có hiệu quả.
Ƣu điểm của phƣơng pháp quy hoạch động: chương trình chạy nhanh.
Phạm vi áp dụng của phƣơng pháp quy hoạch động:
+ Các bài toán tối ưu: như tìm xâu con chung dài nhất, bài toán ba lô, tìm đường đi ngắn
nhất, bài toán Ôtômat với số phép biến đổi ít nhất, …
+ Các bài toán có công thức truy hồi.
Hạn chế của phƣơng pháp quy hoạch động:
Phương pháp quy hoạch động không đem lại hiệu quả trong các trường hợp sau:
+ Sự kết hợp lời giải của các bài toán con chưa chắc cho ta lời giải của bài toán lớn.
+ Số lượng các bài toán con cần giải quyết và lưu trữ kết quả có thể rất lớn, không thể
chấp nhận được.
+ Không tìm được công thức truy hồi.
II. CẤU TRÚC CHUNG CỦA CHƢƠNG TRÌNH CHÍNH:
BEGIN {Chương trình chính}
Chuẩn bị: đọc dữ liệu và khởi gán một số giá trị ban đầu;
Tạo bảng;
Tra bảng và ghi kết quả;
END.
III. CÁC BƢỚC ĐỂ GIẢI BÀI TOÁN QUY HOẠCH ĐỘNG:
1. Mô tả bài toán dưới dạng các bài toán con;
2. Tính nghiệm tối ưu của bài toán con trong trường hợp riêng đơn giản nhất;
3. Tìm các công thức đệ quy biểu diễn nghiệm tối ưu của bài toán lớn thông qua nghiệm
tối ưu của các bài toán con.
4. Tính nghiệm tối ưu từ dưới lên (bottom up) và ghi lại các nghiệm tối ưu của các bài
toán vào bảng.
5. Dựa vào bảng lưu kết quả của các bài toán đã tìm được để tìm nghiệm cho bài toán
ban đầu.
IV. MỘT SỐ VÍ DỤ
1. Dãy Fibonaci:
Đề bài: In ra màn hình 20 số hạng đầu của dãy Fibonaci.
33
Biết: F1 = 1 ; F2 = 1
Fi = Fi-1+ Fi-2 với i > 2
Giải thuật:
+ Tính nghiệm của bài toán trong trường hợp riêng đơn giản nhất.
F1 = F2 = 1
+ Tìm các công thức đệ quy biểu diễn nghiệm tối ưu của bài toán lớn thông qua nghiệm
tối ưu của các bài toán con.
Fi = Fi-1+ Fi-2 với i > 2
Mười số hạng đầu của dãy Fibonaci được biểu diễn trong bảng sau:
i 1 2 3 4 5 6 7 8 9 10
F[i] 1 1 2 3 5 8 13 21 34 55
2. Tổ hợp chập k của n phần tử:
Đề bài: Tính các phần tử của mảng C[n, k] = k
nC = số tổ hợp chập k của n phần tử, với
0 ≤ k ≤ n ≤ 20.
Biết 10 n
nn CC
k
n
k
n
k
n CCC 1
1
1
Giải thuật:
+ Tính nghiệm của bài toán trong trường hợp riêng đơn giản nhất:
FOR i := 1 TO n DO
BEGIN
C[0, i] := 1;
C[i, i] := 1;
END;
+ Tìm các công thức đệ quy biểu diễn nghiệm tối ưu của bài toán lớn thông qua nghiệm
tối ưu của các bài toán con.
FOR i := 2 To n Do
FOR j := 1 To i-1 Do C[i, j] := C[i-1,j-1] + C[i-1,j];
n\k 0 1 2 3 4 5
1 1 1
2 1 2 1
3 1 3 3 1
4 1 4 6 4 1
5 1 5 10 10 5 1
34
3. Dãy con không giảm liên tục dài nhất
Đề bài: Cho dãy số nguyên A1, A2, ... , AN. Hãy tìm dãy con B gồm một số phần tử liên
tục trong A không giảm dài nhất.
Ví dụ:
Input
9
3 2 1 4 5 8 9 6 7
Output
1 4 5 8 9
Giải thuật:
+ Gọi T[i] (i=1..N) là độ dài lớn nhất của dãy con không giảm liên tục khi có i phần tử từ
1 đến i trong A để chọn, trong đó phần tử thứ i phải được chọn.
+ Trường hợp đơn giản nhất giả sử dãy A chỉ có 1 phần tử thì T[1]=1;
+ Công thức đệ quy:
Với i ≥ 2 ta có:
Nếu A[i] ≥ A[i-1] thì T[i] = T[i-1] + 1, ngược lại T[i] = 1.
FOR i:=2 to N do
If A[i] >= a[i-1] then T[i]:=T[i-1] + 1 Else T[i]:=1;
+ Nghiệm của bài toán:
Tìm v là vị trí phần tử lớn nhất trong mảng T
v:=1;
FOR i:=1 to N do if T[i] ≥ T[v] then v:=i;
Như vậy dãy con tìm được sẽ có T[v] phần tử và phần tử cuối cùng là phần tử thứ v
trong A. Cho nên dãy con tìm được sẽ là dãy gồm các phần tử từ v – T[v] + 1 đến phần tử
thứ v của dãy A.
In dãy con tìm được
FOR i:= v-T[v]+1 to v do write(A[i], ‘ ‘);
4. Tìm dãy con không giảm dài nhất:
Đề bài: Cho một dãy n số nguyên. Hãy loại bỏ khỏi dãy một số phần tử để được một dãy
con không giảm dài nhất. In ra dãy con đó.
Ví dụ:
Input:
10
35
2 6 -7 5 8 1 -3 5 15 9
Kết quả tìm được dãy con không giảm dài nhất có 4 phần tử:
-7 -3 5 9
Giải thuật:
+ Tổ chức dữ liệu:
Gọi A là dãy ban đầu.
Gọi B[i] là số phần tử của dãy con dài nhất trong dãy có i phần tử đầu tiên A[1] .. A[i] và
A[i] được chọn làm phần tử cuối. (i [1, n])
C là dãy con không giảm dài nhất tìm được.
Truoc[i] là chỉ số của phần tử trước phần tử i (các phần tử giữ lại C).
+ Giải thuật tạo bảng: (Tính mảng B và mảng Truoc)
Trường hợp đơn giản nhất: dãy chỉ có 1 phần tử, thì B[1] := 1;
FOR i = 2 to n Do
Với mọi (j < i) và (A[j] <= A[i]), tìm B[j] lớn nhất (gọi là BMax).
B[i] := Bmax + 1;
Trước[i] := j; {j là chỉ số ứng với BMax tìm được}
Trong ví dụ trên ta có bảng sau:
i 1 2 3 4 5 6 7 8 9 10
A[i] 2 6 -7 5 8 1 -3 5 15 9
B[i] 1 2 1 2 3 2 2 3 4 4
Truoc[i] 0 1 0 3 4 3 3 7 8 8
Dãy con không giảm dài nhất có 4 phần tử: -7 -3 5 9
PROCEDURE TaoBang;
VAR i, j, BMax, chiSo :byte;
BEGIN
B[1] := 1; truoc[1]:=0;
FOR i := 2 TO n DO
BEGIN
BMax := 0; chiso:=0;
FOR j := i-1 DOwnTO 1 DO
IF (A[j] <= A[i]) and (B[j] > BMax) THEN
BEGIN
BMax := B[j];
chiSo := j;
END;
B[i] := BMax + 1; Truoc[i] := chiSo;
END;
END;
36
+ Tra bảng: để tìm các phần tử của dãy C:
Tìm phần tử lớn nhất của mảng B. (ứng với chỉ số ChiSoMax). Phần tử lớn nhất của
mảng B chính là số phần tử của dãy C.
A[ChiSoMax] là phần tử cuối của dãy C. Dựa vào mảng Truoc, ta tìm các phần tử
còn lại trong dãy C: tìm ngược từ cuối dãy lên đầu dãy.
PROCEDURE TraBang;
VAR chiSo, ChiSoMax, i : byte;
BEGIN
ChiSoMax := n;
FOR i:= n-1 DOwnTO 1 DO
IF B[i] > B[ChiSoMax] THEN ChiSoMax := i;
chiSo := ChiSoMax;
FOR i := B[ChiSoMax] DOwnTO 1 DO
BEGIN
C[i]:= A[chiSo];
chiSo := Truoc[chiSo];
END;
END;
5. Bài toán balô 1:
Đề bài: Cho n món hàng (n ≤ 50). Món thứ i có khối lượng là A[i] (số nguyên). Cần
chọn những món hàng nào để bỏ vào một ba lô sao tổng khối lượng của các món hàng đã
chọn là lớn nhất nhưng không vượt quá khối lượng W cho trước. (W ≤ 100). Mỗi món chỉ
chọn 1 hoặc không chọn.
Input:
n W
A[1] A[2] … A[n]
Ví dụ:
4 10
5 2 4 3
Output:
Tổng khối lượng của các món hàng bỏ vào ba lô.
Khối lượng của các món hàng đã chọn.
Trong ví dụ trên:
Tổng khối lượng của các món hàng bỏ vào ba lô là 10
Khối lượng các món hàng được chọn: 5 2 3
Hƣớng giải:
37
+ Tổ chức dữ liệu:
Fx[k, v] là tổng khối lượng của các món hàng bỏ vào ba lô khi có k món hàng đầu tiên
để chọn và khối lượng tối đa của ba lô là v.
Với k ϵ [1, n], v ϵ [1, W].
Nói cách khác: Khi có k món để chọn, Fx[k, v] là khối lượng tối ưu khi khối lượng tối đa
của ba lô là v.
Khối lượng tối ưu luôn nhỏ hơn hoặc bằng khối lượng tối đa:
Fx[k,v] ≤ v.
Ví dụ: Fx[4, 10] = 8 Nghĩa là trong trường hợp tối ưu, tổng khối lượng của các món
hàng được chọn là 8, khi có 4 món đầu tiên để chọn (từ món thứ 1 đến món thứ 4) và khối
lượng tối đa của ba lô là 10. Không nhất thiết cả 4 món đều được chọn.
+ Giải thuật tạo bảng:
Trường hợp đơn giản chỉ có 1 món để chọn: Ta tính Fx[1, v] với mọi v.
Nếu có thể chọn (nghĩa là khối lượng tối đa của ba lô >= khối lượng của các món hàng
thứ 1), thì chọn: Fx[1, v] := A[1];
Ngược lại ( v < A[1] ), không thể chọn, nghĩa là Fx[1, v] := 0;
* Giả sử ta đã tính được Fx[k–1 , v ] đến dòng k–1, ∀ v ∈ [1, W]. Khi có thêm món thứ k
để chọn, ta cần tính Fx[k , v] ở dòng k, ∀ v ∈ [1,W].
Nếu có thể chọn món hàng thứ k (v >= A[k]), thì có 2 trường hợp:
– Trường hợp 1: Nếu chọn thêm món thứ k bỏ vào ba lô thì:
Fx[k, v] := Fx[k–1, u ] + A[k]; Với u là khối lượng còn lại sau khi chọn món thứ k.
u = v – A[k]
– Trường hợp 2: Ngược lại, không chọn món thứ k, thì
Fx[k, v] := Fx[k–1, v ];
Trong 2 trường hợp trên ta chọn trường hợp nào có Fx[k, v] lớn hơn. Ngược lại (v <
A[k]), thì không thể chọn, nghĩa là Fx[k, v] := Fx[k–1, v];
+ Công thức đệ quy là:
If v >= A[k] Then Fx[k,v] := Max(Fx[k-1, v - A[k]] + A[k] , Fx[k-1,v])
Else Fx[k,v] := Fx[k-1, v];
Dưới đây là bảng Fx[k,v] tính được trong ví dụ trên:
A k\v 1 2 3 4 5 6 7 8 9 10
5 1 0 0 0 0 5 5 5 5 5 5
2 2 0 2 2 2 5 5 7 7 7 7
4 3 0 2 2 4 5 6 7 7 9 9
3 4 0 2 3 4 5 6 7 8 9 10
38
PROCEDURE TaoBang;
VAR k ,v : integer;
BEGIN
FOR v:=1 TO W DO
IF v >= A[1] THEN Fx[1, v] := A[1] ELSE Fx[1, v] := 0;
FOR k:= 2 TO n DO FOR v:=1 TO W DO
IF v >= A[k] THEN
Fx[k,v]:= Max(Fx[k-1,v-A[k]]+ A[k], Fx[k-1,v])
ELSE Fx[k,v]:=Fx[k-1,v];
END;
+ Giải thuật tra bảng để tìm các món hàng đƣợc chọn:
Chú ý: Nếu Fx[k, v] = Fx[k–1, v] thì món thứ k không được chọn.
Fx[n, W] là tổng khối lượng tối ưu của các món hàng bỏ vào ba lô.
Bƣớc 1: Bắt đầu từ k = n, v = W.
Bƣớc 2: Tìm trong cột v, ngược từ dưới lên, ta tìm dòng k sao cho Fx[k,v] > Fx[k–1, v].
Đánh dấu món thứ k được chọn:
Chọn[k] := true;
Bƣớc 3: v := Fx[k, v] – A[k].
Nếu v > 0 thì thực hiện bước 2, ngược lại thực hiện bước 4
Bƣớc 4: Dựa vào mảng Chọn để in ra các món hàng được chọn.
PROCEDURE TraBang;
VAR k, v: Integer;
BEGIN
k := n; v := w;
FillChar(chon,SizeOf(chon),false);
REPEAT
WHILE Fx[k,v] = Fx[k-1,v] DO Dec(k);
chon[k]:= True;
v := Fx[k,v] - A[k];
UNTIL v = 0;
FOR k := 1 TO n DO
IF chon[k] THEN Write(A[k]:5); Writeln;
END;
6. Bài toán ba lô 2
Đề bài: Cho n món hàng (n ≤ 50). Món thứ i có khối lượng là A[i] và giá trị C[i] (số
nguyên). Cần chọn những món hàng nào để bỏ vào một ba lô sao tổng giá trị của các món
39
hàng đã chọn là lớn nhất nhưng tổng khối lượng của chúng không vượt quá khối lượng W
cho trước (W ≤ 100). Mỗi món chỉ chọn 1 hoặc không chọn.
Input:
n W
A[1] C[1]
A[2] C[2]
…
A[n] C[n]
Ví dụ:
5 13
3 4
4 5
5 6
2 3
1 1
Output:
Tổng giá trị của các món hàng bỏ vào ba lô.
Khối lượng và giá trị của các món hàng đã chọn.
Trong ví dụ trên:
Tổng giá trị của các món hàng bỏ vào ba : 16
Các món được chọn:
1(3, 4) 2(4, 5) 3(5, 6) 5(1, 1)
Hƣớng dẫn giải:
Tương tự bài ba lô 1, nhưng Fx[k, v] là giá trị lớn nhất của ba lô khi có k món hàng đầu
tiên để chọn và khối lượng tối đa của ba lô là v.
Công thức đệ quy là:
If v >= A[k] Then
Fx[k,v]:= Max(Fx[k-1, v-A[k]] + C[k], Fx[k-1,v])
Else
Fx[k,v]:=Fx[k–1,v];
Chú ý: chỉ khác bài balô 1 ở chỗ dùng C[k] thay cho A[k]
40
Dưới đây là bảng Fx[k,v] tính được trong ví dụ trên:
k/v 1 2 3 4 5 6 7 8 9 10 11 12 13
1 0 0 4 4 4 4 4 4 4 4 4 4 4
2 0 0 4 5 5 5 9 9 9 9 9 9 9
3 0 0 4 5 6 6 9 10 11 11 11 15 15
4 0 3 4 5 7 8 9 10 12 13 14 15 15
5 1 3 4 5 7 8 9 10 12 13 14 15 16
7. Bài toán ba lô 3
Đề bài: Cho n loại hàng (n ≤ 50). Mỗi món hàng thuộc loại thứ i có khối lượng là A[i] và
giá trị C[i] (số nguyên). Số lượng các món hàng của mỗi loại không hạn chế. Cần chọn những
món hàng của những loại hàng nào để bỏ vào một ba lô sao tổng giá trị của các món hàng đã
chọn là lớn nhất nhưng tổng khối lượng của chúng không lượt quá khối lượng W cho trước
(W ≤ 100). Mỗi loại hàng có thể hoặc không chọn món nào, hoặc chọn 1 món, hoặc chọn
nhiều món.
Input:
n W
A[1] C[1]
A[2] C[2]
…
A[n] C[n]
Ví dụ:
5 13
3 4
4 5
5 6
2 3
1 1
Output:
Tổng giá trị của các món hàng bỏ vào ba lô.
Số lượng của các loại hàng đã chọn.
Trong ví dụ trên:
Tổng giá trị của các món hàng bỏ vào ba lô: 19
Các món được chọn:
Chọn 1 món hàng loại 1, mỗi món có khối lượng là 3 và giá trị là 4
41
Chọn 5 món hàng loại 4, mỗi món có khối lượng là 2 và giá trị là 3
Hƣớng dẫn giải:
+ Tổ chức dữ liệu:
Fx[k, v] là tổng giá trị của các món hàng bỏ vào ba lô khi có k loại hàng
đầu tiên để chọn và khối lượng tối đa của ba lô là v.
Với k [1, n], v ∈ [1, W].
X[k, v] là số lượng các món hàng loại k được chọn khi khối lượng tối đa của
ba lô là v.
+ Giải thuật tạo bảng:
* Trường hợp đơn giản chỉ có 1 món để chọn: Ta tính Fx[1, v] với mọi v:
X[1, v] = v div A[1]
Fx[1, v] = X[1, v] * C[1]
* Giả sử ta đã tính được Fx[k–1 , v ] đến dòng k–1, với mọi v ∈ [1,W].
Khi có thêm loại thứ k để chọn, ta cần tính Fx[k , v] ở dòng k, với mọi v ∈ [1,W]
Nếu ta chọn xk món hàng loại k, thì khối lượng còn lại của ba lô dành cho các loại hàng từ
loại 1 đến loại k – 1 là: u = v – xk * A[k]
Khi đó giá trị của ba lô là: Fx[k, v]= Fx[k–1,u] + xk * C[k]
Với xk thay đổi từ 0 đến yk, ta chọn giá trị lớn nhất và lưu vào Fx[k, v].
Trong đó yk = v div A[k] là số lượng lớn nhất các món hàng loại k có thể được chọn bỏ
vào ba lô, khi khối lượng tối đa của ba lô là v.
Tóm lại: công thức đệ quy là:
Fx[k,v] = Max(Fx[k-1, v – xk * A[k]] + xk * C[k])
Max xét với xk thay đổi từ 0 đến v div A[k], và v – xk * A[k] > 0
Dưới đây là bảng Fx[k,v] và X[k, v] tính được trong ví dụ trên. Bảng màu xám là X[k, v]:
k/v 1 2 3 4 5 6 7 8 9 10 11 12 13
1 0 0 0 0 4 1 4 1 4 1 8 2 8 2 8 2 12 3 12 3 12 3 16 4 16 4
2 0 0 0 0 4 0 4 0 5 1 8 0 9 1 9 1 12 0 13 1 14 2 16 0 17 1
3 0 0 0 0 4 0 4 0 5 0 8 0 9 0 10 1 12 0 13 0 14 0 16 0 17 0
4 0 0 0 0 4 0 4 0 7 1 8 0 10 2 11 1 13 3 14 2 16 4 17 3 19 5
5 0 0 1 1 4 0 5 1 7 0 8 0 10 0 11 0 13 0 14 0 16 0 17 0 19 0
42
PROCEDURE TaoBang;
VAR xk, yk, k: Byte;
FMax, XMax, v : Word;
BEGIN
FOR v:= 1 TO W DO
BEGIN
X[1, v] := v div A[1];
F[1, v] := X[1, v] * C[1];
END;
FOR k:= 2 TO n DO
FOR v:= 1 TO W DO
BEGIN
FMax := F[k-1, v] ;
XMax := 0;
yk := v div A[k];
FOR xk:= 1 TO yk DO
IF(v-xk*A[k]>0)andF[k-1,v-xk*A[k]]+xk*C[k]>FMax) THEN
BEGIN
FMax := F[k-1, v - xk * A[k]] + xk * C[k];
XMax:= xk;
END;
F[k, v] := FMax;
X[k, v] := XMax;
END;
END;
Giải thuật tra bảng:
Fx[n, W] là giá trị lớn nhất của ba lô.
Bắt đầu từ X[n, W] là số món hàng loại k được chọn.
Tính v = W – X[n, W]* A[n].
Tìm đến ô [n – 1, v ] ta tìm được X[n – 1, v]. Cứ tiếp tục ta tìm được X[1, v].
Chú ý: khi tra bảng, ta không dùng mảng Fx[k, v], nên ta có thể cải tiến: dùng 2 mảng
một chiều thay cho mảng hai chiều Fx.
8. Bài toán xâu con chung dài nhất
Đề bài: Cho hai xâu kí tự A và B. Tìm xâu kí tự C có nhiều kí tự nhất, với C vừa là xâu
con của xâu A, vừa là xâu con của xâu B. (Xâu con là xâu kí tự có được khi bỏ bớt một số kí
tự trong xâu cha).
Dƣ liệu: Vào từ tập tin văn bản XauChung.inp gồm hai dòng, mỗi dòng là một xâu kí
tự.
Kết quả: Đưa ra tập tin văn bản XauChung.out gồm 2 dòng:
• Dòng đầu là độ dài của xâu con chung dài nhất.
• Dòng thứ hai là xâu con chung C.
43
Ví dụ
XauChung.INP XauChung.OUT
CEACEEC
AECECA
4
AEEC
Hƣớng dẫn:
+ Tổ chức dữ liệu
Mảng hai chiều L[0..Max, 0..Max] để lưu kết quả các bài toán con.
+ Tạo bảng
Gọi L[i,j] là độ dài lớn nhất của dãy con chung của hai dãy khi dãy A có i phần tử và dãy
B có j phần tử để chọn A1...Ai và B1..Bj. Với i<=m và j<=n (m,n là độ dài hai xâu).
+ Trƣờng hợp đơn giản: nếu i=0 hoặc j=0 (một trong hai xâu rỗng) thì L[i,j] =0.
+ Công thức đệ quy
Nếu (i>0) and (j>0) and (Ai<>Bj)
thì có L[i,j] =Max(L[i-1,j], L[i,j-1])
Nếu (i>0) and (j>0) and (Ai=Bj) thì L[i,j] = 1+ L[i-1,j-1]
+ Tạo bảng
PROCEDURE Taobang;
VAR i,j:byte;
BEGIN
Fillchar(L,sizeof(L),0);
FOR i:=1 TO m DO
FOR j:=1 TO n DO
IF A[i]=B[j] THEN L[i,j]:= 1 + L[i-1,j-1]
ELSE L[i,j]:=Max(L[i-1,j],L[i,j-1]);
END;
+ Tra bảng
Xâu chung C:= „‟
Xuất phát từ ô L[n,m] hướng về ô L[0,0].
Nếu A[i]=B[j] chọn A[i] hoặc B[j] đưa vào đầu dãy C rồi lùi về ô L[i-1,j-1]
Nếu Ai<>Bj thì:
Nếu L[i-1,j] > L[i,j-1] lùi về L[i-1,j]
Nếu L[i-1,j] <= L[i,j-1] thì lùi về L[i,j-1]
44
PROCEDURE Trabang;
VAR i, j:byte;
BEGIN
C:=’’;
I:=m; j:=n;
REPEAT
IF A[i]=B[j] THEN
BEGIN
C:= A[i]+C;
Dec(i); Dec(j);
END
ELSE IF L[i,j-1] > L[i-1,j] THEN j:=j-1 ELSE i:=i-1
UNTIL (i=0) or (j=0);
END;
9. Bài toán chia kẹo
Đề bài: Cho n gói kẹo (n ≤ 50). Gói thứ i có A[i] viên kẹo. Cần chia các gói kẹo này cho
2 em bé sao cho tổng số viên kẹo mỗi em nhận được chênh lệch ít nhất. Mỗi em nhận
nguyên gói. Không mở gói kẹo ra để chia lại. Hãy liệt kê số kẹo trong các gói kẹo mỗi em
nhận được.
Input:
n
A[1] A[2] … A[n]
Output: Số kẹo trong các gói kẹo mỗi em nhận được, và tổng số kẹo mỗi em nhận được.
Hƣớng dẫn giải:
Gọi S là tổng số viện kẹo S := A[1] + A[2] + … + A[n];
S2 là nửa tổng số kẹo: S2 := S div 2;
Cho em bé thứ nhất chọn trước những gói kẹo sao cho tổng số viên kẹo mà em nhận
được là lớn nhất nhưng không vượt quá số kẹo S2.
Gói kẹo nào em bé thứ nhất không chọn thì em bé thứ hai chọn.
Bài toán được đưa về Bài toán ba lô 1(đã hướng dẫn ở trên).
10. Bài toán đổi tiền
Đề bài: Cho n loại tờ giấy bạc. Tờ giấy bạc thứ i có mệnh giá A[i]. Số tờ mỗi loại không
giới hạn. Cần chi trả cho khách hàng số tiền M đồng. Hãy cho biết mỗi loại tiền cần bao
nhiêu tờ sao cho tổng số tờ là ít nhất. Nếu không đổi được, thì thông báo “KHONG DOI
DUOC”. N < 50; A[i] < 256; M < 10000.
Input:
n M
A[1] A[2] … A[n]
45
Ví dụ:
3 18
3 10 12
Output: Tổng số tờ phải trả. Số tờ mỗi loại.
Hƣớng dẫn giải: Tương tự Bài toán ba lô 3
Gọi Fx[i, j ] là số tờ ít nhất được dùng để trả số tiền j đồng khi có i loại tiền từ loại 1 đến
loại i. Với i = 1 .. n; j = 1 .. M.
X[i, j] là số tờ giấy bạc loại thứ i được dùng chi trả số tiền j đồng.
* Trường hợp đơn giản chỉ có 1 loại tiền để chọn: Ta tính Fx[1, j] với mọi j
F[1,j] = j div A[1] nếu j mod A[1] = 0
F[1,j] = ∞ nếu j mod A[1] ≠ 0 (không đổi được)
* Giả sử ta đã tính được Fx[i–1 , j ] đến dòng i–1, với mọi j ∈ [1,M]. Khi có thêm loại
tiền thứ i để chọn, ta cần tính Fx[i , j] ở dòng i, với mọi j ∈ [1, M].
Nếu ta chọn k tờ loại i, thì số tiền còn lại dành cho các loại tiền khác từ loại 1 đến loại i –
1 là: u = j – k * A[k]
Khi đó tổng số tờ là: Fx[i, j]= Fx[i–1,u] + k
Với k thay đổi từ 0 đến kMax, ta chọn giá trị nhỏ nhất và lưu vào Fx[i, j].
Trong đó kMax = j div A[k] là số tờ nhiều nhất của loại tiền i để đổi số tiền j.
Tóm lại: công thức đệ quy là:
Fx[i,j] = Min(Fx[i-1, j – k * A[i]] + k)
Min xét với k thay đổi từ 0 đến j div A[i], và j – k * A[i] > 0
+
V. BÀI TẬP
Bài 1 : Túi ba gang
Trong truyện cổ tích "Cây Khế" ta đã biết rằng chim thần chở người anh với một cái túi
ba gang đến hòn đảo đầy vàng bạc châu báu. Người em băn khoăn không biết chọn đồ vật
nào cho vào túi vì chỉ có một cái túi ba gang..
Giả sử rằng trên hòn đảo kia có N đồ vật khác nhau, đồ vật thứ i có giá trị là ai và có thể
tích là bi. Cũng giả sử rằng cái túi mà người em mang đi chỉ có thể tích là M. Bạn hãy giúp
người em chọn ra trong N đồ vật trên một số đồ vật sao cho tổng thể tích của các đồ vật
được chọn không vượt quá M và tổng giá trị các đồ vật được chọn là lớn nhất.
Input: Cho trong file văn bản CAYKHE.INP
Dòng đầu tiên ghi hai số N, M (N,M≤100)
N dòng tiếp theo, dòng thứ i ghi hai số ai và bi lần lượt là giá trị và thể tích của đồ vật
thứ i (ai, bi≤100)
46
Output: Ghi ra file văn bản CAYKHE.OUT
Dòng đầu tiên ghi tổng giá trị lớn nhất có thể cho vào trong túi
Dòng thứ hai ghi số hiệu các đồ vật được cho vào trong túi. Đầu tiên ghi K là số lượng
đồ vật được chọn, tiếp theo là K số thể hiện số hiệu các đồ vật được chọn.
Ví dụ:
CAYKHE.INP CAYKHE.OUT
5 10
20 3
19 1
30 7
24 3
15 6
63
3 1 2 4
Bài 2: Kinh doanh bất động sản.
Tại thành phố Silicon, Một người nọ được thừa kế một khoản tiền N ngàn USD, người
đó quyết định đầu tư vào việc kinh doanh bất động sản bằng cách mua các mảnh đất hình
vuông có kích thước là các số nguyên, biết rằng mỗi mét vuông đất có giá trị 1 ngàn USD.
Hãy chỉ cách cho người nọ mua đất sao cho tổng số tiền mua đất đúng bằng N ngàn USD và
số mảnh đất mua được càng ít càng tốt.
Dữ liệu: Vào từ file văn bản MUADAT.INP gồm một số nguyên dương duy nhất N có giá
trị không vượt quá 60000
Kết quả: Ghi ra file văn bản MUADAT.OUT một dãy số nguyên dương xếp theo thứ tự
giảm dần là kích thước các mảnh đất mua được
Ví dụ:
MUADAT.INP MUADAT.OUT
30 4 3 2 1
Bài 3: Xây tháp
Có N khối đá hình hộp chữ nhật. Người ta muốn xây một cái tháp bằng cách chồng các
khối đá này lên nhau. Để đảm bảo an toàn, các khối đá được đặt theo nguyên tắc:
+ Chiều cao của mỗi khối là kích thước nhỏ nhất trong ba kích thước
+ Các mép của các khối đươc đặt song song với nhau sao cho không có phần nào
của khối nằm trên bị chìa ra ngoài so với khối nằm dưới.
Hãy tìm phương án xây dựng để tháp đạt được độ cao nhất.
Dữ liệu vào: Cho trong file TOWN.INP:
Dòng đầu tiên là số N
N dòng tiếp, mỗi dòng ghi 3 số nguyên dương là kích thước của một khối đá. Các
khối đá đươc đánh số từ 1 theo trình tự xuất hiện trong file.
47
Kết quả: ghi ra file TOWN.OUT:
Dòng thứ nhất ghi số M là số lượng khối đá dùng để xây tháp
M dòng tiếp theo ghi các khối xếp từ đáy tháp lên đỉnh tháp, mỗi dòng gồm 4 số theo
thứ tự K a b c, trong đó K là số hiệu khối đá, a là kích thước chọn làm đáy nhỏ, b là
kích thước chọn làm đáy lớn, c là kích thước chọn làm chiều cao.
Các số trên một dòng trong các file được ghi cách nhau ít nhất một dấu cách. Giới hạn số
khối đá không quá 5000 và các kích thước của các khối đá không quá 255.
Ví dụ:
TOWN.INP TOWN.OUT
9
7 5 5
4 4 8
1 1 5
4 2 2
5 1 5
4 2 7
2 9 2
1 3 3
5 5 5
4
1 5 7 5
9 5 5 5
5 5 5 1
4 2 4 2
Bài 4: Vòng quanh thế giới (Đề thi học sinh giỏi Toàn quốc 2003)
Trên tuyến đường của xe du lịch vòng quanh thế giới xuất phát từ bến xe x có N khách
sạn đánh số từ 1 đến N theo thứ tự xuất hiện trên tuyến đường, trong đó khách sạn i cách địa
điểm xuất phát A[i] km (i=1,2,...,N): A[1]<A[2]<....<A[n].
Để đảm bảo sức khỏe cho hành khách, theo tính toán của các nhà chuyên môn, sau khi
đã chạy được P km xe nên dừng lại cho khách nghỉ ngơi ở khách sạn. Vì thế, nếu xe dừng
lại cho khách nghỉ ở khách sạn sau khi đã đi được Q km thì lái xe phải trả một lượng phạt là
(Q-P)2.
Yêu cầu: Hãy xác định xem trên tuyến đường đến khách sạn N, xe cần dừng lại nghỉ ở
những khách sạn nào để tổng lượng phạt mà lái xe phải trả là ít nhất.
Dữ liệu vào từ file văn bản TOURISM.INP
- Dòng đầu tiên chứa số nguyên dương N (N10000)
- Dòng thứ hai chứa số nguyên dương P (P500)
- Dòng thứ ba chứa các số nguyên dương A[1]], A[2], ..., A[n] (hai số liên tiếp trên
dòng được ghi cách nhau bởi dấu cách; A[i]2000000, i=1,2,...,N)
Kết quả ghi ra file văn bản TOURISM.OUT:
- Dòng đầu ghi Z là lượng phạt mà lái xe phải trả
- Dòng thứ 2 ghi k là tổng số khách sạn mà lái xe cần dừng lại cho khách nghỉ
48
- Dòng thứ ba chứa chỉ số của k khách sạn mà xe dừng lại cho khách nghỉ (trong đó
nhất thiết phải bao gồm cả chỉ số của khách sạn thứ N)
Ví dụ:
TOURISM.INP TOURISM.OUT
4
300
250 310 550 590
500
2
2 4
Bài 5: Duyệt điểm
Xét lưới ô vuông tạo thành từ n*n dường, các đường của lưới được đánh số từ 1 đến n
từ trái qua phải và từ trên xuống dưới (1≤n≤20000). Ở mỗi hàng thứ i, người ta cho đoạn
thẳng xác định bởi hai điểm li và ri (1≤li≤ri≤n, i=1…n).
Yêu cầu: Xác định độ dài của đường đi ngắn nhất dọc theo các cạnh của lưới từ điểm
(1,1) đến điểm (n,n) và thoả mãn các điều kiện:
Chỉ đi sang phải, sang trái hoặc xuống dưới
Đi qua tất cả các điểm thuộc các đoạn thẳng đã cho
Dữ liệu: Vào từ file văn bản VISIT.INP:
Dòng đầu tiên chứa số nguyên dương n
Dòng thứ i trong n dòng sau chứa hai số nguyên dương li, ri
Kết quả: Đưa ra file văn bản VISIT.OUT một số nguyên duy nhất là độ dài của đường đi
ngắn nhất tìm được.
Ví dụ:
VISIT.INP VISIT.OUT
6
2 6
3 4
1 3
1 2
3 6
4 5
24
Bài 6: Những vị khách sộp vào nhà hàng
Một nhà hàng bắt đầu mở cửa tại thời điểm 0 và đóng cửa tại thời điểm T=2.109. Tại
cửa ra vào nhà hàng có treo một bảng hiện thị số. Tại thời điểm 0, số trên bảng là 0 và cứ
sau 1s số trên bảng giữ nguyên giá trị hoặc tăng, giảm một đơn vị. Bảng chỉ hiện thị được
các số không âm.
49
Có N vị khách sộp đi qua nhà hàng, vị khách sộp thứ i đi qua nhà hàng tại thời điểm Ti
và sở thích của ông ta là số Si. Nếu như ở thời điểm ông ta đi qua nhà hàng biển số trước cửa
nhà hàng hiện đúng số ông ta thích thì ông ta sẽ vào và tiêu một số tiền là Pi.
Yêu cầu: Hãy giúp nhà hàng điều khiển bảng số sao cho tổng số tiền mà các vị khách sộp
vào nhà hàng là lớn nhất.
Input: Cho trong file WELCOME.INP
Dòng đầu tiên ghi số nguyên dương N (N200)
Trong N dòng tiếp theo, dòng thứ i ghi thông tin về vị khách thứ i gồm ba số nguyên
dương Ti, Si và Pi.
Output: Ghi ra file WELCOME.OUT một số nguyên duy nhất là tổng số tiền lớn nhất mà
nhà hàng nhận được.
Ví dụ:
WELCOME.INP WELCOME.OUT
3
2 1 3
3 2 4
1 3 10
7
Bài 7: Di chuyển từ Tây sang Đông
Cho hình chữ nhật M x N ô vuông (các hàng đánh số từ 1 đến M từ trên xuống và các
cột đánh số từ 1 đến N từ trái sang phải), mỗi ô vuông chứa một số nguyên. Có thể di
chuyển từ một ô sang một ô khác thuộc cột bên phải cùng dòng hoặc lệch một dòng. Tìm
cách di chuyển từ cột 1 sang cột N sao cho tổng các số của các ô đi qua là nhỏ nhất.
Input: Từ file văn bản WTOE.INP
Dòng đầu tiên chứa hai số nguyên M, N (1≤M,N≤100)
Tiếp theo là M dòng, mỗi dòng ghi N số nguyên là các số nằm trên các ô vuông của
hàng tương ứng bắt đầu từ cột 1 đến cột N. Các số nguyên này có giá trị tuyệt đối
không vượt quá 30000
Output: Ghi ra file văn bản WTOE.OUT
Dòng thứ nhất ghi tổng các số trong các ô đi qua
Dòng thứ 2 ghi N số lần lượt là chỉ số hàng của các ô trên hành trình bắt đầu từ cột 1
đến cột N
Ví dụ:
WTOE.INP WTOE.OUT
2 2
2 6
3 5
9
2 1
50
Bài 8: Dãy con đối xứng dài nhất (daydx.pas)
Dãy số có A1, A2, …, AN được gọi là đối xứng nếu các cặp số ở các vị trí i và N-i+1
bằng nhau (với i=1..N). Cho trước một dãy số có N phần tử, mỗi phần tử là số nguyên. Hãy
tìm cách loại bỏ một số phần tử trong dãy để dãy thu được tạo thành một dãy đối xứng dài
nhất.
Dữ liệu vào: File văn bản DAYDX.INP có cấu trúc như sau:
+ Dòng 1: Số nguyên N (2≤N≤5000);
+ Dòng thứ 2 ghi N số nguyên là các số hạng trong dãy có giá trị tuyệt đối ≤1000, mỗi
số cách nhau một dấu cách.
Dữ liệu ra: File văn bản DAYDX.OUT với yêu cầu nhhư sau:
+ Dòng đầu ghi số nguyên M là số các các số hạng của dãy đối xứng tìm được;
+ Dòng thứ 2 ghi M số hạng của dãy tìm được, mỗi số cách nhau một dấu cách.
Ví dụ:
DAYDX.INP DAYDX.OUT
13
1 3 2 3 1 5 2 3 4 1 4 3
2
7
2 3 4 1 4 3 2
Bài 9: Nối mạng máy tính
Các học sinh khi đến thực tập trong phòng máy tính thường hay chơi trò chơi điện tử
trên mạng. Để ngăn ngừa, người trực phòng máy đã ngắt tất cả các máy tính ra khỏi mạng
và xếp chúng thành một dãy trên một cái bàn dài và gắn chặt máy xuống mặt bàn rồi đánh
số thứ tự các máy từ 1 đến N theo chiều từ trái sang phải. Các học sinh tinh nghịch không
chịu thua, họ đã quyết định tìm cách nối các máy trên bàn bởi các đoạn dây nối sao cho mỗi
máy được nối với ít nhất một máy khác. Để tiến hành công việc này, họ đã đo khoảng cách
giữa hai máy liên tiếp. Bạn hãy giúp các học sinh này tìm cách nối mạng thoả mãn yêu cầu
đặt ra sao cho tổng độ dài cáp nối phải sử dụng là ít nhất.
Dữ liệu: vào từ file văn bản CABLE.INP:
Dòng đầu tiên chứa số lượng máy N (1N25000)
Dòng thứ i trong số N-1 dòng tiếp theo chứa các khoảng cách từ máy i đến máy i+1
(i=1,2,...,N-1). Giả thiết rằng khoảng cách từ máy 1 đến máy N không vượt quá 106.
Kết quả: Ghi ra file văn bản CABLE.OUT độ dài của cáp nối cần sử dụng.
Ví dụ:
CABLE.INP CABLE.OUT
6
2
2
3
2
2
7
51
Chuyên Đề:
LÝ THUYẾT ĐỒ THỊ
I. MỘT SỐ KHÁI NIỆM
Một đồ thị (Graph) G (V,E) bao gồm một tập hợp hữu hạn V các nút hay đỉnh (Vertices)
và một tập hữu hạn E các cặp đỉnh mà ta gọi là cung (edges)
Ví dụ: Trong thực tế ta bắt gặp nhiều đồ thị như mạng máy tính, mạng điện, mạng giao
thông….Bằng hình vẽ, đồ thị có thể mô tả như sau:
a) b)
Hình 1: Đồ thị
Nếu (v1,v2) là các cặp đỉnh thuộc E thì ta nói: có một cung nối v1 và v2 hay có đường đi
trực tiếp từ v1 đến v2. Nếu cung (v1, v2) khác cung (v2,v1) thì ta có một đồ thị định hướng
(hình 1.b), lúc đó (v1,v2) được gọi là cung định hướng. Nếu thứ tự các nút trên cung không
được coi trọng thì ta gọi là đồ thị vô hướng (hinh 1.a) tức là với mọi cung (v1,v2) trong đồ thị
G, nếu cung (v1,v2) bằng cung (v2,v1) thì đó là đồ thị vô hướng
Trong hình 1.a, đi từ đỉnh 1 đến đỉnh 5 ta có nhiều cách đi, Ví dụ: 1→2→5 hay
1→2→3→5. Ta gọi mỗi cách đi như vậy là một đường đi. Tổng quát: Một đường đi (path)
từ đỉnh vp đến đỉnh vq trong đồ thị G là một dãy đỉnh vp, vi1, vi2, vi3,…,vin , vq mà (vp,vi1),
(vi1, vi2),…,(vin,vq) là các cung của G.
Đường đi đơn (Simple Path) là đường đi mà mọi đỉnh trên đó, trừ đỉnh đầu và đỉnh cuối,
đều khác nhau
Một chu trình đơn (Cycle) là đường đi đơn mà trong đó đỉnh đầu và đỉnh cuối trùng nhau.
Ví dụ trong hình 1.a có một số chu trình: 1→2→4→1; 2→5→4→2.
Trong đồ thị G, hai đỉnh vi và vj gọi là liên thông (Connected) nếu tồn tại ít nhất một
đường đi từ vi đến vj
Một đồ thị G gọi là liên thông nếu với mọi cặp định phân biệt vi và vj trong V(G) đều có
đường đi từ vi đến vj. Hình 1.a là đồ thị liên thông còn đồ thị ở hình 1.b không liên thông vì
không có đường đi từ đỉnh 1 đến đỉnh 2
Trong đồ thị, đôi khi người ta cần gán một giá trị để thể hiện một thông tin liên quan đến
cung (gọi là trọng số), trong trường hợp này đồ thị được gọi là đồ thị có trọng số. Ví dụ,
52
mạng lưới giao thông đường bộ giữa các thành phố với trọng số ứng với mỗi tuyến đường
giữa hai thành phố là độ dài của tuyến đường đó
Hình 2: Đồ thị có trọng số
+ Đối với đồ thị vô hướng G=(V,E). Xét một cạnh eE. nếu e=(u, v) thì ta nói hai đỉnh u và
v là kề nhau và cạnh e này liên thuộc với đỉnh u và đỉnh v
+ Với một đỉnh v trong đồ thì vô hướng, ta định nghĩa bậc (degree) của v, ký hiệu là deg(v)
là số cách cạnh liên thuộc với v
II. BIỂU DIỄN ĐỒ THỊ
1. Danh sách đỉnh kề
Đối với mỗi đỉnh trong đồ thị trong hình 3, ta có danh sách các đỉnh kề tương ứng:
Hình 3
Danh sách các đỉnh kề
Đỉnh Các đỉnh kề
A B, F
B A, C, , F
C B, D
D B, C, E, F
E D F
F A, B, D, E
Để lưu trữ đồ thị ở trong file, ta có thể sử dụng danh sách các đỉnh kể. Với đồ thị trong
hình 3, cấu trúc của file lưu trữ nó trong file DOTHI.INP như sau:
DOTHI.INP Giải thích
6
B F
A C D F
B D
B C E F
D F
A B D E
- Dòng đầu là số đỉnh của đồ thị
- Dòng thứ 2 là các đỉnh kề của đỉnh A
- Dòng thứ 3 là các đỉnh kề của đỉnh B
….
- Dòng thứ 7 là các đỉnh kề của đỉnh F
53
2. Ma trận kề
Danh sách các đỉnh kề của đồ thị ở hình 3 có thể được biểu diễn lại bằng một ma trận A
có kích thước 6x6 trong đó giá trị của ô (i,j) thể đỉnh i và j kề nhau. Quy ước như sau:
Với quy ước đó, đồ thị ở hình 3 được biểu diễn lại bằng ma trận:
Đỉnh A B C D E F
A 0 1 0 0 0 1
B 1 0 1 1 0 1
C 0 1 0 1 0 0
D 0 1 1 0 1 1
E 0 0 0 1 0 1
F 1 1 0 1 1 0
Ví dụ : Ma trận kề của đồ thị có hướng G=(V,E) dưới đây sẽ được biểu diễn bởi ma trận:
Hình 4
A B C D E
A 0 1 1 0 1
B 0 0 0 1 0
C 0 1 0 1 0
D 1 0 0 0 0
E 0 0 0 1 0
+ Ma trận biểu diễn đồ thị vô hướng là ma trận đối xứng, điều này không đúng với đồ thị
có hướng
3. Danh sách cạnh
Ngoài hai cách biểu diễn trên ta có thể biểu diễn đồ thị bằng cách đưa ra tất cả các cạnh
của đồ thị
Đồ thị trong hình 3 được biểu diễn trong file DOTHI.INP như sau
DOTHI.INP Giải thích
A B
A F
B C
B D
B F
C D
D E
D E
E F
Mỗi dòng là một
cạnh của đồ thị
nối hai đỉnh kề
nhau
54
III. DUYỆT ĐỒ THỊ
Cho đồ thị G=(V,E), duyệt đồ thị là cách thăm tất cả các đỉnh của đồ thị theo một trật tự
nào đó
Ví dụ: Cho đồ thị hình 5.a thì ta có hai trật tự thăm các đỉnh của đồ thị là tìm kiếm theo
chiều sâu (hình 5.b) và tìm kiếm theo chiều rộng (hình 5.c) với đỉnh xuất phát là đỉnh 1
a) b) c)
Hình 5
1. Tìm kiếm theo chiều sâu
a. Ý tưởng:
Xuất phát từ một đỉnh u trong đồ thị, ta lần lượt duyệt qua tất cả các lân cận v của u, nếu
v chưa được thăm thì thực hiện thăm v. Quá trình duyệt đồ thị được tiếp tục với đỉnh v sao
cho tất cả các đỉnh của đồ thị đều được thăm.
b. Giải thuật
PROCEDURE DFS(u);
{u là một đỉnh của đồ thị G}
BEGIN
Thăm u;
FOR các đỉnh kề v của u DO
IF v chưa được thăm THEN DFS(v)
END;
c. Chương trình:
Để viết được chương trình dựa vào giải thuật ở trên, ta cần trả lời các câu hỏi
+ Thăm u là thực hiện công việc gì? Đây là câu lệnh mang tính tổng quát, việc thăm u
còn tùy thuộc vào từng yêu cầu cụ thể để thực hiện các công việc khác nhau
+ Làm thế nào để xác định được v là đỉnh kề của u? Nếu ta biểu diễn đồ thị bằng ma trận
A thì chỉ cần kiểm tra giá trị A[u,v], nếu bằng 1 thì v là đỉnh kề của u
55
+ Làm thế nào để biết được đỉnh v chưa được thăm? Khi thăm một đỉnh nào đó ta thực
hiện đánh dấu nó đã được thăm.
Thủ tục tìm kiếm đồ thị theo chiều sâu:
{A là ma trận biểu diễn đồ thị. Chon là mảng dùng để
đánh dấu khi 1 đỉnh được thăm}
PROCEDURE DFS(u:integer);
VAR v:integer;
BEGIN
Write(u:3); chon[u]:=true;
FOR v:=1 TO N DO
IF(Chon[v]=False)and(A[v,u]=1)THEN DFS(v);
END;
d. Mở rộng: {Khử đệ quy}
+ Nhắc lại về STACK: Đó là bộ nhớ do chương trình Pascal tự động tạo ra khi thực hiện
các chương trình con đệ quy, STACK có cơ chế hoạt động là “vào trước ra sau” tức là cái gì
được đưa vào STACK trước thì được lấy ra sau
+ Để chuyển được chương trình con đệ quy sang khử đệ quy ta chỉ cần tạo ra bộ nhớ
STACK có cơ chế đưa vào/lấy ra như trong chương trình đệ quy. Thông thường người ta
dùng kiểu mảng hoặc con trỏ để tạo bộ nhớ STACK.
+ Viết lại giải thuật theo cách khử đệ quy
+ Tham khảo thủ tục tìm kiếm theo chiều sâu bằng cách khử đệ quy
PROCEDURE DFS(u:integer);
VAR v,dinh:integer;
chon:Array[0..100]of boolean;
Stack:Array[0..100] of integer;
BEGIN
Fillchar(chon,sizeof(chon),false);
{Đưa 1 đỉnh vào Stack}
dinh:=1; Stack[1]:=u; chon[u]:=true;
write(u:3);
WHILE dinh>=1 DO
BEGIN
{lấy 1 đỉnh ra khỏi Stack}
u:=Stack[dinh];dinh:=dinh-1;
IF chon[u]=false THEN write(u:3);
chon[u]:=true;
56
FOR v:=N DOWNTO 1 DO
IF(A[u,v]=1)and(chon[v]=false)THEN
BEGIN
{Đưa 1 đỉnh vào Stack}
dinh:=dinh+1; Stack[dinh]:=v;
END;
END;
END;
2. Tìm kiếm theo chiều rộng
a. Ý tưởng:
Xuất phát từ 1 đỉnh u trong đồ thị, sau khi thăm tất cả các đỉnh kề v của u, tiếp tục thăm
các đỉnh kề của v. Cứ tiếp tục như vậy cho đến khi tất cả các đỉnh của đồ thị được thăm.
b. Giải thuật:
Để thực hiện tìm kiếm theo chiều rộng ta sử dụng bộ nhớ Queue (ngược với Stack), cơ
chế của nó là “ vào trước ra trước, vào sau ra sau”
+ Giải thuật liệt kê từng bước
B1: Đưa u vào Queue
B2:Nếu Queue chưa rỗng thì lấy 1 đỉnh trong Queue ra và gán cho p. thăm p
B3: Xét tất cả các đỉnh kề v của p, nếu v chưa được thăm thì đưa vào Queue. Quay lại B2
+ Giải thuật giải mã Pascal
PROCEDURE BFS(u)
BEGIN
Push(u,Q); {đưa u vào Queue;}
WHILE Q chưa rỗng DO
BEGIN
u:=POP(Q); {lấy 1 đỉnh trong Queue ra và gán cho u}
Thăm u;
FOR các lân cận v của u DO
IF v chưa được thăm THEN Push(v,Q);
END;
END;
c. Chương trình:
Tham khảo thủ tục sau đây
PROCEDURE BFS(u:integer);
VAR v,dinh,dau:integer;
chon:Array[0..100]of boolean;
Q:Array[0..100] of integer;
57
BEGIN
Fillchar(chon,sizeof(chon),false);
dinh:=1;
dau:=1;
write(u:3);
Q[1]:=u;
chon[u]:=true;
WHILE dinh>=dau DO
BEGIN
u:=Q[dau];
dau:=dau+1;
IF chon[u]=false THEN write(u:3);
chon[u]:=true;
FOR v:=N DOWNTO 1 DO
IF(A[u,v]=1)and(chon[v]=false)THEN
BEGIN
dinh:=dinh+1;Q[dinh]:=v;
END;
END;
END;
IV. TÍNH LIÊN THÔNG TRONG ĐỒ THỊ
Khái niệm liên thông trong đồ thị: Một đồ thị G gọi là liên thông nếu với mọi cặp định
phân biệt vi và vj trong V(G) đều có đường đi từ vi đến vj.
Bài toán: Cho một đồ thị G=(V,E) Hãy cho biết các đỉnh trong đồ thị liên thông với nhau
hay không?
Ta có thể áp dụng giải thuật phép duyệt đồ thị để kiểm tra tính liên thông trong đồ thị
bằng cách so sánh số lượng đỉnh được thăm với số đỉnh của đồ thị G. Nếu bằng nhau thì G
là liên thông, ngược lại không liên thông. Để biết được số đỉnh được thăm trong giải thuật
DFS hoặc BFS ta dùng thêm một biến để đếm. Hai giải thuật DFS và BFS có thể được sửa
lại như sau:
PROCEDURE DFS(u, dem);
{u là một đỉnh của đồ thị G}
BEGIN
Thăm u; inc(dem);
FOR các đỉnh kề v của u DO
IF v chưa được thăm THEN
DFS(v,dem)
END;
58
PROCEDURE BFS(u, dem)
BEGIN
Push(u,Q); {đưa u vào Queue;}
WHILE Q chưa rỗng DO
BEGIN
u:=POP(Q); {lấy 1 đỉnh trong Queue ra và gán cho u}
Thăm u;inc(dem);
FOR các lân cận v của u DO
IF v chưa được thăm THEN Push(v,Q);
END;
END;
*Những thay đổi được gạch chân
Trong chương trình chính ta có thể viết như sau:
Dem≔0;
DFS(u); {BFS(u);}
IF dem=N THEN Write(‘Đồ thị liên thông’)
ELSE Write(‘Đồ thị không liên thông’);
Trong trường hợp đồ thị không liên thông thì đồ thị sẽ được chia thành nhiều tập đỉnh mà
trong mỗi tập đỉnh các đỉnh có đường đi với nhau nhưng không có đường đi đến các đỉnh
trong tập đỉnh khác. Mỗi tập đỉnh như vậy ta gọi là một thành phần liên thông trong đồ thị.
Ví dụ đồ thị sau có 3 thành phần liên thông:
Trong đó hai thành phần liên thông đầu tiên có 4 đỉnh, thành phần còn lại chỉ có một đỉnh
duy nhất. Ta dễ dàng thấy rằng trong thành phần liên thông đầu tiên có 4 đỉnh (1, 2, 3, 4) và
từ 1 đỉnh bất kỳ trong 4 đỉnh này có thể đi đến các đỉnh còn lại nhưng không có đường đi
đến các đỉnh 5, 6, 7, 8, 9. Đỉnh 9 cũng được xem là một thành phần liên thông vì nó không
có đường đi đến tất cả các đỉnh còn lại.
Hai giải thuật DFS và BFS chỉ thực hiện thăm được các đỉnh trong một thành phần liên
thông nên ta có thể sử dụng hai giải thuật này để liệt kê các thành phần liên thông trong đồ
thị.
Đoạn chương trình sau liệt kê tất cả các thành phần liên thông của đồ thị:
59
FOR u=1 TO N DO
IF đỉnh u chưa được thăm THEN
BEGIN
DFS(u); {BFS(u)}
Writeln;
END;
Trong thủ tục DFS (BFS) khi thực hiện thăm 1 đỉnh thì in đỉnh đó ra màn hình
V. CHU TRÌNH TRONG ĐỒ THỊ
Khái niệm chu trình đơn: Một chu trình đơn (Cycle) là đường đi đơn mà trong đó đỉnh
đầu và đỉnh cuối trùng nhau.
Bài toán: Cho đồ thị G=(V,E). Hãy cho biết trong G có chu trình đơn hay không?
Dựa vào định nghĩa về chu trình đơn ta có thể giải quyết bài toán bằng cách trong quá
trình duyệt đồ thị, tại đỉnh đang xét nếu đỉnh đó có nhiều hơn 1 đỉnh kề đã được thăm thì kết
luận có chu trình đơn. Ngược lại nếu đã xét hết tất cả các đỉnh mà không có đỉnh nào như
trên thì ta kết luận trong G không có chu trình đơn.
Ví dụ 1: Cho đồ thị:
Trong đồ thị trên có một đường đi đơn là 1→2→3→4
Đỉnh đang
xét
Các đỉnh
trong đƣờng
đi đơn
Đỉnh kề đã có
trong đƣờng đi
đơn
Chu
trình
1 0 0 Không
2 1 1 Không
3 1, 2 2 Không
4 1, 2, 3 3, 1 Có
Như vậy trong đường đi đơn 1→2→3→4 khi xét đến đỉnh 4 ta thấy đỉnh 4 có hai đỉnh kề
(1 và 3) tuy nhiên 2 đỉnh này được có trong đường đi đơn nên ta kết luận đồ thị đã cho có
chu trình
Ví dụ 2: Cho đồ thị:
60
Trong đồ thị trên có các đường đi đơn là (1→5→2); (1→5→3); (1→5→4). Trong các
đường đi đơn này chỉ có đỉnh 5 là có 2 đỉnh kề tuy nhiên khi xét đến đỉnh 5 thì luôn có 1
đỉnh kề đã có trong đường đi (đỉnh 1) đỉnh kề còn lại chưa xét. Để xét đến các đường đi đơn
trong dồ thị ta sử dụng thuật toán duyệt đồ thị nên ta cũng có thể áp dụng thuật toán này để
kiểm tra tính chu trình trong đồ thị.
Qua 2 ví dụ trên ta rút ra kết luận sau: Trong quá trình duyệt đồ thị, tại một đỉnh đang
được thăm, ta xét các đình kề của nó, nếu số lượng đỉnh kề của nó đã được thăm lớn hơn 1
thì đồ thị đang xét có chu trình.
Thuật toán sau đây áp dụng giải thuật DFS để kiểm tra tính chu trình trong đồ thị liên
thông, trong đó A là ma trận có kích thước N, u là đỉnh đang xét
FUNCTION KT(A,N,u):Boolean;
BEGIN
Kt≔false; dem≔0;
FOR v≔1 TO N DO
BEGIN
IF có cung (u,v) và v đã được thăm THEN inc(dem);
IF dem>1 THEN exit(true) {có chu trình}
ELSE IF có cung (u,v) và v chưa được thăm THEN KT(A,N,v);
END; exit(false); {không có chu trình}
END;
VI. ĐƢỜNG ĐI NGẮN NHẤT
1. Một nguồn một đích
a. Bài toán:
Cho đồ thị có trọng số G=(V,E), các đỉnh được đánh số từ 1 đến N. Yêu cầu tìm đường đi
ngắn nhất giữa hai đỉnh u và v cho trước
Ví dụ đồ thị G=(V,E) được cho như sau:
Một số đường đi từ đỉnh 1 đến đỉnh 2
+ 1→2 (độ dài 20)
+ 1→4→2 (độ dài 18)
+ 1→4→3→2 (độ dài 19)
+ 1→6→5→4→2 (độ dài 17)
+ 1→6→5→4→3→2 (độ dài 18)
Trong các đường đi liệt kê ở trên thì đường đi 1→6→5→4→2 có độ dài 17 là đường đi
ngắn nhất từ đỉnh 1 đến đỉnh 2. Như vậy để tìm đường đi ngắn nhất giữa hai điểm bất kỳ ta
có thể liệt kê tất cả các đường đi giữa hai đỉnh rồi so sánh để tìm ra đường đi ngắn nhất. Tuy
vậy, giữa hai đỉnh bất kỳ trong một đồ thị liên thông sẽ có bao nhiêu đường đi? Câu trả lời là
61
vô số vì một cung trong đường đi có thể lặp lại nhiều lần. Để tìm đường đi ngắn nhất giữa
hai đỉnh bất kỳ ta dùng thuật toán Dijkstra. Trong ví dụ trên ta gọi đỉnh 1 là đỉnh nguồn,
đỉnh 2 là đỉnh đích
b. Giải thuật
Giải thuật Dijkstra tìm kiếm đường đi ngắn nhất từ đỉnh u đến đỉnh v trong đồ thị có N
đỉnh được lưu trữ bằng ma trận lân cận A, trong đó A[i,j] là giá trị đường đi của cung (i,j)
Thuật toán liệt kê từng bƣớc
Bước 0:
+ Khởi tạo tập S rỗng. Tập S dùng để đánh dấu một đỉnh đã được chọn hay chưa.
+ Tập D có N phần tử dùng để lưu trữ độ dài ngắn nhất từ đỉnh u đến các đỉnh còn
lại.Lúc đầu gán D[i]=A[i,u] với 1≤i≤N; m=u;
Bước 1: Nếu m=v thì kết thúc
Bước 1: Chọn đỉnh m sao cho khoảng cách từ m đến u là nhỏ nhất và m chưa có trong tập S
Bước 2: Đưa m vào tập S
Bước 3: Nếu i chưa đưa vào S và D[m]+A[m,i]<D[i] thì gán D[i]=D[m]+A[m,i]. Quay
lại B1
Thuật toán giải mã Pascal
PROCEDURE Dijkstra(A,u,v);
BEGIN
S=rỗng; D[i]:=A[u,i]; (1≤i≤N)
Đưa u vào S
m:=u;
WHILE m<>v DO
BEGIN
Chọn m sao cho mS và m=min(D[i]) (1≤i≤N)
Đưa m vào S
FOR tất cả lân cận i của m DO
IF iS và D[i]>D[m]+A[m,i] THEN D[i]:=D[m]+A[m,i]
END;
END;
Kết quả: D[v] là độ dài đường đi nhỏ nhất từ u đến v
Đối với đồ thị mô tả trong ví dụ trên,ta có thể mô tả quá trình tìm kiếm đường đi như sau:
62
Bƣớc Tập S
Đỉnh
đƣợc
chọn
D 1 2 3 4 5 6
Khởi
tạo Rỗng 20 11 2
1 1 6 20 11 7 2
2 1, 6 5 20 10 7 2
3 1, 6, 5 4 17 16 10 7 2
4 1, 6, 5, 4 3 17 16 10 7 2
5 1, 6, 5, 4,
3 2
Ta có thể giải thích bảng trên như sau:
+ Khởi tạo: Lúc đầu tập S rỗng, giá trị của D[i]=A[i,u] (1≤i≤6), D[i]= nghĩa là không
có đường đi trực tiếp từ u đến i
+ Bước 1: Đưa đỉnh 1 vào S. Đỉnh m=6 được chọn vì 6S và D[6]=2 (nhỏ nhất trong
mảng D). Lúc này ta so sánh giá trị của D[i] và D[m]+A[m,i] để gán lại giá trị nhỏ hơn cho
D[i] với 1≤i≤6
Ta có:
i = 1 2 3 4 5 6
D[i]= 20 11 2
D[6]+A[6,i]= x 2+ 2+ 2+ 2+5 2+
* Lưu ý: Đỉnh 1 đã đưa vào tập S nên ta không thực hiện tính D[6]+A[6,1]
Trong bảng trên ta thấy D[6]+A[6,5] (=7)<D[5](= ) nên gán lại D[5]= D[6]+A[6,5],
các giá trị khác giữ nguyên
Sau khi gán lại, ta có giá trị của mảng D là
i= 1 2 3 4 5 6
D[i]= 20 11 7 2
+ Bước 2: Đưa đỉnh 6 đã chọn ở Bước 1 vào tập S. Đỉnh tiếp theo được chọn là đỉnh 5
(Do D[5]=7 là nhỏ nhất, đỉnh 6 và đỉnh 1 đã có trong S nên không được chọn). Thực hiện so
sánh và gán lại mảng D tương tự như Bước 1
+ ….
+ Bước 5: Đưa đỉnh 3 vào tập S. Đỉnh được chọn là đỉnh 2, do 2 trùng với đỉnh v (v=2)
nên thuật toán kết thúc.
Kết quả: D[v=2]=17 là giá trị nhỏ nhất của đường đi từ u (u=1) đến v (v=2);
63
c. Nhận xét về giải thuật
+ So với thuật toán thông thường, thuật toán Dijkstra thực hiện nhanh hơn do không cần
phải tìm ra tất cả đường đi từ đỉnh nguồn đến đỉnh đích. Độ phức tạp của Dijkstra là O(n2)
do trong thuật toán sử dụng một vòng lặp WHILE – DO và 1 vòng lặp FOR – DO lồng nhau
+ Thuật toán trên chỉ mới đưa ra được giá trị nhỏ nhất của đường đi từ đỉnh nguồn đến
đỉnh đích chứ không chỉ ra đường đi giữa chúng
d. Vấn đề đường đi trong thuật toán Dijkstra
Ta có nhận xét như sau: Phép gán D[i]:=D[m]+A[m,i]chỉ được thực hiện khi điều kiện
D[i]>D[m]+A[m,i]được thỏa mãn, tức là giá trị của mảng D tại đỉnh i thay đổi, điều này
cũng đồng nghĩa với việc độ dài đường đi ngắn nhất từ đỉnh u đến đỉnh i sẽ bằng độ dài
đường đi ngắn nhất từ đỉnh u đến đỉnh m cộng với giá trị A[m,i], như vậy đường đi ngắn
nhất từ u đến i sẽ đi qua m. Kết luận m là đỉnh trước của i. Với kết luận này, trong thuật
toán Dijkstra ta cần phải lưu lại đỉnh trước của đỉnh i là m khi gán lại giá trị của D[i].
Lưu ý: Do lúc đầu ta khởi tại D[i]:=A[u,i] nên ta cũng khởi tạo đỉnh trước của đỉnh i là
đỉnh u.
Thuật toán được sửa lại như sau:
PROCEDURE Dijkstra(A,u,v);
BEGIN
S=rỗng;
D[i]:=A[u,i];luu[i]:=u; (1≤i≤N)
Đưa u vào S
m:=u;
WHILE m<>v DO
BEGIN
Chọn m sao cho mS và m=min(D[i]) (1≤i≤N)
Đưa m vào S
FOR tất cả lân cận i của m DO
IF iS và D[i]>D[m]+A[m,i] THEN
BEGIN
D[i]:=D[m]+A[m,i]
Luu[i]:=m;
END;
END;
END;
Kết quả: Đường đi ngắn nhất từ u đến v được lưu trong mảng một chiều LUU, giá trị của
đường đi ngắn nhất từ u đến v và D[v]
Đối với giải thuật trên ta có thể mô tả lại quá trình tìm kiếm đường đi như sau:
64
Bƣớc Tập S
Đỉnh
đƣợc
chọn
D 1 2 3 4 5 6
Khởi
tạo 1
20
(1)
(1) 11
(1)
(1) 2
(1)
1 1, 6 6 20(1)
(5)
11(1)
7(6)
2(1)
2 1, 6, 5 5 20(1)
(5)
10(5)
7(6)
2(1)
3 1, 6, 5, 4 4 17(4)
16(4)
10(5)
7(6)
2(1)
4 1, 6, 5, 4, 3 3 17(4)
16(4)
10(5)
7(6)
2(1)
5 1, 6, 5, 4, 3,
2 2
* Giá trị của mảng LUU nằm ở chỉ số trên trong 5 cột cuối. LUU có giá trị
i 1 2 3 4 5 6
Luu[i] X 4 4 5 6 1
Với cách lưu đường đi như vậy ta có thể tìm đường đi ngắn nhất từ đỉnh 1 đến đỉnh 2
bằng cách dò ngược đường đi: đỉnh cuối cùng là đỉnh 2, trước 2 là 4, trước 4 là 5, trước 5 là
6, trước 6 là 1, suy ra đường đi ngắn nhất từ 1 đến 2 là: 2←4←5←6←1. Với mảng LUU
như trên, ta có đoạn chương trình in đường đi như sau:
WHILE u<>v DO
BEGIN
Write( u,‘→’);
U:=Luu[u];
END;
Write(v);
2. Một nguồn mọi đích
Bài toán: Cho đồ thị G=(V,E) có các đỉnh được đánh số thứ tự từ 1 đến N. Yêu cầu tìm
đường đi ngắn nhất từ đỉnh u đến tất cả các đỉnh còn lại.
Từ bài toán một nguồn một đích ta có thể áp dụng để tìm đường đi ngắn nhất từ một đỉnh
nguồn u đến tất cả các đỉnh còn lại bằng câu lệnh: FOR i:=1 TO n DO Dijkstr(A,u,i); Tuy
nhiên, trong ví dụ trên, thuật toán Dijkstra áp dụng cho việc tìm đường đi ngắn nhất từ đỉnh
1 đến đỉnh 2, tất cả các đỉnh đã được đưa vào tập S, điều này cũng đồng nghĩa với việc trong
mảng D, giá trị D[i] là độ dài đường đi ngắn nhất từ đỉnh 1 đến đỉnh i và trong mảng Luu
cũng tìm được đường đi ngắn nhất từ đỉnh 1 đến các đỉnh còn lại. Ví dụ:
+ Đỉnh 1 đến đỉnh 6: 6←1 (độ dài 2)
+ Đỉnh 1 đến đỉnh 5: 5←6←1 (độ dài 7)
+ Đỉnh 1 đến đỉnh 4: 4←5←6←1 (độ dài 10)
65
+ Đỉnh 1 đến đỉnh 3: 3←4←5←6←1 (độ dài 16)
Để giải quyết một bài toán trên ta chỉ cần sửa lại thuật toán Dijkstra như sau:
PROCEDURE SHORTEST-PATH(A,u);
BEGIN
S=rỗng;
D[i]:=A[u,i];luu[i]:=u; (1≤i≤N)
Đưa u vào S
m:=u;
WHILE số đỉnh trong S<N DO
BEGIN
Chọn m sao cho mS và m=min(D[i]) (1≤i≤N)
Đưa m vào S
FOR tất cả lân cận i của m DO
IF iS và D[i]>D[m]+A[m,i] THEN
BEGIN
D[i]:=D[m]+A[m,i]
Luu[i]:=m;
END;
END;
END;
* Cách tìm đường đi dựa vào mảng LUU được thực hiện giống bài toán một nguồn một
đích
3. Bài toán mọi nguồn mọi đích
Trong thuật toán một nguồn mọi đích, kết quả là mảng LUU dùng để lưu đường đi ngắn
nhất từ 1 đỉnh đến các đỉnh còn lại. Bằng cách sử dụng mảng này ta có thể đưa ra đường đi
ngắn nhất cũng như độ dài ngắn nhất giữa hai đỉnh u và v bất kỳ bằng đoạn chương trình
sau:
DOdai:=0;
WHILE u<>v DO
BEGIN
Write(u,‘→’)
DOdai:=DOdai+A[u,luu[u]];
U:=luu[u];
END; Write(v);
Ngoài cách áp dụng giải thuật Dijkstra người ta thường dùng đến giải thuật Floyd để tìm
đường đi ngắn nhất giữa tất cả các đỉnh trong đồ thị
66
VII. CÂY KHUNG VÀ CÂY KHUNG CỰC TIỂU
1. Cây khung
Bài toán: Trong một khu du lịch có N điểm vui chơi, người ta tính toán nếu phải xây hết
tất cả các con đường để đi từ một điểm bất kỳ đến các điểm còn lại thì tốn quá nhiều tiền, do
vậy chỉ cần xây dựng 1 số con đường cần thiết để từ 1 điểm có thể đi đến tất cả các điểm
còn lại. Ví dụ với khu du lịch có 6 điểm vui chơi, nếu xây dựng hết tất cả các con đường từ
1 đỉnh đến 5 đỉnh còn lại thì ta có mô hình 6.a, tuy nhiên ta chỉ cần xây dựng 5 con đường
như hình 6.b hoặc 6.c để tiết kiệm chi phí:
a) b) c)
Hình 6
Hình 6.b và 6.c là hai cách để xây dựng các con đường sao cho từ một đỉnh trong đồ thị
có đường đi đến tất cả các đỉnh còn lại. Ta có bài toán tổng quát: Cho đồ thị liên thông
G=(V,E), trong E đã có một số cạnh. Hãy tìm cách loại bỏ một số cạnh sao cho đồ thị vẫn
liên thông nhưng không có chu trình đơn. Ta nói đây là bài toán tìm cây khung của một đồ
thị. Đồ thị 6.b và 6.c là hai cây khung tìm kiếm theo chiều sâu và tìm kiếm theo chiều rộng
của đồ thị có đỉnh xuất phát là A. Ngoài ra ta có thể liệt kê một số cây khung khác của đồ thị
hình 6
Cây khung là đồ thị liên thông và không có chu trình đơn
2. Tìm cây khung theo chiều sâu và cây khung theo chiều rộng
Để tìm cây khung của một đồ thị ta ứng dụng hai giải thuật duyệt đồ thị là tìm kiếm theo
chiều sâu (DFS – Depth-First Search) và tìm kiếm theo chiều rộng (BFS-Breadth-First
Search)
Bài toán tổng quát: Cho đồ thị G=(V,E) gồm N đỉnh và M cung. Hãy tìm cây khung theo
chiều sâu và cây khung theo chiều rộng
Dữ liệu vào: Cho bởi tệp tin văn bản DFS.INP (Hình 5a)
67
+ Dòng đầu ghi hai số nguyên N và M
+ M dòng tiếp theo, mỗi dòng gồm 2 số nguyên là một cung của đồ thị
Dữ liệu ra: được ghi vào 2 tập tin văn bản:
+ Tập tin SAU.OUT gồm N-1 dòng, mỗi dòng chứa 2 số nguyên là 1 cung trong cây
khung (hình 5b)
+ Tập tin RONG.OUT gồm N-1 dòng, mỗi dòng chứa 2 số nguyên là 1 cung trong cây
khung (hình 5c)
Ví dụ:
DFS.INP SAU.OUT RONG.OUT
8 10
1 2
1 3
2 4
2 5
3 6
3 7
4 8
5 8
6 8
7 8
1 2
2 4
4 8
8 5
8 6
6 3
3 7
1 2
1 3
2 4
2 5
3 6
3 7
4 8
a. Cây khung theo chiều sâu
Khi duyệt đồ thị theo DFS, ta sẽ có được một cây khung theo chiều sâu, tuy nhiên thủ tục
trên chỉ đưa ra thứ tự các đỉnh được duyệt. Trong bài toán ví dụ cần phải đưa ra các cung
của cây khung, do vậy khi đỉnh v thỏa mãn yêu cầu là đỉnh kề của u và v chưa được thăm thì
ta đưa ra cung (u,v) là một cung trong cây khung cần tìm. Thủ tục DFS được sửa lại:
PROCEDURE
DFS_KH(u:integer);
BEGIN
Đánh dấu u được chọn
FOR các lân cận v của u DO
IF v chưa được chọn THEN
BEGIN
In cung (u, v);
DFS(v);
END;
END;
68
b. Cây khung theo chiều rộng
Tương tự như tìm cây khung theo chiều sâu, ta cũng chỉ cần biến đổi một chút trong giải
thuật BFS để tìm cây khung theo chiều rộng
PROCEDURE BFS(u)
BEGIN
Push(u,Q); {đưa u vào Queue;}
WHILE Q chưa rỗng đo
BEGIN
u:=POP(Q); {lấy 1 đỉnh trong Q ra và gán cho u}
FOR các lân cận v của u DO
IF v chưa được thăm THEN
BEGIN
In cung (u,v);
Push(v,Q);
END;
END;
END;
3. Cây khung cực tiểu
a. Khái niệm
Đối với bài toán tìm cây khung ta không quan tâm đến việc đó có phải là đồ thị G có
trọng số hay không vì chỉ cần tìm ra một đồ thị liên thông không có chu trình đơn có đầy đủ
các đỉnh của G là được. Tuy nhiên trong đồ thị có trọng số, các cây khung lại có giá trị khác
nhau, giá trị của cây khung là tổng trọng số của các cung trong cây khung đó, vấn đề là phải
tìm ra cây khung có giá trị nhỏ nhất. Cây khung như vậy gọi là cây khung cực tiểu
b. Bài toán:
Cho đồ thị có trọng số G= (V,E) gồm N đỉnh và M cung. Yêu cầu tìm ra cây khung có giá
trị nhỏ nhất của G
Ví dụ: Đồ thị hình 7 và cây khung cực tiểu tương ứng với nó
a. Đồ thị G b. Cây khung cực tiểu
Hình 7
69
Cây khung cực tiểu cũng là một cây khung nên nó phải đảm bảo các tính chất là liên
thông và không có chu trình. Tất nhiên số cung của nó bằng n-1 đỉnh
Để giải bài toán này ta có nhiều cách khác nhau
Cách 1: Tìm tất cả các cây khung của đồ thị G rồi so sánh để có được cây khung có giá
trị nhỏ nhất.
Nhận xét: Đây là cách làm mà nhiều người sẽ nghĩ đến đầu tiên khi xây dựng cây khung
cực tiểu của một đồ thị, tuy nhiên cách làm này có nhiều nhược điểm như độ phức tạp lớn,
khó tìm cây khung, mất nhiều thời gian
Cách 2 (giải thuật Prim): Tạo một đồ thị T có 2 đỉnh thuộc cung có trọng số nhỏ nhất
trong G. Đưa cung (1, 3) có trọng số nhỏ nhất trong G vào T. Đối với hai đỉnh 1, 3 có các
cung liên thuộc là (1, 2); (1, 4); (2, 3); (3, 4); (3, 5); (3, 6) trong đó cung (3, 6) có trọng số nhỏ
nhất (bằng 10) nên ta đưa cung này vào T. 3 đỉnh 1, 3, 6 trong G có các cung liên thuộc với nó
là (1, 2); (1, 4); (2, 3); (3, 4); (3, 5); (4, 6), cung (4,6) có trọng số nhỏ nhất nên đưa (4,6) vào
T. Cung tiếp theo được đưa vào T là (1, 4) hoặc (2, 3), ta thấy rằng khi đưa (1, 4) vào T thì
trong T xuất hiện chu trình còn khi đưa (2, 3) vào thi không xuất hiện chu trình nên ta chọn (2,
3). Cung tiếp theo là (2, 5). Đến đây ta có T là đồ thị liên thông, không có chu trình, bao gồm
tất cả các đỉnh trong G và có 5 cung. Ta nói T là cây khung cực tiểu của G
Ý tưởng của cách làm trên là: lúc đầu chọn 1 cung nhỏ nhất trong G để đưa vào T, sau đó
chọn 1 cung nhỏ nhất trong G liên thuộc với 1 đỉnh trong T để đưa vào T với điều kiện cung
đưa vào T không tạo ra chu trình trong T. Quá trình này lặp lại đến khi số cung trong T bằng
số đỉnh -1
Cách 3 (Giải thuật Karuskal): Tạo đồ thị T bao gồm các đỉnh trong G và tập cung bằng
rỗng. Trong G, chọn cung có trọng số nhỏ nhất là (1, 3) để đưa vào T và loại khỏi G, tiếp
theo chọn cung (4, 6) để đưa vào T vì (4, 6) là nhỏ nhất trong G (sau khi xóa (1, 3)) và khi
đưa vào T không tạo ra chu trình. Với quy luật chọn cung nhỏ nhất trong G để đưa vào T và
không tạo ra chu trình trong T thì các cung tiếp theo được đưa vào T là (2, 5), (3, 6), (2, 3).
Chú ý: Sau khi chọn cung nhỏ nhất trong G, dù có đưa vào T hay không thì ta cũng thực
hiện xóa cung đó trong G.
Ý tưởng của cách làm trên là: Chọn cung nhỏ nhất trong G để đưa vào T với điều kiện nó
không tạo ra chu trình trong T, quá trình được lặp lại cho đến khi số cung trong T bằng số
đỉnh -1
Ý tưởng tìm cây khung cực tiểu theo cách 2 và 3 là ý tưởng của hai giải thuật Prim và
Karuskal. Bây giờ ta đi vào 2 giải thuật này
c. Giải thuật Prim
PROCEDURE Prim(G) {tìm cây khung cực tiểu
của đồ thị G theo giải thuật Prim}
BEGIN
Chọn cung nhỏ nhất trong G đưa vào T
FOR i:=1 TO số đỉnh-2 DO
BEGIN
70
Đưa cung có trọng số nhỏ nhất trong G và
liên thuộc với 1 đỉnh trong T vào T
END; {T là cây khung cực tiểu của G}
d. Giải thuật Karuskal
PROCEDURE Karuskal(G) {tìm cây khung cực tiểu
của đồ thị G theo giải thuật Karuskal}
BEGIN
Tạo T rỗng;
WHILE số cung trong T< số đỉnh -1 DO
BEGIN
v:= cung trong G có trọng số nhỏ nhất;
Đưa v vào T;
IF T có chu trình THEN loại v khỏi T;
Loại v khỏi G;
END;
e. So sánh Prim và Karuskal
Trong Prim ta chọn cạnh có trọng số nhỏ nhất và liên thuộc với một đỉnh trong T, tức là v
sẽ có một đỉnh trong T và 1 đỉnh ngoài T, còn trong karuskal, ta cũng chọn 1 cạnh có trọng
số nhỏ nhất nhưng không nhất thiết phải liên thuộc với một đỉnh trong G. Chính vì vậy trong
Prim khi đưa v vào T ta không cần kiểm tra T có chu trình hay không.
BÀI TẬP
Bài 1: Dự tiệc bàn tròn
Có n nhà khoa học đánh số 1, 2, ..., n và 26 lĩnh vực khoa học ký hiệu A, B, C, ..., Z.
Thông tin về người thứ i được cho bởi một xâu ký tự Si gồm các chữ cái in hoa thể hiện
những lĩnh vực khoa học mà người đó biết.
Ví dụ: S2 = 'ABCXYZ' cho biết nhà khoa học thứ 2 có hiểu biết về các lĩnh vực A, B, C,
X, Y, Z.
Một lần cả n nhà khoa học đến dự một bữa tiệc. Chủ nhân của bữa tiệc định xếp n nhà
khoa học ngồi quanh một bàn tròn, nhưng một vấn đề khiến chủ nhân rất khó xử là các nhà
khoa học của chúng ta có hiểu biết xã hội tương đối kém, nên nếu như phải ngồi cạnh một ai
đó không hiểu biết gì về các lĩnh vực của mình thì rất khó nói chuyện.
Vậy hãy giúp chủ nhân xếp n nhà khoa học ngồi quanh bàn tròn sao cho hai người bất kỳ
ngồi cạnh nhau phải có ít nhất một lĩnh vực hiểu biết chung, để các nhà khoa học của chúng
ta không những ăn ngon mà còn có thể trò chuyện rôm rả.
Dữ liệu: Vào từ file văn bản PARTY.INP. Trong đó:
Dòng 1: Ghi số n
71
n dòng tiếp theo, dòng thứ i ghi xâu ký tự Si
Kết quả: Ghi ra file văn bản PARTY.OUT gồm n dòng.
Dòng thứ i ghi nhà khoa học ngồi tại vị trí i của bàn (Các vị trí trên bàn tròn được
đánh số từ 1 đến n theo chiều kim đồng hồ)
Lƣu ý:
n 20
Nếu có nhiều cách xếp thì chỉ cần chỉ ra một cách
Nếu không có cách xếp thì ghi vào file PARTY.OUT một dòng: NO SOLUTION
Ví dụ:
PARTY.INP PARTY.OUT PARTY.INP PARTY.OUT PARTY.INP PARTY.OUT
6
AV
DIQR
DV
CQ
AC
DR
1
3
6
2
4
5
10
AX
BI
ABTX
AS
IK
KS
BE
AB
EK
AK
1
3
2
5
6
4
8
7
9
10
6
AB
BC
CD
DE
EF
FG
NO SOLUTION
Hƣớng dẫn:
Ta chuyển bài toán về đồ thị: Mỗi nhà khoa học tương ứng với một đỉnh của đồ thị, trên
đồ thị có cung (i,j) nếu nhà khoa học thứ i có chung 1 lĩnh vực với nhà khoa học thứ j.
Sau khi đã chuyển về đồ thị ta chỉ cần áp dụng thuật toán tìm kiếm chu trình đi qua tất cả
các đỉnh
Bài 2: Bữa tiệc
Một bữa tiệc lớn có N khách tham dự trong một căn nhà có nhiều phòng, mỗi người
khách đều được phát một phiếu ăn có ghi số thứ tự khi đến tham gia. Tuy nhiên họ có thể
không quen nhau. Để tránh việc xếp một người khách vào một phòng mà người đó không
biết tất cả mọi người trong phòng, ban tổ chức quyết định lập ra danh sách những người
quen biết nhau sau đó sắp xếp họ vào cùng một phòng. Danh sách nhanh chóng được lập ra
dưới dạng hai người quen nhau sẽ được ghi trên một dòng bằng số thứ tự của họ, tuy nhiên
công việc chia nhóm lại gặp khó khăn vì số lượng khách tham dự quá đông. Dựa vào danh
72
sách đã có, hãy giúp ban tổ chức chia nhóm và cho biết số lượng khách của nhóm đông nhất.
Kích thước và số lượng của phòng không hạn chế
Dữ liệu vào: Tập tin văn bản NHOM.INP
+ Dòng đầu ghi số nguyên dương N (2≤N≤10000)
+ Các dòng tiếp theo ghi hai số x và y với ý nghĩa hai người khách có số thứ tự x và y
quen nhau
Dữ liệu ra: Ghi vào tập tin văn bản NHOM.OUT
+ Danh sách mỗi nhóm được ghi trên một dòng
+ Dòng cuối cùng là số lượng của nhóm khách đông nhất
Các số cách nhau ít nhất một dấu cách
Ví dụ:
NHOM.INP NHOM.OUT
10
1 6
1 7
2 3
2 8
3 8
3 9
4 5
4 10
5 10
8 9
1 6 7
2 3 8 9
4 5 10
4
Hƣớng dẫn: Áp dụng giải thuật tìm kiếm trên đồ thị để xác định các thành phần liên
thông
Bài 3: Cây khung
Cho đồ thị G=(V,E) có N đỉnh và M cung. Yêu cầu viết chương trình tìm cây khung theo
chiều sâu và cây khung theo chiều rộng của đồ thị G.
Dữ liệu vào: tập tin văn bản CAYKHUNG.INP
+ Dòng đầu gồm 2 số nguyên N và M
+ M dòng tiếp theo, mỗi dòng 2 số nguyên là một cung của G
Dữ liệu ra: tập tin văn bản CAYKHUNG.OUT
+ N-1 dòng đầu tiên, mỗi dòng ghi hai số nguyên là một cung trong cây khung theo chiều
sâu
73
+ N-1 dòng tiếp theo, mỗi dòng ghi hai số nguyên là một cung trong cây khung theo
chiều rộng
Ví dụ:
CAYKHUNG.INP CAYKHUNG.OUT
6 8
1 2
1 5
2 3
2 6
3 6
3 4
4 6
5 6
1 2
2 3
3 4
4 6
6 5
1 2
1 5
2 3
2 6
3 4
Hƣớng dẫn: Sử dụng giải thuật tìm kiếm theo chiều rộng và tìm kiếm theo chiều sâu để
tìm cây khung tương ứng
Bài 4: Nối mạng
Cho N máy tính được đánh số từ 1 đến N. Chi phí nối mạng giữa máy i và máy j là Cij.
Yêu cầu: Tìm cách nối dây cáp sao cho các máy tính trong mạng là đều kết nối được với
nhau và chi phí là nhỏ nhất
Dữ liệu vào: Đọc từ file văn bản NOIMANG.INP có nội dung
+ Dòng đầu là số nguyên N
+ Tiếp theo là một ma trận kích thước NxN gồm các số nguyên, trong đó giá trị của ô (i,j)
cho biết khoảng cách từ máy i đến máy j. Trên cùng một dòng các số cách nhau một dấu
cách
Giới hạn: 2≤N≤1000
Dữ liệu ra: Ghi vào file văn bản NOIMANG.OUT gồm
+ Dòng 1 là chi phí nhỏ nhất
+ Từ dòng 2 trở đi mỗi dòng là một cặp cạnh được nối
NOIMANG.INP NOIMANG.OUT
6
0 16 3 12 0 0
16 0 12 0 7 0
37
1 3
2 5
74
3 12 0 13 16 10
12 0 13 0 0 5
0 7 16 0 0 16
0 0 10 5 16 0
3 2
3 6
6 4
Hƣớng dẫn: Áp dụng giải thuật Karuskal hoặc Prim để tìm cây khung cực tiểu
Bài 5: Trong làng có một phú ông nổi tiếng khoe khoang, môt hôm thấy đám trẻ con đang
chơi, để khỏe ruộng đất của mình, phú ông thách đố: Phú ông có rất nhiều ruộng đất, các
thửa ruộng được đánh số từ 1 đến N và đều có đường đi đến nhau, đố trẻ con hãy chỉ ra tất
cả các cách để ông ta có thể đi từ thửa ruộng thứ i đến thửa ruộng thứ j sao cho mỗi thửa
ruộng được đi qua không quá 1 lần. Đám trẻ lần lượt kể ra các đường đi nhưng do số lượng
ruộng đất của phú ông quá nhiều nên kể mãi mà không hết.
Yêu cầu: Hãy lập trình để giúp trẻ con trong làng thực hiện liệt kê các đường đi theo yêu
cầu của phú ông
Dữ liệu vào: file văn bản DD.INP
+ Dòng đầu gồm 3 số nguyên N, u, v (2≤N≤1000; 1≤u,v≤N)
+ Các dòng tiếp theo gồm 2 số nguyên i và j là một cung trong đồ thị G, hai số cách nhau
bằng 1 dấu cách
Dữ liệu ra: file văn bản DD.OUT
+Mỗi đường đi được ghi trên một dòng
+ Hai số trên mỗi dòng cách nhau bằng 3 ký tự -->
Ví dụ:
DD.INP DD.OUT
5 1 5
1 2
1 3
1 4
2 4
2 5
3 4
4 5
1-->2-->4-->5
1-->2--> 5
1-->3--> 4-->2-->5
1-->3--> 4--> 5
1-->4-->2-->5
1-->4-->5
Bài 6: Bạn Nam vừa mới chuyển nhà đến thành phố XYZ, ở thành phố này Nam có một
người bạn thân là An, sau khi liên lạc An và Nam quyết đinh gặp nhau tại một quán café
trong thành phố. Vì mới chuyển đến nên Nam không biết chọn địa điểm nào và cũng không
biết đường đi. Địa chỉ nhà của An, Nam và các quán café trong thành phố được đánh số từ 1
đến N, hãy giúp 2 bạn tìm quán café gần nhà hai bạn nhất và chỉ đường cho Nam đến quán.
75
Dữ liệu vào: từ file DIADIEM.INP
+ Dòng đầu ghi 3 số nguyên N, u, v trong đó u và v là địa điểm của nhà bạn Nam và An
+ Các dòng tiếp theo mỗi dòng ghi 3 số nguyên x, y, z trong đó z là độ dài đường đi từ
điểm x đến y
Dữ liệu ra: ghi vào file DIADIEM.OUT
+ Dòng đầu là địa điểm m của quán café mà hai bạn chọn
+ Dòng thứ 2 là tổng tổng độ dài đường đi của hai bạn Nam và An đến m
+ Dòng thứ 3 là đường đi từ nhà bạn Nam đến quán café m
Ví dụ:
DIADIEM.IPN DIADIEM.OUT
7 3 5
1 2 14
1 7 10
2 3 13
1 5 15
3 4 28
3 5 30
3 7 35
4 5 40
5 6 3
6 7 5
36
6
6←5←3
Hƣớng dẫn: Áp dụng giải thuật Dijkstral để tìm đường đi ngắn nhất từ nhà An và nhà
Nam đến tất cả các quan café trong thành phố, quán café được chọn là quán có tổng độ dài
ngắn nhất từ nhà Nam và An đến quán đó
76
MỤC LỤC Chuyên đề: PHÂN TÍCH THIẾT KẾ THUẬT TOÁN................................................................... 1 PHẦN 1: THUẬT TOÁN VÀ PHÂN TÍCH THUẬT TOÁN ........................................................ 1
I. KHÁI NIỆM BÀI TOÁN VÀ THUẬT TOÁN ......................................................................... 1 1. Khái niệm bài toán ................................................................................................................... 1
2. Khái niệm thuật toán: ............................................................................................................... 1 3. Các bước giải bài toán trên máy tính:....................................................................................... 1
a. Xác định bài toán: ............................................................................................................ 1 b. Thiết kế hoặc lựa chọn thuật toán: ................................................................................... 2 c. Viết chương trình: ............................................................................................................. 2
d. Kiểm thử và hiệu chỉnh: ................................................................................................... 2
e. Viết tài liệu ........................................................................................................................ 3
II. PHÂN TÍCH THUẬT TOÁN................................................................................................... 3 1. Độ phức tạp của thuật toán: ...................................................................................................... 3
a. Dùng mẫu chuẩn .............................................................................................................. 3 b. Phân tích thuật toán ......................................................................................................... 3
2. Kí pháp độ phức tạp của thuật toán .......................................................................................... 4 3. Các cách xác định độ phức tạp của thuật toán.......................................................................... 4
a. Quy tắc hằng số ................................................................................................................ 4 b. Quy tắc cộng ..................................................................................................................... 4 c. Quy tắc lấy max ................................................................................................................ 4
d. Quy tắc nhân .................................................................................................................... 5
4. Các thuật ngữ thường dùng cho độ phức tạp thuật toán ........................................................... 5
5. Đánh giá thời gian thực hiện chương trình .............................................................................. 5 6. Một số ví dụ ............................................................................................................................. 6
BÀI TẬP .......................................................................................................................................... 7
PHẦN 2: THIẾT KẾ THUẬT TOÁN .............................................................................................. 9 I. THUẬT TOÁN DUYỆT ............................................................................................................. 9
1. Phương pháp: ............................................................................................................................ 9 2. Giải các bài toán cấu hình tổ hợp bằng thuật toán duyệt ........................................................ 10
a. Tổ hợp: ........................................................................................................................... 10 b. Chỉnh hợp lặp: ................................................................................................................ 12
c. Chỉnh hợp không lặp: ..................................................................................................... 13 3. Bài toán 8 quân hậu ................................................................................................................ 15
BÀI TẬP ........................................................................................................................................ 17 II. THUẬT TOÁN SẮP XẾP ....................................................................................................... 18
1. Tầm quan trọng của bài toán sắp xếp: .................................................................................... 18
2. Sắp xếp trong và sắp xếp ngoài .............................................................................................. 18 3. Phát biểu bài toán: .................................................................................................................. 18 4. Các thuật toán sắp xếp đơn giản: ............................................................................................ 19
a. Sắp xếp nổi bọt (sắp xếp tráo đổi - Bubble Sort): .......................................................... 19 b. Sắp xếp chọn (Selection Sort): ....................................................................................... 20
c. Sắp xếp thêm dần (sắp xếp xen - Insertion Sort): ........................................................... 22
5. Thuật toán sắp xếp nhanh (QuickSort): .................................................................................. 23 6. Thuật toán sắp xếp hòa nhập (trộn) hai đường trực tiếp (MergeSort): ................................... 24
a. Trộn hai dãy đã sắp xếp: ................................................................................................ 24 b. Hòa nhập hai đường trược tiếp ...................................................................................... 26
BÀI TẬP ........................................................................................................................................ 27
77
Chuyên đề: PHƢƠNG PHÁP QUY HOẠCH ĐỘNG ................................................................... 31 I. GIỚI THIỆU ............................................................................................................................. 31 II. CẤU TRÚC CHUNG CỦA CHƢƠNG TRÌNH CHÍNH: ................................................... 32 III. CÁC BƢỚC ĐỂ GIẢI BÀI TOÁN QUY HOẠCH ĐỘNG: .............................................. 32
IV. MỘT SỐ VÍ DỤ ..................................................................................................................... 32 1. Dãy Fibonaci: ......................................................................................................................... 32 2. Tổ hợp chập k của n phần tử: ................................................................................................. 33 3. Dãy con không giảm liên tục dài nhất .................................................................................... 34 4. Tìm dãy con không giảm dài nhất: ......................................................................................... 34
5. Bài toán balô 1: ....................................................................................................................... 36 6. Bài toán ba lô 2 ....................................................................................................................... 38 7. Bài toán ba lô 3 ....................................................................................................................... 40
8. Bài toán xâu con chung dài nhất ............................................................................................ 42 9. Bài toán chia kẹo .................................................................................................................... 44 10. Bài toán đổi tiền ................................................................................................................... 44
V. BÀI TẬP ................................................................................................................................... 45
Chuyên Đề: LÝ THUYẾT ĐỒ THỊ ................................................................................................ 51 I. MỘT SỐ KHÁI NIỆM ............................................................................................................. 51
II. BIỂU DIỄN ĐỒ THỊ ............................................................................................................... 52
1. Danh sách đỉnh kề ................................................................................................................ 52 2. Ma trận kề ............................................................................................................................. 53 3. Danh sách cạnh ..................................................................................................................... 53
III. DUYỆT ĐỒ THỊ .................................................................................................................... 54 1. Tìm kiếm theo chiều sâu ........................................................................................................ 54
a. Ý tưởng: .......................................................................................................................... 54 b. Giải thuật ........................................................................................................................ 54 c. Chương trình: ................................................................................................................. 54
d. Mở rộng: {Khử đệ quy} .................................................................................................. 55 2. Tìm kiếm theo chiều rộng ...................................................................................................... 56
a. Ý tưởng: .......................................................................................................................... 56 b. Giải thuật: ...................................................................................................................... 56 c. Chương trình: ................................................................................................................. 56
IV. TÍNH LIÊN THÔNG TRONG ĐỒ THỊ .............................................................................. 57
V. CHU TRÌNH TRONG ĐỒ THỊ ............................................................................................. 59 VI. ĐƢỜNG ĐI NGẮN NHẤT .................................................................................................... 60
1. Một nguồn một đích ............................................................................................................... 60 a. Bài toán: ........................................................................................................................ 60 b. Giải thuật ........................................................................................................................ 61 c. Nhận xét về giải thuật ..................................................................................................... 63 d. Vấn đề đường đi trong thuật toán Dijkstra .................................................................... 63
2. Một nguồn mọi đích ............................................................................................................... 64 3. Bài toán mọi nguồn mọi đích ................................................................................................. 65
VII. CÂY KHUNG VÀ CÂY KHUNG CỰC TIỂU .................................................................. 66 1. Cây khung ............................................................................................................................... 66
2. Tìm cây khung theo chiều sâu và cây khung theo chiều rộng ................................................ 66 a. Cây khung theo chiều sâu ............................................................................................... 67 b. Cây khung theo chiều rộng ............................................................................................. 68
3. Cây khung cực tiểu ................................................................................................................. 68 a. Khái niệm ....................................................................................................................... 68
78
b. Bài toán: ......................................................................................................................... 68
c. Giải thuật Prim ............................................................................................................... 69 d. Giải thuật Karuskal ........................................................................................................ 70 e. So sánh Prim và Karuskal .............................................................................................. 70
BÀI TẬP ........................................................................................................................................ 70
TÀI LIỆU THAM KHẢO
[1]. Tài liệu giáo khoa chuyên Tin Quyển 1, 2, 3 – Hồ Sỹ Đàm (chủ biên)
[2]. Tuyển chọn các bài toán Tin học – Nguyễn Xuân My (chủ biên)
[3]. 150 Bài toán Tin học – Lê Minh Hoàng
[4]. Một số vấn đề chọn lọc trong Tin Học – Nguyễn Xuân My
[5]. Cấu trúc dữ liệu và giải thuật – Lê Minh Hoàng
[6]. Cấu trúc dữ liệu và giải thuật – Đỗ Xuân Lôi
[7]. Giải thuật và lập trình – Lê Minh Hoàng
[8]. Programming Challenges – Miguel A. Revilla
[9]. The Algorithm Design Manual – Steven S. Skiena