[题解] P4071 [SDOI2016] 排列计数

P4071 [SDOI2016] 排列计数

先给出几个便于解题的定义:

\(p[i]\) 表示元素 \(i\) 应该放在哪个位置上,初始时 \(p[i]=i\) ,保证任何时候都是一一对应

\(a[i]=p[i]\),也就是刚好放在该放的位置时,则我们称第 \(i\) 个元素放的位置是正确的,反之称之为错误的。


对于本题的求解,我们先从 \(n\) 个数中无序地选 \(m\) 个放到正确的位置上,共有 \(C_n^m\) 种选法,那么剩下的 \(n-m\) 个必须全错。

这种全错的排列问题,数学上有一个专门的名称——错排列。

与常见的计数问题不一样,错排列使用了一种特殊的求解方式——递推法。

\(D_i\) 表示 \(i\) 个元素作错排列的数量,初始条件 \(D_1=0, D_2=1\)

对于一般的 \(D_i\),考虑第 \(i\) 个元素放在哪里,设 \(i\) 放在下标为 \(k\) 的位置上 \((k \ne i)\) ,则这样的位置 \(k\)\(i-1\) 种选择。放完后,分两种情况讨论:

情况1:第 \(k\) 个元素放在下标为 \(i\) 的位置上

此时 \(i,k\) 与其他元素的放置没有任何的影响,所以现在问题转化为 \(i-2\) 个元素的错排列问题,答案就是 \(D_{i-2}\)

情况2:第 \(k\) 个元素不放在下标为 \(i\) 的位置上

我们令 \(p[k] \gets i\) ,也就是修改 \(k\) 的正确位置,那我们再对除 \(i\) 以外的东西做错排列即可,这样非常巧妙地解决了问题。

于是答案显而易见,为 \(D_{n-1}\)

综合上面两种情况,可以得到递推式:

\[D_{i}=(i-1)(D_{i-1}+D_{i-2}) \]

时间复杂度为 \(O(n)\)

当然,错排列问题也可以非常容易的用容斥原理求解出通项公式,但这样做并不能起到降低时间复杂度的效果,所以不知道也没关系。

#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=1e6+10, mod=1e9+7;
int f[N], fac[N], finv[N];
int n,m,ans;

inline int power(int x, int y){
	int ans=1;
	while(y){
		if(y&1) ans=ans*x%mod;
		x=x*x%mod;
		y>>=1;
	}
	return ans;
}

inline int C(int x, int y){
	return fac[x]*finv[y]%mod*finv[x-y]%mod;
}

inline void solve(){
	cin>>n>>m;
	if(n-m==1){cout<<"0\n"; return;}
	if(m==n){cout<<1<<"\n"; return;}
	if(m==0){cout<<f[n]<<"\n"; return;}
	cout<<C(n,m)*f[n-m]%mod<<"\n";
}

signed main(){
	cin.tie(0)->sync_with_stdio(0);
	f[2]=1; f[3]=2;
	for(int i=4; i<N; i++) f[i]=(i-1)*(f[i-2]+f[i-1])%mod;
	fac[0]=1;
	for(int i=1; i<N; i++) fac[i]=fac[i-1]*i%mod;
	finv[N-1]=power(fac[N-1], mod-2);
	for(int i=N-2; i>=0; i--) finv[i]=finv[i+1]*(i+1)%mod;
	int t; cin>>t;
	while(t--) solve();
	return 0;
}
posted @ 2026-06-01 20:50  Cute_lxy  阅读(13)  评论(0)    收藏  举报