搜索涉及的知识点
BFS--双向广搜
DFS-剪枝(可行性剪枝、最优性剪枝、玄学剪枝)
A*
IDA*
迭代加深搜索IDDFS
DLX
记忆化搜索
模拟退火
遗传算法
爬山算法
随机化搜索
启发式搜索:启发式搜索就是在状态空间中的搜索对每一个搜索的位置进行评估,得到最好的位置,再从这个位置进行搜索直到目标。这样可以省略大量无畏的搜索路径,提到了效率。在启发式搜索中,对位置的估价是十分重要的。采用了不同的估价可以有不同的效果。
估价函数:从当前节点移动到目标节点的预估费用;这个估计就是启发式的。在寻路问题和迷宫问题中,我们通常用曼哈顿(manhattan)估价函数(下文有介绍)预估费用。
A*算法与BFS:可以这样说,BFS是A*算法的一个特例。对于一个BFS算法,从当前节点扩展出来的每一个节点(如果没有被访问过的话)都要放进队列进行进一步扩展。也就是说BFS的估计函数h永远等于0,没有一点启发式的信息,可以认为BFS是“最烂的”A*算法。
http://www.cppblog.com/mythit/archive/2009/04/19/80492.aspx
选取最小估价:如果学过数据结构的话,应该可以知道,对于每次都要选取最小估价的节点,应该用到最小优先级队列(也叫最小二叉堆)。在C++的STL里有现成的数据结构priority_queue,可以直接使用。当然不要忘了重载自定义节点的比较操作符。
A*算法的特点:A*算法在理论上是时间最优的,但是也有缺点:它的空间增长是指数级别的。
IDA*算法:这种算法被称为迭代加深A*算法,可以有效的解决A*空间增长带来的问题,甚至可以不用到优先级队列。如果要知道详细:google一下。
1、打印全排列
//打印全排列 //首先先排序 int a[maxn]; void perm(int st,int en){ if(st==ed){ print(); return; } else{ for(int i=st;i<=en;i++){ swap(a[st],a[i]); perm(st+1,en); swap(a[st],a[i]); } } } //竞赛题在一般情况下限时1s,所以元素个数小于11 //平凡下界:最小的复杂度
2、子集生成,含有m个元素的子集
//子集生成 //n个数,有2^n个子集,子集问题用二进制来表示最简单 void print_subset(int n){ for(int i=0;i<(1<<n);i++){ for(int j=0;j<n;j++){ if(i&(1<<j)) cout<<j<<" "; }//注意写法 cout<<endl; } } //如果想要在集合n中打印子集为m的集合 //那就是有m个1,而操作kk&(kk-1) 的含义就是消除最后的1,这样操作多少次,就会有多少个1 void print_sub(int n,int k){ for(int i=0;i<(1<<n);i++){ int temp=i,num=0; while(temp){ temp=temp&(temp-1); num++; } if(num==k){ for(int j=0;j<n;j++){ if(i&&(1<<j)) cout<<j<<" "; } cout<<endl; } } }
3、八数码问题
给定一个初始的3*3的棋局和一个目标棋局,输出最少需要多少步到达目标棋局
八数码的最重要的问题是判重:康托展开
cantor()的作用在于给出一个排列,输出这个第几个排列,这样就可以判重
其他解决方法:
https://www.cnblogs.com/zufezzt/p/5659276.html
struct node{ int a[10]; int ans; //记录最小步数 }; int dis[4][2]={{0,1},{0,-1},{-1,0},{1,0}}; const int len=362880 //9!=362880 int vis[len]; //标记这种状态有没有访问过 int st[10],ed[10] ; //初始、结局状态 long long fac[]={0,1,2,6,24,120,720,5040,40320,362880}; //cantor用到的常数 bool cantor(int str[],int n){ //判重 LL res=0; int con=0; for(int i=0;i<n;i++){ con=0; for(int j=i+1;j<n;j++){ if(str[j]<str[i]) con++; } res+=con*fac[n-i-1]; } if(vis[res]==0){ vis[res]=1; return 1; } return 0; } queue<node> q; int bfs(){ node head; memcpy(head.a,st,sizeof(head.a)); //注意这个的用法 head.ans=0; q.push(head); cantor(st,9); //对起点进行vis[]=1 while(!q.empty()){ head=q.front(); q.pop(); int op; for(int i=0;i<9;i++){ if(head.a[i]==0) { op=i;break; } } int x=op%3,y=op/3; //空格的坐标 for(int i=0;i<4;i++){ int xx=x+dis[i][0],yy=y+dis[i][1]; int newop=xx+yy*3; //转化为一维 if(xx>=0&&xx<3&&yy>=0&&yy<3){ node newnode; memcpy(&newnode,&head,sizeof(struct node)); swap(newnode.a[newop],newnode.a[op]); newnode.ans++; if(memcmp(newnode.a,en,sizeof(en))==0) return newnode.ans; if(cantor(newnode.a,9)){ q.push(newnode); } } } } return -1; }
判断能不能到达
//直接判断能不能到达: //一个状态表示成一维的形式,求出除0之外所有数字的逆序数之和,也就是每个数字前面比它大的数字的个数的和,称为这个状态的逆序。 //若两个状态的逆序奇偶性相同,则可相互到达,否则不可相互到达。 int sum=0; for(int i=0;t[i];i++){ if(t[i]=='x') continue; for(int j=0;j<i;j++){ if(t[j]=='x') continue; if(t[i]<t[j]) sum++; } } if(sum%2==1) { printf("unsolvable\n"); continue; }
记录路径的方法
。。。
康托展开(逆展开),以及相关的优化算法摸鱼日记1:康托展开/逆康托展开 - Slithery - 博客园 (cnblogs.com)
下面是原始的没有优化的康托展开和逆展开
#include<bits/stdc++.h> using namespace std; int fac[20],num[20]; int cantor(int per[],int len){ int rk=0; for(int i=0;i<len;i++){ int x=0; for(int j=i+1;j<len;j++) if(per[i]>per[j]) x++; rk+=x*fac[len-i-1]; } return rk+1; } vector<int>incantor(int rk,int len){ rk--; int x; vector<int> vec,ans; for(int i=1;i<=len;i++) vec.push_back(i); for(int i=1;i<=len;i++){ x=rk/fac[len-i]; ans.push_back(vec[x]); vec.erase(vec.begin()+x); rk%=fac[len-i]; //不用-1 } return ans; } int main(){ int n; cin>>n; for(int i=0;i<n;i++){ cin>>num[i]; } fac[0]=fac[1]=1; for(int i=2;i<=n;i++) fac[i]=fac[i-1]*i; int rk=cantor(num,n); cout<<"rank: "<<rk<<endl; if(rk!=1){ vector<int> v=incantor(rk,n); cout<<"permutation: "; for(int i=0;i<n;i++) cout<<v[i]<<" "; cout<<endl; } return 0; }
4、A*
启发式搜索的一种,其实就是BFS+贪心,给每个状态一个评估函数
//写一个A*算法:其实就是在队列里面,每次都选择其附加函数F最小的,这就是优先队列的应用 //但是注意要重载小于符 //整体难度不大,但是要理解算法 struct node{ int x,y,step; int g; //实际的距离 int h; //启发的信息 int f; //总的估值函数 bool operator < (const node& a)const{ return f>a.f; //注意是反的,其实是小的排前面,不要忘了基础知识 } }k; bool vis[8][8]; int x1,yy1,x2,y2,ans; int dirs[8][2]={{-2,-1},{-2,1},{2,-1},{2,1},{-1,-2},{-1,2},{1,-2},{1,2}};//8个移动方向 priority_queue<node> q; bool judge(const node &a){ if(a.x<0||a.y<0||a.x>=8||a.y>=8) return 0; return 1; } int manha(const node &a){ return (abs(a.x-x2)+abs(a.y-y2))*10; //? } void astar(){ node t,s; while(!q.empty()){ t=q.top(); q.pop(); vis[t.x][t.y]=1; if(t.x==x2&&t.y==y2){ ans=t.step; return; } for(int i=0;i<8;i++){ s.x=t.x+dirs[i][0]; s.y=t.y+dirs[i][1]; if(judge(s)&&!vis[s.x][s.y]){ s.g=t.g+23;//23表示根号5乘以10再取其ceil s.h=manha(s); s.f=s.g+s.h; s.step=t.step+1; q.push(s); } } } } char line[5]; int main(){ while(gets(line)){ x1=line[0]-'a',yy1=line[1]-'1',x2=line[3]-'a',y2=line[4]-'1'; memset(vis,0,sizeof(vis)); k.x=x1; k.y=yy1; k.step=0;k.g=0; k.h=manha(k); k.f=k.h+k.g; while(!q.empty()) q.pop(); q.push(k); astar(); printf("To get from %c%c to %c%c takes %d knight moves.\n",line[0],line[1],line[3],line[4],ans); } return 0; }
5、双向广搜
//正向逆向交替走,分别标记方向,看某个时刻有没有相遇 //双向广搜 //要标记正反向的搜索记录 int n,sx,sy,ex,ey; int dis[8][2]={{1,2},{-1,2},{1,-2},{-1,-2},{2,1},{-2,-1},{-2,1},{2,-1}}; int vis[maxn][maxn]; //正向是1,逆向是2 int step[maxn][maxn]; int xx[1000000],yy[1000000]; //数组模拟队列 int fun(){ memset(step,0,sizeof(step)); memset(vis,0,sizeof(vis)); int head=0,tail=0; xx[tail]=sx;yy[tail++]=sy;vis[sx][sy]=1; xx[tail]=ex;yy[tail++]=ey;vis[ex][ey]=2; while(head!=tail){ int x=xx[head]; int y=yy[head++]; int t=step[x][y]; for(int i=0;i<8;i++){ int l=x+dis[i][0],r=y+dis[i][1]; if(l<0||l>=n||r<0||r>=n) continue; if(vis[l][r]!=vis[x][y]&&vis[l][r]&&vis[x][y]) return step[l][r]+step[x][y]+1; //有了交会 if(!vis[l][r]){ xx[tail]=l; yy[tail++]=r; step[l][r]=t+1; vis[l][r]=vis[x][y]; } } } return 0; } int main(){ int t; scanf("%d",&t); while(t--){ scanf("%d %d %d %d %d",&n,&sx,&sy,&ex,&ey); printf("%d\n",fun()); } return 0; }
6、迭代加深搜索:其实就是DFS和BFS的结合,在深度上用BFS进行扩散,但是写法实际上是DFS,
可以避免DFS和BFS两者的缺点
例题:埃及分数
逐步地增大搜索的深度
//利用DFS,BFS,深度上用BFS进行扩展,但总体上用DFS进行搜索 //埃及分数 LL ans[maxn],s[maxn],mo,ch; int dep; //迭代加深搜索 //https://www.cnblogs.com/hadilo/p/5746894.html //https://www.cnblogs.com/hchlqlz-oj-mrj/p/5389223.html LL gcd(LL a,LL b){ //返回最大公因数 return b==0? a:gcd(b,a%b); } void outp(){ if(ans[dep]>s[dep]){ //如果结果更优 ,ans[dep]>s[dep]说明最后一位大一些 for(int i=1;i<=dep;i++){ ans[i]=s[i]; } } } void dfs(LL x,LL y,int d){ LL a,b,i,w; if(d==dep){ //已经到了最后的深度了 //如果符合1/a的格式 s[d]=y; if(x==1&&s[d]>s[d+1]) outp(); //且递减 return; } //注意这个下面的范围 //这个是放大 for(i=max(s[d-1]+1,y/x+1);i<(dep-d+1)*y/x;i++){ b=y*i/gcd(y,i); a=b/y*x-b/i; //统分就知道了 w=gcd(a,b); a/=w;b/=w; s[d]=i; dfs(a,b,d+1); } } int main(){ scanf("%lld%lld",&ch,&mo); int i=gcd(ch,mo); ch/=i; mo/=i; for(dep=2;;dep++){ ans[1]=0; s[0]=0; //赋个初值 ans[dep]=INF; //赋个初值 dfs(ch,mo,1); if(ans[1]!=0) break; } for(int j=1;j<=dep;j++) printf("%lld ",ans[j]); printf("\n"); return 0; }
7、IDA*对迭代加深搜索的优化,LIKE A*在IDDFS上面的应用
在IDDFS上面增加一个估价函数,然后进行剪枝操作(利用估价函数进行剪枝)
POJ 3134 power calculus
qes:从数字1开始,进行多少次加减操作能够得到数字n
这道题是IDA*和IDDFS的应用,
IDDFS:指定递归深度,每一次做DFS不会超过这个深度
估价函数:如果当前的值用最快的方式:连续乘2都不能到达n,那么就可以剪枝了
int val[maxn]; //保持每个结果 int pos,n; bool ida(int now,int dep){ //当前的深度、规定的深度 if(now>dep) return false; if(val[pos]<<(dep-now)<n) return false; //ida if(val[pos]==n) return 1; pos++; //往下移 for(int i=0;i<pos;i++){ val[pos]=val[pos-1]+val[i]; if(ida(now+1,dep)) return 1; //DFS val[pos]=abs(val[pos-1]-val[i]); //DFS if(ida(now+1,dep)) return 1; } pos--; return false; //别忘了回溯 } int main(){ int t; while(cin>>n&&n){ int dep; for(dep=0;;dep++){ val[pos=0]=1; if(ida(0,dep)) break; } cout<<dep<<endl; } return 0; }