PHƢƠNG PHÁP SINH I. Khái niệm Phương pháp sinh được sử dụng trong bài toán liệt kê, ví dụ như người ta cần liệt kê tổ hợp chập k của n phần tử hay hoán vị của một tập số, hay khi người cần làm một bài toán mà không thể áp dụng các phương pháp thông minh như quy hoạch động, chia để trị thì phương pháp liệt kê để xét vét cạn là sự lựa chọn cuối cùng. Phương pháp sinh là phương pháp từ cấu hình đầu tiên ta sinh ra các cấu hình tiếp theo. Không phải bài nào cũng có thể làm theo phương pháp sinh, yêu cầu của bài toán để có thể làm theo phương pháp sinh là: - Thứ nhất phải xác định được cấu hình đầu và cấu hình cuối - Thứ hai phải xác định được cấu hình tiếp theo bằng 1 công thức nhất định Việc xác định cấu hình đầu tiên và cấu hình cuối cùng là do yêu cầu đặt ra của từng bài toán và do cách xác định của từng người lập trình. Yêu cầu quan trọng hơn khi sử dụng phương pháp sinh là làm sao từ cấu hình đang có ta có thể đưa ra được cấu hình tiếp theo hoặc là khẳng định đó là cấu hình cuối cùng. Ta tạm gọi bài toán từ cấu hình ban đầu sinh ra cấu hình tiếp theo có thủ tục là sinh_kế_tiếp. Khi đó bài toán sử dụng phương pháp sinh được viết như sau: Procedure Generate; <Xây dựng cấu hình ban đầu>; Stop := False; While not Stop Do <Đưa ra cấu hình đang có>; sinh_kế_tiếp; Trong thủ tục sinh_kế_tiếp, nếu cấu hình đang có là cuối cùng thì thủ tục này cần gán cho biến Stop giá trị True, ngược lại thủ tục này sẽ xây dựng cấu hình kế tiếp của cấu hình đang có trong thứ tự đã xác định. II. Áp dụng phƣơng pháp sinh vào giải một số bài toán. 1. Bài toán liệt kê tất cả các dãy nhị phân có độ dài N. Bài toán có thể được phát biểu như sau: Cho N là một số nguyên dương, hãy chỉ ra tất cả các dãy b 1, b 2,, b N với b i {0; 1}, (1 i N). Ví dụ với N = 3 ta có các dãy đó là:
1. 0 0 0 2. 0 0 1 3. 0 1 0 5. 1 0 0 6. 1 0 1 7. 1 1 0 4. 0 1 1 8. 1 1 1 Phân tích bài toán: Ta có thể thấy ngay dãy đầu tiên là 0 0 0 0 (dãy toàn số 0) và dãy cuối cùng là 1 1 1 1 (dãy toàn số 1). Từ cấu hình đang có thì cấu hình tiếp theo được xây dựng bằng cách cộng thêm 1 (cộng nhị phân) nếu ta cách biểu diễn cấu hình là biểu diễn nhị phân của một số nguyên. Ví dụ từ cấu hình 0 1 0 (2) thì cấu hình tiếp theo là 0 1 1 (3). Từ cấu hình đang có b 1, b 2,, b N ta có thể xây dựng cấu hình tiếp theo với qui tắc sau - Tìm i đầu tiên (theo thứ tự i = n, n-1,.. 1) thoã mãn bi = 0; - Gán lại bi = 1 và bj = 0 với tất cả j > i. Dãy mới thu được sẽ là dãy cần tìm. Ví dụ: Xét dãy nhị phân độ dài 10, b = 1001001111. + Ta tìm được i đầu tiên = 5. + Bây giờ ta gán b 5 =1 và b 6, b 7, b 8, b 9, b 10 1001010000 (tương ứng với 1001001111 (591) + 1 = 1001010000 (592)) Thuật toán sinh kế tiếp cho bài toán này được diễn tả như sau: Procedure Next_Bit_String; i := n; while bi = 1 do bi := 0; i := i -1; bi := 1; Chương trình của bài toán: Program Bai1; var n, i: integer; b: Array[1..20] of 0..1; count : word; stop : boolean;
Procedure init; var i: integer; Write('Do dai day nhi phan = '); readln(n); for i := 1 to n do b[i] := 0; stop := false; count := 0; Procedure Next_Bit_String; Var i: integer; i:=n; while (i>=1) and (b[i]=1) do b[i] := 0; i := i-1; if i < 1 then stop := True Else b[i] := 1; BEGIN init; While not (top) do Count := count + 1; Write(Count:5); For i := 1 to n do Write(b[i]:2); writeln; Next_Bit_String; Write('Bam phim Enter de thoat khoi Chuong trinh '); Readln; END. 2. Bài toán liệt kê các tập con m phần tử của tập n phần tử. Bài toán có thể phát biểu như sau: Cho tập hợp Cho X = {1, 2, 3,.., n}. Hãy liệt kê các tập con có m phần tử của X. Ví dụ: X= {1, 2, 3, 4, 5}, m= 3. Các tập con 3 phần tử của X là: 1. {1, 2, 3} 2. {1, 2, 4} 3. {1, 2, 5} 4. {1, 3, 4} 5. {1, 3, 5} 6. {1, 4, 5} 7. {2, 3, 4} 8. {2, 3, 5} 9. {2, 4, 5} 10. {3, 4, 5} Phân tích bài toán: Mỗi tập con m phần tử của X có thể biểu diễn bởi bộ có thứ tự gồm m thành phần a = {a 1, a 2,.., a m 1 < a 2 < a m n.
Ta thấy cấu hình đầu tiên là {1, 2, 3,, m} và cấu hình cuối cùng là {n-m+1, n- m+2,, n} Từ cấu hình đang có {a 1, a 2,.., a m } ta tìm cấu hình tiếp theo bằng cách: Ví dụ: - Tìm từ bên phải dãy a 1, a 2,, a 3 phần tử a i n-m+i; - Thay a i bởi a i +1; - Thay a j bởi a i + j i với j = i+1, i+2,, m. Với n = 6 và m = 4. Giả sử tập con đang có là {1, 2, 5, 6}, cần xây dựng tập con kế tiếp. + Ta tìm được i = 2 + Thay a 2 = 3 + Thay a 3 =4, a 4 = 5 => ta được tập con kế tiếp là {1, 3, 4, 5} Thuật toán sinh kế tiếp cho bài toàn này được diễn tả như sau: Procedure Next_Combination; i := m; while a i = n m + i do i := i - 1; a i := a i + 1; for j := i + 1 to n do a j := a i + j - i; Chương trình của bài toán được diễn tả như sau: Program tap_con; Var n, m,i : integer; a: Array[1..20] of integer; count: longint; stop : boolean; Procedure init; Var i : integer; Write('Moi nhap N '); Readln(n); Write('Moi nhap M '); Readln(m); For i:= 1 to m do a[i] := i; stop := false; count := 0; Procedure Next_Combination;
Var i, j: integer; i := m; while (i>0) and (a[i] = n-m+i) do i := i-1; if i = 0 then stop := True else a[i] := a[i]+1; for j := i+1 to m do a[j] := a[i]+j-i; BEGIN init; while not stop do count := count+1; Write(count:5); for i := 1 to m do write(a[i]:3); writeln; Next_Combination; Write('Bam Enter de ket thuc '); Readln End. 3. Bài toán liệt kê các hoán vị của tập n phần tử. Bài toán có thể phát biểu như sau: Cho X = {1, 2, 3,.., n}. Hãy liệt kê các hoán vị từ n phần tử của X. Ví dụ với n = 3 thì các hoán vị của nó là: Phân tích bài toán: 1. (1, 2, 3) 2. (1, 3, 2) 3. (2, 1, 3) 4. (2, 3, 1) 5. (3, 1, 2) 6. (3, 2, 1) Ta thấy ngay theo thứ tự trên thì cầu hình đầu tiên là: (1, 2, 3,.. n) và cấu hình cuối cùng là (n, n-1,..., 2, 1). Cần xây dựng thuật toán để từ cấu hình đang có là (a 1, a 2,, a n ) ta tìm được cấu hình tiếp theo. Từ cấu hình đang có {a 1, a 2,, a n } ta xây dựng cấu hình tiếp theo bằng qui tắc: - Tìm j đầu tiên thoả mãn a j <a j+1 (j giảm từ n, n-1,, 1); - Tìm a k là số nhỏ nhất và a k >a j trong các số a j+1, a j+2,, a n ; - Đỗi chỗ a j với a k ;
- Đảo ngược đoạn từ a j+1 đến a n. Ví dụ với n = 6, cấu hình đang có là (3, 6, 2, 5, 4, 1) + Ta tìm được j = 3 + Ta tìm được k = 5 + Hoán vị a 3 với a 6 ta được (3, 6, 4, 5, 2, 1) + Đảo ngược đoạn a 4, a 5, a 6 ta được (3, 6, 4, 1, 2, 5) Cấu hình tiếp theo là (3, 6, 4, 1, 2, 5) Chương trình của bài toán là: Program hoan_vi; Var a:array[1..100] of integer; n,i: integer; count: longint; stop : Boolean; Procedure init; var i: integer; Write('Moi nhap vao N '); Readln(n); For i := 1 to N do a[i]:=i; Stop := False; Count := 0; Procedure next_permution; Var j,k,l,m:integer; tg: integer; j := n-1; while (j >0) and (a[j] > a[j+1]) do dec(j); if j = 0 then stop := True else k := n; while a[j] > a[k] do dec(k); tg := a[j]; a[j] := a[k]; a[k] := tg; l := n; m := j+1; while l > m do
tg := a[l]; a[l] := a[m]; a[m] := tg; dec(l); inc(m); BEGIN init; while not stop do inc(count); write(count,'. '); For i := 1 to N do write(a[i]:3); writeln; next_permution; Write('Bam Enter de ket thuc '); Readln; END. 4. Bài toán phân tích số nguyên dƣơng N thành tổng các số nguyên không âm. Bài toán được phát biểu như sau: Cho một số nguyên dương N. Hãy liệt kê các cách phân tích N thành tổng các số nguyên không âm. Ví dụ với N = 4, ta có thể phân tích thành: 1. 4 Phân tích bài toán: 2. 3 + 1 3. 2 + 2 4. 2 + 1 + 1 5. 1 + 1 + 1 + 1 Ta có thể thấy ngay cấu hình đầu tiên là: N và cấu hình cuối cùng là 1 1 1 1 (N chữ số 1). Cần xây dựng thuật toán để từ cấu hình ta tìm được cấu hình tiếp theo. Từ cấu hình a 1, a 2,, a k ta có thể xây dựng cấu hình tiếp theo với quy tắc sau: - Tìm i đầu tiên sao cho a i 1 (i giảm từ k, k-1, 1) - Thay a i = a i -1 - Phân tích (k-i+1) thành các số như sư: + Gán a j :=a i với j từ i+1 đến i + [(i+ k-i+1) div a i ]; + Gán a j+1 := [(k-i+1) mod a i ] nếu [(k-i+1) mod a i ] <> 0
(Số phần tử được phân tích thành bây giờ là i + [(k-i+1) div a i ] + [(k-i+1) mod a i ] Ví dụ: Với N=10 *Từ cấu hình 4 4 1 1 (k=4) - Ta tìm được i = 2 - a 2 := a 2-1 (a 2 = 3) - ta có k-i+1 div a 2 = 1 và k-i+1 mod a 2 = 0 => số phần tử tiếp theo là 1 đó là a 3 = a 2 (a 3 = 3) => Cấu hình tiếp theo là: 4 3 3 *Từ cấu hình 4 3 3 (k=3) - Ta tìm được i =3 - a 3 := a 3-1 (a 3 = 2) - Ta có k-i+1 div a 3 = 0 và k-i+1 mod a 2 = 1 => số phần tử tiếp theo là 1 đó là a 4 = 1 => Cấu hình tiếp theo là: 4 3 2 1 Chương trình của bài toán là: Program phan_tich; Var c: array[1..1000] of integer; k, n: integer; count: longint; Stop: Boolean; Procedure init; Var i,j:integer; Write('Moi nhap so nguyen duong N '); Readln(N); k := 1; c[k] := n; Count := 0; Stop := False; Procedure Result; Var i: integer; inc(count); Write(count,'. '); For i := 1 to k do write(c[i]:3); writeln;
Procedure Next_Division; Var i, j, r, s, d : integer; i := k; while (i>0) and (c[i] =1) do dec(i); if i>0 then c[i] := c[i]-1; d := k-i+1; r := d div c[i]; s := d mod c[i]; k := i; if r>0 then for j := i+1 to i+r do c[j]:=c[i]; k := k+r; if s>0 then k := k+1; c[k] := s; end else stop := True; Procedure Division; Var i: integer; While not stop do result; next_division; Write('Bam Enter de ket thuc '); Readln BEGIN init; division; END.
Như vậy, với mỗi bài toán như trên, khi sử dụng phương pháp sinh vào giải quyết chúng ta thường gặp khó khăn như sau: một là việc xác định cấu hình đầu tiên và cấu hình cuối cùng, hai là việc xác định được quy tắc để từ cấu hình đang có ta tìm ra được cấu hình tiếp theo (sinh ra cấu hình tiếp theo). Chính vì nguyên nhân đó mà phương pháp sinh không được sử dụng rộng rãi vào giải quyết các bài toán như thuật toán quay lui, quy hoạch động, Phương pháp sinh chỉ được sử dụng vào giải quyết một số bài toán liệt kê các cấu hình tổ hợp đơn giản. Hoàng Thị Minh Huyền