万圣节后的早晨——刘汝佳代码读后感
起因:紫书上说这道题非常经典,强烈推荐读者一试,我就想了二十几分钟,结合书上思路写了140行屎山,最后也跑不出结果不知道挂哪里了,就去看了题解,发现了一篇参考刘汝佳代码写的题解,被里面的代码技巧惊到了,寻思写一篇博客记录一下代码里的技巧和自己写的屎山对比一下,顺便膜一下刘汝佳大神%%%
下面是题解代码:(来源:洛谷用户baiABC)
#include <bits/stdc++.h> using namespace std; int G[150][5], deg[150], st[3], ed[3], d[150][150][150]; int mm(int a, int b, int c) { return (a << 16) | (b << 8) | c; } bool ct(int a, int b, int x, int y) { return b == y || (a == y && b == x); } int bfs() { memset(&d[0][0][0], -1, sizeof(d)); queue<int> q; q.push(mm(st[0], st[1], st[2])); d[st[0]][st[1]][st[2]] = 0; while (!q.empty()) { int x = q.front(), a = x >> 16, b = (x >> 8) & 0xff, c = x & 0xff; q.pop(); if (a == ed[0] && b == ed[1] && c == ed[2]) return d[a][b][c]; for (int i = 0; i < deg[a]; ++i) for (int j = 0; j < deg[b]; ++j) { if (ct(a, G[a][i], b, G[b][j])) continue; for (int k = 0; k < deg[c]; ++k) { if (ct(a, G[a][i], c, G[c][k]) || ct(b, G[b][j], c, G[c][k]) || d[G[a][i]][G[b][j]][G[c][k]] != -1) continue; d[G[a][i]][G[b][j]][G[c][k]] = d[a][b][c] + 1; q.push(mm(G[a][i], G[b][j], G[c][k])); } } } return -1; } int main() { int w, h, n; char s[20][20]; while (scanf("%d%d%d", &w, &h, &n), w || h || n) { int cnt = 0, id[20][20]; memset(deg, 0, sizeof deg); for (int i = 0; i < h; ++i) { scanf(" "); for (int j = 0; j < w; ++j) { s[i][j] = getchar(); if (s[i][j] != '#') { id[i][j] = ++cnt; G[cnt][deg[cnt]++] = cnt; if (s[i - 1][j] != '#') { G[cnt][deg[cnt]++] = id[i - 1][j]; G[id[i - 1][j]][deg[id[i - 1][j]]++] = cnt; } if (s[i][j - 1] != '#') { G[cnt][deg[cnt]++] = id[i][j - 1]; G[id[i][j - 1]][deg[id[i][j - 1]]++] = cnt; } if (islower(s[i][j])) st[s[i][j] - 'a'] = cnt; else if (isupper(s[i][j])) ed[s[i][j] - 'A'] = cnt; } } } if (n <= 2) { deg[++cnt] = 1; G[cnt][0] = cnt; st[2] = ed[2] = cnt; } if (n == 1) { deg[++cnt] = 1; G[cnt][0] = cnt; st[1] = ed[1] = cnt; } printf("%d\n", bfs()); } }
对比与技巧:
1.首先是状态表示:
我:还延续了上一道例题Fill的状态标识方法,开的结构体,其中含3容量数组代表三个鬼,然后写一个dist代表每个状态对应的步数。
刘汝佳:因为三个数都很小(地图是2^4*2^4的,因此二维转化为一维后最大数为((1<<8)-1),因此每个数不超过8bit占位,所以刘汝佳用了一个int通过移位来存储三个数的状态,这样简化了状态让BFS写起来清晰简短)
2.标记数组的设计:
我:用三维bool数组st标记每个状态是否走过。
刘汝佳:虽然他可以用st数组或哈希标记他的移位状态,但是他直接用d距离数组初始化-1代表点未走过,因为BFS的特殊性质,每个状态首次被走到就一定是最短的,所以完全可以舍弃st数组,st=false等价于d=-1。
3.始末状态:
我:开s数组和ed数组,在录入图g后遍历图g,根据图上的字母来决定给s数组和ed数组的哪些点赋值。
刘汝佳:直接用函数isupper和islower判断大小写,然后char类型减去起始字母获得下表,直接记录。
4.二维转一维:
我:我用get_p(int x,int y)返回的坐标,每次计算坐标需要重新调用。
刘汝佳:直接用id和cnt记录每个空格点的唯一坐标,一劳永逸避免重复计算。
5.BFS建图:
由于紫书上提到了这一点,所以我和刘汝佳的建图都是:放弃了dx和dy每个鬼for循环五次枚举行为,而是提前建好空格之间的连边,到时候直接枚举每个空格的出边即可。
注意:刘汝佳的代码没有用dx和dy数组枚举5个方位,而是自己建双向边,只考虑自边、上边、左边即可完成所有空格点之间的建图。
6.图的保存:
我:用链式前向星搞得建边(实际上我只会用这个和vector邻接表建边,邻接矩阵都没用过),枚举空白点的时候看“上,下,左,右,不动”五种方案哪些合法,合法就往对应的get_p坐标连边。
刘汝佳:我觉得他的G数组是一种特殊的邻接数组,因为一个空格最多邻接五个点,所以直接就第二维容量设为5,然后第二维的每个下标对应坐标值为第一维的点的第i个方向的邻接坐标值。然后用deg数组统计一下每个点有多少出边,相当于vector邻接表中的size。(值得一提的是,这道题的特殊性质使得空格点的个数可能远不及255个那么多,因此他的坐标值容量只开到150)。
7.对于一只鬼和两只鬼的情况:
我:由于我在连边的时候用的链式前向星,解析图G的时候把该连的都连了,又用结构体中的顶点数组统计鬼的坐标值,因此我的做法根本就不能在鬼小于3只的情况下去限制其他鬼的走法,这也是我觉得我代码TLE的主要原因之一。
刘汝佳:用了两个if,完美覆盖了n=1和n=2的情况,他直接把不存在的鬼的起始坐标的邻接点设为仅有自己,这样相当于不存在的鬼也不走了。
8.如何枚举鬼的行动组合:
我:沿用上一道例题的方法,两层for循环枚举新旧状态的顶点数组,判断是否有旧【i】==新【j】且旧【j】==新【i】,这样又可以判断新旧两点互换,又可以判断俩人走一起去了的情况,当时还觉得自己真是活学活用。
刘汝佳:直接用ct函数判断两个鬼的新旧坐标值是否冲突,枚举了前两个行为就判断前两个行为,枚举了第三个行为,再用生成的两个合法状态分别判断第三个行为。
总结:
第一次看别人代码这么大反应,感觉刘汝佳都这么极力推荐的题(并且也确实用了很多感觉以后会常用的技巧)写一篇总结也挺值的。真正体会到了别人常说的看大佬的代码会学到东西这句话,我的思路与刘汝佳无异,但是代码方面差了一大截,感觉真得好好研究研究大佬们的代码小技巧弥补不足了...

浙公网安备 33010602011771号