【题解】P8866 [NOIP2022] 喵了个喵(构造,adhoc)
【题解】P8866 [NOIP2022] 喵了个喵
题目链接
题意概述
有一个牌堆和
开始时牌堆中有
- 选择一个栈,将牌堆顶上的卡牌放入栈的顶部。如果这么操作后,这个栈最上方的两张牌有相同的图案,则会自动将这两张牌消去。
- 选择两个不同的栈,如果这两个栈栈底的卡牌有相同的图案,则可以将这两张牌消去,原来在栈底上方的卡牌会成为新的栈底。如果不同,则什么也不会做。
要求构造一个操作序列,满足进行这些操作之后可以使得操作的牌堆和每个栈均为空。
数据范围
设
对于所有数据,保证
测试点 | ||||
---|---|---|---|---|
无限制 | ||||
无限制 | ||||
无限制 | ||||
无限制 |
思路分析
注:为了方便起见,我们将题目中的卡牌称之为元素。
首先看到这个题目,可以尝试发现一些基本的结论:
我们最终一定进行了
观察数据范围就比较容易能联想到 NOIP2018 的旅行,说明解法一定与
所以我们可以考虑先从
观察
那么我们就考虑直接钦定将
那么由于一个栈中只会放两种卡牌,所以任意时刻,一个栈中的元素最多为
那么可以有以下解法:
假设当前我们要将
- 若
为空,直接入栈; - 若
有一个元素:- 该元素和
相同,那么直接进行第一种操作:将 入栈并将两个 相消; - 该元素和
不同,那么也直接进行第一种操作:将 入栈。
- 该元素和
- 若
有两个元素:- 栈顶与
相同,那么直接进行第一种操作:将 入栈并将两个 相消; - 栈底和
相同,那么直接进行第二种操作:将 加入辅助栈并将两个 相消。
- 栈顶与
那么首先显然最后无论如何辅助栈一定为空:因为三类五种情况下只有最后一种情况会用到辅助栈且每次操作都能讲辅助栈清空。
且由于每个元素出现次数为偶数次,最后一次
这部分分给了 15pts,虽然相对于正解还差别很大,但已经给了我们很大的启发来思考正解。
由于
但如果直接沿用上述
考虑另一种使用上述解法的策略但抛开
我们同样在初始时将前
假设当前我们要将
-
当
在之前的栈中已经出现过,假设 之前出现在栈 中。则我们可以按照 的策略将 与栈 中的另外一个 进行简单相消,具体而言:- 当
是 的栈顶时,执行第一种操作; - 当
是 的栈底时,执行第二种操作。
- 当
-
当
在之前的栈中从未出现过,则将 加入到一个新的没满两个元素的栈中。若除了辅助栈之外的其它栈已满,那么发现此时已经不能用 进行操作了,我们考虑进行特殊操作。
发现此时我们已经基本解决了问题,只有最后一种情况当【所有普通栈已满】时的特殊操作还不知道应该怎么办。
那么我们接下来都来说明如何进行特殊操作。
当所有普通栈已满时,此时局面上一定是前
贪心的考虑是直接将
由此可以发现我们的选栈策略实际上与
- 对于
后面在栈顶的元素:这些栈顶的数都很好解决,我们直接将他们放入原来的栈中简单相消即可。 - 对于
后面本来就是 的元素:显然可以将两个 放在辅助栈然后直接相消,这时候辅助栈还是空的; - 对于
后面在栈底的元素:按理来讲,我们应该将它放在辅助栈中,然后与另外一个栈底元素相消;那么这时候操作完辅助栈还是空的
我们发现当
那么我们分类讨论考虑入栈序列中
-
当
后面第一个不在栈顶的元素是 时,那么此时的入栈序列一定是 ,省略号表示了一段在栈顶的元素,那么我们将 放入辅助栈中,对于省略号中的元素可以直接在栈顶自由相消,再将第二个 加入到辅助栈中,使得辅助栈变空且辅助栈位置不变; -
当
后面第一个不在栈顶的元素在栈底时,假设这个元素为 , 原来在栈 的栈底。那么此时的入栈序列一定是 ,我们当然会尽量让
相抵消,那么有两种情况:- 使用操作 1 将入栈序列中的
放入 中,此时一定满足 只有一个元素 ,那么就要把 的栈顶元素全部消掉,我们设 的栈顶元素为 ,那么说明 的省略号中 的个数一定是奇数个,因为这样它们才能与栈中的一个 共同全部消掉。即:当 的省略号中 为奇数时,则将 放入辅助栈中,再将省略号中的元素自由相消,然后将 入栈 ,与栈中 相消。此时 为空,变成新的辅助栈; - 同理当省略号中
的个数为偶数个时,只能让两个 通过操作 2 栈底相消,那么我们只能先将 放入 中,然后再将其它非 的栈顶元素自由相消,偶数个 放在辅助栈中相消,最后再将 放入辅助栈进行操作 2 与栈底的 相消。此时辅助栈为空且辅助栈位置不变。
- 使用操作 1 将入栈序列中的
-
当
后面全部跟的都是在栈顶的元素时,直接一直相消到入栈序列为空即可。
经过上述操作后,局面总保持:
- 存在一个空栈,就是辅助栈。
- 所有元素在栈中最多出现一次;
- 每个栈最多只有两个元素。
由于每种元素数量均为偶数,相消永远是同种元素两两相消,所以到最后,某种数出现奇数次是不可能的。又由于每种元素在栈中最多只出现一次。综合来看,每种元素到最后只能出现零次,也就是必定不会出现。
实现细节
之所以在这个题题解中突然加一个我之前写题解从来没有过的【实现细节】的环节,是因为这个题代码确实难写也比较巧妙,所以专门在这里说一下。
-
如何实现普通局面时,对于
的入栈和简单相消的过程?实际上只要维护一个队列
存储栈的编号,满足:- 如果一个栈中已经存满两个元素,那么当前栈不在队列中;
- 如果一个栈中有一个元素,那么当前栈在队列中出现一次;
- 如果一个栈中没有元素,那么当前栈在队列中出现两次;
- 辅助栈的编号不出现在队列中。
初始时,我们将
到 的所有元素入栈两次。同时,定义一个数组
表示 出现的栈的编号,初始时 。我们用
deque
来存储每个栈的元素和栈内相对位置。并用一个变量 表示当前辅助栈的编号。当一个元素
要入栈时:- 当
时:直接从队列中弹出一个栈,并将 入栈。 - 当
时:- 当
在栈顶,即 时,将 加入 ,并与栈顶 进行相消; - 当
在栈底,即 时,将 加入 ,并与栈底 进行相消。
- 当
这块需要注意,及时更新
(变为 还是变成新的数),及时将每个栈入队出队,及时更新栈内元素。code:
int solve(int x) { if(id[x]) { int ID=id[x]; if(dq[ID].back()==x) { id[x]=0; dq[ID].pop_back(); pb(ID); stk.push(ID); } else if(dq[ID].front()==x) { id[x]=0; dq[ID].pop_front(); pb(spt); del(ID,spt); stk.push(ID); } } else { if(stk.empty())//特殊处理 { return 0; } else//简单插入 { int tt=stk.front(); dq[tt].push_back(x); stk.pop(); id[x]=tt; pb(tt); } } return 1; }
-
特殊处理中,如何求出
后面第一个不为栈顶的元素是谁?我们可以从
开始暴力枚举每一个元素,判断它们是否为栈顶。直到判断到一个元素是 或者是栈底即可。由于
一定没有出现在栈中,所以可以直接判断当前元素是否在栈中出现。code:
int t=pos; pos++; while(id[a[pos]]&&dq[id[a[pos]]].back()==a[pos])pos++;
-
特殊处理中有哪些需要注意的细节?
- 对于
中, 为偶数的情况,未来所有不为 的栈顶自由相消,对于 ,不能直接自由相消,需要我们把它放到辅助栈相消。 - 对于
中, 为偶数的情况,辅助栈即将变到 ,所以辅助栈不能入栈。也不能直接特判当前栈为 时,直接不入栈,例如:当当前入栈序列为3 1 2 1 1 1 ...
,如果直接不弹入 ,会造成后面的一串1 1 1
无法正确入栈。但由于最后暴力扫队列把 删除复杂度存在问题,所以我们可以直接不使用简单相消。
- 对于
-
如何记录最终答案?
我们可以使用一个
vector<pair<int,int>>ans
来存储答案序列,对于 中的元素 ,若 为 ,则是操作 1, 表示操作 1 选择的栈;反之则是操作 2, 和 分别表示操作 2 选择的两个栈。那么操作次数直接输出 即可。
代码实现
//luoguP8866
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<vector>
#define mk make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=305;
const int maxm=2e6+10;
const int maxk=1005;
int a[maxm],id[maxk];
int n,m,k,spt;
queue<int>stk;
deque<int>dq[maxn];
vector<pii>ans;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
void pb(int pos){ans.push_back(mk(pos,0));}
void del(int pos1,int pos2){ans.push_back(mk(pos1,pos2));}
void clear()
{
while(!stk.empty())stk.pop();
for(int i=1;i<=k;i++)id[i]=0;
ans.clear();
spt=n;
}
int solve(int x)
{
if(id[x])
{
int ID=id[x];
if(dq[ID].back()==x)
{
id[x]=0;
dq[ID].pop_back();
pb(ID);
stk.push(ID);
}
else if(dq[ID].front()==x)
{
id[x]=0;
dq[ID].pop_front();
pb(spt);
del(ID,spt);
stk.push(ID);
}
}
else
{
if(stk.empty())//特殊处理
{
return 0;
}
else//简单插入
{
int tt=stk.front();
dq[tt].push_back(x);
stk.pop();
id[x]=tt;
pb(tt);
}
}
return 1;
}
int work(int pos)
{
int t=pos;
pos++;
while(id[a[pos]]&&dq[id[a[pos]]].back()==a[pos])pos++;
if(a[pos]==a[t])
{
pb(spt);
for(int i=t+1;i<pos;i++)solve(a[i]);
pb(spt);
return pos;
}
int cnt=0,idx=id[a[pos]];
for(int i=t;i<pos;i++)if(id[a[i]]==idx)cnt++;
int ID=id[a[pos]];
int y=dq[ID].back();
if(cnt&1)
{
pb(spt);
dq[spt].push_back(a[t]);
for(int i=t+1;i<pos;i++)
{
if(a[i]==y)pb(ID);
else solve(a[i]);
}
pb(ID);
dq[ID].clear();
id[a[pos]]=id[y]=0;
id[a[t]]=spt;
stk.push(spt);
spt=ID;
}
else
{
pb(ID);
dq[ID].push_back(a[t]);
for(int i=t+1;i<pos;i++)
{
if(a[i]==y)pb(spt);
else solve(a[i]);
}
pb(spt);
del(ID,spt);
dq[ID].pop_front();
id[a[pos]]=0;
id[a[t]]=ID;
}
return pos;
}
int main()
{
int T=read();
while(T--)
{
n=read();m=read();k=read();
clear();
for(int i=1;i<=m;i++)a[i]=read();
for(int i=1;i<n;i++){stk.push(i);stk.push(i);}
for(int i=1;i<=m;i++)
{
int x=a[i];
if(solve(x))continue;
int tt=work(i);
i=tt;
}
cout<<ans.size()<<'\n';
for(auto val:ans)
{
if(val.second){cout<<"2 "<<min(val.first,val.second)<<" "<<max(val.first,val.second)<<'\n';}
else cout<<"1 "<<val.first<<'\n';
}
}
}
写在后面
我是很菜的,遇到构造就没招了。更何况这题比较 adhoc。
后来搞了一天才把这题搞明白(可见我有多菜)。但终究还是弄懂了,所以写篇题解来纪念一下(
特别鸣谢:@dbxxx
不管是他的题解还是本人都对我提供了很大帮助,感谢他专门抽出一天时间从早到晚不厌其烦给我讲解+调代码。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统