17. Permutations and combinations — Homework Solutions
Problem 1 is worked on paper. The .cpp files for problem
2 through the challenge are in
exercises/17/homework/solutions/.
Open this page after trying on your own. For the paper problem, write down your answer first, then compare — reading someone else's answer feels clear even when you may not be able to repeat it yourself. The Tests part contains input and output that have already been checked for the coding problems.
Running one of the answers:
g++ -std=c++17 -O2 -o jawab exercises/17/homework/solutions/02-pascal.cpp
./jawab
Problem 1 — Count on paper
a. Arrangements of the letters of BUKU →
12.
Four letters with U appearing twice: 4! / 2! = 24 / 2 =
12.
b. Choosing 3 of 8 students → 336 and 56.
If the three have different tasks, order matters: P(8, 3) = 8 × 7 × 6 = 336. If the tasks are the same, order does not matter: C(8, 3) = 336 / 3! = 56.
Note the relationship: the second answer is the first answer divided by 3!, because every identical group can be arranged in 6 orders.
c. Paths on a 3 × 4 board → 10.
From the top-left corner to the bottom-right it takes 2 steps down and 3 steps right, 5 steps in total. All that remains is choosing 2 positions among 5 for the down steps: C(5, 2) = 10.
d. Choosing at least one of 10 students → 1,023.
There are 2¹⁰ = 1,024 subsets in total, including the empty set. Discard the empty one: 1,023. This is a use of the complement from Chapter 16.
Problem 2 — Pascal's triangle
Problem. Print Pascal's triangle rows 0 through
n (≤ 30).
How to think about it. Every row is built from the row above it with C(n, r) = C(n−1, r−1) + C(n−1, r), and every row starts with C(n, 0) = 1. There is no division and no factorial, so there is no rounding risk.
For n = 30, the largest value is C(30, 15) = 155,117,520
— it still fits in int, but use long long as a
habit.
Walkthrough.
for (int i = 0; i <= n; i++) {
C[i][0] = 1;
for (int r = 1; r <= i; r++)
C[i][r] = C[i - 1][r - 1] + C[i - 1][r];
}Tests. Input 5 produces six lines:
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
Input 0 produces one line containing 1. Two
quick checks for each row: the numbers are symmetric, and the sum of one
row equals 2ⁿ — the last row above sums to 32 = 2⁵.
Common mistakes.
- Filling
C[i][r]forr > i. The value is zero and gets printed too. - Printing a space at the end of the line.
- Using the factorial formula for
n = 30.30!does not fit inlong longeven though the final result does.
Problem 3 — Modular combinations
Problem. 100,000 queries of C(n, r) mod 10⁹ + 7 with
n up to a million.
How to think about it. A Pascal table of a million × a million is impossible. What is used: a factorial table once at the start (a million cells), then every query is answered with
C(n, r) = n! × (r!)⁻¹ × ((n−r)!)⁻¹ mod p
The inverse is computed with Fermat's little theorem, O(log p) per query. Total: 10⁶ for preparation + 10⁵ × 30 for the queries ≈ 4 million steps.
Do not forget ios::sync_with_stdio(false) for input this
large.
Walkthrough.
fakt[0] = 1;
for (int i = 1; i < MAKS; i++) fakt[i] = fakt[i - 1] * i % MOD;
long long kombinasi(int n, int r) {
if (r < 0 || r > n) return 0;
long long bawah = fakt[r] * fakt[n - r] % MOD;
return fakt[n] * pangkat_mod(bawah, MOD - 2, MOD) % MOD;
}Check: C(10, 3) = 120, C(30, 15) = 155,117,520 (same as the Pascal result), C(1,000,000, 500,000) mod p = 996,692,777.
Tests.
n and r |
Output | Note |
|---|---|---|
5 0 |
1 |
choosing zero objects: one way |
10 3 |
120 |
can be checked with the formula by hand |
30 15 |
155117520 |
still below MOD, so this is the true value |
1000000 500000 |
996692777 |
already reduced modulo |
The third row is very useful for testing: C(30, 15) = 155,117,520 is still smaller than 10⁹ + 7, so if your answer differs here, the mistake is certainly in the formula or the inverse — not in the modulo.
Common mistakes.
- Dividing
fakt[n] / bawahwith integer division. The stored values are already remainders; the result has nothing to do with the answer (it is often even zero). - Preparing the factorial table inside the query loop, so it is done 100,000 times.
- Forgetting to handle
r > n, which should be 0.
Problem 4 — Paths on a board
Problem. Paths on a b × k board up to
500,000 × 500,000.
How to think about it. Every path is a sequence of
steps: b − 1 steps down and k − 1 steps right,
in any order. So the problem becomes "choose the positions of the down
steps among all the steps":
C((b−1) + (k−1), b−1)
The largest size is about a million, so a factorial table of a million cells is enough.
Walkthrough.
cout << kombinasi((int)(b - 1 + k - 1), (int)(b - 1)) << "\n";Check: 3 4 gives 10 — the same as the paper answer to
problem 1(c).
Tests.
| Input | Output | Note |
|---|---|---|
1 1 |
1 |
a one-cell board, one path (stay put) |
2 3 |
3 |
can be listed by hand |
3 4 |
10 |
same as problem 1c |
500000 500000 |
738325936 |
the limit the problem asks for |
The last row tests the size of the factorial table: paths on a 500,000 × 500,000 board require C(999,998, 499,999), so the factorial table must be prepared up to about one million — not up to 500,000.
Common mistakes.
- Using a DP over the
b × kboard (dp[i][j] = dp[i-1][j] + dp[i][j-1]). Correct for small boards, but 500,000 × 500,000 cells cannot possibly be stored. - Miscounting the number of steps: a
b × kboard hasbrows andkcolumns, so the steps areb − 1andk − 1, notbandk.
Challenge — Check your formula
Problem. Compare Pascal's triangle with direct enumeration.
How to think about it. cacah(n, r) goes
through all 2ⁿ subsets and counts those of size exactly r.
For n = 15 that is 32,768 masks times 15 bit checks —
light.
The third check (the sum of a row = 2ⁿ) is useful because it is different in nature: it tests a whole row at once, not a single value.
Walkthrough.
long long cacah(int n, int r) {
long long banyak = 0;
for (int mask = 0; mask < (1 << n); mask++) {
int bit = 0;
for (int i = 0; i < n; i++) if (mask & (1 << i)) bit++;
if (bit == r) banyak++;
}
return banyak;
}The output: the formula and the enumeration agree for all
n ≤ 15, and the sum of every row equals 2ⁿ.
This habit — testing a combinatorics formula with a small enumeration — is the fastest way to find a wrong formula before the answer is submitted. It is the same as the comparison tester of Chapter 10, only applied to math.
Tests. The answer program prints its own conclusion:
rumus dan cacahan cocok untuk semua n <= 15
jumlah tiap baris sama dengan 2^n
The two checks complement each other. The first compares the formula
with direct enumeration, so it catches formula mistakes. The second
checks a property that the whole row must satisfy at once, so it catches
mistakes that happen to slip through for one particular value of
r. This two-layer pattern is exactly the comparison tester
of Chapter 10, applied to a formula instead of an algorithm.
Common mistakes.
- Testing with a large
nso that the enumeration itself is too slow. - Writing
cacahusing a formula again (for example factorials), so that what is compared is two forms of the same formula. The enumeration must be plain.
OSN-style exercises — walkthrough
The answer files are in
exercises/17/osn/solutions/.
Problem 1 — Letter Arrangements
How to think about it. Suppose all the letters are
considered different — for example, the two as are marked
a₁ and a₂. Then there are N!
arrangements. But swapping a₁ with a₂ produces
the same word, so every word is actually counted several times: as many
times as there are ways to order those duplicate letters among
themselves.
If letter number h appears cₕ times, every
word is counted ∏ cₕ! times. So the answer is:
N! / (c₁! × c₂! × … × c₂₆!)
Since the answer is modulo, the division is done with the inverse:
for a prime modulus, x⁻¹ ≡ x^(p−2).
Walkthrough.
vector<long long> fakt(n + 1);
fakt[0] = 1;
for (int i = 1; i <= n; i++) fakt[i] = fakt[i - 1] * i % MOD;
long long cacah[26] = {0};
for (char c : s) cacah[c - 'a']++;
long long hasil = fakt[n];
for (int h = 0; h < 26; h++) {
if (cacah[h] > 1) hasil = hasil * invers(fakt[cacah[h]]) % MOD;
}There are at most 26 different letters, so at most 26 modular exponentiations — the cost is negligible compared with preparing the factorials. Measured on the author's machine, a word of length 10⁶ finishes in 0.02 seconds.
Tests.
| Input | Output | What it checks |
|---|---|---|
aabb |
6 |
the sample |
abc |
6 |
all letters different, so 3! |
aaaa |
1 |
all letters the same |
a |
1 |
one letter |
The first three rows are worth checking by hand; also compare with an enumerating program for words of 6–7 letters.
Common mistakes.
- Dividing with
/instead of multiplying by the inverse. The result is almost always wrong becausefakt[n]is already reduced modulo. - Computing
n!withlong longwithout a modulo. 21! alone already overflows. - Using the index
c - 'A', when the input is lowercase letters.
Problem 2 — Sharing Candy
How to think about it. Imagine the N
candies lined up in one row. Between two adjacent candies there is a
gap, and there are N − 1 gaps in total.
Placing a divider in a gap cuts the row into two parts. To get
K parts, exactly K − 1 dividers are needed,
and each gap gets at most one divider — if two dividers were in the same
gap, some child would get nothing.
So the problem turns into: how many ways are there to choose
K − 1 gaps out of N − 1 gaps? The answer is
C(N − 1, K − 1).
The validity condition is K ≤ N: if there are more
children than candies, no distribution is possible.
Walkthrough.
if (k > n) { cout << "0\n"; return 0; }
// C(n-1, k-1)
long long atas = fakt[n - 1];
long long bawah = fakt[k - 1] * fakt[n - k] % MOD;
cout << atas * pangkat(bawah, MOD - 2) % MOD << "\n";The k > n check must come before the formula —
without it, fakt[n - k] is read at a negative index.
Tests.
| Input | Output | What it checks |
|---|---|---|
5 2 |
4 |
the sample |
5 5 |
1 |
every child gets one |
3 5 |
0 |
more children than candies |
1000000 500000 |
998346392 |
upper bound |
Common mistakes.
- Using
C(N, K). That is the answer to a different problem. - Using
C(N + K − 1, K − 1), which is the formula for the case "some children may get nothing". - Forgetting to check
K > N.
Problem 3 — Choosing Without Neighbors
How to think about it. This is a form that looks new but is really an ordinary combination in disguise.
Imagine the K participants already seated in order from
left to right. The condition is that there is at least one empty chair
between two neighboring participants. Take that one mandatory empty
chair out of every gap between participants — K − 1 chairs
in total — and remove them from the count. What remains is
N − (K − 1) chairs, and in that remaining row the
participants may be chosen freely.
So the answer is C(N − K + 1, K).
A quick check: for N = 5, K = 2, the
formula gives C(4, 2) = 6, and there are indeed six valid
choices.
The validity condition is K ≤ N − K + 1. Otherwise, the
answer is 0.
Walkthrough.
long long atasN = n - k + 1;
if (k > atasN || atasN < 0) { cout << "0\n"; return 0; }
vector<long long> fakt(atasN + 1);
fakt[0] = 1;
for (long long i = 1; i <= atasN; i++) fakt[i] = fakt[i - 1] * i % MOD;
long long bawah = fakt[k] * fakt[atasN - k] % MOD;
cout << fakt[atasN] * pangkat(bawah, MOD - 2) % MOD << "\n";Tests.
| Input | Output | What it checks |
|---|---|---|
5 2 |
6 |
the sample |
4 3 |
0 |
impossible, three participants four chairs |
5 0 |
1 |
no participants: exactly one way |
1000000 1000 |
18842136 |
upper bound |
The case 5 0 is often missed. Choosing zero chairs is
one way, not zero ways, and the formula indeed gives
C(6, 0) = 1.
Common mistakes.
- Using
C(N, K)and then subtracting the adjacent ones one by one. It can be correct forK= 2, but it does not generalize. - Forgetting to check
K > N − K + 1, sofaktis read at a negative index. - Answering 0 for
K= 0.
If you are still stuck
- Unsure whether it is a permutation or a combination → Chapter 17, its opening question: does swapping the order produce a different result?
- The answer is
r!times too large → it means you used a permutation for a combination problem. - Modular combinations give the wrong number → Chapter 17, the warning about dividing after the modulo; use the inverse from Chapter 14.
With this chapter, the entire content of the book has been covered. The next step is not a new chapter, but problems: work through the archive of OSN problems on the online judge you registered with in Chapter 01, and reuse the same habits — abstraction, counting steps, testing edge cases.