P8866 [NOIP2022] 喵了个喵
本篇题解将细致讲解本题的思路,并给出一种长度短而可读性高的解法。
P8866 NOIP2022 喵了个喵 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)。
本题解中我们将图案为
观察到数据范围,很难不联想 2018 年的旅行。但是那个
首先发现
观察
考虑将数字
遇到一个数字
- 如果
为空,直接入栈; - 如果
有 个元素,和 相同,将 入栈 并将这两个 消除; - 如果
有 个元素,和 不同,将 入栈 ; - 如果
有 个元素,栈顶和 相同,将 入栈 并将这两个 消除; - 如果
有 个元素,栈顶和 不同,那么栈底一定和 相同。将 入辅助栈 ,对栈 和栈 栈底相消。
容易看出,我们第偶数次遇到
另外,可以注意到本题的操作只对栈顶和栈底有效,所以如果栈中某时元素很多就会造成大量的滞留,非常不优。这也和本做法中栈的大小始终不超过
有关系(达到 之后可以马上通过相消降为 )。
这样 15 分就到手了。是 60 分的四分之一。
一种比较显然的想法是,对于数字
我选择了另一种想法,那就是抛开栈
- 如果我们当前遇到的数
在所有栈中还没出现过,就把 射入一个还没满两个元素的栈,但如果已经有 个栈全都满了,说明我们按照 的策略已经走不下去了(我们必须留一个空栈作为辅助栈,用来栈底相消)。 - 如果
在某个栈中出现了,直接使用 的策略直接让当前的 和栈中出现的那个 消除。
当走不下去的时候,一定是局面上的
提一嘴,我感觉在剩下的 85 分中,应该是有部分数据可以按照上述策略成功走完整个游戏的,毕竟随机数据下,出现走不下去的概率不是特别高(当然也不难构造)。所以也不失为一个骗分好办法。不过这题有多测,所以也有可能会卡。
更新:官方数据卡了这一点,只能拿到 15 pts。民间数据可以拿到 30 pts。
直接把
我们考虑离线:也就是说,我们开始预知这个数后面都是什么数。依据后面数的情况,我们考虑
考虑紧跟在
因此发现,如果
现在讨论
- 如果未来数列的
和 之间, 出现了奇数次:把 放入辅助栈,接下来一直自由相消,直到将处理 为止。由于 的数量是奇数,加上一开始的一个 ,此时 一定被消除光,现在 的栈顶变成 ,将 放入 栈顶相消, 变成了空栈。 - 如果
出现了偶数次:把 放入 ,未来所有不为 的栈顶自由相消,对于 ,我们将其全部放入辅助栈相消,直到将处理 为止。由于 的数量是偶数,这些 一定都被消除光,辅助栈仍为空,我们将 放入辅助栈,和 栈底相消,辅助栈仍为空。
经过上述操作后,局面总会保持:
- 存在一个空栈;
- 所有数都在栈中最多出现一次;
- 所有栈最多只有两个元素。
直接让新的空栈作为辅助栈即可。
如何证明这种做法一定可以报告出解?其实很简单:在最后,每种元素在所有栈中最多只能出现一次;每种数字数量均为偶数,相消永远是同种元素两两相消,所以到最后,某种数出现奇数次是不可能的。综合来看,每种元素到最后只能出现零次,也就是必定不会出现。
一个实现问题:怎么找当前还没满两个元素的栈?我们维护一个队列,存储栈的编号,使得始终满足:
- 如果一个栈当前存满两个元素,队列中不存在当前栈的编号;
- 如果一个栈当前只存了一个元素,让队列中该栈的编号出现一次;
- 如果一个栈当前为空,让队列中该栈的编号出现两次。
- 辅助栈的编号在队列中不能出现。
一开始,我们直接让
假如我们当前遇到了一个数
而当一个元素被弹出栈后,让元素所对应的栈的编号入队。
容易发现,这样就维护好了上面队列应该满足的四条性质。不过,辅助栈的编号可能会变动,要始终保证好它不出现在队列中是有些细节的。
时间复杂度
/*
* @Author: crab-in-the-northeast
* @Date: 2022-12-04 01:03:11
* @Last Modified by: crab-in-the-northeast
* @Last Modified time: 2022-12-04 13:35:14
*/
#include <bits/stdc++.h>
inline int read() {
int x = 0;
bool f = true;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-')
f = false;
for (; isdigit(ch); ch = getchar())
x = (x << 1) + (x << 3) + ch - '0';
return f ? x : (~(x - 1));
}
const int maxn = 305;
const int maxm = (int)2e6 + 5;
int a[maxm];
std :: deque <int> st[maxn];
int id[maxn * 2]; // 维护所在栈编号,局面未出现则为 0
typedef std :: pair <int, int> pii;
std :: vector <pii> ans;
inline void pu(int s) {
ans.emplace_back(s, 0);
}
inline void de(int s, int t) {
ans.emplace_back(s, t);
}
int spt; // 辅助栈编号
std :: queue <int> q; // 维护空栈
inline bool simple(int x) { // 尝试简单相消
int s = id[x];
if (!s) {
if (q.empty())
return false;
id[x] = s = q.front();
q.pop();
pu(s);
st[s].push_back(x);
} else {
id[x] = 0;
q.push(s);
if (x == st[s].back()) {
pu(s);
st[s].pop_back();
} else {
pu(spt);
de(spt, s);
st[s].pop_front();
}
}
return true;
}
int main() {
for (int T = read(); T; --T) {
int n = read(), m = read(); read();
for (int i = 1; i <= m; ++i)
a[i] = read();
std :: memset(id, 0, sizeof(id));
ans.clear();
spt = n;
while (!q.empty())
q.pop();
for (int i = 1; i < n; ++i) {
q.push(i);
q.push(i);
}
for (int i = 1; i <= m; ++i) if (!simple(a[i])) {
int p = a[i];
int r = i + 1, x = a[r];
for (; r <= m && x != p && st[id[x]].back() == x; ++r, x = a[r]);
// 此时 r 是 i 后第一个不在栈顶上的下标,x 是 a[r]
if (x == p) {
pu(spt);
for (int j = i + 1; j < r; ++j)
simple(a[j]);
pu(spt);
} else {
int s = id[x], y = st[s].back();
bool evn = true; // 中间栈顶中,y 的数量是否为偶数
for (int j = i + 1; j < r; ++j)
if (a[j] == y)
evn = !evn;
if (evn) {
pu(s);
st[s].push_back(p);
for (int j = i + 1; j < r; ++j) {
if (a[j] == y)
pu(spt);
else
simple(a[j]);
}
pu(spt);
de(spt, s);
st[s].pop_front();
// st[s] 从栈底到栈顶,原先为 x, y,现在为 y, p
// 依此更新 id
id[x] = 0;
id[p] = s;
} else {
pu(spt);
st[spt].push_back(p);
for (int j = i + 1; j < r; ++j) {
if (a[j] == y)
pu(s); // 注意这里不要直接 simple(a[j])
// 原因是 s 即将变成 spt,所以暂时不能让 s 弹入 q
// 特判让 simple 函数此时不把 s 不弹入 q 也不行
// 假如 3 1 2 1 1 1 ....
// 如果直接不弹入 q,会造成后面的一串 1 1 1 无法正确弹入 s
// 最后暴力扫队列把 s 删除复杂度也不对
// 所以正确的做法就是不用 simple 函数
else
simple(a[j]);
}
pu(s);
st[s].clear();
// 原先辅助栈 spt 此时存在一个元素 p
// s 原先栈底到栈顶为 x,y, 现在为空
// s 作为新的 spt
// 依此更新 id 和 q
id[x] = id[y] = 0;
id[p] = spt;
q.push(spt);
spt = s;
}
}
i = r; // 注意循环会自带 ++i,下一次循环 i = r + 1
}
printf("%d\n", (int)ans.size());
for (pii p : ans) {
if (p.second)
printf("2 %d %d\n", p.first, p.second);
else
printf("1 %d\n", p.first);
}
}
}
删除注释和不必要的空格,并且仍然保持很高的可读性后,代码长度是不足 2K 的,几乎优于所有 AC 提交。而且,可读性非常好。
如果觉得这篇题解写得好,请不要忘记点赞,谢谢!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效