【做题】CF285E. Positions in Permutations——dp+容斥
题意:求所有长度为\(n\)的排列\(p\)中,有多少个满足:对于所有\(i \,(1 \leq i \leq n)\),其中恰好有\(k\)个满足\(|p_i - i| = 1\)。答案对\(10^9 + 7\)取模。
\(n \leq 10^3\)
首先,让我们考虑这个类似反演的结论:
对于\(F(n)\)和\(f(n)\),则满足
\[F(n) = \sum_{k \geq n}{{k}\choose{n}}f(k) \iff f(n) = \sum_{k \geq n}(-1)^{k-n}{{k}\choose{n}}F(k) \]
对于充分性,我们有
\[\begin{aligned} & \sum_{k \geq n}(-1)^{k-n}{{k}\choose{n}}F(k) \\
= & \sum_{k \geq n}(-1)^{k-n}{{k}\choose{n}} \sum_{j \geq k} {{j}\choose{k}} f(j) \\
= & \sum_{j \geq n} f(j) {{j}\choose{n}} \sum_{n \leq k \leq j} (-1)^{k-n} {{j-n}\choose{k-n}} \\
= & \sum_{j \geq n} f(j) {{j}\choose{n}} \sum_{0 \leq k \leq j-n} (-1)^k {{j-n}\choose{k}} \\
= & \sum_{j \geq n} f(j) {{j}\choose{n}} (1-1)^{j-n} \\
= & f(n)\end{aligned}
\]
而对于必要性,我们也能给出类似的证明。
观察这个结论,不难发现,我们平时使用的容斥就是这个结论求\(f(0)\)时的特殊情况。而现在我们要求的是\(f(k)\),问题就变成了把所有\(F(n)\)都求出来。
\(F(n)\)的定义正对应了我们求至少有\(k\)个的情况总数时,重复统计所得到的结果。这使得它可以比较容易地求出。
我们考虑权值和位置表示为二分图的形式,那么问题就在于求二分图恰好有\(k\)个匹配的方案数。考虑dp。设dp[i,j,a,b]表示当前匹配到第\(i\)个权值和位置,已经有\(j\)个匹配,并且\(a\)和\(b\)分别表示第\(i\)个位置与权值是否已经与编号更小的权值与位置匹配。通过枚举\(a\)和\(b\),很容易能得到dp的转移。当然,最后的答案还要乘以一个阶乘。
时间复杂度\(O(n^2)\)。
#include <bits/stdc++.h>
using namespace std;
const int N = 1010, MOD = 1000000007;
typedef long long ll;
ll power(ll a,int b) {
ll res = 1;
while (b) {
if (b&1) res = 1ll * res * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return res;
}
int n,k;
ll dp[N][N][2][2],jc[N],inv[N],ans;
ll comb(int a,int b) {
if (a < 0 || b < 0 || a < b)
return 0;
return jc[a] * inv[b] % MOD * inv[a-b] % MOD;
}
int main() {
scanf("%d%d",&n,&k);
dp[0][0][1][1] = 1;
for (int i = 1 ; i <= n ; ++ i)
for (int j = 0 ; j <= n ; ++ j) {
(dp[i][j][0][0] = dp[i-1][j][0][0] + dp[i-1][j][1][0] + dp[i-1][j][0][1] + dp[i-1][j][1][1]) %= MOD;
if (j >= 1) {
(dp[i][j][1][0] = dp[i-1][j-1][0][0] + dp[i-1][j-1][1][0]) %= MOD;
(dp[i][j][0][1] = dp[i-1][j-1][0][0] + dp[i-1][j-1][0][1]) %= MOD;
}
if (j >= 2)
dp[i][j][1][1] = dp[i-1][j-2][0][0];
}
jc[0] = 1;
for (int i = 1 ; i <= n ; ++ i)
jc[i] = 1ll * i * jc[i-1] % MOD;
inv[n] = power(jc[n],MOD-2);
for (int i = n-1 ; i >= 0 ; -- i)
inv[i] = 1ll * inv[i+1] * (i+1) % MOD;
for (int j = k, p = 1 ; j <= n ; ++ j, p = -p) {
ll v = 1ll * jc[n-j] * (dp[n][j][0][0] + dp[n][j][1][0] + dp[n][j][0][1] + dp[n][j][1][1]) % MOD;
(ans += p * comb(j,k) * v % MOD) %= MOD;
}
ans = (ans % MOD + MOD) % MOD;
cout << ans << endl;
return 0;
}
小结:这种类似于反演的东西是很多组合问题的通用方法,希望自己能实现灵活的运用。