2.11 CW 模拟赛 T1. 刀等数学
思路
题意
- 给定一个长为 的排列 和一个最初为空的大根堆
- 进行 次操作
- 取出堆顶放入 末尾
- 取出 开头放入堆
求最终得到的 的种类数
注意力惊人其实有点, 我说题解
性质
假设 在 中的位置为 , 在 中的位置为
因为 一定是堆中最后弹出来的数, 所以 前 个数的集合应该相同, 不难发现
证明
因为 总是在堆中最后出现, 那么每次 出现在 中, 堆一定是空的, 因此此时 对应数集相同
进一步处理, 剩下的部分中, 其最小值也满足上面的性质
同样可以类似上面的做
所以现在我们知道一个合法解的性质了
如果你执行 : 找到序列的最小值 \(\to\) 切割 \(\to\) 找到序列的最小值 \(\to\) 切割 \(\to \cdots\) , 那么每次切下来的那一段中, 对应的 \(a, b\) 数集相同, 也就是说那一段中 \(a, b\) 对应的最小值相同
更一般的, 我们可以把每次切割产生的两个序列都如上处理, 仍然符合性质, 这提示我们使用区间 \(\rm{dp}\)
但是这样怎么统计数量?
考虑仿照分段的过程, 进行一个记忆化的搜索
我们每次处理当前 \([l, r], v\) , 表示考虑 \([L, R]\) 区间中 \(< v\) 的部分已经被填过了, 保证 \(a_L, a_R \geq v\) , 且存在 \(a_i \in [L, R] = v\) , 保证 \(a_{[L, R]}\) 和 \(b_{[L, R]}\) 中数集相同
关于初始化 / 边界条件
显然应当初始化为
这种情况下整个区间会被跳过, 应当初始化为
细节
- 如果不存在
我们直接找到当前区间 中的最小值, 然后查询 即可, 保证了区间中存在最小值
找到 \(a_{[L, R]}\) 的最小值位置 \(p\) , 枚举 \(b_{[L, R]}\) 中的最小值位置 \(q \in [p, R]\)
递归区间 \([L, p], (p, R]\) , 令 \(v \gets v + 1\) 表示已经处理完了 \(v\) 的位置, 以后 \(a\) 中 \(< v\) 的就不管了, 已经填好了
为什么要递归这两个区间
只有这样才能保证区间中满足「保证 和 中数集相同」
这种分拆类问题还是太神奇了, 做了这么多道题仍然没有什么想法
所以以这道题为例, 对这一种问题进行一些简单的分析
- 定义合法情况, 要求输出一组合法情况 / 合法情况的最值问题 / 求方案数
- 往往利用 \(\rm{dp}\) , 结合约束处理当前方案数
- 关注构造方案 / 顺序
- 关注本质重复的转移是否存在
- 由合法情况导出的答案
- 先考虑最终答案的表达式 \((\)合法解的构造方案\()\) , 基础上进行 \(\rm{dp}\)
- 先找到一组合法解, 然后在基础上进行调整
- 找到所有情况统一的构造方案
- 模拟操作情况
- 找到最好开销, 注意最大和最小
- 先找到一组合法解, 然后观察性质
- 往往利用 \(\rm{dp}\) , 结合约束处理当前方案数
题意
- 给定一个长为 的排列 和一个最初为空的大根堆
- 进行 次操作
- 取出堆顶放入 末尾
- 取出 开头放入堆
求最终得到的 的种类数
找不到什么好处理的构造方案
因此先构造出最终的 \(b\) , 看看有什么好的性质
发现一旦 \(1\) 进入堆里面, 那么其弹出时, 堆一定是空的, 所以之前的数集相同
所以可以分成两个部分, 之前之后的数集都相同, 当前点的约束就是要钦定 \(1\) 在 \(b\) 中的那个位置
对两边的数列可以同理处理, 只是把 \(1\) 换成最小值
但是怎么处理两边的数集
不难发现数集可以简单的用 \(a[L, p]\) 和 \(a[p, R]\) 统计, 然后简单处理出当前序列的最小值即可
唯一一个细节就在数集相同的部分是左闭右闭的, 莫名增加了实现难度
实现
#include <bits/stdc++.h>
#define int long long
const int MAXN = 105;
const int MOD = 8580287;
namespace calc {
int add(int a, int b) { return a + b > MOD ? a + b - MOD : a + b; }
int mul(int a, int b) { return (a * b * 1ll) % MOD; }
int sub(int a, int b) { return a - b < 0 ? a - b + MOD : a - b; }
void addon(int &a, int b) { a = add(a, b); }
void mulon(int &a, int b) { a = mul(a, b); }
} using namespace calc;
int n;
int a[MAXN];
int f[MAXN][MAXN][MAXN];
int dfs(int l, int r, int x) {
if (l >= r) return 1;
if (f[l][r][x] != -1) return f[l][r][x];
bool useless = true; for (int i = l; i <= r; i++) if (a[i] >= x) useless = false;
if (useless) return f[l][r][x] = 1;
int mn = n + 1; for (int i = l; i <= r; i++) if (a[i] >= x) mn = std::min(mn, a[i]);
if (mn > x) return f[l][r][x] = dfs(l, r, mn);
int ans = 0;
bool flag = false;
for (int i = l; i <= r; i++) {
if (a[i] == x) flag = true;
if (a[i] >= x && flag) addon(ans, mul(dfs(l, i, x + 1), dfs(i + 1, r, x + 1)));
}
return f[l][r][x] = ans;
}
signed main()
{
scanf("%lld", &n); memset(f, -1, sizeof(f));
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
printf("%lld", dfs(1, n, 1));
return 0;
}
总结
找合法解的个数, 首先要尝试找到合法解的性质
一个新 trick
排列类问题, 与大小有关系时, 尝试先找到 这些特殊点的位置, 然后按照特殊点分段
区间 \(\rm{dp}\) 往往可以使用记忆化搜索简化难度
记忆化搜索应当处理好初始化 / 边界条件

浙公网安备 33010602011771号