[软构/SC]关于Lab1中Magic Square幻方的思考

文章分析了一个生成MagicSquare幻方的算法,着重讨论了奇数阶幻方的生成原理和偶数阶无法生成的原因,涉及到数组越界问题和群论在幻方生成中的应用。
摘要由CSDN通过智能技术生成

        实验1中第一题给出了一个Magic Square(幻方)的生成方法并要求处理输入偶数和负数时的异常,要求很简单,下面对这个方法的原理和无法生成偶数阶幻方的原因进行分析。

        叠个甲:本文并不解决任何问题或者发表任何创新理论,只是记录作者做题的时候的一点小小思考,笔力有限,如果能对读者有一点点启发则是最好的结果。

Magic Square(幻方)简介

        Magic Square,中文名为幻方、魔法矩阵等,是一个古老的数学游戏,幻方为n*n的二维矩阵构成的“格子”,格子内的数为1~n*n,每个格子内的数不重复,行、列、对角线上的n个数的和都相同。可以证明2阶矩阵无法生成幻方,但偶数阶的幻方是存在的,古往今来有很多特殊的幻方被发现,有一些特殊的性质,感兴趣的小伙伴可以自行深入了解。

代码及原理分析

        首先看代码。

public static boolean generateMagicSquare(int n) {
    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;
        if (i % n == 0) row++;
        else {
            if (row == 0) row = n - 1;
            else row--;
            if (col == (n - 1)) col = 0;
            else col++;
        }
    }
    for (i = 0; i < n; i++) {
        for (j = 0; j < n; j++) System.out.print(magic[i][j] + "\t");
        System.out.println();
    }
    return true;
}

        row和col表示矩阵起始的位置,从第0行和第n/2列开始排列,i用于赋值,square用于控制最大值(即矩阵的格数)。后文把magic[][]二维数组想象为一个n*n的表格,第0行在最上方,第0列在最左方,便于后续对赋值的方向进行说明。另外,后文谈到矩阵的行列时,都默认0为起点,n-1为终点。

        每次循环都会进行判断,如果i不是n的倍数,则判断row和col的位置。若row=0,则跳转到最后一行,否则row-1,游标向上移动;若col=n-1,则跳转到第0列,否则col+1,游标向右移动。不难发现,除了边界条件的检测,游标总是向右上移动的。

n=5的矩阵,红黄蓝为赋值顺序

 

         这种构成方式,就像一张三角形卡纸折成圆筒,或者一个用完的卷纸筒。

图源网络 用完的卷纸筒,具有左上的流向

         理解了这种流向,就很容易理解“数的赋值是一个循环”这种观点。我们以n为一个大循环,每n次赋值,程序中 i/n 的值会加1,而 i%n 会回到0~n-1的循环,则每次赋的值可以表示成k*n+x,0≤k≤n-1,1≤x≤5,k、x为整数。

        每个大循环中k是一样的,且一次大循环中,左下到右上的这个“纸筒”可以覆盖到每一行每一列(游标移动n次,每次行和列都与之前不重复),当一次大循环结束时(i%n=0),游标向下平移1个,避免与已赋值的位置重合并开始新一次大循环,所以每次大循环就可以给每行每列加上一个相同的k*n,不影响行列的和。

        把这个k*n视为一个基底,则每次行列的值就可以视为只增加了一个x,只要分析每行每列的‘x’的总和即可。在基底平均的情况下,如果想要x的总和相同,每行每列的x总和只能为(x+1)*x/2,当且仅当每行每列都有1~5这五个数字的一个序列才能满足,就像数独一样(从这个意义上来看,幻方真的很像数独)。这个分析可以帮助我们判断矩阵是否是幻方,以及分析本文分析的代码为什么不能生成偶数幻方。

 生成5阶幻方模拟

        以n=5的矩阵为例作图分析,下图以“字母+数字”(如a2)表示每个数,字母表示上述公式中的x,数字表示公式k+1,如a2表示的数就是(2-1)*5+1=6。同时,为了便于观看,不同颜色代表不同大循环,每个大循环数字相同,字母从a到e。用这种标记方式,我们可以很方便地判断每行每列是否都有一个x的从1到5的序列,即每行每列都必须有一个abcde的某个组合。

        初始化row=0,col=(5/2)=2,则(0,2)赋值a1(实际赋值为1)。

         游标试图向右上移动,发现到达上边界,row<==n-1,同时col++,实现“右上”移动,两次移动后如下,游标到达c1(3)。

        又一次触及边界,col<==0,row--,继续移动两次,游标到达e1(5),完成一次大循环。 

         

         可以发现,e1处的游标如果再向右上移动,就会移动到已经赋值的a1处,为了满足上述条件“每行每列的都必须有一个1到5的序列”,我们必须向周围的某个位置移动。此外,我们不能让游标随意移动,观察可知,游标即将赋值a2,向左移动会使a1所在的一列有两个a,向上移动又会使a1所在的行有两个a,故只能向下或向右。本文的生成代码选择了向下移动,开启一轮红色的大循环。a2填充后矩阵如下。

         我们使用类似的方案进行循环,每次大循环都填充不同的颜色,填充结果如下图。从行列上,每行每列都有a~e的一个序列。

​​​​​​​        聪明的你一定发现了,这里没有证明主对角线上的元素之和也相等(副对角线已经在前面有所说明),主对角线的证明还是有些复杂,包括但不限于模3同余的群、不同位置元素相邻元素取值讨论,其实只要说明三种情况就可以了(),分别是阶模3余1,阶模3余2和阶模3余0,但是作者时间精力有限,有机会再作补充。我觉得这种证明方法相当暴力,如果有更好的证明方法……

        

5*5矩阵填充后的结果

       

         由结果可以看出,每行每列都有a~e(代表1~5,余数为5并不准确,只是表示在一次大循环中的位置)的余数项x,有k-1=1~5的基底(即填充项的数字),这些基底乘以5,再加上字母代表的余数x就可以还原出填充的结果,还原结果如下,总体的流向为右上。

真实填充的5*5矩阵

         至此,我们理解了实验提供的代码能够生成幻方的原理,模拟了生成奇数阶的幻方的过程。下一步是分析为什么当n为偶数时会报错,而奇数时不会。

n为偶数时的报错分析

        实验的题面已经告诉我们,n为偶数时程序会出错,抛出ArrayIndexOutOfBoundsException,意味着数组越界,显然,越界发生在给magic赋值的时候。

        要分析这个现象的原因,我们可以观察游标的移动。已知当i为n的倍数时,完成了一次大循环,游标向下移动,这就导致在不越界的情况下,下一次大循环的位置为上一次大循环的起始位置向左侧平移一格(列-1)、向下方平移两格(行+2)。如果旧起始位置为(row,col),则新起始位置为(row-2,col-1)。

a1的坐标为(0,2),a2的坐标为(2,1)

        注意到了吗,这是在不越界的情况下才能顺利进行,那么越界的情况呢?根据代码的本意,如果在边界,游标应该传送到边界的另一侧。我们来看下面这段代码。

magic[row][col] = i;
if (i % n == 0) row++;
else {
    if (row == 0) row = n - 1;
    else row--;
    if (col == (n - 1)) col = 0;
    else col++;
}

         可以注意到,代码执行的第一步是赋值,然后判断是否应该调整方向(向下还是向右上),如果不需要调整,才进行越界的判断。那么很明显,如果在调整方向的过程中越界了,越界的调整就不会执行,自然就有了“ArrayIndexOutOfBoundsException”。

        那么为什么只有n为偶数时才会越界呢?我们刚才已经分析了,每个新的起始位置会向旧起始位置的下方两格平移,这种平移需要进行n-1次。

        对奇数来说,除了第0行外还有n-1行。经过(n-1)/2次平移后,正好平移n-1行,起点落到第n-1行(n-1是相对于矩阵坐标的,实际是视觉上的第n行)。再经过(n-1)/2次平移,游标停留在第n-1行,第n/2(下取整)列。

        如下图,经过(n-1)/2次平移后,游标正好从左下角的a3开始循环,循环结束于第一行最后一列,然后从下方的a4开始下一次循环。

        (n-1)/2次平移后在左下角显然也不是巧合,因为每次循环起点向下平移两格的同时也向左平移了一格,在奇数矩阵中正好跨越了中间到两边的距离,所以矩阵副对角线上必然为左下到右上的递增。同理,n-1次循环后结束赋值,结束点必然在起始点的正下方最后一行。这就是,不越界的奥~秘~

e5为赋值的结束点,在a1的正下方

         而对于偶数来说,除了第0行还有n-1行,经过n/2-1次平移后,起始点向下平移了(n/2-1)*2=n-2行,则此次大循环结束时,游标停留在第n-1行(最后一行),再进行row++的操作时,行溢出但直接进入了下次for循环,magic[row][col]越界。可见,当n>2时,无论是哪个偶数,都必然会遇到数组越界的问题。

        同样也可以得出一个结论,除了最后一次大循环,如果大循环的最后一个数位于边界,再将游标向该边界推进就会溢出,对于row++来说,不能位于下边界,对于col--(上文推理得出的,为避免1~5的数字重复,游标可以移动的方向中的另一个),不能位于左边界。

n为偶数无法生成幻方分析

         既然数组会越界,那么我们可以提出相应的对策避免溢出,比如row++时增加一次边界的判断,或者调整游标平移的方向从向下调整为向左(col--,这样不作调整会导致奇数阶溢出)。

        我们运用第一个方法,尝试修改代码使n为偶数时能正常运行,结果如下,可以发现无论是实验中写的判断程序还是自己计算行列值,都不是一个幻方。

第0行的和为108,第0列的和为111

 

         观察下方两个使用不同溢出处理方法得到的4阶“幻方”,可以发现col--策略的偶数阶幻方在列上出现了“acac”的组合,而row++处理越界策略的偶数阶幻方在行上出现了“acac”的组合。

col--生成的4阶“幻方”
row++生成的4阶幻方

         可见,两种处理策略都不可靠,而“acac”的出现,让我有一种siqu的近世代数突然攻击我的感觉……

        (虽然感觉群论用在这里有点大材小用,以及我的表述可能不是很准确)由群的知识可知,模n同余的运算是一个循环群,对于奇数来说,2是它的一个生成元。也就是说,在n为奇数的序列中,任何一个元素加上2再对n求模的运算必然可以得到0~n-1的所有数,将0映射到n,我们便得到了一个1~n的序列。

        在前文的推演中可以看出,每次处理i%n都会使行/列移动两位,这就导致行/列上会以一个元素为起点,反复进行+2取模的操作,最后生成1~n。

        而在偶数n的序列中,显然,2不是模n同余这个群的生成元,故会反复出现奇数位置的字母,而没有偶数位置的。

        综上可以看出,该生成算法只能用于奇数阶幻方的生成,起始位置和处理i%n的方式可能会影响是否越界访问数组和改变元素的位置,但不会影响生成幻方结果的正确性。


 以上仅代表作者本人的思考,学艺不精,百虑难免一疏。如有疏漏,敬请指正。

  • 39
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值