A*与IDA*的奇妙之旅

时间:2021-11-23 03:27:15

因为A*卡了一天QAQ

那么,A*是什么呢?

A*

A*是对于bfs的优化,启发式搜索。

例如下图:

A*与IDA*的奇妙之旅

不错,在这张图上,小人去找电脑,用bfs的话:

A*与IDA*的奇妙之旅

黄色就是bfs的搜索范围。。。不要问我为什么选黄色

Dij不会打全称

A*与IDA*的奇妙之旅

那么,A*是怎样的呢?

A*与IDA*的奇妙之旅

没看出区别?我都看不出区别

那么,这张图呢?

A*

A*与IDA*的奇妙之旅

像这种图,如果用bfs与Dij都会。。。

但是A*的启发式搜索就体现在这。

首先定义函数:

\[F(x)=G(x)+H(x)
\]

\(G(x)\)表示已经走过的路,\(H(x)\)表示期望走的路。

不难看,\(G(x)\)很容易想到,但是\(H(x)\)就十分难受了(╯﹏╰)。

例如,如果只允许你走上下左右,那么切莫雪夫距离当期望值再好不过了。

\(H(x)\)一定要比实际值小,大的话反而容易淡化\(G(x)\)的作用。

建一个小根堆,将找到的\(F(x)\)存进去,这个堆叫"OPEN"

每次取出堆顶,然后把堆顶删除,同时把他丢到"CLOSE"列表里。

我们要承偌丢到"CLOSE"列表里的数不再更新。

然后搜索上下所有四格:

已经在"CLOSE"里面的,不去理他。

不在"OPEN"里面的,加进来。

已经在“OPEN”里的,判断一下是否更优,更新。这里貌似没用

当搜到终点时,结束。

已经在“OPEN”里的,判断一下是否更优,更新。

这句话有没有用?

样例

当八个方向都可以时,就不大一样了,尤其是当走的代价不一样时,这个的作用就很明显了。

为什么终点搜到就可以退出了?大概因为\(H(x)\)都是估计\(x\)到\(终点\)的吧(而且\(H(x)\)也比真实值小的情况下)。

不过A*的具体步骤还是看情况定

那么,对于\(H(x)\)还有一个要注意的点。

首先不用说,小于实际值(否则\(G(x)\)的作用被淡化的话你就废了)。

但是如果\(G(x)\)取的不好,反而会负优化,所以\(G(x)\)尤其重要,尤其是\(G(x)\)刚好等于实际值时,A*就跑得飞快了!

而且A还有个优化,当许多\(F(x)\)都相同时,取\(G(x)\)最大大的那一个,这样能让A避免扩展了多条路径。

IDA*

BFS加迭代加深?

迭代加深是什么:

在DFS的搜索里面,可能会面临一个答案的层数很低,但是DFS搜到了另为一个层数很深的分支里面导致时间很慢,但是又卡BFS的空间,这个时候,可以限制DFS的搜索层数,一次又一次放宽搜索的层数,知道搜索到答案就退出,时间可观,结合了BFS和DFS的优点于一身。

bool  bk;
void dfs(int limit,int x)
{
if(...)
{
bk=true;
return ;
}
if(x==limit)return ;
for(...)dfs(limit,x+1);
}
for(int i=1;i<=100;i++)
{
dfs(i,0);
if(bk==true)return 0;
}

这也是个简洁的迭代加深,简称ID算法。

那么IDA=A+ID?

不是。难道12=1+2?

A*与IDA*的奇妙之旅

不过,特别舒服的事情是IDA是对与A的优化,甚至比A*还好打!

IDA*就是将\(F(x)\)搬到了IDDFS上然后加了个可行性剪枝

例题

这题目,不大难,难在了\(H(x)\)怎么做。

由于骑士要从现有状态恢复到目标状态,DFS稳了!

首先DFS不能让马走,否则状态多,应该让空格走,状态会少很多

又要求15步,IDDFS,稳了!虽然我还是傻愣愣的将0到15每次都迭代加深。

然后,我们再判断一下,在最有情况下,当一个位置的马与目标状态不一样时,那么就cnt++,期望这个马可以一个回归原位,不过在IDDFS里面的判断就不是\(if(x+ex()>limit)\)了(\(ex()\)代表期望函数)。

而是if(x+ex()>limit+1)为什么呢?因为在ex()里面我把空格也计算了一遍,而在最后一步,是可以将空格与马同时回归原位的,所以要让limit+1。

那么,代码君:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int st[7][7]=
{
{3,3,3,3,3,3,3},
{3,1,1,1,1,1,3},
{3,0,1,1,1,1,3},
{3,0,0,2,1,1,3},
{3,0,0,0,0,1,3},
{3,0,0,0,0,0,3},
{3,3,3,3,3,3,3}
};//目标状态
int map[7][7];//记录当前状态
inline int ex()//期望函数
{
int tot=0;
for(int i=1;i<=5;i++)
{
for(int j=1;j<=5;j++)
{
if(map[i][j]!=st[i][j])tot++;
}
}
return tot;
}
inline void wap(int &x,int &y){x^=y^=x^=y;}//交换函数swap
int dx[]={-2,-2,-1,1,2,2,1,-1};
int dy[]={-1,1,2,2,1,-1,-2,-2};//方向
bool dfs(int limit,int x,int mx,int my)
{
if(x==limit)
/*果已经到界限,判断一下,
为什么不用担心x<limit的情况呢,如果有,在上几次DFS就已经解决了。 */
{
return !ex();
}
if(x+ex()>limit+1)return false;//IDA*的可行性剪枝
for(int t=0;t<=7;t++)//空格的发个方向
{
int tx=mx+dx[t],ty=my+dy[t];//新的空格位置
if(tx<1 || tx>5 || ty<1 || ty>5)continue;//判断
wap(map[mx][my],map[tx][ty]);//交换
if(dfs(limit,x+1,tx,ty))// dfs(limit,x+1,tx,ty)==true
{
//找到了答案
wap(map[mx][my],map[tx][ty]);//交换
return true;
}
wap(map[mx][my],map[tx][ty]);//回朔
}
return false;
}
int main()
{
int T;scanf("%d",&T);
while(T--)
{
char ss[10];
int tx=0,ty=0;
for(int i=1;i<=5;i++)//构建map
{
scanf("%s",ss+1);
for(int j=1;j<=5;j++)
{
if(ss[j]=='1')map[i][j]=1;
else if(ss[j]=='0')map[i][j]=0;
else map[i][j]=2,tx=i,ty=j;//记录空格位置
}
}
bool bk=false;
for(int i=0;i<=15;i++)//0代表一开始就是目标状态
{
bk=dfs(i,0,tx,ty);
if(bk)//bk==true;
{
printf("%d\n",i);
break;//输出并break
}
}
if(!bk)printf("-1\n");//判断
}
return 0;
}

如何确定估值

估值函数的确定方法在我做完一道题目以后,突然心血来潮,发现目前做的几道题目貌似都是这样。

  1. 估值函数一般是当状态离目标越近时越优,当然是总体趋势,存在个别的。
  2. 估值函数里的参数一般是比较明显的,且每次操作后一般会改变的,一般也会满足第一点。
  3. 参数一般变化是有范围的,参考题目2。

题目

1

A*的例题(比较难)

因为luogu卡A*,给出bzoj的链接。

不就是计算k最大能是多少

反向建边,\(D(x)\)代表\(n\)到\(x\)的最短路

\[H(x)=D(x)
\]

首先有个优化,设\(kk=能量/D(1),(k≤kk)\)为什么?因为就算你条条最短路,你最多也只能扩展kk条!

那么的话呢,我们从起点开始,每次将周围的点塞进堆里面,这里什么时候丢进CLOSE列表呢,仅当一个数被访问了kk次。

而且这里一个点是可以多次被丢进OPEN列表里的,总之A*的步骤就不太一样就对了。

每次取出堆顶然后更新,当访问到终点时记录一下,然后。。。

代码君!可恶的BZOJ,卡我priority_queue,逼我手写:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
//边目录
struct bian
{
int y,next;
double c;
}v1[210000]/*正向建图*/,v2[210000]/*反向建图*/;int last1[6000],last2[6000],vlen;
int n,m;double QWQ;//能量
inline void ins(int x,int y,double c)
{
vlen++;
v1[vlen].y=y;v1[vlen].c=c;v1[vlen].next=last1[x];last1[x]=vlen;
v2[vlen].y=x;v2[vlen].c=c;v2[vlen].next=last2[y];last2[y]=vlen;
}
//A*判断时所用
struct node
{
int d;//d表示在哪个点
double f,h;//f表示总估值,h表示实际走的长度
}now,fut;//在A*里面当中转的时候用
inline bool operator<(node x,node y){return x.f==y.f?x.h>y.h:x.f<y.f;}//重载
//手写堆。。。
node a[1510000];int len,ans=0;
inline void insert(node x)//插入
{
a[++len]=x;//放进最后一个
int i=len/2,j=len;
while(i>=1)//向上调整
{
if(a[j]<a[i])
{
node tt=a[j];a[j]=a[i];a[i]=tt;//交换
j=i;i=j/2;
}
else break;//退出
}
}
inline void del()//删除堆顶
{
a[1]=a[len];len--;//将a[len]移到堆顶,达到将a[1]删除的目的
int i=1,j=2;
while(j<=len)//向下调整
{
if((j+1)<=len && a[j+1]<a[j])j++;//找两个儿子最小的来做对比
if(a[j]<a[i])//对比
{
node tt=a[j];a[j]=a[i];a[i]=tt;//交换
i=j;j=i*2;
}
else break;//退出
}
}
//SPFA
double d[6000];
int list[6000],head,tail;
bool bol[6000];
void spfa()
{
for(int i=1;i<n;i++)d[i]=999999999.0;
head=1;tail=2;list[head]=n;bol[n]=true;//初始化
while(head!=tail)
{
int x=list[head++];bol[x]=false;
if(head==n+1)head=1;//循环队列优化
for(int k=last2[x];k;k=v2[k].next)
{
int y=v2[k].y;
if(d[x]+v2[k].c<d[y])//判断
{
d[y]=d[x]+v2[k].c;
if(!bol[y])
{
bol[y]=true;
list[tail++]=y;if(tail==n+1)tail=1;
}
//别看了,没有酸辣粉(SLF优化)
}
}
}
}
//A*
int cnt[6000];
void A_star(int kk)//计算k最大为多少
{
now.d=1;now.h=0;now.f=d[1];
insert(now);//插入起点
while(len)//len!=0,堆不为空
{
now=a[1];del();//删除堆顶
cnt[now.d]++;//计算 次数
if(now.f>QWQ)break;//最小的F值都大于能量值,只能退出了。
if(now.d==n)//到达了终点
{
ans++;
QWQ-=now.f;//消耗能量
}
if(cnt[now.d]>kk)continue;//CLOSE列表
for(int k=last1[now.d];k;k=v1[k].next)
{
fut.d=v1[k].y;fut.h=now.h+v1[k].c;fut.f=fut.h+d[fut.d];//加入新的点
insert(fut);//加入进去
}
}
}
int main()
{
scanf("%d%d%lf",&n,&m,&QWQ);
for(int i=1;i<=m;i++)
{
int x,y;double c;scanf("%d%d%lf",&x,&y,&c);
ins(x,y,c);
}
spfa();A_star(QWQ/d[1]);//一个优化
printf("%d\n",ans);//输出
return 0;
}
//强行加注释。。。

2

简单毒瘤题

这道题目我们可以认为把一段放入另外一个地方代表的是两段交换位置。

(这里的后继为每个数后面的一个数)

那么理论上来讲,最多只有三个数会改变后继。

那么在假设改变都是正确的情况下,那么最小步数就是ceil(错误的后继数量/3)。

也就是估值函数。

又回到我们确定的方法:越接近目标状态,错误的后继数量越少,每次操作都会改变,而且改变的数的绝对值为3(因为改变以后有的)。

#include<cstdio>
#include<cstring>
#define N 20
#define NN 400
using namespace std;
int qwq[NN],n;
int ex(int a[])//期望函数
{
int ans=0;
for(int i=1;i<n;i++)
{
if(a[i+1]!=a[i]+1)ans++;
}
return ans;
}
int list[NN];
void huan(int a[],int l,int r,int x)//将[l,r]插到x后面
{
for(int i=l;i<=r;i++)list[i]=a[i];
for(int i=r+1;i<=x;i++)a[i-r+l-1]=a[i];
for(int i=l+(x-r);i<=x;i++)a[i]=list[i-x+r];
}
int lim;
int dfs(int a[],int ans)//只允许把每一段插到自己后面的位置
{
int now=ex(a);
if(ans+now/3+(now%3>0)>=lim)return 5;
else if(!now)return ans;
for(int i=1;i<=n;i++)
{
for(int k=i;k<=n;k++)//找区间
{
for(int j=k+1;j<=n;j++)//找插的位置
{
memcpy(a+n+2,a+1,(n<<2));//部分copy大法好
huan(a+n+1,i,k,j);
now=dfs(a+n+1,ans+1);
if(now<lim)lim=now;
}
}
}
return lim;
}
inline int mymin(int x,int y){return x<y?x:y;}
int main()
{
int T;scanf("%d",&T);
while(T--)
{
lim=5;
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&qwq[i]);
lim=mymin(dfs(qwq,0),lim);
if(lim==5)printf("5 or more\n");
else printf("%d\n",lim);
}
return 0;
}