CF285E Positions in Permutations 分析
题目概述
对于一个 \(n\) 的排列 \(p\) 定义好位置为满足 \(|p_i-i|=1\) 的位置,问恰好为 \(k\) 个好位置的方案。
分析
一看到这道题目,就感觉跟[AGC005D] ~K Perm Counting一样。
考虑容斥,设 \(F(m)\) 表示钦定了 \(m\) 个好位置剩下随便排的方案,\(G(m)\) 表示恰好为 \(m\) 个好位置。
那么根据二项式反演可以得到:
考虑怎么求 \(F(m)\)。
直接沿用那道题目的分成两部图的方法,然后进行连边,具体而言是左边的 \(i\) 连向右边的 \(i-1\) 和 \(i+1\),可以将左边的看作位置,右边的看作 \(p_i\)。
然后我们会依照连边拉出来两条边的数量为 \(n-1\) 的链,其实这样就可以直接 \(dp\) 了,但是我们不要,我们用组合数学的方法。
发现这两条链可以分别处理并且都是互不影响的,所以我们先关注一条链。
我们不能选择两条相邻的边,因为这样会导致他不知道在那个位置,这也就转化成了拥有 \(n-1\) 条边的链选择 \(k\) 条不相邻的边的方案数是多少。
我们考虑我们选择的序列为 \(a\)(长度为 \(k\)),那么他需要满足:对于任意 \(i\geq 2,a_{i+1}\geq a_i+2\)。
发现这个约束不好做,转化成相差为一的,套路地令 \(b_i\leftarrow a_i-(i-1)\),那么 \(b\) 满足:\(1\leq b_1<b_2<\dots<b_k\leq n-(k-1)=n-k+1\)。
这相当于在 \(1\) 到 \(n-k+1\) 这些数选择 \(k\) 个,最后排序就是 \(b\) 了。
综上所述,拥有 \(n-1\) 条边的链选择 \(k\) 条不相邻的边的方案数为 \(\binom{n-k+1}{k}\)。
我们现在要将两条链组合起来,设 \(f_m\) 表示总共选 \(m\) 条边的方案。
那么显然有:\(f_m=\sum_{i=0}^m\binom{n-1-i+1}{i}\binom{n-1-(m-i)+1}{m-i}\)。
那么有:\(F(m)=f_m(n-m)!\)。
然后反演求出 \(G(m)\) 即可。
代码
时间复杂度 \(\mathcal{O}(n^2)\)。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <stdlib.h>
#include <algorithm>
#include <vector>
#define int long long
#define N 1005
using namespace std;
const int mod = 1e9 + 7;
int jc[N],inv[N];
int C(int a,int b) {
if (a < 0 || b < 0 || a < b) return 0;
return jc[a] * inv[b] % mod * inv[a - b] % mod;
}
int f[N];
signed main(){
jc[0] = jc[1] = inv[0] = inv[1] = 1;
for (int i = 2;i < N;i ++) jc[i] = jc[i - 1] * i % mod,inv[i] = (mod - mod / i) * inv[mod % i] % mod;
for (int i = 2;i < N;i ++) inv[i] = inv[i - 1] * inv[i] % mod;
int n,k;
cin >> n >> k;
for (int i = 0;i <= n;i ++)
for (int j = 0;j <= i;j ++) f[i] = (f[i] + C(n - j,j) * C(n - (i - j),i - j) % mod) % mod;
int ans = 0;
for (int i = k,t = 1;i <= n;i ++,t = -t)
ans = (ans + (t * C(i,k) % mod * f[i] % mod * jc[n - i] % mod + mod) % mod) % mod;
cout << ans;
return 0;
}
分析2
当然的,也有 \(dp\) 做法,跟这个一开始的思路不太一样。
我们考虑直接 \(dp\),注意到第 \(i\) 个位置是否是好的只跟 \(i-1\) 和 \(i+1\) 有关。
不妨设 \(f_{i,j,0/1,0/1}\) 表示前 \(i\) 个位置有 \(j\) 个是好的,数字 \(i\) 有无被用,数字 \(i+1\) 有无被用。
转移是简单的。
- 当前位置是好的,那么可能放 \(i+1\),可能放 \(i-1\),看他们的数字有没有被用即可。
- 否则,不好。
代码2
时间复杂度 \(\mathcal{O}(n)\)。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <stdlib.h>
#include <algorithm>
#include <vector>
#define int long long
#define N 1005
using namespace std;
const int mod = 1e9 + 7;
int jc[N],inv[N];
int C(int a,int b) {
if (a < 0 || b < 0 || a < b) return 0;
return jc[a] * inv[b] % mod * inv[a - b] % mod;
}
void pls(int &x,int y) {
(x += y) %= mod;
}
int add(int x,int y){return (x + y) % mod;}
int f[N][N][2][2],n,k,F[N];
signed main(){
jc[0] = jc[1] = inv[0] = inv[1] = 1;
for (int i = 2;i < N;i ++) jc[i] = jc[i - 1] * i % mod,inv[i] = (mod - mod / i) * inv[mod % i] % mod;
for (int i = 2;i < N;i ++) inv[i] = inv[i - 1] * inv[i] % mod;
cin >> n >> k;
f[1][0][0][0] = f[1][1][0][1] = 1;
for (int i = 2;i <= n;i ++) {
f[i][0][0][0] = 1;
for (int j = 1;j <= i;j ++) {
f[i][j][0][0] = add(f[i - 1][j - 1][0][0],add(f[i - 1][j][0][0],f[i - 1][j][1][0]));
f[i][j][0][1] = add(f[i - 1][j - 1][0][0],f[i - 1][j - 1][1][0]);
f[i][j][1][0] = add(f[i - 1][j - 1][0][1],add(f[i - 1][j][1][1],f[i - 1][j][0][1]));
f[i][j][1][1] = add(f[i - 1][j - 1][0][1],f[i - 1][j - 1][1][1]);
}
}
f[n][0][0][0] = 1;
for (int i = 1;i <= n;i ++) {
f[n][i][0][0] = add(f[n - 1][i - 1][0][0],add(f[n - 1][i][0][0],f[n - 1][i][1][0]));
f[n][i][1][0] = add(f[n - 1][i - 1][0][1],add(f[n - 1][i][0][1],f[n - 1][i][1][1]));
}
for (int i = 0;i <= n;i ++) F[i] = (f[n][i][0][0] + f[n][i][1][0]) % mod * jc[n - i] % mod;
int ans = 0;
for (int i = k,t = 1;i <= n;i ++,t = -t)
ans = (ans + (t * C(i,k) % mod * F[i] % mod + mod) % mod) % mod;
cout << ans;
return 0;
}
后记
转移的三种情况
情况1:让位置 \(i\) 作为好位置,选择 \(p_i = i-1\)
选择 \(i-1\) 放在位置 \(i\)(即 \(p_i = i-1\))
条件:
数字 \(i-1\) 必须没有被占用(即前一个状态的第一个维度为0)
因为如果数字 \(i-1\) 被占用,说明它已经是某个 \(p_x = x±1\) 的目标值,不能再被选
转移:
f[i][j][0][0] += f[i-1][j-1][0][0] // i空闲,i+1空闲
f[i][j][1][0] += f[i-1][j-1][0][1] // i被占用,i+1空闲
这里第三个维度(\(i\) 被占用)取决于前一个状态的第二个维度。
情况2:让位置 \(i\) 作为好位置,选择 \(p_i = i+1\)
选择 \(i+1\) 放在位置 \(i\)(即 \(p_i = i+1\))
条件:
数字 \(i+1\) 必须没有被占用(但这里我们正在选择它,所以标记为被占用)
转移:
f[i][j][0][1] += f[i-1][j-1][0][0] + f[i-1][j-1][1][0] // i空闲,i+1被占用
f[i][j][1][1] += f[i-1][j-1][0][1] + f[i-1][j-1][1][1] // i被占用,i+1被占用
这里第二个维度(\(i+1\) 被占用)被设置为 \(1\)。
情况3:位置 \(i\) 不作为好位置
位置 \(i\) 可以放任意值,但不能是 \(i-1\) 或 \(i+1\)
转移:
f[i][j][0][0] += f[i-1][j][0][0] + f[i-1][j][1][0] // i空闲,i+1空闲
f[i][j][1][0] += f[i-1][j][0][1] + f[i-1][j][1][1] // i被占用,i+1空闲
这里状态继承前一个状态,但第三个维度变为 \(0\)(因为 \(i\) 不作为好位置的目标值)。
由于只考虑这特定 \(3\) 个位置的情况,\(i+1\) 是不能算过来的。

浙公网安备 33010602011771号