不同整数的最少数目 图解题解
这道题到底在问什么
- 输入
- arr=[5,5,4], k=1
- 输出
- 1 (删掉 4,只剩 5)
- 输入
- arr=[4,3,1,1,3,3,2], k=3
- 输出
- 2 (优先删低频:删 4 删 2,剩 1 和 3 两种)
最优解:为什么这么做
一句话答案:LeetCode 1481 不同整数的最少数目:删掉恰好 k 个元素让剩下种类最少,按出现次数从小到大删——删一类的代价就是它的频次,先端低频掉得最快,时间 O(n log n)。
删恰好 k 个后,剩几种数字最少
给一个整数数组 arr 和整数 k,删掉恰好 k 个元素,返回剩下的数字里最少能有几种不同的值。要点是删一个数字的某类必须把这类全删光,种类数才减一——只要还剩一个,这种数字就仍然算存在。题面例子 arr=[5,5,4]、k 为 1,删掉那个单独的 4,只剩 5 这一种,答案 1;再看 arr=[4,3,1,1,3,3,2]、k 为 3,答案 2。
为什么不能见一个删一个
删哪 k 个才让种类最少,直接枚举所有删法会炸:从 n 个元素里挑 k 个删,组合数是天文数字,规模一大就跑不动。就算只盯种类,一个想当然的删法是先动出现最多的那类、把最扎眼的数字清掉——可高频类往往有一大把,预算全砸进去也未必删得光,删了半截种类根本不减,纯属白花。
删一类的代价,就是它的频次
把每类删光的代价摆出来就清楚了:删掉某一种数字让种类减一,要花的预算恰好等于它出现的次数。想用同样的 k 把种类压到最低,预算就该花在代价最小的类上,也就是出现次数最少的那些种类。于是每步都端掉当前预算够得着、频次最小的一类,这种每步都盯住最小频次、拿最少预算换一次种类减少的选法就是贪心。按频次从小到大一路删,能删几类种类就减几;高频类先留着,用它们去扛最后那点删不光的零头。
统计、升序、够就删三步
落到代码就三步。先用哈希表(Python 的 Counter、C++ 的 unordered_map、Java 的 HashMap)扫一遍数组,数出每种数字出现几次;再把这些频次值单独取出来,从小到大排序;然后拿 k 当预算从最小的频次开始扣:当前频次 c 若满足 k≥c,就把 k 减去 c、种类数减一,接着看下一类;一旦 k<c,这类删不光了,立刻停,此刻剩下的种类数就是答案。remain 从种类总数起步,删一类扣一个,最后返回它。
arr=[4,3,1,1,3,3,2]、k 为 3 走一遍
先统计频次:4 出现 1 次、3 出现 3 次、1 出现 2 次、2 出现 1 次,一共 4 种,remain 记 4。把频次排成升序 [1,1,2,3]。第一格 c=1,预算 k 还有 3、够删,删光这一类,k 降到 2、remain 减到 3。第二格 c=1,k 还剩 2、再删光,k 降到 1、remain 减到 2。第三格 c=2,k 只剩 1、比 2 小,这一类删不光,停。此刻 remain 是 2 就是答案:耗掉的 2 个预算删净了两类低频,剩下 1 个预算只能从高频类里抠掉一个、删不光不减种类,正好凑满删 3 个。
删法容易错在哪,复杂度多少
有三处容易删错。删的方向反了、从高频开刀,同样的预算换来的种类减少更少,答案会偏大。把一类删掉一半也当种类减少,可只要还剩一个这数字就在,种类不变,半删白花预算。忘了 k 可能一个整类都删不动,这时不该硬凑,直接返回当前种类数即可。
复杂度上,统计频次是 O(n),对至多 n 个频次排序是 O(n log n),合起来 O(n log n);哈希表和频次数组最多存 n 个不同数字,空间 O(n)。
▶ 动画逐步走查(共 24 步)——想跟着动画一帧帧对照就展开
- 3记住「先灭最弱的种类」:同样花预算 k,干掉出现次数少的数字最划算,种类数掉得最快。
- 4第一拍:扫到第 0 个元素 4(紫色)。先看频次表里有没有它:没有,待会儿新建一行。
- 5第二拍:频次表新增一行 4:1(高亮行)。
- 6第一拍:扫到第 1 个元素 3(紫色)。先看频次表里有没有它:没有,待会儿新建一行。
- 7第二拍:频次表新增一行 3:1(高亮行)。
- 8第一拍:扫到第 2 个元素 1(紫色)。先看频次表里有没有它:没有,待会儿新建一行。
- 9第二拍:频次表新增一行 1:1(高亮行)。
- 10第一拍:扫到第 3 个元素 1(紫色)。先看频次表里有没有它:已经有了,待会儿次数 +1。
- 11第二拍:把 1 的次数更新到 2(高亮行)。
- 12第一拍:扫到第 4 个元素 3(紫色)。先看频次表里有没有它:已经有了,待会儿次数 +1。
- 13第二拍:把 3 的次数更新到 2(高亮行)。
- 14第一拍:扫到第 5 个元素 3(紫色)。先看频次表里有没有它:已经有了,待会儿次数 +1。
- 15第二拍:把 3 的次数更新到 3(高亮行)。
- 16第一拍:扫到第 6 个元素 2(紫色)。先看频次表里有没有它:没有,待会儿新建一行。
- 17第二拍:频次表新增一行 2:1(高亮行)。
- 18全部扫完,频次表成型:4→1、3→3、1→2、2→1,一共 4 种数字。接下来只关心这些「次数」,元素本身不再重要。
- 19现在舞台上的每个格子是一个「种类的出现次数」,已从小到大排好:[1, 1, 2, 3]。最左边是最容易被整类删掉的种类。
- 20看第 0 个种类:它出现 1 次。手里还有 k=3 次删除额度,够不够把这 1 个全删光?
- 21k ≥ 1,把这一整类删干净:预算扣掉 1 变成 k=2,不同种类数减到 3。这一格记为已清空(绿色一下,随后灰掉)。
- 22看第 1 个种类:它出现 1 次。手里还有 k=2 次删除额度,够不够把这 1 个全删光?
- 23k ≥ 1,把这一整类删干净:预算扣掉 1 变成 k=1,不同种类数减到 2。这一格记为已清空(绿色一下,随后灰掉)。
- 24看第 2 个种类:它出现 2 次。手里还有 k=1 次删除额度,够不够把这 2 个全删光?
- 25k=1 已经小于 2,删不动这一整类了(红色)。剩下的 1 个额度只能从某一类里删掉一部分,删不光就不会让种类减少,所以计数到此为止。剩下的种类数就是答案。
- 26收尾:左边 2 个低频种类被整类删掉(灰),耗掉 2 个额度;还剩的 1 个额度从某个高频类里删掉一部分(删不光、种类不减),正好凑满删 3 个。绿色这 2 个高频种类还在。答案 = 2。
⚠️ 容易写错的地方
✗ 错:从高频开始删
✓ 对:从低频开始删
同样的预算,删低频种类才能让种类数掉得最快
✗ 错:把一类删了一半也算减少种类
✓ 对:必须整类删完种类才减一
只要还剩一个,这种数字就仍存在,种类不变
✗ 错:忘了 k 可能为 0 或一个都删不动
✓ 对:k 不足以删任何一整类时直接返回当前种类数
半删无意义,剩余种类就是答案
完整代码(Python / C++ / Java)
Python
from typing import List
from collections import Counter
class Solution:
def findLeastNumOfUniqueInts(self, arr: List[int], k: int) -> int:
freq = sorted(Counter(arr).values())
remain = len(freq)
for c in freq:
if k >= c:
k -= c
remain -= 1
else:
break
return remainC++
#include <algorithm>
#include <unordered_map>
#include <vector>
using namespace std;
class Solution {
public:
int findLeastNumOfUniqueInts(vector<int>& arr, int k) {
unordered_map<int,int> count;
for (int x : arr) count[x]++;
vector<int> freq;
for (auto &p : count) freq.push_back(p.second);
sort(freq.begin(), freq.end());
int remain = freq.size();
for (int c : freq) {
if (k >= c) { k -= c; remain--; }
else break;
}
return remain;
}
};Java
import java.util.*;
class Solution {
public int findLeastNumOfUniqueInts(int[] arr, int k) {
Map<Integer, Integer> count = new HashMap<>();
for (int x : arr) count.put(x, count.getOrDefault(x, 0) + 1);
List<Integer> freq = new ArrayList<>(count.values());
Collections.sort(freq);
int remain = freq.size();
for (int c : freq) {
if (k >= c) { k -= c; remain--; }
else break;
}
return remain;
}
}复杂度
时间
O(n log n)
统计 O(n),对至多 n 个频次排序 O(n log n)
空间
O(n)
哈希表与频次数组最多存 n 个不同数字
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 不同整数的最少数目 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
能不能不排序,做到 O(n)?+
可以。频次值一定落在 1 到 n 之间,开一个长度 n+1 的桶,bucket[c] 记有多少种数字恰好出现 c 次。然后从 c 等于 1 往大遍历,对每个频次尽量整类删:预算够就删掉相应的整类、扣掉预算并减少种类数,够不着就停。这样绕开排序,整体 O(n),省掉那个 log。频次分布集中时,桶排的常数还更小。
如果要求删完后不同整数『最多』,贪心方向要怎么改?+
反过来。求最多种类,就尽量别让任何一类清零:优先从高频类里删多余的个数,每类最多删到剩 1 个,把预算尽量摊在少数几类的零头上,被彻底删空的种类越少越好。本题求的是最少种类,所以正好相反,先拿预算去端最容易整类删光的低频。
为什么删掉一类的一半不算种类减少?+
种类数看的是这个值还在不在数组里。一种数字只要还剩一个没删,它就仍然出现在结果里,这一种就还得算上,种类数纹丝不动。所以预算花在半删上是白耗——既没减种类,还占掉了本可以去删光另一整类的额度。这也是为什么循环里一旦 k<当前频次就必须停:再删下去只会削一类的零头,换不来种类减少。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 不同整数的最少数目 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。