题解:P8107 [Cnoi2021] 未来试题

P8107 [Cnoi2021] 未来试题

分析

考虑概率的转移不方便,所以我们考虑方案数。

首先我们可以暴力枚举每一种排列,但是这道题好像没有这个部分分。

我们考虑是否可以进行动态规划。

对于一个 \(dp_i\) 我们发现我们要考虑两个内容:

  1. 有哪些可能的逆序对个数;
  2. 产生某一个逆序对个数的方案数。

如果有两个要维护的内容,我们不容易考虑最优性问题,所以我们可以将其中一维放入状态中。

这里我们显然将逆序对个数放入状态更优。

那我们就有了状态,我们用 \(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\),由上面的式子我们可以得出:

\[y = j + (i - l) \mod k\\ \Rightarrow j = y - (i - l) \mod k \]

其中 \(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\) 的方案数,则:

\[p_{n,i} = \sum_{j=0}^{k - 1} p_{n-1, ((i - k) \mod n)} \cdot \frac{1}{n} \]

因为 \(n \leq k\),则我们将复杂度转化为 \(O(k^2)\)

完结撒花!

posted @ 2025-09-16 19:48  yanbinmu  阅读(7)  评论(0)    收藏  举报