7.6 迭代加深搜索 (IDA*算法实战)

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++) if(b<=a*i) return i;} 

get_first函数其实就是用于确定我们扩展结点时,枚举下一层结点的枚举起点(这个问题是比较特殊的,所以需要单独的这个函数)。

IDA*函数:

bool dfs(int d,int from,ll aa,ll bb){
     //d表示当前搜索的深度
	//aa和bb分别表示在本次dfs的过程中原来的有理数(分数),在不断扩展的情况下(有理数每一层减去一个单位分数)
	//剩余值的分数形式,aa表示当前分子,bb表示当前分母 
	if (d==maxd){
     //maxd表示当前dfs的最大深度,也就是说当d的值扩展到maxd时,则必须要剩余值是否能表示为一个单位分数 
		if (bb%aa) return false; v[d]=bb/aa;//bb%aa不等于0表示无法表示为一个单位分数,否则得到一种新的解的情况 
		if (better(d)) memcpy(ans,v,sizeof(ll)*(d+1));//使用better函数判断这个解是否优于最优解
		return true; 
	}//ok没什么好说的,from表示这层单位分数分母的枚举起点 
	bool ok=false; from=max(from,get_first(aa,bb));
    for(int i=from;;i++){
     //但是根据之前的分析,我们知道如果不加限定,分母的枚举深度是无限的 
        //于是这里我们按照前面分析里提到的的启发函数进行了剪枝,下面就是进行的如下的操作 
		//前i个分数之和为c/d,第i个分数为1/e
		//于是确定出至少还需要(a/b-c/d)/(1/e)个分数,总和才能达到a/b
		//如果本次DFS此时后面的搜索次数已经小于(a/b-c/d)/(1/e),那么就可以直接跳出本次DFS
		//实战中不用将启发函数给单独的写出来 
		if(bb*(maxd+1-d)<=i*aa) break; v[d]=i;
        //接下来就是计算aa/bb-1/i的值,记作a2/b2 
        ll b2=bb*i,a2=aa*i-bb,g=gcd(a2,b2); if(dfs(d+1,i+1,a2/g,b2/g)) ok=true;
    }//如果没有找到符合条件更深层的结点,ok的值就不会发生改变 
    return ok; 
}

这里其实大致和dfs函数没有任何的区别(判断是否已经到了最大的搜索限度和扩展结点),关键在于下面这个部分:

//于是这里我们按照前面分析里提到的的启发函数进行了剪枝,下面就是进行的如下的操作 
		//前i个分数之和为c/d,第i个分数为1/e
		//于是确定出至少还需要(a/b-c/d)/(1/e)个分数,总和才能达到a/b
		//如果本次DFS此时后面的搜索次数已经小于(a/b-c/d)/(1/e),那么就可以直接跳出本次DFS
		//实战中不用将启发函数给单独的写出来 
		if(bb*(maxd+1-d)<=i*aa) break; 

这里就是我在之前文章里提到的的A*与IDA*和BFS与DFS的区别:估价函数(其实这里更应该被称为启发函数)。需要注意的是在实战中可以不严格地在代码里写出估价函数的部分。IDA*估价函数的作用就是用来进行剪枝的而已。

更新解的状态就不看了,但是大家一定要看这个gcd函数的写法:

//这里的gcd我是cv的别人的代码,我也是才知道辗转相除可以通过函数递归有这么优美的写法 
ll gcd(ll a,ll b){
     if(b==0) return a; else return gcd(b,a%b);}

我之前从来没有想过可以通过函数递归将辗转相除法写得这么优美(活到老,学到老)。

完整代码如下:

#include
#include
using namespace std;
typedef long long ll;
const int maxn=1000;
ll ans[maxn],v[maxn];//v数组中存放着当前最新的方案,ans数组中存放当前最优的方案,ans初始化为-1 
int maxd;//maxd表示限定的最大搜索深度
//gcd表示求最大公约数的函数
//这里的gcd我是cv的别人的代码,我也是才知道辗转相除可以通过函数递归有这么优美的写法 
ll gcd(ll a,ll b){
     if(b==0) return a; else return gcd(b,a%b);}
//求满足1/c<=a/b的最小c,即c*a>=b
//这个函数的功能用于找到我们每一次扩展结点,新单位分数分母上的最小值
//即是每一层的枚举起点 
int get_first(ll a,ll b){
     for(int i=1;;i++) if(b<=a*i) return i;} 
bool better(int d){
     //d表示当前搜索的深度,同时代表当前可行方案中分数的个数
	//better函数表示对最优方案的更新 
	for (int i=d;i>=0;i--) if (v[i]!=ans[i]) return ans[i]==-1||v[i]<ans[i];
	return false; 
}
bool dfs(int d,int from,ll aa,ll bb){
     //d表示当前搜索的深度
	//aa和bb分别表示在本次dfs的过程中原来的有理数(分数),在不断扩展的情况下(有理数每一层减去一个单位分数)
	//剩余值的分数形式,aa表示当前分子,bb表示当前分母 
	if (d==maxd){
     //maxd表示当前dfs的最大深度,也就是说当d的值扩展到maxd时,则必须要剩余值是否能表示为一个单位分数 
		if (bb%aa) return false; v[d]=bb/aa;//bb%aa不等于0表示无法表示为一个单位分数,否则得到一种新的解的情况 
		if (better(d)) memcpy(ans,v,sizeof(ll)*(d+1));//使用better函数判断这个解是否优于最优解
		return true; 
	}//ok没什么好说的,from表示这层单位分数分母的枚举起点 
	bool ok=false; from=max(from,get_first(aa,bb));
    for(int i=from;;i++){
     //但是根据之前的分析,我们知道如果不加限定,分母的枚举深度是无限的 
        //于是这里我们按照前面分析里提到的的启发函数进行了剪枝,下面就是进行的如下的操作 
		//前i个分数之和为c/d,第i个分数为1/e
		//于是确定出至少还需要(a/b-c/d)/(1/e)个分数,总和才能达到a/b
		//如果本次DFS此时后面的搜索次数已经小于(a/b-c/d)/(1/e),那么就可以直接跳出本次DFS
		//实战中不用将启发函数给单独的写出来 
		if(bb*(maxd+1-d)<=i*aa) break; v[d]=i;
        //接下来就是计算aa/bb-1/i的值,记作a2/b2 
        ll b2=bb*i,a2=aa*i-bb,g=gcd(a2,b2); if(dfs(d+1,i+1,a2/g,b2/g)) ok=true;
    }//如果没有找到符合条件更深层的结点,ok的值就不会发生改变 
    return ok; 
}
int main(){
     
	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;
}

另外一道题目到现在还没有找到AC的代码,初步实战训练的话就先看上面这个问题吧。

你可能感兴趣的:(算法学习,算法,剪枝,数据结构)