关于启发式搜索

一.定义:

顾名思义,启发式搜索肯定不是普通的搜索,而是在普通的搜索上做了一些启发,进行了一些优化。
一般启发式搜索分为两种类型,一种是优化广度优先搜索的 \(A\) * 算法,一种是优化深度优先搜索的 \(IDA\) * 算法

二.基本函数:

1.距离函数 \(g(x)\)
用来计算当前点到起点的距离,也就是已消耗的代价。
2.估价函数 \(h(x)\)
用来估计最优情况下,从当前点到终点所要走的距离,也就是所要消耗的代价。
3.计算函数 \(f(x)\)
计算距离函数和估价函数的和值。

三.图例分析:

\(A\)*算法的操作步骤如图所示,这样可以大大缩减爆搜的时间复杂度

但是有一些极端的特殊情况需要注意:

image

这里规定圆圈为障碍,五角星为起点,六角星为终点,黑色方框为边界。
可以看到,这里下面是有一个边界的,所以唯一的路线如下:

image

但是按照启发式搜索,估价函数是不考虑障碍的,所以它依然会往右下扫,扫到障碍再回来,这里我们用红叉表示遍历到的点,那么过程如下:

image

显然,启发式搜索是无法将障碍考虑进来的,所以,它会将全图都扫一遍,此时,它就没有对普通搜索再做优化,甚至还要比普通广搜更慢。
因此,当可能有这种极端情况时,我们要做一些特殊判断。

四.注意事项:

这里一定要注意估价一定要小于等于实际值,因为若估价比实际大,在某些情况下,可能实际值更大的反而被取出来就会导致答案错误。

典型例题

例一 一本通1251 仙岛求药

这题虽然用爆搜随便就能做出来,数据范围很小,但用来练习启发式搜索还是可以的。
下面展示一下启发式搜索用结构体形式实现的代码。

Code

#include<bits/stdc++.h>
using namespace std;

int n,m,ex,ey;

char s[25][25]; 
bool vis[25][25];  

struct Node
{
	int x;
	int y; 
	int g;
	int h;
	int f;
	bool operator<(const Node b)const
	{
		return f>b.f;
	}
}start;

int dx[]={0,0,1,-1};  
int dy[]={1,-1,0,0}; 

inline int bfs()
{
	memset(vis,false,sizeof(vis));
	priority_queue<Node>q;
	start.g=0;
	start.h=abs(start.x-ex)+abs(start.y-ey);
	start.f=start.g+start.h;
	q.push(start);
	vis[start.x][start.y]=true;
	while(!q.empty())
	{
		Node now=q.top();
		q.pop();
		for(register int i=0;i<=3;i++)
		{
			int nx=now.x+dx[i];
			int ny=now.y+dy[i];
			if(nx<1||ny<1||nx>m||ny>n||s[nx][ny]=='#'||vis[nx][ny]==true)
				continue;
			if(s[nx][ny]=='*')
				return now.g+1;
			Node nxt;
			nxt.x=nx;
			nxt.y=ny;
			nxt.g=now.g+1;
			nxt.h=abs(nxt.x-ex)+abs(nxt.y-ey);
			nxt.f=nxt.g+nxt.h;
			q.push(nxt);
			vis[nxt.x][nxt.y]=true; 
		}
	}
	return -1;
}


int main()
{
	while(cin>>m>>n)
	{
		if(m==0&&n==0)
			return 0;
		for(register int i=1;i<=m;i++)
		{
			for(register int j=1;j<=n;j++)
			{
				cin>>s[i][j];
				if(s[i][j]=='@')
					start.x=i,start.y=j;
				if(s[i][j]=='*')
					ex=i,ey=j;
			}
		}
		
		printf("%d\n",bfs());
	}
	return 0;
}

这里有可以优化的点,看到数据范围 \(n,m \le 20\) 就可以想到用一个顶多四位的数来表示它的状态,随即可以将标记数组 \(vis_i\) 优化为一维。

Code

#include<bits/stdc++.h>
using namespace std;

int n,m,e;

char s[25][25]; 
bool vis[2025];  

struct Node
{
	int state; 
	int g;
	int h;
	int f;
	bool operator<(const Node b)const
	{
		return f>b.f;
	}
}start;

int dx[]={0,0,1,-1};  
int dy[]={1,-1,0,0}; 

inline int bfs()
{
	memset(vis,false,sizeof(vis));
	priority_queue<Node>q;
	start.g=0;
	start.h=abs(start.state/100-e/100)+abs(start.state%100-e%100);
	start.f=start.g+start.h;
	q.push(start);
	vis[start.state]=true;
	while(!q.empty())
	{
		Node now=q.top();
		q.pop();
		for(register int i=0;i<=3;i++)
		{
			int nx=now.state/100+dx[i];
			int ny=now.state%100+dy[i];
			if(nx<1||ny<1||nx>m||ny>n||s[nx][ny]=='#'||vis[nx*100+ny]==true)
				continue;
			if((nx*100+ny)==e)
				return now.g+1;
			Node nxt;
			nxt.state=nx*100+ny;
			nxt.g=now.g+1;
			nxt.h=abs(nx-e/100)+abs(ny-e%100);
			nxt.f=nxt.g+nxt.h;
			q.push(nxt);
			vis[nxt.state]=true; 
		}
	}
	return -1;
}


int main()
{
	while(cin>>m>>n)
	{
		if(m==0&&n==0)
			return 0;
		for(register int i=1;i<=m;i++)
		{
			for(register int j=1;j<=n;j++)
			{
				cin>>s[i][j];
				if(s[i][j]=='@')
					start.state=i*100+j;
				if(s[i][j]=='*')
					e=i*100+j;
			}
		}
		
		printf("%d\n",bfs());
	}
	return 0;
}

例二 P1379 八数码难题

这道题一样是能够用搜索+剪枝做出来,但用来练启发式是不错的选择。
这道题的状态非常恶心,很容易搞混,我们可以用一个大小为9的数组去存状态,然后每个数字就在第下标 \(/3\) 行,第下标 \(\%3\) 列,这里可以相对理解成0在移动,然后将两数互换,估价就直接求曼哈顿距离就行了。
这里还要用到一个叫康托展开的技巧,应该都知道就不讲了。

Code

#include<bits/stdc++.h>
using namespace std;

struct Node{
	int a[9]; 
	int g;  
	int xpos;  
	int h;  
	int f;  
	bool operator < (const Node b)const 
	{
		return f > b.f;  
	}
}start, e;
int endcantor;  
bool vis[362885]; 
int flag[15], fac[15];   

int dx[]={0,0,1,-1};  
int dy[]={1,-1,0,0}; 
int man(Node x)
{
	int sum=0;   
	for(int i=0; i<=8; i++)
	{
		int nowx=x.a[i]/3, nowy=x.a[i]%3;  
		int ex=flag[x.a[i]]/3, ey=flag[x.a[i]]%3;  
		sum+=abs(nowx-ex)+abs(nowy-ey);  
	}
	return sum;  
}

int cantor(Node x)
{
	int sum=0;  
	for(int i=0; i<=8; i++)
	{
		int cnt=0;  
		for(int j=i+1; j<=8; j++)
		{
			if(x.a[i]>x.a[j])
			{
				cnt++;  
			}
		}
		sum += cnt*fac[8-i];  
	}
	return sum;  
}


int bfs()
{
	memset(vis, 0, sizeof(vis));  
	priority_queue<Node> q;  
	start.g=0;  
	start.h=man(start);   
	start.f=start.g+start.h;  
	vis[cantor(start)]=true;  //状态标记为true
	q.push(start);  
	while(!q.empty())
	{
		Node now = q.top();  q.pop();  
		int nowx=now.xpos/3, nowy=now.xpos%3;  
		for(int i=0; i<=3; i++)
		{
			int nx = nowx+dx[i];  
			int ny = nowy+dy[i];  
			if(nx<0 || ny<0||nx>2 ||ny>2)continue;  
			Node nxt=now;  
			swap(nxt.a[now.xpos], nxt.a[nx*3+ny]);  //原来的0和现在的0位置互换 
			int nxtcantor=cantor(nxt);  
			if(vis[nxtcantor]==true)continue;  
			nxt.g=now.g+1;  
			nxt.h=man(nxt);  
			nxt.f=nxt.g+nxt.h;  
			nxt.xpos=nx*3+ny;   
			if(nxtcantor==endcantor)
				return nxt.g;  
			q.push(nxt);  
			vis[nxtcantor]=true;  
		}
	}
	return 0;   
}

int main()
{
	fac[0]=1;  
	for(int i=1; i<=8; i++)
	{
		fac[i]=fac[i-1]*i;  
	} 
	string s="123804765";  
	for(int i=0; i<=8; i++)
	{
		flag[s[i]-'0']=i;  
		e.a[i]=s[i]-'0';  
	}
	endcantor=cantor(e);  
	char c;  
	for(int i=0; i<=8; i++)
	{
		cin>>c;  
		start.a[i]=c-'0';  
		if(start.a[i]==0)
			start.xpos=i;  
	}
	cout<<bfs();  
	return 0;
}


例三 P5507 机关

这道题的状态就非常的恶心了,这道题的估价函数可以用当前旋钮的状态到目标状态还要几次操作作为h的值来算。
这道题最恶心的就是关于它的位运算了,具体在代码注释里提及到。

#include <bits/stdc++.h>
using namespace std;
const int MAXN=2e7+5;

inline int read()
{
	register int x=0,f=0;register char ch=getchar();
	while(ch<'0' || ch>'9')f|=ch=='-',ch=getchar();
	while(ch>='0' && ch<='9')x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
	return f?-x:x;
}//快读 

struct Node
{
	int state;
	int g;
	int h;
	int f;
	bool operator<(const Node &b)const
	{
		return f>b.f;
	}
};//用来做优先队列和小方块的结构体 

int start,nx[15][5],choice[MAXN],fa[MAXN],ans[20];//choice是记录每次选择转动的旋钮,fa记录他的父亲 
bool vis[MAXN];//标记数组 

inline int cal(int state)//估价函数 
{
	int sum=0;
	for(register int i=0;i<=11;i++)//循环从0开始,包括后面,会更方便 
	{
		int v=(state>>(i*2))&3;//将所有旋钮的状态一一取出 
		if(v!=0)
		{
			sum+=4-v;//当前状态到目标状态需要的操作次数差 
		}
	}
	return sum/2;//由于旋转一个按钮会带动另一个按钮,所以次数除以2 
}

inline int A_Star()//A_Star搜索 
{
	priority_queue<Node>q;//优先队列 
	Node s;
	s.state=start;
	s.g=0;
	s.h=cal(start);
	s.f=s.h+s.g;
	q.push(s);
	vis[start]=true;
	while(!q.empty())
	{
		Node now=q.top();
		q.pop();
		int nows=now.state;//原来的状态 
		int newstate;//新的状态
		for(register int i=0;i<=11;i++)
		{
			int istate=(nows>>(i*2))&3;//第i个旋钮的状态
			int nxt=nx[i][istate];//被带动旋转的旋钮的编号
			int nxtstate=(nows>>(nxt*2))&3;// 被带动旋转的旋钮的状态
			newstate=(nows^(istate<<(i*2)))|(((istate+1)&3)<<(i*2));//将第i个旋钮原来的状态清零并改为现在的状态 
			newstate=(newstate^(nxtstate<<(nxt*2)))|(((nxtstate+1)&3)<<(nxt*2));//将被带动旋转的旋钮也进行上述操作
			if(vis[newstate]==true)
				continue;
			Node tmp;//常规操作修改(小方块) 
			tmp.state=newstate;
			tmp.g=now.g+1;
			tmp.h=cal(newstate);
			tmp.f=tmp.g+tmp.h;
			vis[newstate]=true;//记录已走过 
			choice[newstate]=i;
			fa[newstate]=nows;
			if(newstate==0)//如果已满足结果状态,就返回当前已用次数 
				return tmp.g;
			q.push(tmp);//将小方块塞进队列 
		} 
	}
}

int a;

int main()
{
	for(register int i=0;i<=11;i++)
	{
		a=read();
		start|=((a-1)<<(i*2));//将每个旋钮的初始状态合并为一个数
		for(register int j=0;j<=3;j++)
		{
			nx[i][j]=read();
			nx[i][j]--;//由于下标从0开始,所以要减一 
		} 
	}
	printf("%d\n",A_Star());//A*搜索 
	int now=0,anscnt=0;
	while(now!=start)
	{
		anscnt++;
		ans[anscnt]=choice[now];
		now=fa[now];
	}//将答案转化为题目格式 
	for(register int i=anscnt;i>=1;i--)
		printf("%d ",ans[i]+1);//按题目格式输出答案 
	return 0;
}

例四 P4467 k短路弱化版

由于本蒟蒻太菜了,只能做出弱化版的(普通版就是让你输出k短路径,弱化版就输出k短路径的长度)...
其实思路并不是特别难,就是从起点开始往后搜,然后像之前的搜索一样记录一下已花费的距离g和到终点的估价h(注意这里h是当前点到终点的最短路径长度,所以要用Dijst)
然后就弄一个sum,表示它是第几个到达终点的,如果等于k,就直接输出结果。
由于POJ提交无法使用万能头,所以头文件改一下...

#include <bits/stdc++.h>
using namespace std;
const int MAXN=1e3+5;
const int MAXM=1e5+5;

inline int read()
{
	register int x=0,f=0;register char ch=getchar();
	while(ch<'0' || ch>'9')f|=ch=='-',ch=getchar();
	while(ch>='0' && ch<='9')x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
	return f?-x:x;
}

struct Node
{
	int to,nxt,len;
}e[MAXM];

int head[MAXN],cnt;
int n,m,k,s,E,sum[MAXN];

inline int add(int x,int y,int z)//链式前向星 
{
	e[++cnt].to=y;
	e[cnt].len=z;
	e[cnt].nxt=head[x];
	head[x]=cnt;
}

struct DijNode//Dijkstra结构体 
{
	int id,d;
	bool operator<(const DijNode &b)const
	{
		return d>b.d;
	}
};

int dis[MAXN],vis[MAXN];//dis为最短距离,vis为标记数组 

inline void dij(int st)//Dijkstra运算函数 
{
	memset(dis,0x3f,sizeof(dis));
	memset(vis,false,sizeof(vis));
	priority_queue<DijNode>q;
	q.push({st,0});
	dis[st]=0;//赋初始值 
	while(!q.empty())
	{
		int x=q.top().id;//取出队首 
		q.pop();
		if(vis[x]==true)//若走过,则跳过不管 
			continue;
		vis[x]=true;//标记已走过 
		for(register int i=head[x];i;i=e[i].nxt)//链式前向星循环 
		{
			int y=e[i].to,z=e[i].len;
			if(dis[y]>dis[x]+z)//若此中转点更短 
			{
				dis[y]=dis[x]+z;//中转点赋为更短值 
				q.push({y,dis[y]});//将此点放入队列 
			}
		}
	}
} 

struct ANode//A*结构体 
{
	int x;
	int g;
	int h;
	int f;
	bool operator<(const ANode &b)const
	{
		return f>b.f;
	}
};

inline int A_Star(int st)//A*运算函数 
{
	priority_queue<ANode>q;
	ANode start;
	start.g=0;//起点消耗为0 
	start.h=dis[s];//预估为走到终点的最短路的距离 
	start.f=start.g+start.h;//估价+已消耗 
	start.x=s;//起点为s 
	q.push(start);
	while(!q.empty())
	{
		ANode now=q.top();//取出队首 
		q.pop();
		sum[now.x]++;
		if(now.x==E&&sum[now.x]==k)//若已到终点 
			return now.g;
		if(sum[now.x]>k) 
			continue;
		for(register int i=head[now.x];i;i=e[i].nxt)//链式前向星循环 
		{
			int y=e[i].to,z=e[i].len;//y为目前点,z为连边的长度 
			ANode nxt;
			nxt.g=now.g+z;//目前消耗=上个点的消耗+连边的长度 
			nxt.h=dis[y];//估价=目前到终点最短路的距离 
			nxt.f=nxt.g+nxt.h;//估价+已消耗 
			nxt.x=y;//下一次访问的起始点设为目前点 
			q.push(nxt);//将其放入队列 
		}
	}
	return -1;
}

int main()
{
	n=read(),m=read();
	int x[MAXM],y[MAXM],z[MAXM];
	for(register int i=1;i<=m;i++)
	{
		x[i]=read(),y[i]=read(),z[i]=read();
		add(y[i],x[i],z[i]);//反向建图 
	}
	s=read(),E=read(),k=read();
	dij(E);//反向遍历求最短路 
	cnt=0;
	memset(head,0,sizeof(head));
	for(register int i=1;i<=m;i++)
		add(x[i],y[i],z[i]);//做完最短路再正向建图 
	if(s==E)//若遍历到终点就标记 
		k++;
	printf("%d\n",A_Star(s));//A*搜索 
	return 0;
}

posted @ 2022-05-12 22:14  Code_AC  阅读(809)  评论(0)    收藏  举报