1.给出 3 个启发式方法的例子,解释它们如何在以下情景中发挥重要作用。
(a)在日常生活中。
(b)在面对某种挑战时的问题求解过程中。
---
(a) 在日常生活中,启发式方法可以帮助我们做出更明智的决策和解决问题。以下是三个启发式方法的例子,以及它们在日常生活中的应用:
- 启发式评估:这种方法基于我们对问题的经验和知识,通过评估不同选择的可能结果来做出决策。例如,当我们在购物时,我们可以使用启发式评估来比较不同品牌、价格和质量,以选择最合适的产品。
- 边际效益启发式:这种方法通过评估每个选择的边际效益来做出决策。边际效益是指每个选择所带来的额外收益或成本。例如,在安排时间时,我们可以使用边际效益启发式来决定如何分配时间和资源,以获得最大的效益。
- 归纳启发式:这种方法基于归纳推理,从已有的信息中推断出一般规律,并将其应用于新的情境。例如,在学习新知识时,我们可以使用归纳启发式来推断出一般规律,并将其应用于解决类似的问题。
(b) 在面对某种挑战时的问题求解过程中,启发式方法可以提供指导和帮助我们更高效地解决问题。以下是三个启发式方法的例子,以及它们在问题求解过程中的应用:
-
分而治之启发式:这种方法将复杂的问题分解为更小、更易解决的子问题,并逐个解决它们。例如,在编程中,我们可以使用分而治之的启发式方法来将一个大型的程序分解为多个模块,并分别实现和测试每个模块。
-
启发式搜索:这种方法通过启发函数来指导搜索方向,以快速找到解决问题的路径。例如,在寻找最短路径时,我们可以使用启发式搜索算法(如A*算法),通过估计每个节点到目标的距离来选择下一步的扩展节点,以减少搜索的时间和空间复杂度。
-
试错启发式:这种方法通过不断尝试和调整策略,通过错误和反馈来学习和改进解决问题的方法。例如,在解决复杂的数学问题时,我们可以使用试错启发式来尝试不同的方法和技巧,并根据错误和反馈来调整和改进我们的解决方法。
2.解释爬山法被归类为“贪心算法”的原因。
(a)描述你知道的其他一些“贪心”算法。
(b)最陡爬山法是如何改进爬山法的?最佳优先搜索是如何改进爬山法的?
爬山法(Hill Climbing)是一种基于局部搜索的优化算法,被归类为“贪心算法”的原因是它在每一步选择时都只考虑当前的最优解,而不考虑全局最优解。爬山法会从当前解开始,通过比较邻近解的优劣来选择下一步的移动方向,直到达到一个局部最优解。
(a) 其他一些“贪心”算法的例子包括:
- 背包问题的贪心算法:在背包问题中,贪心算法会根据物品的价值和重量比例,选择每次放入背包的物品,使得每次放入的物品都能获得最大的价值重量比。
- 最小生成树问题的贪心算法:在最小生成树问题中,贪心算法会从一个节点开始,每次选择一条边,将一个新节点添加到生成树中,直到生成树包含所有的节点,并且总权重最小。
- 短作业优先调度算法:在作业调度中,贪心算法会选择最短的作业来执行,以最小化作业的等待时间和周转时间。
(b) 最陡爬山法(Steepest Ascent Hill Climbing)是对爬山法的改进。最陡爬山法在每一步选择时,不仅考虑当前邻近解的优劣,还会考虑所有邻近解中最优解的方向。它会选择能够达到全局最优解的邻近解作为下一步的移动方向,而不仅仅是局部最优解。
- 最佳优先搜索(Best-First Search)也是对爬山法的改进。最佳优先搜索通过启发函数来评估每个邻近解的好坏,并选择最有希望的邻近解作为下一步的移动方向。这样可以更加全面地考虑问题的解空间,而不仅仅局限于局部最优解。最佳优先搜索可以用于解决路径规划、图搜索等问题。
4.关于启发式方法,请完成以下练习。
(a)为传教士与野人问题建议一个可接受的启发值,这个启发值应该足够健壮,从而避免
不安全的状态。
(b)你的启发式方法能够提供足够的信息,以明显地减少 A*算法所要探索的库吗?
---
(a) 传教士与野人问题是一个经典的问题,要求在一条河上将若干传教士和野人从一岸移动到另一岸,但要满足一定的条件。为了设计一个可接受的启发值,我们可以考虑以下因素:
- 保持安全性:启发值应该足够健壮,以避免移动到不安全的状态。在传教士与野人问题中,不安全的状态是指在任何一侧,野人的数量超过传教士的数量。因此,一个可接受的启发值可以是当前岸上野人数量和传教士数量之差的绝对值。
- 考虑目标状态:启发值还可以考虑目标状态,即将所有传教士和野人都移动到另一岸的情况。可以将目标状态的启发值设为0,表示已经达到最终目标。这样,在搜索过程中,启发值越接近0,表示离目标状态越近。
综合考虑上述因素,一个可接受的启发值可以是当前岸上野人数量和传教士数量之差的绝对值,再加上离目标状态的距离。
(b) 启发式方法可以提供足够的信息,以明显地减少A*算法所要探索的空间。启发式方法通过启发函数来评估每个节点的优劣,并指导搜索方向。如果启发函数设计得好,它可以提供有用的信息,帮助算法更快地找到解决问题的路径。
在A算法中,启发函数用于估计每个节点到目标节点的代价。如果启发函数是一致的(admissible),即它对每个节点的估计都不会高估实际代价,那么A算法可以保证找到最优解,并且会明显减少搜索的空间。
然而,启发式方法的效果取决于问题的特性和启发函数的设计。某些问题可能存在启发函数无法提供足够信息的情况,导致A算法仍需探索大量的空间。因此,在设计启发函数时,需要结合问题的特性和启发函数的性质,以提供足够的信息,以明显减少A算法的搜索空间。
5.关于启发值,请完成以下练习。
(a)提供适用于图形着色的启发值。
在图形着色问题中,我们需要为给定的图形的每个节点分配一个颜色,使得相邻节点的颜色不相同。启发值可以提供一种评估当前节点的优劣程度的方法。以下是一个适用于图形着色问题的启发值的示例:
启发值可以基于当前节点的相邻节点已经分配的颜色数量来评估。具体而言,可以使用以下启发值计算方法:
- 最大相邻颜色数量:对于当前节点,计算其相邻节点已经分配的颜色数量,然后取最大值作为启发值。这样的启发值可以帮助我们优先选择那些相邻节点颜色数量最多的节点,以增加分配颜色的灵活性。
- 相邻节点中最少已分配的颜色数量:对于当前节点,计算其相邻节点已经分配的颜色数量,然后取最小值作为启发值。这样的启发值可以帮助我们优先选择那些相邻节点颜色数量最少的节点,以减少分配颜色的冲突可能性。
- 相邻节点颜色数量的平均值:对于当前节点,计算其相邻节点已经分配的颜色数量的平均值作为启发值。这样的启发值可以帮助我们优先选择那些相邻节点颜色数量接近平均值的节点,以保持颜色分配的平衡性。
10.在第 2 章中,我们提出了骑士之旅问题。其中,棋盘上的马访问了 n×n 棋盘上的每一个方块。这个挑战一开始会在完整的 8×8 棋盘上给定一个源方块[比如方块(1,1)],目标是找到移动序列,该序列会访问棋盘上的每个方块,每个方块都会被访问且仅仅被访问一次,在最后一次移动中,马回到源方块。
(a)从方块(1,1)开始,尝试解决骑士之旅问题。(提示:对于这个版本的骑士之旅问题,
你可能会发现需要大量内存来解决。因此,你可能需要确定一个启发式方法,以帮助引导搜索
过程。)
(b)尝试找到一种启发式方法,它能帮助初次求解器找到正确解
----
(a) 骑士之旅问题是一个经典的组合优化问题,它要求马在棋盘上访问每个方块一次,并最终回到起始方块。对于一个完整的8×8棋盘,解决骑士之旅问题是非常困难的,因为搜索空间非常大。在这种情况下,需要使用一种启发式方法来引导搜索过程。
一种常见的启发式方法是Warnsdorff规则,它基于以下原则进行选择下一步的移动方向:选择那些具有最少可能移动目标的方块作为下一步的移动目标。具体步骤如下:
- 从起始方块开始,将当前方块标记为已访问。
- 对于当前方块的所有邻居方块,计算每个邻居方块的可移动目标数量。
- 选择具有最少可移动目标数量的邻居方块作为下一步的移动目标。
- 将马移动到选择的邻居方块,并将该方块标记为已访问。
- 重复步骤2-4,直到所有方块都被访问一次。
- 最后一步移动的邻居方块应该是起始方块,以完成骑士之旅。
(b) 对于初次求解器找到正确解的启发式方法,可以考虑以下方法:
- 距离启发式方法:计算当前方块到起始方块的欧几里得距离,选择距离最小的邻居方块作为下一步的移动目标。这样可以尽可能接近起始方块,增加回到起始方块的可能性。
- 近邻方块数量启发式方法:对于当前方块的所有邻居方块,计算每个邻居方块的邻居方块数量,选择具有最少邻居方块数量的邻居方块作为下一步的移动目标。这样可以尽可能避免陷入死胡同,增加找到正确解的可能性。
- 随机启发式方法:在选择下一步的移动目标时,随机选择一个邻居方块作为下一步的移动目标。这样可以增加搜索的多样性,可能有助于找到正确解。
11.编写一个程序,在图 3.17 中应用本章描述的主要启发式搜索算法,比如爬山法、集束搜索、最佳优先搜索、带有或不带有低估计值的分支定界法以及 A*算法等等。
为了演示如何应用本章描述的主要启发式搜索算法,包括爬山法、集束搜索、最佳优先搜索、带有或不带有低估计值的分支定界法以及A*算法。请注意这只是一个简单的示例,具体的实现可能会有所不同,具体取决于编程语言和问题的特性。
# 导入所需的库
import heapq
# 定义问题的状态表示和启发函数
class State:
def __init__(self, value, heuristic):
self.value = value
self.heuristic = heuristic
# 定义问题的启发函数
def heuristic(state):
# 返回状态的启发值
return state.heuristic
# 定义爬山法
def hill_climbing(initial_state):
current_state = initial_state
while True:
neighbors = generate_neighbors(current_state)
best_neighbor = max(neighbors, key=heuristic)
if heuristic(best_neighbor) <= heuristic(current_state):
return current_state
current_state = best_neighbor
# 定义集束搜索
def beam_search(initial_state, beam_width):
current_states = [initial_state]
while True:
neighbors = []
for state in current_states:
neighbors.extend(generate_neighbors(state))
neighbors.sort(key=heuristic)
current_states = neighbors[:beam_width]
if heuristic(current_states[0]) == 0:
return current_states[0]
# 定义最佳优先搜索
def best_first_search(initial_state):
frontier = []
heapq.heappush(frontier, (heuristic(initial_state), initial_state))
while frontier:
_, current_state = heapq.heappop(frontier)
if heuristic(current_state) == 0:
return current_state
neighbors = generate_neighbors(current_state)
for neighbor in neighbors:
heapq.heappush(frontier, (heuristic(neighbor), neighbor))
# 定义带有低估计值的分支定界法
def branch_and_bound_with_underestimate(initial_state):
best_solution = None
frontier = [initial_state]
while frontier:
current_state = frontier.pop(0)
if heuristic(current_state) == 0:
if not best_solution or current_state.heuristic < best_solution.heuristic:
best_solution = current_state
else:
neighbors = generate_neighbors(current_state)
frontier.extend(neighbors)
return best_solution
# 定义A*算法
def a_star(initial_state):
frontier = []
heapq.heappush(frontier, (heuristic(initial_state), initial_state))
while frontier:
_, current_state = heapq.heappop(frontier)
if heuristic(current_state) == 0:
return current_state
neighbors = generate_neighbors(current_state)
for neighbor in neighbors:
heapq.heappush(frontier, (heuristic(neighbor) + current_state.heuristic, neighbor))
# 生成邻居状态的函数,根据具体问题进行定义
def generate_neighbors(state):
# 根据当前状态生成邻居状态
pass
# 主函数
def main():
# 初始化问题的初始状态
initial_state = State(initial_value, initial_heuristic)
# 应用不同的启发式搜索算法
result_hill_climbing = hill_climbing(initial_state)
result_beam_search = beam_search(initial_state, beam_width)
result_best_first_search = best_first_search(initial_state)
result_branch_and_bound = branch_and_bound_with_underestimate(initial_state)
result_a_star = a_star(initial_state)
# 打印结果
print("Hill Climbing:", result_hill_climbing)
print("Beam Search:", result_beam_search)
print("Best First Search:", result_best_first_search)
print("Branch and Bound:", result_branch_and_bound)
print("A*:", result_a_star)
# 运行主函数
if __name__ == '__main__':
main()
在这个示例程序中,你需要根据具体问题定义generate_neighbors
函数,用于生成邻居状态。另外,你还需要根据具体问题定义initial_value
和initial_heuristic
,作为问题的初始状态和启发值。
12.在第 2 章中,我们提出了 n 皇后问题。编写一个程序来解决 8 皇后问题,在这个问题中,一旦放置某个皇后,就考虑应用移除任何受到攻击的行和列的约束条件。
以下是一个解决8皇后问题的Python程序示例:
def solve_n_queens(n):
def backtrack(row, queens):
if row == n:
result.append(queens)
return
for col in range(n):
if is_valid(row, col, queens):
backtrack(row + 1, queens + [col])
def is_valid(row, col, queens):
for i in range(row):
if queens[i] == col or \
queens[i] - i == col - row or \
queens[i] + i == col + row:
return False
return True
result = []
backtrack(0, [])
return result
# 解决8皇后问题
solutions = solve_n_queens(8)
# 打印解决方案
for solution in solutions:
board = [['.' for _ in range(8)] for _ in range(8)]
for row, col in enumerate(solution):
board[row][col] = 'Q'
for row in board:
print(' '.join(row))
print()
这个程序使用回溯法来解决8皇后问题。首先定义了一个回溯函数backtrack
,它逐行放置皇后,并检查当前位置是否有效。如果遍历到最后一行,则将当前解决方案添加到结果列表中。然后定义了一个辅助函数is_valid
,用于检查当前位置是否受到攻击。最后,调用solve_n_queens
函数来解决8皇后问题,并打印所有解决方案。
13.对于骑士之旅问题的 64 次移动的解,在某一点上,必须放弃你在练习 10.b 中所要确定的启发式方法。尝试确定该点。
骑士之旅问题是一个经典的问题,目标是找到一个骑士在国际象棋棋盘上完成一次遍历所有格子的路径。
在练习10.b中,我们可以使用启发式方法(如 Warnsdorff’s rule)来尝试解决该问题。然而,当限制骑士的移动次数为64次时,启发式方法可能无法找到解决方案。
在64次移动的情况下,骑士将遍历棋盘上的每个格子一次。这意味着每个格子都将成为路径的一部分,而不会有任何选择。因此,无论我们选择哪个点作为起始点,最终的路径都将是相同的。
因此,在64次移动的情况下,我们不需要使用启发式方法来选择起始点。我们可以简单地选择棋盘上的任意一个格子作为起始点,然后使用回溯法来找到一条满足条件的路径。
以下是一个使用回溯法解决64次移动的骑士之旅问题的Python程序示例:
def solve_knight_tour(n):
def is_valid_move(x, y):
return 0 <= x < n and 0 <= y < n and not visited[x][y]
def backtrack(x, y, move_count):
visited[x][y] = True
path.append((x, y))
if move_count == n * n:
return True
for dx, dy in moves:
next_x = x + dx
next_y = y + dy
if is_valid_move(next_x, next_y):
if backtrack(next_x, next_y, move_count + 1):
return True
visited[x][y] = False
path.pop()
return False
moves = [(-2, 1), (-1, 2), (1, 2), (2, 1),
(2, -1), (1, -2), (-1, -2), (-2, -1)]
visited = [[False] * n for _ in range(n)]
path = []
# 选择任意一个起始点
start_x, start_y = 0, 0
backtrack(start_x, start_y, 1)
return path
# 解决64次移动的骑士之旅问题
path = solve_knight_tour(8)
# 打印路径
for x, y in path:
print(f'({x}, {y})')
上述程序使用回溯法来找到一条满足条件的路径。我们选择了棋盘上的任意一个格子作为起始点,并使用递归函数backtrack
来进行回溯搜索。当移动次数达到64次时,我们找到了一条完整的路径,并将其打印出来。