Có bao nhiêu cách chia cái kẹo giống nhau cho bạn, mỗi bạn có thể không nhận cái nào, theo modulo , với ?
Xếp ngôi sao (các cái kẹo) và vạch ngăn thành một hàng: số sao nằm giữa hai vạch liên tiếp là phần của một bạn. Mỗi cách xếp ứng với đúng một cách chia, nên đáp án là số cách chọn vị trí cho vạch trong vị trí, tức . Sau khi chuẩn bị một bảng giai thừa, mỗi đáp án tính trong .
IĐếm cách chọn và cách xếp
Hai quy tắc nền tảng nhất:
- Quy tắc cộng: một việc làm được bằng một trong hai nhóm cách rời nhau (không cách nào thuộc cả hai nhóm), nhóm đầu có cách, nhóm sau có cách, thì có cách. Từ nhà tới trường có 2 đường đi bộ hoặc 3 tuyến xe buýt thì có cách đi.
- Quy tắc nhân: một việc gồm hai bước liên tiếp, bước đầu có cách và với mỗi cách đó bước sau luôn có cách, thì có cách. Có 3 áo và 4 quần thì có bộ đồ.
Xếp người vào một hàng: vị trí đầu có cách chọn người, vị trí sau còn cách, và cứ thế. Theo quy tắc nhân, số cách là , gọi là giai thừa (factorial) và viết , quy ước . Mỗi cách xếp vật khác nhau thành hàng là một hoán vị (permutation).
Chọn ra trong người và xếp họ theo thứ tự (ví dụ chọn đội trưởng rồi đội phó) gọi là chỉnh hợp (arrangement). Số chỉnh hợp là ; chọn 1 đội trưởng và 1 đội phó trong 5 bạn có cách.
Nếu chỉ chọn một nhóm người mà không quan tâm thứ tự thì ta có tổ hợp (combination). Mỗi nhóm người được đếm lần trong số chỉnh hợp, vì người ấy xếp được thành thứ tự khác nhau. Vậy số tổ hợp, viết và đọc là “ chọn ”, là
Chọn 2 trong 5 bạn đi trực có cách. Từ công thức ta thấy (chọn người ở lại cũng là chọn người đi) và .
Hỏi nhanh
Cho vị trí, ta cần đếm số cách chọn hai vị trí khác nhau. Một bạn viết chương trình dưới đây, bấm Chạy để xem với chương trình in gì:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll n;
signed main() {
cin >> n;
cout << n * (n - 1) << '\n';
}
3
6
Ba vị trí chỉ có ba cặp là , , , nhưng chương trình in . Chương trình sai ở đâu?
là số chỉnh hợp: cặp và cặp là hai cách khác nhau. Khi chỉ cần chọn một cặp, hai cách ấy là một, nên đáp án là . Dùng cho ra cũng sai, quy tắc cộng không áp dụng vì hai lần chọn là hai bước liên tiếp, và cho .
Còn một bài toán xếp hay gặp nữa. Có bao nhiêu xâu khác nhau tạo được từ các chữ của aab? Nếu coi hai chữ a là khác nhau thì có cách xếp, nhưng đổi chỗ hai chữ a cho cùng một xâu, nên mỗi xâu bị đếm lần, và đáp án là . Tổng quát, có vật gồm vật loại 1, vật loại 2, ..., thì số cách xếp thành hàng, gọi là hoán vị lặp, là
Còn hoán vị vòng: xếp người quanh một bàn tròn, hai cách xếp coi là một nếu xoay bàn cái này thành được cái kia. Ta cố định một người ở một chỗ, người còn lại xếp tự do vào các chỗ còn lại, nên có cách.
IITính C(n, k)
Khi nhỏ, ta tính từ chính ý nghĩa của nó. Xét vật cuối cùng trong vật. Nếu không chọn nó, ta phải chọn vật trong vật còn lại: cách. Nếu chọn nó, ta còn phải chọn vật trong vật: cách. Vậy
Xếp các giá trị theo từng hàng ta được tam giác Pascal, mỗi số là tổng hai số ngay phía trên nó:
Chương trình dưới đây tính cả bảng theo modulo bất kỳ, chỉ cần cộng và lấy dư:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll n, k, m, c[2005][2005];
signed main() {
cin >> n >> k >> m;
for (ll i = 0; i <= n; i++) {
c[i][0] = 1 % m;
for (ll j = 1; j <= i; j++) c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % m;
}
cout << c[n][k] << '\n';
}
10 4 12
6
Độ phức tạp: , với cần khoảng phép tính.
Với tới thì bảng không còn dùng được, ta dùng công thức . Khi modulo là số nguyên tố, phép chia cho là nhân với nghịch đảo (bài 0.9). Ta tính bảng giai thừa , rồi chỉ một lần lũy thừa nhanh để có nghịch đảo của ; các nghịch đảo còn lại suy ra bằng . Sau đó mỗi chỉ cần hai phép nhân.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll MOD = 1e9 + 7, N = 1e6 + 5;
ll q, fact[N], ifact[N];
ll pw(ll a, ll b) {
ll r = 1;
while (b) {
if (b & 1) r = r * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return r;
}
ll C(ll n, ll k) {
if (k < 0 || k > n) return 0;
return fact[n] * ifact[k] % MOD * ifact[n - k] % MOD;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
fact[0] = 1;
for (ll i = 1; i < N; i++) fact[i] = fact[i - 1] * i % MOD;
ifact[N - 1] = pw(fact[N - 1], MOD - 2);
for (ll i = N - 1; i >= 1; i--) ifact[i - 1] = ifact[i] * i % MOD;
cin >> q;
while (q--) {
ll n, k;
cin >> n >> k;
cout << C(n, k) << '\n';
}
}
3
5 2
10 3
1000000 500000
10
120
996692777
Độ phức tạp: , với cần khoảng phép nhân để lập bảng, mỗi truy vấn thêm 2 phép nhân.
Hỏi nhanh
Một bạn tính theo modulo bằng cách “chia thẳng”: lấy fact[n] đã lấy dư, rồi chia nguyên cho fact[k] và fact[n - k] cũng đã lấy dư. Bấm Chạy để xem với , :
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll MOD = 1e9 + 7;
ll n, k, fact[20];
signed main() {
cin >> n >> k;
fact[0] = 1;
for (ll i = 1; i <= n; i++) fact[i] = fact[i - 1] * i % MOD;
cout << fact[n] / fact[k] / fact[n - k] << '\n';
}
13 6
62
Đáp án đúng là , nhưng chương trình in . Vì sao?
Với thì , chưa có phép lấy dư nào và phép chia nguyên cho kết quả đúng. Từ trở đi, số đã bị lấy dư mất tính chia hết nên chia nguyên cho ra số vô nghĩa; modulo nguyên tố thì dùng nghịch đảo như trong bảng ifact. Lựa chọn thứ ba sai vì nên đáp án không cần lấy dư thêm.
Ví dụHoán vị của đa tập ký tự
Đề. Cho xâu gồm các chữ cái thường, độ dài . Đếm số xâu khác nhau tạo được bằng cách xếp lại toàn bộ các ký tự của , in kết quả modulo .
Lời giải. Đây chính là hoán vị lặp: gọi là số lần chữ cái xuất hiện, đáp án là . Ta tính rồi nhân với nghịch đảo giai thừa của của từng chữ cái.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll MOD = 1e9 + 7, N = 1e6 + 5;
ll fact[N], ifact[N], cnt[26];
string s;
ll pw(ll a, ll b) {
ll r = 1;
while (b) {
if (b & 1) r = r * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return r;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> s;
ll n = s.size();
fact[0] = 1;
for (ll i = 1; i <= n; i++) fact[i] = fact[i - 1] * i % MOD;
ifact[n] = pw(fact[n], MOD - 2);
for (ll i = n; i >= 1; i--) ifact[i - 1] = ifact[i] * i % MOD;
for (char ch : s) cnt[ch - 'a']++;
ll ans = fact[n];
for (ll x = 0; x < 26; x++) ans = ans * ifact[cnt[x]] % MOD;
cout << ans << '\n';
}
aab
3
Độ phức tạp: , với cần khoảng phép nhân.
Cần cẩn thận khi modulo không phải số nguyên tố (chẳng hạn ): khi đó thường không có nghịch đảo, nên không dùng được cách trên. Nếu đủ nhỏ ta dùng bảng Pascal, vì nó chỉ cần cộng; còn nếu phải chia thì ta chia trước, hoặc rút gọn tử và mẫu, rồi mới lấy dư.
Còn một trường hợp nữa: rất lớn (tới ) nhưng nhỏ. Bảng giai thừa tới không thể lập, nhưng chỉ gồm thừa số ở tử và thừa số ở mẫu. Ta nhân hết tử, nhân hết mẫu, rồi chia một lần. Ví dụ sau dùng cách đó, và dùng thêm một đẳng thức: số tập con của phần tử là , vì mỗi phần tử có hai lựa chọn là có mặt hoặc không (quy tắc nhân). Chia các tập con theo kích thước thì .
Ví dụBó chọn tránh hai kích thước
Đề. Có loại hoa, mỗi loại đúng một bông (). Chọn một tập con khác rỗng làm bó hoa, nhưng bó không được có đúng bông hoặc đúng bông (). Đếm số bó hoa, modulo .
Lời giải. Có tập con khác rỗng. Các bó có đúng bông là , các bó có đúng bông là , hai nhóm này rời nhau vì . Đáp án là . Vì nên các thừa số không chia hết cho , và mẫu cũng không. Kết quả có thể âm sau phép trừ, nên cộng thêm trước khi lấy dư lần cuối.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll MOD = 1e9 + 7;
ll n, a, b;
ll pw(ll x, ll y) {
ll r = 1;
while (y) {
if (y & 1) r = r * x % MOD;
x = x * x % MOD;
y >>= 1;
}
return r;
}
ll C(ll n, ll k) {
ll up = 1, down = 1;
for (ll i = 1; i <= k; i++) {
up = up * ((n - k + i) % MOD) % MOD;
down = down * i % MOD;
}
return up * pw(down, MOD - 2) % MOD;
}
signed main() {
cin >> n >> a >> b;
ll ans = (pw(2, n) - 1 - C(n, a) - C(n, b)) % MOD;
cout << (ans + MOD) % MOD << '\n';
}
4 1 3
7
Độ phức tạp: , với cần khoảng phép tính.
IIIChia kẹo: sao và vạch
Bài toán ở đầu trang là đếm nghiệm nguyên không âm của phương trình , trong đó là số kẹo của bạn thứ . Hình dưới là , : dãy trên ứng với , dãy dưới ứng với , và hai vạch đứng cạnh nhau nghĩa là có bạn không nhận được kẹo nào.
Mỗi nghiệm ứng với đúng một dãy sao và vạch, và ngược lại, nên số nghiệm bằng số cách chọn vị trí vạch trong vị trí:
Với , ta được cách. Nếu mỗi bạn phải nhận ít nhất một kẹo (nghiệm dương), ta phát trước cho mỗi bạn một kẹo, rồi chia kẹo còn lại cho tự do: cách. Tổng quát, khi ta đặt và chia kẹo.
Hỏi nhanh
Có cái kẹo giống hệt nhau chia cho bạn, mỗi bạn nhận ít nhất một cái và phải chia hết cả cái. Có bao nhiêu cách chia?
Phát trước mỗi bạn một cái thì còn cái chia tự do cho bạn: . Con số là đáp án khi cho phép có bạn không nhận được cái nào. Các kẹo giống hệt nhau nên chọn “cái nào” trong cái không có nghĩa, và khoảng trống giữa các kẹo là chỗ đặt vạch, ta cần chọn trong chỗ chứ không phải cách.
Ví dụChia các vật giống nhau
Đề. Có trẻ em phân biệt và quả táo giống hệt nhau (); mỗi em có thể nhận quả. Đếm số cách phân phối toàn bộ quả táo, modulo .
Lời giải. Đây đúng là bài sao và vạch với sao, vạch: đáp án . Bảng giai thừa cần tới , tức gần phần tử.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll MOD = 1e9 + 7, N = 2e6 + 5;
ll n, m, fact[N], ifact[N];
ll pw(ll a, ll b) {
ll r = 1;
while (b) {
if (b & 1) r = r * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return r;
}
signed main() {
cin >> n >> m;
ll t = n + m - 1;
fact[0] = 1;
for (ll i = 1; i <= t; i++) fact[i] = fact[i - 1] * i % MOD;
ifact[t] = pw(fact[t], MOD - 2);
for (ll i = t; i >= 1; i--) ifact[i - 1] = ifact[i] * i % MOD;
cout << fact[t] * ifact[n - 1] % MOD * ifact[m] % MOD << '\n';
}
3 5
21
Độ phức tạp: , với cần khoảng phép nhân.
IVSố Catalan: dãy ngoặc đúng
Một xâu gồm các dấu ( và ) là dãy ngoặc đúng nếu số dấu mở bằng số dấu đóng, và trong mọi tiền tố, số dấu đóng không vượt quá số dấu mở. Gọi là số dãy ngoặc đúng có cặp ngoặc. Với có dãy: ((())), (()()), (())(), ()(()), ()()().
Có hai cách tính. Cách thứ nhất là truy hồi: dấu ( đầu tiên được đóng bởi một dấu ) nào đó; bên trong là một dãy đúng có cặp, phần phía sau là một dãy đúng có cặp, nên
cho dãy nhưng tốn . Cách thứ hai là công thức trực tiếp
Chứng minh
Có xâu gồm dấu mở và dấu đóng (chọn vị trí các dấu mở). Ta đếm những xâu trong đó không phải dãy ngoặc đúng, tức có một tiền tố mà số dấu đóng nhiều hơn số dấu mở. Xét tiền tố ngắn nhất như vậy: nó có dấu mở và dấu đóng. Đổi mọi dấu mở thành đóng và ngược lại trong tiền tố này. Xâu mới có dấu mở và dấu đóng ở tiền tố, cộng với dấu mở và dấu đóng ở phần còn lại, tức dấu mở và dấu đóng.
Ngược lại, từ một xâu có dấu mở và dấu đóng, tiền tố ngắn nhất có số dấu mở hơn số dấu đóng một đơn vị luôn tồn tại; đổi lại tiền tố đó ta được xâu ban đầu. Hai phép biến đổi này là song ánh, nên số xâu không đúng bằng số xâu có dấu mở, dấu đóng, tức . Vậy . Vì , hiệu này cũng bằng .
Số Catalan xuất hiện ở nhiều bài toán khác nhau với cùng đáp án: số cách chia một đa giác lồi đỉnh thành các tam giác bằng những đường chéo không cắt nhau, hay số cây nhị phân có nút.
Ví dụĐếm Chuỗi Ngoặc Đúng
Đề. Cho độ dài (). Đếm số dãy ngoặc đúng có đúng ký tự, modulo .
Lời giải. Nếu lẻ thì số dấu mở không thể bằng số dấu đóng, đáp án . Với , đáp án là , chỉ cần bảng giai thừa tới và không cần nghịch đảo của .
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll MOD = 1e9 + 7, N = 1e6 + 5;
ll n, fact[N], ifact[N];
ll pw(ll a, ll b) {
ll r = 1;
while (b) {
if (b & 1) r = r * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return r;
}
ll C(ll n, ll k) {
if (k > n) return 0;
return fact[n] * ifact[k] % MOD * ifact[n - k] % MOD;
}
signed main() {
cin >> n;
if (n % 2) {
cout << 0 << '\n';
return 0;
}
fact[0] = 1;
for (ll i = 1; i <= n; i++) fact[i] = fact[i - 1] * i % MOD;
ifact[n] = pw(fact[n], MOD - 2);
for (ll i = n; i >= 1; i--) ifact[i - 1] = ifact[i] * i % MOD;
ll m = n / 2;
cout << (C(n, m) - C(n, m + 1) + MOD) % MOD << '\n';
}
8
14
Độ phức tạp: , với cần khoảng phép nhân.
VTổng trên mọi tập: đếm theo đóng góp
Khi cần tổng của một đại lượng trên mọi cách chọn, ta không liệt kê từng cách. Thay vào đó, ta hỏi mỗi phần tử góp vào tổng bao nhiêu lần, rồi cộng đóng góp của từng phần tử.
Ví dụTổng khoảng biến thiên
Đề. Cho số nguyên (, ) và số . Với mỗi cách chọn chỉ số, tính giá trị lớn nhất trừ giá trị nhỏ nhất của các số được chọn. In tổng đại lượng đó trên mọi cách chọn, modulo (các phần tử bằng nhau nhưng khác chỉ số vẫn là khác nhau).
Lời giải. Sắp xếp tăng dần và đánh số từ . Phần tử là lớn nhất của một tập phần tử khi phần tử còn lại nằm trong phần tử đứng trước nó: có tập. Tương tự, là nhỏ nhất trong tập, vì phần tử còn lại nằm trong phần tử đứng sau. Mỗi tập có đúng một phần tử lớn nhất và một phần tử nhỏ nhất theo thứ tự này (kể cả khi có giá trị bằng nhau), nên
Với và , hệ số của ba phần tử lần lượt là , , , đáp án ; cũng bằng của ba cặp. Các có thể âm nên đưa về khoảng trước khi nhân.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll MOD = 1e9 + 7, N = 1e5 + 5;
ll n, k, a[N], fact[N], ifact[N];
ll pw(ll x, ll y) {
ll r = 1;
while (y) {
if (y & 1) r = r * x % MOD;
x = x * x % MOD;
y >>= 1;
}
return r;
}
ll C(ll n, ll k) {
if (k > n) return 0;
return fact[n] * ifact[k] % MOD * ifact[n - k] % MOD;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> k;
for (ll i = 0; i < n; i++) cin >> a[i];
sort(a, a + n);
fact[0] = 1;
for (ll i = 1; i <= n; i++) fact[i] = fact[i - 1] * i % MOD;
ifact[n] = pw(fact[n], MOD - 2);
for (ll i = n; i >= 1; i--) ifact[i - 1] = ifact[i] * i % MOD;
ll ans = 0;
for (ll i = 0; i < n; i++) {
ll coef = (C(i, k - 1) - C(n - 1 - i, k - 1) + MOD) % MOD;
ans = (ans + (a[i] % MOD + MOD) % MOD * coef) % MOD;
}
cout << ans << '\n';
}
3 2
1 5 9
16
Độ phức tạp: cho bước sắp xếp, phần còn lại ; với cần khoảng phép tính.
VINguyên lý Dirichlet
Nguyên lý Dirichlet (pigeonhole principle) phát biểu: nhốt con thỏ vào cái chuồng thì có một chuồng chứa ít nhất hai con. Lý do ngắn gọn: nếu mỗi chuồng chứa nhiều nhất một con thì cả thảy chỉ có nhiều nhất con.
Bài toán: cho số nguyên . Tìm một đoạn liên tiếp có tổng chia hết cho . Đoạn như vậy luôn tồn tại. Gọi là tổng tiền tố (bài 0.3), . Có số nhưng chỉ có phần dư khi chia cho , nên có hai chỉ số mà và cùng phần dư. Khi đó chia hết cho . Để tìm đoạn đó, ta ghi nhớ chỉ số đầu tiên của mỗi phần dư.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1e5 + 5;
ll n, a[N], pos[N];
signed main() {
cin >> n;
for (ll i = 1; i <= n; i++) pos[i] = -1;
pos[0] = 0;
ll p = 0;
for (ll i = 1; i <= n; i++) {
cin >> a[i];
p = ((p + a[i]) % n + n) % n;
if (pos[p] != -1) {
cout << pos[p] + 1 << ' ' << i << '\n';
return 0;
}
pos[p] = i;
}
}
5
4 7 3 8 2
2 3
Độ phức tạp: , với cần khoảng phép tính.
Với ví dụ trên, các tổng tiền tố là và phần dư khi chia cho lần lượt là . Phần dư xuất hiện đầu tiên ở chỉ số và lại xuất hiện ở chỉ số , nên đoạn cần tìm là với .
Câu hỏi nhanh dành cho tài khoản Pro
Mở khóa bằng tài khoản ProVIISố Stirling loại hai
Số Stirling loại hai là số cách chia phần tử khác nhau thành nhóm không rỗng, mà các nhóm không phân biệt thứ tự. Ví dụ : chia thành , , .
Để tính, xét phần tử thứ . Nó có thể đứng một mình trong một nhóm: còn chia phần tử thành nhóm, cách. Hoặc nó nằm cùng nhóm với phần tử khác: chia phần tử thành nhóm trước ( cách), rồi xếp phần tử thứ vào một trong nhóm đó. Vậy
Hàng là . Tổng của một hàng, , là số cách chia tập phần tử thành các nhóm bất kỳ, gọi là số Bell; với ta được .
s[0][0] = 1;
for (ll i = 1; i <= n; i++)
for (ll j = 1; j <= i; j++) s[i][j] = (s[i - 1][j - 1] + j * s[i - 1][j]) % MOD;
Độ phức tạp: , với cần khoảng phép tính.
Bài nâng cao
Xem 2 bài nâng cao
Ví dụTổng chi phí các đội
Đề. Có người phân biệt (). Chọn một tập con không rỗng gồm người thì chi phí là (). Tính tổng chi phí trên mọi tập con không rỗng, tức , modulo .
Lời giải. Viết qua số Stirling: là số hàm từ vị trí vào giá trị. Gom các vị trí có cùng giá trị lại thành nhóm không rỗng ( cách), rồi gán cho nhóm các giá trị đôi một khác nhau, có thứ tự: cách, ký hiệu . Vậy
Thay vào tổng và đổi thứ tự cộng, còn lại . Biểu thức đếm cách chọn tập có người, rồi chọn có thứ tự người đôi một khác nhau trong . Ta đếm theo thứ tự ngược lại: chọn có thứ tự người trong người trước, cách; mỗi trong người còn lại nằm trong hoặc không, cách. Vậy
Số Stirling cần cả hàng nên ta tính bảng bằng một mảng, cập nhật từ lớn xuống nhỏ. Khi thì (có thừa số ), nên không cần xét riêng. Giá trị lấy từ rồi nhân dần với theo modulo.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll MOD = 1e9 + 7, K = 5005;
ll n, k, s[K];
ll pw(ll a, ll b) {
ll r = 1;
while (b) {
if (b & 1) r = r * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return r;
}
signed main() {
cin >> n >> k;
s[0] = 1;
for (ll i = 1; i <= k; i++) {
for (ll j = i; j >= 1; j--) s[j] = (s[j - 1] + j * s[j]) % MOD;
s[0] = 0;
}
ll inv2 = (MOD + 1) / 2, fall = 1, p2 = pw(2, n), ans = 0;
for (ll j = 0; j <= k; j++) {
if (j > 0) {
fall = fall * ((n - j + 1) % MOD) % MOD;
p2 = p2 * inv2 % MOD;
}
ans = (ans + s[j] * fall % MOD * p2) % MOD;
}
cout << ans << '\n';
}
3 2
24
Độ phức tạp: , với cần khoảng phép tính.
Ví dụChọn các giá trị may mắn khác nhau
Đề. Cho số nguyên (, mỗi số trong ). Một số là may mắn nếu mọi chữ số thập phân của nó là hoặc . Đếm số cách chọn chỉ số sao cho các giá trị may mắn được chọn đôi một khác nhau (các số không may mắn thì được trùng), modulo .
Lời giải. Số may mắn không quá chỉ có giá trị, nên số giá trị may mắn khác nhau trong dãy không quá . Tách các số thành hai nhóm: số thường (đếm được số) chọn tự do, còn số may mắn gom theo giá trị, giá trị thứ có chỉ số; từ mỗi giá trị chọn nhiều nhất một chỉ số, và có cách nếu chọn.
Gọi là số cách chọn chỉ số may mắn thỏa điều kiện, trong các nhóm đã xét. Khi xét một nhóm có chỉ số, nhóm này hoặc không được chọn, hoặc được chọn một chỉ số trong cách: thêm . Cập nhật từ lớn xuống nhỏ để mỗi nhóm chỉ được tính một lần. Cuối cùng, chọn số may mắn thì chọn thêm số thường tự do: cách, và đáp án là . Để gom nhóm, ta sắp xếp các số may mắn và đếm các đoạn bằng nhau liên tiếp.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll MOD = 1e9 + 7, N = 1e5 + 5;
ll n, k, a[N], b[N], g[N], fact[N], ifact[N];
ll pw(ll x, ll y) {
ll r = 1;
while (y) {
if (y & 1) r = r * x % MOD;
x = x * x % MOD;
y >>= 1;
}
return r;
}
ll C(ll n, ll k) {
if (k < 0 || k > n) return 0;
return fact[n] * ifact[k] % MOD * ifact[n - k] % MOD;
}
bool lucky(ll x) {
while (x) {
if (x % 10 != 4 && x % 10 != 7) return false;
x /= 10;
}
return true;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> k;
ll m = 0, t = 0;
for (ll i = 1; i <= n; i++) {
cin >> a[i];
if (lucky(a[i])) b[++m] = a[i];
else t++;
}
fact[0] = 1;
for (ll i = 1; i <= n; i++) fact[i] = fact[i - 1] * i % MOD;
ifact[n] = pw(fact[n], MOD - 2);
for (ll i = n; i >= 1; i--) ifact[i - 1] = ifact[i] * i % MOD;
sort(b + 1, b + m + 1);
g[0] = 1;
for (ll i = 1; i <= m;) {
ll j = i;
while (j <= m && b[j] == b[i]) j++;
ll c = j - i;
for (ll x = k - 1; x >= 0; x--) g[x + 1] = (g[x + 1] + g[x] * c) % MOD;
i = j;
}
ll ans = 0;
for (ll x = 0; x <= k; x++) ans = (ans + g[x] * C(t, k - x)) % MOD;
cout << ans << '\n';
}
3 2
4 4 7
2
Độ phức tạp: , với cần khoảng phép tính trong trường hợp xấu nhất.
Khi đi thi
- Chuẩn bị bảng giai thừa và nghịch đảo giai thừa tới giá trị lớn nhất ngay đầu chương trình.
- Modulo không nguyên tố thì không dùng nghịch đảo của giai thừa: dùng bảng Pascal, hoặc chia trước khi lấy dư.
- Viết bản vét cạn cho nhỏ rồi so với công thức trước khi nộp.
Tóm tắt
- Quy tắc cộng cho các nhóm cách rời nhau, quy tắc nhân cho các bước liên tiếp; hoán vị , chỉnh hợp có thứ tự, tổ hợp không có thứ tự, hoán vị lặp chia cho giai thừa số lần lặp.
- tính bằng bảng Pascal với modulo bất kỳ và nhỏ, hoặc bằng bảng giai thừa với modulo nguyên tố; lớn, nhỏ thì tính tích thừa số.
- Chia vật giống nhau cho người: ; mỗi người ít nhất một vật: .
- Số dãy ngoặc đúng có cặp là .
- Tổng trên mọi tập: tính đóng góp của từng phần tử; nguyên lý Dirichlet: vật vào ngăn có ngăn chứa hai vật.
- đếm cách chia phần tử vào nhóm không rỗng.