大学四年的全部课程和考试都已经结束啦!
最近闲来无事,随便发发自己的实验报告,供后面的学弟学妹们参考~
目录
实验1 基于A*算法的8数码问题求解
1.1 程序总体流程
1.2 关键代码展示
1.3 输出结果展示及分析
1.3.1 总步数展示
1.3.2 每一次的Open表和Closed表展示
1.3.3 对输出结果的分析
1.4 原始程序存在的问题
1.5 程序修改的核心内容
2.1 对两种新的启发式函数的说明
2.2 关键代码展示(3种不同的启发式函数)
2.3 输出结果展示及对比分析
2.3.1 总步数展示及对比分析
2.3.2 Open表和Closed表展示及对比分析
2.4 三种启发式函数的特点及比较
2.5 实验一心得与体会
实验1 基于A*算法的8数码问题求解
(1)问题描述:在3*3的棋盘中有8个数码(数字块)和一个空格,只有与空格相邻的数码能移动到空格位置。从初始状态以最小的步长移动到目标状态。
(2)实验要求:参考A*算法核心代码,以8数码问题为例实现A*算法。
(3)实验内容:
1、基于参考代码,修改并运行程序,并画出程序的总体流程图。自动统计并输出到达目标的总步数,自动记录并输出每次移动后open表与closed表的变化。如果原始程序中存在不合理的逻辑,请指出其问题所在。如果程序进行了修改,请说明修改的核心内容。要求初始状态与目标状态分别为:
2、设计两种新的启发式函数,并运行程序。对包含示例在内的3种启发式函数的特点与结果进行对比分析。
1.1 程序总体流程
本实验是基于A*算法的求解8数码问题的实现。下面是该程序的总体流程:
(1)初始化初始状态和目标状态 初始状态和目标状态都是一个3x3的矩阵,分别代表了初始时的数码布局和目标状态下的数码布局。
(2)定义状态类 State State 类用于表示问题中的一个状态。每个状态包含当前数码布局、可以移动的方向、父状态、从初始状态到当前状态的实际代价 g(n) 和当前状态到目标状态的估计代价 h(n)。这里使用了 Manhattan 距离作为启发式函数进行估计。
State 类中包含了一系列方法来获取方向、计算0点的位置、生成下一个可能的状态、计算启发式函数值等。
(3)A*算法求解 在 solve 方法中,程序使用 A* 算法来求解问题。初始时将初始状态加入 Open 表,然后循环进行以下步骤:
从 Open 表中选择 f(n) 最小的状态,将其移到 Closed 表中。
检查选择的状态是否为目标状态,如果是,则构建路径并返回;否则,生成当前状态的下一个可能状态,并加入 Open 表中。
对加入到 Open 表中的状态,计算其 g(n) 和 h(n),并更新其 f(n) 值。
如果 Open 表为空,且没有找到目标状态,则说明无解。
(4)打印结果 如果找到了解,程序将打印出解决方案的总步数,并逐步打印出每个状态的数码布局。如果未找到解,则打印出无解的消息。
总体而言,该程序的流程是先初始化状态,然后使用A*算法进行搜索,直到找到解或确定无解为止。
本程序的总体流程详见图1-1。
1.2 关键代码展示
本实验的关键代码如下所示。
class State:
answer = np.array([[1, 2, 3], [8, 0, 4], [7, 6, 5]])
def __init__(self, state, directionFlag=None, parent=None, f=0):
self.state = state
self.direction = ['up', 'down', 'right', 'left']
if directionFlag:
self.direction.remove(directionFlag)
self.parent = parent
self.f = f
def getFunctionValue_ManhattanDistance(self, g_value):
dist = 0
for i in range(len(self.state)):
for j in range(len(self.state)):
if self.state[i][j] != State.answer[i][j]:
index = np.argwhere(State.answer == self.state[i][j])
x = index[0][0]
y = index[0][1]
dist += (abs(x - i) + abs(y - j))
return dist + g_value
def nextStep(self):
subStates = []
x, y = self.getZeroPos()
boarder = len(self.state) - 1
g_value = self.f - self.getFunctionValue_ManhattanDistance(0)
if 'left' in self.direction and y > 0:
s = self.state.copy()
s[x, y], s[x, y - 1] = s[x, y - 1], s[x, y]
news = State(s, directionFlag='right', parent=self)
news.setF(news.getFunctionValue_ManhattanDistance(g_value))
subStates.append(news)
if 'up' in self.direction and x > 0:
s = self.state.copy()
s[x, y], s[x - 1, y] = s[x - 1, y], s[x, y]
news = State(s, directionFlag='down', parent=self)
news.setF(news.getFunctionValue_ManhattanDistance(g_value))
subStates.append(news)
if 'down' in self.direction and x < boarder:
s = self.state.copy()
s[x, y], s[x + 1, y] = s[x + 1, y], s[x, y]
news = State(s, directionFlag='up', parent=self)
news.setF(news.getFunctionValue_ManhattanDistance(g_value))
subStates.append(news)
if 'right' in self.direction and y < boarder:
s = self.state.copy()
s[x, y], s[x, y + 1] = s[x, y + 1], s[x, y]
news = State(s, directionFlag='left', parent=self)
news.setF(news.getFunctionValue_ManhattanDistance(g_value))
subStates.append(news)
return subStates
def solve(self):
openTable = []
closeTable = []
openTable.append(self)
step = 0
while len(openTable) > 0:
step += 1
n = openTable.pop(0)
closeTable.append(n)
subStates = n.nextStep()
for subState in subStates:
if (subState.state == State.answer).all():
openTable.append(subState)
openTable.sort(key=lambda state: state.f)
path = [subState]
while subState.parent and subState.parent != self:
path.append(subState.parent)
subState = subState.parent
path.reverse()
# 此处省略打印Open表和Closed表代码,详见源程序 #
return path
openTable.append(subState)
openTable.sort(key=lambda state: state.f)
# 此处省略打印Open表和Closed表代码,详见源程序 #
return None
1.3 输出结果展示及分析
1.3.1 总步数展示
本程序运行后得到的总步数为5,详见表1-1、表1-2。
表1-1 结果展示(总步数)
初始状态 | 目标状态 | 是否有解 | 启发式函数 | 总步数 |
2 8 3 1 6 4 7 0 5 | 1 2 3 8 0 4 7 6 5 | 是 | 曼哈顿距离 | 5 |
表1-2 每一步的状态变化
步数 | 当前状态 |
0(初始状态) | 2 8 3 1 6 4 7 0 5 |
1 | 2 8 3 1 0 4 7 6 5 |
2 | 2 0 3 1 8 4 7 6 5 |
3 | 0 2 3 1 8 4 7 6 5 |
4 | 1 2 3 0 8 4 7 6 5 |
5(目标状态) | 1 2 3 8 0 4 7 6 5 |
1.3.2 每一次的Open表和Closed表展示
下面展示的是每一次的Open表和Closed表的完整输出内容,详见表1-3和图1-3。(注明:最后一次已经找到目标状态,所以程序未输出此步的Open表和Closed表。)
表1-3 每一步的Open表和Closed表
次数 | Open表 | Closed表 |
1 | 2 8 3 2 8 3 2 8 3 1 0 4 1 6 4 1 6 4 7 6 5 0 7 5 7 5 0 | 2 8 3 1 6 4 7 0 5 |
2 | 2 0 3 2 8 3 2 8 3 1 8 4 0 1 4 1 4 0 7 6 5 7 6 5 7 6 5
2 8 3 2 8 3 1 6 4 1 6 4 0 7 5 7 5 0 | 2 8 3 2 8 3 1 6 4 1 0 4 7 0 5 7 6 5 |
3 | 0 2 3 2 8 3 2 8 3 1 8 4 0 1 4 1 4 0 7 6 5 7 6 5 7 6 5 2 8 3 2 8 3 2 3 0 1 6 4 1 6 4 1 8 4 0 7 5 7 5 0 7 6 5 | 2 8 3 2 8 3 2 0 3 1 6 4 1 0 4 1 8 4 7 0 5 7 6 5 7 6 5 |
4 | 1 2 3 2 8 3 2 8 3 0 8 4 0 1 4 1 4 0 7 6 5 7 6 5 7 6 5 2 8 3 2 8 3 2 3 0 1 6 4 1 6 4 1 8 4 0 7 5 7 5 0 7 6 5 | 2 8 3 2 8 3 2 0 3 1 6 4 1 0 4 1 8 4 7 0 5 7 6 5 7 6 5 0 2 3 1 8 4 7 6 5 |
5 | 1 2 3 2 8 3 2 8 3 8 0 4 0 1 4 1 4 0 7 6 5 7 6 5 7 6 5 2 8 3 2 8 3 2 3 0 1 6 4 1 6 4 1 8 4 0 7 5 7 5 0 7 6 5 1 2 3 7 8 4 0 6 5 | 2 8 3 2 8 3 2 0 3 1 6 4 1 0 4 1 8 4 7 0 5 7 6 5 7 6 5 0 2 3 1 2 3 1 8 4 0 8 4 7 6 5 7 6 5 |
6 | 当前Sg是目标状态,成功退出,本次搜索结束! |
1.3.3 对输出结果的分析
在这个结果中,搜索算法成功地找到了从初始状态到目标状态的路径,总共经过了5个步骤。这个结果表明了所使用的A*算法能够有效地在状态空间中搜索,并找到最优解。其中,分析结果的关键点包括:
(1)搜索策略有效性 搜索过程中,每一步都是根据启发式函数的估计值来决定下一步的状态选择,这使得算法能够朝着更有可能达到目标的方向前进。
(2)搜索算法优化 在搜索过程中,采用了一些优化策略,比如对已经搜索过的状态进行记录和排除,以避免重复搜索。此外,对于下一步可能的状态,根据启发式函数的估值进行排序,优先选择估值较小的状态,以加速搜索过程。
(3)搜索路径可视化 最终输出了搜索过程中的每个步骤和状态,这有助于理解算法的执行过程,并且提供了一种对算法性能的直观评估方式。
总的来说,这个结果表明所使用的搜索算法在解决八数码问题上具有较好的效果,能够快速而有效地找到最优解。
1.4 原始程序存在的问题
原始代码的逻辑错误在于nextStep函数中的实现和solve函数中的调用方式。 具体来说,nextStep函数应该返回一个包含所有可能下一步状态的列表,而不是仅返回一个状态。当前的实现在每次调用nextStep时只返回一个状态,这会导致只能扩展一个状态,而不是所有可能的状态。
在solve函数中,虽然将subStates加入了openTable,但是openTable应该存放待探索的状态,而不是直接放入subStates,应该将subStates中的所有状态加入openTable,而不是仅加入其中的一个。
除了上述逻辑问题外,我还发现原始代码存在下列问题。
(1)getFunctionValue 方法中的变量 self.answer 未定义。这个变量在类中未被初始化或设置。为了解决这个问题,可以将 State 类的构造函数修改为接收 answer 参数,并在初始化时设置该属性。
(2)在 nextStep 方法中,当检查是否可以向右移动时,使用了 count 方法来检查 'right' 是否在 self.direction 中。应该使用 in 关键字来检查。修正这一问题后,if 语句应该是 if 'right' in self.direction and y < boarder:。
(3)在使用曼哈顿距离作为启发式函数getFunctionValue的实现中,g(x)不应该恒等于1,而应该等于初始状态到当前状态的实际路径成本。
1.5 程序修改的核心内容
原始程序的核心修改内容如下。
(1)修改了 nextStep 方法,使其返回包含所有可能下一步状态的列表,而不是仅返回一个状态。
(2)在 solve 方法中,将 subStates 中的所有状态都加入 openTable 中,而不仅仅是其中的一个状态。
(3)在 State 类中添加了 answer 参数,以解决 getFunctionValue 方法中的 self.answer 未定义的问题。
(4)在 getFunctionValue 方法中,计算实际路径成本 g(x),而不是恒等于 1。这通过计算从初始状态到当前状态的移动步数来实现。
(5)在 nextStep 方法中,使用 in 关键字来检查方向是否在 self.direction 中,并修复了向右移动的判断逻辑。
2.1 对两种新的启发式函数的说明
当使用A*搜索算法解决问题时,需要使用启发式函数来评估状态的优劣。这些启发式函数可以帮助算法更有效地搜索最优解。在本实验中,除了一开始使用的曼哈顿距离之外,我还设计了以下两种新的启发式函数。
(1)不在位数字的数量 这个启发式函数简单地计算当前状态中与目标状态不同的数字的数量。这些数字的数量越多,表示状态越远离目标状态。因此,该启发式函数的值越大,表示状态越不优。具体地,如果一个数字不在它应该在的位置上,则它是一个不在位数字。不在位数字的数量可以直接用来评估状态的距离。
(2)欧几里得距离 欧几里得距离是当前状态与目标状态的每个数字位置的欧几里得距离之和。简单来说,对于每个数字,我们计算其在当前状态和目标状态中的位置之间的直线距离,然后将所有数字的距离加起来。这个距离是欧几里得距离的一种应用。该启发式函数考虑了数字在空间上的位置,可以更准确地评估状态之间的距离。因此,如果一个状态中的数字与目标状态的数字更接近,那么欧几里得距离就会更小,启发式函数值也会更小,表示状态更优。
在实践中,这两种启发式函数都可以有效地帮助A*算法搜索到最优解。不在位数字的数量是一种直观的评估方式,但可能不够准确。而欧几里得距离考虑了数字之间的空间关系,提供了更精确的评估,但也更加复杂和计算密集。在选择启发式函数时,需要根据具体情况进行权衡和选择。
2.2 关键代码展示(3种不同的启发式函数)
#使用曼哈顿距离作为启发式函数
def getFunctionValue_ManhattanDistance(self, g_value):
dist = 0
for i in range(len(self.state)):
for j in range(len(self.state)):
if self.state[i][j] != State.answer[i][j]:
index = np.argwhere(State.answer == self.state[i][j])
x = index[0][0]
y = index[0][1]
dist += (abs(x - i) + abs(y - j))
return dist + g_value
#使用不在位个数作为启发式函数
def getFunctionValue_notInPosition(self):
count = 0
for i in range(len(self.state)):
for j in range(len(self.state[0])):
if self.state[i][j] != State.answer[i][j]:
count += 1
if self.state[1][1] == State.answer[1][1]:
count += 1
return count - 1
#使用欧几里得距离作为启发式函数
def getFunctionValue_EuclideanDistance(self, g_value):
dist = 0
for i in range(len(self.state)):
for j in range(len(self.state[0])):
if self.state[i][j] != 0 and self.state[i][j] < len(self.state) * len(self.state[0]):
target_i, target_j = divmod(State.answer.flatten().tolist().index(self.state[i][j]), len(self.state))
dist += ((target_i - i) ** 2 + (target_j - j) ** 2) ** 0.5
return dist + g_value
2.3 输出结果展示及对比分析
下面展示的是三种不同启发式函数的程序运行结果,其中曼哈顿距离作为启发式函数的结果已经在第1个问题中进行了详细说明,因此这里不再赘述。
2.3.1 总步数展示及对比分析
(1)不在位数字的数量 本程序运行后得到的总步数为5,详见图2-1。
(2)欧几里得距离 本程序运行后得到的总步数为5,详见图2-2。
(3)对比分析 由上述结果可知,三种不同的启发式函数的总步数都是相同的,均为5步。
从效率上来看,由三种启发式函数的总步数相同可知,在这个问题中的效率是相同的,它们都找到了最优路径。
从复杂度来看,虽然总步数相同,但每种启发式函数在搜索过程中所考虑的因素不同。不在位数字的数量直接计算了当前状态与目标状态之间的差异,欧几里得距离和曼哈顿距离则考虑了数字的位置信息。在实际问题中,曼哈顿距离通常更容易计算,因为它只涉及数字的位置之差的绝对值之和。
2.3.2 Open表和Closed表展示及对比分析
(1)不在位数字的数量 本程序运行后得到的Open表和Closed表,如图2-3所示。
(2)欧几里得距离 本程序运行后得到的Open表和Closed表和以曼哈顿距离作为启发式函数的结果完全一致,在此不再赘述。
(3)对比分析 由上述结果可知,当不在位数字的数量作为启发式函数时,输出的Open表和Closed表和另外两个启发式函数的不同。
每种启发式函数的Open表和Closed表在每个步骤中可能会有所不同。这是因为不同的启发式函数会影响搜索算法如何评估和排序每个状态的优先级,从而影响到算法的具体执行路径。虽然三者的最终解决方案路径相同的,但在搜索过程中,Open表和Closed表的具体内容可能会有所不同。
不在位数字的数量作为启发式函数,会根据每个状态中不在位的数字数量来评估其与目标状态的距离。这种方式可能会导致搜索算法在展开状态时优先考虑那些不在位数字数量较少的状态,从而影响Open表和Closed表的内容。这也是为什么不在位数字的数量作为启发式函数时,得到了与另外两个启发式函数不同的输出过程。
而欧几里得距离和曼哈顿距离作为启发式函数,考虑了数字的位置信息,因此在搜索过程中可能会产生不同的评估结果,从而影响Open表和Closed表的内容。在这里,我们的发现欧几里得距离和曼哈顿距离作为启发式函数,得到结果相同。
因此,即使是相同的问题和初始状态,不同的启发式函数可能会导致不同的搜索路径和展开顺序,最终可能会得到略有不同的Open表和Closed表。
2.4 三种启发式函数的特点及比较
最后让我们对这三种启发式函数的各自特点进行比较分析。
对于不在位数字的数量,其特点及比较分析如下所示。
(1)简单直观 只需计算当前状态与目标状态中不同数字的数量。
(2)计算简单 只需遍历状态矩阵一次即可完成计算。
(3)比较分析 该启发式函数的结果会高估状态之间的距离,因为它只考虑了数字的数量而忽略了数字的位置信息。
当状态之间的差异主要是由不在位数字引起时,该启发式函数可能会提供合理的估计,但在某些情况下可能不够准确。
对于欧几里得距离,其特点及比较分析如下所示。
(1)考虑空间关系 计算每个数字在空间中的位置,并将其转化为欧几里得距离。
(2)提供精确估计 考虑了数字之间的实际距离,提供了更精确的状态距离估计。
(3)比较分析 由于考虑了数字之间的实际距离,该启发式函数提供了更准确的状态距离估计。但是,计算欧几里得距离可能会更复杂,尤其是对于大型状态空间。
对于曼哈顿距离,其特点及比较分析如下所示。
(1)考虑位移关系 考虑了每个数字在水平和垂直方向上的位移。
(2)简化计算 只需计算数字的水平和垂直位移的绝对值之和。
(3)比较分析 曼哈顿距离提供了对状态距离的相对准确估计。
与欧几里得距离相比,曼哈顿距离更简单,计算速度更快,但通常会高估状态之间的距离。
综上所述,不同的启发式函数在八数码问题中都有其优缺点。欧几里得距离提供了最准确的距离估计,但计算复杂度较高。曼哈顿距离相对较简单,计算速度快,但可能会高估状态之间的距离。而不在位数字的数量则是一种简单直观的评估方式,计算简单但可能不够准确。在实践中,根据问题的具体情况和要求,选择适当的启发式函数非常重要。
2.5 实验一心得与体会
在本次实验中,我实现了A*搜索算法来解决八数码问题,并设计了三种不同的启发式函数。下面是我在完成本实验后的一些个人心得与体会。
首先,启发式函数对A*搜索算法的性能至关重要。曼哈顿距离作为一种经典的启发式函数,简单易用且在许多情况下能提供合理的结果。然而,本次实验中,我发现不在位数字的数量和欧几里得距离这两种新的启发式函数也能有效地帮助算法找到最优解。这提醒我在解决问题时应该多角度思考,不拘泥于传统的方法,而是灵活运用各种技术手段。
其次,实验中对比了三种启发式函数的性能及特点。欧几里得距离在提供更准确的状态距离估计方面具有优势,但计算复杂度相对较高;曼哈顿距离计算简单且速度快,但可能会高估状态之间的距离;而不在位数字的数量则简单直观,但准确性可能不足。这表明在选择启发式函数时需要综合考虑准确性和计算复杂度,并根据具体问题的需求做出权衡。
最后,实验过程中我也意识到了算法的执行路径可能会受到启发式函数的影响。不同的启发式函数会导致搜索算法展开状态的顺序不同,从而影响到算法的执行路径。这提示我在实际问题中,要对不同的启发式函数进行综合评估,并选择最适合的启发式函数以达到更好的性能。
总的来说,本次实验让我深入了解了A*搜索算法及启发式函数的原理和应用,提高了我对算法设计和问题求解的理解能力。同时也加深了我对算法性能评估和选择的认识,为我今后在解决实际问题时提供了有益的指导。