A dynamic programming and combinatorics solution for counting permutations with no fixed positions.
Problem Restatement
We are given an integer n.
We need to count how many permutations of the numbers:
[1, 2, 3, ..., n]have no fixed positions.
A fixed position means:
perm[i] == ifor some index.
A permutation with no fixed positions is called a derangement.
Because the answer can be very large, return it modulo:
10**9 + 7Input and Output
| Item | Meaning |
|---|---|
| Input | Integer n |
| Output | Number of derangements of size n |
| Modulo | 10^9 + 7 |
Example function shape:
def findDerangement(n: int) -> int:
...Examples
Example 1:
n = 3The permutations of [1, 2, 3] are:
| Permutation | Fixed Position? |
|---|---|
[1, 2, 3] | yes |
[1, 3, 2] | yes |
[2, 1, 3] | yes |
[2, 3, 1] | no |
[3, 1, 2] | no |
[3, 2, 1] | yes |
Only two permutations are valid:
[2, 3, 1]
[3, 1, 2]So the answer is:
2Example 2:
n = 2Possible permutations:
| Permutation | Valid? |
|---|---|
[1, 2] | no |
[2, 1] | yes |
So the answer is:
1First Thought: Generate All Permutations
We could generate every permutation and count the ones with no fixed positions.
But there are:
n!permutations.
This becomes impossible even for moderate values of n.
We need a mathematical recurrence.
Key Insight
Let:
dp[n]mean:
the number of derangements of size nWe want to derive a recurrence relation.
Focus on the position of number 1.
Since this is a derangement, 1 cannot stay at position 1.
Suppose 1 moves to position k.
Now consider the number originally at position k.
There are two possibilities.
Case 1: Swap
Suppose the number at position k moves to position 1.
Then:
1 and k swap positionsAfter fixing this pair, the remaining:
n - 2elements must form a derangement.
So this case contributes:
dp[n - 2]ways.
Case 2: No Swap
Suppose the number at position k does not move to position 1.
Then we can think of the remaining problem as a derangement of:
n - 1elements.
So this case contributes:
dp[n - 1]ways.
Building the Recurrence
There are:
n - 1possible choices for where 1 goes.
For each choice, the total number of valid arrangements is:
dp[n - 1] + dp[n - 2]Therefore:
dp[n] = (n - 1) * (dp[n - 1] + dp[n - 2])This is the classic derangement recurrence.
Base Cases
For:
n = 1there is no valid derangement:
dp[1] = 0For:
n = 2there is exactly one derangement:
[2, 1]So:
dp[2] = 1Algorithm
- Handle base cases.
- Build the DP recurrence from
3up ton. - Apply modulo after every operation.
- Return
dp[n].
Implementation
class Solution:
def findDerangement(self, n: int) -> int:
MOD = 10**9 + 7
if n == 1:
return 0
if n == 2:
return 1
dp1 = 0
dp2 = 1
for size in range(3, n + 1):
current = (size - 1) * (dp1 + dp2)
current %= MOD
dp1 = dp2
dp2 = current
return dp2Code Explanation
We use rolling variables instead of a full array.
dp1 = 0
dp2 = 1These represent:
| Variable | Meaning |
|---|---|
dp1 | dp[n - 2] |
dp2 | dp[n - 1] |
For each new size:
current = (size - 1) * (dp1 + dp2)This directly implements:
dp[n] = (n - 1) * (dp[n - 1] + dp[n - 2])Then we shift the rolling variables forward:
dp1 = dp2
dp2 = currentFinally:
return dp2returns the answer for size n.
Correctness
We prove that the recurrence:
dp[n] = (n - 1) * (dp[n - 1] + dp[n - 2])correctly counts all derangements of size n.
Consider any derangement of:
[1, 2, ..., n]The number 1 cannot stay in position 1, so it must move to one of the other:
n - 1positions.
Suppose 1 moves to position k.
Now consider the element originally at position k.
If that element moves to position 1, then 1 and k form a swap. The remaining n - 2 elements must form a derangement. This gives:
dp[n - 2]possibilities.
Otherwise, the element from position k does not move to position 1. In this case, the remaining structure corresponds to a derangement of size:
n - 1This gives:
dp[n - 1]possibilities.
Since there are:
n - 1choices for the destination of 1, the total number of derangements is:
(n - 1) * (dp[n - 1] + dp[n - 2])Therefore the recurrence is correct.
The base cases are also correct:
n | Value | Reason |
|---|---|---|
1 | 0 | The only permutation fixes the element |
2 | 1 | Only [2,1] is valid |
Thus the algorithm computes the correct number of derangements.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | One DP transition per size |
| Space | O(1) | Only rolling variables are stored |
Alternative Solution: Full DP Table
We can also store every DP value.
class Solution:
def findDerangement(self, n: int) -> int:
MOD = 10**9 + 7
if n == 1:
return 0
dp = [0] * (n + 1)
dp[1] = 0
dp[2] = 1
for i in range(3, n + 1):
dp[i] = (i - 1) * (dp[i - 1] + dp[i - 2])
dp[i] %= MOD
return dp[n]This is simpler conceptually but uses more memory.
| Metric | Value |
|---|---|
| Time | O(n) |
| Space | O(n) |
Mathematical Formula
The number of derangements is often written as:
!nThere is also a closed-form formula:
!n = n! * (1 - 1/1! + 1/2! - 1/3! + ...)More precisely:
!n = nearest integer to n! / eBut the recurrence solution is much easier and safer for programming problems.
Testing
def run_tests():
s = Solution()
assert s.findDerangement(1) == 0
assert s.findDerangement(2) == 1
assert s.findDerangement(3) == 2
assert s.findDerangement(4) == 9
assert s.findDerangement(5) == 44
assert s.findDerangement(6) == 265
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
1 | Smallest input |
2 | First non-zero answer |
3 | Official example |
4 | Standard recurrence check |
5 | Larger combinatorial count |
6 | Confirms recurrence growth |