减小和重新排列数组后的最大元素 图解题解
这道题到底在问什么
- 输入
- arr=[2,2,1,2,1]
- 输出
- 2
- 输入
- arr=[100,1,1000]
- 输出
- 3
- 输入
- arr=[3,9,2,7,3,2]
- 输出
- 5
最优解:一步一步想明白
- 3记牢一句:排序后首位设 1,之后每一位都取 min(原值, 前一个 + 1),最后一位就是答案。下面每帧都在套这句。
- 4先看清画面。上面是原始数组 [3,9,2,7,3,2],顺序是乱的。右边这块滚动状态记着我们最关心的三样东西,已经定好的下标范围、本步的上限、以及已定的末位值。现在什么都还没处理。要让最后的最大值尽量大,第一步是把数排好序,好让小的数去垫底、大的数往后排。
- 5把数组从小到大排成 [2,2,3,3,7,9]。为什么要排序?因为首元素必须是 1,而相邻差最多是 1,所以越靠前的位置能取的值越小、越靠后才有机会取到大值。把小的数放前面垫台阶,大的数放后面,才能让末位爬到最高。排好序之后,准备从最左边这一位开始处理。
- 6处理之前把这一路要用的规则钉牢。最左这一位无论原来是几,都要减成 1,因为题目死规定首元素是 1。从第二位起,每一位有一个天花板,它最多只能比前一个大 1,也就是上限等于前一个的值加 1。可我们只能把数改小、不能改大,万一原值本来就比这个上限还小,那它也升不上去。所以每一位最终落在「原值」和「上限」里较小的那个,一句话就是 min(原值, 前一个 + 1)。下面一位一位地量。
- 7轮到最左边第一位,排序后它的原值是 2。这一位是特殊位,不用比上限,题目直接规定了首元素得是 1。因为我们能把数改小,把 2 减成 1 完全合法。下一帧就把它落定成 1。
- 8把第一位正式定成 1,标绿。现在已定的末位值是 1,这就是后面所有位置往上爬的起跑线。记住这条起跑线,第二位的上限就等于它加 1。接着一位一位往右推。
- 9轮到第 2 位,排序后它的原值是 2。左边蓝色的 1 位已经定好了,其中前一位落定在 1。先把这一位的原值 2 拎出来,下一帧算它的上限,再决定它最终能留到多大。
- 10这一位的上限是前一位的值加 1,也就是 1 加 1 等于 2。它最多只能到 2,不能更高。现在原值 2 正好卡在上限 2 上,不多不少,直接留着就合法。
- 11结算:取原值 2 和上限 2 里较小的那个,min(2, 2) 等于 2,把这一位标绿定下来。原值刚好等于上限,原封不动留 2。已定末位值刷新为 2,下一位的上限就是它加 1。
- 12轮到第 3 位,排序后它的原值是 3。左边蓝色的 2 位已经定好了,其中前一位落定在 2。先把这一位的原值 3 拎出来,下一帧算它的上限,再决定它最终能留到多大。
- 13这一位的上限是前一位的值加 1,也就是 2 加 1 等于 3。它最多只能到 3,不能更高。现在原值 3 正好卡在上限 3 上,不多不少,直接留着就合法。
- 14结算:取原值 3 和上限 3 里较小的那个,min(3, 3) 等于 3,把这一位标绿定下来。原值刚好等于上限,原封不动留 3。已定末位值刷新为 3,下一位的上限就是它加 1。
- 15轮到第 4 位,排序后它的原值是 3。左边蓝色的 3 位已经定好了,其中前一位落定在 3。先把这一位的原值 3 拎出来,下一帧算它的上限,再决定它最终能留到多大。
- 16这一位的上限是前一位的值加 1,也就是 3 加 1 等于 4。它最多只能到 4,不能更高。现在原值 3 比上限 4 还小。可我们只能减不能加,升不到 4 那么高,只能保留原值 3。这里就是重复值升不上去的情形。
- 17结算:取原值 3 和上限 4 里较小的那个,min(3, 4) 等于 3,把这一位标绿定下来。原值本来就小于上限,减不了也加不了,就留原值 3。已定末位值刷新为 3,下一位的上限就是它加 1。
- 18轮到第 5 位,排序后它的原值是 7。左边蓝色的 4 位已经定好了,其中前一位落定在 3。先把这一位的原值 7 拎出来,下一帧算它的上限,再决定它最终能留到多大。
- 19这一位的上限是前一位的值加 1,也就是 3 加 1 等于 4。它最多只能到 4,不能更高。现在原值 7 比上限 4 还大,超标了。我们能把它改小,所以要把它压到上限 4。
- 20结算:取原值 7 和上限 4 里较小的那个,min(7, 4) 等于 4,把这一位标绿定下来。原值超了,被上限压到 4。已定末位值刷新为 4,下一位的上限就是它加 1。
- 21轮到第 6 位,排序后它的原值是 9。左边蓝色的 5 位已经定好了,其中前一位落定在 4。先把这一位的原值 9 拎出来,下一帧算它的上限,再决定它最终能留到多大。
- 22这一位的上限是前一位的值加 1,也就是 4 加 1 等于 5。它最多只能到 5,不能更高。现在原值 9 比上限 5 还大,超标了。我们能把它改小,所以要把它压到上限 5。
- 23结算:取原值 9 和上限 5 里较小的那个,min(9, 5) 等于 5,把这一位标绿定下来。原值超了,被上限压到 5。这是最后一位,它的值 5 就是全数组的最大值。
- 24把最终数组 [1,2,3,3,4,5] 检查一遍。首位是 1,合规;相邻两两相减,1 到 2 差 1,2 到 3 差 1,3 到 3 差 0,3 到 4 差 1,4 到 5 差 1,全都不超过 1,完全合法。整条数组里最大的数就是末位的 5。注意中间有两个 3 挨在一起,那正是原来重复的值升不上去、只能持平的地方。
- 25回看整条路:排好序 [2,2,3,3,7,9],首位强制减成 1,之后每一位都取 min(原值, 前一个 + 1),末位值一路被推成 2、3、3、4、5。窍门就一句,排序后首位设 1,其余逐位取 min(原值, 前一个 + 1),末位就是答案。最终答案 5。
⚠️ 容易写错的地方
✗ 错:不排序就直接处理,或从大的数开始,导致小值垫不满、答案算错
✓ 对:一定先从小到大排序,让最小的数去当首位的 1
首位必须是 1、相邻差最多 1,所以越靠前只能放越小的值。不排序或先放大数,前面的位置就顶着大值下不来,后面反而爬不高,最大值会被算错。排序是这套贪心成立的前提
✗ 错:把每一位直接写成 arr[i-1] + 1,忘了和原值取 min
✓ 对:第 i 位必须是 min(原值, arr[i-1] + 1),原值更小时保留原值
我们只能把数改小、不能改大。如果原值本来就比上限小,比如两个相同的数挨着,后一个升不到上限那么高,只能保留原值。直接写 arr[i-1] + 1 会凭空把数改大,违反规则,答案偏大。演示里两个 3 持平就是这种情形
✗ 错:忘了把首位强制减成 1,直接拿排序后的首元素往后推
✓ 对:排序后必须把 arr[0] 赋值为 1
题目死规定首元素等于 1。哪怕排序后最小值是 2 或更大,也要减成 1,这样后面每一位的上限才从 1 起算。若首位不设 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 maximumElementAfterDecrementingAndRearranging(self, arr: List[int]) -> int:
arr.sort()
arr[0] = 1
for i in range(1, len(arr)):
arr[i] = min(arr[i], arr[i - 1] + 1)
return arr[-1]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 maximumElementAfterDecrementingAndRearranging(vector<int>& arr) {
sort(arr.begin(), arr.end());
int n = arr.size();
arr[0] = 1;
for (int i = 1; i < n; ++i) {
arr[i] = min(arr[i], arr[i - 1] + 1);
}
return arr[n - 1];
}
};Java
import java.util.*;
class Solution {
public int maximumElementAfterDecrementingAndRearranging(int[] arr) {
int n = arr.length;
Arrays.sort(arr);
arr[0] = 1;
int ans = 1;
for (int i = 1; i < n; ++i) {
arr[i] = Math.min(arr[i], arr[i - 1] + 1);
}
return arr[n - 1];
}
}复杂度
时间
O(n log n)
n 是数组长度。主要开销在排序 O(n log n);排完之后只从左到右扫一遍,每位做一次比较和一次取 min,是 O(n)。两者相加由排序主导,总体 O(n log n)
空间
O(log n) / O(n)
按峰值算,且只算排序带来的额外开销。算法本身就地改数组、只用了 cap 这类常数个变量,是 O(1)。额外空间看排序:C++ 的 sort、Java 的 Arrays.sort 递归栈深 O(log n);Python 的 sort 是 Timsort,最坏需要 O(n) 的临时缓冲。所以峰值 C++ 与 Java 记 O(log n),Python 记 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 减小和重新排列数组后的最大元素 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
怎么说清这个贪心是对的,为什么末位就是最大值?+
用归纳。排序后从左到右处理,声称每一位都取到了它在合法约束下能达到的最大值。首位:约束死钉在 1,取到 1 就是最大。第 i 位:前面各位已各自取到最大,前一位定为 p,那么第 i 位的上限是 p+1,同时不能超过原值(只能减),所以它能达到的最大就是 min(原值, p+1),我们正是这么取的。既然每一位都尽了最大,而数组整体是不降的,那么最大值一定落在最后一位,末位就是答案。
答案会不会很大、需要用 long 吗?+
不会。注意每一位最多比前一位大 1,而首位是 1,所以第 i 位的值最多是 i+1,末位最多是 n。也就是说答案上界就是数组长度 n。本题 n 最多几万到十万量级,远没到 int 大约二十一亿的上界,所以三种语言都用 int 就够,不必换 long。识别出「答案不超过 n」这一点,面试时能直接打消溢出的顾虑。
能不能不排序、用计数的办法做到 O(n)?+
可以。既然答案不超过 n,可以开一个长度 n+1 的桶,把每个大于 n 的值都并到第 n 个桶里,统计每个值出现多少次。然后从小到大扫桶,维护一个当前能到达的高度,每遇到一个可用的数就让高度加 1,数量不够时高度就停在原地。这样省掉排序,时间降到 O(n)。它和排序法的内核一样,都是让每一位在上限内尽量高,只是用计数代替了排序。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 减小和重新排列数组后的最大元素 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。