划分数组使最大差为 K 图解题解
这道题到底在问什么
- 输入
- nums = [3,6,1,2,5], k = 2
- 输出
- 2
- 输入
- nums = [2,2,4,5], k = 0
- 输出
- 3
先想最直接的笨办法
排好序后变成 1、2、3、6、7、8、11、12。现在最左边的 1 是全局最小,也是第一组的最小值。接下来右指针从左往右一个一个走,拿每个数和当前组的锚点比,决定它是留在本组还是另起一组。(动画第 5 步)
最优解:为什么这么做
一句话答案:LeetCode 2294 划分数组使最大差为 K:排序后从最小值起贪心分组,和当前组锚点差 > k 就另起一组,靠有序保证每组尽量多装、组数最少,时间 O(n log n)。
把数组分成几组,最少能分几组
给一个整数数组 nums 和整数 k,要把每个元素分进若干个子序列,每个元素恰好属于一个;子序列不要求连续,可以隔着原数组挑元素凑成一组。要求每一组里最大值与最小值的差不超过 k,问最少能分成几组。题面例子 nums=[3,6,1,2,5]、k 为 2 时答案是 2。
枚举所有分法试不完,先排序省在哪
子序列不要求连续、能隔着挑元素成组,n 个数硬去枚举所有分法是指数级的,试不完。可只要抓住一点,局面立刻简单:一个组合规不合规,只由组内最大值和最小值决定,和这些数原本排在数组的哪个位置毫无关系。既然位置不参与判定,就先把 nums 从小到大排好——排序不丢掉任何一种合法分法,却把数值相近的数排到了相邻。
锚点盯住组内最小值,差超了才切一刀
排序后,最左边的数是全局最小,它必须自成一组的起点——把它记作锚点 a,也就是当前这一组的最小值。右边的数一个个看过来记作 b:只要 b 减 a 不超过 k,说明 b 和组内最小值挨得够近,收进来这一组的最大差还是不超 k,并入;一旦 b 减 a > k,再收就会让组内最大差破 k,只能在这里切一刀、让 b 当新一组的锚点。
为什么这样切出来的组数最少?排好序后,锚点这一组能装的就是从它开始、值不超过 a 加 k 的那一段;把这段装满,剩下的是同样的子问题,从新锚点再来。每一刀都是被差要破 k 逼着切的,少切一刀就会有组超标。
严格大于、和谁比、换不换锚点,别写错
切的条件必须是严格大于:b 减 a 正好等于 k 时,组内最大差就是 k、并没有超,得留在同组;写成大于等于会把差恰为 k 的对儿硬拆开,多分出冤枉的组。比较的对象必须是锚点 a、不是前一个元素:相邻两数差都小,首尾差未必小,像 1、2、3、4 每步差 1、首尾却差 3,只有盯住组内最小值才管得住整组。另起一组时别忘了把 a 换成当前的 b,不然后面全在拿旧最小值算差,判断连环错。排序也不能省,否则最小值飘忽不定、锚点无从谈起。
[3,6,1,2,5] 和 [2,2,4,5] 各分几组
先看 nums=[3,6,1,2,5]、k 为 2。排序后是 [1,2,3,5,6],锚点 a=1、组数从 1 起。2 减 1 得 1、3 减 1 得 2,都不超过 2,跟 1 一组;到 5,5 减 1 得 4、已经 > 2,切一刀,5 成新锚点、组数变 2;最后 6 减 5 得 1,并进第二组。分成 [1,2,3] 和 [5,6],组数 2。
再看 nums=[2,2,4,5]、k 为 0,这回一点差都不容。排序后 [2,2,4,5],锚点 a=2。两个 2 减 2 都得 0、不超过 0,同组;4 减 2 得 2 > 0,切,a=4、组数 2;5 减 4 得 1 > 0,再切,a=5、组数 3。k 为 0 逼着每种不同的值都单独成组,分出 3 组。
耗时的大头是排序:排好序是 O(n log n),之后从左到右扫一遍是 O(n),相加仍是 O(n log n)。只用锚点和计数两个变量,辅助空间 O(1)。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记牢这一句:排序后,锚点 a 盯住当前组的最小值。b - a ≤ k 就归本组,b - a > k 就另起一组。贪心之所以对,是因为排好序后,每一组尽量往右多装、直到差要超过 k 才切,组数一定最少。下面从头扫一遍。
- 4先看原始输入,8 个数乱序排列。整个贪心都建立在有序之上,因为只有排好序,当前组的最小值才恒等于组里第一个进来的数。所以第一步永远是把 nums 从小到大排好。
- 5排好序后变成 1、2、3、6、7、8、11、12。现在最左边的 1 是全局最小,也是第一组的最小值。接下来右指针从左往右一个一个走,拿每个数和当前组的锚点比,决定它是留在本组还是另起一组。
- 6开扫之前把规则钉死。紫色是正在看的数 b,下方那个标记 a 是当前组的锚点,也就是本组最小值。每一步只做一件事:算 b - a,如果不超过 2 就并进这一组,如果超过 2 就在 b 这里切一刀、另起一组。锚点从最左的 1 起步,第一组先开着。
- 7扫描从最左边开始。第一个数 1 无处可比,它自己就得开一组,并且当仁不让地成为这一组的锚点 a,也就是这一组的最小值。后面所有数,都要拿来和它算差。
- 8把 1 收进第 1 组,这一组现在只有它一个,锚点也是它。继续往右看下一个数。
- 9右指针走到 2。当前这一组的锚点是 1,算一下 2 减 1 等于 1。这个差 1 不超过 2,说明 2 和本组最小值挨得够近,可以安心并进来。
- 10把 2 并进当前组,本组现在是 [1, 2]。注意锚点 a 依然是组里最小的 1,没有变,后面的数还是拿来和它比。分组数保持 1。
- 11右指针走到 3。当前这一组的锚点是 1,算一下 3 减 1 等于 2。这个差 2 不超过 2,说明 3 和本组最小值挨得够近,可以安心并进来。
- 12把 3 并进当前组,本组现在是 [1, 2, 3]。注意锚点 a 依然是组里最小的 1,没有变,后面的数还是拿来和它比。分组数保持 1。
- 13右指针走到 6。当前这一组的锚点是 1,算一下 6 减 1 等于 5。这个差 5 已经大于 2,说明 6 和本组最小值拉得太开,再塞进来这一组的最大差就要爆 2。它进不了这一组。
- 14于是在这里切一刀。前面那组 [1, 2, 3] 就此封口,它内部最大差是 2,稳稳不超过 2。6 成为新一组的第一个数、也是新锚点,分组数从 1 涨到 2。
- 15右指针走到 7。当前这一组的锚点是 6,算一下 7 减 6 等于 1。这个差 1 不超过 2,说明 7 和本组最小值挨得够近,可以安心并进来。
- 16把 7 并进当前组,本组现在是 [6, 7]。注意锚点 a 依然是组里最小的 6,没有变,后面的数还是拿来和它比。分组数保持 2。
- 17右指针走到 8。当前这一组的锚点是 6,算一下 8 减 6 等于 2。这个差 2 不超过 2,说明 8 和本组最小值挨得够近,可以安心并进来。
- 18把 8 并进当前组,本组现在是 [6, 7, 8]。注意锚点 a 依然是组里最小的 6,没有变,后面的数还是拿来和它比。分组数保持 2。
- 19右指针走到 11。当前这一组的锚点是 6,算一下 11 减 6 等于 5。这个差 5 已经大于 2,说明 11 和本组最小值拉得太开,再塞进来这一组的最大差就要爆 2。它进不了这一组。
- 20于是在这里切一刀。前面那组 [6, 7, 8] 就此封口,它内部最大差是 2,稳稳不超过 2。11 成为新一组的第一个数、也是新锚点,分组数从 2 涨到 3。
- 21右指针走到 12。当前这一组的锚点是 11,算一下 12 减 11 等于 1。这个差 1 不超过 2,说明 12 和本组最小值挨得够近,可以安心并进来。
- 22把 12 并进当前组,本组现在是 [11, 12]。注意锚点 a 依然是组里最小的 11,没有变,后面的数还是拿来和它比。分组数保持 3。
- 23扫完了,回放一遍。整条序列被切成三段:[1, 2, 3]、[6, 7, 8]、[11, 12]。相邻两段之间,正是那两处 b - a 大于 2 的地方切开的。绿一段、蓝一段、绿一段,颜色只为让你看清分界,一共 3 组。
- 24再逐组核对一遍最大差。第一组 3 减 1 等于 2,第二组 8 减 6 等于 2,第三组 12 减 11 等于 1,三组全都不超过 2,完全合规。而且你没法再省:每一刀都是被逼着切的,少切一刀就一定有组会爆 2。所以最少划分数就是 3。
⚠️ 容易写错的地方
✗ 错:不排序就按原顺序直接分组
✓ 对:先排序再从左往右贪心
子序列可以任意挑元素,分组只跟数值有关。不排序时当前组的最小值不固定,没法用一个锚点判断,贪心也不再最优
✗ 错:判断条件写成 b - a ≥ k 就另起一组
✓ 对:必须是严格大于 b - a > k
差正好等于 k 时,组内最大差是 k、并没有超,允许同组。用 ≥ 会把等于 k 的情况错误切开,分出多余的组
✗ 错:拿 b 和前一个元素比,而不是和锚点 a 比
✓ 对:始终和当前组最小值 a 比
相邻差都小不代表组内最大差小。比如 1、2、3、4 每步差 1,但首尾差 3,和锚点比才管得住整组的最大差
✗ 错:进新组后忘记更新锚点 a
✓ 对:另起一组时把 a 换成当前 b
新组的最小值是 b,不换锚点会继续拿旧的最小值算差,后面的判断全错
完整代码(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 partitionArray(self, nums: List[int], k: int) -> int:
nums.sort()
ans, a = 1, nums[0]
for b in nums:
if b - a > k:
a = b
ans += 1
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 partitionArray(vector<int>& nums, int k) {
sort(nums.begin(), nums.end());
int ans = 1, a = nums[0];
for (int& b : nums) {
if (b - a > k) {
a = b;
++ans;
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int partitionArray(int[] nums, int k) {
Arrays.sort(nums);
int ans = 1, a = nums[0];
for (int b : nums) {
if (b - a > k) {
a = b;
++ans;
}
}
return ans;
}
}复杂度
时间
O(n log n)
瓶颈在排序,n 个数排序是 O(n log n)。排完之后只从左到右线性扫一遍,每个数做常数次比较和更新,是 O(n),整体被排序主导,合起来 O(n log n)
空间
O(1) 到 O(n)
算法本身只用 ans 和锚点 a 两个变量,辅助空间 O(1)。若把排序开销计入:C plus plus 与 Java 的排序递归栈约 O(log n),Python 的 Timsort 最坏 O(n)。不计排序则 O(1)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 划分数组使最大差为 K 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么必须先排序,按原顺序直接分组不行吗?+
分的是子序列、不要求连续,可以隔着挑元素成组,所以一个组合规不合规只由组内数值范围决定、跟元素原来的位置无关,重排不会丢掉任何合法分法。不排序时,当前组的最小值随扫描飘忽不定,没法用一个固定锚点判断该不该切,贪心也不再最优。排好序后最小值恒等于组里第一个进来的数,锚点才立得住。
切的条件为什么是 b 减 a > k,写成 ≥ k 会怎样?+
差正好等于 k 时,这一组的最大差就是 k、并没有超过限制,两个数完全可以留在同一组;用 ≥ k 会把差恰为 k 的两个数错误地切成两组,凭空多算出组来。比如 k 为 0、两个相等的数差为 0,本该同组,只有严格大于才切,才不会把它们拆开。
怎么证明这个贪心分出来的组数就是最少的?+
排序后看最小的那个数,它所在的组里其它数都不能超过它加 k,所以尽量把从它开始、值不超过 a 加 k 的一段全装进同一组,绝不会更差;这一段装满后,剩下的数是一个同样形状的子问题,从新的最小值再来一遍。每一刀都是被差要破 k 逼出来的,想少切一刀就一定有某组的最大差破 k、不再合规,所以组数被压到了最少。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 划分数组使最大差为 K 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。