7.6 迭代加深搜索 (IDA*算法实战)
大家还是直接通过问题去体会比较好。
埃及分数问题
对于一个分数a/b,将起转化为多个分子为1的分数之和,表示方式有很多种,其中加数少的比加数多的好,加数相同的情况下,则最小的分数越大越好。例如19/45=1/5+1/6+1/18是最佳方案。
分析:这道题如果用回溯法去做,解答树的深度和每一层的宽度都是无法确定的(因为每一层都是无限大的),所以显然不能用我们前面学的两种方法去做。
解决方案是采用迭代加深搜索(IDDFS):从小到大枚举深度上限maxd,每次执行只考虑深度不超过maxd的结点(DFS深度到maxd,无论找没找到结果都直接停止)。
此时我们可以构造一个类似于估价函数的方法(可能就是),在每次DFS中进行剪枝。比如当我们扩展到i层时,前i个分数之和为c/d,而第i个分数为1/e,于是确定出至少还需要(a/b-c/d)/(1/e)个分数,总和才能达到a/b,如果本次DFS此时后面的搜索次数已经小于(a/b-c/d)/(1/e),那么就可以直接跳出本次DFS。这里的关键在于:估计在某个状态至少还要多少步才能出解(启发函数)。
学术一点说,当前DFS的最大搜索深度为maxd,节点n的深度记为g(n),乐观估计函数为h(n),则当g(n)+h(n)>maxd时就要进行剪枝。这样的算法就是IDA*(如果设计出一个乐观估计函数,预测从当前结点至少还需要扩展几层结点才可能得到解,则迭代加深搜索就进化为了IDA*算法,对IDA*算法和A*算法想要做详细了解的可以去看我之前总结的这篇文章:从BFS到A*再到IDA*)。
代码实现如下(可以看做对IDA*实现的训练,非常建议大家仔细领会代码里面的一些细节,我在前后写了非常详细的注释希望能够帮助大家理解):
首先是主框架:
int ok=0,a,b; cin>>a>>b;//get_first得到的是不同分子分母下的枚举起点
//我们之前说maxd表示每次dfs的最大深度,如果我们在某个maxd的情况下找到了解
//这个解可能不止一个,我在这里的意思是找到了最优解
//那么这个最优解我们在放宽的搜索深度肯定还能找到
//所以我们在某个深度找到解了以后就可以break了,不用考虑后面会不会找到更优的解
//并且根据迭代加深搜索的特性,maxd不仅可以表示搜索深度,在这道题还可以表示解的个数
for (maxd=1;;maxd++){
memset(ans,-1,sizeof(ans)); if (dfs(0,get_first(a,b),a,b)){
ok=1; break;} }
if(ok){
for(int i=0;i<=maxd;i++){
if(i>0) cout<<"+"; printf("1/%lld",ans[i]);} }
return 0;
我个人觉得这里的关键在于对maxd的理解,我之前认为maxd仅仅是最大的搜索深度,后来我发现迭代加深搜索的特性,它将每组解(注意这里是组)分割在了其对应的长度(某个搜索深度)之中。换句话说,maxd同时可以表示解的长度,并且不用考虑后面出现更优解的情况。
get_first函数:
//求满足1/c<=a/b的最小c,即c*a>=b
//这个函数的功能用于找到我们每一次扩展结点,新单位分数分母上的最小值
//即是每一层的枚举起点
int get_first(ll a,ll b){
for(int i=1;;i