获取单值网格的最小操作数 图解题解
这道题到底在问什么
- 输入
- grid=[[2,4],[6,8]], x=2
- 输出
- 4
- 输入
- grid=[[1,2],[3,4]], x=2
- 输出
- -1(1、3 是奇数,2、4 是偶数,除 2 余数不同)
先想最直接的笨办法
取正中间那个数当目标。9 个数,下标从 0 到 8,中间是下标 4,值是 6。参考代码里就写成 nums 长度右移一位,也就是 9 除以 2 取整等于 4。把 6 定为大家要一起走到的目标值。下面挨个算,每个数走到 6 要几步。(动画第 17 步)
最优解:一步一步想明白
- 3记牢这套路:第一步查同余定生死,第二步排序取中位数当目标,第三步把每个数到中位数的距离除以 x 累加。下面从拍扁网格开始。
- 4要把 9 个格子变成同一个值这就是我们的输入,一张 3 行 3 列的网格,右上角记着 x 等于 2。每次操作能给任意一个格子加 2 或减 2。你先扫一眼这 9 个数,全是偶数。接下来第一件事,是把这张二维网格拍扁成一条一维的数,处理起来更顺手。
- 5把网格一行一行读下来,拉成一条一维数组:8、2、6、4、10、8、6、4、2,一共 9 个数。位置怎么排其实无所谓,因为最后只关心它们各自离目标有多远。拍扁之后,先别急着算,得先回答一个生死问题:这些数到底能不能凑到同一个值上。
- 6从第 0 个数 8 开始。参考代码拿它除以 x 的余数当作基准:8 除以 2 余 0,于是把 mod 定成 0。之后每一个数都要拿来和这个 0 对比,只要有一个余数不是 0,整张网格就没救了。
- 7轮到第 1 个数 2,除以 2 余 0,正好等于基准 mod 的 0,过关。它和 8 之间相差的是 2 的整数倍,理论上能靠若干次加减 2 走到一块去。绿色表示这个数已经通过同余检查。
- 8轮到第 2 个数 6,除以 2 余 0,正好等于基准 mod 的 0,过关。它和 8 之间相差的是 2 的整数倍,理论上能靠若干次加减 2 走到一块去。绿色表示这个数已经通过同余检查。
- 9轮到第 3 个数 4,除以 2 余 0,正好等于基准 mod 的 0,过关。它和 8 之间相差的是 2 的整数倍,理论上能靠若干次加减 2 走到一块去。绿色表示这个数已经通过同余检查。
- 10轮到第 4 个数 10,除以 2 余 0,正好等于基准 mod 的 0,过关。它和 8 之间相差的是 2 的整数倍,理论上能靠若干次加减 2 走到一块去。绿色表示这个数已经通过同余检查。
- 11轮到第 5 个数 8,除以 2 余 0,正好等于基准 mod 的 0,过关。它和 8 之间相差的是 2 的整数倍,理论上能靠若干次加减 2 走到一块去。绿色表示这个数已经通过同余检查。
- 12轮到第 6 个数 6,除以 2 余 0,正好等于基准 mod 的 0,过关。它和 8 之间相差的是 2 的整数倍,理论上能靠若干次加减 2 走到一块去。绿色表示这个数已经通过同余检查。
- 13轮到第 7 个数 4,除以 2 余 0,正好等于基准 mod 的 0,过关。它和 8 之间相差的是 2 的整数倍,理论上能靠若干次加减 2 走到一块去。绿色表示这个数已经通过同余检查。
- 14轮到第 8 个数 2,除以 2 余 0,正好等于基准 mod 的 0,过关。它和 8 之间相差的是 2 的整数倍,理论上能靠若干次加减 2 走到一块去。绿色表示这个数已经通过同余检查。
- 159 个数全部检查完,除以 2 的余数清一色是 0,同余成立,这张网格一定能变成单值,不用返回 -1。生死关过了,接下来才是正题:选一个共同目标,让总操作数最少。
- 16把这 9 个数从小到大排好,变成 2、2、4、4、6、6、8、8、10。排序是为了下一步好取中位数。你可能会问,为什么不直接取平均值当目标,先按住这个疑问,等会儿有一帧专门算给你看,平均值反而更亏。
- 17取正中间那个数当目标。9 个数,下标从 0 到 8,中间是下标 4,值是 6。参考代码里就写成 nums 长度右移一位,也就是 9 除以 2 取整等于 4。把 6 定为大家要一起走到的目标值。下面挨个算,每个数走到 6 要几步。
- 18看下标 0 这个数 2。它和目标 6 相差 4,而每次操作动 2,所以 要加 2 次 2,折合 2 次操作。把 2 加进累计,现在 ops 等于 2。蓝色的都是已经结算过的数。
- 19看下标 1 这个数 2。它和目标 6 相差 4,而每次操作动 2,所以 要加 2 次 2,折合 2 次操作。把 2 加进累计,现在 ops 等于 4。蓝色的都是已经结算过的数。
- 20看下标 2 这个数 4。它和目标 6 相差 2,而每次操作动 2,所以 要加 1 次 2,折合 1 次操作。把 1 加进累计,现在 ops 等于 5。蓝色的都是已经结算过的数。
- 21看下标 3 这个数 4。它和目标 6 相差 2,而每次操作动 2,所以 要加 1 次 2,折合 1 次操作。把 1 加进累计,现在 ops 等于 6。蓝色的都是已经结算过的数。
- 22看下标 4 这个数 6。它和目标 6 相差 0,而每次操作动 2,所以 本身就是 6,不用动,折合 0 次操作。把 0 加进累计,现在 ops 等于 6。蓝色的都是已经结算过的数。
- 23看下标 5 这个数 6。它和目标 6 相差 0,而每次操作动 2,所以 本身就是 6,不用动,折合 0 次操作。把 0 加进累计,现在 ops 等于 6。蓝色的都是已经结算过的数。
- 24看下标 6 这个数 8。它和目标 6 相差 2,而每次操作动 2,所以 要减 1 次 2,折合 1 次操作。把 1 加进累计,现在 ops 等于 7。蓝色的都是已经结算过的数。
- 25看下标 7 这个数 8。它和目标 6 相差 2,而每次操作动 2,所以 要减 1 次 2,折合 1 次操作。把 1 加进累计,现在 ops 等于 8。蓝色的都是已经结算过的数。
- 26看下标 8 这个数 10。它和目标 6 相差 4,而每次操作动 2,所以 要减 2 次 2,折合 2 次操作。把 2 加进累计,现在 ops 等于 10。蓝色的都是已经结算过的数。
- 279 个数全部结算完,把它们各自走到 6 需要的次数加起来,一共是 10。这就是把整张网格变成单值网格的最少操作数。回头看整条链路:先判同余保证有解,再排序取中位数当目标,最后把每个数的距离折算成次数求和,答案 10。
⚠️ 容易写错的地方
✗ 错:直接取平均值当共同目标
✓ 对:排序后取中位数当目标
让绝对距离之和最小的是中位数不是平均值,平均值还可能是小数、不满足同余,根本当不了目标
✗ 错:不判同余就直接开算
✓ 对:先确认所有数除以 x 的余数相同,否则返回 -1
余数不同的两个数,靠加减 x 永远走不到一起,不先拦住就会算出一个没意义的数
✗ 错:用最大值减最小值估操作数
✓ 对:必须逐个数到中位数的距离折算次数再累加
操作数是每个元素各自移动次数之和,不是极差,极差只反映了两端、漏掉中间所有数
✗ 错:偶数个元素时纠结中位数取哪个
✓ 对:排序后直接取 nums[n >> 1]
偶数个时上中位数和下中位数给出的总距离完全相同,取长度右移一位那个下标即可,不必特殊处理
完整代码(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 minOperations(self, grid: List[List[int]], x: int) -> int:
nums = []
mod = grid[0][0] % x
for row in grid:
for v in row:
if v % x != mod:
return -1
nums.append(v)
nums.sort()
mid = nums[len(nums) >> 1]
return sum(abs(v - mid) // x for v in nums)C++
#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:
int minOperations(vector<vector<int>>& grid, int x) {
int m = grid.size(), n = grid[0].size();
int mod = grid[0][0] % x;
int nums[m * n];
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] % x != mod) {
return -1;
}
nums[i * n + j] = grid[i][j];
}
}
sort(nums, nums + m * n);
int mid = nums[(m * n) >> 1];
int ans = 0;
for (int v : nums) {
ans += abs(v - mid) / x;
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int minOperations(int[][] grid, int x) {
int m = grid.length, n = grid[0].length;
int[] nums = new int[m * n];
int mod = grid[0][0] % x;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] % x != mod) {
return -1;
}
nums[i * n + j] = grid[i][j];
}
}
Arrays.sort(nums);
int mid = nums[nums.length >> 1];
int ans = 0;
for (int v : nums) {
ans += Math.abs(v - mid) / x;
}
return ans;
}
}复杂度
时间
O(mn log(mn))
m 行 n 列共 mn 个数。同余检查和距离求和都是扫一遍 O(mn),真正主导的是中间那次排序,量级 O(mn log(mn))
空间
O(mn)
按峰值算。要把网格拍扁存进一维数组,占 O(mn)。排序内部另有栈开销:C++ 与 Java 约 O(log(mn)) 递归栈,Python 的 Timsort 最坏 O(mn),都不超过数组本身量级
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 获取单值网格的最小操作数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题的核心思路是什么?+
三步:先把网格拍扁,检查所有数除以 x 的余数是否相同,不同就返回 -1;相同则排序取中位数当共同目标;最后把每个数到中位数的距离除以 x 累加,就是最少操作数。
为什么共同目标一定要取中位数,能证明吗?+
把所有数排好,考察目标从中位数往任一方向移动一小步。移向哪一边,那一边的数到目标的距离各减少这一步,另一边各增加这一步。而中位数两侧的数量此消彼长,离开中位数总会让多数那一侧的增量压过少数一侧的减量,总距离不减反增。所以绝对距离之和在中位数处取到最小。偶数个时,两个中位数之间任意取值总距离相同。
复杂度是多少?+
设网格共 mn 个数。同余检查和距离求和各扫一遍是 O(mn),中间排序 O(mn log(mn)) 是主导,所以时间 O(mn log(mn))。空间上要存拍扁后的一维数组,是 O(mn),排序内部栈开销不超过这个量级。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 获取单值网格的最小操作数 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。