2025年4月6日学习记录
启发式搜索
A*算法:优先走估值函数的分支
估值函数可以为 \(f^*(x)=g(x)+h^*(x)\) \(g(x)\)表示从起点到 \(x\) 的代价,\(h(x)\)表示 \(x\) 到终点的代价的估计值。
当 \(h(x)=0\) 时,A*算法就退化成了普通的BFS。
当 \(h^*(x)\) 越大,算法效率越高。
但 \(h^*(x)\) 的值超过了实际到达终点的代价 \(h(x)\),就可能得到错误的答案。
P5507 机关 - 洛谷
题意:一个机关,上面一共有12个旋钮,每个旋钮有4个状态,将旋钮的状态用数字\(1\)到\(4\)表示
每个旋钮只能向一个方向旋转(状态:1->2->3->4->1),在旋转时,会引起另一个旋钮也旋转一次(方向相同,不会引起连锁反应),同一旋钮在不同状态下,可能会引起不同的旋钮旋转(在输入中给出)
当所有旋钮都旋转到状态1时,机关就打开了
请输出打开机关的最少步数和依次旋转的旋钮编号。
题解:妙啊,12个数,每个数有4个状态怎么存储?4个状态可以为0,1,2,3用2位二进制存储,12个数就是24位二进制也就是2\^24次方的大小,众所周知int是2\^31,所以用int类型存储没问题,用桶也可以开这么大,或者用map也没问题。
每个状态使用位运算来拆解状态、更改状态、分解状态。
运用结构体的构造函数,直接求出 \(h(x)\)函数,至于 \(g(x)\),每次更新即可。
输出操作步骤,可以采用链式前向星的思想,存储上一步,然后递归输出即可。存储下一步也没问题,可以直接循环输出或者递归。
至于 \(h(x)\) 所占比例,有的题解说每次两个点变,所以要除2,但其实没关系。
关键的还有 \(h(x)\) 求解时,\((4-((s>>(i*2))\&3))\&3\) ,去掉后面 \(\&3\) 会超时,因为估值函数不会正确的求解,当状态为 0 时,也就是已经到达目标状态,就会出现 \(h+=4-0\) 的情况,但是加上 \(\&3\) 后,当状态为 0 时,就会出现 \(h+=0\) 的情况,就不会破坏估值函数的正确性。不加 \(\&3\) 也可以,特判状态 0 即可。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int read(){
char op = getchar(); int x = 0, f = 1;
while (op < 48 || op > 57) {if (op == '-') f = -1; op = getchar();}
while (48 <= op && op <= 57) x = (x << 1) + (x << 3) + (op ^ 48), op = getchar();
return x * f;
}
const int N=1<<24;
int g[N],nxt[12][4];
bool vis[N];//记录状态是否出现过
int pre[N],but[N];//从哪个状态过来,用的什么按钮
struct node{
int st;//状态
double f;//状态对应估价函数值
node(int s){//构造函数
double h=0;
st=s;
for(int i=0;i<12;i++)
h+=(4-((s>>(i*2))&3))&3;//计算第i旋钮状态到0需要几次
h/=2;//都可以
f=h+g[s];//计算估值函数
}
friend bool operator <(node y,node x){
return x.f<y.f;//估值函数小的优先
}
};
int stzt;
void Astar(){
priority_queue<node> q;
q.push(node(stzt)),vis[stzt]=true;
while(!q.empty()){
int st=q.top().st;//状态
q.pop();
if(st==0) break;//等同于全是1
for(int i=0;i<12;i++){
int sti=(st>>(i*2))&3;//第i旋钮对应状态
int di=nxt[i][sti];//对应旋钮对应状态相连的旋钮
int stdi=(st>>(di*2))&3;//相连旋钮对应状态
//将第i旋钮对应地方使用^清空,再放入扭一次的状态
int dst=st^(sti<<(i*2))^(((sti+1)&3)<<(i*2));
//将相连旋钮对应地方使用^清空,再放入扭一次的状态
dst=dst^(stdi<<(di*2))^(((stdi+1)&3)<<(di*2));
if(!vis[dst]){//如果未出现过
g[dst]=g[st]+1;//次数+1
vis[dst]=true;//记录出现
pre[dst]=st;//记录从哪个状态转移而来
but[dst]=i+1;//使用的是第几个旋钮
q.push(node(dst));
}
}
}
}
void output(int x){
if(x==stzt) return ;
output(pre[x]);
printf("%d ",but[x]);
}
int main(){
for(int i=0,x;i<12;i++){
x=read()-1;
stzt|=(x<<(i*2));//运用二进制存储12个旋钮的状态
for(int j=0;j<4;j++){
nxt[i][j]=read()-1;
}
}
Astar();
printf("%d\n",g[0]);
output(0);
return 0;
}
IDA*算法:启发式迭代加深搜索=A*思想+迭代加深搜索IDDFS
使用迭代加深搜索所枚举的搜索上界 dep 即为总的代价 \(f(x)\) 的最大值;
而从起点到状态 x 的代价 \(g(x)\),即走的步数恰好对应着搜索层数。
而只有 \(f^*(x)=g(x)+h^*(x)≤dep\) 时,该状态才会被本轮搜索拓展。
P2324 骑士精神
题意:5*5的棋盘,给定初始状态和目标状态,只能安装规则移动,最少几步?15步内不行输出-1。
题解:从本状态到终点代价的估计值 \(h^*(x)\) 可以设计为尚未归位的棋子数量,因为每个尚未归位的棋子一定得让开,走至少一步,相当保守的估计。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int m[7][7],flag,dep,sx,sy;
int dx[8]={1,1,-1,-1,2,2,-2,-2};
int dy[8]={2,-2,2,-2,1,-1,1,-1};
string goa="1111101111002110000100000";//目标状态
int h(){//到目标状态的估值
int cnt=0;
for(int i=0;i<5;i++)
for(int j=0;j<5;j++)
if(m[i][j]!=goa[i*5+j]-'0') cnt++;
return cnt;
}
void dfs(int g,int x,int y){//第g层,起点为(x,y)
if(g==dep){
if(!h()) flag=1;//已经到达目标状态
return ;
}
for(int i=0;i<8;i++){//8个方向
int xx=x+dx[i],yy=y+dy[i];//下一步的位置
if(xx<0||xx>=5||yy<0||yy>=5) continue;
swap(m[x][y],m[xx][yy]);//记录现场
if(g+h()<=dep)//启发式搜索关键
dfs(g+1,xx,yy);
swap(m[x][y],m[xx][yy]);//恢复现场
}
}
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int T;cin>>T;
while(T--){
flag=0;
for(int i=0;i<5;i++){
for(int j=0;j<5;j++){
char c;
cin>>c;
if(c=='*')//记录起点
m[i][j]=2,sx=i,sy=j;
else m[i][j]=c-'0';
}
}
for(dep=0;dep<=15;dep++){//迭代加深
dfs(0,sx,sy);
if(flag){//找到了
cout<<dep<<"\n";
break;
}
}
if(!flag) cout<<"-1\n";
}
return 0;
}
总结:两种算法的比较
| A*算法 | IDA*算法 | |
|---|---|---|
| 概括 | 在BFS的基础加上估价函数 | 在IDDFS基础加上估价函数 |
| 应用 | 适用于总体BFS的题目 | 适用于需要迭代加深类的题目 |

