最小操作次数使数组元素相等 图解题解
这道题到底在问什么
- 输入
- nums=[1,2,3]
- 输出
- 3 ([1,2,3]→[2,3,3]→[3,4,3]→[4,4,4])
- 输入
- nums=[1,1,1]
- 输出
- 0 (本来就相等,不用操作)
先想最直接的笨办法
扫完了,最小值是绿色这个 1。接下来每个数都要减到 1,灰色这些就是要往下降的。一个一个算它们各要减几次。(动画第 12 步)
最优解:为什么这么做
一句话答案:LeetCode 453 最小操作次数使数组元素相等:每次给 n−1 个数加 1 等价于给 1 个数减 1,把所有数减到最小值即可,答案 = sum − n×min,时间 O(n)、空间 O(1)。
每次给 n−1 个数加 1,最少几次能让全员相等
给一个长度为 n 的整数数组 nums,一次操作要挑其中 n−1 个元素、各加 1(只留一个数不动),问最少操作几次能让所有元素相等。题面例子 nums=[1,2,3] 答案是 3,nums=[1,1,1] 本来就齐,答案 0。数据不小,n 到 10^5、每个数到 10^9。
照着「每次挑 n−1 个加 1」硬模拟,会卡在哪
按字面模拟:每轮把当前最大的那个留住不动、其余全体加 1,直到齐平。可答案本身就能到 10^9 量级——[1,2,3] 才 3 次,换成几个相差上亿的数,就得循环上亿轮、每轮还要扫一遍数组,稳稳超时。更别扭的是盯着「往上加」,数越加越大,谁也说不清要加到多高才停。
把「加 n−1 个」翻译成「减 1 个」
让一堆数相等,真正在意的只是它们之间的相对差——谁比谁高出多少。给 n−1 个数各加 1,那个没被加到的数,相对大家就矮了 1,这和「单独把它减 1」的效果一模一样。于是每次操作都能等价看成「挑一个数减 1」。
视角一翻转,目标就从「一起往上加到某个高度」变成「各自往下减到某个高度」。而往下减,大家减到最小值 min 就到头了:min 本身没法再降(想让它更低,反而要给别人多减),比它高的数往下够到它就行。
减到最小值,答案就落成 sum − n×min
既然每次减 1、都往 min 靠,那第 i 个数 nums[i] 要减的次数,就是它高出 min 的那部分 nums[i] − min。把每个数各自要减的次数加起来,就是总操作数:(nums[0]−min) + (nums[1]−min) + … 一共 n 项,每项都扣掉一个 min,合起来正好是 sum − n×min。连 min 都不必真的一次次减,求出 sum 和 min 一步代入即可。
拿 nums=[1,2,3] 亲手套一遍
先求总和 sum = 1+2+3 = 6,最小值 min = 1,长度 n = 3。代入 sum − n×min = 6 − 3×1 = 3,答案就是 3。拆开看更踏实:翻译成减 1 之后,1 已经是最小、减 0 次,2 要减到 1 是 1 次,3 要减到 1 是 2 次,0+1+2 = 3,和公式合得上,也正是题面给的 3。
再验一遍 nums=[1,1,1]:sum = 3、min = 1、n = 3,3 − 3×1 = 0。三个数本来就相等,一次都不用动,公式自动给 0。
别真去循环模拟,min 和溢出这两处最容易栽
复杂度上,求 sum 扫一遍、求 min 扫一遍,都是线性,时间 O(n);只用「和」与「最小值」两个变量,空间 O(1)。
有几处容易栽跟头。基准一定得选最小值 min:要是贪图「让大家一起升到最大值 max」,前面那层「加等价于减」的转化就不成立,步数只会更多。再就是溢出——n 到 10^5、元素到 10^9,sum 和 n×min 这类中间量乘出来早破了 32 位整数的上限,C++、Java 得用 long(Python 整数天生没有上限),只在最后结果转回 int。边界也顺手:全相等时 sum − n×min 自然是 0,单个元素同理为 0,两个数套公式即得。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住这条转化:加 n-1 个 ≡ 减 1 个。基准是「最小值」,答案 = 总和减去 n 乘最小值。下面逐帧套它。
- 4先把数组摆出来:4、1、3、2、5、2,一共 6 个数,和是 17。直接想「往上加」会乱,换个角度看。
- 5核心转化:给 n-1 个各加 1,等于把剩下那个相对减 1。只看相对差,就把「加」翻译成了「减」。那大家都减到谁?减到最小的那个最省。
- 6先扫一遍找最小值。第 0 个是 4,暂定它是最小,绿色标住。
- 7第 1 个是 1,比当前最小 4 还小,最小值更新成 1,绿色挪到这里。
- 8第 2 个是 3,不小于当前最小 1,最小值不变,绿色仍在下标 1。
- 9第 3 个是 2,不小于当前最小 1,最小值不变,绿色仍在下标 1。
- 10第 4 个是 5,不小于当前最小 1,最小值不变,绿色仍在下标 1。
- 11第 5 个是 2,不小于当前最小 1,最小值不变,绿色仍在下标 1。
- 12扫完了,最小值是绿色这个 1。接下来每个数都要减到 1,灰色这些就是要往下降的。一个一个算它们各要减几次。
- 13轮到下标 0 的 4。它要降到 1,得减 4 − 1 = 3 次。
- 14把这 3 次加进总数,累计操作变成 3。下标 0 结算完,变绿。
- 15轮到下标 1 的 1。它要降到 1,得减 1 − 1 = 0 次,它本来就是最小,一次都不用减。
- 16把这 0 次加进总数,累计操作变成 3。下标 1 结算完,变绿。
- 17轮到下标 2 的 3。它要降到 1,得减 3 − 1 = 2 次。
- 18把这 2 次加进总数,累计操作变成 5。下标 2 结算完,变绿。
- 19轮到下标 3 的 2。它要降到 1,得减 2 − 1 = 1 次。
- 20把这 1 次加进总数,累计操作变成 6。下标 3 结算完,变绿。
- 21轮到下标 4 的 5。它要降到 1,得减 5 − 1 = 4 次。
- 22把这 4 次加进总数,累计操作变成 10。下标 4 结算完,变绿。
- 23轮到下标 5 的 2。它要降到 1,得减 2 − 1 = 1 次。
- 24把这 1 次加进总数,累计操作变成 11。下标 5 结算完,变绿。
- 25每个元素要减的次数加起来,正好等于 sum 减去 n 乘 min:17 − 6×1 = 11。所以最少操作 11 次,全员降到 1。
⚠️ 容易写错的地方
✗ 错:真去循环模拟「每次给 n-1 个加 1」
✓ 对:套公式 sum - n×min 直接出
数值可达 10^9 量级,逐次模拟会超时
✗ 错:基准选成最大值(让大家升到 max)
✓ 对:基准是最小值 min
等价成「减 1」后,降到最小才最省,升到 max 反而更多步
✗ 错:用 32 位 int 累加 sum、算 n×min 导致中间值溢出
✓ 对:累加和与中间乘积都用足够大的整型(C++ long long、Java asLongStream、Python 天然大整数)
本题 n 到 10^5、元素到 10^9,sum 与 n×min 都会超过 32 位 int 的范围,溢出会算出错误答案;最终结果保证落在 int 范围,再转回 int 返回
完整代码(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 minMoves(self, nums: List[int]) -> int:
return sum(nums) - min(nums) * len(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 minMoves(vector<int>& nums) {
long long s = 0;
int mi = 1 << 30;
for (int x : nums) {
s += x;
mi = min(mi, x);
}
return (int)(s - 1LL * mi * (long long)nums.size());
}
};Java
import java.util.*;
class Solution {
public int minMoves(int[] nums) {
long sum = Arrays.stream(nums).asLongStream().sum();
int mi = Arrays.stream(nums).min().getAsInt();
return (int) (sum - (long) mi * nums.length);
}
}复杂度
时间
O(n)
求 sum 和 min 各扫一遍,常数遍线性
空间
O(1)
只用 sum、min 两个变量
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最小操作次数使数组元素相等 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么基准非得是最小值,换成别的值不行吗?+
把操作翻译成「每次减 1」之后,要让所有数相等,目标高度不能超过当前最小值——最小值只能等别人降下来够到它,自己没法往上抬。也没必要降到比 min 更低,那要给所有数额外多减、操作只增不减。所以恰好降到 min 最省,总步数就是 sum − n×min。若反过来奔着最大值去,等价转化根本不成立,算出来是错的。
这题和 LC462「最少移动次数使数组元素相等 II」有什么区别?+
LC462 每次只能挑某一个元素加 1 或减 1,求的是所有数到目标值的绝对距离之和最小,答案落在中位数上。本题操作形态是固定的「每次给 n−1 个加 1」,前面推过它等价于统一往最小值减,基准是 min 而不是中位数。操作规则不同,最优落点也就不同,别把两题的结论记串。
为什么不能真去循环模拟,非要推公式?+
因为操作次数本身能到 10^9 量级。几个相差上亿的数,逐次「n−1 个加 1」就得循环上亿轮,每轮还要遍历数组,妥妥超时。公式 sum − n×min 把这一整个过程压成两次扫描,直接算出总次数,不必真的一步步加。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最小操作次数使数组元素相等 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。