划分数组并满足最大差限制 图解题解
这道题到底在问什么
- 输入
- nums=[1,3,4,8,7,9,3,5,1], k=2
- 输出
- [[1,1,3],[3,4,5],[7,8,9]]
- 输入
- nums=[2,4,2,2,5,2], k=2
- 输出
- [](四个 2 逼得某组要放 2 和 5,差 3 超过 k)
先想最直接的笨办法
先看组里最小的数。因为整体排过序,这一组最左边的 nums[0] 就是组内最小值,等于 1。不用挨个比,最左即最小,这是排序换来的红利。(动画第 10 步)
最优解:为什么这么做
一句话答案:LeetCode 2966 划分数组并满足最大差限制:把数组每三个一组、组内最大最小之差不超过 k。先从小到大排序,再相邻三个一组,只看每组最右减最左是否超 k,超了就返回空,时间 O(n log n)。
把数组切成一组组三个,组内差不能太大
给一个长度是 3 的倍数的数组 nums 和一个正整数 k。要把 nums 分成若干组、每组正好三个数,同一组里任意两个数的差都不超过 k。能切就返回任意一种切法,切不出来返回空数组。题面例子 nums=[1,3,4,8,7,9,3,5,1]、k 等于 2,一种合法答案是 [[1,1,3],[3,4,5],[7,8,9]]。
组内三对差都要满足,分法又那么多
同一组三个数,两两就有三对差都得压在 k 以内;九个数凑成三组的分法极多,逐个枚举再逐组验差,数一大就爆炸。难处在于既不知道哪三个数该放一起,定了一组又得比三对差才敢下结论,试遍所有分法,n 稍大就跑不动。
为什么排完序,紧挨着的三个一组最省心
先把 nums 从小到大排序。排完之后有个好处:任意连续一段里,最小值一定落在最左、最大值一定落在最右。于是判断一组三个数的跨度,不必比三对,只看最右减最左这一对——它是组里跨度最大的一对,它不超 k,另外两对更小、自然也不超。
再说为什么取紧挨着的三个。用贪心,也就是每步拿当下最划算的、不回头:排完序后,还没分掉的最小那个数总得找两个伙伴,让紧挨它的两个陪它一组,这组最大值压到最低,给后面留的余地最大。这叫交换论证,即假设有更优分法,把它调成相邻分组也不会更差,所以每三个相邻框成一组就是最优。
从左到右每三个切一刀,一超标就收手
排好序后,从下标 0 起,每次取连续三个数当一组,取完往后跳三格到下一组开头,直到走完。每取一组就看最右减最左:只要差>k,这组装不下,整个数组也切不出合法划分,立刻返回空数组;不超 k 就把这组收进答案,继续往后切。全部切完,收集的三元组拼起来就是答案。
拿题面这九个数排完序一刀刀切
先排序:[1,3,4,8,7,9,3,5,1] 从小到大理顺成 [1,1,3,3,4,5,7,8,9],两个 1 挤到最前,9 落到最右。
从下标 0 起切。第一组取 [1,1,3],最右 3 减最左 1 等于 2,不超过 k,收下。跳到下标 3,第二组取 [3,4,5],5 减 3 等于 2,同样不超,收下。再跳到下标 6,第三组取 [7,8,9],9 减 7 等于 2,收下。三组全合格,答案就是 [[1,1,3],[3,4,5],[7,8,9]]。
复杂度,以及差恰好等于 k 算不算数
排序 O(n log n) 外只线性扫一遍:排完后每三个一组走一趟是 O(n),整体仍是 O(n log n)。空间上答案占 O(n),只看排序的额外开销,C++ 和 Java 的递归栈约 O(log n)、Python 的 Timsort(Python 内置排序)最坏 O(n)。
两个边界得拎清。其一,差恰好等于 k 合法,判超标得用严格大于:差>k 才算坏。其二,nums=[2,4,2,2,5,2]、k 为 2 为什么无解——排成 [2,2,2,2,4,5],第一组 [2,2,2] 差 0 过关,第二组只剩 [2,4,5],5 减 2 等于 3,>2,返回空数组,四个 2 把一个挤进了 5 那组。至于 n 是 3 的倍数题目已保证,跳三格正好切完。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记住这一句:排序之后,相邻三个分一组,每组只要盯住最左和最右的差。为什么挨着分最优,是因为排完序后,当前最小的那个数,让紧挨它的两个数陪它一组,组内最大值才被压得最低,给后面留的余地最大。下面一步一步来。
- 4第一步:排序这是原始的 nums,九个数顺序是乱的。直接在乱序上分组很难判断,所以第一步永远是排序。把它从小到大理顺,小的挤到左边、大的挪到右边,后面分组就有章法了。
- 5已排序排完序长这样,从左到右一路不减。原来的两个 1 挤到最前,两个 3 挨在一起,最大的 9 落到最右。排好之后有个天然的好处:任意连续一段里,最小值一定在左端、最大值一定在右端,这正是下面分组要用的关键。
- 6分组规则规则很简单:排好序后,从左往右每三个相邻的数框成一组。九个数正好切成三组,下标 0 到 2 一组、3 到 5 一组、6 到 8 一组。为什么一定挨着切,前面口诀讲过:让最小的数带上离它最近的两个,组内跨度才最小。
- 7关键观察再点透一个关键。每一组内部本来也是排好序的,所以组里最小的就是最左那个,最大的就是最右那个。要判断这一组任意两个数的差是否都不超过 k,只要看跨度最大的那一对,也就是最右减最左。这一步把判断从三对压到了一对。
- 8开始扫描开始正式分组。用一个指针 i 从下标 0 出发,每次处理连续三个数,处理完就往后跳 3,落到下一组的开头。i 走到 0、3、6 三个位置,分别对应三组。现在从 i 等于 0 的第一组开始。
- 9第一组指针 i 现在在下标 0。框出连续三个数 1、1、3,这就是第一组。它们已经在大数组里排好序,所以组内也是从小到大的。先把这三个看清楚,再去量它的跨度。
- 10定位最小先看组里最小的数。因为整体排过序,这一组最左边的 nums[0] 就是组内最小值,等于 1。不用挨个比,最左即最小,这是排序换来的红利。
- 11定位最大再看组里最大的数。同样因为排过序,这一组最右边的 nums[2] 就是组内最大值,等于 3。现在两端都标出来了,最小 1、最大 3,跨度就藏在这两个之间。
- 12判定算这一组的跨度:最右 3 减最左 1 等于 2。拿它和 k 等于 2 比,2 不超过 2,说明组内任意两个数的差都在允许范围内。这一组过关。
- 13收入答案第一组 判定通过,把它整段标绿,收进答案。指针往后跳三步,准备处理下一组。到这里已经稳稳收下 1 组,继续往后走。
- 14第二组指针 i 现在在下标 3。框出连续三个数 3、4、5,这就是第二组。它们已经在大数组里排好序,所以组内也是从小到大的。先把这三个看清楚,再去量它的跨度。
- 15定位最小先看组里最小的数。因为整体排过序,这一组最左边的 nums[3] 就是组内最小值,等于 3。不用挨个比,最左即最小,这是排序换来的红利。
- 16定位最大再看组里最大的数。同样因为排过序,这一组最右边的 nums[5] 就是组内最大值,等于 5。现在两端都标出来了,最小 3、最大 5,跨度就藏在这两个之间。
- 17判定算这一组的跨度:最右 5 减最左 3 等于 2。拿它和 k 等于 2 比,2 不超过 2,说明组内任意两个数的差都在允许范围内。这一组过关。
- 18收入答案第二组 判定通过,把它整段标绿,收进答案。指针往后跳三步,准备处理下一组。到这里已经稳稳收下 2 组,继续往后走。
- 19第三组指针 i 现在在下标 6。框出连续三个数 7、8、9,这就是第三组。它们已经在大数组里排好序,所以组内也是从小到大的。先把这三个看清楚,再去量它的跨度。
- 20定位最小先看组里最小的数。因为整体排过序,这一组最左边的 nums[6] 就是组内最小值,等于 7。不用挨个比,最左即最小,这是排序换来的红利。
- 21定位最大再看组里最大的数。同样因为排过序,这一组最右边的 nums[8] 就是组内最大值,等于 9。现在两端都标出来了,最小 7、最大 9,跨度就藏在这两个之间。
- 22判定算这一组的跨度:最右 9 减最左 7 等于 2。拿它和 k 等于 2 比,2 不超过 2,说明组内任意两个数的差都在允许范围内。这一组过关。
- 23收入答案第三组 判定通过,把它整段标绿,收进答案。指针往后跳三步,准备处理下一组。到这里已经稳稳收下 3 组,三组全部到手。
- 24收束三组全部走完。[1,1,3] 差 2、[3,4,5] 差 2、[7,8,9] 差 2,每一组的跨度都不超过 k 等于 2,划分成立。把绿色的三段按顺序拼起来,就是答案:[[1,1,3],[3,4,5],[7,8,9]],和一开始预告的对上了。只要中途有任何一组跨度超过 k,整件事立刻失败、返回空。
⚠️ 容易写错的地方
✗ 错:不排序就直接每三个分组
✓ 对:必须先从小到大排序再分组
不排序时相邻三个未必是最接近的一批数,组内跨度可能被撑大,会把本来能切的判成不能切
✗ 错:组内把三对差都比一遍
✓ 对:只需比最右减最左这一对
排序后组内最左最小、最右最大,跨度最大的就是这一对;它不超过 k,其余两对自然也不超过
✗ 错:发现某组超过 k 还继续往下切
✓ 对:一旦有组超过 k 立即返回空
题目要求所有组都合法,只要一组不行整体就无解,继续切没有意义,直接返回空数组
✗ 错:担心 n 不是 3 的倍数要补齐
✓ 对:题目已保证 n 是 3 的倍数
约束里写明 n 是 3 的倍数,循环每次跳 3 一定正好切完,不会剩下零头
完整代码(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 divideArray(self, nums: List[int], k: int) -> List[List[int]]:
nums.sort()
ans = []
n = len(nums)
for i in range(0, n, 3):
t = nums[i : i + 3]
if t[2] - t[0] > k:
return []
ans.append(t)
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:
vector<vector<int>> divideArray(vector<int>& nums, int k) {
sort(nums.begin(), nums.end());
vector<vector<int>> ans;
int n = nums.size();
for (int i = 0; i < n; i += 3) {
vector<int> t = {nums[i], nums[i + 1], nums[i + 2]};
if (t[2] - t[0] > k) {
return {};
}
ans.emplace_back(t);
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int[][] divideArray(int[] nums, int k) {
Arrays.sort(nums);
int n = nums.length;
int[][] ans = new int[n / 3][];
for (int i = 0; i < n; i += 3) {
int[] t = Arrays.copyOfRange(nums, i, i + 3);
if (t[2] - t[0] > k) {
return new int[][] {};
}
ans[i / 3] = t;
}
return ans;
}
}复杂度
时间
O(n log n)
排序是主要开销,占 O(n log n)。排完后从头到尾每三个一组扫一遍,只做常数次比较和取值,是 O(n)。两者相加由排序主导,整体 O(n log n)
空间
O(n) / O(log n)
按峰值算。返回的答案本身要占 O(n)。若不计输出只看排序的额外开销:C plus plus 与 Java 的排序递归栈约 O(log n),Python 的 Timsort 最坏 O(n);辅助变量本身是 O(1)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 划分数组并满足最大差限制 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么一定要先排序,直接每三个分组不行吗?+
不排序时,随手挨着的三个数未必是彼此最接近的一批,组内跨度可能被无谓撑大,本来能切的反而被判成不能切。排完序后相近的数自然聚在一起,让最小的数带上紧挨它的两个伙伴,组内跨度压到最低,这才是能切就一定切得出的分法。
判断一组合不合格,为什么只比最右减最左,不用把三对差都比一遍?+
因为组内三个数是排过序的,最左最小、最右最大。三对差里,最右减最左这一对跨度最大;它都没超过 k,另外两对差更小、自然也不会超过。所以量一对就够,剩下两对不必再算。
复杂度是多少,瓶颈在哪?+
时间是 O(n log n),瓶颈在排序;排完序后线性扫一遍分组只是 O(n)。空间上返回的答案占 O(n),若只看排序的额外开销,C++ 和 Java 的递归栈约 O(log n),Python 的 Timsort 最坏 O(n)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 划分数组并满足最大差限制 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。