友情提示:如果你需要自己的思考,请不要急着阅读此题解,如果实在做不出来,可以看看几个性质作为启发。
要读懂本题解,请掌握以下知识:
性质 1
取出 1 之后,剩下的数有 2^{n-k-1} 种取法。
证明:剩下 n-k 次操作,我们每次可以从左边或右边取出,这样是 2^{n-k}。但是对于最后一个数,从左边取出和从右边取出是等价的,所以应该是 2^{n-k-1}。
这就告诉我们,我们可以只考虑取出 1 之前的操作序列。
性质 2
注意到题目中的双端队列满足单峰。
假设 1 所在的位置为 x,则对于 i \in [1,x),a_i>a_{i+1},而对于 i \in [x,n),a_i<a_{i+1}。
证明:注意到我们按顺序放入。1 是第一个放入的,把队列割裂成了左半部分和右半部分。由于 1 一定是最后一个弹出,这可以看做一个双端栈。注意到我们放数一定越放越大,所以对于 1 左边的元素,从左往右单调递减;1 右边的元素,从左往右单调递增。
性质 3
我们取出 1 之前取出的数一定可以拆分成两个单调递减的子序列。
证明:由于取出 1 前是一个双端栈。我们只能取最左边连续的一段和最右边连续的一段。由双端栈的定义可知,左边只能从最左端往右取,右边从最右端往左取。由性质 2 可知取出的这两段都满足单调递减,于是得证。
性质 4
把取出的数拆分成两个单调递减的子序列后,可以构造方案放入双端队列。
证明:我们不妨把 1 放在两个子序列中最小值较小的一个子序列的末尾,把这个子序列放到剩下数中较小的一端;另一个子序列放到较大的一端。根据性质 2 得证。
下面,我们来推导前 k 个数的取法(实际上是前 k-1 个,第 k 个数一定是 1)。
由于放入双端队列有很多很多种方法(指数级),我们肯定没法构造出一个双端队列,那么我们能不能概括一下这个操作序列有什么限制呢?
-
性质 3。
-
第 k 个取出的数是 1。
-
最后剩下的 n-k 个数种不能存在一个数同时大于取出的两个单调递减序列的最小值。
我们考虑如何来构造出一个序列满足以上三条限制。
观察发现数据范围是 1 \leq N \leq 2000,可以接受 N^2 的做法。我们不妨向 dp 的方向思考。
关于 dp 有一个重要的思想,那就是当我们定义方程时出现了一个问题解决不了,就另开一维。我们把这个思想运用到这道题目中。
我们来研究本题的状态转移方程如何定义。注意到我们肯定要开一维记录我们目前选出了多少个数。此外,我们还需要记录选出的最小值。于是我们可以用 f_{i,j} 来表示选到第 i 个数,最小值为 j 的方案数。
方程定义完了,接下来考虑如何转移。转移方程中的 k 与题面中的 k 不同,请读者注意。
-
如果新选一个 k<j,则 f_{i,j} 能转移到 f_{i,k}。k 的范围显然是 (1,j-1)。
-
如果选一个 k>j,会发现比较棘手。我们进行一个详细的分析:
我们能发现 k 一定是未选的数中最大的一个。为什么?注意到 k 一定不会放在较小的一个序列里,否则不满足性质 2。于是 k 只能放在较大的序列里。根据性质 2 和性质 4 可知,如果剩下一个大于最小值较大的序列最小值的数,方案不合法。得证。
既然这样,我们的转移方程就出来了。
边界情况为 $f_{1,i}=1$($i \in (1,n]$)。
答案即为 $\displaystyle{\sum_{i=2}^{n-k+2}f_{k-1,i}}$。我们来讲讲这个 $n-k+2$ 是什么意思。注意到我们取出了 $k-1$ 个数,这些数中的最小值至少为 $n-(k-1)+1=n-k+2$。
现在有一个大问题,注意到我们刚才的 dp 方程是 $O(n^3)$ 的(这里默认 $n,k$ 同阶)。因为我们转移一个 $f_{i+1,j}$ 是 $O(n)$ 的。这样无法通过,怎么办呢?
考虑前缀和优化,我们不妨用 $f_{i+1,j+1}$ 来推出 $f_{i+1,j}$。写一下过程(下面推导过程中的 $k$ 依然不是题面中的 $k$,而是循环变量):
$f_{i+1,j}=\displaystyle{\sum_{k=j}^{n-i+1}f_{i,k}}$。
$f_{i+1,j+1}=\displaystyle{\sum_{k=j+1}^{n-i+1}f_{i,k}}$。
则 $f_{i+1,j}-f_{i+1,j+1}=f_{i,j}$。移项得到 $f_{i+1,j}=f_{i,j}+f_{i+1,j+1}$。在转移的时候倒着做即可。
在最后把 dp 结果统计完后不要忘了乘上 $2^{n-k+1}$。由于数据范围不大,可以不写快速幂(我写习惯了就写了一个)。
代码:
```cpp
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n, m, f[2005][2005], ans = 0;
const int mod = 1000000007;
int qpow(int x, int k){//快速幂,可以暴力乘
if (k <= 0)
return 1;
int ans = 1;
while (k){
if (k & 1)
ans = ans * x % mod;
x = x * x % mod;
k >>= 1;
}
return ans;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin >> n >> m;//m 就是题面中的 k
if (m == 1){//特判 k = 1
cout << qpow(2, n - m - 1);
return 0;
}
for (int i = 2; i <= n; i++)
f[1][i] = 1;//边界
for (int i = 1; i < m - 1; i++){//转移主体部分
f[i + 1][n - i + 1] = f[i][n - i + 1];
for (int j = n - i; j > 1; j--)
f[i + 1][j] = (f[i + 1][j + 1] + f[i][j]) % mod;//前缀和优化
}
for (int i = 1; i <= n - m + 2; i++)
ans = (ans + f[m - 1][i]) % mod;//统计答案
cout << ans * qpow(2, n - m - 1) % mod;//别忘记乘上剩下的贡献
return 0;
}
```