这可能是我目前做到的最猛的魔法 (dp) 之一了。
首先我们不妨先假设我们已经得到了一个操作序列。
因此,我们可以定义对某一个元素的最后一次移动,我们称它为该元素的关键操作。
我们不妨假设我们已经对某两个元素完成了关键操作。
则在当前序列中不在这两个元素之间的元素不能够成为这两个元素之间的元素,换句话说这两个元素间的元素若在目标序列中也在这两个元素之间则他们必然已经完成了他们的关键操作,或者他们完全不用操作。
因此我们得到了结论一:完成关键操作的元素在目标序列中是连续的。
我们考虑那些完全不用操作的元素可能在哪些位置。
我们不妨考虑第一个完成向左的关键操作的元素,则在目标序列中在该元素左边的元素必然需要操作。
第一个完成向右的关键操作的元素同理。
则最后我们发现在第一个完成向左的关键操作的元素和第一个向右的之间的元素是不用操作的。则在目标序列中,这些元素相对位置和原序列中一样,即这些元素单调递增。
我们不妨枚举目标序列中间的一段元素单调递增的区间,由性质一可以得到区间左边这些元素发生关键操作顺序和右边发生关键操作顺序,因此我们可以采用 (dp) 处理。
定义 (dp_{i,l,r}) 表示进行 (i) 个操作后,在目标序列所选定的区间左边的元素中还有 (l) 个没有发生关键操作,右边还有 (r) 个没有发生关键操作。
则有转移方程:
答案是 (dp_{m,0,0})。
于是我们得到了 (O(n^3 imes m)) 的优秀做法。
我们发现我们每次 (dp) 的转移类似,我们考虑一次性 (dp) 预处理出所以可能用到的值。
因此类似的,我们可以定义 (dp_{i,l,r}) 在目标序列所选定的区间左边的元素中还有 (l) 个没有发生关键操作,右边还有 (r) 个没有发生关键操作的一种序列变成 , 在 (i) 次内变成目标序列的方案数。
这次我们考虑倒序还原。
我们考虑当前的情况是如何得到的,考虑正序时这一步不是任何一个元素的关键操作,则得到这样一个情况我们上一步有 ((l+r) imes 2) 个选择。
若正序时这一步是关键操作,则正序时这一步到当前只有唯一一种方式。
则有方程:
对于这个方程我们可以理解为上一个序列是已经确定的,当前这一序列是不确定的,但无论当前长什么样,推到目标序列步数都是一样的,所以不影响。
于是我们得到了 (O(n^2m + n^2)) 的优秀算法。
考虑继续优化,我们发现第二个转移方程和第三个相似,我们不妨将后两维其压缩得到 (f_{i,l+r}) 。转移类似,我们考虑但是实际上, (f) 计算值是只考虑了发生关键操作与否的顺序,而没有考虑左右关键操作的顺序,因此需要额外计算,于是有 (dp_{i,l,r} = f_{i,l+r} imes C(l+r,l)) 。
最后我们得到了 (O(nm + n^2)) 的优秀算法,满足题目要求。
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long LL;
const LL mod = 998244353;
LL dp[3005][3005] , n , m , a[3005] , C[3005][3005];
void Add(LL &x , LL y) {x = (x + y) % mod;}
int main() {
scanf("%lld %lld" , &n , &m);
for (int i = 1; i <= n; ++i) scanf("%lld" , &a[i]);
C[0][0] = 1;
for (int i = 1; i <= n; ++i) {
C[i][0] = 1;
for (int j = 1; j <= i; ++j) {
Add(C[i][j] , C[i - 1][j - 1]);
Add(C[i][j] , C[i - 1][j]);
}
}
dp[0][0] = 1;
for (int i = 0; i < m; ++i) {
for (int j = 0; j <= n; ++j) {
Add(dp[i + 1][j] , dp[i][j] * j * 2ll % mod);
Add(dp[i + 1][j + 1] , dp[i][j]);
}
}
LL ans = 0;
for (int l = 0; l <= n; ++l) {
for (int r = n - l; r >= 0; --r) {
if(n - l - r >= 2 && a[n - r - 1] > a[n - r]) break;
Add(ans , dp[m][l + r] * C[l + r][l] % mod);
}
}
printf("%lld" , ans);
return 0;
}