对于可以用回溯法求解但解答树的深度没有明显上限的题目,可以考虑用迭代加深搜索(iterative deepening)。如果设计出以一个乐观估价函数,预测从当前结点至少还要扩展基层结点才能得到解,则迭代加神搜索编程了IDA*算法。——《算法竞赛入门经典》p207
讲真看书里第一个埃及分数问题,看得我是一脸懵逼。感觉好像是这么个道理,但是没懂到底什么意思。接着就跳过先看了下面那道例题,也就是Editing a Book,看完感觉这题更容易理解点,我好像也能写出来的样子,就试着写了写,两个多小时终于A了。这过程中也确实开始对这个算法有了些理解。
前几天碰巧因为上一篇博客那道题的原因,去看了关于A*算法,所以我在看埃及分数问题的时候,书中提到“当前结点n的深度g(n),乐观估价函数为h(n),则当g(n)+h(n)>maxd的时候应该剪枝”,当时我第一反应:“呀,这玩意儿怎么跟A*这么像”。所以当我看到这个算法名字的时候,就明白了,很直观,就是迭代加深搜加A*。所以,回顾先下A*。
简单来说,A*就是给一般的广搜加一个判断条件,让程序知道,沿这条路下去,有没有机会到达终点,或者说是不是一条不过分绕远的路。我觉得我看的那篇博客里讲得很形象顺便给下传送门吧(A*算法),就相当于走到迷宫这一格的时候,站起来看看终点是不是在我们现在正走向的那个方向,如果是就继续下去,不是就停止扩展这一层。
而IDA*就是我们设计出一个估价函数,来判断,在这一次的迭代中,我们有没有机会通过深搜到达终点的状态。而每一次迭代也是从初始状态开始重新深搜(这也是我一开始搞不懂的地方)。
直接用题目来说:
先设 后继不正确的数字个数为h,当前搜索深度为d,当前迭代深度限制是maxd
通过分析,我们可以知道,每一次剪切时h最多减少3,那么我们就可以得出这样的结论,(maxd-d)必须>=(h/3),
前半部分的含义是,还能剪切几次(即搜索几次),后半部分则是假设每次剪切都能影响最多数字的后继,那么至少需要h/3次剪切才能完成。那么这个式子的意思就是
如果剩余剪切次数少于h/3,那么肯定没有机会了,因此就可以剪枝。
以上是最核心的思想。
剩下还要提的就是,maxd(即迭代次数)最好能给出个上限,本题就可以给出上限8(其实本题上限是5,别问我怎么证明,用程序跑出来的。。。)
总结下这种题目的重点:1.每一次迭代都从初始状态开始深搜 2.设计估价函数来剪枝
代码如下
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<queue>
using namespace std;
const int MAX=11;
int N;
int maxd;
int kase=0;
struct Node{
int arr[MAX];
};
int h(int*a)
{
int cnt = 0;
for (int i = 0; i < N;i++)
if (a[i] + 1 != a[i + 1])
cnt++;
return cnt;
}
void shift(Node& tmp,Node& node,int i,int j,int k){
if(i>k){
for(int p=1;p<=j-i+1;p++)
tmp.arr[k+p]=node.arr[i+p-1];
for(int p=1;p<=i-k-1;p++)
tmp.arr[k+j-i+1+p]=node.arr[k+p];
}
else{
for(int p=1;p<=k-j;p++)
tmp.arr[i+p-1]=node.arr[j+p];
for(int p=0;p<j-i+1;p++)
tmp.arr[k-p]=node.arr[j-p];
}
}
bool dfs(int d,Node node){
if(3*(maxd-d)<h(node.arr)) return false;
if(h(node.arr)==0) return true;
int i,j,k;
for(i=0;i<N;i++)
for(j=i;j<N;j++)
for(k=0;k<N;k++){
if(k>=i&&k<=j) continue;
Node tmp;
memcpy(tmp.arr,node.arr,sizeof(node.arr));
shift(tmp,node,i,j,k);
if(dfs(d+1,tmp)) return true;
}
return false;
}
int solve(Node node){
for(maxd=1;maxd<8;maxd++){
if(dfs(0,node)) break;
}
return maxd;
}
int main()
{
while(scanf("%d",&N)&&N){
int res;
Node node;
for(int i=0;i<N;i++)
scanf("%d",&node.arr[i]);
node.arr[N]=N+1;
if(h(node.arr))
res=solve(node);
else
res=0;
printf("Case %d: %d\n",++kase,res);
}
return 0;
}
ps:初始化数组的时候,一定要记得最后一个数字后面要扩展一位,这样在判断后继正确与否的时候就不会出问题。。。哎,我就是一开始没注意,卡了老半天。。。