题解 [AGC005D] ~K Perm Counting
首先可以想到容斥
但具体怎么容斥还是有点特别的
令 \(f(s)\) 为钦定 \(s\) 中的点满足 \(|a_i-i|=k\),剩下的点随便选的方案数
发现在一个排列中钦定一个 \(a_i\) 的值并不影响剩下的数做排列(即剩下的数的排列方案数是 \((n-1)!\))
那么就可以得到 \(ans=\sum (-1)^{|s|}g(|s|)(n-|s|)!\),其中 \(g(i)\) 为选出 \(i\) 个数使 \(|a_i-i|=k\) 的方案数
- 对于一个排列,\(i\) 与 \(a_i\) 形成二分图关系,可以借此处理一些 \(i\) 与 \(a_i\) 间的限制关系
若由 \(i\) 向 \(i-k, i+k\) 连边,则原图形成若干条链
现在要求选出 \(i\) 条边使每个点度数不超过 1 的方案数
可以想到对每条链分别 DP,最后背包合并
但其实也可以将所有链接在一起,强制连接处不能连边
令 \(f_{i, j, 0/1}\) 为到位置 \(i\),选了 \(j\) 条边,第 \(i\) 个点与第 \(i-1\) 个点间是否有边的方案数
转移显然,具体见这里
听说还能多项式优化到 \(O(n\log n)\)?
点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f3f
#define N 4010
#define ll long long
//#define int long long
char buf[1<<21], *p1=buf, *p2=buf;
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf, 1, 1<<21, stdin)), p1==p2?EOF:*p1++)
inline int read() {
int ans=0, f=1; char c=getchar();
while (!isdigit(c)) {if (c=='-') f=-f; c=getchar();}
while (isdigit(c)) {ans=(ans<<3)+(ans<<1)+(c^48); c=getchar();}
return ans*f;
}
int n, k;
bool ishead[N], vis[N];
const ll mod=924844033;
ll fac[N], f[N][N][2], ans;
int head[N], sta[N], ecnt, top;
struct edge{int to, next;}e[N<<1];
inline void add(int s, int t) {e[++ecnt]={t, head[s]}; head[s]=ecnt;}
void dfs(int u) {
vis[u]=1;
sta[++top]=u;
for (int i=head[u],v; ~i; i=e[i].next) dfs(e[i].to);
}
signed main()
{
n=read(); k=read();
memset(head, -1, sizeof(head));
fac[0]=fac[1]=1;
for (int i=1; i<=n; ++i) fac[i]=fac[i-1]*i%mod;
for (int i=1; i<=n; ++i) {
if (i-k>=1) add(i-k+n, i);
if (i+k<=n) add(i, i+k+n);
}
for (int i=1; i<=n; ++i) {
if (!vis[i]) ishead[top+1]=1, dfs(i);
if (!vis[i+n]) ishead[top+1]=1, dfs(i+n);
}
assert(top==n*2);
f[0][0][0]=1;
for (int i=1; i<=top; ++i) {
for (int j=0; j<=i; ++j) {
if (!ishead[i]) f[i][j][1]=(f[i][j][1]+f[i-1][j-1][0])%mod;
f[i][j][0]=(f[i][j][0]+f[i-1][j][1]+f[i-1][j][0])%mod;
}
}
for (int i=0; i<=n; ++i) ans=(ans+(i&1?-1:1)*(f[top][i][0]+f[top][i][1])*fac[n-i])%mod;
cout<<(ans%mod+mod)%mod<<endl;
return 0;
}