精华内容
下载资源
问答
  • 本源码是针对八数码问题的C语言实现方法,有较详细的注释。着重于广度搜索条件。大概就是这样吧。。。为啥这资源描述要这么多字。。。。
  • 广度优先遍历(BFS),又叫宽度优先搜索或横向优先搜索,是从根结点开始沿着树的宽度搜索遍历,将离根节点最近的节点先遍历出来,在继续深挖下去。基本思想是:1、从图中某个顶点V0出发,并访问此顶点;2、从V0出发...

    何为广度优先遍历呢?

    广度优先遍历(BFS),又叫宽度优先搜索或横向优先搜索,是从根结点开始沿着树的宽度搜索遍历,将离根节点最近的节点先遍历出来,在继续深挖下去。

    基本思想是:

    1、从图中某个顶点V0出发,并访问此顶点;

    2、从V0出发,访问V0的各个未曾访问的邻接点W1,W2,…,Wk;然后,依次从W1,W2,…,Wk出发访问各自未被访问的邻接点;

    3、重复步骤2,直到全部顶点都被访问为止。

    下面给出广度优先遍历的例子:(广度优先遍历不是唯一的哦,只要满足“广度”的含义即可)

    访问顺序为:0,2,1,5,3,4(看图很快就可得知)(将离根节点最近的节点先遍历出来,再继续深挖下去。)

    5bb75d9fafa17214d586ba6f12402664.png

    a72096e48dd03a4128733c2940e9f000.png

    知道了BFS之后,我们又要怎么通过编程来实现BFS呢?

    下面给上详细的分析:

    首先,先准备一个队列(利用队列的结构)和一个Set(Set用来作为类似于注册的作用,防止节点重复进入队列)

    step1.先把根节点1放入队列中,同时将1注册到set中去,证实它进过队列(上面两步是同步的)

    step2.把1poll出来,同时打印出来(System.out...)(前面两步也是同步的),同时将1的所有next节点都放到队列中去(放到队列中去时要提前判断这些元素之前是否有注册过,即有没有在set中)

    step3.如果一个节点已经没有next节点的时候,那就直接将该节点弹出去就行,不用注册也不用添加到队列中。

    下面流程图可以用来参考,下面也有源代码,源代码有注释,很好理解,看不清楚的欢迎留言~

    f575a66ca5314b8ab7b77b6242224124.png

    a72096e48dd03a4128733c2940e9f000.png

    8b8366f8e4c3711a196eedb1a760ee7f.png

    a72096e48dd03a4128733c2940e9f000.png

    下面附上源代码:

    6fd6d9713f1c6e09b950b083d6beae37.png

    a72096e48dd03a4128733c2940e9f000.png

    展开全文
  • 在今天的文章当中除了关于题目的分析和解答之外,我们还会详细解读深度优先搜索和回溯算法,感兴趣的同学不容错过。链接Next Permutation难度Medium描述实现C++当中经典的库函数next permutation,即下一个排列。...

    e99176fa90d1f420b467dcf729061d70.png

    本文始发于个人公众号:TechFlow,原创不易,求个关注

    今天我们讲的是LeetCode的31题,这是一道非常经典的问题,经常会在面试当中遇到。在今天的文章当中除了关于题目的分析和解答之外,我们还会详细解读深度优先搜索和回溯算法,感兴趣的同学不容错过。

    链接

    Next Permutation

    难度

    Medium

    描述

    实现C++当中经典的库函数next permutation,即下一个排列。如果把数组当中的元素看成字典序的话,那下一个排列即是字典序比当前增加1的排列。如果已经是字典序最大的情况,则返回字典序最小的情况,即从头开始。

    Implement next permutation , which rearranges numbers into the lexicographically next greater permutation of numbers.

    If such arrangement is not possible, it must rearrange it as the lowest possible order (ie, sorted in ascending order).

    The replacement must be [in-place](http://en.wikipedia.org/wiki/In- place_algorithm) and use only constant extra memory.

    Here are some examples. Inputs are in the left-hand column and its corresponding outputs are in the right-hand column.

    样例

    1,2,3 -> 1,3,23,2,1 -> 1,2,31,1,5 -> 1,5,1

    介绍和分析

    如果对C++不够熟悉的同学可能不知道next permutation这个库函数,这个库函数可以生成下一个字典序的排列。

    举个例子,比如样例当中1 2 3这三个元素,我们把它所有的排列列举出来,一共是6种:

    1 2 3
    1 3 2
    2 1 3
    2 3 1
    3 1 2
    3 2 1

    这6种排列的字典序依次增加,而next permutation呢则是根据当前的排列返回下一个排列。比如我们输入1 2 3,返回1 3 2,我们输入3 1 2,返回3 2 1。而如果我们输入字典序最大的,也就是3 2 1时,则从头开始,返回字典序最小的1 2 3.

    暴力

    老规矩,我们第一优先级思考最简单的暴力解法。

    最简单粗暴的方法是什么,当然是将所有的排列全部列举出来,然后根据字典顺序排序,最后返回答案。

    说起来很简单,但是实现的话一点都不容易。首先,我们怎么获取所有的排列呢?这个问题最简单的方法是通过递归实现深度优先搜索。我单单这么说,对搜索算法不够熟悉的同学可能理解不了。没关系,我们一步一步来推导。我们先把原问题放一放,来看一道经典的搜索问题:八皇后问题。

    八皇后问题

    八皇后问题是假设我们有一张8*8的国际象棋的棋盘,我们要在棋盘上放置八个皇后,使得这些皇后之间彼此相安无事。

    a1f67ff927b02ce95cf4f26a54ad934a.png

    众所周知,在国际象棋当中皇后比较牛X,可以横竖以及对角线斜着走。也就是说如果将两个皇后放在同行或者同列以及同对角线,那么就会出现皇后之间互相攻击的情况,也就不满足题目要求了。

    这个问题我们显然不考虑无脑的暴力枚举,即使我们知道是8个皇后,如果我们真的用8重循环去查找合法位置的话,那么一定会复杂度爆炸计算到天荒地老的。原因也很简单,我们稍微分析一下复杂度就能知道。对于每重循环,我们都需要遍历64个位置,那么一共的复杂度就是

    ,这是一个非常庞大的数字,几乎是算不完的。而且如果我们变换一下题目,假设我们一开始不知道皇后的数量,变成n皇后问题,那么我们连写几重循环都不知道。

    解决这个问题的方法需要我们人为地将问题的规模缩小,其实我们没有必要去遍历所有的位置。因为根据题目条件,皇后不能同行,所以我们每行只能放置一个皇后。而刚好我们是8*8的棋盘,所以我们刚好是每行和每列都只能放置一个皇后。也就是说,我们一行一行地放置皇后。

    所以解法就变成了,对于第1行而言,我们有8个位置可以放置皇后。我们随意地选择了一个位置之后,只剩下了7个位置给第二行,然后再放一个,再剩下6个位置给第三行……以此类推,当这样所有皇后放置完了,我们检查完合法性之后,我们接下来需要回到之前的行,选择其他的位置。

    举个简单的例子,假设我们一开始的放置方案是[1, 2, 3, 4, 5, 6, 7, 8]。

    我们是放置完第8行的棋子之后进入的结束状态,由于第八行只剩下了一个位置,所以没有其他可以选择的情况。在这种情况下,我们应该往上回顾,回到第7行的时候,对于第7行来说可以选择的位置有[7, 8],第一次我们选择了7,那么我们可以试试选择8,这样就可以得到新的答案[1, 2, 3, 4, 5, 6, 8, 7]。

    这样又结束了之后,我们再回顾的时候发现第7行的情况也都选择完了,那么要继续往上,回顾第6行的情况。如果我们把搜索树画一部分出来的话是这样:

                                          root
                                           /
                                          1
                                         /
                                        2
                                       /
                                      3
                                     /
                                    4
                                   /
                                  5
                                 / 
                                6   7
                               / 
                              7   8
                             /     
                            8       7

    你会发现我们一行一行放置的过程在树上对应的是一层一层往下的过程,而我们枚举完了之后回顾之前的行的时候,体现在搜索树上是反向的逆流而上的过程。我们把枚举完了后续回到之前的决策的算法称为“回溯法”,想象一下一棵从源头展开的搜索树,我们顺着它一层一层往下遍历,遍历结束完了之后又逆流而上去修改之前的决策,再拓展新的搜索空间。这也有点像是平行时空,根据平行时空理论,世界上的每一个随机决定都会生成一个平行宇宙,如果你有穿梭时间的能力,那么你就可以回到之前产生平行时空的节点,去遍历其他的平行时空。如果你看过经典的悬疑电影蝴蝶效应的话,那么你一定知道我在说什么。

    关于回溯这个概念,我也在之前讲解栈和模拟递归的文章当中提到过,感兴趣的同学可以点击下方的链接查看。

    数据结构——30行代码实现栈和模拟递归mp.weixin.qq.com
    ff8634e8d2b6f3dccfdec64607fc2e39.png

    模板代码

    如果是初学者,即使是理解了递归的概念想要自己写出代码来也是一件不容易的事情。因为在我们看来记录所有节点的状态,遍历,再回到之前的状态,再继续搜索,这是一个非常复杂的过程。

    好在我们并不需要自己记录这个过程,因为计算机在执行程序的时候会有一个称为方法栈的东西,记录我们执行的所有函数和方法的状态。比如我们在A程序中执行了B程序的代码,like this:

    def A():
      do_something()
      B()
      do_something()

    我们在A当中的第二行执行了B这个函数,那么系统会为我们记录下我们在执行B的时候的位置是第二行,当B执行结束,还会回到调用的位置,继续往下执行。而递归呢,则是利用这一特性,让A来执行自己:

    def A():
      do_something()
      A()
      do_something()

    和刚才一样,系统同样会为我们记录执行A的位置。A执行了A,在我们看来是一样的,但是系统会储存两份方法状态,我们可以简单认为后面执行的A是A1,那么当A1执行结束之后,又会回到A执行A1的位置。而用递归来实现搜索呢,只是在此基础上加上了一点循环而已。

    def dfs():
        for choice in all_choices():
            record(choice)
            dfs()
            rollback(choice)

    这就是一个简单的搜索的代码框架了,我们遍历每个节点的所有决策,然后记录下这个选择,进行往下递归。当我们执行完再次回到这个位置这一行代码的时候,我们已经遍历完了这个选择所有的情况,所以我们要撤销这个选择带来的一些影响,好方便枚举下一个选择。

    但是这样还没结束,还有一点小问题,就是我们只需要放置8个皇后,我们需要针对这点做限制,不然就会无穷无尽递归下去了。做限制的方法也很简单,本质上来说我们是要限制递归的深度,所以我们可以用一个变量来记录递归的深度,每次往下递归的时候就加上1,如果递归深度达到了我们的要求,就不再往下递归了。

    加上深度判断之后,我们就得到了经典的回溯的代码框架。几乎可以说无论什么样的回溯搜索问题,都脱离不了这个代码框架,只是具体执行和撤销的逻辑有所不同而已:

    def dfs(depth):
        if depth >= 8:
            return
        for choice in all_choices():
            record(choice)
            dfs(depth+1)
            rollback(choice)

    我们利用这个代码框架来实现一下八皇后问题,代码很短,也很好理解

    def dfs(n, queens, record):
        # 记录答案与控制递归深度
        if n >= 8:
            # 由于Python当中传递的是引用,所以这里需要copy
            # 否则当后面queens pop的时候,会影响record当中记录的值
            record.append(queens.copy())
            return
    
        for i in range(8):
            # 判断是否同列
            if i in queens:
                continue
            # 判断是否同对角线
            flag = False
            for j in range(len(queens)):
                if abs(n - j) == abs(i - queens[j]):
                    flag = True
                    break
            if flag:
                continue
            # 递归与回溯
            queens.append(i)
            dfs(n+1, queens, record)
            queens.pop()

    如果我们把八皇后问题当中的不能同列同对角线的判断去掉,也就是说皇后不能同行,一行只能放一个皇后,我们把数组当中的元素看成是皇后,那么去掉限制之后的八皇后问题就变成了全排列问题。而这个问题是找出所有的排列当中大于当前这个排列的最小的排列,也就是说我们不需要记录所有的排列结果,只需要记录满足条件的那一个。

    那么套用上面的代码框架,这个问题立刻迎刃而解。

    def dfs(permutation, array, origin):
        """
        origin记录原排列
        mini  记录答案,即大于原排列的最小排列
        """
        # 由于Python引用机制,在函数内部修改,外部不会生效
        # 所以需要用全局变量来记录答案
        global mini
        # 当所有元素都已经放入permutation
        if len(array) == 0:
            # 判断是否合法,并且是最小的
            if permutation > origin and permutation < mini:
                # 因为后面permutation会执行pop
                # 所以这里需要copy
                mini = permutation.copy()
        # 如果还有元素没有放入
        for i in array.copy():
            array.remove(i)
            permutation.append(i)
            dfs(permutation, array, origin)
            array.append(i)
            permutation.pop()

    我们之所以费这么大劲绕过去讲解八皇后问题,就是为了让大家理解全排列和回溯算法之间的关系。但是如果你真的这么去解题,你会发现是无法通过的,原因也很简单,因为全排列是一个n!复杂度的算法,它的复杂度非常大。对于一个稍微大一点的n,我们就得不到结果。

    贪心

    费了这么大劲获得了一个不能用的答案,看起来有些可惜,但是其实一点也不浪费。我们是为了学习算法和提升自己来做题的,而不仅仅是为了通过。就好像本文的产生并不仅仅是作为题解产生的,而是希望实实在在地传递一些知识和技能,这也是我的初心。

    言归正传,我们不用分析也知道,超时的原因很简单,因为我们做了许多无用功。因为我们不论大小,把所有的全排列都求出来了。那么我们能不能有什么办法不用求出所有的全排列就获得答案呢?

    当然是有的,但是要想能够想到这个答案,需要我们对这个问题有更深一点的理解。

    我们可以很容易想到,最小的全排列是升序的,最大的是降序的。那么中间大小的呢,大概是什么样子的?显然是有升有降,参差不齐的。我们观察一下相邻排列的规律,看看能发现什么。

    举个例子,1 2 3的下一个排列是1 3 2,是交换2和3得到的。我们再来看一个,1 3 2 4的下一个排列是1 3 4 2,也是交换了两个元素得到的。如果你列举更多会发现,不论当前的全排列长什么样,我们都可以交换其中的两个元素得到下一个排列。也就是说我们并不需要遍历所有的全排列,只需要找到两个合适的元素,将它们交换即可。

    根据排列顺序递增的条件,我们可以想明白,我们应该找的这两个元素应该是小的在前,大的在后,这样我们将它们交换之后,它们的字典序才会增加。所以到这里,整个问题就变成了我们该怎么样找到这样的元素呢?

    我们可以继续想出一些条件,比如我们可以想到我们选择的这两个元素当中较大的那个应该尽量小,其中小的那个要尽量大,并且这两个元素的位置要尽量靠后,不然的话我们一交换元素,可能带来太多的提升。

    虽然这些条件看起来比较直观,但其实很有用,我们只需要将这些想法想办法加以量化,就可以得到答案了。在此之前,我们先来观察一下一个可能的通用例子,来寻找一下通用的解法。

    03819d87265a3cd735f366755de04287.png

    这是一个典型的排列的展示图,横坐标是元素放置的位置,纵坐标是元素的大小。这个图很经典是在于如果我们抽象一些理解,它可以代表所有的排列。不论什么样的排列我们都可以认为它由三部分组成,第一部分是序列前端的递增的部分,第二个部分是中间参差不齐的部分,和最后递减的部分。对于不同的排列而言,模式都是一样的,差别只在于这三个部分的长度。

    我们对着图简单分析一下,可以发现对于后面降序的部分,我们不论怎么交换都不可能提升字典序,只能降低。对于前面的部分,我们交换元素可以提升字典序,但显然不是最优的结果。那么问题就很明显了,我们应该把关注点放在中间参差不齐的部分。根据我们之前的直观结论,我们选择的元素应该尽可能靠后,所以我们的关注点可以进一步地缩小,可以锁定在第二个部分的末尾,也就是图中的a[i-1]的位置。

    我们选择a[i-1]的主要原因很简单,因为它比a[i]要小,而从a[i]开始降序。那么剩下的问题就更简单了,我们要做的就是从第三部分选择一个比a[i-1]大的元素和它交换位置。这个元素怎么选呢?我们可以简单地贪心,选择比它大的最小的那个。由于a[i] > a[i-1],所以可以确定,这样的元素一定存在。从图上来看,这个比a[i-1]但又尽量小的元素就是a[j]。

    我们把a[j]和a[i-1]交换了之后得到了一个新的排列,并且这个排列比之前的排列大,也就是说我们提升了字典序。但是这还不是最小的情况。原因也很简单,因为最后一部分的元素还是降序的,如果我们把它变成升序,它的字典序还会进一步降低。

    我们来看一个例子:[1,8, 9, 4, 10, 6, 3]

    根据我们之前的结论,我们会将4和6交换,得到:[1, 8, 9, 6, 10, 4, 3],这个字典序比之前更大,但并不是最小的,我们还可以将最后的10,4,3进行翻转,变成3,4,10。这样最终的答案是: [1, 8, 9, 4, 3, 6, 10]

    我们分析一下可以发现,我们交换了两个元素的位置之后,并没有影响数组第三部分的元素是降序的这个特性。所以我们并不需要额外的处理,直接翻转数组就可以。方法也很简单,我们只需要翻转第三部分的数组即可。因为如果分析一下可以发现,我们交换了两个元素的位置之后,并没有影响第三部分的降序。

    那这样,我们只需要寻找两个元素,可以在

    的时间内找到答案。比之前的算法不知道提升了多少,这个算法的本质其实就是贪心,但是需要我们对题目有非常深入的理解才可以做出来。

    这些都想明白了之后,代码就很简单了,我们来看下:

    class Solution:
        def nextPermutation(self, nums: List[int]) -> None:
            """
            Do not return anything, modify nums in-place instead.
            """
            # 长度
            n = len(nums)
            # 记录图中i-1的位置
            pos = n - 1
            for i in range(n-1, 0, -1):
                # 如果降序破坏,说明找到了i
                if nums[i] > nums[i-1]:
                    pos = i-1
                    break
                    
            for i in range(n-1, pos, -1):
                # 从最后开始找大于pos位置的
                if nums[i] > nums[pos]:
                    # 先交换元素,在进行翻转
                    nums[i], nums[pos] = nums[pos], nums[i]
                    # 翻转[pos+1, n]区间
                    nums[pos+1:] = nums[n:pos:-1]
                    return
            
            # 如果没有return,说明是最大的字典序,那么翻转整个数组
            nums.reverse()

    到这里整个问题就结束了,从代码来看这段代码量并不大,但是想要完美写出来,并不容易。尤其是变量之间的关系需要详细地推导,根据我的经验,结合上图来思考会容易理解很多。

    今天的文章就是这些,如果觉得有所收获,请顺手扫码点个关注吧,你们的举手之劳对我来说很重要。

    9a547adbc962053b02fb6245eec35f81.png
    展开全文
  • 宽度优先搜索,用C++语言实现八数码问题
  • 给大家编好了一个八数码宽度搜索算法!使用C语言编写的!绝对运行!不信试试看!如果好大家给个评价!
  • 利用Java实现人工智能的八数码问题的宽度优先算法,实现对该问题的解决
  • 宽度优先搜索算法解决八数码问题

    千次阅读 2020-05-19 23:44:41
    宽度优先搜索算法解决八数码问题 实验原理 1、宽度优先搜索是指在一个搜索树中,搜索以同层邻近节点依次扩展节点。这种搜索是逐层进行的,在对下一层的任一节点进行搜索之前,必须搜索完本层的所有节点。 宽度优先...

    宽度优先搜索算法解决八数码问题

    原理

    1、宽度优先搜索是指在一个搜索树中,搜索以同层邻近节点依次扩展节点。这种搜索是逐层进行的,在对下一层的任一节点进行搜索之前,必须搜索完本层的所有节点。
    宽度优先搜索算法主要步骤可描述如下:
    ①令N为一个由初始状态构成的表。
    ②若N为空退出,标志失败。
    ③令n为N中第一个节点,将n从N中删除。
    ④若n是目标,则退出,标志成功。
    ⑤若n不是目标,将n的后继节点加入到N表的末端,转第②步。

    宽度优先搜索算法流程图下图所示:
    在这里插入图片描述
    2、八数码问题知识表示方法(状态空间法)分析:
    1.定义状态空间:根据八数码问题定义出状态空间。然后通过对象给出问题的初始状态、目标状态,给出状态的一般表示。
    2.定义操作规则:规定一组算子,作用于一个状态后过渡到另一个状态。
    3.定义搜索策略:使用宽度优先搜索,使得能够从初始状态出发,沿某个路径达到目标状态。搜索过程为:沿着4个方向,如果可行都前进一步看是否达到位置。如果没有达到,则依次从新的位置为起点,沿4个方向继续前进一步,直到搜索到目标位置或者找不到未搜索的位置为止。

    3、八数码问题判断有无解
    对于棋子数列中任何一个棋子c[i],如果有j>i且c[j]<c[i],那么 c[j]是c[i]的一个逆序,或者c i]和c[j]构成一个逆序对。定义棋子c[i]的逆序数为c[i]的逆序个数;棋子数列的逆序数为该数列所有棋子的逆序数总和。
    定理:如果棋子数列经过n次相邻棋子交换后,若n为偶数,则数列逆序数奇偶性不变;若n为奇数,则数列逆序数将发生奇偶性互变。
    推广:
    (1)当初始状态棋局的棋子数列的逆序数是奇数时,八数码问题无解;
    (2)当初始状态棋局的棋子数列的逆序数是偶数时,八数码问题有解。

    设计思路

    1、设计状态空间表示方式
    空格定义为0,将数字按行存入列表中,用列表sample存储八数码问题的初始状态,例如[2,8,3,1,6,4,7,0,5];用goal存储八数码问题的目标状态,例如[1,2,3,8,0,4,7,6,5],八数码问题中数字的移动就看做是空格(0)的移动,在列表中直接完成,同时把操作记录存储在列表的末端,如第一步空格向上移动,则移动后的列表变为[2,8,3,1,0,4,7,6,5,‘up’]。
    初始节点为[2,8,3,1,6,4,7,0,5],它是搜索树的根节点,而目标状态为[1,2,3,8,0,4,7,6,5],我们做的,就是找出一条或者多条从根节点通往[1,2,3,8,0,4,7,6,5]节点的路径。

    2、设计一组算子
    用来求解该问题的算子可以用4条规则来描述
    ①空格(0)向上移动(注意:要满足前提条件,即空格移动方向有数字且移动后的状态为初次生成状态,以下规则也一样)
    ②空格(0)向下移动
    ③空格(0)向左移动
    ④空格(0)向右移动
    这四条规则,事实上就是搜索树上从当前节点生成下一个节点的方法。

    3、移动方法以及可移动的条件
    向上移动和向下移动:假设初始状态 在这里插入图片描述 中,0(空格)所在的位置为中心,存储为[2,8,3,1,0,4,7,6,5]。0所在位置的下标为4,若0要向上移动或向下移动,则实质为与8或6交换位置,则交换操作为:

    temp[flag], temp[flag - 3] = temp[flag - 3], temp[flag](向上移动)
    temp[flag], temp[flag + 3] = temp[flag + 3], temp[flag](向下移动)
    

    若0所在的位置已经在边界,即无法向上或向下移动,则判断条件为:

    flag - 3 >= 0(向上) 
    flag + 3 <= 8(向下)
    

    向左移动和向右移动:假设初始状态如上面相同,0所在的位置为中心,存储为[2,8,3,1,0,4,7,6,5],若0要向左向右移动,则实质为与1或4交换位置,交换操作为:

    temp[flag], temp[flag - 1] = temp[flag - 1], temp[flag](向左移动)
    temp[flag], temp[flag + 1] = temp[flag + 1], temp[flag](向右移动)
    

    若0已经在边界,即无法向左或向右移动,则判断条件为:

    flag % 3 != 0(向左) 
    (flag + 1) % 3 != 0(向右)
    

    4、判断有无解实现方法
    通过双重循环,将列表逆序循环,逐一比较,计算逆序对的个数,遇到0则跳过。最后判断奇偶,偶数有解,奇数无解。

    5、宽度优先搜索实现方法
    ①把起始节点放到OPEN表中,如果该起始节点为一目标节点,则求得一个解答。
    ②若OPEN是空表,则没有解,失败退出;否则继续。
    ③把第一个节点(节点死)从OPEN表移出,并把它放入CLOSED的扩展节点表中。
    ④扩展n。如果没有后继节点,则转向步骤②。
    ⑤把n的所有后继节点放到OPEN表的末端,并提供从这些后继节点回到n的指针。
    ⑥如果n的任一个后继节点是个目标节点,则找到一个解答,成功退出;否则转向步骤②;

    完整代码:

    import time
    class QNode(object):#定义队列的数据结构
        def __init__(self, elem, next=None):
            self.elem = elem
            self.next = next
    
    class Queue(object):  # 队列
        def __init__(self):
            self.head = None
            self.rear = None
    
        def is_empty(self):#判断队列是否为空
            return self.head is None
    
        def enqueue(self, elem):  # 往队列中添加一个elem元素
            p = QNode(elem)
            if self.is_empty():
                self.head = p
                self.rear = p
            else:
                self.rear.next = p
                self.rear = p
    
        def dequeue(self):  # 从队列的头部删除一个元素
            result = self.head.elem
            self.head = self.head.next
            return result
    
        def Search(self,elem):#搜索队列中是否有与elem相同的元素
            temp = self.head
            while temp is not None:
                if elem[:9] == temp.elem[:9]:
                    return False
                temp = temp.next
            return True
    
    def jugde(open,closed,temp,i,creatpoint):#判断是否重复
        if open.Search(temp) and closed.Search(temp):
            temp.append(operato[i])
            Open.enqueue(temp)
            creatpoint += 1
        return creatpoint
    
    def  Jugde(sample):#判断有无解
        flag = 0
        for i in sample[::-1]:
            if i != '0':
                this = sample.index(i)
                for x in sample[:this]:
                    if x > i:
                        flag += 1
        if flag % 2 ==0 :
            return 0
        return 1
    
    if __name__ == '__main__':
        sample = list(input('请输入初始状态:').split())#存储8数码问题的初始状态
        goal = list(input('请输入目标状态:').split(' '))#存储8数码问题的目标状态
        m = Jugde(sample)
        n = Jugde(goal)
        if m != n:#通过求逆序数判断有无解
            print('无解!')
            exit()
        operato = ['up','down','left','right']
        k = creatpoint = 0 # k 为搜索的节点数 creatpoint 为生成节点数
        Open = Queue()#创建open表
        Closed = Queue()#创建closed表
        Open.enqueue(sample)
        Max = input("请输入最大搜索深度:")
        start = time.time()
        while(1):
            if(sample == goal ):
                print("起始节点为目标结点!")
                break
            if Open.is_empty():
                print("没有解!")
                break
            else:
                p = Open.dequeue()#从Open表中取出
                if len(p)-9 >= int(Max)+1:#判断是否超过设置的最大深度
                    print('已达最大深度,未找到解!')
                    exit()
                Closed.enqueue(p)#放入Closed表中
                if p[:9] == goal:#判断是否相等
                    k += 1
                    print('当前层次:{},已搜索节点数:{},已生成结点数{}\n查找成功!'.format(len(p)-9,k,creatpoint))
                    print("空格的移动路径依次为:",end = '')
                    for i in p[9:]:
                        print(i,end='->')
                    end = time.time()
                    print('完成\nRunning time:{} Seconds'.format(end - start))
                    exit()
                k += 1
                flag = p.index('0')# 返回列表中0的索引  flag = p.index('0')
                if flag - 3 >= 0 :#空格向上移动
                    temp = p.copy()
                    temp[flag], temp[flag - 3] = temp[flag - 3], temp[flag]
                    creatpoint = jugde(Open,Closed,temp,0,creatpoint)
                if flag + 3 <= 8:#空格向下移动
                    temp = p.copy()
                    temp[flag], temp[flag + 3] = temp[flag + 3], temp[flag]
                    creatpoint = jugde(Open, Closed, temp, 1, creatpoint)
                if flag % 3 != 0 :#空格向左移动
                    temp = p.copy()
                    temp[flag], temp[flag - 1] = temp[flag - 1], temp[flag]
                    creatpoint = jugde(Open, Closed, temp, 2, creatpoint)
                if (flag + 1) % 3 != 0:#空格向右移动
                    temp = p.copy()
                    temp[flag], temp[flag + 1] = temp[flag + 1], temp[flag]
                    creatpoint = jugde(Open, Closed, temp, 3, creatpoint)
    

    运行结果

    参数设置方案:
    初始状态:2 8 3 1 6 4 7 0 5
    目标状态:1 2 3 8 0 4 7 6 5
    最大搜索深度:10
    在这里插入图片描述

    结果讨论:

    对于一些简单的八数码问题,宽度优先算法可以比较快得找到目标,但是对于一些复杂的步数较多的问题,宽度优先搜索的的效率很低
    比如当初始状态为:[2,4 8,6,0,3,1,7,5],目标状态为:[1,2,3,8,0,4,7,6,5]的八数码问题,当最大深度设置为10时,宽度优先搜索无法找到目标状态
    在这里插入图片描述
    当设置最大深度为15时,找到了目标状态。
    在这里插入图片描述

    展开全文
  • 本程序用C语言实现了八数码问题的宽度优先搜索
  • 八数码问题的搜索策略

    八数码问题的求解
    宽度优先搜索

    一、问题描述
    在这里插入图片描述
    二、算法设计
    在这里插入图片描述
    三、源代码

    //程序描述:基于盲目搜索策略的宽度优先搜索方法
    
    #include <iostream>
    #include <string>
    #include <cstring>
    #include <cmath>
    #include <vector>
    #include <queue>
    #include <set>
    using namespace std;
    
    #define N 9       //九宫格总数字
    
    //数组定义:0~9阶乘定义
    const int jc[N + 1] = { 1,1,2,6,24,120,720,5040,40320,362880 };    //0-9的阶乘
    
    //数组定义,移动规则(分别对应空格右移、下移、上移、左移)
    const int zero_move[4][2] = { {0,1}, {1,0},{0,-1},{-1,0} };
    
    //结构体定义:状态结构体
    typedef struct Status
    {
        int array[N];   //状态数组
        int hash;       //存储某一状态的哈希索引
        int pos;        //0(空格)当前位置
        int step;       //记录移动步数
    }Node;
    
    
    //函数功能:康托展开
    //函数参数:某一状态序列
    //函数返回:康托展开值
    int cantor(int arr[N])
    {
        int i, j;
        int sum = 0;
        for (i = 0; i < N; i++)
        {
            int nmin = 0;
            for (j = i + 1; j < N; j++)
            {
                if (arr[i] > arr[j])
                    nmin++;
            }
            sum += (nmin * jc[N - i - 1]);
    
        }
    
        return sum;
    }
    
    //函数功能:交换数组
    void swap(int* array, int i, int j)
    {
        int t = array[i];
        array[i] = array[j];
        array[j] = t;
    }
    
    
    //函数功能:以矩阵形式打印状态数组
    void printArray(int* array)
    {
        for (int i = 0; i < N; i++)
        {
            if (i % 3 == 0)
                cout << "\n";
            cout << array[i] << " ";
        }
        cout << endl;
    }
    
    
    //函数功能:复制数组
    void copyArray(int source[N], int target[N])
    {
        for (int i = 0; i < N; i++)
        {
            target[i] = source[i];
        }
    
    }
    
    
    //函数功能:宽度优先搜索
    //函数参数:初始状态数组,源状态哈希索引,目标状态哈希索引
    //函数返回:搜索步数(空格移动次数)
    int bfs(int array_source[N], int source_Hash, int target_Hash)
    {
        if (source_Hash == target_Hash)  //消除同一状态
            return 0;
    
        set<int> setHash;
        queue<Node> queue_Open;  //Open表
    
        Node now_Status;   //当前状态
        Node next_Status;  //下一状态
    
        copyArray(array_source, now_Status.array);
    
        //确定状态中空格(0)的位置
        int pos = 0;
        for (int i = 0; i < N; i++)
        {
            if (array_source[i] == 0)
                break;
            pos++;
        }
    
        //初始化当前状态结构体
        now_Status.hash = source_Hash;
        now_Status.pos = pos;
        now_Status.step = 0;
        next_Status.step = 0;
        
    
        //把当前结点放入Open表中
        queue_Open.push(now_Status);
        setHash.insert(now_Status.hash);
    
        //Open表不为空则继续搜索
        while (!queue_Open.empty())
        {
            //从Open表中删除第一个状态
            now_Status = queue_Open.front();
            queue_Open.pop();
    
            //对Open表中的每一个状态判断空格右、下、上、左移动
            for (int i = 0; i < 4; i++)
            {
                int offsetX = 0, offsetY = 0;
                //固定某一坐标,移动另一坐标
                offsetX = (now_Status.pos % 3 + zero_move[i][0]);
                offsetY = (now_Status.pos / 3 + zero_move[i][1]);
    
                //边界判断通过后空格进行移动
                if (offsetX >= 0 && offsetX < 3 && offsetY < 3 && offsetY >= 0)
                {
                    copyArray(now_Status.array, next_Status.array);//空格移动复制
                    next_Status.step = now_Status.step;
                   
                    next_Status.step++;
                    swap(next_Status.array, now_Status.pos, offsetY * 3 + offsetX);
    
                    //移动后的状态哈希索引及空格位置
                    next_Status.hash = cantor(next_Status.array);
                    next_Status.pos = (offsetY * 3 + offsetX);
    
                    int begin = setHash.size();
                    setHash.insert(next_Status.hash);
                    int end = setHash.size();
    
                    if (next_Status.hash == target_Hash)
                    {               
                        return next_Status.step;
                    }
    
                    if (end > begin)
                    {
                        queue_Open.push(next_Status);
                    }
    
                }
            }
    
        }
        return -1;
    }
    
    //函数功能:求逆序数
    //函数参数:表示状态的数字序列
    int inversion(int array[N])
    {
        int sum = 0;
        for (int i = 0; i < N; ++i)
        {
            for (int j = i + 1; j < N; ++j)
            {
                if (array[j] < array[i] && array[j] != 0)  //不与0比较
                {
                    sum++;
                }
            }
        }
        return sum;
    
    }
    
    //主程序入口
    int main()
    {
        string str_source;   //源状态字符串
        string str_target;   //目标状态字符串
    
        cout << "请输入初始状态序列:";
        cin >> str_source;
        cout << endl;
        cout << "请输入目标状态序列:";
        cin >> str_target;
        cout << endl;
    
        //源int数组和目标int数组
        int array_source[N];
        int array_target[N];
    
        for (int i = 0; i < 9; i++)
        {
            if (str_source.at(i) >= '0' && str_source.at(i) <= '8')
            {
                array_source[i] = str_source.at(i) - '0';
            }
            else
            {
                array_source[i] = 0;
            }
            if (str_target.at(i) >= '0' && str_target.at(i) <= '8')
            {
                array_target[i] = str_target.at(i) - '0';
            }
            else
            {
                array_target[i] = 0;
            }
        }
    
        //源状态哈希和目标状态哈希
        int sHash, tHash;
        sHash = cantor(array_source);
        tHash = cantor(array_target);
    
        //求初始状态和目标状态的逆序数
        int inver_source = inversion(array_source);
        int inver_target = inversion(array_target);
    
        //具有同奇或同偶排列的八数码才能移动可达,否则不可达
        if ((inver_source + inver_target) % 2 == 0)
        {
            int step = bfs(array_source, sHash, tHash);
            cout << "从初始状态到目标状态空格0的最小移动步数为:"<<step << endl; //输出步数     
        }
        else
        {
            cout << "无法从初始状态到达目标状态!" << endl;
        }
    
        return 0;
    }
    

    运行结果示例
    在这里插入图片描述

    展开全文
  • this.setTitle("八数码"); this.setVisible(true); this.setSize(300,300); Toolkit kit = Toolkit.getDefaultToolkit(); Dimension screenSize = kit.getScreenSize(); int screenWidth = screen...
  • 宽度优先与深度优先算法在八数码问题上的主要操作大同小异,主要的差别在于,带扩展节点扩展之后放在对列的前后的区别,宽度优先放在表后,保证一层一层递进式的处理。深度优先放在表前,保证当前节点能够优先扩展到...
  • 在用宽度优先搜索写了八数码之后,又想着写一下15数码,在八数码的基础上做了一些更改,比如字符数组的截取,还有内存的大小更改,但是还是不尽人意,给大家看看吧,希望大家能给些意见,准备用A星算法重新写一下十...
  • 八数码 使用bfs去做,难点在状态的表示,状态之间距离的表示上。 代码: #include <iostream> #include <algorithm> #include <unordered_map> #include <queue> using namespace std; ...
  • 上一年的人工智能课就已经把八数码的BFS DFS A* 遗传算法都试了一遍.昨天上传旧的时候觉得实现的不是很优雅,现在想重新用python来一遍.这次我实现了一个很大的突破,起码比原来的算法实现的速度提高了几十倍~ 题目如...
  • 采用宽度优先搜索算法,编程实现八数码问题的求解。初始状态和目标状态可自定;采用宽度优先搜索算法,编程实现八数码问题的求解。初始状态和目标状态可自定采用宽度优先搜索算法,编程实现八数码问题的求解。初始...
  • 人工智能实验-八数码问题 3×3九宫棋盘,放置数码为1 -8的8个棋牌,剩下一个空格,只能通过棋牌向空格的移动来改变棋盘的布局。 要求:根据给定初始布局(即初始状态)和目标布局(即目标状态),如何移动棋牌才能从...
  • 参考: 清华大学出版社....内容: 使用python实现了三种不同启发函数的A*搜索算法(图搜索),以及宽度优先搜索和深度优先搜索,并计算扩展结点数和生成结点数,可以输出解路径。有GUI交互界面。 License: MIT ...
  • 使用盲目搜索中的宽度优先搜索算法或者使用启发式搜索中的全局择优搜索或A算法,对任意的八数码问题给出求解结果。例如:对于如下具体的八数码问题: 通过设计启发函数,编程实现求解过程,如果问题有解,给出数码...
  • 本资源包括宽度优先搜索算法解决八数码问题的实验报告以及用python实现的源代码,有较详细的原理和设计思路,代码有详细的注释,适合初学者学习。
  • 八数码(Eight Digits)问题 1. 问题描述 2. 数据结构设计 3. 核心算法设计

空空如也

空空如也

1 2 3 4 5
收藏数 86
精华内容 34
关键字:

八数码宽度优先搜索