题解:AT_arc068_d [ARC068F] Solitaire

CEFqwq

2024-11-19 20:39:21

Solution

友情提示:如果你需要自己的思考,请不要急着阅读此题解,如果实在做不出来,可以看看几个性质作为启发。

要读懂本题解,请掌握以下知识:

性质 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)。

由于放入双端队列有很多很多种方法(指数级),我们肯定没法构造出一个双端队列,那么我们能不能概括一下这个操作序列有什么限制呢?

  1. 性质 3。

  2. k 个取出的数是 1

  3. 最后剩下的 n-k 个数种不能存在一个数同时大于取出的两个单调递减序列的最小值。

我们考虑如何来构造出一个序列满足以上三条限制。

观察发现数据范围是 1 \leq N \leq 2000,可以接受 N^2 的做法。我们不妨向 dp 的方向思考。

关于 dp 有一个重要的思想,那就是当我们定义方程时出现了一个问题解决不了,就另开一维。我们把这个思想运用到这道题目中。

我们来研究本题的状态转移方程如何定义。注意到我们肯定要开一维记录我们目前选出了多少个数。此外,我们还需要记录选出的最小值。于是我们可以用 f_{i,j} 来表示选到第 i 个数,最小值为 j 的方案数。

方程定义完了,接下来考虑如何转移。转移方程中的 k 与题面中的 k 不同,请读者注意。

  1. 如果新选一个 k<j,则 f_{i,j} 能转移到 f_{i,k}k 的范围显然是 (1,j-1)

  2. 如果选一个 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; } ```