Problem overview
The method countPermutations(n) counts the permutations of 1 to n that you can reach from [1, 2, ..., n] by swapping pairs of items, where each item may take part in at most one swap. Zero swaps is allowed, so the starting arrangement counts.
For n = 3 the answer is 4: the original [1, 2, 3], plus the three arrangements with exactly one swap, [1, 3, 2], [2, 1, 3] and [3, 2, 1]. Two swaps are impossible, because after one swap only one item is left unused, and a swap needs two different items. The notes ask for n from 3 to 12.
Approach: where does the last item go?
Enumerating all n! permutations and testing each is hopeless for larger n. A recurrence is much cheaper. Let dp[i] be the answer for length i, and look at item i:
- It is never swapped. It stays in place, and the other
i - 1items can be arranged indp[i - 1]ways. - It is swapped with one of the other items. There are
i - 1choices of partner. Both items are then used up, and the remainingi - 2items can be arranged indp[i - 2]ways.
The two cases never overlap, so dp[i] = dp[i - 1] + (i - 1) * dp[i - 2]. These permutations are the ones that undo themselves when applied twice, known as involutions, and this is their standard recurrence.
The base cases are dp[1] = 1 (one item can only stay) and dp[2] = 2 (stay, or swap the two). Then dp[3] = 2 + 2 * 1 = 4, dp[4] = 4 + 3 * 2 = 10 and dp[5] = 10 + 4 * 4 = 26.
Step by step
- Create the table. Make an array of
n + 1zeros so thatdp[i]is the answer for lengthi. Index 0 is unused. - Set the base cases.
dp[1]is 1 anddp[2]is 2, written together in one statement. - Loop from 3 to
n, so each value is built from two values that are already known. - Apply the recurrence to compute
dp[i]. - Return
dp[n].
The solution in TypeScript
class Solution {
countPermutations(n: number): number {
const dp: number[] = new Array(n + 1).fill(0);
((dp[1] = 1), (dp[2] = 2));
for (let i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + (i - 1) * dp[i - 2];
}
return dp[n];
}
}
TypeScript details worth knowing:
- The comma operator. The line
((dp[1] = 1), (dp[2] = 2));is two assignments joined by a comma. The comma evaluates both, left to right, and produces the value of the last one. Each assignment is itself an expression with a value, so the whole line has the value 2, which is thrown away. Two separate statements would do the same job. fill(0)is required.new Array(n + 1)alone has empty slots, and arithmetic on an empty slot givesNaN. Withfill(0)every slot is a real number.constand the array. The namedpcan't be reassigned, but its elements can.- Writing past the end grows the array. For
n = 1the array has two slots, anddp[2] = 2quietly adds a third. The function still returnsdp[1], which is 1. - Plain numbers. A TypeScript
numberis a double, so forn = 12the result, 140152, is exact and there is no overflow to think about.
Complexity
Time: O(n). One loop with a constant amount of work per pass.
Space: O(n). The array has n + 1 entries. The recurrence only looks at the last two values, so two variables would reduce this to O(1).
Edge cases and common mistakes
- Forgetting the factor
i - 1. Itemican pair with any of the otheri - 1items. Usingdp[i - 1] + dp[i - 2]would give 3 forn = 3instead of 4. - Using
dp[i - 1]in the swap case. After a swap, two items are used up, so two fewer items remain. - Small inputs.
n = 1returns 1 andn = 2returns 2. They come straight from the base cases. - Treating this as
n!. Forn = 4there are 24 permutations but only 10 swap permutations. A rotation such as[2, 3, 4, 1]needs a swap chain in which an item moves twice, so it is not counted.
Testing
n = 3gives4.n = 4gives10.n = 5gives26.n = 6gives76.n = 12gives140152.