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 ∣Pi−i∣≥X for all integers i i i with 1 ≤ i ≤ N 1≤i≤N 1≤i≤N.
Constraints
- 1 ≤ N ≤ 100 1≤N≤100 1≤N≤100
- 1 ≤ X ≤ 5 1≤X≤5 1≤X≤5
- 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
∣Pi−i∣≥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 ∣Pi−i∣≥X 易得, P i ∈ ( ( − ∞ , i − X ] ∪ [ i + X , + ∞ ) ∩ [ 1 , n ] ) P_i\in((-\infty,i-X]\cup[i+X,+\infty)\cap[1,n]) Pi∈((−∞,i−X]∪[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∈(i−X,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
2X−1 ,即
s
<
(
1
<
<
(
2
X
−
1
)
)
s<(1<<(2X-1))
s<(1<<(2X−1)) )
于是可以形成
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(n−k)! 就是钦定
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(n−k)! ,其中
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=0n∑s(−1)jdp[n][j][s] 。本题得解,空间复杂度
O
(
n
2
2
2
X
−
1
)
O(n^22^{2X-1})
O(n222X−1) ,时间复杂度
O
(
n
2
2
2
X
−
1
(
2
X
−
1
)
)
O(n^22^{2X-1}(2X-1))
O(n222X−1(2X−1)) (相对于空间复杂度多出的部分源于上面的新状态
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(n24X−1) 。
于是我们的三维容斥状压动规如下:
#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));
}