1 条题解
-
0
题解
每次放入一个数以后,可以让它按小值模式或大值模式向上移动。我们知道放入的顺序和最后的数组,要数出有多少串模式选择能得到这个结果。即使两种模式都没交换,也要算作两种选择。
按正序模拟时,一次选择就可能改变后续操作遇到的数,很难只用一个状态概括。倒着考虑会方便很多。第 次放入的数只可能出现在位置 到根的路径上;把这次操作还原,就能继续处理第 次。
小数据先枚举
子任务 1 中 ,可以枚举全部 串模式选择,逐一模拟,检查得到的数组是否与目标相同。每次上浮至多经过 条边,直接写的时间复杂度为 ,空间复杂度为 。
搜索时也可以保留当前数组,记录本次交换过的位置,递归返回后按相反顺序交换回去,省掉重复模拟前缀的开销。两种模式即使得到相同数组,也必须分别搜索,因为题目统计的是选择序列。
这个做法通过子任务 1,得到 分。
两种容易处理的性质
子任务 2 中所有数都相同,任意一次比较都不会交换。题目保证输入数组与目标数组包含相同的数,因此这里目标数组必然能得到。每一步都有两种模式,答案就是 。读入需要 时间,只计算答案时额外空间为 。配合小数据枚举,可以得到子任务 1、2 的 分。
子任务 3 中, 比此前放入的数都大。选择小值模式,它留在位置 ;选择大值模式,它会一直走到根。逆序处理时, 的位置只可能是 或 。若在 ,删去它即可;若在根,就沿根到 的路径把后面的数逐个向父位置搬回去,再删去位置 ;两处都找不到就无解。
这里每个数互不相同,所以 时,两种模式会把 放在不同位置,最后的状态能确定这一步选了哪种模式。只有第一次操作有两种选择。因此,只要逆推成功,答案就是 。每次还原走一条长度为 的路径,总时间为 ,空间为 。结合前两种做法,可以通过子任务 1、2、3,得到 分。
数互不相同时,位置已经确定
子任务 4 去掉了递增限制,但仍保证 两两不同。逆序处理第 步时,当前数组中的 只有一个,它只能在位置 到根的路径上。
沿这条路径寻找 。若找不到,答案为 ;若找到了,就尝试把它移回位置 ,并检查这些交换是否符合某一种模式,以及它能否在找到的位置停止。下面会把这两个检查展开。
数互不相同时,发生交换的模式是唯一的;没有发生交换且 时,父位置的数也不等于 ,仍只有一种模式能让它留下。因此合法目标的答案同样为 ,我们只需要判断逆推能否完成。
每步查找、检查和还原都只遍历这条路径,时间为 ,空间为 。这能通过子任务 3、4;和枚举、全相等的特判合用,可以得到前四个子任务的 分。
重复值也能唯一还原
处理第 步前,维护的数组是做完前 次操作后的状态。记本次放入的数为 。
假设它最后停在位置 。从 向上到 的过程中, 每交换一次,就把父位置原有的数推到下面。于是操作结束后,路径上 下方的数,恰好是这次与 交换过的数。
这些数都不可能等于 。小值模式只会越过严格大于 的数,大值模式只会越过严格小于 的数。只要遇到相等的数,上浮就会停止。
所以,从位置 往根走,第一个满足 的位置,一定就是本次放入的 的停点。若要把更高处的另一个 当作停点,下面这个相等的数就必须曾经与 交换,与规则矛盾。路径外即使还有许多相等的数,也不会参与本次操作。
这就排除了重复值带来的歧义。沿路找不到 时直接判无解;找到了以后,停点和需要还原的路径都确定了。
发生过交换时,检查整条路和停点
把路径记为
$$q_0=i,\quad q_1=\lfloor i/2\rfloor,\quad\ldots,\quad q_k=p.$$当 时,当前的 是第一次与 交换的数。若 ,本次只能选择小值模式;若 ,只能选择大值模式。由于 是第一个等于 的位置,这里不会相等。
随后要检查 是否都在 的同一侧。小值模式要求它们全部严格大于 ,大值模式要求它们全部严格小于 。不能只检查第一个数,否则可能把中途本来就应停止的上浮算进去。这里也不要求这些数彼此有序,只要求它们分别与 的大小关系符合所选模式。
还要看 的父位置。若 ,小值模式要求 ,大值模式要求 ,这样才会在 停下。等号是允许的。若这个检查不满足,即使一路交换都合法, 也会继续向上走,无法得到当前数组。 时没有父位置,不用检查。
检查通过后,前一个状态也确定了。把 改成 ,再把 改成 ,依次向下处理,最后删去位置 。赋值要从靠近 的地方开始,避免把还没读过的值覆盖。路径外的数保持原样。
这样还原出的数组重新插入 ,按确定的模式操作,会逐个发生我们检查过的交换,并在 停下,恰好回到当前状态。所以这两个检查既不能省略,也已经足够。
没有交换时,两种模式可能都合法
若 ,说明 放下后就没有移动,删掉位置 即可。模式的数量取决于父位置的数。
若 ,两种模式都无事可做,贡献 种。若 且父位置的数也等于 ,两种模式同样都不交换,贡献 种。父位置的数与 不相等时,一种模式会交换,另一种会停止,只有停止的那种符合当前状态,贡献 种。
发生过交换时,第一条交换已经确定了模式,所以贡献也只有 种。
为什么这些数量可以直接相乘
每次逆推得到的前一个数组都是唯一的。即使某一步有两种合法模式,它们也都没有交换,删去新放入的数后得到的是同一个数组。我们不需要为这两种模式分别维护状态,后面要逆推的问题完全相同。
更具体地说,设当前状态有 种合法的第 步模式,且唯一还原出的前一个状态有 种选择序列。每一种前缀序列都能接上这 种模式,不同的前缀或最后一种模式也会产生不同的序列,所以
前面的讨论已经列出了全部可能的停点和模式。检查失败时没有合法前驱,答案为 ;检查通过时,所有合法序列都会还原到我们维护的那个前驱。一直逆推到空数组,而空数组对应唯一的空序列。于是从 开始,把沿途的 依次乘起来,就是要求的答案。
复杂度与实现
位置 每向上走一步,编号至少减半,因此路径长度为 。查找、检查、还原各遍历这条路径一次,总时间复杂度为 。两个数组占用 空间,临时路径只占 空间,总空间复杂度为 。
代码用循环变量 表示当前有效数组的末尾。处理完这一步以后,后面只会访问编号不超过 的位置,所以无需真的清空 。答案每乘一次 就取模。
这个做法通过全部子任务,得到 分。
参考代码
#include <iostream> #include <vector> using namespace std; const int MOD = 998244353; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; vector<int> v(n + 1), a(n + 1); for (int i = 1; i <= n; ++i) cin >> v[i]; for (int i = 1; i <= n; ++i) cin >> a[i]; long long answer = 1; for (int i = n; i >= 1; --i) { int x = v[i], p = i; vector<int> path; // path 从下往上记录被 x 越过的位置,不含 x 的停点。 while (p > 0 && a[p] != x) { path.push_back(p); p /= 2; } if (p == 0) { cout << 0 << '\n'; return 0; } if (path.empty()) { if (p == 1 || a[p / 2] == x) answer = answer * 2 % MOD; } else { bool small = a[path[0]] > x; for (int t : path) { if (small ? a[t] <= x : a[t] >= x) { cout << 0 << '\n'; return 0; } } // 既要能走到 p,也要确实停在 p。 if (p > 1 && (small ? a[p / 2] > x : a[p / 2] < x)) { cout << 0 << '\n'; return 0; } // 从靠近 p 的位置开始恢复,避免覆盖尚未使用的值。 for (auto it = path.rbegin(); it != path.rend(); ++it) { a[*it / 2] = a[*it]; } } // 位置 i 已被删除,后续只会访问 1 到 i - 1。 } cout << answer << '\n'; return 0; }
- 1
信息
- ID
- 821
- 时间
- 3000ms
- 内存
- 256MiB
- 难度
- 5
- 标签
- 递交数
- 17
- 已通过
- 3
- 上传者