#include <bits/stdc++.h>
#define endl "\n"
using namespace std;
typedef long long ll;
const ll mod = 998244353;
const int maxn = 5e3 + 10;
int n, a[maxn];
int cnt[maxn], sum[maxn], pos[maxn];
ll inv[maxn], dp[maxn][maxn];//dp[i][j]代表上一次自己选了第i小的,对方刚选j,到自己选的期望伦次
ll qpow(ll a, ll b) {
ll res = 1;
while (b) {
if (b & 1) res = res * a % mod;
a = a * a % mod;
b >>= 1;
}
return res;
}
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i], pos[a[i]] = i; //存每个数的位置,因为是1~n的排列
for (int i = 1; i <= n; i++) inv[i] = qpow(i, mod - 2); //计算逆元
//从大往小更新dp,因为答案在dp[0][i]
for (int i = n; i >= 1; i--) { //选第i小的,之后别人只能选比自己大的数
memset(sum, 0, (n + 1) * sizeof(int)); //初始化
memset(cnt, 0, (n + 1) * sizeof(int)); //初始化
for (int j = i + 1; j <= n; j++) { //选第j小的,要比i大
cnt[pos[j]] = 1; //将比i大的位置赋值为1,方便等会能够利用后缀和来统计有多少个可以选
sum[pos[j]] = dp[i][j] % mod; //当前可选的期望轮次
}
for (int j = n - 1; j >= 0; j--) cnt[j] = (cnt[j + 1] + cnt[j]) % mod; //个数的后缀和
for (int j = n - 1; j >= 0; j--) sum[j] = (sum[j + 1] + sum[j]) % mod; //期望轮次的后缀和
for (int j = 0; j < i; j++) { //利用后缀和来更新dp[j][i],dp[j][i]下一轮就变成了合法的dp[i][k]
int t = cnt[pos[j]]; //t代表可选个数
int s = sum[pos[j]]; //s代表可以到下一步的期望轮次和
if (t) dp[j][i] = (s * inv[t] % mod + 1) % mod; //每次+1是因为是合法轮次,所以要加1
}
}
int ans = 0;
for (int i = 1; i <= n; i++) ans = (ans + dp[0][i]) % mod; //dp[0][i]即代表上一次自己选了第0小的,对方可以随便选i,即刚开始的时候
ans = ans * inv[n] % mod; //
ans = (ans + 1) % mod; //
cout << ans << endl;
}
2021牛客暑期多校训练营1 Increasing Subsequence
最新推荐文章于 2022-02-23 10:03:06 发布
这篇博客介绍了一个关于期望轮次的数学问题,通过动态规划求解。代码中展示了如何利用逆元和后缀和技巧计算在1到n的排列中,选择特定数字的期望轮次。算法涉及到了高精度快速幂运算,并给出了解决此类问题的思路和实现细节。
摘要由CSDN通过智能技术生成