錯排問題,這個問題的背景可能有多種表述方式,將其形式化,可表為:將 1 至 n 這 n 個數字排列,使得每個數字不出現在其所在序號的位置上,問所有可能的排列數。詳見 wiki
對於這一問題的「官方」解法是這樣的:考慮編號為 1 的數字,顯然它有 \(n-1\) 個可能的位置。假定它出現在 i 位置上,那么分兩種情況考慮:
- \(i\) 號數字出現在 1 位置上,則問題縮減為 \(n-2\) 個數字錯排的情況;
- \(i\) 號數字出現在非 1 位置上。這里出現了一個精妙的想法:對於 \(i\) 號數字來說,它「不允許」出現在 1 位置上,而其他的各元素 \(2, 3, ..., i-1, i+1, ..., n\) 不允許出現在各自的編號位置上——所以,\(i\) 號元素和其他元素的地位是等價的——問題化歸為 \(n-2\) 個數字錯排的情況。
綜上,可以得到遞推公式,\(D(n)=(n-1)(D(n-1)+D(n-2)), n\ge 3\)。當然,初始條件為 \(D(1)=0, D(2)=1\)。
另一種也比較容易想到的方法是這樣的:對於所有可能的排列,減去其中出現矛盾的情況。
- 若只有 1 個數字出現了矛盾(在它的位置上),共有 \(C_n^1\) 種可能,對於剩下的每個元素都處於非自己的位置上,即划歸為 \(n-1\) 時的問題;
- 若有 2 個數字出現矛盾,共有 \(C_n^2\) 種可能,對其他元素進行錯排,即 \(n-2\) 時的情況;...
- 若有 \(n-2\) 個數字出現矛盾,共有 \(C_n^{n-2}\) 種可能,對其他元素進行錯排,即 \(2\) 時的情況;
- 注意到,不可能恰好只有 \(n-2\) 個數字出現矛盾(最后那個數字只能在它的位置上),因此最后再減去 1,即所有元素在它的位置上的情況。
綜上,遞推公式為 \(D(n)=n!-C_n^1D(n-1)-C_n^2D(n-2)-...-C_n^{n-2}D(2)-1, n\ge 3\),初始條件和上種解法一致。
from scipy.special import comb
import math
def derangement_1(n):
if n == 1:
return 0
if n == 2:
return 1
return (n-1) * (derangement_1(n-1)+derangement_1(n-2))
def derangement_2(n):
if n == 1:
return 0
if n == 2:
return 1
der = math.factorial(n)
for i in range(n-1, 1, -1):
der -= comb(n, n-i) * derangement_2(i)
return int(der-1)
m = 15
for i in range(2, m):
print(derangement_1(i), end=" ")
print()
for i in range(2, m):
print(derangement_1(i), end=" ")
結果為
1 2 9 44 265 1854 14833 133496 1334961 14684570 176214841 2290792932 32071101049
1 2 9 44 265 1854 14833 133496 1334961 14684570 176214841 2290792932 32071101049
可見,兩種計算方法是一致的。