划分数组使最大差为 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 步)
最优解:一步一步想明白
- 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++ 跟着练。
面试官可能追问
这题的核心思路是什么?+
关键点是分的是子序列、不要求连续,可以隔着挑元素成组,所以一个组合是否合规只由数值范围决定、跟元素在原数组的位置无关。于是先排序,再从左往右贪心:维护当前组的锚点 a,它是组内最小值,新数 b 满足 b - a ≤ k 就并入,否则以 b 另起一组。扫一遍得到的组数就是最少划分数。
怎么证明这个贪心是最优的?+
排序后,考虑最小的那个数,它所在的组里其它数都不能超过它加 k,所以尽量把从它开始、值不超过它加 k 的一段全装进同一组,不会更差。装满后剩下的数是同样的子问题,递归地贪心即可。每一刀都是被差要超过 k 逼出来的,少切必然违规,所以组数最少。
复杂度是多少?+
时间 O(n log n),瓶颈在排序,扫描只是线性一遍。空间上算法本身只用两个变量,是 O(1);若把排序自身的开销算进去,C plus plus 与 Java 约 O(log n) 的递归栈,Python 的 Timsort 最坏 O(n)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 划分数组使最大差为 K 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。