- vừa được xem lúc

Bài toán chia kẹo Euler

0 0 52

Người đăng: Viblo Algorithm

Theo Viblo Asia

I. Bài toán mở đầu

Bài toán chia kẹo Euler là một bài toán tổ hợp xuất hiện từ thời xa xưa, đây là một bài toán rất hay và có nhiều ứng dụng trong Toán học. Xuất phát từ một vấn đề rất đơn giản, nhà bác học Leohard Euler đã phát biểu nó thành một bài toán như sau:

"Có mm chiếc kẹo giống nhau, cần chia chúng cho nn em bé. Hỏi có bao nhiêu cách chia kẹo như vậy?"

Bài toán tưởng chừng như đơn giản, nhưng nó lại gây khó khăn cho không ít học sinh. Từ bài toán này, người ta đã phát triển ra cách giải cho vô số bài toán đếm khác nhau. Trong bài viết này, tôi sẽ giới thiệu tới các bạn phương pháp giải bài toán chia kẹo Euler và một số ứng dụng từ cơ bản tới nâng cao của nó. Tất nhiên, nội dung các bài toán sẽ liên quan nhiều tới lập trình, do đó tôi sẽ bỏ qua những bài toán quá khó (mà chỉ dành cho học sinh chuyên Toán).

1. Phương pháp giải bài toán cơ bản

Nếu gọi số kẹo mà mỗi em bé nhận được lần lượt là x1,x2,,xn (0xim;i:1in)x_1, x_2, \dots, x_n \ (0 \le x_i \le m; \forall i: 1 \le i \le n). Bài toán khi đó sẽ trở thành: Đếm số nghiệm nguyên không âm của phương trình:

x1+x2++xn=mx_1 + x_2 + \cdots + x_n = m

Sử dụng kĩ thuật song ánh, coi rằng giữa mỗi em bé có một số 00 và số kẹo của em bé thứ ii nhận được sẽ biểu diễn bằng một dãy gồm xix_i số 1,1, thì bài toán trở thành đếm số cấu hình có dạng:

<center>

Với mm số 11n1n - 1 số 00

</center>

Như vậy, thực tế ta đang đếm số cách đặt n1n - 1 số 00 vào một dãy gồm m+n1m + n - 1 vị trí, còn lại sẽ là các số 11. Theo quy tắc tổ hợp không lặp, số nghiệm của bài toán sẽ là:

Cm+n1n1C^{n - 1}_{m + n - 1}

Tuy nhiên, khi lập trình các bạn sẽ cần lưu ý cả tới giới hạn dữ liệu của bài toán. Nếu như bài toán yêu cầu đưa ra kết quả là phần dư sau khi chia cho một giá trị nào đó thì cần sử dụng các phương pháp phù hợp như Nghịch đảo modulo, Thuật toán bình phương và nhân hay Phép nhân Ấn Độ tương ứng. Lập trình ở phần này không khó nên tôi sẽ không viết lại code nữa!

2. Nếu các em bé đều phải nhận được ít nhất 1 chiếc kẹo?

Bài toán có thể lắt léo hơn một chút, nếu như đề bài yêu cầu rằng khi chia kẹo, mỗi em bé đều phải được nhận ít nhất 11 chiếc kẹo. Khi đó, bài toán sẽ trở thành: Đếm số nghiệm nguyên dương của phương trình:

x1+x2++xn=mx_1 + x_2 + \cdots + x_n = m

Đối với bài toán này, ta giải như sau: Đặt yi=xi1;i:1iny_i = x_i - 1; \forall i: 1 \le i \le n. Khi đó ta có:

y1+y2++yn=x1+x2++xnn=mn (1)y_1 + y_2 + \cdots + y_n = x_1 + x_2 + \cdots + x_n - n = m - n \ (1)

Lúc này phương trình xảy ra hai trường hợp kết quả:

  • Nếu m<nm < n thì phương trình vô nghiệm.
  • Nếu mnm \le n thì bài toán lại trở thành dạng giống với bài toán cơ bản, lúc này số nghiệm của phương trình chính là số bộ giá trị (y1,y2,,yn)(y_1, y_2, \dots, y_n) thỏa mãn phương trình (1),(1), đó là:

    Cm1n1C^{n - 1}_{m - 1}

3. Phát triển bài toán tổng quát

Các bài toán có dạng như hai bài toán nói trên đều có thể phát biểu thành dạng tổng quát như sau: Đếm số nghiệm nguyên của phương trình x1+x2++xn=m;x_1 + x_2 + \cdots + x_n = m; với xiai (i:1in)x_i \ge a_i \ (\forall i: 1 \le i \le n).

Lời giải của bài toán này tương tự với bài toán số 2, ta gọi yi=xiai;i:1iny_i = x_i - a_i; \forall i: 1 \le i \le ns=i=1nais = \sum_{i = 1}^n a_i. Phương trình đã cho sẽ trở thành:

y1+y2++yn=(x1a1)+(x2a2)++(xnan)=ms (2)y_1 + y_2 + \cdots + y_n = (x_1 - a_1) + (x_2 - a_2) + \cdots + (x_n - a_n) = m - s \ (2)

Giờ ta cần xét tới ba trường hợp kết quả:

  • Nếu m<s,m < s, thì phương trình đã cho sẽ vô nghiệm.
  • Nếu m=s,m = s, thì phương trình đã cho có duy nhất một nghiệm là xi=ai;i:1inx_i = a_i; \forall i: 1 \le i \le n.
  • Nếu m>s,m > s, thì ta cần đếm số bộ giá trị (y1,y2,,yn)(y_1, y_2, \dots, y_n) thỏa mãn phương trình (2),(2), đó là:

    Cm+ns1n1C^{n - 1}_{m + n - s - 1}

Bây giờ chúng ta hãy cùng xét một vài bài toán ứng dụng của công thức chia kẹo Euler trong lập trình thi đấu, để hiểu rõ hơn về ứng dụng của công thức này trong các kì thi lập trình!

II. Một số bài toán minh họa

1. Đếm đường đi

Đề bài

Cho một lưới gồm các ô vuông. Các ô được đánh số từ 00 đến mm theo chiều từ trái sang phải và từ 00 đến nn theo chiều từ dưới lên trên. Giả sử ta đang ở ô (0,0);(0,0); ta chỉ có thể di chuyển trên cạnh các ô vuông theo chiều từ trái sang phải hoặc từ dưới lên trên.

Yêu cầu: Hãy tính số đường đi khác nhau từ ô (0,0)(0,0) đến ô (m,n)(m,n) của lưới ô vuông?

Input:

  • Một dòng duy nhất chứa hai số nguyên mmnn.

Ràng buộc:

  • 1m,n50001 \le m, n \le 5000.
  • m+n5000m + n \le 5000.

Output:

  • In ra số dư của kết quả của bài toán sau khi chia cho 109+710^9 + 7.

Sample Input:

2 3

Sample Output:

10

Ý tưởng

Nhận xét thấy, một đường đi thỏa mãn gồm m+nm + n bước đi (mỗi bước đi là một cạnh ô vuông). Tại mỗi bước đi chỉ được chọn một trong hai giá trị đi lên (ta đặt là 11) hoặc đi sang phải (ta đặt là 00). Số bước đi lên đúng bằng nn và số bước sang phải đúng bằng mm. Bài toán lúc này dẫn đến việc tìm xem có bao nhiêu dãy nhị phân có độ dài m+nm + n trong đó có đúng nn thành phần có giá trị bằng 11.

Dựa trên bài toán chia kẹo Euler, kết quả cần tìm lúc này là (m+nm)m + n \choose m.

Ta có thể tính tổ hợp (nk)n \choose k bằng quy hoạch động dựa trên tính chất sau của tổ hợp:

(nk)=(n1k1)+(n1k){n \choose k} = {n - 1 \choose k - 1} + {n - 1 \choose k}

Độ phức tạp: O(n2)O(n^2). Các bạn cần kết hợp với việc tính toán modulo liên tục để tránh gây ra tràn số nếu như sử dụng ngôn ngữ C++.

Cài đặt

#include <bits/stdc++.h> using namespace std; const int MOD = 1e9 + 7;
long long ncr[5005][5005], n, m; void pre_compute() { int k; for (int i = 0; i < 5001; i++) { ncr[i][0] = ncr[i][i] = 1; // Chỉ tính đến vị trí cột thứ i / 2 là đủ cho hàng i. k = i >> 1; for (int j = 1; j < k + 1; j++) ncr[i][j] = ncr[i][i - j] = (ncr[i - 1][j] + ncr[i - 1][j - 1]) % MOD; }
} int main() { int m, n; cin >> m >> n; pre_compute(); cout << ncr[m + n][m]; return 0;
}

2. Hợp nhất danh sách

Đề bài

Hôm nay Tí vừa học xong về danh sách liên kết trên trường. Cậu ấy đã được học về cách hợp nhất hai danh sách liên kết. Khi ta hợp nhất hai danh sách liên kết, thứ tự của các phần tử của mỗi danh sách không thay đổi. Ví dụ, nếu ta hợp nhất [1,2,3][1,2,3][4,5,6][4,5,6], ta sẽ thu được danh sách mới là [1,4,2,3,5,6][1,4,2,3,5,6], tuy nhiên [1,4,3,2,5,6][1,4,3,2,5,6] không hợp lệ vì 33 đứng sau 22.

Yêu cầu: Tí có hai danh sách liên kết gồm nnmm phần tử, hãy giúp cậu ấy tính xem có bao nhiêu cách để hợp nhất cả hai danh sách. Dữ liệu đảm bảo rằng n+mn + m phần tử trong hai danh sách đều phân biệt.

Input:

  • Dòng đầu tiên chứa số nguyên tt chỉ số lượng truy vấn.
  • tt dòng tiếp theo, mỗi dòng chứa một truy vấn gồm hai số nguyên là nnmm.

Ràng buộc:

  • 1t101 \le t \le 10.
  • 1n,m1001 \le n, m \le 100.

Output:

  • In ra đáp án của bài toán sau khi chia cho 109+710^9 + 7 và lấy số dư làm kết quả.

Sample Input:

1
2 2

Sample Output:

6

Giải thích:

Giả sử hai danh sách là [1,2][1,2][3,4][3,4], các cách khác nhau để hợp nhất các danh sách này là:

  • [1,2,3,4][1,2,3,4].
  • [1,3,2,4][1,3,2,4].
  • [3,4,1,2][3,4,1,2].
  • [3,1,4,2][3,1,4,2].
  • [1,3,4,2][1,3,4,2].
  • [3,1,2,4][3,1,2,4].

Ý tưởng

Bạn phải hợp nhất hai danh sách gồm mmnn phần tử lại với nhau. Điều này tương đương với việc sắp xếp mm vật thuộc cùng một loại với nn vật cùng thuộc loại khác trên cùng một hàng. Đây chính là bài toán chia kẹo Euler!

Tổng số cách để hợp nhất hai danh sách sẽ là (n+mn)n + m \choose n. Lí do là vì ta cần chọn ra nn vị trí trong m+nm + n vị trí để đặt nn vật thuộc loại thứ 22 vào. Kết quả không có gì thay đổi nếu như bạn chọn nó bằng (n+mm)n + m \choose m.

Ta có thể tính tổ hợp (nk)n \choose k bằng quy hoạch động dựa trên tính chất sau (chính là tam giác Pascal, nếu các bạn chưa nhớ về công thức này thì hãy tìm đọc lại trên internet):

(nk)=(n1k1)+(n1k){n \choose k} = {n - 1 \choose k - 1} + {n - 1 \choose k}

Độ phức tạp: O(n2)O(n^2). Các bạn nên khởi tạo trước mảng hai chiều lưu tam giác Pascal rồi đưa ra kết quả của mỗi test case trong O(1)O(1).

Cài đặt

#include <bits/stdc++.h>
#define int long long using namespace std; const int MOD = 1e9 + 7; void pre_compute(vector < vector < int > > &ncr, int max_size)
{ ncr = vector < vector < int > >(max_size + 1, vector < int >(max_size + 1)); for (int i = 0; i <= max_size; ++i) { ncr[i][0] = ncr[i][i] = 1; for (int j = 1; j < i; ++j) ncr[i][j] = (ncr[i - 1][j] + ncr[i - 1][j - 1]) % MOD; }
} main()
{ vector < vector < int > > ncr; pre_compute(ncr, 200); int t; cin >> t; while (t--) { int n, m; cin >> n >> m; cout << c[n + m][m] << endl; } return 0;
}

III. Tài liệu tham khảo

Bình luận

Bài viết tương tự

- vừa được xem lúc

Thuật toán quay lui (Backtracking)

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.

0 0 51

- vừa được xem lúc

Các thuật toán cơ bản trong AI - Phân biệt Best First Search và Uniform Cost Search (UCS)

Nếu bạn từng đọc các thuật toán trong AI (Artificial Intelligence - Trí tuệ nhân tạo), rất có thể bạn từng nghe qua về các thuật toán tìm kiếm cơ bản: UCS (thuộc chiến lược tìm kiếm mù) và Best First Search (thuộc chiến lược tìm kiếm kinh nghiệm). Khác nhau rõ từ khâu phân loại rồi, thế nhưng hai th

0 0 169

- vừa được xem lúc

Sử dụng vector trong lập trình C++ - giải bài toán lập trình muôn thủa

Chào buổi tối mọi người, hôm nay lang thang trên mạng bắt gặp bài toán quen thuộc một thời của quãng đường sinh viên IT. Đấy chính là câu số 1 trong đề thi dưới đây:.

0 0 54

- vừa được xem lúc

MÔ PHỎNG THUẬT TOÁN VƯƠNG HẠO TRONG PROLOG

. 1. Các luật suy diễn trong thuật toán Vương Hạo. Luật 1: Chuyển vế các giả thuyết và kết luận ở dạng phủ định. Ví dụ: p v q, !(r ^ s), !q, p v r -> s, !p <=> p v q, p v r, p -> s, r ^ s, q.

0 0 89

- vừa được xem lúc

A* Search Algorithm

What is A* Search Algorithm. How it works. . Explanation.

0 0 58

- vừa được xem lúc

Python: Jump Search

Search là một từ khóa khá là quen thuộc đối với chúng ta. Hiểu theo đúng nghĩa đen của nó chính là "Tìm kiếm".

0 0 50