搜索---从初始状态到目标状态(学习笔记)

对于这种"求从初始状态到目标状态的步数"的搜索题,BFS是较好的选择.

移动玩具

\(4*4\)的正方形内每个格子上是数字0或1,要由初始状态移动到目标状态,每次移动只能上下左右四个方向移动,求最少的移动次数

对比两幅图(初始状态和目标状态),对于数字一样的格子(同为0或者同为1),在两幅图上都赋值为0;然后对于初始状态图中剩下未匹配的点(即该点在两幅图中的值不同),开一个结构体记下横纵坐标.

四维数组\(f[i][j][k][l]\)记录的是\([i,j]\)\([k,l]\)两点之间的曼哈顿距离(即横纵坐标之差的绝对值的和),表示\([i,j]\)要移动到\([k,l]\)需要多少次移动.

int sum,ans=1e9;
int a[5][5],b[5][5],f[5][5][5][5],visit[5][5];
struct A{
    int x,y;
}point[30];
//cnt:搜索过程中匹配好的点的数量
//sum:先前未匹配的点的数量
//ans,val:移动步数
void dfs(int cnt,int val){
    if(cnt>sum){ans=min(ans,val);return;}
//全匹配好了,就比较大小求出最小值
    for(int i=1;i<=4;i++)
	for(int j=1;j<=4;j++){
	    if(f[point[cnt].x][point[cnt].y][i][j]&&!visit[i][j]){
			visit[i][j]=1;
			dfs(cnt+1,f[point[cnt].x][point[cnt].y][i][j]+val);
			visit[i][j]=0;
	    }
//如果当前要匹配的点[x,y]与搜索到的点[i,j]可以发生匹配,且[i,j]没和其它点匹配过
	}
}
int main(){
    for(int i=1;i<=4;i++)
	for(int j=1;j<=4;j++){
	    char ch;cin>>ch;
	    a[i][j]=ch-'0';
	}
    for(int i=1;i<=4;i++)
	for(int j=1;j<=4;j++){
	    char ch;cin>>ch;
	    b[i][j]=ch-'0';
	    if(a[i][j]==b[i][j])
        	a[i][j]=b[i][j]=0;
	}
//经过上述步骤:把两个图中相同的1变为0后
//此时b图中的1对应在a图中一定是0(即是需要匹配,需要被交换位置的0)
    for(int i=1;i<=4;i++)
	for(int j=1;j<=4;j++){
	    if(a[i][j]==1){
			point[++sum].x=i;point[sum].y=j;
			for(int k=1;k<=4;k++)
			for(int l=1;l<=4;l++)
				if(b[k][l]==1)
            		f[i][j][k][l]=abs(l-j)+abs(k-i);
	    }
	}
//所以这里实际上是在a图中把所有1与所有需要匹配的0
//两两之间的距离算出来存在f数组中
//为什么要算距离?因为这些1和0两两需要调换位置
//而我们无法预测某个1一定会和哪个0交换位置
    dfs(1,0);
    printf("%d\n",ans);
    return 0;
}

本题还可以把\(4*4\)的格子(状态)通过哈希状态压缩,不同的局面最多只有\(2^{16}\)=65536个,然后跑BFS.

八数码难题

\(3×3\)的棋盘上,摆有八个棋子,每个棋子上标有1至8中的某一数字。棋盘中留有一个空格,空格用0来表示。空格周围的棋子可以移到空格中。

要求解的问题是:给出一种初始状态和目标状态(目标状态为123804765),找到一种最少步骤的移动方法,实现从初始状态到目标状态的转变。

发现本题只有\(3*3\)的格子,也就是只有9位数,那就可以直接用int存下状态.

为了优化,既然我们知道了初始状态和目标状态,不妨跑双向广搜.

int st,ed=123804765;
int a[5][5];
int dx[4]={1,-1,0,0},
    dy[4]={0,0,1,-1};
map<int,int> ans,visit;
queue<int> q;
void bfs(){
    q.push(st);q.push(ed);
//把初始状态和目标状态都放入队列中
//下面赋的初值好好思考一下
    ans[st]=0,ans[ed]=1;
//就算是双向广搜,也不可能同时走两步(拓展两个状态)
//一定是先拓展一个状态,再对另一状态进行拓展
//故设初始状态的ans值为0,则目标状态的ans值就为1    
    visit[st]=1,visit[ed]=2;
//不同的取值是为了区分是哪个状态拓展的
    while(!q.empty()){
		int u=q.front(),now=u,zeroi,zeroj;
    	q.pop();
		for(int i=3;i>=1;i--)
	    for(int j=3;j>=1;j--){
			a[i][j]=now%10;
			now/=10;
			if(a[i][j]==0){
            	zeroi=i;
                zeroj=j;
            }
	    }
//把int存的状态转为3*3的格子中,同时记录0所在的位置
		for(int i=0;i<=3;i++){
			int xx=dx[i]+zeroi;
        	int yy=dy[i]+zeroj;
			if(xx<1||xx>3||yy<1||yy>3)
            	continue;
			swap(a[xx][yy],a[zeroi][zeroj]);
//找到0位置周围一个合法的位置进行位置交换
			now=0;
			for(int j=1;j<=3;j++)
		    for(int k=1;k<=3;k++){
				now=now*10+a[j][k];
		    }
//位置交换后,又把3*3的状态转到int中存下
			if(visit[now]==visit[u]){
		    	swap(a[xx][yy],a[zeroi][zeroj]);
		    	continue;
			}
//如果now这个状态之前已经访问过,
//且与当前出队状态u是同一个状态拓展出的,则不合法
			if(visit[now]+visit[u]==3){
		    	printf("%d\n",ans[now]+ans[u]);
		    	exit(0);
			}
//如果now这个状态之前已经访问过,
//且1+2=3:
//即一个是初始状态拓展而来,一个是目标状态拓展而来
//则说明双向搜索途中相遇了,答案也就出来了
//通过这两个if语句的判断,理解visit赋值不同的作用
			ans[now]=ans[u]+1;
			visit[now]=visit[u];
			q.push(now);
//既不是非法状态,又不是答案状态,则记录信息,继续入队
			swap(a[xx][yy],a[zeroi][zeroj]);
//最后记得把格子复原(因为当前状态已存入int中入队)
    }
}
int main(){
    st=read();
    if(st==ed){
		puts("0");
		return 0;
    }//特判一下
    bfs();
    return 0;
}

posted on 2019-01-23 18:53  PPXppx  阅读(526)  评论(0编辑  收藏  举报