Problem overview
The method countPermutations(_ n: Int) counts the arrangements of 1, 2, ..., n that can be reached from the identity arrangement by swaps, where no item may take part in more than one swap. Zero swaps is allowed, so the identity itself counts.
For n = 3 the answer is 4: [1, 2, 3], [1, 3, 2], [2, 1, 3] and [3, 2, 1]. After one swap of two items, only one item is left untouched, so there are no more swaps to make. The notes say 3 <= N <= 12.
Approach: decide what happens to the last item
Because no item is swapped twice, the swaps never overlap. A swap permutation is therefore a set of disjoint pairs of items that trade places, with every other item staying put. Count them by looking at item i, the last one:
- It stays in place. The other
i - 1items can be arranged indp[i - 1]ways. - It swaps with one of the other
i - 1items. That choice uses up two items, so the remainingi - 2are arranged indp[i - 2]ways.
These cases are different and cover everything, so dp[i] = dp[i - 1] + (i - 1) * dp[i - 2]. The base cases are dp[1] = 1 (one item can only stay) and dp[2] = 2 (stay, or swap once).
Check it for n = 3: dp[3] = dp[2] + 2 * dp[1] = 2 + 2 = 4. For n = 4: dp[4] = dp[3] + 3 * dp[2] = 4 + 6 = 10. Those numbers are also the count of involutions, the permutations that are their own inverse, which is the same family of permutations.
Step by step
- Create
dpas avararray ofn + 1zeros. Index 0 is unused so thatdp[i]means lengthi. - Set the base cases
dp[1] = 1anddp[2] = 2, in one tuple assignment. - Loop
iover3...n. Each value needs the two before it, and both bases are already set. - Apply the recurrence:
dp[i]isdp[i - 1]plus(i - 1) * dp[i - 2]. - Return
dp[n].
The solution in Swift
class Solution {
func countPermutations(_ n: Int) -> Int {
var dp = [Int](repeating: 0, count: n + 1)
(dp[1], dp[2]) = (1, 2)
for i in 3...n {
dp[i] = dp[i - 1] + (i - 1) * dp[i - 2]
}
return dp[n]
}
}
Swift details worth knowing:
- Tuple assignment.
(dp[1], dp[2]) = (1, 2)assigns both elements at once. The left side is a tuple of two array elements, and each one is assigned the matching value. It is the same as two separate assignment lines. - Closed range.
3...nincludesn. Ifnis less than 3 this range is invalid, and Swift stops the program instead of quietly skipping the loop. That is acceptable only because the notes promisen >= 3. - The array size. The array has
n + 1slots, sodp[2]exists only whenn >= 2. Forn = 1the write todp[2]is out of bounds and the program crashes, because Swift checks every array index. - Overflow traps.
Intis 64 bits and a too-large result crashes instead of wrapping. The values stay small here:dp[12]is 140152.
Complexity
Time: O(n). One loop with a constant amount of work per step.
Space: O(n). The array holds n + 1 integers. The recurrence only looks two steps back, so two variables would be enough for O(1) space.
Edge cases and common mistakes
- Multiplying by
iinstead ofi - 1. Itemihas onlyi - 1possible partners, because it can't swap with itself. - Using
dp[i - 1]in the swap case. After a swap, two items are used, so the rest are counted bydp[i - 2]. - Small
n.n = 1andn = 2stop the program in this solution. The answers by the recurrence's bases would be 1 and 2, so a version that has to handle them needs to return early. - Confusing this with all permutations. There are 6 permutations of 3 items but only 4 swap permutations: the two 3-cycles need chained swaps that reuse an item.
Testing
n = 3gives4.n = 4gives10.n = 5gives26.n = 6gives76.n = 12gives140152.
