在广度优先搜索中跟踪访问状态


10

因此,我试图在“ 滑块”难题(数字类型)上实现BFS 。现在,我主要注意到的是,如果您有一块4*4木板,那么状态数可能会很多,16!因此我无法事先枚举所有状态。

所以我的问题是如何跟踪已经访问过的州?(我正在使用一个类板,每个类实例包含一个唯一的板模式,并通过枚举当前步骤中的所有可能步骤来创建)。

我在网上搜索,很显然它们并没有返回到刚刚完成的上一步,但是我们也可以通过另一条路线返回到上一步,然后再次枚举之前已访问过的所有步骤。那么,当尚未枚举所有状态时,如何跟踪访问的状态呢?(将已经存在的状态与当前步骤进行比较将很昂贵)。


1
旁注:我想不出一个更合适的Stack来发布这个问题。我知道在此Stack中通常不欢迎实现细节。
DuttaA

2
imo对于SE:AI来说,这是一个很大的问题,因为它不仅与实现有关,而且与概念本身有关。更不用说,这个问题在几个小时内吸引了四个合法答案。 (自由编辑标题进行搜索,并创建一个BFS标签)
周四(DukeZhou)

Answers:


8

您可以使用set(从单词的数学意义上讲,即不能包含重复项的集合)存储您已经看到的状态。您需要执行的操作是:

  • 插入元素
  • 测试元素是否已经存在

几乎每种编程语言都应该已经支持可以在恒定()时间内执行这两个操作的数据结构。例如:O(1)

  • set 在Python中
  • HashSet 在Java中

乍一看,将您曾经看到的所有状态添加到这样的集合中似乎在内存方面是昂贵的,但是与边界所需的内存相比,这还不错。如果你的分支因子为,你前沿将增长b - 1名,每个节点所访问的元素(除去1个从前沿节点,以“游”,添加b新的继任者/子女),而您的集将仅增长1个额外每个被访问节点的节点数。bb11b1

在伪代码中,可以在“广度优先搜索”中使用这样的集合(将其命名为closed_set,与Wikipedia的伪代码保持一致):

frontier = First-In-First-Out Queue
frontier.add(initial_state)

closed_set = set()

while frontier not empty:
    current = frontier.remove_next()

    if current == goal_state:
        return something

    for each child in current.generate_children()
        if child not in closed_set:    // This operation should be supported in O(1) time regardless of closed_set's current size
            frontier.add(child)

    closed_set.add(current)    // this should also run in O(1) time

(此伪代码的某些变体也可能起作用,并且视情况而定,效率或多或少会有所提高;例如,您还可以使用closed_set来包含已经向其边界添加了子代的所有节点,然后完全避免generate_children()调用如果current已经在closed_set。)


我上面描述的将是处理此问题的标准方法。凭直觉,我怀疑一个不同的“解决方案”可能是在将新的继承国列表添加到边境之前总是对其顺序进行随机化处理。这样,您就不会避免偶尔添加先前已经扩展到边界的状态的问题,但是我确实认为这应该大大降低陷入无限循环的风险。

请注意:我不知道对此解决方案有任何形式上的分析,但可以证明它始终避免了无限循环。如果我试图直觉地“运行”此程序,我怀疑它应该可以工作,并且不需要任何额外的内存。可能有些情况我暂时不会考虑,因此也可能根本不起作用,上述标准解决方案将是一个更安全的选择(以增加内存为代价)。


1
我将能够做到,但是比较时间将成倍增加
DuttaA

3
SO(1)S

1
@DuttaA我添加了一些伪代码来确切描述如何使用该集合,希望可以澄清一些事情。注意,我们永远不会遍历整个closed_set,它的大小永远不会影响我们的(渐近)计算时间。
丹尼斯·苏美斯

1
其实我是用c ++做到的,我对哈希没有任何想法...猜猜我现在将使用python ...谢谢您的回答
DuttaA

3
@DuttaA在C ++中,您可能想使用std :: unordered_set
Dennis Soemers

16

Dennis Soemers的答案是正确的:您应该使用HashSet或类似的结构来跟踪BFS Graph Search中的访问状态。

但是,它并不能完全回答您的问题。没错,在最坏的情况下,BFS将要求您存储16!节点。即使集合中的插入和检查时间为O(1),您仍然需要大量的内存。

要解决此问题,请不要使用BFS。除了最简单的问题以外,它都是棘手的问题,因为它需要的时间和记忆力与到最近的目标状态的距离成指数关系。

一种内存效率更高的算法是迭代加深。它具有BFS的所有理想属性,但仅使用O(n)内存,其中n是达到最近解的步数。可能还需要一段时间,但是您将在与CPU相关的限制之前达到内存限制。

更好的是,开发特定于域的启发式方法,并使用A * search。这应该只需要检查极少数的节点,并允许搜索在更接近线性时间的时间内完成。


2
16!

@DennisSoemers是正确的..您也是正确的..我只是在尝试磨练自己的技能...稍后,我将使用更高级的搜索方法
DuttaA

BFS是否可以返回可接受的本地解决方案?(我正在处理的值更像是81!在不可预测的游戏键盘的拓扑结构的阵列弱”一般性能)。
DukeZhou

2
@DukeZhou BFS通常仅在寻求完整解决方案时使用。为了尽早停止它,您需要一个函数来估计不同部分解决方案的相对质量,但是如果您有这样的函数,则可以只使用A *即可!
John Doucette

我建议不要说“达到目标的最小移动数”,而不是说“游戏中的移动数”。我认为游戏中的移动数量就是使您从16个中的一个移动的每一步!声明其他任何状态,这比迭代加深使用的内存要多得多。
NotThatGuy

7

虽然给出的答案通常是正确的,但是15拼图中的BFS不仅非常可行,而且是在2005年完成的!描述此方法的论文可以在这里找到:

http://www.aaai.org/Papers/AAAI/2005/AAAI05-219.pdf

一些要点:

  • 为此,需要外部存储器-即BFS使用硬盘驱动器而不是RAM进行存储。
  • 实际上,只有15!/ 2个状态,因为状态空间有两个互不可达的分量。
  • 这在滑动拼图中很有效,因为状态空间在各个级别之间的增长非常缓慢。这意味着任何级别所需的总内存远远小于状态空间的全部大小。(这与诸如Rubik's Cube之类的状态空间形成鲜明对比,在该状态空间中,状态空间的增长要快得多。)
  • 由于滑动块拼图是无向的,因此您只需要担心当前层或上一层中的重复项。在定向空间中,您可能会在搜索的任何先前层中生成重复项,这会使事情变得更加复杂。
  • 在科尔夫(上面链接)的原始作品中,他们实际上并没有存储搜索结果-搜索只是计算出每个级别有多少个状态。如果要存储最初的结果,则需要WMBFS之类的东西(http://www.cs.du.edu/~sturtevant/papers/bfs_min_write.pdf
  • 当状态存储在磁盘上时,可以使用三种主要方法来比较前几层的状态。
    • 首先是基于排序。如果对两个后继文件进行排序,则可以按线性顺序扫描它们以查找重复项。
    • 第二个是基于哈希的。如果使用哈希函数将后继文件分组到文件中,则可以加载小于完整状态空间的文件以检查重复项。(请注意,这里有两个哈希函数-一个用于将状态发送到文件,另一个用于区分文件中的状态。)
    • 第三是结构化重复检测。这是基于哈希的检测的一种形式,但是这样做的方式是,可以在生成重复项时立即对其进行检查,而不必在所有重复项产生之后对其进行检查。

这里还有很多要说的,但是上面的论文提供了更多的细节。


这是一个很好的答案..但对于像我这样的菜鸟不是:)...我不是程序员的专家
。– DuttaA

无向将如何帮助您避免其他层的重复?当然,您可以通过在一个圆圈中移动3个图块来返回另一层中的节点。如果有的话,有针对性的做法会帮助您避免重复,因为它的限制更为严格。链接的文章讨论了重复检测,但根本没有提到无向或有向的检测,也似乎没有提到避免在不同级别进行重复检测(但是在我的简短扫描中我可能会错过它)。
NotThatGuy

@NotThatGuy在无向图中,父母和孩子在BFS中发现的深度最多相距1。这是因为一旦找到了一个,无向边保证了之后将立即找到另一个。但是,在有向图中,深度为10的状态可以生成深度为2的子级,因为深度2的子级不必具有返回其他状态的边(这将使其变为深度3而不是深度10)。 。
Nathan S.

@NotThatGuy如果您在一个圆圈中移动3个图块,则会创建一个循环,但是BFS会同时在两个方向上进行探索,因此实际上并不会带您回到更浅的深度。此演示中显示了完整的3x2滑动块,您可以跟踪周期以查看周期如何发生:movingai.com/SAS/IDA
Nathan

1
太棒了 欢迎来到SE:AI!
周公克

3

具有讽刺意味的是,答案是“使用所需的任何系统”。hashSet是一个好主意。但是,事实证明,您对内存使用情况的担心是没有根据的。BFS在这类问题上非常糟糕,以至于您可以解决这个问题。

考虑到您的BFS要求您保留一堆未处理的状态。当您进入难题时,您要处理的状态变得越来越不同,因此您很可能会看到,BFS的每一层都将要查看的状态数乘以3。

这意味着,当您处理BFS的最后一层时,必须在内存中至少包含16!/ 3个状态。无论您采用哪种方法来确保适合内存,都足以确保以前访问的列表也适合于内存。

正如其他人指出的那样,这不是最佳的算法。使用更适合该问题的算法。


2

15个难题的问题在4x4板上播放。在源代码中实现此步骤是逐步完成的。首先,必须对游戏引擎本身进行编程。这允许操作员玩游戏。15个益智游戏只有一个免费元素,并且在该元素上执行动作。游戏引擎接受四个可能的命令:左,右,上和下。不允许执行其他操作,并且只能使用这些说明来控制游戏。

玩游戏的下一层是GUI。这非常重要,因为它可以测试游戏引擎并尝试手动解决游戏。同样,GUI也很重要,因为我们需要找出潜在的启发式方法。现在我们可以谈谈AI本身。AI必须将命令发送到游戏引擎(左,右,上和下)。一种简单的求解器方法是蛮力搜索算法,这意味着AI正在发送随机命令,直到达到目标状态为止。一个更高级的想法是实现某种模式数据库,以减少状态空间。广度优先搜索不是直接进行启发式搜索,而是一个开始。等同于创建一个图表以按时间顺序测试可能的运动。

可以使用图形来跟踪现有状态。每个状态都是一个节点,具有一个ID和一个父ID。AI可以在图中添加和删除节点,并且计划者可以对图进行求解以找到到达目标的路径。从编程的角度来看,这个难题的游戏引擎是15个对象,许多对象的列表是一个arraylist。它们存储在图类中。在源代码中实现这一点有些棘手,通常第一次试用会失败,并且该项目会产生很多错误。为了管理复杂性,这样的项目通常是在学术项目中完成的,也就是说,这是撰写有关此论文的主题,该论文可以包含100页以上。


1

游戏方法

16!

但是,如果目标是在最少的计算周期内完成难题,那么这些琐碎的事实就无关紧要。广度优先搜索不是完成正交移动难题的实用方法。仅当由于某些原因移动次数极为重要时,才需要广度优先搜索的非常高的成本。

子序列下降

代表状态的大多数顶点将永远不会被访问,并且被访问的每个状态可以具有两个到四个输出边缘。每个块都有一个初始位置和一个最终位置,并且板是对称的。当开放空间是四个中间位置之一时,存在最大的选择自由。最少的是当开放空间是四个角位置之一时。

合理的视差(误差)函数只是所有x视差之和加上所有y视差的总和,该数字启发式地表示由于自由空间(中间,边缘,角落)。

尽管区块可能会暂时离开目的地以支持需要一系列动作才能完成的策略,但这种策略很少超过八步,平均产生5184个排列,可以比较最终状态使用上面的视差函数。

如果将块1到15的空白和位置编码为半字节数组,则仅需要加,减和按位运算,从而使算法更快。可以重复八种暴力手段,直到差异降至零为止。

摘要

该算法无法循环,因为除了初始状态已经完成以外,总是有八个移动的置换中的至少一个减小视差,而不论初始状态如何。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.