【问题标题】:Resolving a puzzle (optimal solution)解决难题(最佳解决方案)
【发布时间】:2023-12-30 11:02:01
【问题描述】:

我有一个 3x3 的数字拼图,如下所示:

3 | 5 | 2
7 | 8 | 9
1 | 6 | 4

作为解决方案:

1 | 2 | 3
4 | 5 | 6
7 | 8 | 9

规则是我只能移动附近的“碎片”,直到我得到解决方案。

我对此的看法是计算偏移量,然后将其运行到“花式”算法中以获得有效的解决方案。但是,我只能考虑使用蛮力并检查程序执行的步骤数量以找到最有效的步骤。

我的意思是偏移量:

(2,  0) | (0,  1) | (-1,  0)
(0,  1) | (0,  1) | ( 0,  1)
(0, -2) | (1, -1) | (-2, -1)

什么是笛卡尔平面中 x 和 y 的偏移量。我得到了以下计算偏移量的方法,但对“花式算法”没有任何想法。

https://ideone.com/0RP83x

有没有一种有效的方法可以在不使用暴力破解的情况下获得最少的移动?

【问题讨论】:

  • 棋子如何移动?你交换它们吗?
  • 糟糕。是的。但仅在 X 或 Y 轴上。没有对角线移动。
  • 我可能有个主意。我会做一些事情并发布一个解决方案,看看你是否喜欢它。
  • 您需要设计一个可以与 A* 或交互式深化 A* 结合的良好启发式算法。查看模式数据库、线性冲突和曼哈顿距离启发式算法。
  • 我的想法是使用数组上的地址交换将值移动到正确的位置并根据它们的新位置重新计算相对位置。这将使用单独的int[][] values = int[3][3]{}; 数组来跟踪实际的单元格值。我研究了一个小时,似乎它可能会奏效。其他有更多时间的人也许可以让它发挥作用。

标签: c++ algorithm a-star heuristics


【解决方案1】:

可以将网格视为一个节点(图的一部分)。

让我们来写网格

abc
def
ghi

作为单行abcdefghi

您从节点 352789164 开始,您希望到达节点 123456789

节点的邻居是您可以通过应用交换到达的所有节点。 例如123456789 用于邻居

[
  213456789, 132456789,
  123546789, 123465789,
  123456879, 123456798,
  423156789, 153426789,
  126453789, 123756489,
  123486759, 123459786
]

然后你可以申请 A*,提供:

  • d(nodeA, nodeB) = weight(nodeA, nodeB) = 1(所有交换费用为 1)
  • h(node) = 获得解决方案所需的交换次数最少。

要获得最小的h,请考虑计算错位的数字。

  • 如果您有偶数个错位的数字,则至少需要“一半”交换
  • 如果您有奇数个错位数字,则一半 + 1(例如,目标 123 的 312 需要 2 次交换)

下面的 js 示例,我从 wiki 复制粘贴的代码

function h (node) {
  const s = ''+node
  let misplaced = 0
  for(let i = 0; i < s.length; ++i) {
    if (parseInt(s[i]) != i+1) {
      misplaced++
    }
  }
  if (misplaced % 2 === 0) {
    return misplaced / 2
  }
  return Math.ceil(misplaced / 2)
}

function d (a, b) {
  return 1
}

const swaps = (_ => {
  const arr = [[1,2],[2,3],[4,5],[5,6],[7,8],[8,9],[1,4],[2,5],[3,6],[4,7],[5,8],[6,9]]
  function makePermFunc([a,b]) {
    a--
    b--
    return function (node) {
      const s = (''+node)
      const da = parseInt(s[a])
      const db = parseInt(s[b])
      const powa = 9 - a - 1
      const powb = 9 - b - 1
      node -= da * 10**powa
      node -= db * 10**powb
      node += da * 10**powb
      node += db * 10**powa
      return node
    }
  }
  const funcs = arr.map(makePermFunc)

  return node => funcs.map(f => f(node))
})();

//https://en.wikipedia.org/wiki/A*_search_algorithm
function reconstruct_path (cameFrom, current) {
  const total_path = [current]
  while(cameFrom.has(current)) {
    current = cameFrom.get(current)
    total_path.unshift(current)
  }
  return total_path
}


// A* finds a path from start to goal.
// h is the heuristic function. h(n) estimates the cost to reach goal from node n.
function A_Star(start, goal, h) {
  // The set of discovered nodes that may need to be (re-)expanded.
  // Initially, only the start node is known.
  const openSet = new Set([start])

  // For node n, cameFrom[n] is the node immediately preceding it on the cheapest path from start to n currently known.
  const cameFrom = new Map()

  // For node n, gScore[n] is the cost of the cheapest path from start to n currently known.
  const gScore = new Map()
  gScore.set(start, 0)

  // For node n, fScore[n] := gScore[n] + h(n).
  const fScore = new Map()
  fScore.set(start, h(start))

  while (openSet.size) {
    //current := the node in openSet having the lowest fScore[] value
    let current
    let bestScore = Number.MAX_SAFE_INTEGER
    for (let node of openSet) {
      const score = fScore.get(node)
      if (score < bestScore) {
        bestScore = score
        current = node
      }
    }
    
    if (current == goal) {
      return reconstruct_path(cameFrom, current)
    }
    openSet.delete(current)
    swaps(current).forEach(neighbor => {
      // d(current,neighbor) is the weight of the edge from current to neighbor
      // tentative_gScore is the distance from start to the neighbor through current
      const tentative_gScore = gScore.get(current) + d(current, neighbor)
      if (!gScore.has(neighbor) || tentative_gScore < gScore.get(neighbor)) {
        // This path to neighbor is better than any previous one. Record it!
        cameFrom.set(neighbor, current)
        gScore.set(neighbor, tentative_gScore)
        fScore.set(neighbor, gScore.get(neighbor) + h(neighbor))
        if (!openSet.has(neighbor)){
          openSet.add(neighbor)
        }
      }
    })
  }
  // Open set is empty but goal was never reached
  return false
}

console.log(A_Star(352789164, 123456789, h).map(x=>(''+x).split(/(...)/).filter(x=>x).join('\n')).join('\n----\n'))
console.log('a more basic one: ', A_Star(123654987, 123456789, h))

【讨论】: