文章目录
前言
微信在几年前有一个比较火的小游戏,叫“一笔画完”(如图为游戏的第15关)。游戏规则是根据游戏界面,由起点开始,一笔画完所有的格子即为通关。本文章就是通过设计一个Java程序,输入游戏界面的状态,而由代码执行出我们通关的路径。
一、算法分析
首先,通过分析游戏界面,不难得出以下五点:
第一点:游戏界面是一个矩阵,如游戏第15关是一个4行4列(4×4)的矩阵
第二点:每一个格子有三种状态,即无效、走过和未走过
注:以上面的第15关游戏界面为例,把其当做4×4的矩阵,那么第一行的第1个、第3个、第4个和第四行的第1个格子是游戏中用不上的格子,即为无效格子。
第三点:格子最多有四个方向,即向上、向下、向左、向右
第四点:所有的格子均被走过为通关
结论:用穷举法,暴力破解,因为穷举法最符合人脑的思维方式,可以先码出一个程序再说
二、算法设计
经过上述的分析,可以一一对应地设计:
第一步:创建一个二维的整型数组,行数和列数由输入的整数决定,起点也由输入决定
第二步:二维数组的数字由0、1组成,1为无效和走过的格子、0为未走过的格子
第三步:格子的上、下、左、右的移动。在移动前先判断此格子有哪几种可选择的路径方向,然后在可选择的方向中随机选一种,如果没有选择的方向,就重新开始,直至达到第四步的要求。格子的移动采用递归
向上移动:行下标-1、列下标不变
向下移动:行下标+1、列下标不变
向左移动:行下标不变、列下标-1
向右移动:行下标不变、列下标+1
第四步:如果此格子没有可选择的方向且二维数组未走过(0)的格子数量为1,即为通关,打印出路径,即下面这种情况:
三、算法实现
好了,上代码,先创建出几个基本方法:
方法一、int[] Channel(int arr[][], int i, int j):根据传入的2维数组和坐标,返回此格子可选择的方向集合m,代码如下:
/**
* 找出此位置有几种选择的方向
* @param arr 游戏矩阵
* @param i 传入的坐标i
* @param j 传入的坐标j
* @return 可选择的方向数组,长度为4,分别代表上,下,左,右
*/
public static int[] Channel(int arr[][], int i, int j){
int[] num = new int[4];
if(i > 0 && arr[i - 1][j] != 1) { //上
num[0] = 1;
}
if(i < arr.length - 1 && arr[i + 1][j] != 1){ //下
num[1] = 1;
}
if(j > 0 && arr[i][j - 1] != 1) { //左
num[2] = 1;
}
if(j < arr[0].length - 1 && arr[i][j + 1] != 1) { //右
num[3] = 1;
}
return num;
}
注:m为一维整形数组,长度为4,对应格子的上、下、左、右四个方向,用0、1填充,1为次格子的该方向可以走,0反之
方法二、boolean isNotChoice(int arr[][]):判断传入的2维矩阵是否存在没有方向选择的格子,有返回True,代码如下:
/**
* 判断矩阵中是否存在可选择路径为0的坐标
* @param arr 游戏矩阵
* @return 有返回false,没有返回true
*/
public static boolean isNotChoice(int arr[][]){
for(int i = 0; i < arr.length; i++){
for(int j = 0; j < arr[0].length; j++){
if (arr[i][j] == 0 && NotZero(Channel(arr, i, j)) == 0)
return true;
}
}
return false;
}
注:NotZero()方法作用为找出格子放回的m数组中1(可走)的个数,代码如下:
/**
* 找出方向选择数组中可选择方向的数量
* @param arr 方向选择数组
* @return 返回方向选择数组中可选择方向的数量
*/
public static int NotZero(int arr[]){
int n = 0;
for(int i = 0; i < 4; i++){
if(arr[i] != 0){
n++;
}
}
return n;
}
方法三、int Zero(int arr[][]):返回2维数组中0(未走过)的个数,代码如下:
/**
* 判断矩阵中0的数量,即未走过的坐标
* @param arr 游戏矩阵
* @return 返回矩阵中未走过的坐标数量(int)
*/
public static int Zero(int arr[][]){
int end = 0;
for(int i = 0; i < arr.length; i++){
for(int j = 0; j < arr[0].length; j++){
if (arr[i][j] == 0)
end++;
}
}
return end;
}
方法四:int FindPos(int n, int arr[]):返回产生的随机数在数组中不为零的位置
例:一个格子的方向集合m,产生的随机数n如下
m = {0,1,0,1}(下、右) n = 0[0,2) FindPos(n,m) = 1(下)
m = {0,1,1,1}(下、左、右) n = 2[0,3) FindPos(n,m) = 3(右)
m = {1,0,0,1}(上、右) n = 1[0,2) FindPos(n,m) = 3(右)
代码如下:
/**
* 找出方向选择数组中第n个不为0的数
* @param n 整型
* @param arr 方向选择数组
* @return 返回第n个不为0的元素的下标
*/
public static int FindPos(int n, int arr[]){
int m = 0;
for(int i = 0; i < arr.length; i++){
if(arr[i] == 1)
m++;
if(m == n + 1) {
return i;
}
}
return 0;
}
方法五:主方法Start(),代码如下:
/**
* 暴力破解开始
* @param arr 游戏矩阵
* @param i 起点坐标的i值,从0开始
* @param j 起点坐标的j值,从0开始
* @param map 存储执行路线的字符矩阵
* @param blank 无效坐标ID数组
* @param start 起点坐标
* @param matrix 存储矩阵参数数组
* @param count 统计暴力破解次数
*/
public static void Start(int arr[][], int i, int j, char[][] map, int[] blank, int[] start, int[] matrix, int count){
System.out.println("-----------------------");
PrintArray(map);
//如果矩阵中还存在未走过(0)的格子,就进入循环
while(Zero(arr) != 0) {
//根据传入的二维矩阵和坐标,计算该格子可选择的方向,返回方向选择集合m
int[] m = Channel(arr, i, j);
//如果m中可选择(1)的个数大于0,则进行随机选择一个方向进行移动
if (NotZero(m) > 0 && !isNotChoice(arr)) {
//将此位置置为1,即"走过"
arr[i][j] = 1;
//根据可选择方向数量,随机选择一个方向,进行移动
int random = (int) (Math.random() * NotZero(m));
//找出此随机数在方向选择数组m中代表的方向
int n = FindPos(random, m);
//根据n进行移动
if (n == 0) { //上
if (i > 0 && arr[i - 1][j] != 1) {
map[i][j] = '↑';
Start(arr, i - 1, j, map, blank, start, matrix, count);
}
}
if (n == 1) {
if (i < arr.length - 1 && arr[i + 1][j] != 1) { //下
map[i][j] = '↓';
Start(arr, i + 1, j, map, blank, start, matrix, count);
}
}
if (n == 2) {
if (j > 0 && arr[i][j - 1] != 1) { //左
map[i][j] = '←';
Start(arr, i, j - 1, map, blank, start, matrix, count);
}
}
if (n == 3) {
if (j < arr[0].length - 1 && arr[i][j + 1] != 1) { //右
map[i][j] = '→';
Start(arr, i, j + 1, map, blank, start, matrix, count);
}
}
//如果m中可选择(1)的个数等于0,且二维矩阵还仅有一个格子未走过(0),即代表通关,打印路径
} else if (NotZero(m) == 0 && Zero(arr) == 1){
arr[i][j] = 1;
map[i][j] = '●';
System.out.println("-----------------------");
System.out.println("7.路线图如下:");
PrintArray(map);
System.out.println("Count:" + (++count));
//如果m中可选择(1)的个数等于0,代表走到死胡同,清空二维数组和map数组,重新开始
}else if (NotZero(m) == 0 || isNotChoice(arr)){
System.out.println("Count:" + (++count));
System.out.println("-----------------------");
System.out.println("重新开始:");
char[][] mapRestart = SetArray(matrix, blank);
mapRestart[start[0]][start[1]] = '◎';
ClearArray(arr);
SetArray(arr, blank);
Start(arr, start[0], start[1], mapRestart, blank, start, matrix, count);
}
}
}
注:map[][]是复刻arr[][]二维数组的字符数组,方便观察程序的运行情况
四、演示(OneStrokeV1.0)
以上面的第15关为例,开始演示:
第一步:输入矩阵的行列数
注:第15关是4×4的矩阵,输入:4 4
第二步:输入无效矩阵ID
注:第15关,按照行编号,从0开始,ID为0、2、3、12的格子是无效的,故输入:0 2 3 12
第三步:输入游戏起点坐标
注:从0开始,15关的起点是,第0行,第1列,输入:0 1
运行结果
注:可以看出,通关的路线打印出来了。走了两次错误的路径,Count的值为2,在第三次的时候通关了。
五、有待改进
沿着算法的设计思路下来,不难发现以下几点有待改进的地方:
1.不符合算法的有穷性,可能无法得到通关的路径
算法的有穷性是指算法必须能在执行有限个步骤之后终止;很明显,因为此算法没有排除错误路径的机制,所以按道理来说,运气足够好或者运气足够差,程序都是有可能一直走错误的路径,还可能是重复的。程序之所以能运行出通关路径,是因为15关的矩阵较为简单,路径的变化比较少。如演示的15关,我列举了一下,一共有26种路径变化(如图),其中有4种通关路径,也就是说,程序运行一次得出结果的概率为2/13,理论上运行时间足够长,尝试的次数足够多,是大概率能得出结果的,但实际上也可能一直得不出结果,这就不符合上面说的算法有穷性的“必须能在执行有限个步骤之后终止”,这个算法做不到“必须”,只能做到2/13
2.空间和时间复杂度高,IDEA的资源开销非常大
运行一下6×6的矩阵试一下,问题一下就会暴露出来。如117这关,运行之后,IDEA报错
这是IDEA中java虚拟机中的线程的栈内存太小,满足不了程序递归的层数了,所以报错了。解决这个问题需要设置程序配置中的这个参数,“-Xss128m”,上面这一关我是设置128MB的,如下,运行说明还是不够,可以再调高一点,但这治标不治本。
总结
通过穷举法,先大致码出一个程序,视为V1.0,再分析程序中的问题和改进的方法。需要全部源码的请留言