使数组元素相等的减少操作次数 图解题解
这道题到底在问什么
- 输入
- nums=[5,1,3]
- 输出
- 3
- 输入
- nums=[1,1,1]
- 输出
- 0
- 输入
- nums=[2,5,5,1,3]
- 输出
- 9
最优解:一步一步想明白
- 3记牢一句:排序后从左往右,遇到严格更大的新值就让层级 cnt 加 1,每个元素把当前 cnt 累加进答案,总和就是操作次数。下面每帧都在套这句。
- 4先看清画面。上面是原始数组 [2,5,5,1,3],顺序是乱的。右边这块计数面板记着两样东西,当前层级 cnt 和答案累计 ans,现在都是 0。要把这道题变简单,第一步是把数从小到大排好序,让相同的值挨在一起,方便一层一层地数。
- 5把数组从小到大排成 [1,2,3,5,5]。为什么排序?因为最终所有数都要降到最小值 1,排好序之后,一个数「下面垫着几个更小的不同值」就一目了然。相等的两个 5 挨在了一起,它们属于同一层。排好序,接下来从左往右一位一位地数层级。
- 6先看清整体结构。这里有 4 个不同的值,1、2、3、5,就是 4 个层级。最小的 1 是地面,任何数最终都要降到这里。一个数要操作几次,就看它下面垫着几层更小的不同值:2 降到 1 只跨 1 层;3 降到 1 要跨 2 层,走 3 到 2 再到 1;5 降到 1 要跨 3 层。下面从左往右,用一个计数器 cnt 把这些层级数出来。
- 7把这一路的规则钉牢。层级计数 cnt 从 0 起步。从下标 1 开始,拿每个元素和它左边的邻居比:如果它严格更大,说明踩上了一个新台阶,cnt 加 1;如果和左邻相等,说明还在同一层,cnt 不变。不管是不是新层,当前这个元素都要把此刻的 cnt 累加进答案,因为 cnt 正好是它降到地面要走的层数。最左的第一个元素是最小值,它就是地面,不用降,贡献 0。
- 8轮到最左边第 0 个元素,值是 1,它就是整个数组的最小值,也是所有数最终要降到的地面。它自己已经在最底层,不用做任何操作,贡献 0 次。cnt 保持 0,ans 保持 0。
- 9把第 0 个标绿,当作地面记下。它是后面所有元素往下降的目标。答案面板还是 0。接着从下标 1 开始,一位一位往右数,看每一位比左邻是不是跨了新层。
- 10接着看,轮到第 1 个元素,值是 2。左边蓝色那 1 个已经数过了。先把它和左边的邻居 1 拎出来比一比,下一帧判断它有没有踩上新层。此刻 cnt 还是 0。
- 11把第 1 个的值 2 和左邻 1 比。2 严格大于 1,说明从左邻那一层往上又冒出了一个新层级,层级计数 cnt 从 0 加 1 变成 1。这个 cnt 的含义是:值 2 下面垫着 1 个更小的不同值,需要它一层层降过去。
- 12把当前 cnt 累加进答案。值 2 要一层层降到最小值 1,路线是 2→1,正好 1 步,也就是 1 次操作。于是 ans 加上 1,从 0 变成 1,把这一位标绿。接着看下一个。
- 13往右挪一位,轮到第 2 个元素,值是 3。左边蓝色那 2 个已经数过了。先把它和左边的邻居 2 拎出来比一比,下一帧判断它有没有踩上新层。此刻 cnt 还是 1。
- 14把第 2 个的值 3 和左邻 2 比。3 严格大于 2,说明从左邻那一层往上又冒出了一个新层级,层级计数 cnt 从 1 加 1 变成 2。这个 cnt 的含义是:值 3 下面垫着 2 个更小的不同值,需要它一层层降过去。
- 15把当前 cnt 累加进答案。值 3 要一层层降到最小值 1,路线是 3→2→1,正好 2 步,也就是 2 次操作。于是 ans 加上 2,从 1 变成 3,把这一位标绿。接着看下一个。
- 16继续往右,轮到第 3 个元素,值是 5。左边蓝色那 3 个已经数过了。先把它和左边的邻居 3 拎出来比一比,下一帧判断它有没有踩上新层。此刻 cnt 还是 2。
- 17把第 3 个的值 5 和左邻 3 比。5 严格大于 3,说明从左邻那一层往上又冒出了一个新层级,层级计数 cnt 从 2 加 1 变成 3。这个 cnt 的含义是:值 5 下面垫着 3 个更小的不同值,需要它一层层降过去。
- 18把当前 cnt 累加进答案。值 5 要一层层降到最小值 1,路线是 5→3→2→1,正好 3 步,也就是 3 次操作。于是 ans 加上 3,从 3 变成 6,把这一位标绿。接着看下一个。
- 19来到最后一位,轮到第 4 个元素,值是 5。左边蓝色那 4 个已经数过了。先把它和左边的邻居 5 拎出来比一比,下一帧判断它有没有踩上新层。此刻 cnt 还是 3。
- 20把第 4 个的值 5 和左邻 5 比。两个相等,说明它俩是同一个值、属于同一层,没有踩上新台阶,所以 cnt 不增加,还是 3。这正是重复值的情形,前一个 5 和这个 5 站在同一层。
- 21把当前 cnt 累加进答案。值 5 要一层层降到最小值 1,路线是 5→3→2→1,正好 3 步,也就是 3 次操作。于是 ans 加上 3,从 6 变成 9,把这一位标绿。所有元素都数完了,ans 就是最终答案。
- 22把每个元素的贡献列出来看清楚。第 0 个 1 贡献 0;第 1 个 2 贡献 1;第 2 个 3 贡献 2;第 3 个 5 贡献 3;第 4 个 5 也贡献 3。加起来 0 加 1 加 2 加 3 加 3 等于 9。注意两个 5 各自都要降 3 层,所以各贡献 3,这就是为什么相同的值虽然同层、但每个都得单独操作。
- 23回看整条路:排好序 [1,2,3,5,5],从左往右数层级,遇到严格更大的新值就让 cnt 加 1,每个元素把当前 cnt 累加进答案。cnt 一路是 0、1、2、3、3,累加得到 9。窍门就一句,排序后数层级,每个元素贡献「它下面更小的不同值个数」,总和就是操作次数。最终答案 9。
⚠️ 容易写错的地方
✗ 错:老老实实按题面每次找最大值再减一次去模拟,数据一大就超时
✓ 对:换成排序后数层级,一次遍历直接把总次数算出来
题面描述的是逐次操作,但真去模拟每次找最大值,最坏要 O(n 的平方) 甚至更糟。关键洞察是:一个元素的操作次数只取决于它下面垫着几个更小的不同值,和操作先后无关。排序后一遍扫描就能把每个元素的贡献一次性加出来
✗ 错:把 cnt 当成「前面有几个元素」,相同值也让 cnt 加 1
✓ 对:cnt 只在遇到严格更大的新值时加 1,相等的值同层不加
cnt 数的是「不同的层级个数」,不是元素个数。两个相等的 5 属于同一层,第二个 5 不该让 cnt 变成 4。若相等也加 1,层级会虚高,答案偏大。判断条件必须是严格不等,也就是当前值确实比左邻大
✗ 错:以为相同的值只需操作一次,漏掉重复元素各自的贡献
✓ 对:每个元素都要单独累加它的 cnt,哪怕值相同
同一层的每个元素都要各自降到地面,操作是分开做的。演示里两个 5 各自都要降 3 层,各贡献 3,一共 6。cnt 判断是否新层看的是「值」,但累加进答案是「每个元素」都做一次
完整代码(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 reductionOperations(self, nums: List[int]) -> int:
nums.sort()
ans = cnt = 0
for a, b in pairwise(nums):
if a != b:
cnt += 1
ans += cnt
return ansC++
#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 reductionOperations(vector<int>& nums) {
sort(nums.begin(), nums.end());
int ans = 0, cnt = 0;
for (int i = 1; i < nums.size(); ++i) {
cnt += nums[i] != nums[i - 1];
ans += cnt;
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int reductionOperations(int[] nums) {
Arrays.sort(nums);
int ans = 0, cnt = 0;
for (int i = 1; i < nums.length; ++i) {
if (nums[i] != nums[i - 1]) {
++cnt;
}
ans += cnt;
}
return ans;
}
}复杂度
时间
O(n log n)
n 是数组长度。主要开销在排序 O(n log n);排完之后只从左到右扫一遍,每位做一次比较和一次累加,是 O(n)。两者相加由排序主导,总体 O(n log n)
空间
O(log n) / O(n)
按峰值算,且只算排序带来的额外开销。算法本身只用了 ans、cnt 两个常数变量,是 O(1)。额外空间看排序:C++ 的 sort、Java 的 Arrays.sort 递归栈深 O(log n);Python 的 sort 是 Timsort,最坏需要 O(n) 的临时缓冲(pairwise 本身是惰性迭代器,只占 O(1))。所以峰值 C++ 与 Java 记 O(log n),Python 记 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 使数组元素相等的减少操作次数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题为什么能从「逐次操作模拟」跳到「排序数层级」?+
关键是发现操作次数与顺序无关。最终所有数都要降到最小值,而每次操作只把一个数往下挪一层(降到严格小于它的次大值)。所以某个数的操作次数,就等于它到最小值之间隔着多少个不同的值,和先操作谁后操作谁完全无关。既然如此,就不必真去模拟每一次,只要排好序、从小到大数每个数脚下垫着的层数,把所有层数加起来即可。这类「模拟代价高但结果只依赖静态结构」的题,识别出不变量就能大幅降复杂度。
能不能不用排序、做到线性时间?+
可以。因为值域有限,可以用计数排序的思路。开一个大小为最大值加 1 的桶,统计每个值出现的次数,然后从小到大遍历桶:维护一个当前层级 cnt,每遇到一个出现过且不是最小的值就 cnt 加 1,再把这个值的出现次数乘以 cnt 累加进答案。这样省掉基于比较的排序,时间是 O(n 加 值域)。当值域不太大时它比 O(n log n) 更快,内核和排序法完全一样。
答案会不会溢出,需要 long 吗?+
算一下上界。最坏情况是所有元素两两不同,这时第 i 个(从 0 数)贡献 i,总和是 0 加 1 一直加到 n 减 1,等于 n 乘以 n 减 1 再除以 2,大约是 n 的平方的一半。本题 n 最多五万左右,平方一半约十二亿多,还在 int 的二十一亿上界之内,所以三种语言都用 int 就够。不过这个值离上界不算很远,面试时点明「答案是 O(n 的平方) 量级、但仍在 int 范围」能显示你估过界。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 使数组元素相等的减少操作次数 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。