关于在 15 方谜题中使用 A* 的问题
Posted
技术标签:
【中文标题】关于在 15 方谜题中使用 A* 的问题【英文标题】:questions regarding the use of A* with the 15-square puzzle 【发布时间】:2010-12-29 14:49:33 【问题描述】:我正在尝试为15-square puzzle 构建一个A* solver。
目标是重新排列图块,使它们出现在其自然位置。您一次只能滑动一个图块。拼图的每个可能状态都是搜索图中的一个节点。
对于 h(x) 函数,我在所有图块中使用图块与目标状态的错位的总和。在上图中,5 位于位置 0,0,它属于位置 1,0,因此它对 h(x) 函数的贡献为 1。下一个图块是 11,位于 0,1,属于 2,2,因此它对 h(x) 的贡献为 3。等等。 编辑:我现在明白这就是他们所说的“曼哈顿距离”或“taxicab distance”。
我一直在使用 g(x) 的步数。在我的实现中,对于状态图中的任何节点,g 只是前一个节点的 g 的 +1。
为了找到连续的节点,我只是检查我可以在拼图中移动“洞”的位置。显示的拼图状态(又名节点)有 3 个邻居:洞可以向北、向西或向东移动。
我的 A* 搜索有时会在 20 秒内收敛到一个解,有时会在 180 秒内收敛,有时根本不会收敛(等待 10 分钟或更长时间)。我认为 h 是合理的。我想知道我是否正确地模拟了 g 。换句话说,我的 A* 函数是否有可能通过不是最短路径的路径到达图中的节点?
也许我等的时间不够长?也许10分钟还不够长?
对于完全随机排列,(假设没有奇偶性问题),A* 解决方案将检查的平均排列数是多少?(请展示数学)
我将在我的代码中查找逻辑错误,但与此同时, 有小费吗?
(ps:它是用javascript完成的)。
另外,不,这不是 CompSci 作业。这只是个人探索的事情。我只是想学习 Javascript。
编辑:我发现运行时间高度依赖于启发式。我从某人提到的文章中看到了应用于启发式算法的 10 倍因子,这让我想知道 - 为什么是 10 倍?为什么是线性的?因为这是在 javascript 中完成的,所以我可以修改代码以使用当前正在考虑的节点动态更新 html 表。这让我可以看到算法的进展。使用常规的出租车距离启发式,我看着它未能收敛。
顶行有 5 个和 12 个,他们一直在附近闲逛。我会看到 1,2,3,4 爬到顶行,但随后他们会退出,其他数字会向上移动。我希望看到的是 1,2,3,4 爬到顶部,然后呆在那里。
我心想——这不是我个人解决这个问题的方法。手动执行此操作,我解决了第一行,然后是第 2 行,然后是第 3 和第 4 行。
所以我调整了 h(x) 函数,以增加较高行和“左侧”列的权重。结果是 A* 收敛得更快。它现在在 3 分钟内运行,而不是“无限期”运行。通过我所说的“窥视”,我可以看到较小的数字爬到较高的行并留在那里。这不仅看起来是正确的,而且运行得更快。
我正在尝试多种变体。很明显,A* 运行时对启发式非常敏感。目前我发现的最好的启发式方法使用dislocation * ((4-i) + (4-j))
的总和,其中 i 和 j 是行和列,错位是出租车距离。
我得到的结果中有一个有趣的部分:使用特定的启发式方法,我很快就找到了一条路径,但它显然不是最短路径。我认为这是因为我正在加权启发式。在一种情况下,我在 10 秒内获得了 178 步的路径。我自己的手动工作在 87 步中产生了一个解决方案。 (远远超过 10 秒)。有必要进行更多调查。
所以结果是我看到它收敛得更快,而且路径肯定不是最短的。我必须更多地考虑这一点。
代码:
var stop = false;
function Astar(start, goal, callback)
// start and goal are nodes in the graph, represented by
// an array of 16 ints. The goal is: [1,2,3,...14,15,0]
// Zero represents the hole.
// callback is a method to call when finished. This runs a long time,
// therefore we need to use setTimeout() to break it up, to avoid
// the browser warning like "Stop running this script?"
// g is the actual distance traveled from initial node to current node.
// h is the heuristic estimate of distance from current to goal.
stop = false;
start.g = start.dontgo = 0;
// calcHeuristic inserts an .h member into the array
calcHeuristicDistance(start);
// start the stack with one element
var closed = []; // set of nodes already evaluated.
var open = [ start ]; // set of nodes to evaluate (start with initial node)
var iteration = function()
if (open.length==0)
// no more nodes. Fail.
callback(null);
return;
var current = open.shift(); // get highest priority node
// update the browser with a table representation of the
// node being evaluated
$("#solution").html(stateToString(current));
// check solution returns true if current == goal
if (checkSolution(current,goal))
// reconstructPath just records the position of the hole
// through each node
var path= reconstructPath(start,current);
callback(path);
return;
closed.push(current);
// get the set of neighbors. This is 3 or fewer nodes.
// (nextStates is optimized to NOT turn directly back on itself)
var neighbors = nextStates(current, goal);
for (var i=0; i<neighbors.length; i++)
var n = neighbors[i];
// skip this one if we've already visited it
if (closed.containsNode(n)) continue;
// .g, .h, and .previous get assigned implicitly when
// calculating neighbors. n.g is nothing more than
// current.g+1 ;
// add to the open list
if (!open.containsNode(n))
// slot into the list, in priority order (minimum f first)
open.priorityPush(n);
n.previous = current;
if (stop)
callback(null);
return;
setTimeout(iteration, 1);
;
// kick off the first iteration
iteration();
return null;
【问题讨论】:
使用瓷砖与其目标位置的距离之和作为启发式。它更有效。 (参见人工智能,现代方法第 2 版第 107 页:启发式准确性对性能的影响) 这就是我正在做的。我写道:对于 h(x) 函数,我在所有图块中使用图块与目标状态的错位的总和。 当你说“更有效”时,什么你的意思是?与什么相比? 【参考方案1】:是的,我听说这个问题就是这样解决的。 g(x) 是已经发生的瓦片滑动的数量,h(x) 是所有瓦片与其所需方块的总距离。在今天之前,除了这种方法(Manhattan heuristic)之外,我没有看到任何使用过的东西,但我发现了这种所谓的diagonal shortcut——你可能想看看。
【讨论】:
是的,这是常见的方法,但我从未见过它被称为“曼哈顿启发式”。我一直看到它被称为“出租车指标”(在花哨的数学术语中,L_1
norm)。
在花哨的 AI 圈子里,是“曼哈顿距离”。对于像这样的问题,事情确实只是正交移动,它非常适合。
我正在使用曼哈顿。关于您用对角线快捷方式引用的文章的问题 - 为什么 H 被 10 倍超重?为什么不只是 abs(currentX-targetX) - abs(currentY-targetY) ?【参考方案2】:
您使用什么测试数据?如果它是随机的,您将无法解决大约一半的难题。在保持其余部分保持相同位置的同时切换两个图块是不可能的,因此,如果您几乎到达终点位置但交换了两个图块,则不可能将其移至所需位置,并且没有搜索算法可能会成功终止。
在 19 世纪,美国拼图大师山姆·洛伊德 (Sam Loyd) 出售了将 15 号和 14 号颠倒的这些玩具,并为任何能够展示转换瓷砖的解决方案的人提供大奖(大概除了我的那个,一个小螺丝刀)。在当今的法律环境下,我不知道他是否敢。
一种可能性是尝试使其进入正确的配置或 15-14 配置。
【讨论】:
起始排列不是随机的。我从“已解决”的棋盘开始——然后随机移动 200 次,并以此为起点。 好主意。不幸的是,我没有更多的机会检查代码和运行。【参考方案3】:如果你先瞄准中间目标,也许它会更快地收敛。例如,只对顶部和右侧的行进行评分。将这些行放置到位应该不会花费很长时间,然后您可以解决剩余的 3x3。
【讨论】:
【参考方案4】:A-star 搜索将通过证明所有尚未解决的路径都无法通过比当前解决方案更少的移动来解决来找到最佳解决方案。您不是在寻找最佳解决方案,而是最快解决方案。因此,您可以通过返回第一个解决方案,通过加权低于启发式函数的移动次数来优化您的算法,并且启发式可能会返回高估。
启发式函数本身通常最好由Manhattan distance 和线性冲突建模。曼哈顿距离在其他答案和***文章中得到了很好的解释,您似乎已经掌握了它。线性冲突为每对必须交换以达到解决方案的块的曼哈顿距离增加了两个。例如,如果一行包含“3 2 1 4”,则必须交换一个和三个,并且必须将一个移动到另一行。
使用模式数据库是一种选择,它可以帮助您的搜索避免某些死胡同,并且对于 15 道谜题,这样做的内存使用量应该是可控的。
【讨论】:
【参考方案5】:我学到了什么
显然这是well-known,但对我来说不是:A* 收敛对启发式函数非常敏感。 如果我编写的启发式算法对前 2 行的权重比其他行的权重更大,它会更快地收敛,但路径通常更长。 我发现here 所示的对角线 H(x) 函数比曼哈顿距离收敛得更快,对于 15 方拼图。 即使使用鼓励更快收敛的启发式函数,运行时间也存在很大差异。有时它会在 10 秒内找到路径。有时10分钟。有时更长。 使用对角启发法,在找到的路径中所需的移动次数范围从 30-ish 到 110。【讨论】:
【参考方案6】:我曾经编写过这样的算法(windowsApp),我有以下经验
1) 如果机器人使用(接近)最优解决方案,那么在行动中看到机器人是最有趣的。 (对于人类观察者来说,无法理解机器人是如何“思考”的,从混乱到有序的交易是突然的)
2) 如果您想找到最佳解决方案,您的 h() 函数必须低估真实距离。如果您高估它,您将找不到最佳值。
3) 潜在的状态空间很大,15!/2 (10^12)。如果您使用错误的启发式函数,您的数据集将远远超出主内存的大小,并且每次数据访问都需要多次磁盘访问。如果发生这种情况,执行时间将是“无限的”。
【讨论】:
【参考方案7】:check this
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.lang.Object;
class Puzzle extends JPanel implements ActionListener
JButton[] b = new JButton[16];
Puzzle()
b[0] = new JButton("4");
b[1] = new JButton("11");
b[2] = new JButton("5");
b[3] = new JButton("9");
b[4] = new JButton("1");
b[5] = new JButton("10");
b[6] = new JButton("12");
b[7] = new JButton("13");
b[8] = new JButton("15");
b[9] = new JButton("14");
b[10] = new JButton("3");
b[11] = new JButton("2");
b[12] = new JButton("7");
b[13] = new JButton("8");
b[14] = new JButton("6");
b[15] = new JButton("");
GridLayout grid = new GridLayout(4,4);
setLayout(grid);
for(int i=0;i<16;i++)
add(b[i]);
for(int i=0;i<16;i++)
b[i].addActionListener(this);
public void actionPerformed(ActionEvent e)
/*if(e.getSource()==b[11])
if(b[15].getText()=="")
b[15].setText("");
else if(e.getSource()==b[3])
if(b[2].getText()=="")
b[2].setText("");
*/
for(int i=0;i<16;i++)
System.out.println(e.getSource());
if(e.getSource()==b[i])
if(i==5 || i==6 || i==9 || i==10)
if(b[i-1].getText()=="")
b[i-1].setText(b[i].getText());
b[i].setText("");
else if(b[i+1].getText()=="")
b[i+1].setText(b[i].getText());
b[i].setText("");
else if(b[i-4].getText()=="")
b[i-4].setText(b[i].getText());
b[i].setText("");
else if(b[i+4].getText()=="")
b[i+4].setText(b[i].getText());
b[i].setText("");
else if(i==4 || i==8)
if(b[i+1].getText()=="")
b[i+1].setText(b[i].getText());
b[i].setText("");
else if(b[i-4].getText()=="")
b[i-4].setText(b[i].getText());
b[i].setText("");
else if(b[i+4].getText()=="")
b[i+4].setText(b[i].getText());
b[i].setText("");
else if(i==7 || i==11)
if(b[i-1].getText()=="")
b[i-1].setText(b[i].getText());
b[i].setText("");
else if(b[i-4].getText()=="")
b[i-4].setText(b[i].getText());
b[i].setText("");
else if(b[i+4].getText()=="")
b[i+4].setText(b[i].getText());
b[i].setText("");
if(i==0)
if(b[i+1].getText()=="")
b[i+1].setText(b[i].getText());
b[i].setText("");
else if(b[i+4].getText()=="")
b[i+4].setText(b[i].getText());
b[i].setText("");
if(i==3)
if(b[i-1].getText()=="")
b[i-1].setText(b[i].getText());
b[i].setText("");
else if(b[i+4].getText()=="")
b[i+4].setText(b[i].getText());
b[i].setText("");
if(i==15)
if(b[i-1].getText()=="")
b[i-1].setText(b[i].getText());
b[i].setText("");
else if(b[i-4].getText()=="")
b[i-4].setText(b[i].getText());
b[i].setText("");
else if(b[i+4].getText()=="")
b[i+4].setText(b[i].getText());
b[i].setText("");
if(i==12)
if(b[i+1].getText()=="")
b[i+1].setText(b[i].getText());
b[i].setText("");
else if(b[i-4].getText()=="")
b[i-4].setText(b[i].getText());
b[i].setText("");
if(i==1 || i==2)
if(b[i+1].getText()=="")
b[i+1].setText(b[i].getText());
b[i].setText("");
else if(b[i-1].getText()=="")
b[i-1].setText(b[i].getText());
b[i].setText("");
else if(b[i+4].getText()=="")
b[i+4].setText(b[i].getText());
b[i].setText("");
if(i==13 || i==14)
if(b[i+1].getText()=="")
b[i+1].setText(b[i].getText());
b[i].setText("");
else if(b[i-1].getText()=="")
b[i-1].setText(b[i].getText());
b[i].setText("");
else if(b[i-4].getText()=="")
b[i-4].setText(b[i].getText());
b[i].setText("");
//System.out.println(e.getActionCommand());
public static void main(String[] args)
JFrame frame = new JFrame("15-Puzzle");
//frame.setContentPane(panel);
JComponent newContentPane = new Puzzle();
//newContentPane.setOpaque(true); //content panes must be opaque
frame.setContentPane(newContentPane);
//panel.add(button);
frame.setSize(400,400);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
【讨论】:
【参考方案8】:使用 IDA* 代替 A*。你需要更少的内存。作为启发式方法,"Walking distance" developed by Ken'ichiro Takahashi 更有效,尽管只使用 25 kB 内存。Here 和 here 是英文翻译。
【讨论】:
以上是关于关于在 15 方谜题中使用 A* 的问题的主要内容,如果未能解决你的问题,请参考以下文章