Skip to content

题解

像这样,如果一道题要求我们每一步该干什么,而我们确实让代码一步步照着它说的做就可以解决它,那么这道题就是“模拟题”。

模拟题会考验你的细致程度和耐心程度。

对于这道题,复杂的规则其实可以用比较简单的说法统一描述:

  • 从第一行的中间开始填。
  • 一直往右上方向走,一边走一边填数。如果碰到了边界,那就从另一边出来。就像吃豆人小游戏一样:吃豆人如果走向屏幕右侧边界,那它就会从左侧同一高度重新出现。对于其他方向的边界也是一样的。
  • 如果要往右上填数时发现右上这个地方已经填过了,那么这一步就不往右上走,而是往下走。如果碰到边界,也是一样要循环。

这样的“边界循环”和模算术有着很好的关系。我们引入数论中的模运算。 \(x \bmod y\) 表示 \(x\) 除以 \(y\) 所得的余数,要求 \(y > 0\) 。结果一定落在区间 \([0, y)\) 之中。

我们以一维的格子为例。如果有 \(10\) 个格子,编号为 \(0, 1, 2, \ldots, 9\) ,现在有一个小人站在编号为 \(7\) 的格子上。小人可以向左、向右走,在超出边界时会从另一边出现。现在想知道,如果小人往右走了 \(26\) 格 ,那么他最终所在格子的编号是几呢?这可以通过计算 \((7 + 26) \bmod 10 = 3\) 得到结果。与之类似,向左走 \(26\) 格后所在格子的编号将是 \((7 - 26) \bmod 10 = 1\) 。

数论的 \(\bmod\) 运算和 C 或 C++ 的 % 运算符不太一样。准确地说,是 C99 标准及以后的 C,以及 C++11 标准及以后的 C++。

在接下来代码里,我们定义了一个函数 ntmod 来表示数论中的 \(\bmod\) 运算。

如果要了解更多关于模、余数的事情,可以参考我的博客文章 带余除法策略 。

完整代码

C:

#include <stdio.h>

// 全局变量 int 数组会自动初始化为全 0。
int mat[39][39];

// 数论 mod。
// i = ntmod(i + 1, n); 就是向下走一格。
// i = ntmod(i - 1, n); 就是向上走一格。
// j = ntmod(j + 1, n); 就是向右走一格。
// j = ntmod(j - 1, n); 就是向左走一格。
int ntmod(int a, int n)
{
    const int res = a % n;
    return res < 0 ? res + n : res;
}

// 向全局变量 mat 填入幻方内容。
// 参数 n 表示应该填多大范围。
void fill_in(int n)
{
    // 当前坐标。这里用了 x y 而不是 i j,因为循环变量叫 i 了。
    int x = 0;
    int y = (n - 1) / 2;
    for (int i = 1; i <= n * n; ++i) {
        // 填数
        mat[x][y] = i;
        // 算出当前坐标右上方的坐标
        const int nx = ntmod(x - 1, n);
        const int ny = ntmod(y + 1, n);
        if (mat[nx][ny]) {
            // 若右上方已经填入了数,则向下走
            x = ntmod(x + 1, n);
        } else {
            // 否则就向右上走
            x = nx;
            y = ny;
        }
    }
}

// 将全局变量 mat 中的幻方内容打印出来。
// 参数 n 表示应该打印多大范围的。
void print_out(int n)
{
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            printf("%d ", mat[i][j]);
        }
        putchar('\n');
    }
}

int main()
{
    int n;
    scanf("%d", &n);
    fill_in(n);
    print_out(n);
}

C++:

#include <iostream>
#include <vector>

// 数论 mod。
// i = ntmod(i + 1, n); 就是向下走一格。
// i = ntmod(i - 1, n); 就是向上走一格。
// j = ntmod(j + 1, n); 就是向右走一格。
// j = ntmod(j - 1, n); 就是向左走一格。
int ntmod(int a, int n)
{
    const int res = a % n;
    return res < 0 ? res + n : res;
}

struct MagicSquare {
    std::vector<std::vector<int>> mat;

    // 构造函数同时承担了分配内存和填入幻方内容的职责。
    MagicSquare(int n)
        : mat(n, std::vector<int>(n, 0))
    {
        // 当前坐标。这里用了 x y 而不是 i j,因为循环变量叫 i 了。
        int x = 0;
        int y = (n - 1) / 2;
        for (int i = 1; i <= n * n; ++i) {
            // 填数
            mat[x][y] = i;
            // 算出当前坐标右上方的坐标
            const int nx = ntmod(x - 1, n);
            const int ny = ntmod(y + 1, n);
            if (mat[nx][ny]) {
                // 若右上方已经填入了数,则向下走
                x = ntmod(x + 1, n);
            } else {
                // 否则就向右上走
                x = nx;
                y = ny;
            }
        }
    }

    friend std::ostream& operator<<(std::ostream& os, const MagicSquare& sq)
    {
        for (const auto& row : sq.mat) {
            for (const int cell : row) {
                os << cell << ' ';
            }
            os << '\n';
        }
        return os;
    }
};

int main()
{
    int n;
    std::cin >> n;
    std::cout << MagicSquare{n};
}