【CF 285E】Positions in Permutations(容斥原理/二项式反演)

洛谷链接

题目描述

称一个 1 n 1\sim n 的排列的完美数为有多少个 i i 满足 P i i = 1 |P_i-i|=1
求有多少个长度为n 的完美数恰好为 m 的排列。

Sol

f ( k ) f(k) 表示长度为 n n 的排列恰好有 k k 个完美数的方案数
但是发现不好算 , 我们先往容斥上想

F ( k ) F(k) 表示至少有 k k 个的方案
先考虑一个dp , 我们考虑当前位置上的数是不是要是幸运数

f [ i ] [ j ] [ 0 / 1 ] [ 0 / 1 ] f[i][j][0/1][0/1] 表示考虑到了第 i i 位 , 至少已经有了 j j 个幸运数 , i i i + 1 i+1 是否已经被用于充当幸运数 , 其他的位置不予考虑的方案数

为什么要这样设? 因为我们只考虑当前为就很好统计个数 , 然后当前位要成为幸运数就只能是 i + 1 i+1 i 1 i-1 , 所以记录一下有没有用过

我们先自己 YY 一个转移(我这里卡常,数组换了维):

转移代码:

for(register int i=2;i<=n;++i){
	for(register int j=0;j<i;++j){
		upd(dp[0][0][i][j],dp[0][0][i-1][j]);
		upd(dp[0][0][i][j],dp[1][0][i-1][j]);
		upd(dp[1][0][i][j],dp[0][1][i-1][j]);
		upd(dp[1][0][i][j],dp[1][1][i-1][j]);//不钦定当前位置
		
		//钦定 i+1 过来
		if(i<n){
			upd(dp[0][1][i][j+1],dp[0][0][i-1][j]);
			upd(dp[0][1][i][j+1],dp[1][0][i-1][j]);
			upd(dp[1][1][i][j+1],dp[0][1][i-1][j]);
			upd(dp[1][1][i][j+1],dp[1][1][i-1][j]);
		}
		//钦定了 i-1 过来
		upd(dp[0][0][i][j+1],dp[0][0][i-1][j]);
		upd(dp[1][0][i][j+1],dp[0][1][i-1][j]);
	}
}

这个还是很好想的 , 最后由于不考虑的随便排列所以要乘上一个阶乘
于是答案是 f [ n ] [ k ] f [ n ] [ k + 1 ] f[n][k]-f[n][k+1] ??

显然我们多算了方案
假设我们的钦定法要确定 j j 个是幸运数 , 那么可能我们决策时 钦定了 不同的 j j 个,但是排列后却可能和之前已经钦定过 j j 个的方案产生重复 , 从而把
对于要求的 f ( k ) f(k) 来说 , 我们会算 C ( j , k ) C(j,k) f [ n ] [ j ] f[n][j] ,也就是任意选 k k 个确定后 , 每次都包括了所有大于 k k 个幸运数的方案
所以以 C ( j , k ) C(j,k) 为容斥系数就行了

(其实我自己也很懵逼)

实在难以理解就二项式反演吧 , 上面的容斥其实就是二项式反演

具体来说 , 普通的二项式反演是这样的:
F ( n ) = k = 0 n C ( n , k ) f ( k ) f ( n ) = k = 0 n ( 1 ) n k C ( n , k ) F ( k ) F(n)=\sum_{k=0}^{n} C(n,k) f(k)\\ f(n)=\sum_{k=0}^n (-1)^{n-k} C(n,k) F(k)

证明就是倒推…

和莫比乌斯反演一样 , 这种形式是做不了题的 , 有另外一种形式:
F ( n ) = k = n + C ( k , n ) f ( k ) f ( n ) = k = n + ( 1 ) k n C ( k , n ) F ( k ) F(n)=\sum_{k=n}^{+ \infty } C(k,n)f(k)\\ f(n)=\sum_{k=n}^{+ \infty} (-1)^{k-n} C(k,n) F(k)

那么 f ( k ) f(k) 表示那个恰好有 k k 个幸运数的方案数

思考一下 F ( k ) F(k) 的意义

由于我们是至少 , 很容易发现就是把多于 k k 的都算了组合数把么多次 , 正好就是我们钦定法中算重的东西 , 于是就做完了 (神仙题)

代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<cstdlib>
#include<cmath>
#include<set>
using namespace std;
int n,m;
const int N=1001;
const int mod=1e9+7;
inline void upd(int &x,int y){x+=y;if(x>=mod) x-=mod;}
inline int sum(int x,int y){x+=y;if(x>=mod) x-=mod;return x;}
int dp[2][2][N][N];//前 i 个位置 至少有 j 个 最后一个位置 i和i+1 有没有已经被钦定 的方案数.... 瞎算算情况下的方案数
// 只考虑当前位置放数的时候产生的幸运数
int fac[N],inv[N];
inline int fpow(int x,int k)
{
	register int ret=1;
	while(k){
		if(k&1) ret=1ll*ret*x%mod;
		x=1ll*x*x%mod;k>>=1;
	}
	return ret;
}
inline int C(int i,int j){return 1ll*fac[i]*inv[j]%mod*inv[i-j]%mod;}
int main()
{
	scanf("%d %d",&n,&m);
	dp[0][0][1][0]=1;
	dp[0][1][1][1]=1;
	for(register int i=2;i<=n;++i){
		
		for(register int j=0;j<i;++j){
			upd(dp[0][0][i][j],dp[0][0][i-1][j]);
			upd(dp[0][0][i][j],dp[1][0][i-1][j]);
			upd(dp[1][0][i][j],dp[0][1][i-1][j]);
			upd(dp[1][0][i][j],dp[1][1][i-1][j]);//不钦定当前位置
			
			//钦定 i+1 过来
			if(i<n){
				upd(dp[0][1][i][j+1],dp[0][0][i-1][j]);
				upd(dp[0][1][i][j+1],dp[1][0][i-1][j]);
				upd(dp[1][1][i][j+1],dp[0][1][i-1][j]);
				upd(dp[1][1][i][j+1],dp[1][1][i-1][j]);
			}
			//钦定了 i-1 过来
			upd(dp[0][0][i][j+1],dp[0][0][i-1][j]);
			upd(dp[1][0][i][j+1],dp[0][1][i-1][j]);
		}
	}
	fac[0]=1;
	for(register int i=1;i<=n;++i) fac[i]=1ll*fac[i-1]*i%mod;
	inv[0]=1;inv[n]=fpow(fac[n],mod-2);
	for(register int i=n-1;i;--i) inv[i]=(1ll*inv[i+1]*(i+1))%mod;
	register int ans=0;
	for(register int i=m,p=1;i<=n;++i,p=-p){//二项式反演
		register int S=1ll*sum(dp[0][0][n][i],dp[1][0][n][i])*fac[n-i]%mod;
		ans+=(1ll*S*C(i,m)%mod*p+mod)%mod;
		if(ans>=mod) ans-=mod;
	}
	printf("%d\n",ans);
}

猜你喜欢

转载自blog.csdn.net/element_hero/article/details/82808101