你能构造出连续值的最大数目 图解题解
这道题到底在问什么
- 输入
- coins=[1,3]
- 输出
- 2
- 输入
- coins=[1,1,1,4]
- 输出
- 8
- 输入
- coins=[1,4,10,3,1]
- 输出
- 20
最优解:为什么这么做
一句话答案:LeetCode 1798 你能构造出连续值的最大数目:硬币升序排序后维护已能连续凑出的最大金额,新硬币不超过它加一就无缝扩张、否则断开,靠排序保证越界即真缺口,时间 O(n log n)。
从 0 开始,连续能凑出几个金额
给一把硬币,第 i 枚面值 coins[i],面值可能重复。任意挑一部分,它们的和就是能凑出的一个金额;什么都不挑和是 0。问从 0 起最多能连续凑出多少个整数金额。题面 coins=[1,3] 只能凑 0 和 1、返回 2,金额 2 就断在这里;coins=[1,1,1,4] 返回 8。问的是从 0 连续覆盖到哪,不是某个具体金额能不能拼。
挨个子集去试,为什么试不起
n 枚硬币能挑出 2 的 n 次方个子集,把每个子集的和算出来标记可达金额,这是能直接想到的暴力,可 n 到二三十就是上亿个子集,枚举不完。就算改用背包式可达标记,也得开一张覆盖所有金额的表逐枚更新,没利用上从 0 连续覆盖这个特点。
排序之后,新硬币凭什么能接上去
先把硬币从小到大排好,盯住一个量:眼下已能连续凑出的最大金额,叫它上界。什么都不挑只能凑出 0,上界就是 0,能凑 [0, 0]。排好序后逐枚考察,轮到的这枚 v 一定不小于前面用过的硬币;它新变出的金额是在每个可凑金额上再加一个 v,等于把 [0, 上界] 平移成 [v, v+上界]。
两段要接成一条不断的区间,[v, v+上界] 的左端 v 就不能越过 [0, 上界] 的右端太远:只要 v ≤ 上界+1,v 落在上界右边紧挨一格或和旧区间重叠,两段并成 [0, v+上界],上界扩到 v+上界。可一旦 v > 上界+1,金额上界+1 被跳过去,这枚 v 起步就在它右边,谁也补不出,连续到此为止。排序是前提:面值一路不减,越界这枚之后再没更小的能回头填坑;乱序会先撞上大硬币误判缺口。
落到代码:ans 从 1 起,v > ans 就停
上界加 1 就是下面代码里的 ans。参考代码不另设上界,直接记 ans = 能连续凑出的金额个数,也就是上界加一:空集能凑出 0,所以 ans 从 1 起步、不是 0,写成 0 第一枚哪怕面值 1 也会被当缺口、答案恒卡 0。判定换到 ans 上就是 v ≤ ans,代码写作 v > ans 就 break,把 v 等于 ans 的那枚留下继续吃:它补出的最小新金额恰好是 ans,接在 ans-1 后面严丝合缝,等号非取到不可,写成 v ≤ ans-1 会提前停、漏掉金额。满足就 ans += v,遍历完返回 ans。整体时间 O(n log n),排序最重,扫描 O(n) 不改量级;额外只用一个 ans,空间 O(1)。
把 [1,4,10,3,1] 从头滚一遍 ans
拿题面 coins=[1,4,10,3,1],排序成 [1,1,3,4,10],ans 从 1 起步、能凑 [0,0]。第一枚 1 不大于 ans=1,ans 变 2;第二枚 1 不大于 2,ans 变 3;第三枚 3 正好等于 ans=3、等号取到,ans 变 6;第四枚 4 不大于 6,ans 变 10;第五枚 10 等于 ans=10,ans 变 20,0 到 19 都能凑。硬币用完返回 ans = 20,五枚总和才 19、连 20 都凑不满,答案就是 0 到 19 这 20 个值。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记牢一句:ans = 能连续凑出的金额个数(能凑 [0, ans-1]),从小到大拿硬币,v ≤ ans 就把它吃进来 ans += v,否则在 ans 处断开停手。下面每帧都在套这句。
- 4先看清画面。上面是原始硬币 [1,4,10,3,1],顺序是乱的。右边这块滚动状态记着我们最关心的量 ans,它表示从 0 开始已经能连续凑出多少个金额。什么硬币都不挑就能凑出金额 0,所以 ans 从 1 起步,此刻只能凑出 [0, 0] 这一个值。要往前推,第一件事是把硬币排好序。
- 5把硬币从小到大排成 [1,1,3,4,10]。为什么要从小的开始?排序的真正作用是让硬币面值一路不减:这样一旦某枚 v 超过当前 ans,后面所有硬币都不小于 v、也就都大于 ans,金额 ans 真的谁也补不出,才能放心停手。如果不排序,可能先撞上那枚 10,把本来后面几枚小硬币还能填的位置误判成缺口,过早停错。排好序,ans 还是 1,准备从最左边这枚 1 开始一枚枚吃进来。
- 6正式开始前把起点钉牢。现在一枚硬币都还没吃进来,能凑出的只有金额 0,也就是能凑区间 [0, 0],连续值个数 ans 等于 1。接下来每吃进一枚硬币,如果不留缺口,这个区间就往右扩,ans 也跟着变大。目标是把五枚硬币尽量都吃进来。
- 7看最左边这枚待考察的硬币,记住贯穿全程的判定规则。当前能凑 [0, ans-1],最大凑到 ans-1。再添一枚面值 v,它能补出的最小新金额正好是 v。要想不留窟窿,v 必须不能越过已经铺好的地面往前跳,也就是 v 要小于等于 ans。只要 v ≤ ans,新旧两段就严丝合缝连起来;一旦 v 比 ans 还大,金额 ans 这个位置谁都凑不出,后面全断,只能停手。下面就拿这把尺子一枚枚量。
- 8轮到最小的第 1 枚硬币,面值是 1。此刻还没吃任何硬币,ans = 1,能凑的只有 [0, 0]。先把它拎出来,看看这枚 1 能不能无缝接上去。
- 9拿尺子一量:面值 1 小于等于当前 ans = 1,满足 v ≤ ans。既然它没有越过已经铺好的地面,新旧两段金额就能严丝合缝地接起来,不会留窟窿。这枚可以放心吃进来。
- 10把这枚 1 正式吃进来(标绿),它能凑出的 [1, 1] 和原来的 [0, 0] 拼成一整片。ans 从 1 加上 1 变成 2,现在从 0 到 1 每个金额都能凑出来。继续看下一枚。
- 11轮到第 2 枚硬币,面值是 1。前面 1 枚已经吃进来(蓝色),把 ans 推到了 2,现在能凑 [0, 1]。先把它拎出来,看看这枚 1 能不能无缝接上去。
- 12拿尺子一量:面值 1 小于等于当前 ans = 2,满足 v ≤ ans。既然它没有越过已经铺好的地面,新旧两段金额就能严丝合缝地接起来,不会留窟窿。这枚可以放心吃进来。
- 13把这枚 1 正式吃进来(标绿),它能凑出的 [1, 2] 和原来的 [0, 1] 拼成一整片。ans 从 2 加上 1 变成 3,现在从 0 到 2 每个金额都能凑出来。继续看下一枚。
- 14轮到第 3 枚硬币,面值是 3。前面 2 枚已经吃进来(蓝色),把 ans 推到了 3,现在能凑 [0, 2]。先把它拎出来,看看这枚 3 能不能无缝接上去。
- 15拿尺子一量:面值 3 小于等于当前 ans = 3,满足 v ≤ ans。既然它没有越过已经铺好的地面,新旧两段金额就能严丝合缝地接起来,不会留窟窿。这枚可以放心吃进来。
- 16把这枚 3 正式吃进来(标绿),它能凑出的 [3, 5] 和原来的 [0, 2] 拼成一整片。ans 从 3 加上 3 变成 6,现在从 0 到 5 每个金额都能凑出来。继续看下一枚。
- 17轮到第 4 枚硬币,面值是 4。前面 3 枚已经吃进来(蓝色),把 ans 推到了 6,现在能凑 [0, 5]。先把它拎出来,看看这枚 4 能不能无缝接上去。
- 18拿尺子一量:面值 4 小于等于当前 ans = 6,满足 v ≤ ans。既然它没有越过已经铺好的地面,新旧两段金额就能严丝合缝地接起来,不会留窟窿。这枚可以放心吃进来。
- 19把这枚 4 正式吃进来(标绿),它能凑出的 [4, 9] 和原来的 [0, 5] 拼成一整片。ans 从 6 加上 4 变成 10,现在从 0 到 9 每个金额都能凑出来。继续看下一枚。
- 20轮到第 5 枚硬币,面值是 10。前面 4 枚已经吃进来(蓝色),把 ans 推到了 10,现在能凑 [0, 9]。先把它拎出来,看看这枚 10 能不能无缝接上去。
- 21拿尺子一量:面值 10 小于等于当前 ans = 10,满足 v ≤ ans。既然它没有越过已经铺好的地面,新旧两段金额就能严丝合缝地接起来,不会留窟窿。这枚可以放心吃进来。
- 22把这枚 10 正式吃进来(标绿),它能凑出的 [10, 19] 和原来的 [0, 9] 拼成一整片。ans 从 10 加上 10 变成 20,现在从 0 到 19 每个金额都能凑出来。五枚硬币全部吃进,循环结束。
- 23随手抽查几个金额验证一下。13 就是 10 加 3,19 是 10 加 4 加 3 加 1 加 1,一路数下来 0 到 19 里的每个整数都能挑出一组硬币凑出来。那 20 呢?五枚硬币加起来总和才 19,连凑都凑不满 20,所以最大只能连续到 19。能连续凑出的正好是 0 到 19 这 20 个金额。
- 24回看整条路:排好序 [1,1,3,4,10],ans 从 1 起步,每枚硬币都满足 v ≤ ans,于是依次把 ans 推到 2、3、6、10,最后加上 10 冲到 20。整道题的窍门就一句:从小到大拿硬币,能接上就吃进来 ans += v,接不上就停。最终答案 20。
⚠️ 容易写错的地方
✗ 错:不排序就直接套「v 大于 ans 就停」的停手规则(乱序时会提前撞上大硬币误判缺口)
✓ 对:一定先从小到大排序,再从最小的硬币逐枚吃进来
「v 大于 ans 就停」这条停手规则只有在硬币从小到大排好序时才成立:排序后一旦 v 超过 ans,剩下的硬币都不小于 v、更大于 ans,金额 ans 确实谁也补不出,停得对。若乱序,可能先撞上一枚大硬币就误判缺口提前停,而后面本还有小硬币能把这个位置填上,ans 会被算小甚至算错。排序是这套贪心成立的前提
✗ 错:把判定写成 v ≤ ans-1(即 v 严格小于 ans),漏掉 v 正好等于 ans 的情况
✓ 对:判定条件是 v ≤ ans,等号必须取到
当前能凑到最大值 ans-1,再添一枚 v = ans 时,它能补出的最小新金额正好是 ans,恰好接在 ans-1 后面,两段严丝合缝,应当吃进来。若写成 v ≤ ans-1 就会在这里提前停,把本该能凑的金额漏掉,答案偏小。参考代码写成「v 大于 ans 才 break」,正是包含了 v = ans 继续累加
✗ 错:ans 初始值写成 0,导致第一枚硬币的判定和最终计数全部错位
✓ 对:ans 必须初始化为 1
什么硬币都不挑就能凑出金额 0,这本身就是一个能凑出的连续值,所以起点是「能凑 1 个值」,ans = 1。若写成 0,第一枚哪怕是面值 1 的硬币也会因为 1 大于 0 而被误判成缺口直接停,答案恒为 0,彻底错
完整代码(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 getMaximumConsecutive(self, coins: List[int]) -> int:
ans = 1
for v in sorted(coins):
if v > ans:
break
ans += v
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 getMaximumConsecutive(vector<int>& coins) {
sort(coins.begin(), coins.end());
int ans = 1;
for (int& v : coins) {
if (v > ans) break;
ans += v;
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int getMaximumConsecutive(int[] coins) {
Arrays.sort(coins);
int ans = 1;
for (int v : coins) {
if (v > ans) {
break;
}
ans += v;
}
return ans;
}
}复杂度
时间
O(n log n)
n 是硬币枚数。主要开销在排序 O(n log n);排完之后只从左到右扫一遍,每枚硬币做一次比较加一次累加,是 O(n)。两者相加由排序主导,总体 O(n log n)。相比枚举所有子集去凑金额的指数级做法,这是质的飞跃
空间
O(log n) / O(n)
按峰值算,且只算排序带来的额外开销。算法本身只用了一个 ans 变量,是常数 O(1)。但排序要占额外空间:C++ 的 sort、Java 的 Arrays.sort 用递归快排,栈深 O(log n);Python 的 sorted 用 Timsort 且新建了一个列表,最坏 O(n)。所以峰值 C++ 与 Java 记 O(log n),Python 记 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 你能构造出连续值的最大数目 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么一定要先排序,乱序直接套 v > ans 就停行不行?+
不行。这条停手规则只有在硬币从小到大排好序时才成立:排序后一旦某枚 v 超过 ans,剩下的硬币都不小于 v、更大于 ans,金额 ans 确实谁也补不出,停得对。若是乱序,可能先撞上一枚大硬币就误判出缺口提前停,而后面本还有小硬币能把这个位置填上,ans 会被算小甚至算错。排序把面值理成一路不减,是这套贪心能成立的地基。
判定为什么是 v ≤ ans,等号非取到不可?+
当前能凑到 ans-1,再添一枚 v = ans 时,它能补出的最小新金额正好是 ans,恰好接在 ans-1 后面,两段严丝合缝,就该吃进来。若把条件写成 v ≤ ans-1、也就是 v 严格小于 ans,就会在 v 正好等于 ans 这里提前停手,把本该能凑的金额漏掉,答案偏小。参考代码写成 v > ans 才 break,正是把 v = ans 的情形留在循环里继续累加。
如果改问某个具体金额能不能凑出,还用这套贪心吗?+
那是另一类问题,一般不能只靠这套贪心。问特定金额能否用子集凑出本质是子集和,通常得用背包式的动态规划:开一个布尔数组标记每个金额可达与否,逐枚硬币更新。本题特殊在只关心从 0 起连续覆盖到哪,这份连续性让我们能用一个滚动的 ans 顶替整张可达表,空间从背包的一大片压到常数,时间也只要排序加一遍扫描。认出连续这个特点,才是能用贪心而不必上背包的原因。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 你能构造出连续值的最大数目 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。