P1312 [NOIP 2011 提高组] Mayan 游戏
两个半小时大战暴力大法师,拼尽全力无法战胜。
首先这个题数据这么小,肯定是爆搜。一般这种题都不是很好写,一定要注意细节,不要写错变量名,不要脑抽写错一些奇奇怪怪的地方,不然你都不知道挂哪了。
为了方便起见,我们写一些会用到的函数:
1. ept(p)
判断二维数组 \(p\) 是否全部清零。如果是则返回 1,否则返回 0,代码实现十分好写:
ept
inline bool ept(int p[N][N]){
for(int i=0;i<5;i++){
for(int j=0;j<7;j++){
if(p[i][j]) return 0;
}
}
return 1;
}
2. copy(p,q)
将 \(q\) 数组的值拷贝给 \(p\)。用来处理 dfs 回溯时的还原问题及下落时的重新赋值问题。
这个也很简单捏,直接 \(7 \times 5\) 枚举赋值即可。
copy
inline void copy(int p[N][N],int q[N][N]){
for(int i=0;i<5;i++){
for(int j=0;j<7;j++){
p[i][j]=q[i][j];
}
}
}
3. drop(p)
用来处理 \(p\) 数组某些方块要下落的问题的,稍微复杂了一点。但总之也很简单,新开一个 \(c\) 数组,然后枚举每一排,将所有非 0 数字放在底部,最后 \(copy(p,c)\) 即可。
drop
inline void drop(int p[N][N]){
int c[N][N];
for(int i=0;i<5;i++){
int k=-1;
//k:指针,指向当前c数组存储到哪里了
for(int j=0;j<7;j++) c[i][j]=0;
for(int j=0;j<7;j++){
if(p[i][j]) c[i][++k]=p[i][j];
//非0就让它"下落"
}
}
//不要忘了最后的copy
copy(p,c);
}
4. clr(p)
用来处理 \(p\) 数组消元的问题的。一方面它会返回当前是否能消元,另一方面它能将所有当前可以消元的位置全部消成 0。(消元过程中不考虑方块下落)
clear
inline bool clr(int p[N][N]){
//警示后人:不要写错变量名
//横向消除
bool fl=0;
for(int j=0;j<7;j++){
for(int i=0;i<3;i++){
//当前格如果没有数字就别消了
if(!p[i][j]) continue;
int xx;
//注意细节边界问题
//找到最靠下的xx
for(xx=i;xx+1<5&&p[xx+1][j]==p[i][j];xx++);
if(xx-i+1>=3){//可消
//注意,此时竖向也可能消,所以要额外判断竖直方向。
for(int k=i;k<=xx;k++){
int up=j,dn=j;
//判好边界条件
while(up+1<7&&p[k][j]==p[k][up+1]) up++;
while(dn>0&&p[k][j]==p[k][dn-1]) dn--;
if(up-dn+1>=3){//竖向可消
for(int l=dn;l<=up;l++) p[k][l]=0;
}
else p[k][j]=0;//竖向不可消
}
fl=1;//进行了消除
}
}
}
//纵向消除(注释同上)
for(int i=0;i<5;i++){
for(int j=0;j<5;j++){
if(!p[i][j]) continue;
int yy;
//注意细节边界问题
for(yy=j;yy+1<7&&p[i][j]==p[i][yy+1];yy++);
if(yy-j+1>=3){
for(int k=j;k<=yy;k++){
int up=i,dn=i;
//注意不要把k打成j(我当时就这么干的。。。)
while(up<4&&p[up][k]==p[up+1][k]) up++;//
while(dn>1&&p[dn][k]==p[dn-1][k]) dn--;//
if(up-dn+1>=3){
for(int l=dn;l<=up;l++) p[l][k]=0;
}
else p[i][k]=0;
}
fl=1;
}
}
}
return fl;
}
5. dfs
然后就到了我们的 \(dfs\)。它就是很普通的 \(dfs\),用 \(stp\) 表示当前在走第几步,如果 \(stp > n\),那么直接判断 \(a\) 数组是否为空,是则为正解直接输出。
这里有一处剪枝:如果当前某个颜色的块有 1 个或 2 个时,它们无论如何都消不掉,所以直接返回。
否则我们从小到大枚举每个可能移动的位置。
这里又有一个小优化:因为移动的本质是两个位置的数交换(这里可以思考一下),所以当我们想让 \((i,j)\) 与 \((i+1,j)\) 交换的的话,除非 \(a_{i,j}=0\),否则直接让 \((i,j)\) 右移即可。否则让 \((i+1,j)\) 左移。
然后就可以存询问了,注意要有一个小标记,表示当前是从 \((i,j)\) 左移换过来还是 \((i+1,j)\) 右移换过来。
换过来以后,一要考虑方格第一次坠落的问题,二是考虑多次消除方块并坠落的结果。
(是的,这里的逻辑是这样的:不管你能否消除方块都要坠落一次,否则后续你处理 \(clr\) 时有一部分没有坠下去考虑不到)
(另外,就算你后续不 \(clr\) 后续处理也会有其他的错误。)
(另外,题目提示我们有可能会有多次消除操作,所以我们也要执行多次,并且每一次都需要 \(drop\) 一下,否则理由同上)
然后就可以愉快地往下操作了。
最后不要忘了还原 \(a\) 数组。
dfs
inline void dfs(int stp){
if(stp>n){
if(ept(a)){
for(int i=1;i<=n;i++){
//注意交换完后真正移动的是哪里
if(ans[i].op==-1) printf("%lld %lld %lld\n",ans[i].x+1,ans[i].y,ans[i].op);
else printf("%lld %lld %lld\n",ans[i].x,ans[i].y,ans[i].op);
}
exit(0);
}
//注意就算不合法也要先返回
return ;
}
//判断当前是否已经无解
for(int i=0;i<=10;i++){
sum[i]=0;
}
for(int i=0;i<5;i++){
for(int j=0;j<7;j++){
sum[a[i][j]]++;
}
}
for(int i=1;i<=10;i++){
if(0<sum[i]&&sum[i]<3) return ;
}
for(int i=0;i<4;i++){
for(int j=0;j<7;j++){
//两个位置至少要有一个位置有数,否则不合法
if(a[i][j]||a[i+1][j]){
//b数组一定要开在dfs里面,否则你a回溯回来b也变了
int b[N][N];
copy(b,a);
//注意语句位置
ans[stp]={i,j,(a[i][j]?1:-1)};
swap(a[i][j],a[i+1][j]);
drop(a);
while(clr(a)) drop(a);
dfs(stp+1);
//一定要记得把a数组还原回来
copy(a,b);
}
}
}
}
好啦,该讲的都讲完了,放一下完整版代码:
我已是完全之P1312
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
int x=0,f=1;char c=getchar();
while(c<48){
if(c=='-') f=-1;
c=getchar();
}
while(c>47) x=(x<<1)+(x<<3)+(c^48),c=getchar();
return x*f;
}
const int N=12;
int n,a[N][N],sum[N],qwq=0;
struct sw{
int x,y,op;
}ans[N];
inline bool ept(int p[N][N]){
for(int i=0;i<5;i++){
for(int j=0;j<7;j++){
if(p[i][j]) return 0;
}
}
return 1;
}
inline void copy(int p[N][N],int q[N][N]){
for(int i=0;i<5;i++){
for(int j=0;j<7;j++){
p[i][j]=q[i][j];
}
}
}
inline void drop(int p[N][N]){
int c[N][N];
for(int i=0;i<5;i++){
int k=-1;
//k:指针,指向当前c数组存储到哪里了
for(int j=0;j<7;j++) c[i][j]=0;
for(int j=0;j<7;j++){
if(p[i][j]) c[i][++k]=p[i][j];
//非0就让它"下落"
}
}
//不要忘了最后的copy
copy(p,c);
}
inline bool clr(int p[N][N]){
//警示后人:不要写错变量名
//横向消除
bool fl=0;
for(int j=0;j<7;j++){
for(int i=0;i<3;i++){
//当前格如果没有数字就别消了
if(!p[i][j]) continue;
int xx;
//注意细节边界问题
//找到最靠下的xx
for(xx=i;xx+1<5&&p[xx+1][j]==p[i][j];xx++);
if(xx-i+1>=3){//可消
//注意,此时竖向也可能消,所以要额外判断竖直方向。
for(int k=i;k<=xx;k++){
int up=j,dn=j;
//判好边界条件
while(up+1<7&&p[k][j]==p[k][up+1]) up++;
while(dn>0&&p[k][j]==p[k][dn-1]) dn--;
if(up-dn+1>=3){//竖向可消
for(int l=dn;l<=up;l++) p[k][l]=0;
}
else p[k][j]=0;//竖向不可消
}
fl=1;//进行了消除
}
}
}
//纵向消除(注释同上)
for(int i=0;i<5;i++){
for(int j=0;j<5;j++){
if(!p[i][j]) continue;
int yy;
//注意细节边界问题
for(yy=j;yy+1<7&&p[i][j]==p[i][yy+1];yy++);
if(yy-j+1>=3){
for(int k=j;k<=yy;k++){
int up=i,dn=i;
//注意不要把k打成j(我当时就这么干的。。。)
while(up<4&&p[up][k]==p[up+1][k]) up++;//
while(dn>1&&p[dn][k]==p[dn-1][k]) dn--;//
if(up-dn+1>=3){
for(int l=dn;l<=up;l++) p[l][k]=0;
}
else p[i][k]=0;
}
fl=1;
}
}
}
return fl;
}
inline void dfs(int stp){
if(stp>n){
if(ept(a)){
for(int i=1;i<=n;i++){
//注意交换完后真正移动的是哪里
if(ans[i].op==-1) printf("%lld %lld %lld\n",ans[i].x+1,ans[i].y,ans[i].op);
else printf("%lld %lld %lld\n",ans[i].x,ans[i].y,ans[i].op);
}
exit(0);
}
//注意就算不合法也要先返回
return ;
}
//判断当前是否已经无解
for(int i=0;i<=10;i++){
sum[i]=0;
}
for(int i=0;i<5;i++){
for(int j=0;j<7;j++){
sum[a[i][j]]++;
}
}
for(int i=1;i<=10;i++){
if(0<sum[i]&&sum[i]<3) return ;
}
for(int i=0;i<4;i++){
for(int j=0;j<7;j++){
//两个位置至少要有一个位置有数,否则不合法
if(a[i][j]||a[i+1][j]){
//b数组一定要开在dfs里面,否则你a回溯回来b也变了
int b[N][N];
copy(b,a);
//注意语句位置
ans[stp]={i,j,(a[i][j]?1:-1)};
swap(a[i][j],a[i+1][j]);
drop(a);
while(clr(a)) drop(a);
dfs(stp+1);
//一定要记得把a数组还原回来
copy(a,b);
}
}
}
}
signed main(){
n=read();
for(int i=0;i<5;i++){
//警示后人:7列,输入输满了有可能j=7,所以你 j<7 会 WA on #6
for(int j=0;j<8;j++){
a[i][j]=read();
if(!a[i][j]) break;
}
}
dfs(1);
printf("-1");
return 0;
}

浙公网安备 33010602011771号