最近在《软件构造》课程实验中遇到了幻方问题,写一篇博客简单解析一下用JAVA编程解决幻方问题的几种方法。
目录
幻方简介
首先简单介绍一下幻方的概念,幻方分为完全幻方,乘幻方,高次幻方和反幻方等很多种,而我们平时所说的n阶幻方都是指一次n阶幻方,在这篇文章中,我们介绍的幻方问题也都是关于一次n阶幻方的。一次n阶幻方是一个n*n的数字方阵,一般称为n阶幻方,其各行,各列和两条对角线之和都相等。下图就是一个简单的幻方的例子,其行数,列数都为3,每行,每列,和两条对角线的和都为15,我们就可以说他是一个3阶幻方。
幻方问题求解
幻方的判定问题
首先,幻方的判定问题就是判定一个矩阵是否为幻方,由之前的介绍我们不难看出:一个n阶幻方成立,只需要数字矩阵的行数,列数相等,且各行,各列,及两条对角线求和也相等即可。但是我们遇到的幻方问题往往有其他要求,比如填入方阵的数字需要为正数,指定为1-n^2的连续自然数,或要求填入的数字是某个特定的等差数列等。这里我们的以数字矩阵存入文本文件,每行数字由\t分隔开,且数字均为正整数为例,进行幻方的判定。
数据读入部分
由于我们认为数字矩阵存入文本文件,那么首先的问题就是如何将其读出来,这里我们采用BufferedReader流,用readLine()函数逐行读入,每次读入用split("\t")将其分割为字符串数组,由于不确定幻方的阶数,我们在第一次读入数据时用.length读取分割后的数组大小,作为阶数n,之后建立n*n的int型2维数组,将读出的数据依次存入数组中即可,期间用自己的方法做好异常输入处理即可,比如判断输入是否为正整数,每行数字个数是否相同,是否用\t分割等等。
我们将读入数据的循环的条件表达式写成 (i < SIZE) && ((MSread = fr.readLine()) != null) ,这表示存储数据的数组读满或者文件读完,这样做的原因是便于检查输入的数字矩阵行列是否相等,在读入结束后,如果MSread = fr.readLine()) != null,表示文件还没读完,数组就存满了,也就是说文本文件中行数>列数;如果i < j,说明数组还没读满,文件就读完了,即文本文件中行数<列数,这些情况可以直接返回,不需要做后续的计算判断。
对于其他的要求,比如给定幻方阶数,或从控制台读入数据,只需要对数据读入过程稍作修改即可。
以下是实现这部分功能的部分代码:
int i = 0, j = 0, sum = 0, sum1 = 0, sum2 = 0;// 用于完成循环和记录对角线,行,列数字之和
int SIZE = 1;// 定义幻方边长
File f = new File(fileName);
String MSread = null;// 用于从文件读取数据
BufferedReader fr = null;
try {
fr = new BufferedReader(new FileReader(f));
} catch (FileNotFoundException fileNotFoundException) {
fileNotFoundException.printStackTrace();
}
try {
MSread = fr.readLine();
} catch (IOException ioException) {
ioException.printStackTrace();
}
String[] split0 = MSread.split("\t");
SIZE = split0.length;// 读取第一行数字个数作为边长
int[][] MSdata = new int[SIZE][SIZE];// 用于存储幻方数据
for (j = 0; j < SIZE; j++) {
try {
MSdata[i][j] = Integer.parseInt(split0[j]);
} catch (Exception e) {
System.out.println("文本中的" + split0[j] + "部分不是一个整数");// 检测读入数据是否为整数
return false;
}
if (MSdata[i][j] <= 0) {
System.out.println("文本中的" + MSdata[i][j] + "<=0");// 检测读入数据是否>0
return false;
}
}
for (i = 1; (i < SIZE) && ((MSread = fr.readLine()) != null); i++) {// 从第二行开始循环读入数据
String[] split = MSread.split("\t");
if (split.length != SIZE) {// 判断之后每行数据个数与SIZE是否相等
j = i + 1;
if (i == 1) {
System.out.println("文本第1,2行被\\t分割的部分数目不同");
return false;
}
System.out.println("文本第" + j + "行未被\\t分割为" + SIZE + "部分");
return false;
}
for (j = 0; j < SIZE; j++) {
try {
MSdata[i][j] = Integer.parseInt(split[j]);
} catch (Exception e) {
System.out.println("文本中的" + split[j] + "不是一个整数");// 检测读入数据是否为整数
return false;
}
if (MSdata[i][j] < 0) {
System.out.println("文本中的" + MSdata[i][j] + "<0");// 检测读入数据是否>0
return false;
}
}
}
try {
if ((MSread = fr.readLine()) != null) {// 下一行还有数据未读完,行数>列数
System.out.println("文本中的行数大于列数");
return false;
}
} catch (IOException ioException) {
ioException.printStackTrace();
}
if (i < j) {// 未读满最后一行数据,列数>行数
System.out.println("文本中的列数大于行数");
return false;
}
数据检测部分
将数据读入数组后,对数据的检测就很简单了,只需要检测行,列,对角线之和是否全部相等,这里我做了两次循环进行检测,第一次做一重循环,记录主次对角线之和进行比较,第二次做二重循环,遍历数组,求各行各列之和,与对角线之和作比较,全部相等就说明给出的数字矩阵是幻方。该部分代码如下:
for (i = 0; i < SIZE; i++) {
sum1 += MSdata[i][i];// 求和左对角线
sum2 += MSdata[i][SIZE - i - 1];// 求和右对角线
}
if (sum1 != sum2) {
return false;// 比较两对角线之和,不等返回false
}
sum = sum1;// 储存对角线之和用于后续比较
for (i = 0; i < SIZE; i++) {
sum1 = 0;
sum2 = 0;
for (j = 0; j < SIZE; j++) {
sum1 += MSdata[i][j];// 求和第i行
sum2 += MSdata[j][i];// 求和第i列
}
if ((sum != sum1) || (sum1 != sum2)) {
return false;// 比较对角线之和与第i行或第i列之和,不等返回false
}
}
return true;// 每行,列和对角线之和都相等,是幻方,返回true
幻方的构造问题
对于幻方的构造问题,目前已经由许多解决方法,奇阶幻方的生成方法包括罗伯法,巴舍法,horse法等,单偶幻方的生成方法包括Strachey法等,双偶数幻方的生成方法包括对称交换法等,这里我们只介绍罗伯法和对称交换法的实现过程,填入的数字为1-n^2的自然数。
罗伯法生成奇阶幻方
罗伯法是经典的生成奇阶幻方的方式,即生成n阶幻方,n必须为奇数,其生成幻方的过程有一个口诀:
一居上行正中央,依次斜填右上方,上出框往下填,右出框左边放,排重便在下格填,右上排重一个样。
其具体流程就是将首位数字放在最上行的中央位置,每次填写时向右上方的位置填写下一个数字,如果从上面超出边界,数字就放到最下面一行,如果从右面超出边界,数字就放在最左边一行,如果下一个位置填过数字,或者下一个数字是从整个方阵的右上角出界(此时次对角线应该全部填满,也相当于排重),要将该数字填在前一个数字下方,直至所有数字填完。
这里我们不对方法的数学证明做介绍,我们主要看其实现过程,首先创建n*n的二维数组存放幻方数据,将表示行,列的变量初始化到第一行中央,用变量i计数做循环。每次循环时,填入数字,再让行数-1,列数+1,定位到下一个位置(右上方)。当row==0,即下一次将从上方出界时,定位到最后一行;当col==n-1,即下一次将从右方出界时,定位到第一列。而对于排重的检测,由于每次填完数字,定位到其右上方,即每次都填写n个数字后回到第一次填写的位置,也就是说,排重现象发生在填写n+1,2n+1,3n+1...这些数字的位置,那么只需要当计数到n的倍数时,直接将下一次填写定位在原数字下方即可,即行数+1。循环直至所有数字填完,即完成罗伯法构造奇阶幻方,代码如下:
int magic[][] = new int[n][n];// 建立数组,存储幻方数据
int row = 0, col = n / 2, i, j, square = n * n;// 初始化行,列;定位到最上行中央
for (i = 1; i <= square; i++) {
magic[row][col] = i;// 每次确定位置后,将从1到n^2的数字i依次存入数组
if (i % n == 0)// 罗伯法要求,当查找的位置已经填过数字,需要将下一个数字填到上一个数字下方,由于填数字按照对角线斜向右上方,每隔n个就会出现一次这种情况
row++;// 需要将后一个数字填在前一个数字下方,寻找位置只需行数+1
else {
if (row == 0)// 当上一个数字在第一行时,下一个数字填到最后一行
row = n - 1;// 定位到最后一行
else
row--;// 否则找右上方的位置,行数-1
if (col == (n - 1))// 当上一个数字在最后一列时,下一个数字填到第一列
col = 0;// 定位到第一列
else
col++;// 否则找右上方的位置,列数+1
}
}
对称交换法生成双偶幻方
对称交换法时生成双偶幻方的一种简单方式,双偶就是4的倍数,也就是说n=4m,此时幻方可以被划分成数个4阶幻方,而对称交换法就是基于这种划分完成的。
其构造过程是将方阵划分为4*4的方阵,对每个小方阵的主次对角线做标记,得到标记后的幻方,之后逐行填入1-n^2数字。
之后再对在标记位置的数字,关于整个方阵做中心对称交换,得到结果即为双偶幻方。
对于该种方式生成的双偶幻方很好实现,只需要检测其位置是不是标记位置的数字,是标记位置则填入其对称位置该填入的数字即可,由于标记位置是各个4阶幻方块的对角线,我们只需要求出某个位置在其4阶幻方中的相对位置即可,创建n*n的二维数组存放幻方数据,初始化i,j做二重循环便利整个数组,每次让i,j对4取余,得到的坐标即为其在相应4阶幻方中的坐标,之后判断横纵坐标相等或横纵坐标之和为3(坐标从0开始),即可判断其是标记位置,若是标记位置,则存储magic[i][j] = (n - 1 - i) * n + (n - 1 - j) + 1,为其对称位置的数字,否则存储magic[i][j] = i * n + j + 1为其本身的数字即可,代码如下:
int magic[][] = new int[n][n];// 建立数组,存储幻方数据
int i, j;
for (i = 0; i < n; i++) {//循环遍历幻方
for (j = 0; j < n; j++) {
if (((i % 4) == (j % 4)) || ((i % 4 + j % 4) == 3)) {//判断是否为标记位置
magic[i][j] = (n - 1 - i) * n + (n - 1 - j) + 1;//储存对称位置数据
} else {
magic[i][j] = i * n + j + 1;//储存该位置数据
}
}
}