在前一段时间所做的的软件构造实验一中有一个很有趣的实验部分,magic square。这一部分要求是写一个判断任意n*n矩阵是否为magic square的代码,为其给出的一个代码画出流程图并测试其结果是否正确。
首先简单介绍一下magic square的定义,magic square是一个由整数构成的矩阵,简单来说就像数独一样,要求矩阵内的每一个整数均不相同,同时每一行,每一列以及对角线之和应是相等的。
需要注意的是,可以通过数学证明来得到,一个矩阵为magic square的充要条件是矩阵内的整数均不同,位于中值的数位于矩阵正中心,每一行每一列之和相等。
这个实验的这一部分第一个小要求很简单,这里直接贴出代码:
static boolean isLegalMagicSquare(String fileName) throws FileNotFoundException {
File file = new File(fileName);
Reader reader = new InputStreamReader(new FileInputStream(file));
int temp, gint = 0, values1 = 0, values0 = 0;
char chartemp;
String temps = "00";
int orderint[][] = new int[200][200];
int i = 0, j = 0;
try {
while((temp = reader.read()) != -1) {
chartemp = (char)temp;
if(chartemp <= '9' && chartemp >= '0')
{
temps = temps + chartemp;
}
else if(chartemp == '\t')
{
orderint[i][j] = Integer.parseInt(temps);
temps = "00";
i++;
}
else if(chartemp == '\n')
{
orderint[i][j] = Integer.parseInt(temps);
temps = "00";
if(j == 0)
{
gint = i;
i = 0;
}
else
{
if(i != gint)
{
System.out.println("格式错误!");
return false;
}
i = 0;
}
j++;
}
else if(chartemp == '\r')
{
}
else
{
System.out.println("格式错误!");
return false;
}
}
orderint[i][j] = Integer.parseInt(temps);
if(gint != j)
{
System.out.println("格式错误!");
return false;
}
reader.close();
} catch (IOException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
j = 0;
for(i = 0; i <= gint; i++)
{
values0 = orderint[i][j] + values0;
}
while(j <= gint)
{
for(i = 0; i <= gint; i++)
{
values1 = orderint[i][j] + values1;
}
if(values1 != values0)
{
System.out.println("格式错误!");
return false;
}
j++;
values1 = 0;
}
i = 0;
j = 0;
values1 = 0;
while(i <= gint)
{
for(j = 0; j <= gint; j++)
{
values1 = orderint[i][j] + values1;
}
if(values1 != values0)
{
System.out.println("格式错误!");
return false;
}
i++;
values1 = 0;
}
i = 0;
j = 0;
values1 = 0;
while(i <= gint && j <= gint)
{
values1 = orderint[i][j] + values1;
i++;
j++;
}
if(values1 != values0)
{
System.out.println("格式错误!");
return false;
}
return true;
}
我想把这篇博客的重点放在第二个小要求的分析上,第二个小要求给出了一个很有意思的代码来对n为奇数的情况生成一个简单的magic square,那么这一代码是通过怎样的算法生成的呢?
先给出其代码(在这一代码中我已添加好注释,但并不简单明了,代码后我会给出详细说明)
public static boolean generateMagicSquare(int n) throws IOException {
if(n % 2 == 0)
{
System.out.println("n为偶数,不合法!");
return false;
}
else if(n <= 0)
{
System.out.println("n非正数,不合法!");
return false;
}
int magic[][] = new int[n][n];//定义一个n*n二维整型数组magic存储magicsquare
int row = 0, col = n / 2, i, j, square = n * n;//定义初始变量row,col,i.j
for (i = 1; i <= square; i++) {
magic[row][col] = i;//初始填充位置于第一行中间保证填充结束后其中间值于magicsquare中间位置
if (i % n == 0)
row++;//如果遇到行/列的倍数,向下一格填充,保证对同一行/列,其下一填充值对n余数与之前值均不同
else {
if (row == 0)
row = n - 1;//如果行数减至0,则将行数修改为n-1
else
row--;//否则,行数减一
if (col == (n - 1))
col = 0;//如果列数达到最大n-1。则修改为0
else
col++;//否则列数加一
}//填充方式每n个为一组,每组以对角线方向填充。遇顶则反弹
}//通过以斜线方向填充,每n个一组,每组位置于上一组下方一格保证不会有属于同一组的两个数处于同一行或列,同时每一行列包含每一组的一个数且行列中每个数对n求余均不同。
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++)
System.out.print(magic[i][j] + "\t");//输出magicsquare
System.out.println();
}
try {
FileOutputStream op = new FileOutputStream("./src/p1/txt/6.txt", false);
String txt6 = new String();
for(i = 0; i < n; i++)
{
for(j = 0; j < n; j++)
{
txt6 = txt6 + magic[i][j];
if(j == n - 1 && i != n - 1)
{
txt6 = txt6 + "\n";
}
else
{
txt6 = txt6 + "\t";
}
}
}
op.write(txt6.getBytes());
op.close();
} catch (FileNotFoundException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
return true;//返回true,输出成功
}
我们对这一代码进行分析的过程中,用笔和纸实际去跟进写一下每一步的填充顺序是最便于理解的。在跟进的过程中我们会发现,这一算法的填充思路大体如下:
1.以1为开始,每次加一,即以1,2,3,4,5...的顺序填充
2.第一个值,即1应该填入第一行的中间位置
3.每一次填充位置为上一次填充位置的行数减一,列数加一处,即右上方处
4.如果这一次行数为0,则下一次则将行数修改为n - 1,如果列数为n - 1,则修改为0
5.每次填充到n的倍数,下一次填充位置则不遵循3,4规律,而是在这一次位置正下方
可以发现,这一算法的填充思路是以左下到右上的斜线方向填充,触及边界则从另一侧继续。
那么为什么要以斜线方向填充呢?这一点要从算法的分组方式说起,不难发现算法实际上是以每n各一组进行斜线填充的,每一组的下一组要整体向下平移一个位置再继续以相同方向进行填充、为什么要分为n组,每组n个?原因很简单,这样分组后同一组的数都具有着一个相同的属性:即对n的除数是相同的,同一组数的区别在于对n的余数不同,矩阵共有n行,n列,我们将一组的n个数以斜线方向填充完毕后会发现一个现象,就是无论我们以哪个位置作为起始,在遵从3,4规则下填充完毕后这一组的数必然落在不同行且不同列内,即不存在属于同一组的数位于同一行,同一列。这样的组有n个,也就是说,当我们将这n组全部填充完毕时,对于任意一行或任意一列,其内填充的n个数必然来自与n个不同的组。如果我们把某一行/列第i个位置的数对n除数设为ti,可以发现,每一行/列的t0,t1,t2,t3...tn-1和均相等。要保证每一行/列和相等,接下来只需要保证同一行/列中的每一个数对n余数之和相等即可。
算法实现同一行/列中的每一个数对n余数之和相等是通过规则5实现的,再次回到我们的跟进过程中,因为算法是顺序填充的数值,当其以斜线填充遇n倍数下移的过程中,我们只关注某一行/列会发现,下移后的组整体位置要比原组低1,即每一组达到此行/列填充的数的余数之间存在着一个递增或递减的规律,这保证了同一行/列每个数对n的余数均是不同的,即同一行/列中的每一个数对n余数之和相等。
回顾我们前面说的magic square充要条件,现在就差一个条件了,即数的中值应在中间,将第一个值1填充在第一行中间即是为了满足这一条件,跟踪可知,在这一情况下我们的中值永远会填在矩阵中间的位置。