九宫重排(bfs)

1. 问题描述:

问题描述

  如下面第一个图的九宫格中,放着 1~8 的数字卡片,还有一个格子空着。与空格子相邻的格子中的卡片可以移动到空格中。经过若干次移动,可以形成第二个图所示的局面。

  我们把第一个图的局面记为:12345678.
  把第二个图的局面记为:123.46758
  显然是按从上到下,从左到右的顺序记录数字,空格记为句点。
  本题目的任务是已知九宫的初态和终态,求最少经过多少步的移动可以到达。如果无论多少步都无法到达,则输出-1。

输入格式

  输入第一行包含九宫的初态,第二行包含九宫的终态。

输出格式

  输出最少的步数,如果不存在方案,则输出-1。

样例输入

12345678.
123.46758

样例输出

3

样例输入

13524678.
46758123.

样例输出

22

2. 思路分析

① 从题目中可以看到 "开始状态" 和 "最终状态" 的字眼,并且从一个状态转移到另外的一个状态需要执行一次操作,最终需要求解的是从一个状态转移到另外一个状态需要执行的最少次数,这些都是bfs能够解决问题的典型特征,所以我们可以使用bfs来进行解决,这道题目也类似于走出迷宫的最少步数的问题,除了使用bfs来进行,对于这种可能性不太确定的问题,我们还可以使用深度优先搜索来进行解决,但是使用深搜解决时间复杂度可能更高

② bfs首先要有一个队列,所以我们可以使用声明一个队列,首先需要解决的是队列的初始化的问题,因为对于bfs解决的问题来说,队列中的初始状态是核心,它的下一个状态就是由上一个状态经过一次操作得到的,所以来说首先解决队列的初始化问题,由题目中可以知道,九宫格中能够将空格与空格相邻的数字进行交换,所以我们需要知道的有如下几方面的信息

a:当前空格的位置在什么地方

b:与空格相邻的的位置

假如九宫格中最中间那个位置是空格,那么它最多能够移动四次,上下左右的位置都可以与中间的空格位置进行交换,这个是可以交换情况最多的位置,但是并不是当前空格的位置都能与其他相邻的四个位置上的数字进行交换,我们还需要进行判断一下,因为有可能发生越界的问题,所以依次判断四个邻居(与空格相邻的四个位置),假如邻居中的节点没有发生越界那么这个邻居是满足条件的

③ 我们可以把当前九宫格的状态的使用字符串来表示,即使用String对象来存储当前九宫格的状态,比如在题目中的初始状态是"1234567.8"那么空格在第八个的位置(从零位置算起)与空格相邻的位置有5,7这两个位置,所以一开始的时候需要记录这两个位置,因为我们求解的是最少的步骤需要记录多个数据类型所以自然就想到了面向对象的思想模式来进行数据的封装,所以可以使用一个类来封装当前状态下(肯定与空格是相邻的)的对应的字符串的值,当前空格的位置,当前的位置,还有就是一个需要维护的到达当前状态需要的步数,封装之后对于数据的取出和相应的操作就很方便了

④ 所以队列应该加入一个内部的私有类封装的节点,节点中包含了关于当前状态下的所以信息,一开始的时候队列加入了两个节点,分别是与空格位置相邻的5位置和7位置,并且当前状态下的字符串的值是一样的

⑤ 初始状态好了之后,进入while循环,使用bfs的典型的处理方法,弹出一个节点,加入若干个符合条件的邻居节点,弹出来一个节点之后我们需要进行当前位置与空格这个位置进行交换,所以交换之后空格的位置就变化了,所以我们需要判断新状态下的与空格位置相邻的位置,然后符合条件的状态下的与零相邻的位置,当前状态下的值等数据封装成一个节点加入到队列中,进行下一次的循环

寻找满足条件的邻居的时候需要注意的是,我使用的是字符串来表示当前的状态值,所以判断邻居的时候在左边的邻居不能够直接-1,后面的邻居不能够直接加一,假如是二维数组才可以进行这样的直接加一减一的操作,通过简单的分析可以得到前面的一个取余数的限制

if(index % 3 != 0 && index - 1 >= 0){
	queue.add(new Node(start, index, index - 1, 0));
}
if(index % 3 != 2 && index + 1 < len){
	queue.add(new Node(start, index, index + 1, 0));
}

⑥ 假如发现满足条件的直接return就好了,因为我们知道假如首先到达的肯定是操作步数最少的,这与迷宫的问题也是一样的,仔细想一下就知道了

⑦ 需要注意的一个问题是字符串的替换问题,假如直接使用for循环加上判断的话会更加超时,下面我采用的是直接截取字符串的方法加上判断添加上对应的字符串拼接得到新状态下的字符串

⑧ 在操作的过程中可能会回到之前的那个状态所以为了防止这个问题可以使用set数据结构来进行判断之前的操作是否到达过这个状态,假如到达过那么不能够加入队列中,这也是对于bfs这一列问题解决特别需要注意的地方

3.  具体的代码如下,但是Java写超时了,只得了60分

import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;
import java.util.Set;
public class Main {
	static String start;
	static String end;
	static int count = 0;
	static Set<String> set = new HashSet<String>();
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		start = sc.next();
		end = sc.next();
		int res = bfs();
		System.out.println(res);
		sc.close();
	}
	
	private static int bfs() {
		int len = start.length();
		int index = len - 1;
		Queue<Node> queue = new LinkedList<Node>();
		for(int i = 0; i < start.length(); i++){
			if(start.charAt(i) != '.'){
				continue;
			}else{
                //找出一开始空格的位置
				index = i;
				break;
			}
		}
        //加入与空格位置的邻居
		if(index % 3 != 0 && index - 1 >= 0){
			queue.add(new Node(start, index, index - 1, 0));
		}
		if(index % 3 != 2 && index + 1 < len){
			queue.add(new Node(start, index, index + 1, 0));
		}
		if(index - 3 >= 0){
			queue.add(new Node(start, index, index - 3, 0));
		}
		if(index + 3 < len){
			queue.add(new Node(start, index, index + 3, 0));
		}
		set.add(start);
		while(!queue.isEmpty()){
			count++;
			Node p = queue.poll();
			int i = p.index;
			int pos = p.position;
			String val = p.val; 
			if(val.equals(end)){
				return p.depth;
			}
			String t = val;
			char c1 = t.charAt(i);
			char c2 = t.charAt(pos);
            //需要先进行状态的转移然后再将新状态下的与空格位置相邻的位置加进来
			String s = "";
			//for循环转变了使用了下面直接截取字符串的时间消耗可能少一点
			if(i > pos){
				s += t.substring(0, pos) + c1 + t.substring(pos + 1, i) + c2 + t.substring(i + 1, len);
				
			}else{
				s += t.substring(0, i) + c2 + t.substring(i + 1, pos) + c1 + t.substring(pos + 1, len);
			}
			if(!set.contains(s)){
				if(i % 3 != 0 && pos - 1 >= 0){
					queue.add(new Node(s, pos, pos - 1, p.depth + 1));
				}
				if(i % 3 != 2 && pos + 1 < len){
					queue.add(new Node(s, pos, pos + 1, p.depth + 1));
				}
				if(pos - 3 >= 0){
					queue.add(new Node(s, pos, pos - 3, p.depth + 1));
				}
				if(pos + 3 < len){
					queue.add(new Node(s, pos, pos + 3, p.depth + 1));
				}
				set.add(s);
			}
		}
		return -1;
	}

	public static class Node{
		public String val;
		public int index;
		public int position;
		public int depth;
		public Node(String val, int index, int position, int depth) {
			super();
			this.val = val;
			this.index = index;
			this.position = position;
			this.depth = depth;
		}
		@Override
		public String toString() {
			return "Node [val=" + val + ", index=" + index + ", position=" + position + ", depth=" + depth + "]";
		}
	}
}

4. 在搜索的过程中,发现一篇比较简洁和容易理解的的Java代码,他是使用一个二维数组来表示向上下左右四个方向的移动,使用当前空格的位置加上空格可以交换的的四个位置进行判断,这样的话更加简洁,而且使用字符串替换的方式与我的有所不同,它是先将需要调换的第一个的字符先替换为其他的字符,然后再将第二个需要替换的字符替换为第一个需要调换的字符,然后再把第一个字符换回来

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Scanner;
public class Main {
    public static String start, end;
    public static int x1, y1;   //起始就空格状态start中的空格子坐标
    public static int[][] move = {{-1,0},{1,0},{0,-1},{0,1}};//表示分别向上、下、左、右移动一步
    public static HashSet<String> set = new HashSet<String>(); //用于存放每次移动空格子后的结果,用于判重
    
    static class Move {  //内部类,存放空格子移动一步后的结果
        public int x;   //空格子位置横坐标
        public int y;   //空格子纵坐标移动步数
        public int step;  //记录最终
        public String temp;  //当前九宫格状态
        
        public Move(int x, int y, int step, String temp) {
            this.x = x;
            this.y = y;
            this.step = step;
            this.temp = temp;
        }
    }
    
    public void bfs() {
        for(int i = 0;i < start.length();i++) {   //寻找九宫格初始状态空格子的位置
            if(start.charAt(i) == '.') {
                x1 = i / 3;
                y1 = i % 3;
            }
        }
        ArrayList<Move> list = new ArrayList<Move>();
        list.add(new Move(x1, y1, 0, start));    
        set.add(start);
        while(!list.isEmpty()) {
            Move now = list.get(0);
            list.remove(0);
            if(now.temp.equals(end)) {     //当前状态为最终状态时,直接退出
                System.out.println(now.step);
                return;
            }
            for(int i = 0;i < 4;i++) {   //四种行走方案
                int x = now.x + move[i][0];
                int y = now.y + move[i][1];
                if(x < 0 || x > 2 || y < 0 || y > 2)  //出现九宫格越界
                    continue;
                int step = now.step + 1;
                char n = now.temp.charAt(x * 3 + y);   //获取当前行走的新位置字符
                String temp0 = now.temp;
                temp0 = temp0.replace(n, '-');   //交换'.'字符和n字符
                temp0 = temp0.replace('.', n);
                temp0 = temp0.replace('-', '.');    
                if(!set.contains(temp0)) {  //判定当前行走结果是否已经行走过
                    set.add(temp0);
                    list.add(new Move(x, y, step, temp0));
                }
            }
        }
        System.out.println("-1");
        return;
    }
    
    public static void main(String[] args) {
        Main test = new Main();
        Scanner in = new Scanner(System.in);
        start = in.next();  //九宫格的初始状态
        end = in.next();   //九宫格的最终状态
        test.bfs();
    }
}

猜你喜欢

转载自blog.csdn.net/qq_39445165/article/details/88362392