搜索涉及的知识点
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;
}
posted on
浙公网安备 33010602011771号