一次脑瘫事件 - 广搜八数码问题

fireale

2023-04-30 15:28:10

Personal

一次脑瘫事件 - 关于广搜八数码问题一个隐秘的 Bug

好吧我真不知道我能遇到这样的bug

------------ ### 1. 首先贴一下[原题(P1379)](https://www.luogu.com.cn/problem/P1379) ------------ ------------ ## 八数码难题 ### 题目描述 在 $3\times 3$ 的棋盘上,摆有八个棋子,每个棋子上标有 $1$ 至 $8$ 的某一数字。棋盘中留有一个空格,空格用 $0$ 来表示。空格周围的棋子可以移到空格中。要求解的问题是:给出一种初始布局(初始状态)和目标布局(为了使题目简单,设目标状态为 $123804765$),找到一种最少步骤的移动方法,实现从初始布局到目标布局的转变。 ### 输入格式 输入初始状态,一行九个数字,空格用 $0$ 表示。 ### 输出格式 只有一行,该行只有一个数字,表示从初始状态到目标状态需要的最少移动次数。保证测试数据中无特殊无法到达目标状态数据。 ### 样例 #1 #### 样例输入 #1 ``` 283104765 ``` #### 样例输出 #1 ``` 4 ``` ### 提示 #### 样例解释 ![](https://cdn.luogu.com.cn/upload/image_hosting/7rhxbnup.png) 图中标有 $0$ 的是空格。绿色格子是空格所在位置,橙色格子是下一步可以移动到空格的位置。如图所示,用四步可以达到目标状态。 并且可以证明,不存在更优的策略。 ------------ ------------ ### 2. 分析 ~~一眼丁真,~~ 最少移动步数,鉴定为广搜 至于判重,声明一个9维数组vis,利用桶的思想我们执行 $if(vis[s[0]][s[1]][s[2]]...[s[8]])$ ?虽然可以 $O(1)$ 时间判重,但是 $9$ 维数组每一维都要包含 $9$ 个元素,总共 $9^9=387420489$ 项,太多了,数组开不了这么大 注意到可以把 $9$ 个数字从左到右、从上到下**排成排列数**,鉴定为康托展开,再写个逆康托展开 。这样就只有 $9!=362880$ 项,数组存得了了。至于康托展开和逆康托展开是什么,可以看看[百度百科词条](https://baike.baidu.com/item/%E5%BA%B7%E6%89%98%E5%B1%95%E5%BC%80) 貌似排成一排之后还有一个**特色**:根据字符串存储状态,设空格当前位置为 $P$ ,则有: 1. 空格向上移动:空格位置减 $3$ ,即交换 $P$ 和 $P-3$ 的字符 2. 空格向左移动:空格位置减 $1$ ,即交换 $P$ 和 $P-1$ 的字符 3. 空格向右移动:空格位置加 $1$ ,即交换 $P$ 和 $P+1$ 的字符 4. 空格向下移动:空格位置加 $3$ ,即交换 $P$ 和 $P+3$ 的字符 如果设规则编号为 $k$ ,则上述四条规则可归纳为一条:**交换 $P$ 和 $P+(2*k-5)$ 的字符**,照这样就好写状态转移了 直接开写!—— ```cpp #include<iostream> #include<cstdio> #include<cmath> #include<cstring> //#include<cctype> #include<algorithm> #define INF 0x3f3f3f3f using namespace std; const int maxn=362881,dx[]={0,1,-1,0,0},dy[]={0,0,0,1,-1}, fac[]={1,1,2,6,24,120,720,5040,40320,362880}; //struct ikun{ // int x,y,step; //}q[maxn]; int n,fi,que[maxn],ti,step[maxn]; bool memo[maxn+10]; string ts,tts; inline int Max(int a,int b){return a>b?a:b;}; void bfs(int x,int y); int turn(string a); string inturn(int a); int main(){ fi=turn("123804765"); cin>>ts; int head=0,tail=1,z; que[1]=turn(ts),step[1]=0; // printf("%d\n",que[1]); // string ttt=inturn(que[1]); // cout<<ttt; if(que[1]==fi){ printf("0"); return 0; } while(tail>head){ head++,ts=inturn(que[head]); for(int i=1;i<=4;i++){ tts=ts; for(z=0;z<9;z++) if(tts[z]=='0') break; int aim=z+2*i-5; if(!(aim>=0&&aim<9)) continue; swap(tts[z],tts[aim]); ti=turn(tts); if(!memo[ti]){ tail++,memo[ti]=true,que[tail]=ti,step[tail]=step[head]+1; if(ti==fi){ printf("%d",step[tail]); return 0; } } } } printf("-1"); return 0; } int turn(string a){ int sum=0,cnt; for(int i=0;i<9;i++){ cnt=0; for(int j=i+1;j<9;j++) if(a[j]<a[i]) cnt++; sum+=cnt*fac[8-i]; } return sum; } string inturn(int a){ string str; int b,c,sum,num[9]; bool pl[10]={0}; for(int i=8;i>=0;i--){ b=a/fac[i],c=a-fac[i]*b,sum=0; for(int j=0;j<=8;j++){ if(pl[j]) continue; sum++; if(sum==b+1) num[8-i]=j,pl[j]=true; } a=c; } for(int i=0;i<9;i++) str=str+(char)(num[i]+'0'); return str; } ``` 诶,感觉还行,测个样例—— ``` in:283104765 out:4 correct:4 ``` 噫!好!我中了!!直接交罢—— ### [**$25$ 分。**](https://www.luogu.com.cn/record/107949468) 啊???怎么会事呢?测个错误点看看—— ``` in:603712458 out:17 correct:23 ``` 嗯……改成用 $set$ 和 $char$ 数组试试,可能是康托展开(或者逆康托展开)或者是 $string$ 的毛病吧 \*小改 ```cpp #include<iostream> #include<cstdio> #include<cmath> #include<cstring> //#include<cctype> #include<algorithm> #include<set> #define INF 0x3f3f3f3f using namespace std; const int maxn=362881,dx[]={0,1,-1,0,0},dy[]={0,0,0,1,-1}, fac[]={1,1,2,6,24,120,720,5040,40320,362880}; int n,fi,que[maxn],ti,step[maxn]; //bool memo[maxn+10]; //string ts,tts; int ts,tts; set<int> memo; inline int Max(int &a,int &b){return a>b?a:b;}; //int turn(string a); //string inturn(int a); int change(int aim,int &p1,int &p2); int main(){ memo.clear(); // fi=turn("123804765"); fi=123804765; // cin>>ts; scanf("%d",&que[1]); int head=0,tail=1,z; // que[1]=turn(ts),step[1]=0; step[1]=0; // printf("%d\n",que[1]); // string ttt=inturn(que[1]); // cout<<ttt; if(que[1]==fi){ printf("0"); return 0; } while(tail>head){ // head++,ts=inturn(que[head]); head++,ts=que[head]; for(int i=1;i<=4;i++){ tts=ts; // for(z=0;z<9;z++) // if(tts[z]=='0') // break; for(z=9;z>=1;z--){ if(tts%10==0) break; tts/=10; } tts=ts; int aim=z+2*i-5; if(!(aim>=0&&aim<9)) continue; // swap(tts[z],tts[aim]); // ti=turn(tts); ti=change(tts,z,aim); if(!memo.count(ti)){//!memo[ti] // tail++,memo[ti]=true,que[tail]=ti,step[tail]=step[head]+1; tail++,memo.insert(ti),que[tail]=ti,step[tail]=step[head]+1; if(ti==fi){ printf("%d",step[tail]); return 0; } } } } printf("-1"); return 0; } // //int turn(string a){ // int sum=0,cnt; // for(int i=0;i<9;i++){ // cnt=0; // for(int j=i+1;j<9;j++) // if(a[j]<a[i]) // cnt++; // sum+=cnt*fac[8-i]; // } // return sum; //} // //string inturn(int a){ // string str; // int b,c,sum,num[9]; // bool pl[10]={0}; // for(int i=8;i>=0;i--){ // b=a/fac[i],c=a-fac[i]*b,sum=0; // for(int j=0;j<=8;j++){ // if(pl[j]) // continue; // sum++; // if(sum==b+1) // num[8-i]=j,pl[j]=true; // } // a=c; // } // for(int i=0;i<9;i++) // str=str+(char)(num[i]+'0'); // return str; //} int change(int n,int &p1,int &p2){ int temp[10]; for(int i=9;i>=1;i--) temp[i]=n%10,n/=10; temp[p1]^=temp[p2],temp[p2]^=temp[p1],temp[p1]^=temp[p2],n=0; for(int i=1;i<=9;i++) n=(n*10+temp[i]); return n; } ``` 再试试样例—— ``` in:283104765 out:4 correct:4 ``` 嗯……不好说,测个之前的错误点吧—— ``` in:603712458 out:17 correct:23 ``` ……好吧,看来不是康托展开(或逆康托展开)的错 并且写的时候调试过康托展开和逆康托展开,确实没错啊…… $string$ 也应该不会错吧…… \*于是若智的我盯着代码看了整整两天都没找到 $bug$ ,两天后: 算了,去看看讨论吧—— ``` “你程序的tx有问题, 比如如果'0'在边界上的话, 就不能向左移了,如下例: 1 2 3 0 4 5 6 7 8 这个例子的空格就不能向左移” ``` [原贴](https://www.luogu.com.cn/discuss/196975) ### **!!!!!!!!!!!!!!!!!!!!!!!!** 千想万想没想到这一层——存储与实际的差别:在用一维数组存储和状态转移的时候, $1\ 2\ 3\ 0\ 4\ 5\ 6\ 7\ 8$ 把 $3$ 和 $5$ 交换变成 $1\ 2\ 0\ 3\ 4\ 5\ 6\ 7\ 8$ 看来是显而易见符合这个精炼的公式:**交换 $P$ 和 $P+(2*k-5)$ 的字符**,但是在二维里面却是不合理的移动 于是我只用了 $2$ 分钟加一点判断就改好了困扰我两天的 $bug$ —— ```cpp if(!(aim>=0&&aim<9)) continue; ↓改成 if(!(aim>0&&aim<=9)||(z%3==1&&aim%3==0)||(z%3==0&&aim%3==1)) continue; ``` 顺利的过了样例和错误点—— ``` in:283104765 out:4 correct:4 in:603712458 out:23 correct:23 ``` [也AC了](https://www.luogu.com.cn/record/108020669) ------------ ### 3. 尾声 \*去世