A*与IDA*的奇妙之旅

因为A*卡了一天QAQ

那么,A*是什么呢?

A*

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

例如下图:
在这里插入图片描述

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

在这里插入图片描述

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

Dij不会打全称

在这里插入图片描述

那么,A*是怎样的呢?
在这里插入图片描述

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

那么,这张图呢?
A*
在这里插入图片描述

像这种图,如果用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?
在这里插入图片描述

不过,特别舒服的事情是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*的例题(比较难)

注: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;
}
posted @ 2018-10-31 11:04  敌敌畏58  阅读(350)  评论(0编辑  收藏  举报