【SinGuLaRiTy-1039】 Copyright (c) SinGuLaRiTy 2017. All Rights Reserved.
迭代加深搜索(ID)
迭代加深搜索,实质上就是限定下界的深度优先搜索。即首先允许深度优先搜索K层搜索树,若没有发现可行解,再将K+1后重复以上步骤搜索,直到搜索到可行解。
在迭代加深搜索的算法中,连续的深度优先搜索被引入,每一个深度约束逐次加1,直到搜索到目标为止。
迭代加深搜索算法就是仿广度优先搜索的深度优先搜索。既能满足深度优先搜索的线性存储要求,又能保证发现一个最小深度的目标结点。
从实际应用来看,迭代加深搜索的效果比较好,并不比广度优先搜索慢很多,但是空间复杂度却与深度优先搜索相同,比广度优先搜索小很多。
<伪代码>
dfs(int depth,int maxdepth) { if(depth>maxdepth) return; if(找到答案) 输出答案; for each(当前节点的儿子节点) dfs(depth+1,maxdepth); } int main() { for(int i=mindepth;;i++) dfs(0,i); }
eg.埃及分数
Click HERE to get to the problem.
在古埃及,人们使用单位分数的和(形如1/a的,a是自然数)表示一切有理数。如:2/3=1/2+1/6,但不允许2/3=1/3+1/3,因为加数中有相同的。
对于一个分数a/b,表示方法有很多种,但是哪种最好呢?首先,加数少的比加数多的好,其次,加数个数相同的,最小的分数越大越好。如:
19/45=1/3 + 1/12 + 1/180
19/45=1/3 + 1/15 + 1/45
19/45=1/3 + 1/18 + 1/30
19/45=1/4 + 1/6 + 1/180
19/45=1/5 + 1/6 + 1/18.
最好的是最后一种,因为1/18比1/180,1/45,1/30,1/180都大。
给出a,b(0<a<b<1000),编程计算最好的表达方式。
解析
很明显这道题是一道搜索题。
我们首先来描述它对应的隐式搜索树:这是一个k叉树,k几乎无穷大,而且层数也是无穷大的。
如果直接使用DFS,有可能遇到一个“无底洞”——即第一个可行解的层次可能非常大。即使加上剪枝,仍然会超时;而如果使用BFS,则需要用队列保存节点,而每层的节点理论上有无穷多个,根本无法用队列存储。
这个时候,我们就可以用IDDFS。
Code
#include<iostream> #include<cstdio> #include<cstring> #include<cstdlib> #include<cmath> #define INF 0x7fffffff using namespace std; int error[8]; int res[20000]; int temp[2000]; int a,b,k; int gcd(int a,int b) { return (b ? gcd(b,a%b) : a); } bool IDDFS(int cur,int limit,int a,int b,int last) { if(cur==0)memset(temp,0,sizeof(temp)); if(cur==limit-1) { if(a!=1||b<=last) return false; for(int i=1;i<=k;i++) if(b==error[i]) return false; temp[limit]=b; if(res[1]) { for(int i=limit;i>=1;i--) { if(temp[i]<res[i]) { for(int j=1;j<=limit;j++) res[j]=temp[j]; break; } else if(temp[i]>res[i]) break; } return true; } else { for(int j=1;j<=limit;j++) res[j]=temp[j]; return true; } } if(a==0) return false; bool flag=false; for(int i=max(last+1,b/a);i<=INF/b&&i<=(limit-cur)*b/a;i++) { bool check=false; for(int j=1;j<=k;j++) if(error[j]==i) check=true; if(check==true) continue; int a1=a*i-b,b1=b*i; int x=gcd(a1,b1); a1/=x,b1/=x; temp[cur+1]=i; if(IDDFS(cur+1,limit,a1,b1,i))flag=true; } return flag; } int main() { int T; int cases=0; scanf("%d",&T); while(T--) { memset(res,0,sizeof(res)); memset(temp,0,sizeof(temp)); scanf("%d%d%d",&a,&b,&k); for(int i=1;i<=k;i++) scanf("%d",&error[i]); int x=1; while(1) { if(IDDFS(0,x,a,b,1)) break; else x++; } printf("Case %d: %d/%d=1/%d",++cases,a,b,res[1]); for(int i=2;i<=x;i++) printf("+1/%d",res[i]); printf(" "); } return 0; }
eg.加法链
Click HERE to get to the problem.
一个n的加法链是指符合下面4个条件的整数数列:
1.a0=1
2.am=n
3.a0<a1<a2<……<am-1<am
4.对每个ak(1<k<=m),都存在ai和aj,(i,j可以相同),满足ak=ai+aj
给你一个整数n,你的任务是构造一个n的加法链,使得链的长度最短。如果有多个答案,则任意输出一个即可。
例如,<1,2,4,5>是一种满足条件的5的加法链;<1,2,4,8,9,17,34,68,77>是一种满足条件的77的加法链。
解析
首先可以求出最少需要几个元素可以达到n。按照贪心的策略,对于每个元素的值,都选择让它等于一个数的两倍,即对于每个Ai = Ai-1 + Ai-1,当Ai>=n时就跳出循环,得到最少元素个数。
然后从最少步数开始迭代加深搜索,再用上一些剪枝技巧即可。
<关于这些“剪枝技巧”>
1.当前深度要填的数 肯定来自于它前几位中,较大的两位组成,且其和 要小于等于末端值n 又要大于前一位的值(序列严格递增)。即a[k-1]<a[i]+a[j]<=n
2.对于当前要填的值 temp,由于已经确定了深搜的深度,那么 我们按最大的取值(i,j都取前一位,即a[k-1]*2)延伸到 最大深度时候的temp,如果其值都比n小(能取的最大值都比n小),意味着在这趟搜索中它永远不可能达到n,所以剪掉这支。
Code
#include<iostream> #include<algorithm> #include<stdio> #include<cstring> #include<stdlib> using namespace std; int a[1000]={1}; int n=0,depth=0,flag=0; int DFS(int len,int depth) { if(flag) return(0); if(len==depth&&a[len-1]==n) { flag=1; return(0); } else { for(int i=len-1;i>=0;i--) for(int j=i;j>=0;j--) if(a[i]+a[j]<=n&&a[i]+a[j]>a[len-1]) { a[len]=a[i]+a[j]; int temp=a[len]; int k=0; for(k=len+1;k<=depth;k++) temp*=2; if(temp<n) continue; DFS(len+1,depth); if(flag) return 0; } } return 0; } int main() { while(scanf("%d",&n)!=EOF&&n>0) { a[0]=1; depth=0; int temp=1; while(temp<n) { depth++; temp*=2; } depth++; flag=0; while(!flag) { DFS(1,depth); if(!flag) depth++; } printf("%d",a[0]); for(int i=1;i<=depth-1;i++) printf(" %d",a[i]); printf(" "); } return 0; }
双向BFS搜索
双向BFS即从开始状态和结束状态同时往中间搜,当两端的路径交会于同一个顶点时,则最短路径就找到了。
一般的BFS,如果搜索树的深度为L,度为K,则搜索的最坏时间复杂度为K^L;而如果我们采用双向BFS,时间复杂度则降为2*K^(L/2),自然可以极大的提高搜索速度。
双向BFS本质上还是BFS,只是从两端同时搜索而已,我们可以采用两个队列来实现它。
双向BFS搜索,可以使用两个队列,交替进行搜索,每个方向搜完一层再换另一个方向,即逐层交替搜索。可以使用两个标记数组,一个用于标记正向搜索已经访问的节点,一个用于反向搜索已经访问的节点。如果正向搜索和反向搜索都访问到同一个节点,则表示已经找到了一条由起点到终点的路径。
<伪代码>
while(!queue.empty()) { t=queue.front(); queue.pop(); foreach(t下一个状态next) { if(当前是正向搜索) { if(vis2[next]==1) { 处理结果,搜索结束; } if(vis1[next]==0) { queue.push(next); } } else//当前是反向搜索 { if(vis1[next]==1) { 处理结果,搜索结束; } if(vis2[next]==0) { queue.push(next); } } } }
eg.8数码问题
Click HERE to get to the problem.
如下如所示,a,b分别表示两种状态,每个九宫格中的数字都是0~8共9个数字的任意一种排列,现在要把算出a状态移动到b状态的最佳步骤(移动步数最少)。移动的规则是0方块与上下左右任意一块互换位置。
解析
本题其实可以采用双向搜索算法: 因为初始状态和目标状态都是确定的,要求的是最短路径。
Code
这是来自zy691357966的代码,他对这个问题的分析与展开也很棒。
#include<cstdio> #include<cstdlib> #include<cmath> #include<cstring> #include<ctime> #include<algorithm> #include<iostream> #include<sstream> #include<string> #include<queue> #define oo 0x13131313 using namespace std; struct node { int operator[](int index) const { return A[index]; } int& operator[](int index) { return A[index]; } int A[10]; int xnum; int deep; int Contor; void JSContor() { int temp=0,p=1; for(int i=8;i>=0;i--) { int tot=0; for(int j=0;j<i;j++) if(A[j]<A[i]) tot++; temp+=(A[i]-tot-1)*p; p=p*(9-i); } Contor=temp+1; } }; int visit[2][370000]; int dist[2][370000]; int DEEP[2][370000]; int dANS[2][370000]; char f[4]={'u','d','l','r'}; int fx[5]={-1,+1,0,0}; int fy[5]={0,0,-1,+1}; queue <node> Q[2]; node start; node End; char AAA[20]; void input() { memset(visit,0,sizeof(visit)); for(int i=0;i<9;i++) { if(AAA[i]=='x') { start[i]=9; start.xnum=i; } else start[i]=AAA[i]-'0'; End[i]=i+1; } End.xnum=8; End.deep=0; start.deep=0; } void csh() { while(Q[0].empty()!=1) Q[0].pop(); start.JSContor(); visit[0][start.Contor]=1; Q[0].push(start); while(Q[1].empty()!=1) Q[1].pop(); End.JSContor(); visit[1][End.Contor]=1; Q[1].push(End); } int GAN(int pos) { node s,t; int x,y,p; s=Q[pos].front(); Q[pos].pop(); if(visit[pos^1][s.Contor]==1) return s.Contor; s.deep++; x=s.xnum/3;y=s.xnum%3; for(int i=0;i<=3;i++) { t=s; if(x+fx[i]<=2&&x+fx[i]>=0&&y+fy[i]<=2&&y+fy[i]>=0) { p=(x+fx[i])*3+y+fy[i]; swap(t[t.xnum],t[p]); t.xnum=p; t.JSContor(); if(visit[pos][t.Contor]==0) { Q[pos].push(t); visit[pos][t.Contor]=1; dist[pos][t.Contor]=s.Contor; dANS[pos][t.Contor]=i; DEEP[pos][t.Contor]=t.deep; } } } return 0; } void twobfs() { void print(int ok1); csh(); int ok1=0,ok2=0; while(Q[0].empty()!=1&&Q[1].empty()!=1) { ok1=GAN(0); if(ok1!=0) break; ok2=GAN(1); if(ok2!=0) {ok1=ok2;break;} } print(ok1); } char ANS[20]; void print(int ok1) { int tot=1; for(int p=ok1;p!=start.Contor;p=dist[0][p]) { ANS[tot]=f[dANS[0][p]]; tot++; } for(int i=tot-1;i>=1;i--) printf("%c",ANS[i]); tot=1; for(int p=ok1;p!=End.Contor;p=dist[1][p]) { ANS[tot]=f[dANS[1][p]^1]; tot++; } for(int i=1;i<tot;i++) printf("%c",ANS[i]); printf(" "); } int OK=1; int JZyj() { int tot1=0; for(int i=0;i<=8;i++) for(int j=0;j<i;j++) if(start[i]!=9&&start[j]!=9) if(start[i]<start[j]) tot1++; if(tot1%2==0) return 1; else return 0; } int main() { while(scanf("%c %c %c %c %c %c %c %c %c ",&AAA[0],&AAA[1],&AAA[2],&AAA[3],&AAA[4],&AAA[5],&AAA[6],&AAA[7],&AAA[8])!=EOF) { input(); if(JZyj()) twobfs(); else printf("unsolvable "); } return 0; }
其实,8数码难题还有一个更快的解法:A*算法
A*算法
在学习A*算法之前,我们来了解一下什么是启发式搜索。我们知道,DFS和BFS在展开子结点时均属于盲目型搜索,也就是说,它不会考虑哪个结点在下一次搜索中更优而去选择该结点进行下一步的搜索。在运气不好的情形中,均需要试探完整个解集空间。而启发式搜索,与DFS和BFS这类盲目型搜索最大的不同,就在于选择下一步要搜索的结点时,可以通过一个启发函数来进行选择,选择代价最少的结点作为下一步搜索的结点(遇到有一个以上代价最少的结点,不妨选距离当前搜索点最近一次展开的搜索点进行下一步搜索)。一个经过仔细设计的启发函数,往往在很快的时间内就可得到一个搜索问题的最优解,对于NP问题,亦可在多项式时间内得到一个较优解。
A*算法,作为启发式算法中很重要的一种,被广泛应用在最优路径求解和一些策略设计的问题中。而A*算法最为核心的部分,就在于它的一个估值函数的设计上:
f(n)=g(n)+h(n)
其中f(n)是每个可能试探点的估值,它有两部分组成:一部分为g(n),它表示从起始搜索点到当前点的代价(通常用某结点在搜索树中的深度来表示)。另一部分,即h(n),它表示启发式搜索中最为重要的一部分,即当前结点到目标结点的估值,h(n)设计的好坏,直接影响着具有此种启发式函数的启发式算法的是否能称为A*算法。
一种具有f(n)=g(n)+h(n)策略的启发式算法能成为A*算法的充分条件是:
1) 搜索树上存在着从起始点到终点的最优路径。
2) 问题域是有限的
3) 所有结点的子结点的搜索代价值>0。
4) h(n)<=h*(n) h*(n)为实际问题的代价值
当此四个条件都满足时,该启发式算法才能成为A*算法,并一定能找到最优解。
对于一个搜索问题,显然,条件1,2,3都是很容易满足的,而条件4——h(n)<=h*(n)是需要精心设计的,由于h*(n)显然是无法知道的。
所以,一个满足条件4的启发策略h(n)就来的难能可贵了。不过,对于图的最优路径搜索和八数码问题,有些相关策略h(n)不仅很好理解,而且已经在理论上证明是满足条件4)的,从而为这个算法的推广起到了决定性的作用。不过h(n)距离h*(n)的呈度不能过大,否则h(n)就没有过强的区分能力,算法效率并不会很高。对一个好的h(n)的评价是:h(n)在h*(n)的下界之下,并且尽量接近h*(n).
<8数码难题的A*算法思路>
带有注释的该算法代码:
#pragma warning(disable:4786) #include <algorithm> #include <cstdio> #include <set> #include <utility> #include <ctime> #include <cassert> #include <cstring> #include <iostream> using namespace std; /*item记录搜索空间中一个结点 state 记录用整数形式表示的8数码格局 blank 记录当前空格位置,主要用于程序优化, 扩展时可不必在寻找空格位置 g, h 对应g(n), h(n) pre 记录当前结点由哪个结点扩展而来 */ struct item { int state; int blank; int g; int h; int pre; }; const int MAXSTEPS = 100000; const int MAXCHAR = 100; char buf[MAXCHAR][MAXCHAR]; //open表 item open[MAXSTEPS]; //vector<item> open; int steps = 0; //closed表,已查询状态只要知道该状态以及它由哪个结点扩展而来即可,用于输出路径 //每次只需得到对应f值最小的待扩展结点,用堆实现提高效率 pair<int, int> closed[MAXSTEPS]; //读入,将8数码矩阵格局转换为整数表示 bool read(pair<int,int> &state) { if (!gets(buf[0])) return false; if (!gets(buf[1])) return false; if (!gets(buf[2])) return false; //cout << strlen(buf[0]) << ' ' << strlen(buf[1]) << ' ' << strlen(buf[2]) << endl; assert(strlen(buf[0]) == 5 && strlen(buf[1]) == 5 && strlen(buf[2]) == 5); // astar.in中的每行数据长度必须为5 state.first = 0; for (int i = 0, p = 1; i < 3; ++i) { for (int j = 0; j < 6; j += 2) { if (buf[i][j] == '0') state.second = i * 3 + j / 2; // state.second为0(空格)在节点中的位置 else state.first += p * (buf[i][j] - '0'); p *= 10; } } /* 若初试节点为: 1 2 3 8 0 4 7 6 5 则state.first为567408321,state.second为4 */ return true; } //计算当前结点距目标的距离 int calculate(int current, int target) // return h=the sum of distances each block have to move to the right position,这里的each block不包括空格 { int c[9], t[9]; int i, cnt = 0; for (i = 0; i < 9; ++i) { c[current % 10] = t[target % 10] = i; current /= 10; target /= 10; } for (i = 1; i < 9; ++i) cnt += abs(c[i] / 3 - t[i] / 3) + abs(c[i] % 3 - t[i] % 3); return cnt; } //open表中结点间选择时的规则 f(n) = g(n) + h(n) class cmp { public: inline bool operator()(item a, item b) { return a.g + a.h > b.g + b.h; } }; //将整数形式表示转换为矩阵表示输出 void pr(int state) { memset(buf, ' ', sizeof(buf)); for (int i = 0; i < 3; ++i) { for (int j = 0; j < 6; j += 2) { if (state % 10) buf[i][j] = state % 10 + '0'; state /= 10; } buf[i][5] = '