搜索算法 学习笔记

概述

做搜索题时,直接朴素 DFS 或 BFS 可能会寄,这时需要对搜索方法进行改进。

下面以 P1379 八数码问题为例。

双向广搜

如果直接广搜,搜索树会比较庞大。这题中由于终点已知,可以从起点和终点同时广搜,可以将复杂度的指数减半。

具体来说,维护两个队列,从起点和终点交替扩展队列,搜索的两端相遇则返回这个状态到起点和终点的距离之和。

#include<bits/stdc++.h>
using namespace std;
string a; 
queue<string>q1,q2;
map<string,int>dis1,dis2;
const int dx[4]={-1,0,0,1},dy[4]={0,-1,1,0};
void solve1(){
  string now=q1.front();
  q1.pop();
  if(dis1[now]&&dis2[now])cout<<dis1[now]+dis2[now]<<'\n',exit(0);
  int temp[3][3],x0,y0;
  for(int i=0,cnt=0;i<3;i++){
    for(int j=0;j<3;j++){
      temp[i][j]=now[cnt++]-'0';
      if(!temp[i][j])x0=i,y0=j;
    }
  }
  for(int i=0,nx,ny;i<4;i++){
    nx=x0+dx[i],ny=y0+dy[i];
    if(nx<0||nx>2||ny<0||ny>2)continue;
    swap(temp[nx][ny],temp[x0][y0]);
    string s;
    for(int i=0;i<3;i++)for(int j=0;j<3;j++)s+=(char)(temp[i][j]+'0');
    if(!dis1[s])dis1[s]=dis1[now]+1,q1.push(s);
    swap(temp[nx][ny],temp[x0][y0]);
  }
}
void solve2(){
  string now=q2.front();
  q2.pop();
  if(dis1[now]&&dis2[now])cout<<dis1[now]+dis2[now]<<'\n',exit(0);
  int temp[3][3],x0,y0;
  for(int i=0,cnt=0;i<3;i++){
    for(int j=0;j<3;j++){
      temp[i][j]=now[cnt++]-'0';
      if(!temp[i][j])x0=i,y0=j;
    }
  }
  for(int i=0,nx,ny;i<4;i++){
    nx=x0+dx[i],ny=y0+dy[i];
    if(nx<0||nx>2||ny<0||ny>2)continue;
    swap(temp[nx][ny],temp[x0][y0]);
    string s;
    for(int i=0;i<3;i++)for(int j=0;j<3;j++)s+=(char)(temp[i][j]+'0');
    if(!dis2[s])dis2[s]=dis2[now]+1,q2.push(s);
    swap(temp[nx][ny],temp[x0][y0]);
  }
}
int main(){
  cin>>a;
  if(a=="123804765")return puts("0"),0;
  q1.push(a),q2.push("123804765"),dis1[a]=dis2["123804765"]=0;
  while(1)solve1(),solve2();
  return 0;
}

A*

在 BFS 拓展时,可以优先选择更优的数字扩展。

设从起点到某个状态的代价为 \(g(x)\),某个状态到终点的代价的为 \(h^\ast(x)\),估计值为 \(h(x)\)。其中 \(g(x)\) 在搜索到这个状态时已知,而 \(h(x)\) 未知。这个状态的估价函数为 \(f(x)=g(x)+h(x)\)。把队列换成堆,优先拓展 \(f(x)\) 更小的队列。

\(h(x)\leq h^\ast(x)\),A*能保证找到最优解,而当 \(h\) 满足三角形不等式时可以去重。

口胡一个证明:三角形不等式即 \(h(u)\leq h(v)+w(u,v)\)。去重必须满足第一次访问某个状态时不绕远。假设第二次访问 \(u\) 经过 \(v\),那么 \(f(u)\leq f(v)\)\(f(u)-h(u)-w(u,v)=g(u)-w(u,v)\leq f(v)-h(v)=g(v)\)\(g(u)\leq g(v)+w(u,v)\),得证。

这一题中设估价函数为所有数字到目标位置的曼哈顿距离之和(不含 \(0\)),显然满足两个性质。

#include<bits/stdc++.h>
using namespace std;
string a;
priority_queue<pair<int,string>,vector<pair<int,string>>,greater<pair<int,string>>>q;
map<string,int>dis;
const int dx[4]={-1,0,0,1},dy[4]={0,-1,1,0},pos[9][2]={{1,1},{0,0},{0,1},{0,2},{1,2},{2,2},{2,1},{2,0},{1,0}};
int h(int a[3][3],int ans=0){
  for(int i=0;i<3;i++)for(int j=0;j<3;j++)ans+=(a[i][j]?abs(pos[a[i][j]][0]-i)+abs(pos[a[i][j]][1]-j):0);
  return ans;
}
int main(){
  cin>>a,q.push(make_pair(0,a)),dis[a]=0;
  while(!q.empty()){
    string now=q.top().second;
    q.pop();
    if(now=="123804765")break;
    int temp[3][3],x0,y0;
    for(int i=0,cnt=0;i<3;i++){
      for(int j=0;j<3;j++){
        temp[i][j]=now[cnt++]-'0';
        if(!temp[i][j])x0=i,y0=j;
      }
    }
    for(int i=0,nx,ny;i<4;i++){
      nx=x0+dx[i],ny=y0+dy[i];
      if(nx<0||nx>2||ny<0||ny>2)continue;
      swap(temp[nx][ny],temp[x0][y0]);
      string s;
      for(int i=0;i<3;i++)for(int j=0;j<3;j++)s+=(char)(temp[i][j]+'0');
      if(!dis[s])dis[s]=dis[now]+1,q.push(make_pair(dis[s]+h(temp),s));
      swap(temp[nx][ny],temp[x0][y0]);
    }
  }
  return cout<<dis["123804765"]<<'\n',0;
}

IDDFS

迭代加深搜索(IDDFS)是每次限制搜索深度的深搜。

某些搜索题的 DFS 搜索树非常庞大,比如这一题可以无限操作下去,搜索树的深度是无穷大的。可以使用广搜,因为 BFS 搜索树是一层层拓展的。而 IDDFS 中设置一个深度的上限 \(lim\),从小到大枚举 \(lim\) 跑 DFS。这样就类似于用 DFS 实现 BFS,并且减少了空间。

然而这题用朴素的 IDDFS 是过不了的,因为 DFS 的特性不能满足第一次访问某个状态时不绕远,就难以判重。

双向迭代加深搜索

类似双向广搜,从起点和终点轮流跑 DFS。这样可以过。

#include<bits/stdc++.h>
using namespace std;
string a;
int lim,dx[4]={-1,0,0,1},dy[4]={0,-1,1,0};
map<string,pair<int,int>>dis;
void dfs(string now,int d,int p,int c){
  if(d>lim)return;
  if(c==2&&dis[now].second==1)cout<<d+dis[now].first-2<<'\n',exit(0);
  dis[now]=make_pair(d,c);
  int temp[3][3],x0,y0;
  for(int i=0,cnt=0;i<3;i++){
    for(int j=0;j<3;j++){
      temp[i][j]=now[cnt++]-'0';
      if(!temp[i][j])x0=i,y0=j;
    }
  }
  for(int i=0,nx,ny;i<4;i++){
    nx=x0+dx[i],ny=y0+dy[i];
    if(nx<0||nx>2||ny<0||ny>2||i+p==3)continue;
    swap(temp[nx][ny],temp[x0][y0]);
    string s;
    for(int i=0;i<3;i++)for(int j=0;j<3;j++)s+=(char)(temp[i][j]+'0');
    dfs(s,d+1,i,c),swap(temp[nx][ny],temp[x0][y0]);
  }
}
int main(){
  cin>>a;
  while(1)lim++,dfs(a,1,-1,1),dfs("123804765",1,-1,2),dis.clear();
  return 0;
}

IDA*

IDDFS 与 A* 的结合。同样是上面的估价函数,如果 \(f(x)>lim\),显然经过这个状态不可行,则剪枝。

#include<bits/stdc++.h>
using namespace std;
string a;
int lim,dx[4]={-1,0,0,1},dy[4]={0,-1,1,0},pos[9][2]={{1,1},{0,0},{0,1},{0,2},{1,2},{2,2},{2,1},{2,0},{1,0}};
int h(int a[3][3],int ans=0){
  for(int i=0;i<3;i++)for(int j=0;j<3;j++)ans+=(a[i][j]?abs(pos[a[i][j]][0]-i)+abs(pos[a[i][j]][1]-j):0);
  return ans;
}
void dfs(string now,int d,int p){
  if(now=="123804765")cout<<lim-1<<'\n',exit(0);
  int temp[3][3],x0,y0;
  for(int i=0,cnt=0;i<3;i++){
    for(int j=0;j<3;j++){
      temp[i][j]=now[cnt++]-'0';
      if(!temp[i][j])x0=i,y0=j;
    }
  }
  if(d+h(temp)>lim)return;
  for(int i=0,nx,ny;i<4;i++){
    nx=x0+dx[i],ny=y0+dy[i];
    if(nx<0||nx>2||ny<0||ny>2||i+p==3)continue;
    swap(temp[nx][ny],temp[x0][y0]);
    string s;
    for(int i=0;i<3;i++)for(int j=0;j<3;j++)s+=(char)(temp[i][j]+'0');
    dfs(s,d+1,i),swap(temp[nx][ny],temp[x0][y0]);
  }
}
int main(){
  cin>>a;
  while(1)lim++,dfs(a,1,-1);
  return 0;
}

[[搜索]]

posted @ 2024-03-01 09:42  lgh_2009  阅读(2)  评论(0编辑  收藏  举报