扫雷游戏中周围地雷数量的算法优化心得
最近跟一位大佬的视频,用vue3写一个纯前端的扫雷。(去给肉山大佬点个三连呀)
其中用grid布局好棋盘之后需要计算节点周围地雷数,dalao在这里留了一个优化作业。我先贴上大佬的做法,再分享一下我逐步优化(走弯路)的思路,以及一点心得。
一、待优化版本
const rows = ref(10) // 行数 const columns = ref(10) // 列数 const boobNumber = ref(10) // 炸弹数 const total = computed(() => rows.value * columns.value) // 总格子数 // 待优化初版 const grid = computed(() => { let arr: number[] = new Array(total.value).fill(0).fill(1, 0, boobNumber.value) // 初始化数组,并填入地雷 arr = arr.sort((a,b) => Math.random() - 0.5) // 随机排序 return arr.map((item, index) => { // 通过index计算位置 const units:number = index%rows.value const tens:number = index/columns.value >>0 let count = 0 // 嵌套for循环来计算几节点,同时Math.min和Math.max用来判定边界 for (let i = Math.max(0, tens-1); i < Math.min(tens+2, columns.value);i++){ for (let j = Math.max(0, units-1); j < Math.min(units+2, rows.value);j++){ if (arr[i*columns.value+j] === 1 && !(i === j && j === i)) { count++ } } } // 返回对象组成的数组,isBoob表示是否为炸弹节点,count是附近地雷数 return { isBoob: !item, count } }) })
二、几乎没提升,但是看起来厉害一点的优化
因为不喜欢嵌套for循环,联想到dfs算法计算小岛面积,所以列了一个位移数组来遍历
// 几乎没提升,但是看起来厉害一点的优化 const grid = computed(() => { let arr:number[] = new Array(total.value).fill(0).fill(1, 0, boobNumber.value) arr = arr.sort((a,b) => Math.random() - 0.5) // 位移数组 const list: number[][] = [[-1,-1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]] return arr.map((item, index) => { const units:number = index%rows.value const tens:number = index/columns.value >>0 let count:number = 0 // 循环位移数组来判断周围节点 list.map(i => { const y:number = tens+i[0] >= 0 ? (tens+i[0] < columns.value ? tens+i[0] : -1) :-1 const x:number = units+i[1] >= 0 ? (units+i[1] < rows.value ? units+i[1] : -1) :-1 if (arr[y*columns.value +x] && x>=0 && y>=0){ count++ } }) return { isBoob: item ? true : false, count } }) })
大佬表示这个优化,不太有效。“一般来说,优化,要跳出原先的思路。左手倒右手,用的力气是一样的”
三、计算减少,由计算所有节点变成计算地雷节点
这里算是在当前框架下的思路优化。之前在遍历节点时,每个节点都会计算一边。100个节点查找周围地雷。但是反过来做也可以,我们只计算地雷几点,遍历到地雷节点的时候就给周围节点的计数加1,这样要计算的节点数就变成十分之一了。不过循环数没有减少。
// 版本二:一样的遍历,但只计算地雷节点 const grid = computed(() => { let arr: number[] = new Array(total.value).fill(0).fill(1, 0, boobNumber.value) arr = arr.sort((a,b) => Math.random() - 0.5) const list: number[][] = [[-1,-1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]] const grid = arr.map((item) => {return { isBoob: item ? true : false, count: 0}} ) // 先赋值转换为对线的数组 grid.map((item, index) => { // 判断地雷节点进行计算 if(item.isBoob) { const units:number = index%rows.value const tens:number = index/columns.value >>0 list.map(i => { const y:number = tens+i[0] >= 0 ? (tens+i[0] < columns.value ? tens+i[0] : -1) :-1 const x:number = units+i[1] >= 0 ? (units+i[1] < rows.value ? units+i[1] : -1) :-1 if (x>=0 && y>=0){ grid[y*columns.value +x].count++ } }) } }) return grid })
这样的确实让计算减少了,算是进步。但是我个人觉得还是有点不好。为了改变周围节点,必须先map赋值一遍对线才可以。
所以我又搞了两个过渡的版本。
首先是为了不map赋值,直接fill了对象。但是这样的结果导致全部节点的count值一样了。
然后意识到,fill进去的其实是一个对象,并不是分开的多个,所以引用类型的变化会同步。
然后是又定义了一个数组来存储count,然后计算时操作这个数组,赋值时从这个数组中拿值。
我本意是,这样赋值的是最后计算完成的count,或者说,赋值引用类型会计算保持数据一致。但是实际上,赋值时只是基础类型的数字,所以地雷前面的节点算不到后面的雷。
这时候大佬继续提示,“能不能不要遍历整个数组,循环优化的第一点,减少循环次数”
四、各种方面减少循环
好的,按照dalao的说法,减少循环,第一个想办法的点在于赋值的map循环,本身数组在填充的时候如果能够一步到位就好了。但是又不能fill对象进去。
那么就fill数字吧,已知每个节点周围最多八个节点,也就是最多八颗地雷。那么这种情况count最大为8,而我可以给地雷节点的count赋值为9,这样就可以只用一个数字来表示节点的情况了。
同时减少循环,还可以提前终止这个循环,给炸弹计数,如果已经找到了全部炸弹就提前终止循环。(那这样的话,就不能使用map、foreach等等)
// 版本三 赋值和计算都不去全部遍历 const grid = computed(() => { // 数字大于等于9就为地雷节点 let arr: number[] = new Array(total.value).fill(0).fill(9, 0, boobNumber.value) arr = arr.sort((a,b) => Math.random() - 0.5) const list: number[][] = [[-1,-1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]] // 还是会遍历一遍全部数组 // arr.forEach((item, index) => { // if (item === 9) { // const units:number = index%rows.value // const tens:number = index/columns.value >>0 // list.map(i => { // const y:number = tens+i[0] >= 0 ? (tens+i[0] < columns.value ? tens+i[0] : -1) :-1 // const x:number = units+i[1] >= 0 ? (units+i[1] < rows.value ? units+i[1] : -1) :-1 // if (x>=0 && y>=0 && arr[y*columns.value +x] !== 9){ // arr[y*columns.value +x]++ // } // }) // } // }) // 最多会遍历一遍,但是可能会提前找到所以地雷而终止循环 for (let i=0,b=boobNumber.value;i<total.value && b>0;i++) { if (arr[i] >= 9) { const units:number = i%rows.value const tens:number = i/columns.value >>0 list.map(i => { const y:number = tens+i[0] >= 0 ? (tens+i[0] < columns.value ? tens+i[0] : -1) :-1 const x:number = units+i[1] >= 0 ? (units+i[1] < rows.value ? units+i[1] : -1) :-1 if (x>=0 && y>=0){ arr[y*columns.value +x]++ } }) b-- } } return arr })
五、最终版本
前面的版本其实是我在那个思路下想到的极致了,但是大佬表示,依然不是最优解,并且提示“我建议你从函数的一开始动手开始改”
这时候我马上想到我最开始设想扫雷棋盘的思路:用二维数组,然后循环炸弹数量,随机xy坐标填入炸弹,周围格子加一。
但是对于渲染棋盘,一维的数组用grid更方便。
所以结果就显而易见了。
// 最终版本 const grid = computed(() => { let arr: number[] = new Array(total.value).fill(0) const list: number[][] = [[-1,-1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]] // 循环炸弹数量 for (let b=0;b<boobNumber.value;) { // 随机数找index const index = Math.random()*total.value >>0 if (arr[index] <= 9) { // 此处没炸弹则放入炸弹,并循环数减1 b++ arr[index] = 9 const units:number = index%rows.value const tens:number = index/columns.value >>0 list.map(i => { const y:number = tens+i[0] >= 0 ? (tens+i[0] < columns.value ? tens+i[0] : -1) :-1 const x:number = units+i[1] >= 0 ? (units+i[1] < rows.value ? units+i[1] : -1) :-1 if (x>=0 && y>=0){ arr[y*columns.value +x]++ } }) } } return arr })
六、总结
这次的优化过程其实给我启发挺大的。
倒不是说难不难的问题,而且解决问题的过程带来了反思。如果再遇到这样的问题,应该怎么思考才能效率最高,弯路最少呢。
对于优化首先要从顶部开始思索,顶层策略还有没有可以改进的地方。再往下思考。好比进行二叉树的层次遍历一样。
像我第一步想的优化,就是最后一步,实际优化效果又一般,就属于纯纯无用功了。
而对于循环的优化,则要尽可能考虑怎么减少循环次数。
浙公网安备 33010602011771号