从一个范围内选择最多整数 I 图解题解
这道题到底在问什么
- 输入
- banned=[1,6,5], n=5, maxSum=6
- 输出
- 2 (选 2 和 4,和是 6)
- 输入
- banned=[11], n=7, maxSum=50
- 输出
- 7 (1 到 7 全选,和 28 没超)
先想最直接的笨办法
先把舞台摆好。上面是 1 到 9 这 9 个数,就是可挑的范围。我们的预算 maxSum 是 16。开局答案 ans 是 0,已选数字的累计和 s 也是 0。策略是从最小的 1 开始,一个一个往后看。(动画第 4 步)
最优解:一步一步想明白
- 3记牢这套:从最小的数开始,先看和会不会超,超了就停;没超且不被禁就选下。下面每一帧都在套它。
- 4先把舞台摆好。上面是 1 到 9 这 9 个数,就是可挑的范围。我们的预算 maxSum 是 16。开局答案 ans 是 0,已选数字的累计和 s 也是 0。策略是从最小的 1 开始,一个一个往后看。
- 5正式扫之前,先把 banned 里的数放进一个哈希集合,这样待会儿判断某个数能不能选,查一下就是常数时间。banned 第一个是 2,把它收进集合,数轴上的 2 就涂灰,表示这个位置禁选。
- 6banned 里第二个是 3,也收进集合,数轴上的 3 跟着涂灰。你看 2 和 3 挨在一块,待会儿扫到它们会连着被跳过。
- 7最后一个禁选数是 6,收进集合,数轴上的 6 涂灰。到这里禁选集合建好了,里面是 2、3、6 三个数。这三个位置无论如何都不能选,别的位置才有机会。
- 8禁选集合建好,灰色三个是 2、3、6。现在把指针 i 指到最左边的 1,准备从这里从小到大一路扫过去。每到一个数,先卡预算、再卡禁选。开扫。
- 9轮到数字 1。先算预算:0 加 1 等于 1,没有超过 16,和这一关过了。接着查禁选集合,1 不在里面,可以选。
- 10两关都过,把 1 选下,数轴上标绿。答案 ans 加一变成 1,累计和 s 加上 1 变成 1。这就是我们要的:多攒一个数,预算还没花完,继续往后。
- 11轮到数字 2。先算预算:1 加 2 等于 3,没有超过 16,和这一关过了。接着查禁选集合,发现 2 正好在里面,是个禁选数。
- 12既然 2 在禁选集合里,就跳过它,答案 ans 和累计和 s 都保持不变,继续看下一个数。
- 13轮到数字 3。先算预算:1 加 3 等于 4,没有超过 16,和这一关过了。接着查禁选集合,发现 3 正好在里面,是个禁选数。
- 14既然 3 在禁选集合里,就跳过它,答案 ans 和累计和 s 都保持不变,继续看下一个数。
- 15轮到数字 4。先算预算:1 加 4 等于 5,没有超过 16,和这一关过了。接着查禁选集合,4 不在里面,可以选。
- 16两关都过,把 4 选下,数轴上标绿。答案 ans 加一变成 2,累计和 s 加上 4 变成 5。这就是我们要的:多攒一个数,预算还没花完,继续往后。
- 17轮到数字 5。先算预算:5 加 5 等于 10,没有超过 16,和这一关过了。接着查禁选集合,5 不在里面,可以选。
- 18两关都过,把 5 选下,数轴上标绿。答案 ans 加一变成 3,累计和 s 加上 5 变成 10。这就是我们要的:多攒一个数,预算还没花完,继续往后。
- 19轮到数字 6。先算预算:10 加 6 等于 16,没有超过 16,和这一关过了。接着查禁选集合,发现 6 正好在里面,是个禁选数。
- 20注意这一下:6 加上去和是 16,恰好不超预算,可它偏偏在禁选集合里,所以照样不能选。预算这关过了不代表能选,禁选这关也得过。跳过它,答案和累计和都不动。
- 21轮到数字 7。规矩是先算预算:当前累计和 10 再加上 7 等于 17,已经超过了 16。既然连和都装不下,而且后面的数只会更大,那就没有必要再看了。
- 22把 7 标红,循环就停在这里。这是贪心从小到大的好处:一旦某个数让和超了预算,再往后的数更大,一个都塞不下,直接收工,不用一个个试。到此答案已经定了,是 3。
- 23循环停在 7 之后,后面的 8 和 9 比 7 还大,加进来只会更超预算,所以一个都不用再看,直接把它们跟 7 一起放下。这正是从小到大贪心的妙处:一旦停下,后面的全都可以不管。
- 24回放一遍。绿色的是最终选中的数:1、4、5,一共 3 个,它们的和是 10,没有超过预算 16。中间 2、3 因为被禁跳过,6 虽然和不超但也被禁,7 让和超了预算触发停止。所以答案就是 3,和开头说的对上了。
⚠️ 容易写错的地方
✗ 错:想着挑和最大的几个数
✓ 对:要挑个数最多,就从最小的数贪心地选
目标是个数最多不是和最大,同一份预算里小数塞得进的个数更多,从小到大选才最优
✗ 错:和恰好等于 maxSum 时不敢选
✓ 对:和等于 maxSum 也可以选
题目是不超过 maxSum,取等号是允许的,判断要用小于等于而不是严格小于,否则会少算一个
✗ 错:发现和超了还继续往后 continue
✓ 对:和一超立刻停止整个循环
从小到大扫,当前数已让和超预算,后面的数更大只会更超,继续白费力气,直接 break
✗ 错:用数组线性查某个数是否被禁
✓ 对:用哈希集合判断在不在 banned
banned 可能很长,每个数都线性找会退化;哈希集合把每次判断压到常数时间
完整代码(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 maxCount(self, banned: List[int], n: int, maxSum: int) -> int:
ans = s = 0
ban = set(banned)
for i in range(1, n + 1):
if s + i > maxSum:
break
if i not in ban:
ans += 1
s += i
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 maxCount(vector<int>& banned, int n, int maxSum) {
unordered_set<int> ban(banned.begin(), banned.end());
int ans = 0, s = 0;
for (int i = 1; i <= n && s + i <= maxSum; ++i) {
if (!ban.count(i)) {
++ans;
s += i;
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int maxCount(int[] banned, int n, int maxSum) {
Set<Integer> ban = new HashSet<>(banned.length);
for (int x : banned) {
ban.add(x);
}
int ans = 0, s = 0;
for (int i = 1; i <= n && s + i <= maxSum; ++i) {
if (!ban.contains(i)) {
++ans;
s += i;
}
}
return ans;
}
}复杂度
时间
O(n + m)
m 是 banned 的长度。建哈希集合把 banned 全部塞进去是 O(m);再从 1 到 n 扫一遍,每个数只做常数次的加法和集合查询,是 O(n)。合起来 O(n + m),实际因为和一超就停,往往扫不到 n 就结束
空间
O(m)
额外空间只有那个装 banned 的哈希集合,里面最多 m 个元素;累计和、答案、循环变量都是常数个,不随 n 增长。所以空间是 O(m)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 从一个范围内选择最多整数 I 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题的核心思路是什么?+
贪心。目标是选的个数最多,而不是和最大,所以从最小的数开始选最划算。把 banned 放进哈希集合,从 1 到 n 从小到大遍历,先看当前累计和加上这个数会不会超过 maxSum,超了就停,因为后面的数更大;没超再看它是否被禁,不被禁就选下、答案加一、累计和加上它。
为什么从小到大贪心一定最优?+
可以用交换论证:假设最优解里选了某个较大的数,而有一个更小的、没被禁的数没选,那把大数换成小数,个数不变但累计和更小,腾出的预算说不定还能再多选一个。所以总能调整成优先选小数的形式,从小到大贪心不会更差。
如果 n 非常大,比如到十亿,这套还行吗?+
这就是本题的进阶版思路。当 n 很大时不能逐个扫,可以用前缀和公式加二分:1 到某个数的总和有闭式,再减去落在范围内、且不超过 maxSum 的被禁数的和,二分找出能覆盖多少个数。本题 n 不大,直接从小到大扫一遍就够,时间 O(n 加 m)、空间 O(m)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 从一个范围内选择最多整数 I 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。