《软件构造》JAVA编程问题:幻方问题(Magic Square)部分解法浅析

最近在《软件构造》课程实验中遇到了幻方问题,写一篇博客简单解析一下用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;//储存该位置数据
                    }
                }
            }

 

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值