luogu1379:八数码难题(宽搜+哈希表+双向搜索)

题目传送门

    矩阵中经典的宽搜题,这里我主要是想讲哈希表(散列表)的应用而已。

    下面几个知识点都有大神讲解过,有兴趣的同学,请认真阅读,如果你懒得看,我后面也会简单讲解。

    关于sprintf()函数的用法

    关于哈希表的用法

    关于双向搜索

题目大意:

    1、一个3*3的矩阵,给出开始状态和结束状态,请您推箱子(0是那个箱子)

    2、输出推箱子的步数

解题思路:

    1、这是宽搜入门的经典题,经典程度可以媲美 深搜的八皇后问题

    2、首先,要熟悉掌握字符串和int 类型之间的自由切换,这里用了一个 sprintf()函数,就是把数字,输出到字符串中。

    3、宽搜的框架,也是必要的先决条件;

    4、这题最难的地方是判重,在普通地图中,我们用一个bool 数组表示这个点是否到达过,用O(1)的时间就可以判重;

         但这题如果我开bool 数组,用9位数作为数组下标,要开一个 规模 10 亿的数组,空间爆了~

         仔细观察发现,这个10 亿的数组,很多空间我们是用不到的(因为每个数字每次只能出现1次),如果用离散化的思维:

         我们可以把空间压缩在100W内,那么数组就不爆空间了,yeah! 

    5、那么问题来了:怎么压?

         直接mod 100W~(就是这个暴力,这就是本题hash函数的核心)

    6、那么问题又来了,大规模缩小的话,mod完了,还是会有重复,怎么办?

         本题用一个 next数组 来做链表,就搞定了。 

额,哈希的简单应用,并没有那么难。


上代码1:(这是单搜的代码,后面是双搜的代码)

#include
#include
const int mx=1000005,inf=1000003;
int t[mx];
int h[mx];//h的下标就是(哈希值),h数组存的是(这个哈希值,最后一次在队列中出现的位置) 
int next[mx];//next的下标是(队列中的位置),值是(哈希值相同的上一个位置); 
int s,e=123804765;
char st[11],ed[11];
int dx[4]={0,1,0,-1};
int dy[4]={1,0,-1,0};
char l[mx][11];//队列 

int hash(int x)//求队列中(x位置)的 哈希值 
{	
	int su=0;
	for(int i=0;i<9;i++)
	{
		su=su*10+(l[x][i]-'0');
	}
	return su%inf;//就是直接暴力 mod  
}
int pd(int x)//判断 (现在的x位置)是否在队列中出现过 
{
	int s=hash(x);//求出 哈希值
	int k=h[s];//看看哈希值 对应位置的 指针
	while(k>0)// k表示 队列中(以前出现过 这个值 )的位置 
	{
		if(strcmp(l[k],l[x])==0) return 0;//重复了 
		k=next[k];//没重复,链表的跳跃 
	} 
	next[x]=h[s];//记录新的 (最后一个链表位置) 
	h[s]=x; // h数组也更新 
	return 1;
}

int bfs()// 宽搜的框架,不懂请点右上角红叉 
{

	int tou=1,wei=2;
	t[1]=0;// t数组记录步数(本题是从 0 开始)
	 
	memset(h,0,sizeof(h));
	s=hash(1); h[s]=1;//哈希数组 初始化 
	while(tou=0&&nx<3&&ny>=0&&ny<3)
			{ 
				strcpy(l[wei],l[tou]); //推出新的状态 
				l[wei][nk]=l[tou][k];
				l[wei][k]=l[tou][nk];
				
				t[wei]=t[tou]+1; //步数+1
				
				if(strcmp(l[wei],ed)==0) return t[wei];//到终点了 
				
				if(pd(wei)>0) wei++;//判重,如果符合,就进队 
			}
		}
		tou++;
	}
	return 0;
}

int main()
{
	scanf("%s",l[1]); //直接将开始状态读到队列上 
	sprintf(ed,"%d",e);//结束状态转换为 字符串,方便匹配
	 
	if(strcmp(l[1],ed)==0) { printf("0\n");return 0; }//特判 0 步 的状态 
	
	printf("%d\n",bfs());
	
	return 0;	
}




    双向搜索要多开个数组记录方向,我用f 数组,0表示从st出发,1表示从ed 出发,判重函数作了修改:

    情况1:新点与队列中某点重复+两点的方向不同=两条路交叉了,答案!

    情况2:新点与队列中某点重复+两点的方向相同=这个点以前来过,重复走,没意义~

    情况3:新点与队列中无点重复=新加进队列中。

代码2:双向宽搜(理论上,节省一半的运算量)

#include
#include
const int mx=1000005,inf=1000003;
int h[mx],nx[mx],t[mx],f[mx];
char l[mx][11];
int dx[4]={0,1,0,-1};
int dy[4]={1,0,-1,0};

int hash(int x)//暴力求哈希值 
{
	int ans=0;
	for(int i=0;i<9;i++) ans=ans*10+(l[x][i]-'0');
	return ans%inf;
}
int pd(int x,int fa) //fa是父亲节点,需要知道方向和步数 
{
	int xx=hash(x);
	int k=h[xx];
	if(k>0)//说明有人有相同的 哈希值 
	{
		if(strcmp(l[x],l[k])==0)//x 与 k 重复 
		{
			if(f[fa]==f[k]) return -1;// 情况3:自己方向走的重复路:没事情发生
			return t[fa]+t[k]+1; //情况1:不是同边来的相遇,是交汇处,答案有了 
		}
		k=nx[k];
	}
	nx[x]=h[xx]; h[xx]=x; //情况2:无重复,新进队 
	return 0;
}

int bfs()
{
	memset(h,0,sizeof(h));
	int tou=1,wei=3;
	t[1]=t[2]=0;
	f[1]=0;f[2]=1;//判断该点来的方向
	int s1=hash(1); h[s1]=1;
	int s2=hash(2); h[s2]=2;
	
	while(tou=0&&nx<3&&ny>=0&&ny<3)
			{
				int nz=nx*3+ny;
				strcpy(l[wei],l[tou]);
				l[wei][z]=l[tou][nz];l[wei][nz]=l[tou][z];
				
				int su=pd(wei,tou); //这里是双搜的核心,用到父亲节点的信息 
				if(su>0) return su;//如果不同边来的,重合:有答案了
				if(su==0) //如果同边来的,不重复:可以进队 
				{
					f[wei]=f[tou]; t[wei]=t[tou]+1;wei++;
				} 
			}
		}
		tou++; 
	}
}

int main()
{
	scanf("%s",l[1]);strcpy(l[2],"123804765");
	
	if(strcmp(l[1],l[2])==0) { printf("0\n");return 0; }
	printf("%d\n",bfs());
	return 0;
} 



你可能感兴趣的:(题解,宽搜,状态压缩,哈希表,模板题)