← 目录 / 算法文档 · 模块十四 搜索进阶 / 14.5 IDA* 算法

14.5 IDA* 算法

把 14.3 节"深度限制、反复加深"的思路,用在 14.4 节的 f 值上——用 DFS 的低内存,换来和 A* 一样的方向感。

本页目录
① 为什么需要 IDA*:A* 也有内存问题

14.4 节的 A* 有了"方向感",能少走很多冤枉路,但它用优先队列保存所有待探索的节点——这一点和 BFS 一样,队列(这里是优先队列)在最坏情况下要同时装下和搜索前沿一样多的节点,内存开销是 O(前沿大小)。当搜索空间巨大(比如经典的十五数码问题,状态数是天文数字)时,A* 一样会遇到内存爆炸的问题。

14.3 节已经展示过一次同样的解决思路:IDDFS 用"深度限制 + 反复加深",把 BFS 的 O(b^d) 内存压到了 DFS 的 O(深度)IDA*(Iterative Deepening A*)把这个思路原封不动地搬过来,只是把限制的对象从"深度"换成了 A* 里的 f(n) = g(n) + h(n)

② 核心思想:把"深度限制"换成"f 值限制"

IDA* 做的事情和 IDDFS 几乎一样:跑一次带限制的 DFS,如果没找到解,就放宽限制,重新跑一次。区别只有一处:

IDDFS(14.3 节)IDA*(本节)
限制的对象深度(整数,从 0 开始)f(n) = g(n) + h(n)(可能是任意数值)
剪枝条件depth == limit 就停止往下f(n) > limit 就停止往下
下一轮的新限制直接 limit + 1本轮所有被剪掉的节点里,最小的那个 f 值
💡
为什么新限制不能简单地"+1"?深度是整数,一层一层加没有问题;但 f 值可能是任意的数(取决于边权和启发式函数),如果本轮限制是 4,被剪掉的节点里最小的 f 值是 7,那么把限制改成 56 完全是浪费——中间根本不存在任何节点的 f 落在 (4,7) 之间,这两轮注定还是白跑。IDA* 每轮结束后,直接把新限制设成"本轮被剪掉的节点里最小的 f 值",跳过所有注定失败的中间尝试,这是 IDA* 和 IDDFS 最关键的不同之处。
③ 图解:两轮搜索如何一步步逼近答案

用一棵带边权、带启发值的小树演示:S 是起点,G 是目标;每个节点旁边标出 g(从 S 走到这里的实际代价)、h(启发式估计)、f = g+h

搜索树结构(边上数字是移动代价)
S
g=0 h=4 f=4
边权 6
A
g=6 h=2 f=8
边权 1
G(目标)
g=7 h=0 f=7
边权 1
B
g=1 h=3 f=4
边权 2
C
g=3 h=1 f=4
边权 1
D
g=2 h=5 f=7
C、D 都是死路(没有孩子,也不是目标),真正的目标 G 挂在 A 下面。
三轮 IDA* 搜索过程

第 1 轮,limit = h(S) = 4:S(f=4,展开)→ A(f=8>4,剪枝,记录 8)→ B(f=4,展开)→ C(f=4,展开,是死路)→ D(f=7>4,剪枝,记录 7)。本轮被剪掉的节点中最小的 f 是 min(8,7)=7,新限制设为 7(如果只是"+1",会先浪费两轮去尝试 5 和 6,那两个值根本不存在任何节点)。

第 2 轮,limit = 7:S(f=4,展开)→ A(f=8>7,仍然剪枝,记录 8)→ B(f=4,展开)→ C(死路)→ D(f=7≤7,展开,但 D 也是死路)。本轮被剪掉的节点只有 A,新限制设为 8

第 3 轮,limit = 8:S(f=4,展开)→ A(f=8≤8,展开)→ G(f=7≤8,检查是目标,找到!)。答案就是 g(G) = 7

④ 模板代码
C++ · IDA* 模板
1const int FOUND = -1, INF = 0x3f3f3f3f;
2int ansG; // 找到目标时,记录下它的 g 值(最终答案)
3
4// 返回 FOUND 表示找到目标;否则返回"这棵子树里,超过 limit 的最小 f 值"
5int DFS(int u, int g, int limit)
6{
7 int f = g + H(u);
8 if (f > limit) return f; // ★ 超过限制,剪枝;把这个 f 值带回去,供下一轮参考
9 if (IsGoal(u)) { ansG = g; return FOUND; }
10
11 int minExceed = INF;
12 for (auto [v, w] : adj[u]) // v:邻居节点,w:这条边的代价
13 {
14 int t = DFS(v, g + w, limit);
15 if (t == FOUND) return FOUND; // 子树里找到了,一路返回 FOUND
16 minExceed = min(minExceed, t); // 记录这棵子树里"最小的超限 f 值"
17 }
18 return minExceed;
19}
20
21int IDAStar(int start)
22{
23 int limit = H(start); // 第一轮的限制,就是起点的启发值(g=0)
24 while (true)
25 {
26 int t = DFS(start, 0, limit);
27 if (t == FOUND) return ansG;
28 if (t == INF) return -1; // 没有任何节点被剪枝,说明搜索空间已经穷尽,无解
29 limit = t; // ★ 直接跳到"本轮被剪掉的最小 f 值",而不是 +1
30 }
31}
💡
对照 14.3 节的 IDDFS:整体结构几乎一模一样——都是"带限制的 DFS + 外层不断放宽限制的循环"。差别集中在两行:第 8 行剪枝条件从"深度到没到上限"换成"f 值超没超过上限";第 29 行下一轮的新限制不再是简单的 +1,而是取"这一轮里所有被剪枝的节点中最小的 f 值"(第 16 行 minExceed 就是为了收集这个信息)。
⑤ IDA* vs A*:用时间换内存

IDA* 和 A* 找到的答案完全一样(只要 h 不高估),区别只在于用什么资源去换取效率:

对比项A*(14.4 节)IDA*(本节)
需要的额外数据结构优先队列,保存整个搜索前沿不需要,只是普通的 DFS 递归栈
内存占用O(前沿大小),可能很大O(深度),非常小
时间开销每个节点只访问一次浅层节点会在多轮之间被重复访问(和 IDDFS 同理,通常代价不大)
适合场景内存足够、搜索空间不算特别巨大搜索空间极大(如十五数码),内存是主要瓶颈
⑥ 适用条件与常见陷阱
下一轮限制忘记取"最小超限 f 值",直接简单地 +1:这是本节和 IDDFS 唯一的关键区别,也是最容易被忽略的一步——如果偷懒直接 limit++,虽然结果依然正确,但会导致大量"中间限制"轮次完全找不到任何新节点、白白浪费整轮的搜索时间,尤其当边权跨度较大时问题更明显。
h(n) 一旦高估,IDA* 和 A* 会犯同样的错误:14.4 节讲过的"启发式函数不能高估"这条铁律在 IDA* 里同样适用——本节的剪枝条件、下一轮限制的计算,都是建立在"f 是代价下界"这个前提上的,如果 h 高估,可能把通向真正最优解的分支提前剪掉,导致算出的答案不是最短路。
图中存在环时忘记判重:和 14.3 节 IDDFS 的陷阱一样,如果搜索空间是一般的图而不是树,需要在 DFS 路径上记录"当前路径已经访问过的节点",避免绕环导致无限递归——这一点在 DFS 函数里容易被忽略,尤其是从简单的树形例子(如本节演示)直接搬到实际的图上时。
🏆
模块小结:14.1~14.5 节走完了搜索进阶的整条线:BFS(保证最短路,但内存大)→ 双向 BFS(起点终点都已知时对半砍指数)→ IDDFS(用 DFS 的低内存换 BFS 的最浅解保证)→ A*(给搜索加上方向感)→ IDA*(把 IDDFS 的"限制、加深"用在 A* 的 f 值上,同时拥有低内存和方向感)。这五种搜索方法层层递进,核心思路始终围绕同一个问题展开:在"内存开销"、"时间开销"和"有没有额外信息可以利用(方向感)"这三者之间,根据具体场景做取舍