You are on page 1of 28

MỤC LỤC

1 Mô hình thuật toán sinh..................................................................................................... 3

1.1 Giới thiệu Thuật toán sinh (Generation) .................................................................... 3

1.2 Mô hình Thuật toán sinh (Generation) ....................................................................... 3

1.3 Ví dụ ........................................................................................................................... 3

1.3.1 Ví dụ 1: Thuật toán sinh phần tử tiếp thep của dãy nhị phân độ dài n ................ 3

1.3.2 Ví dụ 2: Thuật toán sinh tổ các phần tử của dãy tổ hợp chập k của n phần tử .... 4

1.3.3 Ví dụ 3: Thuật toán sinh hoán vị của n phần tử................................................... 4

1.3.4 Ví dụ 4: Thuật toán liệt kê các số tự nhiên có tổng là một số n cho trước .......... 5

2 Mô hình thuật toán đệ quy ............................................................................................... 6

2.1 Giới thiệu Thuật toán đệ quy (Recursion) .................................................................. 6

2.2 Mô hình Thuật toán đệ quy (Recursion) .................................................................... 6

2.2.1 Đệ quy tuyến tính (Linear Recursion) ................................................................. 6

2.2.2 Đệ quy nhị phân (Binary Recursion) ................................................................... 6

2.3 Ví dụ ........................................................................................................................... 8

2.3.1 Ví dụ 1: Hàm tính giai thừa bằng đề quy ............................................................ 8

2.3.2 Ví dụ 2: Tính tổ hợp chập K của N bằng đệ quy ................................................. 8

3 Mô hình Thuật toán quay lui............................................................................................ 9

3.1 Giới thiệu Thuật toán quay lui (Back track)............................................................... 9

3.2 Mô hình Thuật toán quay lui (Back track) ................................................................. 9

3.3 Ví dụ ......................................................................................................................... 10

4 Mô hình thuật toán tham lam ......................................................................................... 12

4.1 Giới thiệu Thuật toán tham lam (Greedy Algorithm) .............................................. 12

4.2 Mô hình Thuật toán tham lam (Greedy Algorithm) ................................................. 12

4.3 Ví dụ ......................................................................................................................... 12

5 Mô hình Thuật toán chia để trị....................................................................................... 14

5.1 Giới thiệu .................................................................................................................. 14


5.2 Mô hình .................................................................................................................... 14

5.3 Ví dụ ......................................................................................................................... 15

5.3.1 Ví dụ Tìm kiếm nhị phân................................................................................... 15

5.3.2 Ví dụ Quicksort ................................................................................................. 16

6 Mô hình thuật toán nhánh cận........................................................................................ 18

6.1 Giới thiệu thuật toán nhánh cận (Branch-And-Bound) ............................................ 18

6.2 Mô hình thuật toán nhánh cận (Branch-And-Bound) .............................................. 19

6.3 Ví dụ ......................................................................................................................... 19

7 Mô hình thuật toán quy hoạch động .............................................................................. 20

7.1 Giới thiệu thuật toán quy hoạch động (Dynamic Programming) ............................. 20

7.2 Các bước cơ bản để giải một bài toán quy hoạch động: .......................................... 22

7.3 Ví dụ về các bài toán có thể giải bằng phương pháp quy hoạch động ..................... 22

7.3.1 Bài toán Tính N! GT.PAS .............................................................................. 22

7.3.2 Bài toán Tính dãy Fibonaci FIBO.PAS .................................................... 23

7.3.3 Bài toán Tính tổng của dãy số SUM.PAS ...................................................... 23

7.3.4 TRIANGLE PASCAL (Tam giác Pascal) TRIANPAS.PAS .......................... 25


1 Mô hình thuật toán sinh
1.1 Giới thiệu Thuật toán sinh (Generation)
Thuật toán sinh được dùng để giải lớp các bài toán thỏa mãn hai điều kiện:

 Xác định được một thứ tự trên tập các cấu hình cần liệt kê của bài toán. Biết
được cấu hình đầu tiên, biết được cấu hình cuối cùng.
 Từ một cấu hình cuối cùng, ta xây dựng được thuật toán sinh ra cấu hình đứng
ngay sau nó theo thứ tự.
1.2 Mô hình Thuật toán sinh (Generation)
Thuật toán sinh được dùng để giải lớp các bài toán thỏa mãn hai điều kiện:

 Xác định được một thứ tự trên tập các cấu hình cần liệt kê của bài toán. Biết
được cấu hình đầu tiên, biết được cấu hình cuối cùng.
 Từ một cấu hình cuối cùng, ta xây dựng được thuật toán sinh ra cấu hình đứng
ngay sau nó theo thứ tự.

Thuật toán:
Thuật toán Generation:

Bước1 (Khởi tạo):

<Thiết lập cấu hình đầu tiên>;

Bước 2 (Bước lặp):

while (<Lặp khi cấu hình chưa phải cuối cùng>) do

<Đưa ra cấu hình hiện tại>;

<Sinh ra cấu hình kế tiếp>;

endwhile;

End.

1.3 Ví dụ
1.3.1 Ví dụ 1: Thuật toán sinh phần tử tiếp thep của dãy nhị phân độ dài n
public void nextBit(){

int i =n;

while(i>0 && X[i]>0){

i--;X[i]=0;

if(i>0) X[i] = 1;

else OK =0;

}
1.3.2 Ví dụ 2: Thuật toán sinh tổ các phần tử của dãy tổ hợp chập k của n phần tử
public void nextCombination(){

int i = k;

while(i>0 && X[i] == n-k+i) i--;

if(i>0){

X[i] = X[i]+1;

for(int j=i+1;j<=k;j++){

X[j] = X[i]+j-i;

else OK=0;

1.3.3 Ví dụ 3: Thuật toán sinh hoán vị của n phần tử


public void nextPermutation(){

int j = n-1;

while(j>0 && j>X[j]>X[j+1]){

j--;

if(j>0) {

int k = n;

while(x[j] > x[k]){

k--;

int temp = x[j];

x[j] = x[k];

x[k] = temp;

int r =j+1;s = n;

while(r<=s){

temp = X[r];
X[r] = X[s];

X[s] = temp;

r++;j--;

}else OK = 0;

1.3.4 Ví dụ 4: Thuật toán liệt kê các số tự nhiên có tổng là một số n cho trước
public void nextDivision(){

int i=k; // k lưu vị trí cuối cùng

while(i>0 && X[k] == 1) i--; // tìm vị trí khác 1 đầu tiên từ cuối

if(i>0){

X[i] = X[i]-1;

int D = k-i+1;// tổng giá trị còn thiếu để tổng các chữ số bằng n

int R = D/X[i];

int S = D%X[i];

if(R>0){

for(int j=i+1;j<i+R;i++)

X[j] = X[i];

k=k+R;

if(S>0){

k=k+1;

X[k] = S;

} else {

OK= 0;

}
2 Mô hình thuật toán đệ quy
2.1 Giới thiệu Thuật toán đệ quy (Recursion)
Ta nói một đối tượng là đệ quy nếu nó được định nghĩa qua chính nó hoặc một đối
tượng khác cùng dạng với chính nó bằng quy nạp.

Nếu lời giải của một bài toán T được thực hiện bằng lời giải của bài toán T’ có dạng
giống T thì đó là một lời giải đệ quy. Giải thuậttương ứng với lời giải như vậy được gọi là
giải thuật đệ quy. Chú ý, T’ có dạng giống T nhưng theo một nghĩa nào đó thì T’ phải“nhỏ”
hơn T, dễ giải hơn T và việc giải T’ không phụ thuộc vào T.

Ví dụ nổi tiếng về bài toán đệ quy và lời giải của nó như : bài toán tháp Hà Nội, số
Fibonacci …

Định nghĩa một hàm đệ quy như sau:

 Phần neo: Phần này sẽ được thực hiện khi công việc quá đơn giản, có thể giải
trực tiếp, nhanh chóng mà không cần nhờ đếnmột bài toán con nào.
 Phần đệ quy: Thực hiện khi bài toán phức tạp hơn, chưa thể giải được bằng phần
neo, ta xác định những bài toán con và đệquy để giải những bài đó. Khi đã có
lời giải của những bài toán con rồi thì phối hợp lại để giải bài toán gốc.

Phần đệ quy thể hiện tính quy nạp của thuật toán đệ quy.

Vì mỗi lần gọi đệ quy bộ nhớ sẽ cần 1 lưu trữ 1 vùng nhớ mới trong khi vùng nhớ cũ
vẫn phải duy trì, nên trong các ứng dụng thực tế số lần gọi đệ quy không những phải hữu hạn
mà còn phải đủ nhỏ.
2.2 Mô hình Thuật toán đệ quy (Recursion)
2.2.1 Đệ quy tuyến tính (Linear Recursion)

Mỗi lần thực thi chỉ gọi đệ quy một lần. Ví dụ hàm tính giai thừa:
P = { if thỏa điều kiện dừng then thực hiện S;

else {

thực hiện S*;gọi P

(S, S* là các thao tác không đệ quy)

2.2.2 Đệ quy nhị phân (Binary Recursion)

Mỗi lần thực thi có thể gọi đệ quy 2 lần. Tính tổ hợp chập K của N bằng đệ quy
P= { if thỏa điều kiện dừng thenthực hiện S;

Else {

thực hiện S*;


gọi P;

gọi P

(S, S* là các thao tác không đệ quy)


2.3 Ví dụ
2.3.1 Ví dụ 1: Hàm tính giai thừa bằng đề quy
int Factorial(int n) {

if (n == 0)

return 1;

else

return n * Factorial(n - 1); // Linear Recursion

2.3.2 Ví dụ 2: Tính tổ hợp chập K của N bằng đệ quy


int Combine(int n, int k) {

if (k == 0 || k == n)

return 1;

else

return (Combine(n - 1, k) + Combine(n - 1, k - 1)); // Binary Recursion

}
3 Mô hình Thuật toán quay lui
3.1 Giới thiệu Thuật toán quay lui (Back track)
Quay lui là một kĩ thuật thiết kế giải thuật dựa trên đệ quy. Ý tưởng của quay lui là
tìm lời giải từng bước, mỗi bước chọn một trong số các lựa chọn khả dĩ và đệ quy. Người
đầu tiên đề ra thuật ngữ này (backtrack) là nhà toán học người Mỹ D. H. Lehmer vào những
năm 1950.

Tư tưởng

Dùng để giải bài toán liệt kê các cấu hình. Mỗi cấu hình được xây dựng bằng từng
phần tử. Mỗi phần tử lại được chọn bằng cách thử tất cả các khả năng.

Các bước trong việc liệt kê cấu hình dạng X[1...n]:

 Xét tất cả các giá trị X[1] có thể nhận, thử X[1] nhận các giá trị đó. Với mỗi giá
trị của X[1] ta sẽ:
 Xét tất cả giá trị X[2] có thể nhận, lại thử X[2] cho các giá trị đó. Với mỗi giá
trị X[2] lại xét khả năng giá trị của X[3]...tiếp tục như vậy cho tới bước:
 ...
 ...
 Xét tất cả giá trị X[n] có thể nhận, thử cho X[n] nhận lần lượt giá trị đó.
 Thông báo cấu hình tìm được.
 Bản chất của quay lui là một quá trình tìm kiếm theo chiều sâu (Depth-First
Search).
3.2 Mô hình Thuật toán quay lui (Back track)
Mã giả cho thuật toán quay lui.
Backtracking(k) {

for([Mỗi phương án chọn i(thuộc tập D)]) {

if ([Chấp nhận i]) {

[Chọn i cho X[k]];

if ([Thành công]) {

[Đưa ra kết quả];

} else {

Backtracking(k+1);

[Bỏ chọn i cho X[k]];

}
3.3 Ví dụ
Sudoku là một trò chơi khá phổ biến và chắc ai cũng biết. Trò chơi như sau: có một
hình vuông được chia thành 9x9 ô vuông con. Mỗi ô vuông con có giá trị trong khoảng từ 1
đến 9. Ban đầu hình vuông có một số ô vuông con cho trước (có điền sẵn số) và còn lại là
trống. Hãy điền các số từ 1-9 vào các ô con lại sao cho: hàng ngang là các số khác nhau từ 1
đến 9, hàng dọc là các số khác nhau từ 1 đến 9, và mỗi khối 3x3 chính là các số khác nhau
từ 1 đến 9. Sau đây là 1 ví dụ về đề bài và lời giải:

Áp dụng quay lui để giải bài toán sudoku. Ý tưởng: Mỗi bước tìm tập các giá trị khả
dĩ để điền vào ô trống, và sau đó đệ quy để điền ô tiếp theo. Giả mã của thuật toán (ở đây
chú ý mảng chỉ có kích thước 9×9×9)
void solveSudoku(int S[][9], int x, int y){

if(y == 9){

if(x == 8){

printSolution(S);

exit(0);

} else {
solveSudoku(S, x+1,0);

} else if(S[x][y] == 0){

for (int k = 1; k <=9; k++){

if(checkValid(S,x,y,k)){

S[x][y] = k;

solveSudoku(S, x, y+1);

S[x][y] = 0;

} else {

solveSudoku(S,x,y+1);

boolean checkValid(int S[][9], int x, int y, int k){

for(int i = 0; i <9 ; i++){

if(S[x][i] == k) return false;

for(int i = 0; i <9 ; i++){

if(S[i][y] == k) return false;

int a = x/3, b = y/3;

for(int i = 3*a; i < 3*a+3; i++){

for(int j = 3*b; j < 3*b+3; j++){

if(S[i][j] == k) return false;

return true;

}
4 Mô hình thuật toán tham lam
4.1 Giới thiệu Thuật toán tham lam (Greedy Algorithm)
Giải thuật tham lam (tiếng Anh: Greedy algorithm) là một thuật toán giải quyết một
bài toán theo kiểu metaheuristic để tìm kiếm lựa chọn tối ưu địa phương ở mỗi bước đi với
hy vọng tìm được tối ưu toàn cục.
4.2 Mô hình Thuật toán tham lam (Greedy Algorithm)
Giải thuật tham lam có năm thành phần:

 Một tập hợp các ứng viên (candidate), để từ đó tạo ra lời giải
 Một hàm lựa chọn, để theo đó lựa chọn ứng viên tốt nhất để bổ sung vào lời giải
 Một hàm khả thi (feasibility), dùng để quyết định nếu một ứng viên có thể được
dùng để xây dựng lời giải
 Một hàm mục tiêu, ấn định giá trị của lời giải hoặc một lời giải chưa hoàn chỉnh
 Một hàm đánh giá, chỉ ra khi nào ta tìm ra một lời giải hoàn chỉnh.

Có hai thành phần quyết định nhất tới quyết định tham lam:

Tính chất lựa chọn tham lam

Chúng ta có thể lựa chọn giải pháp nào được cho là tốt nhất ở thời điểm hiện tại và sau
đó giải bài toán con nảy sinh từ việc thực hiện lựa chọn vừa rồi. Lựa chọn của thuật toán tham
lam có thể phụ thuộc vào các lựa chọn trước đó. Nhưng nó không thể phụ thuộc vào một lựa
chọn nào trong tương lai hay phụ thuộc vào lời giải của các bài toán con. Thuật toán tiến triển
theo kiểu thực hiện các chọn lựa theo một vòng lặp, cùng lúc đó thu nhỏ bài toán đã cho về
một bài toán con nhỏ hơn. Đấy là khác biệt giữa thuật toán này và giải thuật Quy Hoạnh Động.
Giải thuật quy hoạch động duyệt hết và luôn đảm bảo tìm thấy lời giải. Tại mỗi bước của
thuật toán, quy hoạch động đưa ra quyết định dựa trên các quyết định của bước trước, và có
thể xét lại đường đi của bước trước hướng tới lời giải. Giải thuật tham lam quyết định sớm và
thay đổi đường đi thuật toán theo quyết định đó, và không bao giờ xét lại các quyết định cũ.
Đối với một số bài toán, đây có thể là một thuật toán không chính xác.

Cấu trúc con tối ưu

Một bài toán được gọi là “có cấu trúc tối ưu”, nếu một lời giải tối ưu của bài toán con
chứa lời giải tối ưu của bài toán lớn hơn.
4.3 Ví dụ
Ta có một ba lô có trọng lượng là 37 và 4 loại đồ vật với trọng lượng và giá trị tương
ứng được cho như sau :

Loại đồ vật : A - B - C - D

Trọng lượng : 15 - 10 - 2 - 4

Giá trị : 30 - 25 - 2 - 6
Từ bảng đã cho ta tính đơn giá cho các loại đồ vật và sắp xếp các loại đồ vật này theo
thứ tự đơn giá giảm dần ta có bảng sau.

Loại đồ vật : B - A - D - C

Trọng lượng : 10 - 15 - 4 - 2

Giá trị : 25 - 30 - 6 - 2

Đơn giá : 2.5 - 2.0 - 1.5 - 1.0

Các bước thực hiện

1. Tính đơn giá của các sản phẩm.


CodeC
struct DoVat {
char Ten [20];
float TrongLuong, GiaTri, DonGia;
int PhuongAn;//so luong do vat chon
};`

2. Tính đơn giá của các sản phẩm. Độ phức tạp thuật toán là O(n)
CodeC
void TinhDonGia(DoVat sp[], int n)
{
for(int i = 1; i <= n; i++)
sp[i].DonGia = sp[i].GiaTri / sp[i].TrongLuong;
}

3. Sắp xếp giảm dần theo đơn giá. Độ phức tạp thuật toán O(n2)
CodeC
void SapXep(DoVat sp[], int n)
{
for(int i = 1; i <= n - 1; i++)
for(int j = i + 1; j <= n; j++)
if (sp[i].DonGia < sp[j].DonGia)
swap(sp[i], sp[j]);
}

4. Xác định sản phẩm cần lấy. Độ phức tạp thuật toán là O(n)
CodeC
void Greedy(DoVat sp[], int n, float W)
{
for (int i = 0; i < n; i++) {
sp[i].PhuongAn = W / sp[i].TrongLuong;
W -= sp[i].PhuongAn * sp[i].TrongLuong;
}
}
5 Mô hình Thuật toán chia để trị
5.1 Giới thiệu
Chia để trị là 1 phương pháp áp dụng cho các bài toán có thể giải quyết bằng cách chia
nhỏ ra thành các bài toán con từ việc giải quyết các bài toán con này. Sau đó lời giải của các
bài toán nhỏ được tổng hợp lại thành lời giải cho bài toán ban đầu

5.2 Mô hình
Các bài toán có thể giải quyết bằng phương pháp chia để trị thông qua 3 bước:

Bước 1: Chia/Tách nhỏ

Tại bước này thì bài toán ban đầu sẽ được chia thành các bài toán con cho đến khi
không thể chia nhỏ được nữa. Các bài toán con kiểu sẽ trở thành 1 bước nhỏ trong việc giải
quyết bài toán lớn.

Bước 2: Trị/Giải quyết bài toán con

Tại bước này ta sẽ phải tìm phương án để giải quyết cho bài toán con một cách cụ thể.

Bước 3: Kết hợp lời giải lại để suy ra lời giải

Khi đã giải quyết xong cái bài toán nhỏ, lặp lại các bước giải quyết đó và kết hợp lại
những lời giải đó để suy ra kết quả cần tìm (có thể ở dạng đệ quy).
5.3 Ví dụ
5.3.1 Ví dụ Tìm kiếm nhị phân

Tìm kiếm nhị phân là một thuật toán dùng để tìm kiếm 1 phần tử trong một danh sách
đã được sắp xếp. Thuật toán hoạt động như sau:

Bước 1(Chia): Danh sách ban đầu sẽ được chia thành 2 nửa

Bước 2 (Trị): Trong mỗi bước, so sánh phần tử cần tìm với phần tử nằm ở chính giữa
danh sách. Nếu hai phần tử bằng nhau thì phép tìm kiếm thành công và thuật toán kết thúc.
Nếu chúng không bằng nhau thì tùy vào phần tử nào lớn hơn, thuật toán lặp lại bước so sánh
trên với nửa đầu hoặc nửa sau của danh sách. Vì số lượng phần tử trong danh sách cần xem
xét giảm đi một nửa sau mỗi bước, nên thời gian thực thi của thuật toán là hàm lôgarit.

Bước 3: Bằng việc lặp lại cách giải quyết như bước 2 ta sẽ tìm được kết quả.

int binarySearch(int array[], int left, int right, int x)

// nếu chỉ số left > right dừng lại và return -1 không có kết quả

if (left > right) return -1;

// tìm chỉ số ở giữa của mảng

int mid = (left + right) / 2;

// nếu số cần tìm bằng số ở giữa của mảng thì return

if (x == array[mid])

return mid; // nếu số cần tìm nhỏ hơn số ở giữa của mảng thì tìm
sang nửa bên trái

if (x < array[mid])

return binarySearch(array, left , mid-1, x); // nếu số cần tìm


lớn hơn số ở giữa của mảng thì tìm sang nửa bên phải

if (x > a[mid])

return binarySearch(a, mid+1 , right, x);

}
5.3.2 Ví dụ Quicksort
Thuật toán quicksort được áp dụng rất nhiều trong thực tế, hãy cùng tìm hiểu thuật
toán này áp dụng chia để trị như thế nào.
Bước 1(chia): Thuật toán quicksort chia danh sách cần sắp xếp mảng array[1..n]
thành hai danh sách con có kích thước tương đối bằng nhau nhờ chỉ số của phần tử gọi là
chốt, ta có thể chọn chốt là phần tử ở giữa, ở cuối, ở đầu hoặc phần tử ngẫu nhiên nào trong
mảng.
Bước 2(trị): Sau khi đã chia thành 2 mảng dựa vào phần tử chốt nhiệm vụ của bước
này là phải sắp xếp sao cho: những phần tử nhỏ hơn hoặc bằng phần tử chốt được đưa về
phía trước và nằm trong danh sách con thứ nhất, các phần tử lớn hơn chốt được đưa về phía
sau và thuộc danh sách đứng sau(Trường hợp sắp xếp tăng dần). Cứ tiếp tục chia như vậy
tới khi các danh sách con đều có độ dài bằng 1
Bước 3: Bằng việc lặp các bước giải quyết các bài toán con trên ta sẽ thu được kết
quả là mảng sẽ được sắp xếp.
Dưới đây là hình ảnh minh họa việc thực hiện thuật toán quicksort:

int partition(int arr[], int low, int high)

int pivot = arr[high];

int i = (low-1);

for (int j=low; j<high; j++)

{
// nếu phần tử nhỏ hơn hoặc bằng với chốt

if (arr[j] <= pivot)

i++;

// đổi chỗ arr[i] và arr[j]

int temp = arr[i];

arr[i] = arr[j];

arr[j] = temp;

// đổi chỗ arr[i+1] và arr[high] (chốt)

int temp = arr[i+1];

arr[i+1] = arr[high];

arr[high] = temp;

// trả về chỉ số của chốt

return i+1;

// Hàm thực hiện quicksort

void sort(int arr[], int low, int high)

{ // nếu chỉ số của đầu mảng nhỏ hơn chỉ số cuối mảng

if (low < high)

// tìm chỉ số của chốt sau khi đã thực hiện sắp xếp

int pi = partition(arr, low, high);


// lặp lại các bước với mảng từ phần tử đầu tiên đến chốt - 1

// và từ chốt + 1 đến phần tử cuối cùng của mảng.

sort(arr, low, pi-1);

sort(arr, pi+1, high);

6 Mô hình thuật toán nhánh cận


6.1 Giới thiệu thuật toán nhánh cận (Branch-And-Bound)
Trong thực tế có rất nhiều bài toán yêu cầu tìm ra nghiệm theo một số điều kiện nào
đó, và phương án đó là tốt nhất theo một tiêu chí cụ thể nào đó. Những bài toán như vậy được
gọi là bài toán tối ưu. để giải một bìa toán tối ưu, chúng ta có thể có rất nhiều cách, nhưng
cũng có thể không có cách nào khác ngoài việc phải vét cạn toàn bộ rồi so sánh xem nghiệm
nào thoả mãn hết các tiêu chí mà ta định nghĩa là tối ưu rồi chọn lấy nghiệm đó. Tối ưu ở đây
có thể là một giá trị lớn nhất hay giá trị nhỏ nhất trong những tình huống nhất định nào đó.

Ví dụ như từ nhà đến trường chúng ta phải đi qua rất nhiều con đường, do vậy có nhiều
cách đi khác nhau qua những con đường này. Mỗi cách đi đó mang nhiều giá trị như độ dài
đường đi, thời gian đi, chi phí đi, ...Việc chúng ta lựa chọn cách đi nào tối ưu là phụ thuộc
vào việc chúng ta lấy tiêu chí nào để so sánh. Chẳn hạn ta lấy tiêu chí đường đi ngắn nhất làm
tiêu chí thì cách đơn giản nhất là ta liệt kê ra tất cả các cách đi cũng quảng đường tương ứng.
Sau đó so sánh các quản đường rồi tìm ra cách đi tốt nhất. đây chính xác là cách làm của quay
lui - vét cạn. Nhưng nếu ta có cách nào đó để dự đoán được trước những những cách đi mà
sẽ dài hơn cách ta đã chọn trước thì ta không cần thử những cách này nữa, đó chính là phương
pháp nhánh cận, là một sự cải tiến của phương pháp quay lui, cho phép ta tìm ra nghiệm tối
ưu nhanh hơn.

Tư tưởng nhánh cận

Vì cải tiến dựa trên quay lui, nên trước tiên chúng ta cũng mô hình hoá nghiệm thành
một vector X = (x1, x2, ...,xn), mỗi thành phần xi (i=1, 2,...,n) được chọn ra từ tập Si. Mỗi
nghiệm X của bài toán được xác định đôt tốt bang một hàm f(x) và mục tiêu là tìm ra nghiệm
X có giá trị f(x) là nhỏ nhất ( hoặc lớn nhât) tuỳ theo ngữ cảnh.

Tư tưởng chính của nhánh cận như sau: Giả sử ta đã xây dung được k thành phần từ
x1 đến xk, giờ ta chuẩn bị mở rộng thành phần thứ xk+1 từ xk. Nhưng khi đánh giá ta lại thấy
tất cả các nghiệm mở rộng từ xk không có nghiệm nào có giá trị tốt hơn giá trị tối ưu ta đã
biết tại thời điểm đó, vậy thì ta không cần mở rộng nữa, như vậy ta đã cắt bỏ đi một nhánh,
giảm được số nghiệm phải tìm rất nhiều.

Điều khó nhất ở đây là phải đánh giá được các nghiệm mở rộng, nếu đánh giá được
tốt, thuật toánh nhánh cận sẽ chạy nhanh hơn rất nhiều so với vét cạn.

6.2 Mô hình thuật toán nhánh cận (Branch-And-Bound)


Mô hình tổng quát

void tim (int i)

for( Các giá trị j có thể gán cho x[i])

x[i]=j;

if( Việc thử trên vẫn còn hi vọng tìm ra cấu hình tốt hơn BESTCONFIG)

if( x[i] là phần tử cuối cùng)

Cập nhật lại BEST CONFIG

else

Ghi nhận việc thử x[i]=j nếu cần

tim(i+1);

Bỏ ghi nhận việc thử x[i]=j nếu cần

6.3 Ví dụ
Bài toán người đi du lịch

Cho n thành phố đánh số từ 1 đến n và các tuyến đường giao thông hai chiều giữa
chúng, mạng lưới giao thông này được cho bởi mảng C[1..n,1..n], Cij=Cji là chi phí đi đoạn
đường trực tiếp từ thành phố i đến thành phố j.

Một người du lịch xuất phát từ thành phố 1, muốn đi thăm tất cả các thành phố còn lại
mỗi thành phố đúng 1 lần rồi quay lại thành phố 1. Hãy chỉ ra cho người đó hành trình với
chi phí ít nhất. Bài toán được gọi là bài toàn người đi du lịch, hay người chào hang ( Travelling
Salesman Problem - TSP)

Input: Tệp "TSP.INP" có dạng:

- Dòng đầu chứa số n ( 1<n<=20), là số thành phố

- n dòng tiếp theo, mỗi dòng n số mô tả mảng C

Output: Tếp :TSP.OUT" có dạng:

- Dòng đầu là chi phí ít nhất.

- Dòng thứ 2 mô tả hành trình

7 Mô hình thuật toán quy hoạch động


7.1 Giới thiệu thuật toán quy hoạch động (Dynamic Programming)
Phương pháp quy hoạch động dùng để giải bài toán tối ưu có bản chất đệ quy, tức là
việc tìm phương án tối ưu cho bài toán đó có thể đưa về tìm phương án tối ưu của một số hữu
hạn các bài toán con.
Ðối với một số bài toán đệ quy, nguyên lý chia để trị (divide and conquer) thường đóng
vai trò chủ đạo trong việc thiết kế thuật toán. Ðể giải quyết một bài toán lớn, ta chia nó thành
nhiều bài toán con cùng dạng với nó để có thể giải quyết độc lập.

Trong phương án quy hoạch động, nguyên lý chia để trị càng được thể hiện rõ: Khi
không biết phải giải quyết những bài toán con nào, ta sẽ đi giải quyết toàn bộ các bài toán con
và lưu trữ những lời giải hay đáp số của chúng với mục đích sử dụng lại theo một sự phối hợp
nào đó để giải quyết những bài toán tổng quát hơn.

Ðó chính là điểm khác nhau giữa Quy hoạch động và phép phân giải đệ quy và cũng
là nội dung phương pháp quy hoạch động:

 Phép phân giải đệ quy bắt đầu từ bài toán lớn phân ra thành nhiều bài toán con
và đi giải từng bài toán con đó. Việc giải từng bài toán con lại đưa về phép phân
ra tiếp thành nhiều bài toán nhỏ hơn và lại đi giải các bài toán nhỏ hơn đó bất
kể nó đã được giải hay chưa.
 Quy hoạch động bắt đầu từ việc giải tất cả các bài toán nhỏ nhất (bài toán cơ
sở) để từ đó từng bước giải quyết nhưng bài toán lớn hơn, cho tới khi giải được
bài toán lớn nhất (bài toán ban đầu).

Bài toán giải theo phương pháp quy hoạch động gọi là bài toán quy hoạch động.

Công thức phối hợp nghiệm của các bài toán con để có nghiệm của bài toán lớn gọi là
công thức truy hồi của quy hoạch động.

Tập các bài toán có ngay lời giải để từ đó giải quyết các bài toán lớn hơn gọi là cơ sở
quy hoạch động.

Không gian lưu trữ lời giải các bài toán con để tìm cách phối hợp chúng gọi là bảng
phương án của quy hoạch động.

Trước khi áp dụng phương pháp quy hoạch động ta phải xét xem phương pháp đó có
thỏa mãn những yêu cầu dưới đây không:

 Bài toán lớn phải phân rã được thành nhiều bài toán con, mà sự phối hợp lời
giải của các bài toán con đó cho ta lời giải của bài toán lớn.
 Vì quy hoạch động là đi giải tất cả các bài toán con, nên nếu không đủ không
gian vật lý lưu trữ lời giải (bộ nhớ, đĩa, …) để phối hợp chúng thì phương pháp
quy hoạch động cũng không thể thực hiện được.
 Quá trình từ bài toán cơ sở tìm ra lời giải bài toán ban đầu phải qua hữu hạn
bước. Các bước cài đặt một chương trình sử dụng quy hoạch động:
 Giải tất cả các bài toán cơ sở (thông thường rất dễ), lưu các lời giải vào bảng
phương án.
 Dùng công thức truy hồi phối hợp những lời giải của các bài toán nhỏ đã lưu
trong bảng phương án để tìm lời giải của các bài toán lớn hơn rồi lưu chúng vào
bảng phương án. Cho tới khi bài toán ban đầu tìm được lời giải.
 Dựa vào bảng phương án, truy vết tìm ra nghiệm tối ưu.

Cho tới nay, vẫn chưa có một định lý nào cho biết một cách chính xác những bài toán
nào có thể giải quyết hiệu quả bằng quy hoạch động. Tuy nhiên để biết được bài toán có thể
giải bằng quy hoạch động hay không, ta có thể đặt câu hỏi:

1. “Một nghiệm tối ưu của bài toán lớn có phải là sự phối hợp các nghiệm tối ưu của
các bài toán con hay không?”

2. “Liệu có thể nào lưu trữ được nghiệm các bài toán con dưới một hình thức nào đó
để phối hợp tìm được ngiệm bài toán lớn?”.

7.2 Các bước cơ bản để giải một bài toán quy hoạch động:
Với mỗi bài toán ứng dụng giải thuật quy hoạch động, ta vẫn phải trả lời rõ ràng, chính
xác 6 câu hỏi:

 Tên và ý nghĩa các biến phục vụ sơ đồ lặp,


 Cách khai báo các biến đó,
 Sơ đồ (công thức) lặp chuyển từ một bước sang bước tiếp theo,
 Giá trị đầu của các biến tham gia tính lặp,
 Tham số điều khiển lặp: thay đổi từ đâu đến đâu,
 Kết quả: ở đâu và làm thế nào để dẫn xuất ra.

Các cách trả lời khác nhau sẽ dẫn đến những giải thuật khác nhau cả về cách thực hiện
lẫn độ phức tạp.

7.3 Ví dụ về các bài toán có thể giải bằng phương pháp quy hoạch động
7.3.1 Bài toán Tính N! GT.PAS
1 n0
Ta có định nghĩa như sau: n! =  nếu
n *(n  1)! n0
Cho một số nguyên dương n (0  n  13).
Yêu cầu: Hãy tính n! bằng phương pháp quy hoạch động (lập bảng phương án).
Dữ liệu vào: Ghi trong file văn bản GT.INP có cấu trúc như sau:
- Dòng 1: Ghi số nguyên dương n.
Dữ liệu ra: Ghi ra file văn bản GT.OUT theo cấu trúc như sau:
- Dòng 1: Ghi giá trị tính được của n!
Ví dụ:
GT.INP GT.OUT
3 6
Thuật toán:
Gọi GT[i] là giá trị của i! (0  i  13)
Ta có công thức quy hoạch động như sau:
GT[i] := GT[i-1]*i;
Như vậy, việc tính n! sẽ được thực hiện bằng vòng lặp:
GT[0] :=1;
For i:=1 to n do
GT[i] := GT[i-1]*i;
Kết quả: giá trị của n! nằm trong phần tử GT[n].

7.3.2 Bài toán Tính dãy Fibonaci FIBO.PAS


1 n  0 or n  1
Ta có định nghĩa như sau: F(n) =  nếu
 F (n  1)  F (n  2) n 1
Cho một số nguyên dương n (0  n  50).
Yêu cầu: Hãy tính F(n) bằng phương pháp quy hoạch động (lập bảng phương án).
Dữ liệu vào: Ghi trong file văn bản FIBO.INP có cấu trúc như sau:
- Dòng 1: Ghi số nguyên dương n.
Dữ liệu ra: Ghi ra file văn bản FIBO.OUT theo cấu trúc như sau:
- Dòng 1: Ghi giá trị tính được của F(n).
Ví dụ:
FIBO.INP FIBO.OUT
5 8
Thuật toán:
Gọi F[i] là giá trị Fibonaci của fi (0  i  50).
Ta có công thức quy hoạch động như sau:
F[i] := F[i-1] + F[i-2];
Như vậy, việc tính fn được thực hiện bằng vòng lặp:
F[0] := 0;
F[1] := 1;
For i := 2 to n do
F[i] := F[i-1] + F[i-2];
Kết quả: giá trị fn nằm trong F[n].
7.3.3 Bài toán Tính tổng của dãy số SUM.PAS
Cho dãy số nguyên gồm n phần tử a1, a2, …, an (1  n  105) và hai số nguyên
dương p và q (1  p  q  n).
Yêu cầu: Hãy tính tổng của các phần tử liên tiếp từ ap … aq bằng phương pháp quy
hoạch động (lập bảng phương án).
Dữ liệu vào: Ghi trong file văn bản SUM.INP có cấu trúc như sau:
- Dòng 1: Ghi số nguyên dương n và k, hai số được ghi cách nhau một dấu cách.
- Dòng 2: Ghi n số nguyên a1, a2, …, an, các số được ghi cách nhau ít nhất một dấu
cách (-32000  ai  32000).
- Dòng thứ i trong k dòng tiếp theo: Mỗi dòng ghi hai số nguyên dương pi và qi, hai số
được ghi cách nhau một dấu cách (1  pi  qi  n).
Dữ liệu ra: Ghi ra file văn bản SUM.OUT theo cấu trúc như sau:
- Dữ liệu được ghi trên k dòng: Dòng thứ i ghi một số nguyên là tổng giá trị của các
phần tử trong đoạn a p ...aq
i i

Ví dụ:
SUM.INP SUM.OUT
5 3 21
2 9 -3 5 8 6
1 5 5
2 3
4 4
Thuật toán:
Gọi A[i] là giá trị của phần tử thứ i trong dãy số a1, a2, …, an.
Gọi T[i] là tổng giá trị các phần tử a1, a2, …, ai (1  i  n).
Ta có công thức quy hoạch động để tính T[i] như sau:
T[i] := T[i - 1] + A[i];
Như vậy, việc tính T[n] được thực hiện bằng vòng lặp:
T[0] := 0;
For i:=1 to n do
T[i] := T[i - 1] + A[i];
Kết quả: Tổng các phần tử liên tiếp từ ap đến aq được tính theo công thức:
Sum := A[q] - A[p-1];
7.3.4 TRIANGLE PASCAL (Tam giác Pascal) TRIANPAS.PAS

Tam giác Pascal là một mô hình dùng để


đưa ra các hệ số của khai triển nhị thức Newton
bậc N (x+1)N.

Ví dụ: trong khai triển (x+1)2 = x2 + 2x +1 có các


hệ số là 1 2 1

Trong khai triển (x+1)3 = x3 + 3x2 + 3x + 1 có các


hệ số là 1 3 3 1

Yêu cầu: Hãy tìm các hệ số trong khai triển nhị thức Newton (x + 1)N.

Dữ liệu vào: Cho trong file văn bản TRIANPAS.INP có cấu trúc như sau:

Dòng 1: Ghi số nguyên dương N (1 ≤ N ≤ 100).

Dữ liệu ra: Ghi ra file văn bản TRIANNUM.OUT theo cấu trúc:

Dòng 1: Ghi ra các số nguyên dương lần lượt là các hệ số trong khai triển nhị thức Newton
(x + 1)N, các số được ghi cách nhau một dấu cách.

Ví dụ:

TRIANPAS.INP TRIANPAS.OUT

5 1 5 10 10 5 1

Thuật toán:

+ Ta xây dựng mảng hai chiều có kích thước [0..100, 0..101]

+ Sử dụng phương pháp quy hoạch động với công thức như sau:

Dòng thứ i được tính thông qua dòng i-1

L[i, j] = L[i-1, j-1] + L[i-1, j]

+ Thuật toán cụ thể như sau:

L[0,1] = 1; L[1,1] = 1; L[1,2] = 1;

For i:= 2 to N do

Begin
L[i, 1] :=1;

For j:=2 to i+1 do

L[i, j] = L[i-1, j-1] + L[i-1, j];

End;

+ Kết quả được lưu trữ ở dòng N, cụ thể:

L[N, 1], L[N, 2], L[N, 3], ..., L[N, N], L[N,N+1]

3.5. TRIANGLE NUMBER (Tam giác số) TRIANNUM.PAS

Cho tam giác số như hình vẽ. Ta định nghĩa


một đường đi trong tam giác số là đường đi xuất
phát từ hình thoi ở đỉnh tam giác và đi đến được các
hình thoi có chung cạnh với nó, đường đi kết thúc
khi gặp một hình thoi ở đáy tam giác.

Yêu cầu: Hãy tìm một đường đi trong tam giác số


sao cho tổng giá trị của các ô trong đường đi có giá
trị lớn nhất.

Dữ liệu vào: Cho trong file văn bản TRIANNUM.INP có cấu trúc như sau:

Dòng 1: Ghi số nguyên dương N là số hàng của tam giác (1 ≤ N ≤ 100).

Dòng thư i trong N dòng tiếp theo: Ghi i số nguyên dương lần lượt là giá trị của các ô trên
dòng thứ i tưng ứng trong tam giác (Các số có giá trị không quá 32000). Các số được ghi
cách nhau một dấu cách.

Dữ liệu ra: Ghi ra file văn bản TRIANNUM.OUT theo cấu trúc:

Dòng 1: Ghi ra số nguyên dương S là tổng giá trị của đường đi tìm được.
Ví dụ:

TRIANNUM.INP TRIANNUM.OUT

6 48

2 8

5 9 3

4 6 9 2

7 8 5 6 6

1 3 4 9 8 1

Thuật toán:

+ Ta xây dựng mảng hai chiều có kích thước [1..100, 0..101]

+ Sử dụng phương pháp quy hoạch động với công thức như sau:

Dòng thứ i được tính thông qua dòng i-1

L[i, j] = Max(L[i-1, j-1] + L[i-1, j]) + A[i, j]

+ Thuật toán cụ thể như sau:

L[1, 1] = A[1, 1];

For i:= 2 to N do

Begin

For j:=1 to i do

L[i, j] = Max(L[i-1, j-1] + L[i-1, j]) + A[i, j];

End;

+ Kết quả được lưu trữ ở dòng N, cụ thể:

Tong: = L[N, 1]

For j:= 2 to N do

if (L[N, j] > Tong) then

Tong := L[N, j];


TÀI LIỆU THAM KHẢO

[1]Robert Sedgewick, “Algorihms”, McGraw Hill Company, 2006, Vol 1, 2, 3, 4.

[2] N. Knuth, “The Art of Programming”, “Algorihms”, McGraw Hill Company, 2006,
Vol 1, 2, 3, 4.

[3] Robert Sedgewick, “Cẩm nang thuật toán”, Nhà xuất bản khoa học kỹ thuật Hà Nội,
2004, Vol 1, 2, 3, 4.

[4] Lê Minh Hoàng, “Bài giảng các chuyên đề”, Nhà xuất bản khoa học kỹ thuật Hà Nội,
2008.

You might also like