刷 LeetCode 1895 之前,我对“幻方”的印象还停留在小学奥数的九宫格填数,以为又是什么构造题,结果读完题目发现完全不是一回事。这题要求在一个 m x n 的矩阵里,找出边长最大的正方形子矩阵,满足这个子矩阵的每一行、每一列、两条对角线的元素和全部相等。说白了,考的是前缀和、枚举顺序、还有边界处理,难度定在“中等”也算合理,但如果不提前想清楚,写起来很容易被各种下标绕晕。
我刷完这道题最大的感受是:它本身并不难,难的是把“检查一个子矩阵是否满足条件”的操作从 O(k) 降到 O(1)。只要想通这一点,整道题的代码量其实非常小。这篇文章我会把从读题到 AC 的完整思考过程写出来,包括四张前缀表的搭建思路、判断幻方时的边界陷阱,还有我实际调试时踩过的坑。如果你正在刷矩阵类题目,或者准备面试想巩固一下前缀和的用法,这篇应该对你有帮助。
1. 题目到底在说什么:最大幻方的问题拆解
1.1 幻方不是玄学,定义一句话就能讲清楚
题目里的“幻方”不是让你去构造 3x3 的九宫格,它的定义很简单:一个 k x k 的正方形子矩阵,如果所有行的和、所有列的和、两条对角线的和全部相等,那这个子矩阵就是幻方,k 就是这个幻方的边长。
举个例子,一个 2x2 的矩阵:
text复制2 7
3 6
第一行和是 9,第二行和是 9,第一列和是 5,第二列和是 13,主对角线 2+6=8,副对角线 7+3=10。这些和并不全相等,所以它不是幻方。但如果矩阵是:
text复制2 7
7 2
第一行 9,第二行 9,第一列 9,第二列 9,两个对角线也都是 9,这就是一个合法幻方。
还有一个容易被忽略的点:边长 k=1 的单个格子一定是幻方。因为一个元素单独形成的矩阵,行和、列和、对角线全都是它自己,天然相等。所以这道题的答案至少是 1,不存在“找不到幻方”返回 0 或 -1 的情况。这个性质看似废话,但它决定了我们枚举边长时可以从大到小直接返回,也决定了最坏情况下一定有个兜底值。
1.2 数据范围藏着解题线索
看数据范围永远是刷题的第一步。这道题里 m 和 n 都小于等于 50,矩阵元素值不超过 10^5。
50 这个规模意味着什么?如果完全暴力,枚举所有可能的正方形子矩阵:边长 k 有 50 种可能,每种边长下左上角起点有大约 (m-k+1)*(n-k+1) 个,检查一个 k x k 矩阵是否满足幻方需要算 k 行、k 列、2 条对角线,也就是 O(k) 次求和。最坏情况下总操作量大约在千万级别,理论上暴力加一点优化也不是完全不能过。但 LeetCode 考这种题,重点不是让你卡着时间过,而是考察你有没有意识到“重复计算”的问题。
同一个格子会在不同行、不同列、不同对角线里被反复累加很多次。假设我们暴力检查一个 50x50 的矩阵,左上角在 (0,0)、边长为 50 时,第一行会被算一次,第一列会被算一次,主对角线会被算一次,这些和之间没有任何复用。可如果我们提前把“某一行从第几列到第几列的和”“某一列从第几行到第几行的和”“从某个点出发沿对角线到边界的和”都存下来,那么任何一个 k x k 子矩阵的行、列、对角线求和都可以变成一次减法,判断时间从 O(k) 变成 O(1)。这才是这题真正想教的东西:用空间换时间,用前缀和把重复计算缓存起来。
1.3 枚举方向决定代码简洁度
确定用前缀和之后,下一步就是枚举策略。要找的是“最大的幻方”,也就是边长 k 最大的子矩阵。最直观的想法是从小到大枚举 k,把所有满足条件的结果记录下来,最后取最大值。这样当然可以,但你会发现代码里要多维护一个 ans 变量,而且即使找到了一个很大的合法幻方,只要后面还有更大的可能,你就不能提前退出,必须把所有情况都遍历完。
更好的做法是从大到小枚举 k,从 min(m, n) 开始往下递减,一旦发现某个 k 下存在合法幻方,直接返回这个 k。因为 k 是从大到小枚举的,第一个命中的一定是最大的。这样既省去了 ans 变量,又能在大多数情况下提前结束循环,代码逻辑也更贴近人的直觉。我自己写的时候一开始用的从小到大,后面发现从大到小明显清爽得多,算是这道题第一个值得记住的小技巧。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 四张前缀表:把重复计算压成 O(1)
2.1 行前缀和:一维想法的横向延伸
先来说最基础的行前缀和。如果你刷过“区域和检索”或者“一维前缀和”这类题,对下面的写法肯定不陌生:用 rowSum[i][j] 表示第 i 行从第 0 列到第 j-1 列的元素和。注意这里我用的是“第 j-1 列”,也就是说 rowSum[i][j] 的下标 j 比实际列数多偏移了一位,这种写法是为了让区间 [left, right] 的和可以直接用 rowSum[right+1] - rowSum[left] 表示,避免处理减一的问题。
具体到这道题,我们想要求第 r 行从 l 列到 r 列(左闭右闭)的和,只需:
text复制rowSum[r][r+1] - rowSum[r][l]
构建的代码也很简单:
cpp复制vector<vector<int>> rowSum(m, vector<int>(n + 1, 0));
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
rowSum[i][j + 1]
