从此无心爱良夜,任他明月下西楼

扫雷游戏中周围地雷数量的算法优化心得

最近跟一位大佬的视频,用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
})

六、总结

这次的优化过程其实给我启发挺大的。

倒不是说难不难的问题,而且解决问题的过程带来了反思。如果再遇到这样的问题,应该怎么思考才能效率最高,弯路最少呢。

对于优化首先要从顶部开始思索,顶层策略还有没有可以改进的地方。再往下思考。好比进行二叉树的层次遍历一样。

像我第一步想的优化,就是最后一步,实际优化效果又一般,就属于纯纯无用功了。

而对于循环的优化,则要尽可能考虑怎么减少循环次数。

 

posted @ 2022-09-22 14:24  明月下  阅读(365)  评论(0)    收藏  举报

页脚