题目描述:
如下图所示,有一个 #
形的棋盘,上面有 1,2,3 三种数字各 8 个。
给定 8 种操作,分别为图中的 A∼H。
这些操作会按照图中字母和箭头所指明的方向,把一条长为 7 的序列循环移动 1 个单位。
例如下图最左边的 #
形棋盘执行操作 A 后,会变为下图中间的 #
形棋盘,再执行操作 C 后会变成下图最右边的 #
形棋盘。
给定一个初始状态,请使用最少的操作次数,使 #
形棋盘最中间的 8 个格子里的数字相同。
输入格式
输入包含多组测试用例。
每个测试用例占一行,包含 24 个数字,表示将初始棋盘中的每一个位置的数字,按整体从上到下,同行从左到右的顺序依次列出。
输入样例中的第一个测试用例,对应上图最左边棋盘的初始状态。
当输入只包含一个 0 的行时,表示输入终止。
输出格式
每个测试用例输出占两行。
第一行包含所有移动步骤,每步移动用大写字母 A∼H 中的一个表示,字母之间没有空格,如果不需要移动则输出 No moves needed
。
第二行包含一个整数,表示移动完成后,中间 88 个格子里的数字。
如果有多种方案,则输出字典序最小的解决方案。
输入样例:
1 1 1 1 3 2 3 2 3 1 3 2 2 3 1 2 2 2 3 1 2 1 3 3
1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3
0
输出样例:
AC
2
DDHH
2
思路分析:
题目给出四种操作,每种操作图中标出方向,就是按照此方向抽出这一行(这一列)的第一一个数查到尾部,让我们求出最少步数使得#图中间部分数相同,且在最小步数的基础上,如果有多种方案,则输出最小字典序的那一方案的操作顺序和该最小字典序。
对于本题给出的八种操作我们可以用一个二维数组存储,对于操作后的变化可以直接对此数组操作即可,即把首位数移到末位数,除首位数之外的所有数都前移一位。
本题可以采用IDA*解决,因为暴力搜索对于每一种走法(无论成功与否)都需要从头搜到尾,并且每步都要搜索是否达到目标,更别说还要找到可行的最小字典序序列,时间复杂度太高。对于bfs+A*,求字典序比较麻烦,因为优先队列中是按估价函数排序而非字典序排序,所以除非将步数最小的所有方案全部求出,否则是无法求出字典序的。所以用迭代加深+A*来做比较好。
对于本题,最主要的就是求出估价函数,所谓估价函数就是求最理想的操作,对于本题,最理想的操作显而易见就是每一步操作都可以让中间位置出现一个我们想要的数,这个想要的数当然就是初始状态中间位置出现最多的那个数(1-3之间),我们可以对初始状态中间一圈循环对出现的数进行计数,最终得出出现最多的那个数s,假设s出现了cnt次,因为中间一共就8个数,所以最理想的操作步数就是8-cnt(每步操作都新增一个s)。
另外,此题还可以进一步优化,排除等效冗余剪枝,我们发现如果上一次操作和本次操作正好相反(如刚执行完A操作,又执行了F操作),则相当于抵消了,两次操作白费了,所以我们没执行一次操作时,都要判断是否跟上一次操作方向相反,如何判断呢,我们可以开设一个反方向操作数组,记录每次操作的反方向操作是什么。
处理到这里题目大致做法已经考虑完成,然而我们发现本题需要处理的对象位置是无规律的,如果我们全部按照循环遍历一遍去处理对应位置的话,时间上我们又过不去了,所以我们需要对每个处理方案开设对应的数组,用顺序化(如顺时针方向从1-n、一层一层从1-n记录)的方式记录每个位置的相对位置,这样我们只需要操作该数组即可完成对应操作。
对于8种操作,我们按照题目的#字型层序从0-23标号,按照每种操作箭头的方向书写我们的op数组;对于查找中间部分出现最多的那个数,我们可以拿到刚刚标号的#字图,层序或者顺时针书写标号记录到我们的center数组;对于操作的反方向操作,我们可以顺时针从A操作开始到H操作用0-7标号,对应书写我们的opposite数组(如A操作编号为0,其反方向操作F编号为5)。
如图:
代码如下:
import java.util.ArrayList; import java.util.List; import java.util.Scanner; public class 回转游戏_IDA星 { static final int N = 24; static int[] a = new int[N]; static int[][] op = { // 8种操作(A-G)每行(每列)操作在#图的相对位置 {0, 2, 6, 11, 15, 20, 22}, {1, 3, 8, 12, 17, 21, 23}, {10, 9, 8, 7, 6, 5, 4}, {19, 18, 17, 16, 15, 14, 13}, {23, 21, 17, 12, 8, 3, 1}, {22, 20, 15, 11, 6, 2, 0}, {13, 14, 15, 16, 17, 18, 19}, {4, 5, 6, 7, 8, 9, 10} }; static int[] oppsite = {5, 4, 7, 6, 1, 0, 3, 2}; // 反操作数组 static int[] center = {6, 7, 8, 11, 12, 15, 16, 17}; // 中间部分位置索引数组 static boolean flag = false; static List<Integer> path = new ArrayList<>(); // 记录每步操作 static int f(){ int[] sum = new int[4]; // 因为图中只会出现123三个数,所以数组大小开到4即可 for(int i = 0; i < 8; i++) sum[a[center[i]]]++; // 当前数是什么,对其计数 int max = 0; for(int x : sum) max = Math.max(max, x); // 找出出现最多的那个数出现了几次 return 8 - max; // 估价函数 } static boolean check(){ // 如果第二个数到第8个数组其中一个不同于第一个数,则说明当前状态不满足 for(int i = 1; i < 8; i++) if(a[center[i]] != a[center[0]]) return false; return true; } static void change(int x){ // 记下当前操作首位数 int t = a[op[x][0]]; // 除首位数之外后面数前移一位 for(int i = 0; i < 6; i++) a[op[x][i]] = a[op[x][i + 1]]; // 与末位数交换,末尾赋值为首位数 a[op[x][6]] = t; } static void dfs(int u, int d, int last){ // 当前步数 + 估价步数 > 层数(当前最大限制步数) 或者 前面已经找到(存在更小的操作步数满足题意) 则跳过走下一方案 if(u + f() > d || flag) return; if(u == d){ if(check()){ flag = true; // 第0层找到目标状态,说明不用走初始棋盘即目标 if(d == 0) System.out.println("No moves needed"); else{ // 输出走过的每一步 for(int x : path) System.out.print((char)(x + 'A')); System.out.println(); } // 输出中间部分是哪个数 System.out.println(a[6]); } } // 循环走这8种操作 for(int i = 0; i < 8; i++){ // 上一次操作跟本次操作的反操作相同,说明本次操作是上一次的反操作,会相互抵消,之间跳过本操作。 if(oppsite[i] == last) continue; // 执行操作 change(i); // 记录操作 path.add(i); // 走在本层下一步 dfs(u + 1, d, i); // 还原现场 path.remove(path.size() - 1); change(oppsite[i]); } } public static void main(String[] args) { Scanner sc = new Scanner(System.in); while(true){ boolean turn = false; for(int i = 0; i < N; i++){ a[i] = sc.nextInt(); if(a[i] == 0){ turn = true; break; } } // 输入0,退出输入操作 if(turn) break; int d = 0; // 每组数据初始要重置为没找到状态 flag = false; while(true){ // 当前所用0步,当前处在d层,上一次操作是什么(初始设为-1,目的是不与任何一种操作互为反操作,以完成第一步操作及后面所有操作) dfs(0, d, -1); // 找到则退出下一层的搜索 if(flag) break; // 没找到则去下一层继续找 d++; } } } }