Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 23 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
23
Dung lượng
301,32 KB
Nội dung
Chương 1 Bài toánliênquan tổ hợp 1.1 Phương pháp sinh Phương pháp sinh được áp dụng để giải quyết bàitoán liệt kê của lý thuyết tổ hợp. Để áp dụng được phương pháp này thì bàitoán phải thoả mãn hai điều kiện sau: o Có thể xác định được thứ tự trên tập các cấu hình tổhợp cần liệt kê. Từ đó có thể xác định được cấu hình đầu tiên và cấu hình cuối cùng trong thứ tự đó. o Xây dựng được một thuật toán cho phép từ một cấu hình chưa phải cấu hình cuối, sinh ra được cấu hình kế tiếp của nó. Phương pháp sinh có thể được mô tả tổng quát như sau: <Xây dựng cấu hình đầu tiên> Do <Đưa ra cấu hình đang có> <Từ cấu hình đang có sinh ra cấu hình kế tiếp> While <Còn cấu hình or khác cấu hình cuối> 1.1.1 Bàitoán sinh dãy nhị phân độ dài n Bài toán: một tập hợp hữu hạn có n phần tử có thể được biểu diễn tương đương với tập các số tự nhiên 1, 2, , n. Bàitoán đặt ra là: cho một tập hợp gồm n phần tử X = {X 1 , X 2 , , X n } hãy liệt kê tất cả các tập con của tập này. Để biểu diễn tập con Y của X ta dùng xâu nhị phân B n = {B 1 , B 2 , , B n }, sao cho nếu B i = 0 thì X i ∉ Y, ngược lại B i = 1 thì X i ∈ Y. Ví dụ như dãy 0011 của tập hợp gồm n thể hiện cho tập Y = {X 3 , X 4 } do phần tử B 3 và B 4 có giá trị là 1. Khi đó ta quy về bàitoán liệt kê tất cả xâu nhị phân có kích thước n. Số các xâu nhị phân là 2 n . Một dãy nhị phân x độ dài n là biểu diễn một số nguyên p(x) nào đó trong đoạn [0, 2 n -1]. Do đó số các dãy nhị phân độ dài n = số các số nguyên ∈ [0, 2 n -1] = 2 n . Mục tiêu là lập một chương trình liệt kê các dãy nhị phân n phần tử theo thứ tự từ điển, có nghĩa là liệt kê dãy nhị phân biểu diễn các số nguyên theo thứ tự 0, 1, , 2 n -1. Khi n =3, các độ dài 3 được liệt kê như sau: p(x) 0 1 2 3 4 5 6 7 x 000 001 010 011 100 101 110 111 Khi đó dãy đầu tiên là: 000 và dãy cuối cùng là 111. Nhận thấy rằng nếu x là dãy đang có và phải là dãy cuối cùng thì dãy tiếp theo cần liệt kê chính là x cộng thêm 1 đơn vị trong hệ nhị phân! Ví dụ n = 6: Dãy đang có: 010000 Dãy đang có: 010111 Cộng thêm 1: +1 Cộng thêm 1: +1 ______ ______ Dãy mới: 010001 Dãy mới: 011000 Kỹ thuật sinh kế tiếp từ cấu hình hiện tại có thể mô tả như sau: xét từ cuối dãy lên từ hàng đơn vị tìm số 0 đầu tiên. Nếu tìm thấy thì thay số 0 bằng số 1 và đặt tất cả phần tử phía sau vị trí đó bằng 0. Nếu không tìm thấy thì toàn là dãy chứa 1, đây là cấu hình cuối cùng. Chương trình minh họa 1: chương trình C/C++ liệt kê chuỗi nhị phân n bit. int Stop; // biến toàn cục void Next_BS(int B[MAX], int n) // Hàm phát sinh chuỗi kế tiếp { int i = n; // duyệt từ cuối while (i>0 && B[i]) // lặp khi chưa tìm thấy B[i] ==0 { B[i] = 0; // gán các bit sau là 0 i--; // giảm về trước } if (i==0 ) Stop = 1; // cấu hình cuối nên không tìm được B[i] = 0 -> dừng else B[i] = 1; // gán 1 cho B[i] } void Generate(int B[MAX], int n) // Hàm sinh chuỗi nhị phân { Stop = 0; while (! Stop) { Result(B,n); // xuất chuỗi nhị phân hiện tại Next_BS(B,n); // chuỗi nhị phân tiếp theo. } } void Result(int B[MAX], int n) { static int count=0; printf(“\n Xau nhi phan thu %d”, ++count); for(int i=0; i < n;i++) printf(“%3d”, B[i]); } int main() { int i, B[MAX], n; printf(“Nhap n: ”); scanf(“%d”,&n); for(i=0; i< n; i++) B[i] =0; Generate(b, n); getch(); return 0; } 1.1.2 Bàitoán liệt kê tập con k phần tử Phát biểu: Cho tập hợp X = {1, 2, , n}. Hãy liệt kê tất cả tập con k phần tử của X. Mỗi tập con k phần tử của X cho thể biểu diễn như bộ thứ tự: a = (a 1 , a 2 , , a k ) thỏa mãn 1 ≤ a 1 ≤ a 2 ≤ . ≤ a k ≤ n. Trên tập con k phần tử của X, ta định nghĩa thứ tự của các tập con như sau: Ta nói tập a = (a 1 , a 2 , , a k ) có thứ tự trước tập a’ = (a’ 1 , a’ 2 , , a’ k ) theo thứ tự từ điển và ký hiệu là a < a’ nếu tìm được j sao cho: a 1 = a’ 1 , a 2 = a’ 2 ., a j-1 = a’ j-1 và a j < a’ j . Ví dụ với n = 5, k = 3, ta liệt kê 10 tập con của 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}} + Ta thấy cấu hình đầu tiên là {1, 2 ., k} + Cấu hình kết thúc là {n-k+1, n-k+2, , n}. Nhận xét: chúng ta sẽ in ra tập con với các phần tử của nó theo thứ tự tăng dần. Biểu diễn tập con là một dãy a{a 1 , a 2 , ., a k } trong đó a 1 < a 2 < .<a k . Ta nhận thấy giới hạn trên của a k là n, của a k-1 là n-1, của a k-2 là n-2. Tổng quát giới hạn trên của a i = n-k+i. Còn giới hạn dưới của của a i (giá trị nhỏ nhất ai có thể nhận) là a i-1 + 1. Như vậy nếu ta đang có một dãy x đại diện cho tập con, nếu x là cấu hình kết thúc thì có nghĩa tất cả các phần tử trong x đều đạt tới giới hạn trên thì quá trình sinh kết thúc. Nếu không thì phải phát sinh một dãy x tăng dần thỏa mãn đủ lớn hơn dãy x và không có dãy nào chen vào giữa hai dãy theo thứ tự từ điển. Ví dụ: n = 9, k = 6, cấu hình đang có <1, 2, 6, 7, 8, 9>, các phần tử a 3 ⇒ a 6 đã đạt đến giới hạn nên ta không thể tăng các phần tử này được, ta phải tăng a 2 từ 2 lên thành 3. Được cấu hình mới là <1, 3, 6, 7, 8, 9> cấu hình này thoả mãn lớn hơn cấu hình cũ, nhưng chưa thoả mãn tính chất vừa đủ lớn do đó ta phải thay a 3 , a 4 , a 5 , a 6 bằng giới hạn dưới của nó như sau: a 3 = a (3-1= 2) + 1 = 3 + 1 = 4 a 4 = a (4-1= 3) + 1 = 4 + 1 = 5 a 5 = a (5-1= 4) + 1 = 5 + 1 = 6 a 6 = a (6-1= 5) + 1 = 6 + 1 = 7 Vậy cấu hình tiếp theo <1, 3, 4, 5, 6, 7> là cấu hình cần tìm. Do đó muốn xác định cấu hình tiếp ta thấy a 6 = 7 chưa đạt đến giới hạn ta chỉ cần tăng a 6 lên một là được cấu hình tiếp theo: <1, 3, 4, 5, 6, 8>. Vậy kỹ thuật sinh tập con kế tiếp từ tập x đã có có thể xây dựng như sau: Tìm từ cuối lên đầu dãy cho tới khi gặp phần tử a i chưa đạt đến giới hạn n-k+i. Nếu tìm thấy: o Tăng a i đó lên 1. o Đặt tất cả phần tử phía sau a i bằng giới hạn dưới. Nếu không tìm thấy tức là phần tử đã đạt giới hạn trên, đây là cấu hình cuối cùng. Kết thúc thuật toán. Chương trình minh họa 2: liệt kê tập con k phần tử của n. int A[MAX], Stop, n ,k; void Next_SubSet() { int i,j; i = k; // duyệt từ cuối dãy // lặp khi chưa tìm được phần tử chưa tới giới hạn while (i >0 && A[i] == n-k+i) i--; // duyệt về đầu if ( i > 0) { A[i] = A[i] +1; // tăng một đơn vị // cho các phần tử còn lại qua giới hạn dưới for(j = i+1; j <= k; j++) A[j] = A[j-1]+ 1 } else Stop = 1; // kết thúc phát sinh cấu hình } void GenerateSet() { Stop = 0; while (!Stop) { Result(); // xuất cấu hình hiện tại Next_SubSet(); // qua cấu hình khác } } void Result() { static int count=0; printf(“Tap con thu %d”, ++count); for(i=1; i <=k; i++) printf(“%3d”, A[i]); } int main() { printf(“Nhap n: ”); scanf(“%d”, &n); printf(“Nhap k: ”); scanf(“%d”, &k); for(int i=1; i <= n;i++) A[i] = i; GenerateSet(); getch(); return 0; } 1.1.3 Bàitoán liệt kê các hoán vị Bài toán: Cho tập hợp X = {1, 2, ., n}, hãy liệt kê tất cả hoán vị của X. Mỗi hoán vị n phần tử của tập X có thể biểu diễn bởi bộ có thứ tự gồm n thành phần a = {a 1 , a 2 , , a n } thoả a i ∈ X; i = 1, 2, , n; a p ≠ a q nếu p ≠ q. Trên các tập hoán vị của X ta định nghĩa thứ tự của các hoán vị như sau: a = (a 1 , a 2 , ., a n ) được gọi là có thứ tự trước hoán vị a’=(a’ 1 ,a’ 2 , ,a’ n ). Có ký hiệu a < a’ nếu tìm được chỉ số k sao cho. a 1 = a’ 1 , a 2 = a’ 2 , ., a k-1 = a’ k-1 , a k <a’ k . Ví dụ X = {1, 2, 3, 4} khi đó thứ tự hoán vị n = 4 được liệt kê như sau: {{1, 2, 3, 4}, {1, 2, 4, 3}, {1, 3, 2, 4} {1, 3, 4, 2} {1, 4, 2, 3} {1, 4, 3, 2} {2, 1, 3, 4}, {2, 1, 4, 3}, {2, 3, 1, 4} {2, 3, 4, 1} {2, 4, 1, 3} {2, 4, 3, 1} {3, 1, 2, 4}, {3, 1, 4, 2}, {3, 2, 1, 4} {3, 2, 4, 1} {3, 4, 1, 2} {3, 4, 2, 1} {4, 1, 2, 3}, {4, 1, 3, 2}, {4, 2, 1, 3} {4, 2, 3, 1} {4, 3, 1, 2} {4, 3, 2, 1}} Hoán vị đầu tiên là: {1, 2, ., n-1, n} và hoán vị cuối cùng là {n, n-1, ,2, 1}. Khi đó hoán vị kế tiếp sinh ra phải lớn hơn hoán vị hiện tại, và hơn nữa nó phải đủ lớn hơn hoán vị hiện tại theo nghĩa không có hoán vị nào khác chen vào giữa nó khi sắp theo thứ tự từ điển. Giả sử có hoán vị sau: <3, 2, 6, 5, 4, 1>, ta xét 4 phần tử cuối cùng, do chúng được sắp theo thứ tự giảm dần. Khi đó ta hoán vị 4 giá trị này thì cũng chỉ được hoán vị nhỏ hơn hoán vị hiện tại. Như vậy ta phải xét đến a 2 = 2, ta phải thay giá trị này, nhưng thay giá trị nào? ta không thể thay bằng 1 vì nếu như vậy sẽ được hoán vị nhỏ hơn, không thể thay bằng 3 vì giá trị này đã có rồi a 1 = 3 (phần tử sau không được chọn vào những giá trị xuất hiện ở phần tử trước). Chỉ còn lại giá trị 4, 5, 6. Vì cần một hoán vị đủ lớn nên ta chọn a 2 = 4. Còn các giá trị a 3 , a 4 , a 5 , a 6 sẽ lấy trong tập {2, 6, 5, 1}. Cũng do tính chất vừa đủ lớn nên ta sẽ tìm biểu diễn nhỏ nhất của 4 số này để gán cho a 3 , a 4 , a 5 , a 6 , là <1, 2, 5, 6> vậy ta được hoán vị mới là <3, 4, 1, 2, 5, 6> Nhận xét: đoạn cuối của hoán vị hiện tại được sắp giảm dần. số a 5 là 4 là số nhỏ nhất trong đoạn cuối lớn hơn a 2 = 2. Nếu đổi chỗ a 5 cho a 2 thì ta được a 2 = 4 và đoạn cuối vẫn được xếp giảm dần là <6, 5, 2, 1> khi đó muốn biểu diễn nhỏ nhất cho các giá trị trong đoạn cuối thì ta chỉ cần đảo ngược đoạn cuối. Ví dụ trong hoán vị hiện tại <2, 1, 3, 4> có hoán vị kế tiếp là <2, 1, 4, 3>. Ta có thể xem <2, 1, 3, 4> có đoạn cuối giảm dần là một phần tử <4>. Vậy kỹ thuật sinh hoán vị kế tiếp từ hoán vị hiện tại có thể xây dựng như sau: Xác định đoạn cuối giảm dần dài nhất, tìm phần tử ai đứng trước đoạn cuối đó. Điều này đồng nghĩa với việc tìm từ vị trí sát cuối dãy lên đầu, gặp chỉ số i đầu tiên thoả mãn a i < a i+1 . Nếu tìm thấy chỉ số i như trên: trong đoạn cuối giảm dần, tìm phần tử a k nhỏ nhất thoả mãn a k > a i . Do đoạn cuối giảm dần nên thực hiện bằng cách từ cuối dãy lên đầu gặp chỉ số k đầu tiên thoả a k > a i . o Đảo giá trị a k và a i . o Lật ngược thứ tự đoạn cuối giảm dần (a i+1 đến a k ) trở thành tăng dần Nếu không tìm thấy tức là dãy giảm dần, đây là cấu hình cuối cùng. Chương trình minh họa 3: Liệt kê hoán vị n phần tử. int n, P[MAX], Stop; void Next_Permutation() { int j, k; j = n -1; while (j>0 && P[j]> P[j+1]) j--; if (j == 0) Stop = 1; else { k = n; while (P[j] > P[k]) k--; Swap(P[j], P[k]); l = j+1; r = n; while (l < r) { Swap(P[l], P[r]); l++; r--; } }// end else } void Permutation() { Stop = 0; while (! Stop) { Result(); Next_Permutation(); } } void Result() { static int count=0; printf(“\n Hoan vi thu %d”, ++count); for(int i=1; i <= n; i++) printf(”%3d”, P[i]); } int main() { printf(“Nhap n: ”); scanf(“%d”, &n); for(int i=1; i <= n; i++) P[i] = i; return 0; } 1.2 Thuật toán quay lui (Back Tracking) Thuật toán quay lui dùng để giải quyết các bàitoán liệt kê các cấu hình. Phương pháp sinh trong phần trước cũng được giải quyết cho các bàitoán liệt kê khi nhận biết được cấu hình đầu tiên của bài toán. Tuy nhiên, không phải bất cứ cấu hình sinh kế tiếp nào cũng có thể sinh một cách đơn giản từ cấu hình hiện tại. Do đó thuật toán sinh kế tiếp chỉ giải quyết được cái bàitoán liệt kê đơn giản. Để giải quyết những bài toántổhợp phức tạp, người ta dùng thuật toán quay lui. Nội dung chính của thuật toán quay lui: Xây dựng dần dần các thành phần của cấu hình bằng cách thử tất cả các khả năng có thể xảy ra. Giả sử cấu hình cần liệt kê có dạng x = (x 1 , x 2 , ,x n ) khi đó thuật toán quay lui thực hiện qua các bước: 1. Xét tất cả những giá trị có thể có của x 1 , thử cho x 1 nhận lần lượt các giá trị đó. Với mỗi giá trị thử gán cho x 1 ta sẽ làm tiếp như sau: 2. Xét tất cả giá trị x 2 có thể nhận, lại thử cho x 2 nhận lần lượt các giá trị đó. Với mỗi giá trị x 2 ta lại xét lần lượt những giá trị của x 3 . cứ tiếp tục như vậy cho đến bước n. 3. Xét giá trị có thể nhận cho x n , thử cho x n lần lượt nhận những giá trị đó, thông báo những cấu hình tìm được như (x 1 , x 2 , ., x n ). Tóm lại thuật toán quay lui liệt kê các cấu hình n phần tử dạng x = (x 1 , x 2 , , x n ) bằng cách thử cho x 1 nhận lần lượt các giá trị có thể được. Với mỗi giá trị thử gán cho x 1 thì bàitoán trở thành liệt kê tiếp cấu hình n-1 phần tử x = (x 2 , x 3 , , x n ). Gốc Khả năng chọn x 1 Khả năng chọn x 2 với x 1 đã chọn Khả năng chọn x 3 với x 1 và x 2 đã chọn Hình 3.1: Liệt kê các lời giải theo thuật toán quay lui. Mô hình chung của thuật toán quay lui xác định thành phần thứ i được mô tả tổng quát như sau: (thuật toán này thử cho x i nhận lần lượt những giá trị mà nó có thể nhận). void Try(int i) { for <mọi giá trị v có thể gán cho x[i]> do { <Thử cho x[i] bằng giá trị v> if <x[i] là phần tử cuối cùng trong cấu hình hoặc i==n> then <Thông báo cấu hình tìm được> else { <Ghi nhận việc cho x[i] nhận giá trị v (nếu cần thiết)> Try(i+1); // gọi đệ quy cho tiếp chi x[i+1]. <Nếu cần thì bỏ ghi nhận việc thử x[i]:= v để thử giá trị khác> } } } Thuật toán quay lui sẽ bắt đầu bằng lời gọi Try(1). 1.2.1 Thuật toán quay lui liệt kê dãy nhị phân n Biểu diễn dãy nhị phân độ dài n dưới dạng x = (x 1 , x 2 , ., x n ) trong đó x i nhận các giá trị là {0, 1}. Với mỗi giá trị gán cho x i ta lại thử gán các giá trị có thể có cho x i+1 . Thuật toán quay lui được viết như sau: void Try(int i, int B[MAX], int n) { int j; for(j=0; j <= 1; j++) { B[i] = j; if (i == n) Result(B, n); else Try(i+1, B, n); } } void Result(int B[MAX], int n) { int i; printf(“\n”); for(i=1; i <= n; i++) printf(“%3d”, B[i]); } int main() { int n, B[MAX]; printf(“Nhap n: ”); scanf(“%d”, &n); for(int i=1; i <= n; i++) // khởi tạo cho mảng B B[i] = 0; Try(1, B, n); // gọi thuật toán quay lui return 0; } Khi n = 3, cây tìm kiếm quay lui như sau: Try(1) Try(2) Try(2) Try(3) Try(3) Try(3) Try(3) 000 x 1 = 1 x 1 = 0 x 2 = 0 x 2 = 1 x 2 = 0 x 2 = 1 x 3 = 0 x 3 = 0 x 3 = 0 x 3 = 0 x 3 = 1 x 3 = 1 x 3 = 1 x 3 = 1 001 010 011 100 101 110 111 Hình 3.2: Cây tìm kiếm quay lui trong bàitoán liệt kê dãy nhị phân. [...]... 5) 3) 1) 4) (5, (5, (5, (5, (5, (5, (5, (5, (5, (5, 4) 3) 5) 4) 5) 1) 2) 1) 3) 2) 1.2.5 Bài toán mã đi tuần Yêu cầu: Cho một bàn cờ tổng quát dạng nxn, hãy chỉ ra một hành trình của một quân Mã, xuất phát từ một vị trí bắt đầu đi qua tất cả các ô còn lại của bàn cờ, mỗi ô đi đúng một lần Ý tưởng cơ bản: dùng thuật toán quay lui; xuất phát từ 1 ô, gọi số nước đi là t=1, ta cho quân mã thử đi tiếp 1 ô... này chưa đi qua thì chọn làm bước đi tiếp theo Tại mỗi nước đi kiểm tra xem tổng số nước đi bằng n*n chưa, nếu bằng thì mã đã đi qua tất cả các ô ⇒ dừng (do chỉ cần tìm một giải pháp) Trường hợp ngược lại, gọi đệ quy để chọn nước đi tiếp theo Ngoài ra, nếu tại một bước tìm đường đi, nếu không tìm được đường đi tiếp thì thuật toán sẽ quay lui lại nước đi trước và tìm đường đi khác… Hình 3.10: Minh họa... mỗi nước đi kế tiếp (ii, jj) từ (i, j) + Nếu (ii,jj) hợp lệ chọn (ii, jj) làm nước đi kế tiếp + nếu đi hết bàn cờ xuất 1 kết quả + ngược lại Gọi đệ quy Try(step +1, ii, jj) Không chọn (ii, jj) là nước đi kế tiếp } Chương trình C/C++ minh họa cho trường hợp bàn cờ 8x8 #include "stdafx.h" #include "conio.h" #include "stdlib.h" #define MAX 12 // trường hợp bàn cờ 8x8 void Show(int board[MAX][MAX]); void... ta phải bỏ việc đánh dấu cột j và 2 đường chéo, lúc này cột j và 2 đường chéo đó sẽ tự do Thao tác này cho phép quân hậu khác có thể đặt ở vị trí đó ở những bước tiếp sau Chương trình C/C++ minh họa bài toán n-Hậu: #define MAX 12 void ShowResult(int b[MAX], int n) { /*Xuat ket qua theo dong*/ for(int i=0; i < n; i++) printf("(%d, %d)\t", i+1, b[i]+1); printf("\n"); } void Try(int *r,int *b, int n, int... tất cả cách chọn x1 từ 1 (x0 +1) đến n-k+1, với mỗi giá trị đó, xét tiếp tất cả cách chọn x2 từ x1+1 đến n-k+2 cứ như vậy khi chọn được xk thì ta có cấu hình cần liệt kê Với trường hợp n = 5 {1, 2, 3, 4, 5} và k = 3 thuật toán quay lui liệt kê tập con k phần tử được minh họa như sau: 1 2 3 4 5 4 5 3 4 5 3 4 2 3 4 4 5 5 5 N = 5; k = 3 Hình 3.3: Cây liệt kê tập con 3 phần tử với n = 5 Chương trình quay... ++count); for(int i=1; i . Chương 1 Bài toán liên quan tổ hợp 1.1 Phương pháp sinh Phương pháp sinh được áp dụng để giải quyết bài toán liệt kê của lý thuyết tổ hợp. Để áp. đó thuật toán sinh kế tiếp chỉ giải quyết được cái bài toán liệt kê đơn giản. Để giải quyết những bài toán tổ hợp phức tạp, người ta dùng thuật toán quay