Problem overview
The function countPermutations(n) counts the permutations of 1, 2, ..., n that you can reach from the identity permutation by swapping pairs of items, where each item may take part in at most one swap. Zero swaps is allowed, so the identity itself counts.
For n = 3 the answer is 4: [1, 2, 3] with no swap, and the three single swaps [1, 3, 2], [2, 1, 3] and [3, 2, 1]. A second swap is impossible, because after one swap only one item is left unused, and a swap needs two. For larger n, several disjoint swaps can be chained. The notes say 3 <= N <= 12.
Approach: ask what happens to the last item
Generating all permutations and testing each one is slow, since there are n! of them. A recurrence is faster. Let dp[i] be the answer for length i, and look at item i, the last one:
- It is never swapped. It stays in place and the other
i - 1items are arranged in any valid way, givingdp[i - 1]permutations. - It is swapped with one of the other
i - 1items. That partner is used up along with itemi, and the remainingi - 2items are arranged in any valid way, givingdp[i - 2]permutations for each of thei - 1possible partners.
The two cases are disjoint, so the totals add: dp[i] = dp[i - 1] + (i - 1) * dp[i - 2]. The base cases are dp[1] = 1, because one item can only stay put, and dp[2] = 2, because two items either stay or swap. The recurrence gives dp[3] = 2 + 2 * 1 = 4, which matches the example. The same numbers count the permutations that are their own inverse, called involutions.
Step by step
- Create
dpas anIntArrayof sizen + 1, so thatdp[n]is a valid index. Index 0 goes unused. - Set the base cases
dp[1] = 1anddp[2] = 2. - Loop
iover3..n. Each value needs the two before it, which are already filled. - Apply the recurrence in the loop body.
- Return
dp[n].
The solution in Kotlin
class Solution {
fun countPermutations(n: Int): Int {
val dp = IntArray(n + 1)
dp[1] = 1; dp[2] = 2
for (i in 3..n) {
dp[i] = dp[i - 1] + (i - 1) * dp[i - 2]
}
return dp[n]
}
}
The Kotlin details worth a closer look:
IntArray(n + 1). It createsn + 1zeros.valonly stops you from pointingdpat a different array, so assigning to its elements is fine.- The semicolon. Kotlin statements normally end at the line break. A semicolon lets you put two statements on one line, as in
dp[1] = 1; dp[2] = 2. - Ranges.
3..nincludesn, which is what you want here. Ifnis less than 3 the range is empty and the loop body never runs. Intis enough. The value forn = 12is 140152, nowhere near theIntlimit, so noLongis needed.
Complexity
Time: O(n). The loop runs n - 2 times with constant work in each.
Space: O(n). The array holds n + 1 integers. Each value depends only on the previous two, so two variables could replace the array.
Edge cases and common mistakes
- Small
n. The notes promisen >= 3. Withn = 1the array has size 2, and the linedp[2] = 2throwsArrayIndexOutOfBoundsException.n = 0fails earlier, atdp[1] = 1. - Forgetting the multiplier. Writing
dp[i - 1] + dp[i - 2]gives the Fibonacci numbers. The factori - 1counts the choices of partner. - Using
iinstead ofi - 1. Itemican't swap with itself, so there are onlyi - 1partners. - Counting different swap orders separately. Swapping items 1 and 2 and then 3 and 4 gives the same permutation as the other order. The recurrence counts permutations, not sequences of swaps.
Testing
n = 3gives4.n = 4gives10.n = 5gives26.n = 6gives76.n = 12gives140152.