搜索算法 学习笔记
概述
做搜索题时,直接朴素 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;
}
[[搜索]]