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

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

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

Chƣơng 4
KỸ THUẬT QUAY LUI
4.1. Nội dung kỹ thuật
Nét đặc trưng của kỹ thuật quay lui là các bước hướng tới lời giải hoàn toàn
được làm thử. Tại mỗi bước, nếu có một lựa chọn được chấp nhận thì ghi nhận lựa
chọn này và tiến hành các bước thử tiếp theo. Ngược lại nếu khơng có lựa chọn nào
thích hợp thì làm lại bước trước, xố bỏ sự ghi nhận và quay về chu trình thử các
lựa chọn cịn lại.
Hµnh động này đ-ợc gọi là quay lui, thut toỏn s dụng kỹ thuật này là thuật
toán quay lui.
Lời giải của bài toán thường được biểu diễn bằng một bộ gồm n thành phần x
= (x1,.., xn ) phải thoả mãn các điều kiện nào đó. Để chỉ ra lời giải x, ta phải xây
dựng dần các thành phần xi.
Nhu vậy nội dung chính của kỹ thuật này là việc xây dựng dần các thành
phần xi bằng cách thử tất các khả năng. Giả sử đã xác định được i -1 thành phần x1,
x2,…, xi-1 (mà ta sẽ gọi là lời giải bộ phận cấp i- 1), bây giờ ta xác định thành phần
xi bằng cách duyệt tất cả các khả năng có thể đề cử cho nó (đánh số các khả năng từ
1 đến ni ). Với mỗi khả năng j, kiểm tra xem j có chấp nhận được khơng. Xảy ra hai
trường hợp:
- Nếu chấp nhận j thì xác định xi theo j. Sau đó nếu i = n thì ta được một cấu
hình, cịn trái lại ta tiến hành xác định xi+1.
- Nếu thử tất cả các khả năng mà khơng có khả năng nào chấp nhận được thì
quay lại bước trước để xác định lại xi-1.
Điểm quan trọng của thuật toán là phải ghi nhớ tại mỗi bước đã đi qua những
khả năng nào đã thử để tránh trùng lặp. Rõ ràng những thông tin này cần được lưu
trữ theo cơ cấu ngăn xếp (Stack- Vào sau ra trước). Vì thế kỹ thuật này rất phù hợp
với việc lập trình trên một ngơn ngữ cho phép gọi đệ qui. Bước xác định xi có thể
diễn tả qua hàm được tổ chức đệ qui dưới đây:
Try(i)
{
for(j=1;j


if (<chấp nhận j>)
{
86


<Xác định xi theo j>
if (i == n) <ghi nhận một cấu hình>;
else try(i+1);
}
}
Phần quan trọng nhất trong hàm trên là việc đưa ra một danh sách các khả
năng đề cử và việc xác định giá trị biểu thức <Chấp nhận j> thơng thường giá trị
này, ngồi việc phụ thuộc j, còn phụ thuộc vào việc đã chọn các khả năng tại i -1
bước trước. Trong những trường hợp như vậy, cần ghi nhớ trạng thái mới của quá
trình tìm kiếm sau khi <xác định xi theo j> và trả lại trạng thái cũ sau lời gọi
Try(i+1). Các trạng thái này được ghi nhận nhờ một số biến tổng thể, gọi là biến
trạng thái.
Sau khi xây dựng thủ tục đệ qui Try, đoạn chương trình chính giải bài tốn
liệt kê có dạng:
main()
{
Init;
Try(1);
}
trong đó Init là hàm khởi tạo các giá trị ban đầu (nhập các giá trị tham số, của bài
toán, khởi gán các biến trạng thái, biến đếm, ...
Người ta đã chứng tỏ được rằng độ phức tạp tính toán của các thuật toán
quay lui thường là hàm mũ. Ta cơng nhận điều này và trong các ví dụ áp dụng ta sẽ
không đặt vấn đề xác định độ phức tạp tính tốn của các thuật tốn.
Kỹ thuật quay lui thường được áp dụng cho các bài toán liệt kê tổ hợp.

4.2. Các ví dụ áp dụng
4.2.1. Đƣa ra các dãy nhị phân độ dài n
1) Bài toán
Cho một số nguyên dương n. Hãy đưa ra các dãy nhị phân độ dài n
2) Thiết kế thuật toán
Biểu diễn dãy nhị phân dưới dạng (b1, b2,…, bn), trong đó bi{0,1}.
87


Mỗi thành phần bi có các giá trị đề cử là 0 và 1. Các giá trị này mặc nhiên được
chấp nhận mà khơng phải thoả mãn điều kiện gì.
Mảng b lưu trữ dãy nhị phân xây dựng được.
Hàm Try(i) xác định thành phần thứ i và hàm Result() đưa ra dãy tìm được.
result()
{
printf("\n");
for(i=1;i<=n;i++) printf(b[i]);
}
Try(i)
{
for(j=0;j<=1;j++)
{
b[i]:=j;
if(i==n) result();
else try(i+1);
}
}
main()
{
scanf(n);

try(1);
getch();
}
4.2.2. Đƣa ra các hoán vị của n số nguyên
1) Bài toán
Cho X = { x1, x2, ..., xn}, trong đó xi (i=1, 2, ...,n) là các số nguyên Hãy đưa
ra tất cả các hoán vị của X.
2) Thiết kế thuật toán

88


Ta nhận thấy rằng mỗi một hoán vị của tập X tương đương với một hoán vị
của tập chỉ số {1, 2, ..., n}
Biểu diễn hoán vị tập chỉ số dưới dạng a1, a2, …, an trong đó ai nhận giá trị từ
1 đến n và ai  aj với i  j. Các giá trị từ 1 đến n được lần lượt đề cử cho ai, trong đó
giá trị j được chấp nhận nếu nó chưa được dùng. Vì thế cần phải ghi nhớ đối với
mỗi giá trị j xem nó đã được dùng hay chưa. Điều này được thực hiện nhờ một dãy
biến bj, trong đó bj bằng 1 nếu j chưa được dùng. Sau khi xác định ai theo j cần gán
giá trị 0 cho bj. Khi thực hiện xong Result hay Try(i+1) cần phải gán lại giá trị 1
cho bj.
Tổ chức dữ liệu:
- Mảng x: dãy số nguyên đã cho
- Mảng a: lưu trữ một hoán vị của tập chỉ số {1, 2, ..., n}
- Mảng b: lưu trữ trạng thái các phần tử của x với b[j]=1 thì x[j] đã được dùng,
b[j]=0 thì x[j] chưa được dùng.
Các hàm
- Hàm init(): nhập n và khởi tạo
- Hàm result(): đưa ra hốn vị vừa tìm được
- Hàm Try(i): xác định thành phần thứ i của hoán vị.

init()
{
scanf(n);
for(i=1;i<=n;i++) b[i]=1;
end;
result()
{
for(i=1;i<=n;i++)
printf(x[a[i]]);
}
Try(i)
{
for(i=1;i<=n;i++)
89


if(b[j]==1)
{
a[i]=j;
b[j]=0;
if(i==n) result();
else try(i+1);
b[j]:=true;
}
}
main()
{
init();
try(1);
getch();

}
4.2.3. Đƣa ra các tập con của tập gồm n số nguyên
1) Bài toán
Cho X = {x1, x2, ..., xn}, trong đó xi (i=1, 2, ...,n) là các số nguyên. Hãy đưa
ra tất cả các tập con gồm m (m<=n) phần tử của X .
2) Thiết kế thuật toán
Mỗi tập con của X gồm m phần tử tương đương với một tập con gồm m phần
tử của tập chỉ số {1, 2, ..., n}
Mỗi tập con gồm m phần tử của tập chỉ số {1, 2, ..., n} có thể biểu diễn bởi
bộ có thứ tự gồm m thành phần a = (a1, a2, ... , am) thỏa mãn
1 a1 < a2 < .... Trên tập các tập con gồm m phần tử của tập chỉ số {1, 2, ..., n} ta xác định một
thứ tự như sau:
Ta nói tập con a = (a1, a2, ... , am) đi trước tập con a’ = (a1’, a2’, ... , am’) nếu
tìm được chỉ số k (1 k  m) sao cho:
a1 = a’1
90


a2= a’2
ak-1 = a’k-1
ak < a’k.
Chẳng hạn X = {1, 2, 3, 4, 5} thì các tập con gồm 3 phần tử của X được liệt
kê theo thứ tự từ điển như sau:
1

2

3


1

2

4

1

2

5

1

3

4

1

3

5

1

4

5


2

3

4

2

3

5

2

4

5

3

4

5

Như vậy tập con đầu tiên trong thứ tự là (1, 2, ..., m) và tập con cuối cùng là
(n - m +1, n - m + 2, ..., n).
Từ đó suy ra các giá trị đề cử cho ai là từ ai-1+1 đến n - m +i. Để điều này
đúng cho cả trường hợp i =1 cần có a0 = 0. Các giá trị đề cử này mặc nhiên được
chấp nhận.
Tổ chức dữ liệu:

- Mảng x: dãy số nguyên đã cho
- Mảng a: lưu trữ một tập con gồm m phần tử của tập chỉ số {1, 2, ..., n}
Các hàm
- Hàm init(): nhập n, m và khởi tạo
- Hàm result(): đưa ra tập con gồm m phần tử của x vừa tìm được
- Hàm Try(i): xác định thành phần thứ i của tập con.
init()
{
scanf(n,m)
91


a[0]=0;
end;
result()
{
for(i=1;i<=m;i++)
printf(x[a[i]]);
}
Try(i)
{
for(j=a[i-1]+1;j<=n-m+i;j++)
{
a[i]=j;
if (i==m) result();
else try(i+1);
}
}
main()
{

init;
try(1);
getch();
}
4.2.4. Bài toán xếp hậu
1) Bài toán
Liệt kê tất cả các cách xếp 8 quân hậu trên bàn cờ sao cho chúng không ăn
được lẫn nhau.
2) Thiết kế thuật tốn
Đánh số cột và số dịng của bàn cờ từ 1 đến 8. Mỗi dòng được xếp đúng một
quân hậu. Vấn đề là xem mỗi quân hậu trên mỗi hàng được xếp vào cột nào. Từ đó,
ta có thể biểu diễn một cách xếp bằng một bộ 8 thành phần (x1, x2, ..., x8) trong đó
92


xi = j nghĩa là quân hậu dòng i được xếp vào cột j. Các giá trị đề cử cho xi là từ 1
đến 8. Giá trị j là được chấp nhận nếu ô (i, j) chưa bị quân hậu nào chiếu đến (quân
hậu có thể ăn ngang, dọc và hai đường chéo). Để kiểm soát được điều này, ta cần
phải ghi nhận trạng thái của bàn cờ trước cũng như sau khi xếp được một quân hậu.
Việc kiểm soát theo chiều ngang là khơng cần thiết vì mỗi dịng được xếp
đúng một quân hậu. Việc kiểm soát chiều dọc được ghi nhận nhờ dãy biến aj với qui
ước rằng aj bằng 1 nếu cột j còn trống. Hai đường chéo đi qua ơ (i, j) thì một đường
chéo gồm những ơ có i + j khơng đổi, cịn một đường chéo gồm những ơ có i - j
khơng đổi (2 i+j  16, -7 i-j  7).
Từ đó đường chéo thứ nhất được ghi nhận nhờ dãy biến bj (2 j  16) và
đường chéo thứ hai nhờ nhờ dãy biến cj (-7 j  7) với qui ước các đường này cịn
trống nếu biến tương ứng có giá trị 1.
Các biến trạng thái aj, bj, cj được khởi gán giá trị 1 trong hàm Init(). Như vậy
giá trị j được chấp nhận khi và chỉ khi cả 3 biến ai, bi+j, ci-j cùng có giá trị 1. Các
biến này phải được gán lại giá trị 0 khi xếp xong quân hậu thứ i và được trả lại giá

trị 1 sau khi gọi Result() hay Try(i+1).
Tổ chức dữ liệu:
- Mảng x để lưu trữ cách xếp 8 quân hậu tìm được.
- Mảng c với các phần tử c[j] để ghi nhận trạng thái cột j (c[j]=1 thì cột j
chưa có qn hậu nào xếp, c[j]=0 thì cột j đã có quân hậu xếp)
- Mảng ct để ghi nhận trạng thái đường chéo tổng i+j:
+ ct[i+j]=1 thì đường chéo i+j chưa có qn hậu nào chiếu đến
+ ct[i+j]=0 thì đường chéo i+j đã có quân hậu chiếu đến
- Mảng ch để ghi nhận trạng thái đường chéo hiệu i-j: Để chỉ số của mảng
chạy từ 1 nên với i-j chạy từ -7 đến 7 ta đặt tương ứng với i-j+8 chạy từ 1 đến 15.
+ ct[i-j+8]=1thì đường chéo i-j chưa có qn hậu nào chiếu đến
+ ct[i-j+8]=0 thì đường chéo i-j đã có quân hậu chiếu đến
Các hàm:
init() //Hàm khởi tạo
{
for(i=1;i<=8;i++)
c[i]=1;
93


for(i=2;i<=2*8;i++)
ct[i]=1;
for(i=1;i<=2*8-1;i++)
ch[i]=1;
}
try(i) // Xếp quân hậu hàng i;
{
for(j=1;j<=8;j++)
if(c[j]&&ct[i+j]&&ch[i-j+8])
{

x[i]=j;
c[j]=0;
ct[i+j]=0;
ch[i-j+8]=0;
if(i==8)
ht(); // Hàm đưa ra cách xếp vừa tìm được
else try(i+1);
c[j]=1;
ct[i+j]=1;
ch[i-j+8]=1;
}
}
main()
{
init();
try(1);
}
4.2.5. Tìm đƣờng đi trên đồ thị
1) Bài toán
94


G = (V, E) là đơn đồ thị (có hướng hoặc vô hướng). V = {1,. ., n} là tập các đỉnh,
E là tập cạnh (cung). Với s, t  V, tìm tất cả các đường đi từ s đến t.
Các thuật tốn tìm kiếm cơ bản :
Thuật tốn DFS : Tìm kiếm theo chiều sâu.
Thuật tốn BFS : Tìm kiếm theo chiều rộng.
2) ThiÕt kÕ tht to¸n
* Thuật tốn DFS tiến hành tìm kiếm trong đồ thị theo chiều sâu. Thuật toán thực
hiện việc thăm tất cả các đỉnh có thể đạt được cho tới đỉnh t từ đỉnh s cho trước.

Đỉnh được thăm càng muộn sẽ càng sớm được duyệt xong (cơ chế LIFO –Vào sau
ra trước). Nên thuật tốn có thể tổ chức bằng một thủ tục đệ quy quay lui.
Input: G = (V,E), s, t
Output: Tất cả các đường đi từ s đến t (nếu có).
DFS (s)
{
for ( u = 1; u <= n; u++)
{
if (chấp nhận được)
{
Ghi nhận nó;
if (u  t)
DFS(u);
else
In đường đi;
bỏ việc ghi nhận;
}
}
Ta cần mô tả dữ liệu đồ thị và các mệnh đề được phát biểu trong mơ hình.
Ðồ thị sẽ được biểu diễn bằng ma trận kề : a =(aij) 1<=i, j<=n và:
aij = 1 nếu (i, j )  E
aij = 0 nếu (i, j )  E
Ghi nhận đỉnh được thăm để tránh trùng lặp khi quay lui bằng cách đánh dấu.
Ta sữ dụng một mảng một chiều Daxet[] với qui ước :
95


Daxet[u] = 1 , u đã được thăm.
Daxet[u] = 0 , u chưa được thăm.
Mảng Daxet[] lúc đầu khởi t?o bằng 0 tất cả.

Điều kiện chấp nhận được cho đỉnh u chính là u kề với v (a vu = 1) và u chưa
được thăm ( Daxet[u] = 0).
Để ghi nhận các đỉnh trong đường đi, ta dùng một mảng một chiều Truoc[ ],
với qui ước :
Truoc[u] = v với v là đỉnh đứng trước đỉnh u, và u kề với v
Ta khởi tạo mảng Truoc[ ] bằng 0 tất cả.
Thuật toán đượcc làm mịn hơn :
Input G = (aij)nxn , s, t
Output Tất cả các đường đi từ s đến t (nếu có).
DFS(s)
{
daxet[s] = 1;
for( u = 1;u <= n; u++)
{
if( a[s][u] && !daxet[u])
{
Truoc[u] = s;
if ( u == t )
duongdi();
else
DFS(u);
daxet[u] = 0;
}
}
Mảng truoc[ ] lưu trử các đỉnh trên đường đi. Nếu kết thúc thuật toán,
Daxet[t] = 0 ( Truoc[t] = 0 ) thì khơng có đường đi từ s đến t.
Trong trường hợp tồn tại đường đi, xuất đường đi chính là xuất mảng Truoc[].
Thao tác này có thể viết như sau
duongdi()
96



{
printf(t,"<--");
j = t;
while (truoc[j] != s)
{
printf(truoc[j],"<--");
j = truoc[j];
}
printf(s);
}
* Thuật toán BFS tiến hành tìm kiếm trên đồ thị theo chiều rộng. Thuật toán thực
hiện việc thăm tất cả các đỉnh có thể đạt được cho tới đỉnh t từ đỉnh s cho trước theo
từng mức kề. Đỉnh được thăm càng sớm thì sẽ càng sớm được duyệt xong (cơ chế
FIFO – Vào trước ra trước).
Input G = (V,E),
s, t V;
Output
Đường đi từ s đến t.
Mô tả :
Bước 0 : A0 = {s}.
Bước 1 : A1 = {x V \ A0 : ( s, x) E}.
Bước 2 : A2 = {x ? V \ {A0?A1} : ?y  A1 , ( y, x)  E}.
....
i 1

Bước i : Ai = {xV/  Ak : yAi-1, (y,x)E} .
k 0



Thuật tốn có khơng quá n bước lặp, một trong hai trường hợp sau xảy ra :
- Nếu với mọi i, t  Ai : khơng có đường đi từ s đến t;
- Ngược lại, tAm với m nào đó. Khi đó tồn tại đường đi từ s tới t, và đó là
một đường đi ngắn nhất từ s đến t.
Trong trường hợp này, ta xác định được các đỉnh trên đường đi bằng cách quay
97


ngược lại từ t đến các đỉnh trước t trong từng các tập trước cho đến khi gặp s.
Trong thuật toán BFS, đỉnh được thăm càng sớm sẽ càng sớm trở thành duyệt
xong, nên các đỉnh được thăm sẽ được lưu tr? trong hàng đợi queue. Một đỉnh sẽ trở
thành duyệt xong ngay sau khi ta xét xong tất cả các đỉnh kề của nó .
Ta dùng một mảng Daxet[] để đánh dấu các đỉnh được thăm, Daxet[i]=0 là
đỉnh i chưa được thăm, Daxet[i]= là đỉnh i đã được thăm. Mảng này được khởi
động bằng 0 tất cả để chỉ rằng lúc đầu chưa đỉnh nào được thăm.
Một mảng truoc[ ] để lưu trữ các đỉnh nằm trên đường đi ngắn nhất cần tìm
(nếu có), với ý nghĩa Truoc[i] là đỉnh đứng trước đỉnh i trong đường đi. Mảng
Truoc[ ] được khởi tạo bằng 0 tất cả để chỉ rằng lúc đầu chưa có đỉnh nào.
Đồ thị sẽ được biểu diễn bằng ma trận kề : a =(aij) 1<=i, j<=n và:
aij = 1 nếu (i, j ) E
aij = 0 nếu (i, j ) E
Queue được tổ chức bằng mảng. Thuật toán được viết mịn hơn như sau :
BFS(s)
{
dauQ = 1, cuoiQ = 1;
queue[cuoiQ] = s;
Daxet[s] = 1;
while ( dauQ <= cuoiQ)
{

u = queue[dauQ];
dauQ++;
for ( j = 1; j <= n; j++)
if( a[u][j] == 1 && !Daxet[j] )
{
cuoiQ++;
queue[cuoiQ] = j;
Daxet[j] = 1;
Truoc[j] = u;
}
}
Nhận xét :
98


Ta có thể thấy mỗi lần gọi DFS(s), BFS(s) thì mọi đỉnh thuộc cùng một thành
phần liên thông với s sẽ được thăm, nên sau khi thực hiện hàm trên thì :
• Truoc[t] = 0 : khơng tồn tại đường đi từ s đến t,
• Ngược lại, có đường đi từ s đến t. Khi đó lời giải được cho bởi :
t ← p1 = Truoc[t] ← p2 = Truoc[p1] ← … ← s .
4.2.6. Bài toán ngựa đi tuần
1) Bài tốn
Cho bàn cờ có n x n ơ. Một con ngựa được phép đi theo luật cờ vua, đầu tiên
được đặt x0 , y0. Một hành trình của ngựa là đi qua tất cả các ô của bàn cờ, mỗi ô đi
qua đúng một lần. Hãy đưa ra các hành trình (nếu có) của ngựa.
2) ThiÕt kÕ tht to¸n
Cách giải quyết rõ ràng là xét xem có thể thực hiện một nước đi kế nữa hay
không. Sơ đồ đầu tiên có thể phát thảo như sau :
Try(i)
{

for ( j = 1 -> k)
if ( xi chấp nhận được khả năng j)
{
Xác định xi theo khả năng j;
Ghi nhận trạng thái;
if( i < n2 )
Try(i+1);
else
Ghi nhận nghiệm;
Hoàn trả trạng thái ;
}
}
Để mơ tả chi tiết thuật tốn, ta phải qui định cách mơ tả dữ liệu và các thao
tác, đó là :
- Biểu diễn bàn cờ .
99


- Các khả năng chọn lựa cho xi
- Cách thức xác định xi theo j.
- Cách thức gi nhận trạng thái mới, trả về trạng thái cũ.
- Ghi nhận nghiệm.
* Ta sẽ biểu diễn bàn cờ bằng 1 ma trận vuông cấp n: h[n][n] mà h[i][j] tương ứng
với ô ở hàng i cột j (1 ≤ i, j ≤ n) và dùng các phần tử của ma trận để ghi nhận quá
trình di chuyển của con ngựa với qui ước như sau:
h[x][y] = 0: Ô <x,y> ngựa chưa đi qua;
h[x][y] = i : Ô <x,y> ngựa đã đi qua ở bước thứ i (1  i  n2 ).
* Các khả năng lựa chọn cho xi? Đó chính là các nước đi của ngựa mà xi có thể
chấp nhận được.
Với ơ bắt đầu <x,y> như trong hình vẽ, có tất cả 8 ơ <u,v> mà con ngựa có

thể đi đến. Giả sử chúng được đánh số từ 0 đến 7 như hình sau :
( ơ có dấu * là ơ ở hàng x cột y nơi ngựa đang đứng)
4

3

5

2
*

6

1
7

0

Hình 4.1. Các ơ mà ngựa có thể đi đến
Một cách tương đối các ơ mà ngựa có thể đi qua khi đang đứng ở ơ <x,y> là:
Ơ đánh số 0:

<x+2, y+1>

Ơ đánh số 1:

<x+1, y+2>

Ô đánh số 2:


<x-1, y+2>

Ô đánh số 3:

<x-2, y+1>

Ô đánh số 4:

<x-2, y-1>

Ô đánh số 5:

<x-1, y-2>

Ô đánh số 6:

<x+1, y-2>

Ô đánh số 7:

<x+2, y-1>
100


Ta dùng mảng a lưu trữ các sai khác về chỉ số hàng của các ô trên so với x và
dùng mảng b lưu trữ caùc sai khác về chỉ số cột của các ơ trên so với y thì mảng a
và mảng b sẽ được khởi tạo như sau:
Mảng a:
a[0] = 2;
a[1] = 1;

a[2] = -1;
a[3] = -2;
a[4] = -2;
a[5] = -1;
a[6] = 1;
a[7] = 2;
Mảng b:
b[0] = 1;
b[1] = 2;
b[2] = 2;
b[3] = 1;
b[4] = -1;
b[5] = -2;
b[6] = -2;
b[7] = -1;
Dùng một biến chỉ số k để đánh số các bước đi tiếp theo có thể thì việc
duyệt các bước đi tiếp theo có thể được diễn tả là:
for(k = 0; k <= 7; k++)
u = x + a[k];
v = y + b[k];
Điều kiện <chấp nhận được> có thể được biểu diễn như kết hợp của các điều
kiện :
Ô mới phải thuộc bàn cờ (1 ≤ u ≤ n và 1 ≤ v ≤ n) và ngựa chưa đi qua ơ đó,
nghĩa là h[u,v] = 0;
* Để ghi nhận nước đi hợp lệ ở bước i, ta gán h[u][v] = i (ghi nhận trạng
101


thái); và để hủy một nước đi thì ta gán h[u][v] = 0 (hoàn trả trạng thái).
* Ma trận h ghi nhận kết quả nghiệm. Nếu có <x,y> sao cho h<x,y> = 0 thì

khơng có đường đi , cịn ngược là h chứa đường đi của ngựa.
Thuật tốn có thể mơ tả như sau :
Input n, //Kích thước bàn cờ
x, y;//Toạ độ xuất phát ở bước i
Output h;
Try(i, x, y)
{
for(k = 0; k <= 7; k++)
{
u = x + a[k];
v = y + b[k];
if (1 <= u ,v <= n &&h[u][v] == 0)
{
h[u][v] = i;
if (i < n*n)
Try(i+1,u,v);
else
in(); // In ma trận h
h[u][v] = 0;
}
}
}
Hàm in(): hàm này sẽ in hành trình của ngựa được lưu trong ma trận h (nếu
có)
Hàm init(): nhập n, khởi tạo giá trị 0 cho tất cả các phần tử của mảng h.
Khi đó ta có chương trình chính:
main()
{
102



init();
h[x0 ][y0 ] = 1;
try(2,x0 , y0);
}
Với n=5 và x0=1, y0=1 thì thuật tốn cho một trong các lời giải là:
1

6

15

10

21

14

9

20

5

16

19

2


7

22

11

8

13

24

17

4

25

18

3

12

23

Hình 4.2. Một lời giải
Nếu chọn ơ xuất phát chẳng hạn là (2,3) thì bài tốn khơng có lời giải.

103



BÀI TẬP CHƢƠNG 4
1.Cho một dãy số nguyên a1, a2, ..., an và một số nguyên S. Liệt kê tất cả các cách
phân tích S = ai1 + ai2 + ... + aik (ai1, ai2, ..., aik là các số của dãy đã cho)
Hướng dẫn:
Với mỗi giá trị k từ 1 đến n ta xây dựng các tập con gồm k phần tử của dãy
số nguyên a1, a2, ..., an. Với mỗi tập con này ta tính tổng các phần tử của nó, nếu
tổng bằng S thì hiển thị tổng đó ra.
Ta áp dụng thuật tốn quay lui để liệt kê các cách phân tích S = ai1 + ai2 + ...
+ aik
Sử dụng :
- Mảng a để lưu trữ n số nguyên a1, a2, ..., an
- Mảng b với các phần tử b[i] dùng để ghi nhận chỉ số của phần tử của mảng
a. Phần tử có chỉ số này của mảng a được gán cho c[i].
- Mảng c dùng để lưu trữ tập con gồm k phần tử xây dựng được.
- Hàm khoitao để nhập n, nhập S, nhập n số nguyên a1, a2, ..., an
- Hàm Try để xây dựng các tập con gồm k phần tử.
- Hàm ht để hiển thị ra tổng thoả mãn điều kiện. Với mỗi tập con gồm k phần
tử lấy từ n số nguyên a1, a2, ..., an, hàm ht sẽ kiểm tra xem tổng của k phần tử này
có bằng S khơng? Nếu bằng thì hiển thị tổng đó ra.
try(int i)
{int j;
for(j=b[i-1]+1;j<=n-k+i;j++)
{ b[i]=j;
c[i]=a[j];
if(i==k) ht(c);
else try(i+1);
}
}

main()
{
khoitao();
104


for(k=1;k<=n;k++)
try(1);
getch();
2. Cho một dãy gồm n số nguyên a1, a2, ..., an. Hãy đưa ra các tổng đại số thành lập
được từ dãy này mà có tổng bằng 0.
Ví dụ: Cho các số nguyên 1, 2, 3, 4, 5, 6, 7 thì một tổng đại số bằng 0 thành
lập được là:
- 1 + 2 - 3 -4 + 5 - 6 + 7 =0
Hướng dẫn:
Ta phải điền các dấu + hoặc - vào các số từ a1 đến an. Áp dụng thuật toán
quay lui để giải quyết bài toán này, ta sẽ dùng hàm đệ quy Try(i). Giả sử ta đã điền
các dấu’+’ và ’-’ vào các số từ a1 đến ai, bây giờ cần điền dấu cho ai + 1. Ta có thể
chọn một trong hai khả năng: hoặc là điền dấu ’+’, hoặc là điền dấu ’-’ bằng cách
gọi đệ quy Try(i+1). Ta sẽ lần lượt duyệt tất cả các khả năng đó để tìm tất cả các
nghiệm của bài toán.
Nếu i=n ta được một tổng đại số, khi đó kiểm tra xem tổng đại số đó có kết
quả bằng 0 hay khơng. Nếu bằng 0 thì đưa ra tổng đó, ngược lại thì khơng đưa ra.
Ta dùng hàm in() để thực hiện điều này
Dùng mảng b mà phần tử bi lưu trữ dấu của số nguyên ai. Qui ước chẳng hạn
0 ứng với dấu + và 1 ứng với dấu - tức là:
Nếu bi =0 thì dấu của ai là +
Nếu bi =1 thì dấu của ai là Try(i)
{
for(j=0;j<=1;j++)

{
b[i]:=j;
if(i==n) in();
else try(i+1);
}
}
in()
105


{
s=0;
for(i=1;i<=n;i++)
if(b[i]==0)
s=s+a[i];
else
s= s-a[i];
if(s==0) // hiển thị tổng đại số
for(i=1;i<=n;i++)
if((i==1)&&(b[i]==0))// phần tử đầu dấu + không cần đưa ra dấu
printf(a[i]);
else
if(b[i]==0)
printf("+",a[i]);
else
printf("-",a[i]);
}
3. Liệt kê các số có 6 chữ số với các chữ số khác nhau sao cho trong các số đó thì
tổng 3 chữ số đầu bằng tổng 3 chữ số cuối.
Hướng dẫn:

Các số có 6 chữ số có dạng a1 a2 a3 a4 a5 a6 trong đó a1 nhận một trong các
giá trị từ 1 đến 9, các chữ số a2, a3, a4, a5, a6 mỗi chữ số có thể nhận một trong các
giá trị từ 0 đến 9. Ta dùng thuật toán quay lui thực hiện liệt kê các số thoả mãn điều
kiện.
- Mảng a để lưu trữ số có 6 chữ số khác nhau tìm được (ở đây ta chấp nhận
cả số mà chữ số đầu tiên bằng 0, khi hiển thị ta không quan tâm tới những số này)
- Mảng b mỗi phần tử dùng để ghi nhận một chữ số đã được dùng hay chưa.
Nếu b[j]=0 thì chữ số j chưa được dùng, nếu b[j]=1 thì chữ số j đã được dùng
- Hàm khoitao: khởi tạo giá trị các phần tử mảng b và biến d (biến d dùng để
dếm các số thoả mãn điều kiện)
106


- Hàm ht kiểm tra số vừa tìm được, nếu thoả mãn điều kiện thì hiển thị số đó
ra.
- Hàm try để xây dựng các số có 6 chữ số khác nhau.
main()
{
khoitao();
try(1);
getch();
}
khoitao()
{for(i=0;i<=9;i++)b[i]=0;
d=0;
}
ht(x)
{
if((x[1]!=0)&&(x[1]+x[2]+x[3]==x[4]+x[5]+x[6]))
{ d++;

printf("\n%d: ",d);
for(i=1;i<=6;i++)printf("%d",x[i]);
}
}
try(i)
{
for(j=0;j<=9;j++)
if(b[j]==0)
{
a[i]=j;
b[j]=1;
if(i==6)
ht(a);
107


else
try(i+1);
b[j]=0;
}
}
4. Liệt kê các chỉnh hợp không lặp chập k của n số nguyên a1, a2, ..., an (k  n).
Hướng dẫn:
Ta áp dụng thuật toán quay lui để liệt kê các chỉnh hợp không lặp chập k của
tập n số nguyên a1, a2, ..., an.
- Mảng a để lưu trữ n số nguyên a1, a2, ..., an
- Mảng b với các phần tử b[i] dùng để ghi nhận số nguyên tương ứng a[i] đã
được dùng chưa. Nếu b[i]= 0 thì số nguyên a[i] chưa được dùng, nếu b[i]= 1 thì số
nguyên a[i] đã được dùng. Đầu tiên b[i] = 0 với mọi i.
- Mảng c dùng để lưu trữ chỉnh hợp không lặp chập k của tập n số nguyên a1,

a2, ..., an xây dựng được.
- Biến d để đếm số chỉnh hợp không lặp chập k của tập n số nguyên a1, a2, ...,
an.
- Hàm khoitao để nhập n, nhập k, khởi tạo biến đếm d, khởi tạo giá trị cho
các phần tử của mảng b.
- Hàm nhap để nhập các số nguyên.
- Hàm Try để xây dựng các chỉnh hợp không lặp chập k của tập n số nguyên
a1, a2, ..., an.
- Hàm ht để hiển thị ra chỉnh hợp không lặp chập k của tập n số nguyên a1,
a2, ..., an vừa xây dựng được.
main()
{
khoitao();
nhap();
try(1);
getch();
}
108


khoitao()
{
scanf(n);
scanf(k);
d=0;
for(i=1;i<=n;i++)
b[i]=0;
}
try(i)
{

for(j=1;j<=n;j++)
if(b[j]==0)
{
c[i]=a[j];
b[j]=1;
if(i==k)
ht();
else
try(i+1);
b[j]=0;
}
}
5. Ma phương bậc n là ma trận vuông cấp n với các phần tử là các số tự nhiên từ 1
đến n2 thoả mãn tính chất: Tổng các phần tử trên mỗi hàng, mỗi cột và mỗi đường
chéo đều bằng nhau. Liệt kê các ma phương bậc 3.
Hướng dẫn:
- Các phần tử của ma trận vuông cấp ba A lần lượt được lưu trữ trong các phần tử từ
x[1] đến x[9] của mảng x:
a11 được lưu trữ trong x[1]
a12 được lưu trữ trong x[2]
109


a13 được lưu trữ trong x[3]
a21 được lưu trữ trong x[4]
a22 được lưu trữ trong x[5]
a23 được lưu trữ trong x[6]
a31 được lưu trữ trong x[7]
a32 được lưu trữ trong x[8]
a33 được lưu trữ trong x[9]

- Dùng thuật toán quay lui xây dựng tất cả các hoán vị của 9 phần tử 1, 2, 3,..., 9
Mỗi hoán vị sẽ được lưu trữ trong các phần tử của mảng x.
- Mỗi khi được một hốn vị thì kiểm tra điều kiện, tức là kiểm tra các tổng:
x[1] + x[2] + x[3]; x[4] + x[5] + x[6]; x[7] + x[8] + x[9] (tổng các hàng)
x[1] + x[4] + x[7]; x[2] + x[5] + x[8]; x[3] + x[6] + x[9] (tổng các cột)
x[1] + x[5] + x[9]; x[3] + x[5] + x[7]

(tổng các đường chéo)

nếu các tổng đó bằng nhau thì hiển thị các phần tử của mảng x ra màn hình dưới
dạng ma trận (Nếu i mod 3 = 0 thì xuống hàng)

110


×