使用js解数独难题

不想你离开。 提交于 2020-10-24 17:27:12

在线数独,这个网站有解答

https://www.sudoku.name/index-cn.php

 

 

算法思路

使用二维数组表示棋盘,0表示空

先计算出每个空可能的值的集合

依次进行dfs搜索,每次确定一个值之后,将该值所在的九宫和横竖所有集合进行更新,即减去该值

直至搜索结束

 

优化思路

使用一维数组表示棋盘,每个数字0-9位表示可能的值的集合,10位表示该值是否是空

使用位运算加速

同时依赖了是否为空,可以只对空进行搜索,避免多个空删除一个值后变为单个数时无法确定是否是空,必须使用check函数的问题

 

 

const _ = require('lodash')

// 0 表示未知
const INIT_MAP =
  // 难
  // [
  //   [8, 0, 0, 0, 0, 0, 0, 0, 0],
  //   [0, 0, 3, 6, 0, 0, 0, 0, 0],
  //   [0, 7, 0, 0, 9, 0, 2, 0, 0],
  //   [0, 5, 0, 0, 0, 7, 0, 0, 0],
  //   [0, 0, 0, 0, 4, 5, 7, 0, 0],
  //   [0, 0, 0, 1, 0, 0, 0, 3, 0],
  //   [0, 0, 1, 0, 0, 0, 0, 6, 8],
  //   [0, 0, 8, 5, 0, 0, 0, 1, 0],
  //   [0, 9, 0, 0, 0, 0, 4, 0, 0]
  // ]


// [  //容易
//   [8, 2, 0, 5, 0, 0, 0, 0, 7],
//   [4, 0, 0, 0, 0, 6, 8, 0, 0],
//   [0, 5, 7, 8, 0, 0, 6, 0, 0],
//   [0, 0, 0, 3, 0, 0, 0, 2, 4],
//   [7, 0, 0, 4, 0, 1, 0, 0, 0],
//   [0, 8, 4, 0, 2, 0, 1, 0, 0],
//   [9, 0, 0, 0, 3, 0, 0, 5, 8],
//   [0, 7, 0, 0, 8, 0, 9, 0, 6],
//   [0, 0, 8, 0, 5, 2, 0, 7, 0]
// ]

// [  //容易
//   [7, 4, 8, 1, 2, 3, 6, 5, 9],
//   [2, 6, 3, 4, 9, 5, 1, 8, 7],
//   [1, 5, 9, 6, 7, 8, 2, 4, 3],
//   [4, 1, 5, 3, 6, 7, 9, 2, 8],
//   [3, 8, 2, 9, 5, 4, 7, 6, 1],
//   [9, 7,6, 8, 1, 2, 4, 3, 5],
//   [5, 2, 4, 7, 3, 9, 8, 1, 6],
//   [8, 9, 1, 5, 4, 6, 3, 7, 2],
//   [6, 3, 7, 2, 8, 1, 5, 9, 4]
// ]

//
// [  //容易
//   [7, 4, 0, 1, 2, 0, 6, 5, 0],
//   [2, 6, 3, 4, 9, 5, 1, 8, 7],
//   [0, 5, 9, 6, 7, 8, 0, 4, 3],
//   [0, 1, 0, 0, 0, 0, 0, 2, 8],
//   [0, 8, 2, 0, 0, 4, 0, 6, 1],
//   [0, 7, 6, 0, 0, 0, 0, 3, 0],
//   [0, 2, 4, 0, 3, 0, 8, 1, 6],
//   [8, 9, 0, 5, 4, 0, 3, 7, 2],
//   [0, 3, 0, 2, 0, 1, 5, 9, 0]
// ]

// [  //容易
//   [0, 0, 0, 1, 2, 3, 6, 5, 0],
//   [0, 0, 0, 4, 9, 5, 1, 8, 0],
//   [0, 0, 0, 6, 7, 8, 2, 4, 0],
//   [4, 1, 5, 3, 6, 7, 0, 0, 0],
//   [3, 8, 2, 9, 5, 4, 7, 6, 1],
//   [9, 7, 6, 8, 1, 2, 4, 3, 5],
//   [0, 2, 4, 7, 3, 9, 8, 1, 0],
//   [0, 9, 1, 5, 4, 6, 3, 7, 0],
//   [0, 0, 0, 2, 8, 1, 0, 0, 0]
// ]

  // [ // 容易
  //   [0, 9, 0, 6, 0, 0, 3, 0, 0],
  //   [0, 3, 0, 0, 5, 0, 0, 2, 8],
  //   [5, 0, 1, 0, 0, 2, 7, 0, 0],
  //   [0, 0, 6, 1, 0, 8, 0, 0, 2],
  //   [0, 1, 0, 0, 0, 0, 0, 3, 0],
  //   [8, 0, 0, 5, 0, 9, 6, 0, 0],
  //   [0, 0, 9, 7, 0, 0, 4, 0, 3],
  //   [1, 4, 0, 0, 2, 0, 0, 9, 0],
  //   [0, 0, 8, 0, 0, 6, 0, 7, 0]
  // ]
  // 7,9,2,6,8,1,3,4,5
// 6,3,4,9,5,7,1,2,8
// 5,8,1,3,4,2,7,6,9
// 4,7,6,1,3,8,9,5,2
// 9,1,5,2,6,4,8,3,7
// 8,2,3,5,7,9,6,1,4
// 2,6,9,7,1,5,4,8,3
// 1,4,7,8,2,3,5,9,6
// 3,5,8,4,9,6,2,7,1


  [
    [0, 0, 5, 0, 0, 4, 0, 7, 0],
    [9, 0, 0, 0, 0, 7, 0, 0, 0],
    [0, 0, 3, 0, 0, 0, 2, 0, 0],
    [0, 5, 0, 0, 8, 0, 0, 9, 0],
    [0, 0, 0, 0, 0, 3, 0, 0, 0],
    [0, 7, 0, 0, 0, 0, 0, 2, 0],
    [0, 0, 2, 0, 0, 0, 8, 0, 0],
    [0, 0, 0, 5, 0, 0, 0, 0, 4],
    [0, 6, 0, 9, 0, 0, 5, 0, 0],
  ]

// 1,2,5,3,6,4,9,7,8
// 9,4,6,8,2,7,1,5,3
// 7,8,3,1,5,9,2,4,6
// 2,5,4,6,8,1,3,9,7
// 6,1,9,2,7,3,4,8,5
// 3,7,8,4,9,5,6,2,1
// 5,3,2,7,4,6,8,1,9
// 8,9,1,5,3,2,7,6,4
// 4,6,7,9,1,8,5,3,2

function range(n = 9) {
  return Array(n).fill(0).map((_, i) => i + 1)
}

// console.log(getSet(INIT_MAP, 0, 8))

// 根据一个状态,计算某个点的可能数字集合
// 改点必须为确定的数字
function getSet(m, x, y) {
  // 确定的数字
  if (1 < m[x][y] && m[x][y] <= 9)
    return new Set([m[x][y]])

  // 一个数字的集合,返回该集合的拷贝
  if (m[x][y].size === 1)
    return new Set([...m[x][y]]);

  // 默认9个数,然后去除不合法的
  let s = new Set(range(9))

  for (let i = 0; i < 9; i++) {
    if (i === x) continue
    // 去除列中的重复数
    if (m[i][y] > 0 && m[i][y] <= 9)
      s.delete(m[i][y])
    if (m[x][i].size === 1)
      s.delete(...m[i][y])
  }

  for (let i = 0; i < 9; i++) {
    if (i === y) continue
    // 去除行的重复数
    if (m[x][i] > 0 && m[x][i] <= 9)
      s.delete(m[x][i])
    if (m[x][i].size === 1)
      s.delete(...m[x][i])
  }

  // 去除九宫格中的重复数
  let stx = Math.floor(x / 3) * 3// 左上角x
  let sty = Math.floor(y / 3) * 3// 左上角y
  for (let i = 0; i < 3; i++)
    for (let j = 0; j < 3; j++) {
      let nx = stx + i
      let ny = sty + j
      let v = m[nx][ny]
      // console.log(nx, ny)
      if (v > 0 && v <= 9)
        s.delete(v)
      if (v.size === 1)
        s.delete(...v)
    }

  return s
}

// 根据初始状态,获取每个位置可能的数字集合
function init(m) {
  let res = Array(9).fill(0).map(
    () => Array(9).fill(0)
  )
  for (let i = 0; i < 9; i++)
    for (let j = 0; j < 9; j++) {
      if (0 < m[i][j] && m[i][j] <= 9) {
        // 如果是数字
        res[i][j] = new Set([m[i][j]])
      } else {
        // 如果是未知
        res[i][j] = getSet(m, i, j)
      }
    }
  return res
}

// 深度拷贝一个对象
function copy(obj) {
  return _.cloneDeep(obj)
}

// 删除m,x,y相关的数,横纵,九宫格
function delSet(m, x, y, v) {
  // 删除 行和列中的数字
  for (let i = 0; i < 9; i++) {
    m[i][y].delete(v)
    m[x][i].delete(v)
  }

  // 去除九宫格中的重复数
  let stx = Math.floor(x / 3) * 3// 左上角x
  let sty = Math.floor(y / 3) * 3// 左上角y
  for (let i = 0; i < 3; i++)
    for (let j = 0; j < 3; j++) {
      let nx = stx + i
      let ny = sty + j
      m[nx][ny].delete(v)
    }

  m[x][y] = new Set([v])
  for (let i = 0; i < 9; i++)
    for (let j = 0; j < 9; j++)
      if (m[i][j].size === 0) return false

  // true 表示可以是该数字
  // 细致的检查
  return check(m)
}

// 检测是否满足要求
function check(m) {
  // console.log(toStr(m))
  // let rows = m.map(x => new Set(x))
  // let cols = []
  for (let i = 0; i < 9; i++) {
    let c = new Set()
    let r = new Set()
    for (let j = 0; j < 9; j++) {
      let colVal = m[i][j]
      let rowVal = m[j][i]
      if (colVal.size === 1) {
        if (c.has(...colVal))
          return false
        c.add(...colVal)

      }
      if (rowVal.size === 1) {
        if (r.has(...rowVal))
          return false
        r.add(...rowVal)

      }
    }
    // console.log('check', c, r)
  }

  // 九宫格
  for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {

      let s = new Set()
      for (let x = 0; x < 3; x++) {
        for (let y = 0; y < 3; y++) {
          let nx = i * 3 + x
          let ny = j * 3 + y
          let v = m[nx][ny]
          if (v.size > 1) continue

          if (s.has(...v))
            return false
          s.add(...v)
        }
      }
    }
  }

  return true
  // let f1 = rows.every(x => x.size === 9)
  // let f2 = cols.every(x => x.size === 9)
  //
  // return f1 && f2
}

function toStr(m) {
  // console.log(m)
  let s = m.map(
    row => row.map(x => [...x].join(''))
  )
  return s.join('n')
}


// 从x,y 开始找一个需要填补的位置,即集合的尺寸大于1
function getPos(m, x, y) {
  // console.log('getPos', x, y)
  for (let i = 0; i < 9; i++)
    for (let j = 0; j < 9; j++) {
      if (i * 9 + j < x * 9 + y) continue
      // console.log('getPos', i, j, m[i][j])
      if (m[i][j].size > 1)
        return [i, j]
    }
  // 没有的话表示搜索已经完成
  return [9, 9]
}

// 深度搜索
// 由m开始,x,y坐标进行搜索
// 每个坐标中是一个可能的数的集合
function dfs(m, x, y, deep = 0) {
  console.log('dfs', x, y, deep)
  // console.log('---dfs')
  // console.log(toStr(m))
  // console.log('---dfsn')
  if (x === 9 && y === 9) {
    // console.log('---end')
    // console.log(toStr(m))
    // console.log('---endn')
    // 搜索到最后一个格子时表示搜索结束了
    for (let i = 0; i < 9; i++)
      for (let j = 0; j < 9; j++) {
        if (m[i][j].size !== 1) return false
      }
    return m
  }
  let p = getPos(m, x, y)
  // console.log('getPos', p)
  let [i, j] = p
  let v = m[i][j]
  // 非跳过的数字,在集合中去除一个数字后,修改相关所有集合
  for (let delVal of v) {
    let mm = copy(m)
    let f = delSet(mm, i, j, delVal)
    console.log('delVal', i, j, delVal, 'n', toStr(mm))
    // 不能选择改数字
    if (!f) continue

    let [nx, ny] = getPos(mm, i, j)
    // console.log('nx,ny', nx, ny)
    let res = dfs(mm, nx, ny, deep + 1)
    if (res)
      return res
  }

  return false
}

function main() {
  let m = init(INIT_MAP)
  console.log(toStr(m))
  console.log('---nnn---')
  let res = dfs(m, 0, 0)
  if (res)
    console.log(toStr(res))

  // let res = dfs(m, 0, 0)
  // console.log(res)
  // console.log(init(m))
}

main()

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!