You are on page 1of 24

HỘI CÁC TRƯỜNG CHUYÊN DUYÊN HẢI BẮC BỘ

CÁC BÀI TOÁN VỀ ĐẾM CẤU HÌNH TẬP HỢP

-1-
ĐẶT VẤN ĐỀ

Xét bài toán sau:


Cho tập hợp S các dãy (x 1 , x 2 ,… , x p ) được sắp xếp theo thứ tự từ điển và các phần tử
được đánh số theo thứ tự này bằng các số nguyên dương 1, 2, .... Hãy:
1. Tính ¿ S∨¿ là lực lượng của tập S
2. Cho một số nguyên n(1≤ n ≤|S|) hãy tìm phần tử đứng thứ n trong thứ tự từ điển.
3. Cho một phần tử (x 1 , x 2 ,… x p ) hãy tìm thứ tự của nó trong tập S khi xếp tập này theo
thứ tự từ điển.
Ở đây việc so sánh các phần tử theo thứ tự từ điển là qui tắc: Giả sử có hai phần tử
X =(x 1 , x 2 , … , x p ) và Y =( y 1 , y 2 , … , y q ). Khi đó ta nói X <Y nếu một trong hai tình huống
dưới đây xảy ra:
1. ∃i :1≤ i≤ min ⁡(p , q) sao cho x u= y u ∀ u <i và x i < y i
2. x i= y i ∀ i=1 ÷ p và p<q

Bài toán trên là bài toán điển hình thường rất hay được sử dụng hoặc như là một dạng
bài tập riêng hoặc như là một phần trong một bài tập lớn hơn (chẳng hạn như việc
đánh số thứ tự các đỉnh trên một đồ thị lớn...). Để minh họa, ta xét hai ví dụ

Ví dụ 1(Đề thi IOI - Olympic Tin học Quốc tế)


Cho một rubic 2 chiều
a1 a2 a3 a4
a5 a6 a7 a8
Trong đó (a 1 , a 2 , a3 , a 4 ,a 5 , a 6 ,a 7 ,a 8) là một hoán vị của (1 , 2, 3 , 4 ,5 , 6 , 7 , 8) với các phép
quay:
1) Tịnh tiến các cột sang trái:
a4 a1 a2 a3
a8 a5 a6 a7
2) Đổi chỗ hàng trên và hàng dưới:
a5 a6 a7 a8
a1 a2 a3 a4
3) Xoay nhóm 4 ô giữa ngược chiều kim đồng hồ
a1 a3 a7 a4
a5 a2 a6 a8
Yêu cầu: Hãy thực hiện các phép biến đổi để đưa rubic 2 chiều trên về trạng thái:
1 2 3 4
5 6 7 8

Để giải quyết bài toán trên ta phải xây dựng một mô hình đồ thị trong đó mỗi đỉnh là
một hoán vị của (1, 2, 3, 4, 5, 6, 7, 8) và các cung là các phép biến đổi và bài toán qui

-2-
về tìm một đường đi từ đỉnh xuất phát đến đỉnh (1, 2, 3, 4, 5, 6, 7, 8). Một trong
những yêu cầu chính để làm điều này là cần phải xây dựng hai hàm: hàm thứ nhất
cho phép từ một hoán vị tương ứng với một số nguyên trong khoảng [1, 8!=40320] và
hàm thứ hai cho phép từ một số nguyên trong khoảng trên tìm hoán vị. Đây chính là
hai bài toán cơ bản của đếm cấu hình tập hợp (trong trường hợp này tập hợp là tập
hợp các hoán vị của 8 số tự nhiên đầu tiên)

Ví dụ 2 (VOI - Thi HSG Tin học Quốc Gia)


Cho một xâu s gồm n ký tự khác nhau từng đôi một (5 ≤ n ≤52) chỉ gồm các chữ cái
trong bảng chữ cái tiếng Anh (in thường và in hoa). Một nhà máy sản xuất khóa
quyết định sản xuất các khóa chữ với mã khóa là một xâu ký tự nhận được từ S khi
xóa không quá n−m ký tự khỏi S và giữ nguyên thứ tự các ký tự còn lại. Mỗi mã khóa
được đánh số thứ tự theo thứ tự từ điển. Hỏi:
1. Nhà máy sản xuất được bao nhiêu khóa khác nhau?
2. Cho biết một thứ tự, tìm mã khóa tương ứng.
3. Cho biết một mã khóa, tìm thứ tự tương ứng.

Dễ thấy đây là phiên bản của bài toán tổng quát phát biểu ở trên với S - tập hợp các
dãy (x 1 , x 2 ,… , x p ) với 1 ≤ x i ≤n , x i ≠ x j , m≤ p ≤n và phép so sánh x i < x j khi và chỉ khi
s [ x i ]< s [x j ]

Một điều đáng tiếc, trong các tài liệu dành cho học sinh chuyên tin thì bài toán phát
biểu ở trên thường được trình bày sơ lược cùng với chuyên đề qui hoạch động hoặc
chuyên đề về số học. Chính vì vậy nên các em học sinh thường gặp nhiều lúng túng
khi xử lý các bài tập có nội dung là bài toán trên.

Từ kinh nghiệm thực tế giảng dạy Tin học cho các học sinh chuyên tin và các em
trong các đội tuyển học sinh giỏi Tỉnh, Quốc gia ở nhiều địa phương khác nhau, tôi
thấy rằng cần phải tập trung các bài tập về cấu hình tổ hợp thành một chuyên đề, xây
dựng hệ thống lý luận về phương pháp, hệ thống các bài tập cơ sở và các bài tập áp
dụng để qua đó hình thành phong cách tư duy về đếm cấu hình tổ hợp cho học sinh,
đặc biệt là các học sinh chuẩn bị tham gia các kỳ thi học sinh giỏi cấp Tỉnh, Quốc gia
và Quốc tế.

Trong ba yêu cầu về đếm trong bài toán phát biểu ở trên thì yêu cầu 1 đơn thuần là
một bài toán qui hoạch động điển hình về đếm; các yêu cầu 2-3 đòi hỏi một kỹ năng
phân tích cấu trúc việc sắp xếp của các thành phần theo lớp. Có nhiều cách để làm
điều này và ở đây chúng ta chỉ xem xét một số cách điển hình. Cần lưu ý rằng do sự
"bùng nổ tổ hợp" nên số lượng các cấu hình tăng lên rất nhanh (thường tỷ lệ với a n
hoặc n !). Do đó các kiểu số nguyên mặc định trong Pascal hoặc C++ không sử dụng

-3-
được trong trường hợp này. Để giải quyết trọn vẹn ta cần phải xây dựng các kiểu số
nguyên riêng (phụ lục A)

Có thể thấy, trong 3 yêu cầu của bài toán tổng quát thì yêu cầu đầu tiên - đếm số
lượng phần tử của tập hợp là dễ thực hiện hơn cả; các yêu cầu 2-3 đòi hỏi học sinh
phải có tư duy tương đối sắc bén. Do vậy, về mặt phương pháp - theo tôi - khi giảng
dạy nên chia thành hai giai đoạn: giai đoạn cơ bản - chỉ làm yêu cầu 1 và để giải
quyết "bùng nổ tổ hợp" thì thay vì lập một kiểu số nguyên riêng chúng ta có thể yêu
cầu chỉ tính kết quả trong một trường đồng dư nào đó (các phép tính trong trường
đồng dư cũng được trình bày ở đây dưới dạng phụ lục B).

Theo kinh nghiệm của tôi, với yêu cầu thứ nhất của bài toán tổng quát có thể giảng
dạy như là phần kiến thức cơ sở cho tất cả các học sinh (lớp chuyên), và với hai yêu
cầu còn lại có thể tổ chức thành các nhóm báo cáo riêng (điều này cho phép các em
học sinh khá-giỏi có điều kiện hỗ trợ các em yếu hơn dưới sự hướng dẫn của thầy,
kéo theo việc tiếp thu kiến thức của các em này hiệu quả hơn).

Các bản phác thảo đầu tiên của chuyên đề này được hình thành và áp dụng giảng dạy
lần đầu cho các em học sinh tham gia thi HSG Quốc gia của tỉnh trong năm học
2014-2015, sau đó được tổng hợp lại và trở thành chuyên đề chính thức giảng dạy
cho các lớp chuyên tin từ năm học 2015-2016. Chuyên đề này đã được báo cáo trong
các hội thảo của giáo viên Tin học các trường THPT Chuyên trên toàn quốc dưới
dạng các thảo luận ngắn và các báo cáo xê mi na.

Tất nhiên, chuyên đề này chỉ được triển khai dạy và học khi có một số điều kiện tiền
đề nhất định: học sinh phải được trang bị các kiến thức qui hoạch động cơ bản, các
kiến thức số học nền (số học trong trường đồng dư, số nguyên lớn...).

Điều làm cho chuyên đề có tính đặc trưng so với các tài liệu trước đó, theo tôi, nằm ở
việc trang bị cho học sinh một cách nhìn tổng quát về các trường hợp khác nhau của
việc đếm. Điều này giúp các em hình thành nên một thói quen tư duy hiệu quả khi
giải quyết các bài toán về cấu hình tập hợp.

Chuyên đề này không phải là một chuyên đề có thể áp dụng đại trà trên qui mô rộng
để giảng dạy cho tất cả học sinh phổ thông. Tuy vậy, theo tôi, nó cũng là một gợi ý
tốt cho các thầy cô khi cần thiết phải xây dựng các chủ đề chuyên sâu cho học sinh
làm việc.

-4-
TÓM TẮT LÝ THUYẾT

1. Các bài toán cơ bản hình thành phương pháp phân tích cấu hình tập hợp

Trong mục này, tôi sẽ trình bày các bài toán - theo tôi - là các bài toán cơ bản hình
thành lên các phương pháp khác nhau để phân tích cấu hình tập hợp. Nó đại diện cho
các phương pháp tư duy khác nhau trên cùng một bài toán tổng quát (tôi đã phát biểu
ở phần đầu khi tóm tắt sáng kiến). Một điều đáng lưu ý là lời giải ở đây không phải là
lời giải duy nhất (hay tốt nhất) cho bài toán đưa ra. Lưu ý này là rất quan trọng khi áp
dụng bởi vì yếu tố chính cần ghi nhớ là một cách thức giải quyết bài toán đếm thông
qua lời giải minh họa.

1. 1 Phương pháp sử dụng cây đếm

Bài toán 1:
(https://drive.google.com/file/d/1_a1viPjBE7Iu7axGX52dK2yweRwQ_-gw/view?usp=sharing)
Xét tập S các hoán vị của (1,2,...,n) được xếp theo thứ tự từ điển
1. Đếm số lượng phần tử của S
2. Cho một số thứ tự, hãy tìm hoán vị tương ứng với thứ tự đó
3. Cho một hoán vị, hãy tìm thứ tự của nó trong từ điển.
Ví dụ với n=6 ta có 6 hoán vị:
1. 1 2 3
2. 1 2 4
3. 2 1 3
4. 2 3 1
5. 3 1 2
6. 3 2 1
Câu trả lời cho 1) là 6, nếu cho số thứ tự 4 thì câu trả lời cho 2) là 2 3 1 và nếu cho
hoán vị là 2 1 3 câu trả lời cho 3) là 3.

Ở đây ta không xem xét lời giải bằng cách liệt kê tất cả các hoán vị theo thứ tự từ
điển vì với độ phức tạp O(2n ) thì thuật toán này không hiệu quả khi giá trị n lớn (do sự
"bùng nổ tổ hợp")

Để giải quyết câu hỏi 1) ta sử dụng phương pháp qui hoạch động: Đặt f [n] là số
lượng hoán vị tìm được với kích thước n. Giả sử mỗi hoán vị được liệt kê bởi dãy
(x 1 , x 2 ,… , x n ). Khi đó x 1 có thể nhận các giá trị 1 , 2, … , n (n trường hợp khác nhau); khi
cố định x 1 thì (x 2 ,… , x n ) chỉ nhận các giá trị khác nhau trong số n−1 giá trị còn lại (bỏ
đi x 1 ¿ cho nên số lượng các hoán vị sẽ là f [n−1]. Vậy nên:
f [ n ] =n ∙ f [n−1]
Trường hợp cơ bản f [ 0 ] =1

-5-
Đoạn code C++ dưới dây cho phép tìm số lượng hoán vị trong O(n):

f[0]=1;
for(int i=1;i<=n;++i) f[i]=i*f[i-1];

Để giải quyết các câu hỏi 2) và 3) ta xây dựng "cây đếm'. Ví dụ khi n=3 "cây đếm"
của chúng ta có dạng

6
1
2 3

2 2 2
2 3 1 3 1 2

1 1 1 1 1 1

3 2 3 1 2 1

1 1 1 1 1 1

Cây đếm ở trên được chia thành n lớp cạnh, mỗi lớp cạnh tương ứng với một phần tử
của hoán vị từ 1 đến n (chữ bên ở bên cạnh là giá trị của phần tử). Mỗi hoán vị sẽ
tương ứng với một đường đi từ gốc đến lá, cây được thiết kế sao cho đường đi ứng
với hoán vị nhỏ hơn sẽ ở "bên trái" đường đi ứng với hoán vị lớn hơn. Giá trị ở mỗi
nút của cây là số lượng lá trong cây con với nút đó (cũng là số lượng đường đi qua
nút này).

Nếu như có được cây đếm trên việc tìm thứ tự của mỗi hoán vị sẽ thực hiện bằng
cách đếm số đường đi nằm bên trái của nó cộng thêm 1. Việc đếm số đương đi nằm
bên trái sẽ được thực hiện bằng cách đi từ lớp trên xuống lớp dưới, bỏ qua số nhánh
bên trái. Tương tự ta cũng có thể giải bài toán ngược lại.

Tuy vậy, việc mô tả cây đếm một cách tường minh và áp dụng qui trình đếm trên cây
ở trên là không khả thi (vì số lượng nút sẽ rất lớn do sự "bùng nổ tổ hợp"). Nhưng
trên thực tế không cần phải như vậy. Nếu quan sát kỹ chúng ta thấy:
 Giá trị ở nút gốc (lớp 0) là f [n]
 Giá trị ở nút lớp thứ 1 là f [n−1]
 Giá trị ở lớp thứ hai là f [n−2],...

Ngoài ra số đường đi bên trái khi đến một lớp chính bằng số lượng các phần tử chưa
xuất hiện nhỏ hơn giá trị của phần tử đang xét.

-6-
Với các nhận xét trên, bằng cách sử dụng thêm mảng nho[...] để đánh dấu các giá trị
đã xuất hiện (1-xuất hiện, 0-chưa xuất hiện), ta có đoạn code tính thứ tự của hoán vị
(x 1 , x 2 ,… , x n ) như sau (code bằng C++):

for(int i=1;i<=n;++i) nho[i]=0;


res=1;
for(int i=1;i<=n;++i) {
for(int j=1;j<x[i];++j)
if (nho[j]==0) res +=f[n-i]; // bỏ qua nhánh có f[n-i] đường đi
nho[x[i]]=1;
}
Ngược lại khi có một số thứ tự p, ta cũng xuất phát từ gốc và chừng nào p lớn hơn số
đường đi qua nhánh thì bỏ qua thêm một nhánh. Điều này cho phép ta tìm thấy nhánh
chứa giá trị x[i] là nhánh thứ bao nhiêu. Qua đó tìm được giá trị này:
for(int i=1;i<=n;++i) nho[i]=0;
for(int i=1;i<=n;++i) {
int d=0; // d là số nhánh bỏ qua
while (p>f[n-i]) p -= f[n-i], ++d;
++d; // d bây giờ là thứ tự x[i];
// đoạn code tìm x[i]
x[i]=0;
while (d>0) {
do ++x[i]; while (nho[x[i]]==1);
--d;

}
}
Như vậy có thể thây việc mô tả cây đếm là một hình thức để tự duy thuật toán. Trên
thực tế chúng ta không bao giờ mô tả tường minh cây đếm này mà thay vào đó tính
toán số liệu dựa trên các bảng qui hoạch động bằng cách phân tích bản chất của công
thức qui hoạch động. Ở trong ví dụ trên công thức qui hoạch động thực chất là:
f [ i ] =f⏟
[ i−1 ] + f [ i−1 ] +…+ f [i−1]
i giá trị

(để thấy đã bỏ qua i−1 nhánh)


Để minh họa chi tiết hơn ta xét bài toán tiếp theo:
Bài toán 2:
(https://drive.google.com/file/d/1Hi7kdWKEZxyQss72BLniB4FnrL4-YpTJ/view?usp=sharing)
Cho S là dãy các tổ hợp chập k của {1,2,...n} được xếp theo thứ tự từ điển. Ở đây mỗi
tổ hợp được mô tả bằng dãy ( x 1 , x 2 , … , x k ) với 1 ≤ x 1 < x 2< …< x k ≤n . Hãy:
1. Tính ¿ S∨¿ - số lượng phần từ của S
2. Cho một thứ tự, tìm tổ hợp chập k tương ứng
3. Cho một tổ hợp chập k , tìm thứ tự của nó trong từ điển.

-7-
Ví dụ với n=5 , k=3 các tổ hợp được liệt kê 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
a) Giải quyết câu hỏi 1)
Đặt f [n , k ] là số lượng các tổ hợp chập k ( x 1 , x 2 , … , x k ) :1 ≤ x 1 <…< x k ≤ n. Hai trường hợp
xảy ra:
TH1: x 1 nhận giá trị nhỏ nhất (1): Khi đó (x 2 , x 3 , … , x k ) chính là một tổ hợp chập k −1
của (2 , … , n). Do đó số lượng tổ hợp trong trường hợp này là f [n−1 , k−1]
TH2: x 1 không nhận giá trị nhỏ nhất (1). Khi đó (x ¿ ¿ 1 , x 2 , … , x k )¿ là số lượng tổ hợp
chập k của (2 , … , n). Số lượng tổ hợp trong trường hợp này là f [n−1 , k ]. Tóm lại ta có
công thức qui hoạch động:
f [ n , k ] =f⏟
[ n−1 , k −1 ] + ⏟
f [ n , k −1 ]
x 1 là nhỏ nhất x 1 không nhỏ nhất

Một điều thú vị là qua việc phân tích công thức qui hoạch động trên ta có ngay
f [n−1 , k−1] cấu hình ứng với x 1 nhỏ nhất luôn đứng trước f [n , k−1] cấu hình với x 1
không phải là nhỏ nhất.
(Chú ý công thức tìm được ở trên chính là công thức Pascal).
Với n=5 , k=3 bảng qui hoạch động có thể mô tả:
0 1 2 3
0 1
1 1 1
2 1 2 1
3 1 3 3 1
4 1 4 6 4
5 1 5 10 10
Đoạn code dưới đây (C++) mô tả việc xây dựng bảng trên:

f[0][0]=1;
for(int i=1;i<=n;++i) {
f[i][0]=1, f[i][i]=1;
for(int j=1;j<i;++j) f[i][j]=f[i-1][j-1]+f[i-1][j];
}

-8-
b) Giải quyết các câu hỏi 2-3)

Để giải quyết câu hỏi 2, 3 ta phải xây dựng cây đếm. Chú ý rằng ta không xây dựng
tường minh cây đếm mà chỉ hình dung nó. Trong trường hợp này với một nút mô tả
bằng cặp (n , k ) nhận xét rằng số lượng nhánh có giá trị phần tử đầu tiên nhỏ nhất luôn
là f [n−1 , k−1] và trong thứ tự từ điển các nhánh này luôn được xêp trước các nhánh
còn lại (có số lượng là f [n−1 , k ]).

Ví dụ với n=5 , k=3 (hình trên) và ta cần tìm tổ hợp có thứ tự p=8.
+) Xuất phát từ ô (5,3) ta thấy số lượng nhánh ứng với giá trị nhỏ nhất là f[4,2]=6,
thứ tự cần tìm p=8 nên tổ hợp này chắc chắn không phải có phần tử x[1] nhỏ nhất (1)
và ta đi lên ô (4,3) với thứ tự mới p=8-6=2. Chú ý giá trị nhỏ nhất bây giờ là 2
+) Tại ô (4,3) có f[3,2]=3 nhánh có giá trị nhỏ nhất, thứ tự cần tìm p=2≤3 nên ta chọn
nhánh nhỏ nhất này và đi đến ô (3,2) ở đây ta kết luận x[1]=2. Chú ý rằng sau bước
này giá trị nhỏ nhất của các phần tử còn lại là 3. Thứ tự cần tìm vẫn là p=2
+) Tại ô (3,2) có f[2,1]=2 nhánh có giá trị nhỏ nhất, thứ tự cần tìm p=2≤2 nên ta đi
theo nhánh nhỏ nhất đi đến ô (2,1). Lúc này ta kết luận x[2]=3,thứ tự cần tìm vẫn là
p=2 và giá trị nhỏ nhất trong các phần tử còn lại là 4
+) Ở ô (2,1) ta có số nhánh nhỏ nhất là f[1,0]=1<p=2 nên ta bỏ qua nhánh nhỏ nhất
này đi đến ô (1,1), thứ tự mới p=2-1=1. Chú ý rằng giá trị nhỏ nhất của các phần tử
còn lại bây giờ là 5
+) Tại ô (1,0) ta có số nhánh nhỏ nhất f[0,0]=1≥p=1 nên ta tiếp tục đi theo nhánh nhỏ
nhất, kết luận x[3]=5 đi đến ô (0,0).
Như vậy cấu hình cần tìm là (2, 3, 5). Đường đi mũi tên ở hình vẽ trên mô tả quá
trình này.

Đoạn code dưới đây (C++) minh họa ý tưởng trên:

int u=n, v=k;


int d=1; // Giá trị nhỏ nhất của các phần tử còn lại
for(int i=1;i<=k;++i) {
while (f[u-1][v-1]>p) p -= f[u-1][v-1], --u, ++d;
x[i]=d, ++d, --u, --v;
}

Bài toán ngược lại cũng làm tương tự nhưng chú ý rằng chừng nào giá trị x[..] tại vị
trí tương ứng không phải là nhỏ nhất thì ta luôn bỏ qua các nhánh nhỏ nhất và cộng
thêm vào thứ tự.

Đoạn code dưới đây (C++) mô tả điều trên:


p=1;
int u=n, v=k, d=1;

-9-
for(int i=1;i<=n;++i) {
while (x[i]>d) p += f[u-1][v-1], --u, ++d;
--u, --v;
}

Có thể thấy cây đếm là công cụ hữu hiệu để xây dựng các thuật toán tìm cấu hình.
Điều quan trọng nhất khi xây dựng các giải thuật theo phương pháp này là giải quyết
được câu hỏi 1 và điều đặc biệt là phân tích được cấu trúc của công thức qui hoạch
động để qua đó hình dung được cấu trúc của cây đếm. Các đoạn code mô phỏng ở
trên là sự cải tiến của kỹ thuật đếm trên cây tường minh.

1.2 Phương pháp "đường đi trên lưới"

Trong toán học phương pháp "đường đi trên lưới" là phương pháp cổ điển để đếm số
cấu hình của tập hợp. Ý tưởng chính của phương pháp này gồm hai giai đoạn:
 Giai đoạn đầu là xây dựng ánh xạ 1-1 tương ứng mỗi phần tử của tập S với một
dãy nhị phân
 Giai đoạn tiếp theo là xây dựng tương ứng 1-1 giữa dãy nhị phân với một
đường đi trên mặt phẳng tọa độ, đường đi này xuất phát từ (0,0), khi gặp số 1
sẽ đi lên trên và khi gặp số 0 sẽ đi sang phải. Tập hợp các dãy nhị phân bây giờ
sẽ tương ứng với tập các đường đi trên lưới ô vuông (lưới xây dựng bằng các
đường ngang, dọc song song với các trục tọa độ và đi qua các điểm nguyên
trên mặt phẳng). Bài toán đếm cấu hình bây giờ trở thành bài toán đếm đường
đi trên lưới.
Để có thể giải quyết được việc tìm thứ tự hoặc xây dựng cấu hình thì một điều quan
trọng là các dãy nhị phân phải được sắp xếp (tăng dần hoặc giảm dần) theo thứ tự từ
điển của các phần tử trong tập S ban đầu. Ta xét bài toán dưới đây:

Bài toán 3: (Dãy Catalan)


(https://drive.google.com/file/d/1YePXCswhuWAHLVplZkhO87i-wYXQP16u/view?usp=sharing)
Ta định nghĩa một dãy Catalan cấp n là dãy x 0 , x 1 , … , x 2 n thỏa mãn:
1. x 0=x 2 n=0
2. x i ≥ 0 ∀ i=0 ÷ 2n
3. |x i−x i+1|=1 ∀ i=0 ,1 , … , 2n−1
Yêu cầu:
1. Tính ¿ S∨¿ với S là tập các dãy Catalan cấp n được xếp từ điển
2. Cho một số thứ tự, tìm dãy Catalan cấp nứng với thứ tự này
3. Cho một dãy Catalan cấp n tìm thứ tự của nó
Ví dụ với n=3 ta có các dãy Catalan là:
1. 0 1 0 1 0 1 0
2. 0 1 0 1 2 1 0
-10-
3. 0 1 2 1 0 1 0
4. 0 1 2 1 2 1 0
5. 0 1 2 3 2 1 0

Để giải quyết bài toán theo phương pháp vừa mô tả đầu tiên ta tương ứng mỗi dãy
Catalan ( x 0 , x 1 , … , x 2 n ) với dãy nhị phân (b 1 , b2 , … , b2 n ) theo công thức:
b i=
{0 nếu x i−x i−1=−1
1 nếu x i−x i−1=1

Dễ thấy tương ứng này là 1-1 và ngoài ra tập các dãy nhị phân được sắp xếp tăng dần
theo thứ tự từ điển. Chẳng hạn với n=3 ta có:
1. 0 1 0 1 0 1 0 ↔ 101010
2. 0 1 0 1 2 1 0 ↔ 101100
3. 0 1 2 1 0 1 0 ↔ 110010
4. 0 1 2 1 2 1 0 ↔ 110100
5. 0 1 2 3 2 1 0 ↔ 111000

Tiếp theo ta ứng mỗi dãy nhị phân với một đường đi trên lưới xuất phát từ (0,0), đi
lên nếu gặp số 1 và đi sang phải nếu gặp số 0:

C
B
1 1 1 1

3 2 1

5 2

A
5

Nhận xét rằng các đường đi luôn xuất phát từ A(0,0) và kết thúc tại B(3,3) và luôn
nằm trong tam giác ABC.

Đặt f [i , j] là số cách khác nhau để đi đến B(n , n) nếu như xuất phát từ tọa độ (i , j) và
đường đi luôn nằm trong tam giác ABC với A(0 ,0), C (0 , n) ta có công thức:
f [ i , n ] =1 ∀ i=0 ,1 , … , n
f [ i ,i ] =f [ i+1 , i ] ∀ i=0 ,1 , 2 , … , n−1
f [ i , j ] =f [ i+1 , j ] + f [ i , j+1 ] ∀ i=1 , 2 , … ,n−1 , j=0 , … , i

Và số lượng dãy Catalan cho bởi f [0 , 0].

-11-
Ngoài ra với một dãy nhị phân thì bằng cách xuất phát từ (0,0) đi lên trên nếu gặp 1
(khi đó số thứ tự sẽ được cộng thêm những nhánh bên phải do số 0 đứng trước số 1)
và đi sang ngang nếu gặp 0 (số thứ tự không đổi) ta có thể tìm thứ tự dãy nhị phân
trong từ điển.

Với tư duy tương tự khi cho một thứ tự ta cũng tìm được dãy nhị phân tương ứng.
Từ dãy nhị phân xây dựng dãy Catalan là công việc đơn giản.

Minh họa các nhận xét trên qua các đoạn code (C++) dưới đây:

a) Đoạn code xây dựng bảng


for(int j=0;j<=n;++j) f[n][j]=1;
for(int i=n-1;i>=0;--i) {
f[i][i]=f[i+1][i];
for(int j=i-1;j>=0;--j) f[i][j]=f[i+1][j]+f[i][j+1];
}

b) Cho dãy nhị phân b 1 … b 2n tìm số thứ tự

int u=0, v=0;


p=1;
for(int i=1;i<=2*n;++i) {
if (b[i]==1) p += f[u][v+1], ++u; else ++v;
}

c) Cho số p tìm dãy nhị phân có thứ tự p

int u=0, v=0;


for(int i=1;i<=2*n;++i) {
if (p>f[u][v+1]) b[i]=1, p -= f[u][v+1], ++u;
else b[i]=0, ++v;
}

1.3 Phương pháp "Đếm số cấu hình phía trước"

Có một lớp các bài toán số học liên quan đến việc sắp xếp từ điển và đếm cấu hình.
Những bài toán này có một dngj chung phát biểu như sau:

Giả sử S là tập hợp các số nguyên dương thỏa mãn tính chất F và được sắp xếp tăng
dần. Hãy:
1. Tìm số lượng các phần tử của S trong đoạn [L, R]
2. Tìm số đứng thứ k của S
3. Cho một số x ∈ S hỏi x đứng thứ mấy trong S.

-12-
Do độ dài của các số nguyên là khác nhau nên việc sắp xếp đơn thuần các số nguyên
theo thứ tự từ điển sẽ không làm cho các số nguyên này sắp xếp tăng dần. Ví dụ, các
số tự nhiên từ 1 đến 10 nếu sắp xếp theo thứ tự từ điển sẽ là:
1, 10 2, 3, 4, 5, 6, 7, 8, 9
Tuy vậy,bằng cách thêm các số 0 vào bên trái ta có thể làm cho tất cả các số nguyên
đều có độ dài bằng nhau. Khi đó việc sắp xếp các số nguyên tăng dần cũng đồng nhất
với việc sắp xếp các số nguyên theo thứ tự từ điển. Chẳng hạn, với các số nguyên từ 1
đến 10 nếu thêm các số 0 vào bên trái t có sắp xếp từ điển:
01, 02, 03 , 04 , 05 , 06 , 07 , 08, 09, 10
Do đó không mất tổng quát ta luôn coi độ dài các số nguyên là không đổi và bằng m
(thường đây là độ dài của số nguyên lớn nhất có thể có trong tập S).

Do tập số nguyên là tập đặc biệt, nên bài toán cấu hình tổ hợp trên tập này có một
phương pháp đặc trưng để giải quyết. Cụ thể:
Trước tiên ta xây dựng hàm f (x) cho số lượng các số nguyên thuộc S trong đoạn [1, x ]
. Khi đó:
+) Số lượng các số nguyên trong đoạn [ L, R] cho bởi công thức f ( R )−f ( L−1)
+) Để tìm số đứng thứ k ta chú ý rằng hàm f ( x )là hàm đơn điệu không giảm. Do vậy
có thể sử dụng kỹ thuật tìm kiếm nhị phân để tìm:
int Rank(int k) {
int lo=0, hi=∞ ;
while (hi-lo>1) {
int mid=(lo+hi)/2;
if (f(mid)<k) lo=mid; else hi=mid;
}
return hi;
}
+) Với số x ∈ S thì thứ tự của nó đơn giản là f (x).

Do vậy bài toán về đếm cấu hình được giải quyết hoàn toàn nếu như xây dựng được
hàm f ( x ). Việc xây dựng hàm này dựa trên phân tích cấu tạo số và qui hoạch động.
Xét bài toán sau:

Bài toán 4:(COCI -2007)


(https://drive.google.com/file/d/1L1ihPDRtbABMkLJ1604QuZqXmRtbDQpU/view?usp=sharing)
Cho số nguyên dương S hỏi rằng có bao nhiêu số nguyên dương trong đoạn [ A , B] có
tổng các chữ số bằng S. Tìm số nhỏ nhất thỏa mãn điều kiện trên?
Giới hạn (1 ≤ A ≤ B≤ 1015 ; 1≤ S ≤135)

Gọi tập các số nguyên dương có tổng chữ số bằng S là T . Ta cần phải xây dựng được
hàm f (x) cho biết số lượng các phần tử thuộc T nhỏ hơn hoặc bằng x . Khi đó câu trả

-13-
lời đầu tiên của bài toán là f ( B )−f ( A−1). Để trả lời câu hỏi thứ hai ta cần tìm số nhỏ
nhất x ∈[ A , B ] có f ( x ) > f ( A−1). Việc này có thể thực hiện bằng tìm kiếm nhị phân.

Như đã trình bày, ta luôn coi các số nguyên có dạng x=x m x m−1 … x 1 x 0 (ở đây có thể lấy
m=15 ). Đặt dp [i , s ] là số lượng các số nguyên nhỏ hơn hoặc bằng x i x i−1 … x 1 x 0. và có
tổng các chữ số bằng s. Tất nhiên, nếu tìm được mảng trên thì kết quả sẽ là dp [m, S ].
Ta có các trường hợp sau:
+) Chữ số đầu tiên của số cần tìm là u< x i. Khi đó tổng các chữ số còn lại sẽ là s−u và
các số này chắc chắn sẽ nhỏ hơn x i x i−1 … x 1 x 0. Đặt dp 1[i , s] là số lượng các số có i+1
chữ số và có tổng các chữ số bằng s. Ta có công thức số lượng trong trường hợp này
xi −1

sẽ là ∑ dp 1[i−1 , s−u]
u=0

+) Chữ số đầu tiên chính bằng x i. Khi đó tổng các chữ số còn lại sẽ là s− xi và số
lượng trong trường hợp này là dp [i−1, , s−x i ].

Tóm lại ta có công thức qui hoạch động:


x i −1
dp [ i , s ] =dp [ i−1 , s−x i ]+ ∑ dp 1[i−1 , s−u]
u =0
9
dp 1 [ i, s ] =∑ dp 1[i−1 , s−u ]
u=0

Ở trường hợp tới hạn (chỉ có 1 chữ số):


dp 1 [ 0 , s ] =
{
1 nếu s ≤9
0 nếu s >9 {
dp [ 0 , s ]= 1 nếu s ≤ x 0
0 nếu s> x 0

Ta có hàm tính số lượng số có tổng bằng S và nhỏ hơn hoặc bằng x (C++):

int calc(int x) {
for(int i=0;i<=m;++i) c[i]=x%10, x/=10;
for(int s=0;s<=maxS;++s) dp[0][s]=(s<=c[0]) ? 1 : 0;
for(int i=1;i<=m;++i)
for(int s=0;s<=maxS;++s) {
dp[i][s]=(s>=c[i]) ? dp[i-1][s-c[i]] : 0;
for(int u=0;u<c[i];++u) if (u<=s)
dp[i][s] += dp1[i-1][s-u];
}
return dp[m][S];
}

Ở đây mảng dp 1 được xây dựng một lần ban đầu:

for(int s=0;s<=maxS;++s)
dp1[0][s]=(s<=9) ? 1 : 0;

-14-
for(int i=1;i<=m;++i)
for(int s=0;s<=maxS;++s) {
dp1[i][s]=0;
for(int u=0;u<=9;++u) if (u<=s)
dp1[i][s] += dp1[i-1][s-u];
}

1.4 Tổng kết

Có ba phương pháp cơ bản để xử lý các bài toán phân tích cấu hình tập hợp. Phương
pháp đầu tiên là phương pháp phổ biến, nói chung có thể áp dụng cho hầu hết các
trường hợp. Điều quan trọng là phải hình dung được cấu trúc của cây đếm trên các
bảng qui hoạch động. Việc này chủ yếu dựa trên phân tích ý nghĩa của các công thức
qui hoạch động. Rèn luyện kỹ năng này cho học sinh là một vấn đề khó, đòi hỏi sự
kiên trì của người thầy và đặc biệt là sự tích cực trong hoạt động tư duy của học sinh.

Phương pháp thứ hai không phải là phương pháp có thể áp dụng rộng rãi. Tuy nhiên
đây là phương pháp đơn giản nếu như cấu hình tập hợp phù hợp (dãy nhị phân sau
khi ánh xạ phải là dãy được sắp)

Phương pháp thứ ba là phương pháp đặc thù cho một lớp bài toán riêng cấu hình tập
hợp trên số nguyên. Thực tế ta có thể áp dụng phương pháp này cho cơ số bất kỳ chữ
không phải chỉ riêng cơ số 10. Do đặc thù trên tập số nguyên có trang bị các phép
tính số học cơ bản nên trong phương pháp này, việc giải quyết một câu hỏi sẽ dẫn đến
việc giải quyết các câu hỏi còn lại bằng cách sử dụng các chiến lược tìm kiếm đơn
giản.
Về mặt kiến thức là như vậy. Tuy nhiên khi giảng dạy, tùy thuộc vào từng đối tượng
học sinh, giáo viên cần xây dựng các hệ thống bài tập phù hợp.

2. Một số bài tập

Bài toán 5:Đếm số lượng các tập hợp con


(https://drive.google.com/file/d/1Spsr21DFGzoyKtAWQXiTMlu0OI9tKB9E/view?usp=sharing)
Cho tập hợp {1,2,...,n}. Người ta liệt kê các tập con (khác rỗng) của tập này theo thứ
tự từ điển (với mỗi tập con khác rỗng, các phần tử của nó được liệt kê tăng dần). Ví
dụ,
với n=3 ta có 7 tập con đánh số như sau:
1: 1 5: 2
2: 1 2 6: 2 3
3: 1 2 3 7: 3
4: 1 3

-15-
1. Đếm số lượng các tập hợp con
2. Cho một tập con, tìm số thứ tự của nó
3. Cho một số thứ tự , tìm tập con tương ứng.
Dữ liệu: Vào từ file văn bản TAPCON.INP:
 Dòng đầu ghi số N(N≤100)
 Tiếp theo là một số dòng, mỗi dòng có một trong hai dạng sau:
o 1 x1 x2 ... xk thể hiện một tập con, tìm số thứ tự tương ứng
o 2 P thể hiện một số thứ tự, yêu cầu tìm tập con tương ứng
 Kết quả: Ghi ra file văn bản TAPCON.OUT
 Dòng đầu ghi số lượng tập con tìm được
 Các dòng tiếp theo tương ứng với các câu trả lời đối với một dòng trong file dữ
liệu vào. Nếu là loại 1 ... thì cho ra số thứ tự tương ứng, nếu là loại 2 ... thì cho
ra tập con tương ứng.
Ví dụ:
TAPCON.INP TAPCON.OUT
3 7
1123 3
6 23

Thuật toán:
Mỗi tập con sẽ được mô tả bằng dãy (x 1 , x 2 ,… , x k ) trong đó x 1< …< x k .
Đặt f [n] là sô lượng tập con khác rỗng của tập n phần tử ta có f [ 1 ] =1. Ngoài ra:
+) Trường hợp 1: x 1 là phần tử nhỏ nhất, khi đó các phần tử còn lại
 Hoặc không có phần tử nào
 Hoặc là một tập con của tập n−1 còn lại
Do vậy trong trường hợp này số lượng sẽ bằng f [ n−1 ] + 1
+) Trường hợp 2: x 1 không phải là phần tử nhỏ nhất. Tất nhiên số lượng tập con trong
trường hợp này là f [n−1].
Ta có công thức :
f [ n ] =¿) + ⏟
f [n−1]
x1 không nhỏ nhất

Từ việc phân tích bản chất công thức qui hoạch động ở trên ta có thể dây dựng chiến
lược đếm trên cây đếm qua bảng qui hoạch động trên (không cần xây dựng tường
minh cây đếm) tương tự như bài toán 2

Bài toán 6: Hoán vị vòng tròn


(https://drive.google.com/file/d/1h9t8T-g4DJboxF3HMBunPGyKqIpk6a5v/view?usp=sharing)
Trong các hoán vị, hai hoán vị được coi là có quan hệ nếu hoán vị này có thể nhận
được từ hoán vị kia sau một số thao tác thuộc một trong ba loại sau:
 Dịch phải: ( x 1 , x 2 , .. . , x n ) → ( x n , x 1 , x 2 ,. .. . , x n−1 )
-16-
 Dịch trái: ( x 1 , x 2 , .. . , x n ) → ( x 2 , x 3 ,. . ., x n , x 1 )
 Đảo ngược: ( x 1 , x 2 , .. .. . , x n−1 , x n ) → ( x n , x n−1 ,. .. . , x 2 , x 1 )
Ví dụ, (1,2,3) và (2,1,3) là hai hoán vị có quan hệ do (2,1,3) có thể nhận được từ
(1,2,3) bằng cách:
- Dịch phải: ( 1, 2, 3 ) → ( 3 , 1,2 )
- Đảo ngược: ( 3, 1 ,2 ) → ( 2 , 1,3 )
Các hoán vi của tập {1,2,...,n} được liệt kê theo thứ tự từ điển, một hoán vị sẽ không
được liệt kê nếu trước nó có một hoán vị có quan hệ với nó đã được liệt kê. Số hiệu
của hoán vị là số thứ tự của hoán vị.
Hãy đếm số lượng hoán vị trong từ điển và cho trước một hoán vị, tìm số thứ tự của
hoán vị có quan hệ với nó trong từ điển trên
Dữ liệu: Vào từ file văn bản HVRING.INP:
 Dòng đầu tiên ghi sốnguyên dương N (N≤50)
 Dòng tiếp theo ghi một hoán vị của {1,...,n}
Kết quả: Ghi ra file văn bản HVRING.OUT:
 Dòng đầu tiên ghi tổng số hoán vị trong từ điển
 Dòng thứ hai ghi số thứ tự của hoán vị có quan hệ với hoán vị đã cho ở trong
từ điển
 Dòng thứ ba ghi hoán vị có quan hệ với hoán vị đã cho trong từ điển
Ví dụ:
HVRING.INP HVRING.OUT
5 12
1 5 3 2 4 11
1 4 2 3 5

Thuật toán:
Trước tiên chúng ta chia tập các hoán vị thành các lớp, mỗi lớp là nhóm các hoán vị
có quan hệ với nhau. Theo điều kiện của đề bài, mỗi lớp sẽ chỉ có một hoán vị nhỏ
nhất có trong từ điển. Vậy nên, việc đầu tiên là cần tì hoán vị có thứ tự nhỏ nhất trong
một lớp. Điều này có thể làm dựa trên các chú ý sau:
+) Đầu tiên, do có phép dịch chuyển lên số 1 luôn có thể được đưa về vị trí đầu tiên
trong lớp. Do đó với hoán vị nhỏ nhất trong một lớp, phần tử đầu tiên của nó luôn
bằng 1 ( x 1=1 ¿
+) Tiếp theo, do có các phép đảo ngược nên với hoán vị nhỏ nhất ta luôn có x 2< x n (vì
nếu không chỉ cần đảo ngược hoán vị ta được hoán vị nhỏ hơn).
Với các nhận xét trên từ điển các hoán vị vòng tròn có thể được mô tả là các hoán vị
luôn bắt đầu bằng 1 ngoài ra x 2< x n. Như vậy các hoán vị này sẽ được liệt kê theo thứ
tự x 2=2 , 3 ,… ,n−1 và với mỗi x 2 thì x n sẽ xuất hiện theo thứ tự x n=n , n−1 ,… , x 2 +1. Khi

-17-
cố định x 1 , x 2 , x n thì n−3 phần tử còn lại là các hoán vị của n−3 phần tử được liệt kê
theo từ điển tăng dần. Đây chính là bài toán 1

Bài toán 7: Đếm số lượng dãy nhị phân không quá k bit 1
(https://drive.google.com/file/d/19lG5pGMx4Ioy9UZuRe1TU2xFH5Vv2-Sv/view?usp=sharing)
Các dãy nhị phân có độ dài n và có không quá K bit 1 được sắp xếp theo thứ tự từ
điển.
1. Đếm số lượng các dãy nhị phân nói trên
2. Cho một dãy nhị phân, tìm số hiệu cuả nó trong từ điển
3. Cho một số hiệu tìm dãy nhị phân tương ứng
Dữ liệu: Vào từ file văn bản BINARY.INP:
 Dòng đầu ghi số N K (1≤K≤N≤100)
 Tiếp theo là một số dòng, mỗi dòng có một trong hai dạng sau:
o 1 x1 x2 ... xn thể hiện một dãy nhị phân, yêu cầu tìm số thứ tự tương ứng
o 2 P thể hiện một số thứ tự, yêu cầu tìm dãy nhị phân tương ứng
Kết quả: Ghi ra file văn bản BINARY.OUT
 Dòng đầu ghi số lượng dãy nhị phân tìm được
 Các dòng tiếp theo tương ứng với các câu trả lời đối với một dòng trong file dữ
liệu vào. Nếu là loại 1 ... thì cho ra số thứ tự tương ứng, nếu là loại 2 ... thì cho
ra dãy nhị phân tương ứng.
Ví dụ:
BINARY.INP BINARY.OUT
53 26
100111 8
2 26 11100
Thuật toán:
Đây là bài toán đơn giản để vận dụng phương pháp "đường đi trên lưới" để đếm. Coi
mỗi dãy nhị phân như là một đường đi trên lưới xuất phát từ (0,0), gắp số 1 thì đi lên
trên còn gặp số 2 thì đi sang phải. Vì các dãy nhị phân có không quá K bit 1 nên sẽ có
không quá K lần đi lên trên và có it nhất N-K lần đi sang ngang.
Tập các điểm kết thúc sẽ là:
( N−K , K ) , ( N−K +1 , K−1 ) , … . ,( N , 0)
Chú ý rằng nếu đặt A ( 0 , 0 ) , B ( 0 , K ) , C ( N−K , K ) , D (N ,0) thì các đường đi luôn nằm
trong hình thang ABCD.
Việc triển khai việc đếm tương tự như bài toán 3.

-18-
KẾT LUẬN

Chuyên đề này mô tả ba phương pháp cơ bản để xử lý các bài toán về đếm cấu hình
tập hợp. Ngoài phương pháp 1 là phương pháp áp dụng phổ biến thì các phương pháp
2 và 3 là các phương pháp được áp dụng trong các trường hợp riêng.

Dựa trên các phương pháp mô tả, có thể triển khai các kế hoạch giảng dạy khác nhau
phù hợp với đối tượng học sinh.

-19-
PHỤ LỤC A
PHÉP TÍNH SỐ NGUYÊN LỚN

Do số lượng cấu hình của các tập hợp là rất lớn (thường tăng với cấp độ 2n hoặc n !)
nên các kiểu số nguyên trang bị cho các ngôn ngữ lập trình không đáp ứng được. Do
vậy các phép tính cần thực hiện trên các kiểu số nguyên lớn (bignum) do người lập
trình tự xây dựng. Phụ lục này trình bày một trong những cách xây dựng kiểu số
nguyên lớn như vậy trên ngôn ngữ C++.

Trong phần này, chúng ta sẽ xây dựng kiểu số nguyên bằng cách sử dụng mảng ký tự
của C++. Tuy nhiên, khác với cách biểu diễn thông thường, trong cách biểu diễn này
chúng ta lưu hàng đơn vị trước sau đó đến hàng chục, hàng trăm,…. Tóm lại ta có
khai báo sau:

struct bignum{
int deg; // deg - lưu lũy thừa cao nhất của số nguyên (hệ đếm 10)
char d[5001]; // d - mảng ký tự chứa các chữ số bắt đầu từ hàng đơn vị
};

1. Hàm đọc bignum:


void read(bignum &x) {
scanf("%s",x.d); //đọc dãy x.d từ input, có thể sử dụng gets(x.d) nếu trên 1
dòng
strrev(x.d); //đảo ngược các chữ số, khi đó hàng đơn vị đứng đầu
x.deg=strlen(x.d)-1; //lấy lũy thừa cao nhất (bằng số chữ số trừ đi 1)
}

2. Hàm ghi bignum:


void write(bignum &x) {
for(int i=x.deg;i>=0;i--) printf("%c",x.d[i]);
}

3. Hàm so sánh hai số bignum:


// Hàm trả về giá trị -1, 0, 1 tùy theo x<y, x=y, x>y
int cmp(bignum x,bignum y) {
if (x.deg<y.deg) return -1;
if (x.deg>y.deg) return 1;
for(int i=x.deg;i>=0;i--) {
if (x.d[i]<y.d[i]) return -1;
if (x.d[i]>y.d[i]) return 1;
}
return 0;
}

4. Hàm đổi một số kiểu int sang bignum:


void gan(int k,bignum &x) {
sprintf(x.d,"%d",k); // Chuyển int sang xâu x.d
strrev(x.d); // Đảo ngược x.d

-20-
x.deg=strlen(x.d)-1; // Tính bậc x
}

5. Cộng hai số bignum


bignum operator + (bignum x,bignum y) {
bignum z; z.deg=max(x.deg,y.deg);
for(int i=x.deg+1;i<=z.deg;i++) x.d[i]='0';
for(int i=y.deg+1;i<=z.deg;i++) y.d[i]='0';
int nho=0;
for(int i=0;i<=z.deg;i++) {
int tong=x.d[i]+y.d[i]-96+nho;
z.d[i]=tong%10+48;
nho=tong/10;
}
if (nho) z.d[++z.deg]='1';
return z;
}

6.Trừ một số bignum cho một số bignum không lớn hơn nó:
bignum operator - (bignum x,bignum y) {
bignum z; z.deg=x.deg;
for(int i=y.deg+1;i<=z.deg;i++) y.d[i]='0';
int nho=0;
for(int i=0;i<=z.deg;i++) {
int hieu=x.d[i]-y.d[i]-nho;
if (hieu<0) {hieu+=10; nho=1;} else nho=0;
z.d[i]=hieu+48;
}
while (z.deg && z.d[z.deg]=='0') z.deg--;
return z;
}

7. Nhân một số int với một số bignum


bignum operator * (int k,bignum x) {
bignum z; z.deg=x.deg;
int nho=0;
for(int i=0;i<=z.deg;i++) {
int tich=(x.d[i]-48)*k+nho;
z.d[i]=tich%10+48;
nho=tich/10;
}
while (nho) {z.d[++z.deg]=nho%10+48; nho/=10;}
while (z.deg && z.d[z.deg]=='0') z.deg--;
return z;
}

8. Nhân hai số bignum


bignum operator * (bignum x,bignum y) {
bignum z; z.deg=x.deg+y.deg;
for(int i=x.deg+1;i<=z.deg;i++) x.d[i]='0';
for(int i=y.deg+1;i<=z.deg;i++) y.d[i]='0';
int nho=0;

-21-
for(int k=0;k<=z.deg;k++) {
int tong=nho;
for(int i=0;i<=k;i++) tong+=(x.d[i]-48)*(y.d[k-i]-48);
z.d[k]=tong%10+48;
nho=tong/10;
}
while (nho) {z.d[++z.deg]=nho%10+48;nho/=10;}
while (z.deg && z.d[z.deg]=='0') z.deg--;
return z;
}

9. Chia một số bignum cho một số int (chia nguyên)


bignum operator / (bignum x,int k) {
bignum z; z.deg=-1;
int du=0;
for(int i=x.deg;i>=0;i--) {
du=du*10+x.d[i]-48;
z.d[++z.deg]=du/k+48;
du%=k;
}
z.d[z.deg+1]=0;
strrev(z.d);
while (z.deg && z.d[z.deg]=='0') z.deg--;
return z;
}

10. Lấy phần dư của một số bignum cho một số int


int operator % (bignum x,int k) {
int du=0;
for(int i=x.deg;i>=0;i--) du=(du*10+x.d[i]-48)%k;
return du;
}

11. Chia hai số bignum


Chú ý, đây là phép tính phức tạp. Thuật toán sử dụng ở đây là tìm kiếm nhị phân:
bignum operator / (bignum x,bignum y) {
bignum lo,hi, mot;
gan(0,lo); gan(1,hi); gan(1,mot);
while (cmp(y*hi,x)!=1) {lo=hi; hi=2*hi;}
while (cmp(hi-lo,mot)==1) {
bignum mid=(hi+lo)/2;
if (cmp(y*mid,x)!=1) lo=mid; else hi=mid;
}
return lo;
}
Lưu ý rằng kỹ thuật tìm kiếm nhị phân áp dụng trong mục này có thể dùng để thực
hiện các phép toán phức tạp hơn (nhưng phải đảm bảo có tính đơn điệu) như căn bậc
hai, logarit, ….

-22-
PHỤ LỤC B
CÁC PHÉP TÍNH TRONG TRƯỜNG ĐỒNG DƯ

Do số lượng cấu hình có thể rất lớn nên trong một số bài toán nếu chỉ yêu cầu đếm số
cấu hình thì thay vì cho ra kết quả đúng (sử dụng kiểu số nguyên lớn), đề bài thường
chỉ yêu cầu đưa ra phần dư khi chia cho một số nguyên P>1 nào đó. Vì kết quả lớn
nên thích hợp nhất là cần phải có các phép toán thực hiện trực tiếp trong trường đồng
dư Z p ={ 0 , 1 ,2 , … , P−1 }. Phụ lục này giới thiệu các phép toán như vậy

1. Phép cộng, trừ, nhân trong trường đồng dư

( a+ b ) % P=( a % P+b % P+2∗P ) % P


( a−b ) % P=( a % P−b % P+ 2∗P ) % P
( a∗b ) % P=( ( a%P )∗( b %P ) + P∗P ) % P

2. Phép chia trong trường đồng dư.

(a)
Giả sử a ⋮ b ta cần tính b % P mà không cần phải thực hiện phép chia. Thông thường
luôn có giả thiết b và P là nguyên tố cùng nhau để cho kết quả là duy nhất.

Trong số học, ta đã biết rằng nếu d=gcd ⁡(a , b) thì luôn tồn tại x , y ∈ Z sao cho:
a ∙ x+ b ∙ y =d

Thuật toán Euclid mở rộng cho phép xác định các giá trị x , y .

( [] )
a
Ta có d=gcd ( a , b )=gcd ( b ,a %b )=gcd b , a− b ∙ b . Vậy nên sẽ tồn tại x ' , y ' để:

'
d=a ∙ x +b ∙ y=b ∙ x + a−
( []) a
b
' '
[]
'
∙ b ∙ y =a ∙ y +b ∙ (x −
a '
b
∙y)

Vậy nên:

{
x= y '
y=x ' −
[]
a
b
∙y'

Hàm E_gcd bằng C++ dưới đây ngoài việc tìm gcd ⁡(a ,b) còn chỉ ra hai giá trị x , y :

void E_gcd(int a, int b, int &d, int &x, int &y) {


if (b==0) {d=a; x=1; y=0; return;}
int x1, y1;
E_gcd(b,a%b,d,x1,y1);
x=y1, y=x1-(a/b)*y1;
}

-23-
a
()
Quay trở lại vấn đề tính b %P . Ta có gcd ( b , P )=1 do vậy tồn tại x , y ∈ Z sao cho:
1=b∙ x + P ∙ y
Vậy nên ( b ∙ x ) %P=1. Do đó

( ab )%P=( ba∙∙ xx ) %P= ( a ∙ x ) %P


Ta có hàm sau thực hiện phép chia

int Chia(int a, int b) {


int d, x, y;
E_gcd(b,P,d,x,y);
return ((a%P)*(x%P)+P*P)%P);
}

3. Phép lũy thừa

Cần phải tính ( a n ) %P . Ở đây nếu n lớn thì phép nhân thông thường sẽ không cho phép
về thời gian. Cần phải có giải pháp tốt hơn. Nhận xét rằng:
+) Nếu n=2 k thì a n=a 2k =ak ∙ ak
+) Nếu n=2 k +1 thì a n=a 2k +1=a k ∙ a k ∙ a

Do đó ta có hàm đệ qui tính lũy thừa như sau:


int LuyThua(int a,int n) {
if (n==0) return 1;
int t=LuyThua(a,n/2);
t=(t*t) % P;
if (n%2) t=(t*a) % P;
return t;
}

-24-

You might also like