第6章:启发的艺术(近似与估测) 兔狲教授的亲切开场 在探索了贪心算法的智慧与局限之后,我们面对一个新的问题:当完美解不可得时,我们如何做出聪明的选择? 有时候,我们无法保证找到最优解,甚至无法保证在合理时间内找到解。今天,我们探索启发式的智慧——用聪明的猜测导航未知。 核心议题:在不确定中,我们如何找到方向? 康乐园的傍晚,珠江的水面上映照着夕阳的金辉,几只白鹭掠过水面,留下淡淡的涟漪。黑石屋书房里,兔狲教授正在整理书架,小小猪和小海豹围在一张迷宫图前。 “这个迷宫好复杂!”小小猪指着图上弯弯曲曲的路径,“如果我不知道出口在哪里,该怎么走?” 小海豹思考着,“理论上,我们可以用深度优先搜索探索所有路径,但这样可能要尝试成千上万条死路。
:::info
兔狲教授的亲切开场
在探索了贪心算法的智慧与局限之后,我们面对一个新的问题:当完美解不可得时,我们如何做出聪明的选择? 有时候,我们无法保证找到最优解,甚至无法保证在合理时间内找到解。今天,我们探索启发式的智慧——用聪明的猜测导航未知。
:::
康乐园的傍晚,珠江的水面上映照着夕阳的金辉,几只白鹭掠过水面,留下淡淡的涟漪。黑石屋书房里,兔狲教授正在整理书架,小小猪和小海豹围在一张迷宫图前。
“这个迷宫好复杂!”小小猪指着图上弯弯曲曲的路径,“如果我不知道出口在哪里,该怎么走?”
小海豹思考着,“理论上,我们可以用深度优先搜索探索所有路径,但这样可能要尝试成千上万条死路。”
兔狲教授走过来,微笑道:“你们提出了一个经典问题:在未知环境中,如何高效地寻找目标? 当完美信息不可得时,我们需要依赖启发式——一种基于经验的‘聪明猜测’。”
小小猪好奇地问:“启发式?就像走迷宫时‘尽量往中心走’那样的直觉吗?”
“正是,”兔狲教授说,“启发式不是保证正确的规则,而是提高找到好解概率的经验法则。今天我们要探索的核心议题是:在不确定中,我们如何找到方向? 更具体地说:如何用有限的资源和信息,做出足够好的决策?”
他走到白板前,写下两个词:
精确搜索 —— 启发式搜索
“精确搜索保证找到最优解,但代价可能是天文数字的时间复杂度,”兔狲教授解释,“启发式搜索不保证最优,但能在可接受时间内找到‘足够好’的解。这种权衡是现代算法设计的核心。”
“让我们看一个经典的启发式算法:A(A-star)搜索*,”兔狲教授在白板上画出网格地图,“假设我们要从起点S到终点G,中间有障碍物。”
小海豹说:“A* 算法结合了实际代价和估计代价,对吧?”
“没错,”兔狲教授写出公式:
f(n) = g(n) + h(n)
“A* 总是选择f(n)最小的节点扩展,”兔狲教授解释,“这就像在迷宫中,你既考虑已经走了多远,又估计离出口还有多远,选择‘看起来最近’的方向。”
小小猪问:“h(n)怎么估计?随便猜吗?”
“这是启发式设计的核心,”兔狲教授说,“好的启发函数需要满足两个性质:
“最常用的启发函数是曼哈顿距离或欧几里得距离,”他举例,“在网格地图中,如果我们只能上下左右移动,曼哈顿距离(|x₁−x₂| + |y₁−y₂|)是可采纳的。”
小海豹思考:“但如果可以斜对角移动呢?”
“那么曼哈顿距离会高估,需要调整,”兔狲教授说,“启发函数的设计需要匹配问题的结构。好的启发式能极大提升搜索效率,差的启发式可能比盲目搜索还慢。”
:::info
兔狲教授的评语
启发式搜索的哲学是满意原则的延伸:我们不追求绝对的最优,而是在可接受时间内找到足够好的解。这反映了现实世界决策的常态——信息有限,时间有限,我们需要在不确定性中做出最佳猜测。
:::
“让我们对比两种启发式策略,”兔狲教授画出对比表:
| 策略 | 选择标准 | 优点 | 缺点 |
|---|---|---|---|
| 贪婪最佳优先 | 最小h(n)(只考虑估计代价) | 快速趋向目标 | 可能陷入局部最优,不保证最优解 |
| A 搜索* | 最小f(n)=g(n)+h(n) | 保证最优解(如果h可采纳) | 可能需要更多内存和时间 |
小小猪恍然大悟:“所以贪婪最佳优先是‘只看未来’,A* 是‘既看过去又看未来’!”
“精辟的总结,”兔狲教授赞许道,“贪婪算法可以看作启发式搜索的特例——它只考虑局部信息,而A* 结合了全局信息。”
小海豹问:“教授,如果问题本身是NP难的,比如旅行商问题,我们该怎么办?”
“这时我们需要近似算法,”兔狲教授说,“它们不保证找到最优解,但保证解的质量在某个比例内。”
他举例:“对于旅行商问题(TSP),有几种近似策略:
小小猪计算着:“所以如果我们接受解可能比最优解差50%,就能在多项式时间内解决?”
“正是,”兔狲教授说,“近似算法的核心是权衡解的质量与计算时间。在某些应用中,95%的最优解已经足够好,而计算时间可以从指数级降到多项式级。”
:::detail
进阶科普:近似算法的性能保证
近似算法通常用近似比来衡量性能:
例如:
设计近似算法需要巧妙的构造和证明,是理论计算机科学的重要领域。
:::
兔狲教授打开投影仪,一幅规整的计算图出现在屏幕上。
“这是A* 算法的正交计算图,”兔狲教授指着图说,“起点经过实际代价累积和启发式估计,选择最小f(n)的节点扩展,循环直至找到目标。”
小小猪盯着图,“这个图有两个分支!g(n)和h(n)并行计算,然后合并成f(n)。”
“正是,”兔狲教授说,“A* 算法的核心是双路径评估:一条记录已经付出的代价,一条预测未来的代价。这种‘回顾与前瞻’的结合,使它比单纯的前瞻(贪婪)或回顾(Dijkstra)更高效。”
小海豹仔细观察图的层级结构,“教授,这个图的设计……实际代价和启发代价在同一层级计算,然后合并选择。这反映了A* 算法的对称美。”
“这就是启发式搜索的优雅之处,”兔狲教授解释,“它平衡了经验(已付出的代价)与直觉(对未来代价的估计)。这种平衡不仅在算法中重要,在生活中也同样重要——最好的决策往往既考虑过去投入,又考虑未来收益。”
兔狲教授回到座位,倒了三杯茶。“让我们总结今天最重要的思想模型:满意解与最优解的永恒张力。”
他在白板上画出对比表:
| 维度 | 最优解思维 | 满意解思维 |
|---|---|---|
| 目标 | 追求绝对最好 | 追求足够好 |
| 时间观 | 不惜时间代价 | 重视时间效率 |
| 信息需求 | 需要完整信息 | 容忍信息不全 |
| 适用场景 | 小规模、关键决策 | 大规模、日常决策 |
| 哲学根源 | 完美主义 | 实用主义 |
“这两种思维不是对立的,”兔狲教授解释,“而是决策光谱的两端。聪明的决策者知道何时追求完美,何时接受满意。”
小小猪问:“教授,什么时候应该用启发式方法?”
兔狲教授在白板上列出条件:
适合启发式方法的问题通常具有: 1. **问题规模大**:精确算法计算时间不可接受 2. **信息不完全**:无法获得完整的问题信息 3. **实时性要求**:需要在有限时间内给出答案 4. **容错性**:可以接受近似解或概率性正确 5. **人类直觉可用**:存在经验法则可以借鉴 常见启发式应用场景: ├── 路径规划(A*、D* 算法) ├── 游戏AI(Minimax with alpha-beta pruning) ├── 调度问题(遗传算法、模拟退火) ├── 机器学习(梯度下降、随机梯度下降) └── 日常决策(经验法则、拇指规则)
“理解这些条件,”兔狲教授说,“帮助我们判断何时可以‘启发式’,何时必须‘精确式’。”
:::info
兔狲教授的评语
启发式思维的哲学是有限理性(Bounded Rationality)——在资源有限、信息有限、时间有限的情况下,做出尽可能好的决策。这不仅是算法设计的原则,也是人类智慧的体现。我们永远无法拥有上帝视角,但我们可以在迷雾中点亮一盏灯。
:::
:::info
兔狲教授的总结:启发式思维的智慧
"让我们用代码来探索启发式搜索的魅力,"兔狲教授说,"从A*算法到不同启发函数,从迷宫寻路到拼图问题。"
import heapq import math class Node: """A*算法节点类""" def __init__(self, position, parent=None): self.position = position # 节点位置(如(x,y)坐标) self.parent = parent # 父节点 # 代价估计 self.g = 0 # 从起点到当前节点的实际代价 self.h = 0 # 从当前节点到目标的启发式估计 self.f = 0 # 总估计代价 f = g + h def __lt__(self, other): # 用于堆排序,比较f值 return self.f < other.f def __eq__(self, other): # 比较节点位置 return self.position == other.position def __hash__(self): # 使节点可哈希,用于集合 return hash(self.position) def a_star_search(start, goal, grid, heuristic): """A*搜索算法 问题描述: 在网格地图上找到从起点到终点的最短路径。 网格中0表示可通行区域,1表示障碍物。 算法思路(启发式搜索): A*算法结合了Dijkstra算法(保证最优)和贪心最佳优先搜索(快速)。 使用评估函数 f(n) = g(n) + h(n): - g(n): 从起点到节点n的实际代价 - h(n): 从节点n到终点的估计代价(启发函数) 步骤: 1. 初始化开放列表(优先队列)和关闭集合 2. 将起点加入开放列表,g=0,f=h(start) 3. 当开放列表非空时循环: a. 从开放列表取出f值最小的节点作为当前节点 b. 如果当前节点是目标,回溯得到路径 c. 将当前节点加入关闭集合 d. 生成当前节点的所有邻居节点 e. 对于每个邻居: - 如果是障碍或已在关闭集合,跳过 - 计算新的g值 = 当前g值 + 移动代价(通常为1) - 如果邻居不在开放列表或新g值更小: * 更新邻居的g、h、f值 * 设置邻居的父节点为当前节点 * 如果不在开放列表,加入开放列表 关键特点: - 完备性:如果路径存在,一定能找到 - 最优性:如果启发函数是可采纳的(h(n) ≤ 实际代价),能找到最短路径 - 效率:比Dijkstra快,比贪心最佳优先搜索更可能找到最优解 - 启发函数质量影响搜索效率 启发函数要求: 1. 可采纳性:h(n) ≤ 从n到goal的实际代价(保证最优性) 2. 一致性(单调性):h(n) ≤ c(n, n') + h(n')(保证效率) 常用启发函数: - 曼哈顿距离:|x1-x2| + |y1-y2|(网格中只能上下左右移动) - 欧几里得距离:√((x1-x2)² + (y1-y2)²)(可斜向移动) - 切比雪夫距离:max(|x1-x2|, |y1-y2|)(可八方向移动) 参数: start: 起点坐标 (x, y) goal: 终点坐标 (x, y) grid: 二维网格地图,0表示可通行,1表示障碍 heuristic: 启发函数,接收两个坐标返回估计距离 返回: 路径(坐标列表,从起点到终点),如果找不到则返回None 时间复杂度: O(b^d),其中b是分支因子,d是解深度 实际中远小于穷举搜索,取决于启发函数质量 空间复杂度: O(b^d),需要存储所有探索的节点 示例: >>> grid = [ ... [0, 0, 0, 0, 0], ... [0, 1, 1, 1, 0], ... [0, 0, 0, 0, 0], ... [0, 1, 1, 1, 0], ... [0, 0, 0, 0, 0] ... ] >>> def manhattan(p1, p2): ... return abs(p1[0]-p2[0]) + abs(p1[1]-p2[1]) >>> a_star_search((0, 0), (4, 4), grid, manhattan) [(0, 0), (1, 0), (2, 0), (3, 0), (4, 0), (4, 1), (4, 2), (4, 3), (4, 4)] 应用场景: - 游戏AI路径规划 - 机器人导航 - 地图应用路线规划 - 拼图游戏求解 变体: - 双向A*:从起点和终点同时搜索 - 迭代加深A*:结合迭代加深的A* - 权重A*:f(n) = g(n) + w*h(n),w>1加速但可能非最优 - D*:动态环境中的A* """ # 创建起点和终点节点 start_node = Node(start) goal_node = Node(goal) # 初始化开放列表和关闭列表 open_list = [] closed_set = set() # 将起点加入开放列表 heapq.heappush(open_list, start_node) # 搜索循环 while open_list: # 取出f值最小的节点 current_node = heapq.heappop(open_list) # 如果到达目标,回溯路径 if current_node.position == goal_node.position: path = [] current = current_node while current is not None: path.append(current.position) current = current.parent return path[::-1] # 反转得到从起点到终点的路径 # 将当前节点加入关闭列表 closed_set.add(current_node.position) # 生成邻居节点(上下左右四个方向) neighbors = [] for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]: new_x = current_node.position[0] + dx new_y = current_node.position[1] + dy # 检查边界 if 0 <= new_x < len(grid) and 0 <= new_y < len(grid[0]): # 检查是否为障碍物 if grid[new_x][new_y] == 0: neighbors.append((new_x, new_y)) # 处理邻居节点 for neighbor_pos in neighbors: # 如果邻居在关闭列表中,跳过 if neighbor_pos in closed_set: continue # 创建邻居节点 neighbor = Node(neighbor_pos, current_node) # 计算g值:从起点到当前节点的g值 + 移动代价(假设为1) neighbor.g = current_node.g + 1 # 计算h值:启发函数估计 neighbor.h = heuristic(neighbor_pos, goal) # 计算f值 neighbor.f = neighbor.g + neighbor.h # 检查开放列表中是否已有该节点且g值更小 found_in_open = False for open_node in open_list: if neighbor.position == open_node.position and neighbor.g >= open_node.g: found_in_open = True break # 如果不在开放列表中或g值更小,加入开放列表 if not found_in_open: heapq.heappush(open_list, neighbor) # 开放列表为空,未找到路径 return None
# 不同启发函数实现 def manhattan_distance(pos1, pos2): """曼哈顿距离(适用于只能上下左右移动的网格) 公式: h(n) = |x1 - x2| + |y1 - y2| 特点: - 适用于网格环境中只能上下左右移动的情况 - 是可采纳的(实际最短路径 ≥ 曼哈顿距离) - 是一致的(满足三角不等式) - 计算简单快速 参数: pos1: 起点坐标 (x1, y1) pos2: 终点坐标 (x2, y2) 返回: 曼哈顿距离 示例: >>> manhattan_distance((0, 0), (3, 4)) 7 # |0-3| + |0-4| = 3+4=7 """ x1, y1 = pos1 x2, y2 = pos2 return abs(x1 - x2) + abs(y1 - y2) def euclidean_distance(pos1, pos2): """欧几里得距离(适用于可以斜对角移动的情况) 公式: h(n) = √((x1 - x2)² + (y1 - y2)²) 特点: - 适用于可以任意方向移动的连续空间 - 是可采纳的(直线距离 ≤ 实际路径长度) - 是一致的 - 计算涉及平方根,稍慢 参数: pos1: 起点坐标 (x1, y1) pos2: 终点坐标 (x2, y2) 返回: 欧几里得距离 示例: >>> euclidean_distance((0, 0), (3, 4)) 5.0 # √(3²+4²)=√(9+16)=√25=5 """ x1, y1 = pos1 x2, y2 = pos2 return math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2) def chebyshev_distance(pos1, pos2): """切比雪夫距离(适用于可以八个方向移动的情况) 公式: h(n) = max(|x1 - x2|, |y1 - y2|) 特点: - 适用于可以八个方向移动的网格(上下左右+对角线) - 是可采纳的(对角线移动代价为1时) - 是一致的 - 计算简单 参数: pos1: 起点坐标 (x1, y1) pos2: 终点坐标 (x2, y2) 返回: 切比雪夫距离 示例: >>> chebyshev_distance((0, 0), (3, 4)) 4 # max(|0-3|, |0-4|)=max(3,4)=4 """ x1, y1 = pos1 x2, y2 = pos2 return max(abs(x1 - x2), abs(y1 - y2)) def zero_heuristic(pos1, pos2): """零启发函数(退化为Dijkstra算法) 公式: h(n) = 0 特点: - 总是可采纳的(0 ≤ 实际距离) - 退化为Dijkstra算法,保证找到最短路径 - 没有启发信息,搜索效率低 - 适用于没有好的启发函数的情况 参数: pos1: 起点坐标 (x1, y1)(未使用) pos2: 终点坐标 (x2, y2)(未使用) 返回: 0 注意: - 当h(n)=0时,A*退化为Dijkstra算法 - 搜索会探索所有可能路径,保证最优但效率低 - 实际中很少使用,除非没有更好的启发函数 """ return 0
def maze_experiment(): """迷宫问题实验:比较不同启发函数的效果""" print("迷宫问题实验:A*算法不同启发函数比较") print("=" * 50) # 定义迷宫地图(0=可通行,1=障碍) # 10x10网格 maze = [ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 1, 1, 1, 0, 1, 1, 1, 0], [0, 0, 0, 0, 1, 0, 0, 0, 1, 0], [0, 1, 1, 0, 1, 1, 1, 0, 1, 0], [0, 1, 0, 0, 0, 0, 0, 0, 1, 0], [0, 1, 0, 1, 1, 1, 1, 1, 1, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 1, 1, 1, 1, 1, 1, 1, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] # 起点和终点 start = (0, 0) goal = (9, 9) print(f"迷宫大小: {len(maze)}x{len(maze[0])}") print(f"起点: {start}, 终点: {goal}") # 测试不同启发函数 heuristics = [ ("曼哈顿距离", manhattan_distance), ("欧几里得距离", euclidean_distance), ("切比雪夫距离", chebyshev_distance), ("零启发函数(Dijkstra)", zero_heuristic), ] results = [] for name, heuristic_func in heuristics: print(f"\n使用{name}:") # 记录搜索过程统计 class CountingAStar: def __init__(self, heuristic): self.heuristic = heuristic self.nodes_expanded = 0 def count_heuristic(self, pos1, pos2): self.nodes_expanded += 1 return self.heuristic(pos1, pos2) counter = CountingAStar(heuristic_func) # 运行A*算法 import time start_time = time.time() path = a_star_search(start, goal, maze, counter.count_heuristic) elapsed = time.time() - start_time if path: print(f" 找到路径,长度: {len(path)-1}步") print(f" 扩展节点数: {counter.nodes_expanded}") print(f" 运行时间: {elapsed:.6f}秒") results.append((name, len(path)-1, counter.nodes_expanded, elapsed)) else: print(f" 未找到路径") results.append((name, None, counter.nodes_expanded, elapsed)) # 结果分析 print("\n" + "=" * 50) print("结果分析") print("=" * 50) print("\n性能比较:") print("启发函数 路径长度 扩展节点数 时间(秒)") print("-" * 50) for name, path_len, nodes, time_taken in results: if path_len is not None: print(f"{name:20} {path_len:8} {nodes:12} {time_taken:10.6f}") else: print(f"{name:20} {'无路径':8} {nodes:12} {time_taken:10.6f}") # 启发函数性质分析 print("\n启发函数性质分析:") print("1. 可采纳性 (Admissible): 启发函数不高估实际代价") print(" - 曼哈顿距离: ✓ (在网格中实际是最短路径的下界)") print(" - 欧几里得距离: ✓ (直线距离 <= 实际路径)") print(" - 切比雪夫距离: ✓ (在允许八方向移动时是可采纳的)") print(" - 零启发函数: ✓ (永远不高估)") print("\n2. 一致性 (Consistent): h(n) <= c(n, n') + h(n')") print(" - 曼哈顿距离: ✓ (满足三角不等式)") print(" - 欧几里得距离: ✓") print(" - 切比雪夫距离: ✓") print(" - 零启发函数: ✓") print("\n3. 启发能力 (Informedness): 估计越接近实际,搜索越快") print(" - 曼哈顿距离: 高 (在网格中很准确)") print(" - 欧几里得距离: 中") print(" - 切比雪夫距离: 中") print(" - 零启发函数: 低 (无启发信息,退化为Dijkstra)") # 运行迷宫实验 maze_experiment()
def sliding_puzzle_heuristic(puzzle, goal): """八数码问题启发函数 常用启发函数: 1. 错位棋子数 (Misplaced Tiles) 2. 曼哈顿距离总和 """ def misplaced_tiles(puzzle, goal): """错位棋子数启发函数""" count = 0 for i in range(3): for j in range(3): if puzzle[i][j] != 0 and puzzle[i][j] != goal[i][j]: count += 1 return count def manhattan_sum(puzzle, goal): """曼哈顿距离总和启发函数""" # 创建目标位置映射 goal_positions = {} for i in range(3): for j in range(3): if goal[i][j] != 0: goal_positions[goal[i][j]] = (i, j) distance = 0 for i in range(3): for j in range(3): value = puzzle[i][j] if value != 0: goal_i, goal_j = goal_positions[value] distance += abs(i - goal_i) + abs(j - goal_j) return distance # 返回两种启发函数的估计值 return misplaced_tiles(puzzle, goal), manhattan_sum(puzzle, goal) def sliding_puzzle_example(): """八数码问题示例""" print("\n" + "=" * 50) print("八数码问题(滑动拼图)启发函数设计") print("=" * 50) # 初始状态和目标状态 initial = [ [1, 2, 3], [4, 0, 6], [7, 5, 8] ] goal = [ [1, 2, 3], [4, 5, 6], [7, 8, 0] ] print("初始状态:") for row in initial: print(" " + " ".join(str(x) if x != 0 else " " for x in row)) print("\n目标状态:") for row in goal: print(" " + " ".join(str(x) if x != 0 else " " for x in row)) # 计算启发函数值 misplaced, manhattan = sliding_puzzle_heuristic(initial, goal) print(f"\n启发函数估计:") print(f" 错位棋子数: {misplaced}") print(f" 曼哈顿距离总和: {manhattan}") # 启发函数性质分析 print("\n启发函数比较:") print("1. 可采纳性:") print(" - 错位棋子数: ✓ (每次移动最多减少1个错位)") print(" - 曼哈顿距离: ✓ (每次移动最多减少1曼哈顿距离)") print("\n2. 启发能力 (哪个更准确?):") print(" - 错位棋子数: 低估实际步数") print(" - 曼哈顿距离: 更接近实际步数") print(" → 曼哈顿距离通常更高效") print("\n3. 一致性检查:") print(" - 曼哈顿距离满足一致性") print(" - 错位棋子数不严格满足一致性,但可采纳") # 运行八数码示例 sliding_puzzle_example()
"通过这些代码,"兔狲教授总结道,"我们不仅理解了启发式搜索的实现,更理解了在不确定中寻找方向的智慧。启发式算法教给我们:在信息不完全的世界里,最好的策略往往不是等待完美信息,而是基于现有信息做出最佳猜测。"
算法实现:实现A* 算法解决迷宫问题。测试不同启发函数(曼哈顿距离、欧几里得距离、切比雪夫距离)的效果。哪个启发函数扩展的节点最少?为什么?
启发式设计:设计一个启发函数用于八数码问题(滑动拼图)。思考:如何保证启发函数的可采纳性?能否设计一个既高效又可采纳的启发函数?
现实观察:观察生活中的启发式决策。超市购物时选择最短队伍是启发式吗?什么时候会失败?(提示:考虑队伍中顾客购物车内容、收银员速度差异)
思想溯源:“启发式”(Heuristic)这个词源自希腊语“εὑρίσκω”(发现),在计算机科学中何时被正式引入?查阅早期AI文献(如Newell & Simon 1958, Hart et al. 1968)。
跨学科联系:启发式思维与心理学中的“启发式与偏见”研究有什么联系?卡尼曼和特沃斯基的前景理论与算法中的启发式有什么相似和不同?
哲学反思:启发式算法体现了哲学中的“实用主义”传统吗?比较威廉·詹姆斯、约翰·杜威的实用主义思想与启发式算法设计原则。
伦理考量:自动驾驶汽车在紧急情况下的决策可以用启发式算法吗?(例如:总是选择最小化立即伤害的转向)这种基于经验的决策可能有什么伦理问题?
算法比较:启发式搜索与贪心算法、动态规划有什么根本区别?为什么有些问题启发式有效而动态规划不可行?有些则相反?
创造练习:设计一个“启发式寓言”——用故事解释启发式思维的优点和局限。(提示:可以比喻为航海者在雾中用罗盘和星图导航)
极限挑战:证明如果启发函数h(n)满足一致性,那么A* 算法不会重复扩展节点。理解一致性与可采纳性的关系。
夜色渐深,珠江两岸的灯光倒映在水中,宛如星河落地。康乐园的榕树在晚风中轻轻摇曳,仿佛在思考着刚才的讨论。
小小猪正在电脑上模拟不同启发函数的效果,屏幕上显示着迷宫地图和各种搜索路径。小海豹则在白板上推导启发式可采纳性的证明,试图理解那看似直觉的估计背后的数学严谨。
“今天我们从‘在不确定中如何找到方向’开始,”兔狲教授收拾着茶具,“探索了启发式搜索的智慧,理解了满意解与最优解的永恒张力。”
小海豹放下粉笔,“教授,如果启发式算法仍然太慢,或者我们需要绝对的最优解,还有什么方法呢?”
兔狲教授微笑:“好问题。这就需要更强大的工具了——动态规划。”
小小猪抬头,“动态规划?就是下一章的内容吗?”
“正是,”兔狲教授解释,“动态规划是有记忆的推理,它存储中间结果,避免重复计算,能够解决许多启发式算法无法保证最优的问题。但它的代价是更高的空间复杂度。”
“下一章,”兔狲教授走向书架,“我们要探索爬楼梯问题、斐波那契数列、背包问题……看看动态规划如何用空间换时间,解决复杂的优化问题。”
窗外,珠江的游船缓缓驶过,船上的灯光在黑石屋的窗户上投下流动的光影。书房里的三个人,就像三个探索者,在算法的海洋中寻找着智慧的灯塔。
小小猪的笔记:我测试了不同启发函数对A* 算法的影响。曼哈顿距离在网格地图中扩展节点最少,但欧几里得距离在允许斜对角移动时更优。启发式就像给算法一双“眼睛”,让它能看到目标的大致方向!
小海豹的笔记:查了启发式搜索的历史,发现A* 算法由Hart、Nilsson和Raphael在1968年提出。有趣的是,启发式思想在人工智能早期就出现了,但形式化证明花了很长时间。有时,直觉走在形式化之前。
兔狲教授的结语:记住,启发式算法教给我们决策的重要一课:在信息不完全的世界里,最好的策略往往不是等待完美信息,而是基于现有信息做出最佳猜测。我们慢慢来,理解了最重要。