【BFS】由八数码问题的BFS解法引出的三种BFS经典状态判重方法

 

问题描述:编号为0~8的正方形滑块被摆成3行3列(一个格子留空,用编号0表示),每次可以把与空格相邻的滑块和空格交换位置,给定初始局面和目标局面,计算出到达目标局面最小需要的移动步数,如果无法到达,则输出-1

 

sample input:

2 6 4 1 3 7 0 5 8
8 1 5 7 3 6 4 0 2

sample output:

31

 

关于八数码问题有很多种经典解法,这里先只讨论使用盲目搜索的BFS

基本思想与迷宫类问题类似,对于每个状态结点需找其0的位置,再向四个方向搜索产生扩展结点(产生扩展结点的条件是未遍历,不越界的合法移动)

这里有两个难点,一个是如何保存当前状态,另一个是如何判断新的状态是否与之前的状态重复

 

保存当前状态我们采用一个一维数组即可,但是下标注意应从0开始,即0~8表示每一个位置的状态,那么下标i的行数为i/3,列数为i%3

值得一提,这里有一个小优化:因为本题会涉及到大量状态比较,即一维数组的标记,当比较两个状态是否相同和复制状态时 我们采用memcmp和memcpy 完成整块内存的比较和复制,就如memset一样是整块操作,比常规循环操作更快

 

对于判重问题是本文的重点内容,我们稍后重点讨论,现给出不涉及判重部分的全部代码:

#include <cstdio>
#include <cstring>

typedef int State[9];		//定义状态的数据类型

const int MAXSTATE=1000000; //定义最大状态数 八数码问题状态数为0~8的全排  即9!=362880
State st[MAXSTATE],goal;    //st保存所有状态 goal保存目标状态
int dist[MAXSTATE];			//距离数组 保存当前状态离初始状态的步数


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




//注意 这里的返回值为目标状态在st数组的下标
int bfs()
{
	init_lookup_table();
	int front,rear;
	dist[1]=0;
	front=1,rear=2;			//这里避免使用下标0 我们把下标0看作不存在解
	while(front<rear)
	{
		State& s=st[front];
		if (memcmp(goal,s,sizeof(s))==0)	//到达终点
		{
			return front;
		}
		int z;
		for (z=0;z<9;z++)		//寻找0的位置
		{
			if (!s[z])
			{
				break;
			}
		}

		int x=z/3;			//获取行号和列号
		int y=z%3;

		for (int d=0;d<4;d++)
		{
			int newx=x+dx[d];
			int newy=y+dy[d];
			int newz=newx*3+newy;			//0的新位置
			if (newx>=0 && newx<3 && newy>=0 && newy<3 )	//移动合法
			{
				State& t=st[rear];
				memcpy(&t,&s,sizeof(s));					//扩展了一个和状态s一样的状态结点
				t[newz]=s[z];
				t[z]=s[newz];								//将s的t和newt位置的编号互换后赋给t 即把0移动
				
				dist[rear]=dist[front]+1;
				if (try_to_insert(rear))					//看新状态能否成功插入 即判重
				{
					rear++;
				}
			}	
		
		}
		front++;
	}


	return -1;		//寻找失败
}




int main()
{
	int i,ans;
	for (i=0;i<9;i++)
	{								//这里下标从0开始 以便获取行列号
		scanf("%d",&st[1][i]);
	}
	for (i=0;i<9;i++)
	{
		scanf("%d",&goal[i]);
	}
	ans=bfs();
	if (ans>0)
	{
		printf("%d\n",dist[ans]);
	}
	else
	{
		printf("-1\n");
	}
	
	return 0;
}

以上代码是除了init_lookup_table初始化查找表函数和try_to_insert判重并插入查找表这两个用于判重的函数没实现以外,其他都是完整的代码,依旧采用数组模拟队列,接下来我们讨论如何判重

 

按照以往的思想,开一个vis的九维数组进行判重,这样数组大小为9^9=387420489项 而排列的所有方式也只有9!即362880种 这样造成了极大的空间浪费,并且在一般情况下会超空间,因此我们需要寻求其他更优秀的判重方法,这里我们给出三种最常用和最经典的判重方式,其方法可以扩展到大多数的BFS问题中

 

方法1:

使用STL的set进行动态判重,创建一个set,每次将当前状态在其中寻找有没有相同的状态,没有则插入其中,每次寻找的时间复杂度是logn,随着set内容的增多,效率会越来越慢

 优点:代码简单,空间消耗小

 缺点:时间较慢,每次查询和插入的复杂度都为O(logn)

为节省空间,我们把每一个状态按位置压缩到一个int变量保存,代码如下:

#include <cstdio>
#include <cstring>
#include <set>

using namespace std;
typedef int State[9];		//定义状态的数据类型

const int MAXSTATE=1000000; //定义最大状态数 八数码问题状态数为0~8的全排  即9!=362880
State st[MAXSTATE],goal;    //st保存所有状态 goal保存目标状态
int dist[MAXSTATE];			//距离数组 保存当前状态离初始状态的步数


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

//用STL的set实现判重
set<int>vis;
void init_lookup_table()
{
	vis.clear();
}

int try_to_insert(int s)
{
	int v=0;
	for(int i=0;i<9;i++)
	{
		v=v*10+st[s][i];
	}
	if (vis.find(v)!=vis.end())
	{
		return 0;
	}
	else
	{
		vis.insert(v);
		return 1;
	}
}


//注意 这里的返回值为目标状态在st数组的下标
int bfs()
{
	init_lookup_table();
	int front,rear;
	dist[1]=0;
	front=1,rear=2;			//这里避免使用下标0 我们把下标0看作不存在解
	while(front<rear)
	{
		State& s=st[front];
		if (memcmp(goal,s,sizeof(s))==0)	//到达终点
		{
			return front;
		}
		int z;
		for (z=0;z<9;z++)		//寻找0的位置
		{
			if (!s[z])
			{
				break;
			}
		}

		int x=z/3;			//获取行号和列号
		int y=z%3;

		for (int d=0;d<4;d++)
		{
			int newx=x+dx[d];
			int newy=y+dy[d];
			int newz=newx*3+newy;			//0的新位置
			if (newx>=0 && newx<3 && newy>=0 && newy<3 )	//移动合法
			{
				State& t=st[rear];
				memcpy(&t,&s,sizeof(s));					//扩展了一个和状态s一样的状态结点
				t[newz]=s[z];
				t[z]=s[newz];								//将s的t和newt位置的编号互换后赋给t 即把0移动
				
				dist[rear]=dist[front]+1;
				if (try_to_insert(rear))					//看新状态能否成功插入 即判重
				{
					rear++;
				}
			}	
		
		}
		front++;
	}


	return -1;		//寻找失败
}




int main()
{
	int i,ans;
	for (i=0;i<9;i++)
	{								//这里下标从0开始 以便获取行列号
		scanf("%d",&st[1][i]);
	}
	for (i=0;i<9;i++)
	{
		scanf("%d",&goal[i]);
	}
	ans=bfs();
	if (ans>0)
	{
		printf("%d\n",dist[ans]);
	}
	else
	{
		printf("-1\n");
	}
	
	return 0;
}

 对于其他的大多数问题,很难像上面一样把一个状态转化为一个int值进行比较并存储到set中,我们需要自己定义状态之间的比较,重载运算符

代码如下:

struct cmp
{
	bool operator()(int a,int b)const
	{
		return memcmp(&st[a],&st[b],sizeof(st[a]))<0;
	}
};
set<int,cmp>vis;

int try_to_insert(int s)
{

	if (vis.find(s)!=vis.end())
	{
		return 0;
	}
	else
	{
		vis.insert(s);
		return 1;
	}
}
void init_lookup_table()
{
	vis.clear();
}

注意:对于STL容器,为了节省空间的开销,我们往往只将结构体的下标存入STL容器中,而不直接存整个结构体,因此在重载比较函数的时候要稍微注意一下,可以参照上面的代码进行重载

而这种情况对于此题从时间上显然会比之前压缩到一个int中的情况要慢很多,因为每次比较是整块的比较,因此在大多数对时间要求紧迫的情况下,我们不能使用set

 

方法2::

接下来给大家介绍一种更加常用和主流的方法——哈希(hash)技术

首先,建立一个大小为MAXHASHSIZE的哈希链表

然后,通过某些计算方式,将一个状态映射成一个范围在0~MAXHASHSIZE(哈希表的最大大小)中的值,而hash表的每一条链表就储存hash值相同的状态

查找的时候就将该状态转化为hash值并在相应的链表中搜寻,插入的时候则插入到对应的hash值的链表中即可

注意,我们通常用两个数组head和next实现,状态的数量不超过head和next数组的总空间

而hash技术的关键在于hash函数的设计,因为一个hash值对应的状态数越少,那么效率则越高

 

代码如下:

//用Hash实现判重
const int MAXHASHSIZE=1000003;

int head[MAXHASHSIZE],next[MAXHASHSIZE];
void init_lookup_table()
{
	memset(head,0,sizeof(head));
}
int hash(State& s)	//哈希函数 将一个状态转化为一个哈希值 一个哈希值对应的状态数越少越好
{
	int v=0;
	for (int i=0;i<9;i++)
	{
		v=v*10+s[i];
	}
	return v%MAXHASHSIZE;
}

int try_to_insert(int s)
{
	int h=hash(st[s]);
	int u=head[h];			//这里是一个hash链表 
	//head[i]表示哈希值为i的第1个点的编号 而next[i]表示结点i在链表里的下一个结点的编号,值为0表示到了链尾
	while(u)
	{
		if (memcmp(st[u],st[s],sizeof(st[u]))==0)
		{
			return 0;
		}
		u=next[u];
	}
	next[s]=head[h];	//将该结点从链头插入
	head[h]=s;
	return 1;
}

这段程序的代码会在空间上造成额外的开销,但是当hash函数设计得当时,它的查找和插入的时间复杂度是O(1)的

 

方法3:

接下来介绍最后一种方法,即编码解码法

即将所有状态通过某种运算编码成一一对应的数字,然后通过逆运算将数字转换为相应的状态,其实在方法1的第一种set方法中,我们就使用了编码技术,当我们通过编码技术找到一套编码方案可以把所有的状态数(对于八数码问题状态数为362879个)编码,并且编码的范围不大,那么我们只需要开一个一维数组,就能实现O(1)的查找和插入

从某种意义上来看,编码解码法就相当于是hash的一种变体,可以理解为完美hash

下面给出八数码的编码解码法供大家参考:

int vis[36288],fact[9];
void init_lookup_table()	//初始化查找表,主要是初始化vis数组和fact数组
{
	memset(vis,0,sizeof(vis));
	fact[0]=1;
	for (int i=1;i<9;i++)
	{
		fact[i]=fact[i-1]*i;
	}
}

int try_to_insert(int x)
{
	int code=0;
	for (int i=0;i<9;i++)
	{
		int cnt=0;
		for (int j=i+1;j<9;j++)
		{
			if (st[x][j]<st[x][i])
			{
				cnt++;
			}
		}
		code+=fact[8-i]*cnt;
	}
	if (vis[code])
	{
		return 0;
	}
	else
	{
		vis[code]=1;
		return 1;
	}
}

 

 

 

    原文作者:BFS
    原文地址: https://blog.csdn.net/lth404391139/article/details/40391401
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞