AtCoder Beginner Contest 309G

G - Ban Permutation

Problem Statement

Find the number, modulo 998244353 998244353 998244353, of permutations P = ( P 1 , P 2 , … , P N ) P=(P_1,P_2,…,P_N) P=(P1,P2,,PN) of ( 1 , 2 , … , N ) (1,2,…,N) (1,2,,N) such that:

  • ∣ P i − i ∣ ≥ X ∣P_i−i∣≥X Pii∣≥X for all integers i i i with 1 ≤ i ≤ N 1≤i≤N 1iN.

Constraints

  • 1 ≤ N ≤ 100 1≤N≤100 1N100
  • 1 ≤ X ≤ 5 1≤X≤5 1X5
  • All input values are integers.

Input

The input is given from Standard Input in the following format:

N X

Output

Print the answer.


Sample Input 1

3 1

Sample Output 1

2

The conforming permutations P = ( P 1 , P 2 , P 3 ) P=(P_1,P_2,P_3) P=(P1,P2,P3) are the following two, ( 2 , 3 , 1 ) (2,3,1) (2,3,1) and ( 3 , 1 , 2 ) (3,1,2) (3,1,2), so the answer is 2 2 2.


Sample Input 2

5 2

Sample Output 2

4

Sample Input 3

98 5

Sample Output 3

809422418

题目大意:

题目很短嚎,就是求 1 1 1 N N N 的排列中 ∣ | 数值与下标之差 ∣ | 都小于等于 X X X ( ∣ P i − i ∣ ≥ X ∣P_i−i∣≥X Pii∣≥X )的数量;
数据也很小嚎, N N N 只有 100 100 100 X X X 更是只有 5 5 5
一眼定真,这题大概率是排列组合和状压 dp \text{dp} dp

分析:

相信我,at的题解只会比这更抽象,因为ta题解默认每一个看的人都见过类似算法。

可以将数字排列的过程看成是往表格里填数的过程,由 ∣ P i − i ∣ ≥ X ∣P_i−i∣≥X Pii∣≥X 易得, P i ∈ ( ( − ∞ , i − X ] ∪ [ i + X , + ∞ ) ∩ [ 1 , n ] ) P_i\in((-\infty,i-X]\cup[i+X,+\infty)\cap[1,n]) Pi((,iX][i+X,+)[1,n]) ,可以看到 n n n 并没有那么小实际枚举情况将较为复杂,于是可以考虑 P i ∉ ( i − X , i + X ) ∩ [ 1 , n ] P_i\not\in(i-X,i+X)\cap[1,n] Pi(iX,i+X)[1,n] (下面称这个范围为“禁区”),即考虑数处在禁区中的方案数,形式较为简单,这是正难则反的思想。

如果我们使用搜索来填数,我们需要如下几个参数:
i i i ,即当前填到了第几个数;
j j j ,已经填过的数字中有多少在其禁区中;
s s s ,该位置的禁区的覆盖情况,使用状压来实现( 0 0 0 为空位, 1 1 1 非空位,低位为序号更小的位置以方便转移)( X X X 最大为 5 5 5 ,一个点的禁区范围最大为 2 X − 1 2X-1 2X1 ,即 s < ( 1 < < ( 2 X − 1 ) ) s<(1<<(2X-1)) s<(1<<(2X1))

于是可以形成 dp \text{dp} dp d p [ i ] [ j ] [ s ] dp[i][j][s] dp[i][j][s] 来记录所有状态,转移方程为
d p [ i + 1 ] [ j ] [ s > > 1 ] + = d p [ i ] [ j ] [ s ] dp[i+1][j][s>>1]+=dp[i][j][s] dp[i+1][j][s>>1]+=dp[i][j][s]
d p [ i + 1 ] [ j + 1 ] [ T ] + = d p [ i ] [ j ] [ s ] dp[i+1][j+1][T]+=dp[i][j][s] dp[i+1][j+1][T]+=dp[i][j][s] ,其中 T T T s > > 1 s>>1 s>>1 将其中一位 0 0 0 赋为 1 1 1 的新状态。
边界 d p [ 0 ] [ 0 ] [ 0 ] = 1 ,   d p [ 0 ] [ 0 ] [ other ] = 0 dp[0][0][0]=1,~dp[0][0][\text{other}]=0 dp[0][0][0]=1, dp[0][0][other]=0

但是有人就问了,这不着调的动规完全背离了实际,因为不仅可能有空位而且只考虑自己禁区的填充情况,在哪?考虑下面一个问题:
n n n 个数放入一些格子中,其中有一些格子是不能放的,令 k k k 个数在禁区时的方案数为 w k w_k wk w k ( n − k ) ! w_k(n-k)! wk(nk)! 就是钦定 k k k 个数处于禁区的方案数(不考虑其他数在不在禁区,是容斥原理的方便之处),因为剩下的数可以随便排列。则 0 0 0 个数处于禁区的方案数为 ∑ k = 0 n ( − 1 ) k w k ( n − k ) ! \sum_{k=0}^{n}(-1)^kw_k(n-k)! k=0n(1)kwk(nk)! ,其中 k = 0 k=0 k=0 时显然系数为正,所以偶正奇负。

(注:我们动规是为了在禁区填数,重点不在考虑哪些禁区,而是考虑确定总共有多少个数在禁区(可能存在不确定的数在禁区),这么多数在禁区时的方案数有多少)

我们会发现这两个问题惊人地相似:都存在禁区。容斥可做。
由于本题的禁区的随点而定性,不能够使用排列组合得出所谓 w k w_k wk ,所以上面讲到的动规有了意义:钦定 j j j 个数在禁区时这 j j j 个数总排列方式数,所以最终结果为 ∑ j = 0 n ∑ s ( − 1 ) j d p [ n ] [ j ] [ s ] \sum_{j=0}^{n}\sum_s(-1)^jdp[n][j][s] j=0ns(1)jdp[n][j][s] 。本题得解,空间复杂度 O ( n 2 2 2 X − 1 ) O(n^22^{2X-1}) O(n222X1) ,时间复杂度 O ( n 2 2 2 X − 1 ( 2 X − 1 ) ) O(n^22^{2X-1}(2X-1)) O(n222X1(2X1)) (相对于空间复杂度多出的部分源于上面的新状态 T T T 的计算),小于 5 × 1 0 7 5\times10^7 5×107 ,可过。

此题仍有效率更高的方法,将填数的过程看作是两个点集 ( 1 , 2 , … , n ) (1,2,\dots,n) (1,2,,n) ( 1 , 2 , … , n ) (1,2,\dots,n) (1,2,,n) 间连边,要求每个点都被不重复不遗漏地连到,而且每条边连接的两个点的值之差要大于等于 X X X ,问题转换为二分图匹配,网络流可做,at原站上的非官方题解有提到,时间复杂度为 O ( n 2 4 X − 1 ) O(n^24^{X-1}) O(n24X1)

于是我们的三维容斥状压动规如下:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define md 998244353//模上大质数!
int n,x,dp[105][105][1<<9]={1};//初始化
ll ftr[105],ans=0;//这两个涉及到乘法运算,需要开到longlong
int main(){
	cin>>n>>x;
	ftr[0]=1;//0的阶乘为1
	for(int i=1;i<=n;++i)
		ftr[i]=ftr[i-1]*i%md;//预处理出阶乘
	for(int i=0;i<n;++i)//这里写的是我为人人的dp,即用已知状态主动去更新未知状态,这里是用i的状态去更新i+1
	for(int j=0;j<=i;++j)//意义同分析
	for(int s=0;s<(1<<(2*x-1));++s){//意义同分析
		if(!dp[i][j][s])//如果该状态方案数为0,更新等于没更新,直接跳出,对于0很多的情况有一定帮助
			continue;
		(dp[i+1][j][s>>1]+=dp[i][j][s])%=md;//此处采用取模+=和%=配合的取模方式
		for(int pos=max(1,i-x+2);pos<=min(n,i+x);++pos){//这枚举的是位置i+1的禁区,注意下标的界限
			short dif=pos-(i-x+2);//这是放在状态s中的位数
			if((s>>1)&(1<<dif))//如果位置i的状态为s,那么位置i+1的状态为s>>1,只有禁区中尚未被占据的地方可以放数
				continue;
			(dp[i+1][j+1][(s>>1)|(1<<dif)]+=dp[i][j][s])%=md;//出现新的数在禁区中,注意第二维为j+1
		}
	}for(int i=0;i<=n;++i)//注意边界
		for(int s=0;s<(1<<(2*x-1));++s){//意义同上
			if(i%2==0)//分析中的容斥
				ans=(ans+dp[n][i][s]*ftr[n-i]%md)%md;//注意乘法最好一步一模,相当容易爆
			else
				ans=(ans-dp[n][i][s]*ftr[n-i]%md)%md;//这里采取的是更常见的取模处理方式,-ftr[n-i]是负数,可以转换为+(md-ftr[n-i]),则不需要下面一行的处理负数
		}ans=(ans+md)%md;//ans模了那个大质数,可能造成大数变小,而使减法运算中出现负数,由同余性质易得可+md再%md
	cout<<ans<<endl;//我们终于得到了答案
	return 0;
}

at上的二分图匹配(https://atcoder.jp/contests/abc309/editorial/6767):

其中 __builtin_popcountll(x) 以 long long \text{long long} long long 型返回二进制的 x x x 中有多少位 1 1 1

#include<stdio.h>
#include<string.h>
#define mod 998244353
#define int long long
int n,m,mm,f[109][109][1<<8];
inline int dfs(const int&i,const int&j,const int&s)
{
	if(j>>31)return 0;
	if(i==n)return!j;
	if(~f[i][j][s])return f[i][j][s];
	f[i][j][s]=0;
	f[i][j][s]=(f[i][j][s]+dfs(i+1,j+1,s<<2&mm))%mod;
	f[i][j][s]=(f[i][j][s]+(j-__builtin_popcountll(~s&mm&2+8+32+128))*
		dfs(i+1,j,(s<<2&mm)|1))%mod;
	f[i][j][s]=(f[i][j][s]+(j-__builtin_popcountll(~s&mm&1+4+16+64))*
		dfs(i+1,j,(s<<2&mm)|2))%mod;
	f[i][j][s]=(f[i][j][s]+(j-__builtin_popcountll(~s&mm&2+8+32+128))*
		(j-__builtin_popcountll(~s&mm&1+4+16+64))*
		dfs(i+1,j-1,(s<<2&mm)|3))%mod;
	return f[i][j][s];
}
main()
{
	memset(f,-1,sizeof(f));
	scanf("%lld%lld",&n,&m);mm=(1<<m-1+m-1)-1;
	if(m>=n){putchar('0');return 0;}
	printf("%lld",dfs(m-1,m-1,0));
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值