最大方阵和 图解题解
这道题到底在问什么
- 输入
- matrix=[[1,-1],[-1,1]]
- 输出
- 4
- 输入
- matrix=[[1,2,3],[-1,-2,-3],[1,2,3]]
- 输出
- 16
最优解:为什么这么做
一句话答案:LeetCode 1975 最大方阵和:相邻两格能同时翻号,等价于任意两格随便翻,于是最大和只由负数个数的奇偶决定——偶数全取绝对值之和,奇数再减去 2 乘最小绝对值,一遍扫描 O(n²)。
方阵里翻来翻去,到底求的是什么最大值
给一个 n 行 n 列的整数方阵 matrix。一次操作能挑相邻、也就是有公共边的两个格子,把它俩同时乘以 -1,操作次数不限。求折腾到最后,方阵所有元素加起来最大能是多少。题面第二个例子 matrix=[[1,2,3],[-1,-2,-3],[1,2,3]] 答案是 16,翻掉正中那行三个负数即可。
盯着「相邻两格」去搬负号,越搬越乱
负数散落各处,顺着操作去搬就麻烦:哪两个相邻负数凑一块翻成正、落单的再沿哪条路挪去抵消,顺序随负号变多爆炸式增长,方阵稍大就理不清。力气全花在「怎么搬」,可最大和只关心搬完剩下什么。
只能翻相邻两格,为什么等于任意两格随便翻
松开「相邻」这层表象:想同时翻两个不挨着的格子,只要沿一条连起它俩的路径逐段翻相邻两个,中间格子被翻两次抵消复原,最后只有路径两端真的变号。任意两个格子都能被同时变号。
再数一层:每次操作恰好改变两个符号,负号总个数只会加二、减二或不变,奇偶从头到尾锁死。负数是奇数个就永远奇数个、偶数个就永远偶数个。两条合起来,整道题塌缩成一个问题:负数个数是奇是偶。
偶数全清成正,奇数把负号丢给绝对值最小的格
先不看正负,每个数的绝对值都是想收进总和的那份。负数个数是偶数时,两两配对、每对同时翻正,一个负号都不剩,答案就是所有元素绝对值之和。
是奇数时,配到最后必剩一个负号清不掉,得让某个格子背着。一个本该加绝对值 a 的格子被压成负,总和从 +a 掉到 -a,损失 2a。要让损失最小,就把负号丢给绝对值最小的格子,答案是绝对值之和减去 2 乘最小绝对值。这个格子原本正负都无所谓;方阵里有 0 时,负号塞给它损失为零,绝对值之和照拿。
题面例二一遍扫描得 16
拿题面第二个例子走一遍。一次扫过九个格子,同时攒三样东西:绝对值之和 s、负数个数 cnt、最小绝对值 mi。
第一行 1、2、3 是正数,s 到 6,cnt 仍 0,mi 刷到 1。第二行 -1、-2、-3,绝对值 1、2、3 接着加,s 到 12,三个负数把 cnt 顶到 3,mi 碰到 1 不再更小。第三行再加 1、2、3,s 到 18,cnt 不动,mi 仍是 1。扫完 s=18、cnt=3、mi=1;cnt 是奇数,答案 = 18 − 2×1 = 16,与答案一致。第一个例子 [[1,-1],[-1,1]] 绝对值之和 4、负数 2 个是偶数,直接返回 4。
一遍扫完 O(n²),真正会栽的是溢出和那个 0
复杂度上,n 乘 n 个元素只扫一遍,每格做取绝对值、比大小、累加这些常数操作,时间 O(n²);只留 s、cnt、mi 三个标量、不排序不开数组,空间 O(1)。
两个地方最容易栽。求和变量别用 32 位整数——n 到 250、单个绝对值到十万,和最坏能到六十多亿,早超过 int 上限,得 C++ 用 long long、Java 用 long,Python 无此上限。另一处是 0:它的绝对值是 0,天然就是最小绝对值,于是负数个数即便为奇,把负号塞给它损失也是零,公式原样覆盖、无需特判。反过来,整张方阵全是负数、但个数为偶时,也照样两两翻正、一个不剩。
▶ 动画逐步走查(共 24 步)——想跟着动画一帧帧对照就展开
- 3记牢这句话:符号翻转能把任意两个格子一起翻。没有 0 时,真正被卡死的只有负数个数的奇偶,偶数能全清成正、奇数得留一个负号;有 0 时多出的负号可被 0 吸收,相当于最小绝对值是 0。下面一遍扫描,把绝对值之和、负数个数、最小绝对值三样东西同时数出来。
- 4扫描前先看清这个 3 乘 3 的方阵。标红的三个格子是负数,分别是 (0,1) 的 -3,(1,0) 的 -4,(2,1) 的 -7,一共三个负数。其余都是正数。我们要做的是一遍扫描,把每个格子的绝对值加起来,同时数清负数有几个、绝对值最小的是多少。
- 5读取中扫到第 1 个格子 (0,0),它的值是 2,是个正数,绝对值是 2。先把它举起来看清楚,下一拍再把它算进三个累计量里。
- 6已扫 1 格把绝对值 2 加进总和,和从 0 变成 2。它是正数,负数个数还是 0。它的绝对值 2 比之前更小,最小绝对值刷新成 2。这一格结算完毕,给它上色收进已扫的行列。
- 7读取中扫到第 2 个格子 (0,1),它的值是 -3,是个负数,绝对值是 3。先把它举起来看清楚,下一拍再把它算进三个累计量里。
- 8已扫 2 格把绝对值 3 加进总和,和从 2 变成 5。它是负数,负数个数加到 1。最小绝对值仍是 2。这一格结算完毕,给它上色收进已扫的行列。
- 9读取中扫到第 3 个格子 (0,2),它的值是 5,是个正数,绝对值是 5。先把它举起来看清楚,下一拍再把它算进三个累计量里。
- 10已扫 3 格把绝对值 5 加进总和,和从 5 变成 10。它是正数,负数个数还是 1。最小绝对值仍是 2。这一格结算完毕,给它上色收进已扫的行列。
- 11读取中扫到第 4 个格子 (1,0),它的值是 -4,是个负数,绝对值是 4。先把它举起来看清楚,下一拍再把它算进三个累计量里。
- 12已扫 4 格把绝对值 4 加进总和,和从 10 变成 14。它是负数,负数个数加到 2。最小绝对值仍是 2。这一格结算完毕,给它上色收进已扫的行列。
- 13读取中扫到第 5 个格子 (1,1),它的值是 1,是个正数,绝对值是 1。先把它举起来看清楚,下一拍再把它算进三个累计量里。
- 14已扫 5 格把绝对值 1 加进总和,和从 14 变成 15。它是正数,负数个数还是 2。它的绝对值 1 比之前更小,最小绝对值刷新成 1。这一格结算完毕,给它上色收进已扫的行列。
- 15读取中扫到第 6 个格子 (1,2),它的值是 6,是个正数,绝对值是 6。先把它举起来看清楚,下一拍再把它算进三个累计量里。
- 16已扫 6 格把绝对值 6 加进总和,和从 15 变成 21。它是正数,负数个数还是 2。最小绝对值仍是 1。这一格结算完毕,给它上色收进已扫的行列。
- 17读取中扫到第 7 个格子 (2,0),它的值是 8,是个正数,绝对值是 8。先把它举起来看清楚,下一拍再把它算进三个累计量里。
- 18已扫 7 格把绝对值 8 加进总和,和从 21 变成 29。它是正数,负数个数还是 2。最小绝对值仍是 1。这一格结算完毕,给它上色收进已扫的行列。
- 19读取中扫到第 8 个格子 (2,1),它的值是 -7,是个负数,绝对值是 7。先把它举起来看清楚,下一拍再把它算进三个累计量里。
- 20已扫 8 格把绝对值 7 加进总和,和从 29 变成 36。它是负数,负数个数加到 3。最小绝对值仍是 1。这一格结算完毕,给它上色收进已扫的行列。
- 21读取中扫到第 9 个格子 (2,2),它的值是 2,是个正数,绝对值是 2。先把它举起来看清楚,下一拍再把它算进三个累计量里。
- 22已扫 9 格把绝对值 2 加进总和,和从 36 变成 38。它是正数,负数个数还是 3。最小绝对值仍是 1。这一格结算完毕,给它上色收进已扫的行列。
- 23扫描完成九个格子全部扫完。绝对值之和是 38,负数个数是 3,绝对值最小的是 1,就在中心那个正数格 (1,1)。三个数据齐了,接下来只看一件事:负数个数是奇是偶。
- 24奇偶裁决负数个数是 3,是个奇数。这个例子里没有 0,前面说过每次操作翻两个符号,非零格子里负号的奇偶变不了,奇数就永远是奇数。所以无论怎么折腾,最后至少要剩一个负号,清不干净。既然躲不掉,那就想办法让这一个负号带来的损失尽可能小。
- 25牺牲最小格三个负号里,通过翻转可以把其中两个配对清成正,剩下这一个负号,我们不给别人,专门丢给绝对值最小的格子 (1,1)。注意它原本是正数 1,现在被牺牲成 -1。为什么选它,因为一个本该加 1 的格子变成减 1,一来一回损失是 2 乘 1,这个损失在所有格子里最小。
- 26答案 = 36最终布局:八个格子都是正的,只有中心 (1,1) 留成 -1。总和等于绝对值之和 38 减去被牺牲的那 2 乘 1,也就是 38 减 2,等于 36。这就是这个方阵能达到的最大和。
⚠️ 容易写错的地方
✗ 错:用 32 位 int 累加绝对值之和
✓ 对:C 加加 用 long long、Java 用 long,Python 天然不溢出
n 到 250、元素绝对值到十万,和最坏约六十多亿,远超 int 上限,溢出会得到负数或错值
✗ 错:以为操作只能翻相邻两格、清不掉隔开的负号
✓ 对:沿路径连翻,中间格抵消,任意两格可同时翻号
看不透这一层就想不到答案只由负数个数的奇偶决定,会去纠结具体怎么移动而算不出来
✗ 错:奇数情形把某个原来的负数留成负号
✓ 对:留负号的应是绝对值最小的格子,不管它原来正负
损失是 2 乘留负格的绝对值,要让损失最小就得选绝对值最小的那个,它常常是个正数
✗ 错:忘了方阵里可能有 0
✓ 对:0 的绝对值是 0,会自然成为最小绝对值
有 0 时哪怕负数个数是奇数,把负号丢给 0 损失是 0,答案仍是绝对值之和,mi 取到 0 时公式自动覆盖
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def maxMatrixSum(self, matrix: List[List[int]]) -> int:
mi = inf
s = cnt = 0
for row in matrix:
for x in row:
cnt += x < 0
y = abs(x)
mi = min(mi, y)
s += y
return s if cnt % 2 == 0 else s - mi * 2C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
long long maxMatrixSum(vector<vector<int>>& matrix) {
long long s = 0;
int mi = 1 << 30, cnt = 0;
for (const auto& row : matrix) {
for (int x : row) {
cnt += x < 0 ? 1 : 0;
int y = abs(x);
mi = min(mi, y);
s += y;
}
}
return cnt % 2 == 0 ? s : s - mi * 2;
}
};Java
import java.util.*;
class Solution {
public long maxMatrixSum(int[][] matrix) {
long s = 0;
int mi = 1 << 30, cnt = 0;
for (int[] row : matrix) {
for (int x : row) {
cnt += x < 0 ? 1 : 0;
int y = Math.abs(x);
mi = Math.min(mi, y);
s += y;
}
}
return cnt % 2 == 0 ? s : s - mi * 2;
}
}复杂度
时间
O(n²)
n 行 n 列共 n 平方个元素,只需一遍扫描,每个元素做的是取绝对值、比较、累加这些常数操作,总量随元素个数线性增长,对边长 n 就是平方级
空间
O(1)
只用了 s、cnt、mi 这三个标量,不随方阵规模增长。没有排序、没有额外数组,是货真价实的常数空间
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最大方阵和 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么最后只看负数个数的奇偶,跟具体怎么操作没关系?+
因为每次操作把相邻两格同时乘 -1,恰好改变两个符号,负号总数只会加二、减二或不变,奇偶从头到尾不变;又因为沿一条路径逐段翻,可以做到只让路径两端两格变号、中间全部复原,等于任意两格都能自由变号。于是能不能把负号清光,只取决于它一开始是奇数个还是偶数个,跟你按什么顺序去翻毫无关系。
奇数个负数时,为什么把负号留给绝对值最小的格子,而且它可能是正数?+
留一个负号,意味着某个绝对值为 a 的格子从 +a 变成 -a,总和少了 2a。要让这份损失最小,自然选 a 最小的格子,也就是绝对值最小的那个,不管它原来是正是负。题面第二个例子里绝对值最小的是正数 1,就把它压成 -1,损失只有 2。如果方阵里有 0,负号丢给 0 损失为零,等于没损失。
求和为什么要提醒用 long,int 装不下吗?+
n 最大 250,格子数到 62500,单个元素绝对值可到十万,绝对值之和最坏约六十多亿,而 32 位有符号整数上限才二十一亿出头,直接溢出会得到负数或错值。所以 C++ 要用 long long、Java 要用 long;Python 整数没有位宽限制,不用操心这点。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最大方阵和 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。