题解:P8107 [Cnoi2021] 未来试题
P8107 [Cnoi2021] 未来试题
分析
考虑概率的转移不方便,所以我们考虑方案数。
首先我们可以暴力枚举每一种排列,但是这道题好像没有这个部分分。
我们考虑是否可以进行动态规划。
对于一个 \(dp_i\) 我们发现我们要考虑两个内容:
- 有哪些可能的逆序对个数;
- 产生某一个逆序对个数的方案数。
如果有两个要维护的内容,我们不容易考虑最优性问题,所以我们可以将其中一维放入状态中。
这里我们显然将逆序对个数放入状态更优。
那我们就有了状态,我们用 \(i\) 表示枚举到了前 \(i\) 个,则 \(\displaystyle dp_{i, x}\) 表示前 \(i\) 个数全排列,全排列中逆序的个数对 \(k\) 取模后,余数为 \(x\) 的方案数。
那我们可以怎么转移呢?
首先,我们发现如果我们有了前 \(i - 1\) 的全排列,如果我再加入一个数 \(i\),那他一定比其中任意一个数都大,那我们可以考虑将 \(i\) 插到哪里,他会比后面的数都大,所以后面有多少个数,我们会多出来多少对逆序对,我们就可以枚举第 \(i\) 个数插到哪里,然后更新答案。
时间复杂度:\(O(n^2k)\)
空间给的比较小,我们挂一个滚动数组。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int Mod = 998244353;
const int N = 1e5;
int qpow(int a, int b) {
int res = 1;
while(b) {
if(b & 1) {
res *= a;
res %= Mod;
}
a *= a;
a %= Mod;
b >>= 1;
}
return res;
}
int f[2][1005];
signed main() {
int n, k;
cin >> n >> k;
if(n >= k) {
for(int i = 1;i <= k;i ++ ) {
cout << qpow(k, Mod - 2) << " " ;
}
return 0;
}
int fac = 1;
for(int i = 1;i <= n;i ++ ) {
fac *= i;
fac %= Mod;
}
fac = qpow(fac, Mod - 2);
f[1][0] = 1;
for(int i = 2;i <= n;i ++ ) {
int u = (i & 1);
int v = (u ^ 1);
// cout << u << " " << v << "\n";
for(int j = 0;j < k;j ++ ) {
for(int l = 1;l <= i;l ++ ) {
f[u][(j + (i - l)) % k] += f[v][j];
f[u][(j + (i - l)) % k] %= Mod;
}
}
for(int j = 0;j < k;j ++ ) f[v][j] = 0; // 记得清空!!!
}
for(int i = 0;i < k;i ++ ) {
cout << f[n & 1][i] * fac % Mod << " ";
}
return 0;
}
时间复杂度爆炸,可以获得 33pts。
我们看看可以怎么优化。
我们发现我们一个状态只会推向 \(k\) 个状态,那么我们可以考虑一个状态能推向什么状态,这样我们可以将时间复杂度优化为 \(O(nk^2)\)。
如果是由 \(j\) 推向 \(y\),由上面的式子我们可以得出:
其中 \(l \in [1, i]\),所以 \(j\) 的下界是 \(y - (i - 1) \mod k\),上界是 \(y\)。
那我们就知道了从哪些状态转移过来,应注意的是,由于取模的原因,下界可能比 \(y\) 要大,要特判一下。
时间复杂度:\(O(nk^2)\)。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int Mod = 998244353;
const int N = 1e5;
int qpow(int a, int b) {
int res = 1;
while(b) {
if(b & 1) {
res *= a;
res %= Mod;
}
a *= a;
a %= Mod;
b >>= 1;
}
return res;
}
int f[2][1005];
signed main() {
int n, k;
cin >> n >> k;
if(n >= k) {
for(int i = 1;i <= k;i ++ ) {
cout << qpow(k, Mod - 2) << " " ;
}
return 0;
}
int fac = 1;
for(int i = 1;i <= n;i ++ ) {
fac *= i;
fac %= Mod;
}
fac = qpow(fac, Mod - 2);
f[1][0] = 1;
for(int i = 2;i <= n;i ++ ) {
int u = (i & 1);
int v = (u ^ 1);
// cout << u << " " << v << "\n";
for(int j = 0;j < k;j ++ ) {
int d = ((j - i + 1) % k + k) % k; // d ~ j
if(d <= j) {
for(int l = d;l <= j;l ++ ) {
f[u][j] += f[v][l];
f[u][j] %= Mod;
}
}
else {
for(int l = d;l < k;l ++ ) {
f[u][j] += f[v][l];
f[u][j] %= Mod;
}
for(int l = 0;l <= j;l ++ ) {
f[u][j] += f[v][l];
f[u][j] %= Mod;
}
}
}
for(int j = 0;j < k;j ++ ) f[v][j] = 0;
}
for(int i = 0;i < k;i ++ ) {
cout << f[n & 1][i] * fac % Mod << " ";
}
return 0;
}
获得 67pts。
再次发现,我们每次都是由一整段的东西之和转移给过来,我们可以用前缀和优化。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int Mod = 998244353;
const int N = 1e5;
int qpow(int a, int b) {
int res = 1;
while(b) {
if(b & 1) {
res *= a;
res %= Mod;
}
a *= a;
a %= Mod;
b >>= 1;
}
return res;
}
int f[2][1005];
int sum[2][1005];
signed main() {
int n, k;
cin >> n >> k;
if(n >= k) {
for(int i = 1;i <= k;i ++ ) {
cout << qpow(k, Mod - 2) << " " ;
}
return 0;
}
int fac = 1;
for(int i = 1;i <= n;i ++ ) {
fac *= i;
fac %= Mod;
}
fac = qpow(fac, Mod - 2);
f[1][0] = 1;
for(int j = 0;j < k;j ++ ) {
sum[1][j] = sum[1][j - 1] + f[1][j];
}
for(int i = 2;i <= n;i ++ ) {
// sum[i][0] = 0;
int u = (i & 1);
int v = (u ^ 1);
// cout << u << " " << v << "\n";
for(int j = 0;j < k;j ++ ) {
int d = ((j - i + 1) % k + k) % k; // d ~ j
if(d <= j) {
// for(int l = d;l <= j;l ++ ) {
// f[u][j] += f[v][l];
// f[u][j] %= Mod;
// }
f[u][j] += sum[v][j] - sum[v][d - 1];
f[u][j] %= Mod;
}
else {
// for(int l = d;l < k;l ++ ) {
// f[u][j] += f[v][l];
// f[u][j] %= Mod;
// }
// for(int l = 0;l <= j;l ++ ) {
// f[u][j] += f[v][l];
// f[u][j] %= Mod;
// }
f[u][j] += sum[v][k - 1] - sum[v][d - 1];
f[u][j] %= Mod;
f[u][j] += sum[v][j];
f[u][j] %= Mod;
}
sum[u][j] = sum[u][j - 1] + f[u][j];
sum[u][j] %= Mod;
}
for(int j = 0;j < k;j ++ ) f[v][j] = 0;
}
for(int i = 0;i < k;i ++ ) {
cout << f[n & 1][i] * fac % Mod << " ";
}
return 0;
}
时间复杂度:\(O(nk)\)。
但是会 T 一个点。
再考虑优化,我们回归原始,考虑加入一个数时逆序对数的变化,同上文所言,他会多出插入位置后面的元素个数个逆序对。
若设 \(p_{n,i}\) 表示在 \(n\) 个元素的排列中,逆序数 \(\mod n\) 等于 \(i\) 的方案数,则:
因为 \(n \leq k\),则我们将复杂度转化为 \(O(k^2)\)。
完结撒花!

浙公网安备 33010602011771号