☰
LeetCode 634. 寻找数组的错位排列
2026/10/1 8:44:10 网站建设 项目流程

LeetCode 634. 寻找数组的错位排列(Find the Derangement of An Array)

难度:中等
标签:数学、动态规划、组合数学

一、题目原文

给定一个整数n,原始数组是[1,2,3,…,n]。错位排列(derangement)是一种排列,要求:没有任何元素出现在它原来的位置上。
求该数组一共有多少种错位排列方案。
因为答案可能很大,请返回结果对109+710^9+7109+7取模。

示例

  • 输入 n = 3,输出 = 2
    原始数组 [1,2,3]
    合法错位排列:[2,3,1]、[3,1,2],一共2种
  • 输入 n = 1,输出 = 0
    数组 [1],只能放在原位,没有错位方案
  • 输入 n = 2,输出 = 1
    原始 [1,2],唯一错位:[2,1]

约束

1≤n≤1061 \le n \le 10^61≤n≤106


二、费曼学习法拆解思路(用大白话讲懂,像讲给小白)

费曼核心:不要直接甩公式,先举例子,分类讨论,再推递推式,再优化空间

1. 定义 D(n)

D(n)D(n)D(n)= n个数的错位排列总数。

基础边界(先看小例子找感觉)

  • D(1) = 0:只有数字1,只能站原位,没法错位
  • D(2) = 1:交换1、2,仅此1种
  • D(3) = 2
  • D(4) = 9

2. 推导递推公式(核心逻辑)

我们现在处理数字n,它不能放在第n个位置,所以它可以放在前面 n-1 任意一个位置(位置1,位置2 … 位置n-1),一共n-1种选择。

假设我们把数字n放到位置i。
现在分两种情况讨论数字i放哪里:

情况①:数字i放到位置n(n和i互相交换)
交换完,这两个数已经处理完毕。剩下还有 n-2 个数需要错位排列,方案数 = D(n-2)

情况②:数字i不能放到位置n
现在,数字i不能放位置n,其他数字也不能放自己原来位置。
等价:剩下 n-1 个数做错位排列,方案数 = D(n-1)

✅ 两种情况相加,并且前面有n-1种位置选择:
D(n)=(n−1)×[D(n−1)+D(n−2)]\boldsymbol{D(n) = (n-1) \times [D(n-1)+D(n-2)]}D(n)=(n−1)×[D(n−1)+D(n−2)]
取模:MOD=109+7MOD=10^9+7MOD=109+7

一句话记忆:第n个数字有n-1个坑可以放;放完之后要么两数互换剩D(n-2),要么i被限制不能放n,变成D(n-1)。

3. 解法分类

  1. 递归(暴力,不可行):会大量重复计算,n大直接栈溢出,时间爆炸
  2. DP数组:保存D(1),D(2)…D(n),时间O(n),空间O(n)
  3. 空间优化DP(推荐):只保留前两项 D(n-1), D(n-2),时间O(n),空间O(1),适配n到1e6上限

三、Python代码实现 + 逐行详细注释

解法1:空间优化DP(最优,O(n)时间,O(1)空间,推荐)

deffindDerangement(n:int)->int:# 定义取模常量,题目要求结果对10^9+7取模MOD=10**9+7# 边界条件ifn==1:# 只有1个元素,无法错位,直接返回0return0ifn==2:# [1,2]只能交换,1种方案return1# prev2 代表 D(n-2),初始D(1)=0prev2=0# prev1 代表 D(n-1),初始D(2)=1prev1=1# 从i=3一直循环计算到i=nforiinrange(3,n+1):# D(i) = (i-1) * (D(i-1)+D(i-2)) mod MODcurrent=((i-1)*(prev1+prev2))%MOD# 更新两个前置变量,准备下一轮循环# 原来的D(n-1)变成下一轮D(n-2)prev2=prev1# 当前计算得到D(i),作为下一轮D(n-1)prev1=current# 循环结束,prev1就是D(n)returnprev1# 测试示例if__name__=="__main__":print(findDerangement(1))# 0print(findDerangement(2))# 1print(findDerangement(3))# 2print(findDerangement(4))# 9print(findDerangement(5))# 44

解法2:DP数组版本(方便看完整序列,O(n)空间)

deffindDerangement_dp_array(n:int)->int:MOD=10**9+7ifn==1:return0# dp数组 dp[k] 代表k个数字的错位排列数量dp=[0]*(n+1)dp[1]=0# D(1)=0dp[2]=1# D(2)=1# 从3遍历到nforiinrange(3,n+1):dp[i]=((i-1)*(dp[i-1]+dp[i-2]))%MODreturndp[n]# 测试print(findDerangement_dp_array(4))# 9

解法3:递归(仅教学,不适合大数据,会超时)

deffindDerangement_recursive(n:int)->int:MOD=10**9+7# 边界ifn==1:return0ifn==2:return1# 递推公式直接写递归return((n-1)*(findDerangement_recursive(n-1)+findDerangement_recursive(n-2)))%MOD# n超过20就明显变慢,n=1e6直接崩溃print(findDerangement_recursive(4))#9

四、应用场景举例(错位排列真实使用场景)

场景1:密码/信封问题(经典错位排列原型)

有n封信,n个信封,每封信必须装错信封,求总共有多少种装法。
就是本题,D(n)就是全部装错的方案数。

场景2:抽奖,所有人不能抽到自己的礼物(交换礼物)

聚会n个人,每人准备一份礼物,随机抽签,任何人不能抽到自己准备的礼物,求总共有多少种抽法。
👉 直接调用findDerangement(n)。

场景3:测试用例生成、排列组合概率计算

求随机排列中,没有一个元素落在原位的概率:P=D(n)/n!P = D(n)/n!P=D(n)/n!
当n很大时,概率趋近1/e≈0.36791/e ≈ 0.36791/e≈0.3679(自然常数倒数,错位排列经典结论)。

比如n=100个人随机抽礼物,大约36.79%概率所有人都没有拿到自己的礼物。

场景4:哈希/置换密码、置换打乱算法

密码学里构造置换,要求置换中不存在不动点(没有元素映射到自身),需要统计这种置换总数,使用错位排列。

五、费曼复盘(检验你懂没懂,自问自答)

Q:D(n)公式怎么来的?
A:数字n有n-1个位置放,分两种情况:n和i互换,剩下n-2个;i不能放到n,剩下n-1个错位。相加 ×(n-1)。

Q:为什么要不断取模?
A:D(n)数值爆炸增长,n=1e6时数字极大,Python虽然支持大整数,但题目强制要求返回mod 1e9+7,中途取模防止数字过大拖慢运算。

Q:为什么递归不行?
A:递归重复计算D(n-1),D(n-2),指数级时间;n稍微大一点栈溢出。迭代DP只向前保存两个变量,线性时间。

Q:n很大1e6,空间优化版本为什么能跑?
A:只存prev1、prev2两个变量,常数空间,循环1e6次Python可以快速跑完。

推错位排列另一个公式D(n)=n!⋅(1−11!+12!−13!+...+(−1)n1n!)D(n)=n!\cdot(1-\frac1{1!}+\frac1{2!}-\frac1{3!}+...+(-1)^n\frac1{n!})D(n)=n!⋅(1−1!1​+2!1​−3!1​+...+(−1)nn!1​)并写代码实现。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询