← 返回题目列表

一手顺子如何用贪心判断能否分组?(LeetCode 846)

高频 中等 第 16 / 29 题 更新于 2026/07/30
贪心哈希表有序映射分组

简化版

每次从当前最小的牌开始,必须凑出一组连续长度为 groupSize 的顺子。

用有序计数表统计牌面;如果最小牌是 x 且还有 cnt 张,就必须从 x, x+1, ..., x+groupSize-1 各扣掉 cnt 张,缺任意一张都返回 false。

详细版

这题的贪心点在于「最小牌没有退路」。当前剩下的最小牌 x 不可能放到以更小数字开头的顺子里,因为那些牌已经不存在;它也不能跳过 x+1 去和更大的牌组成合法顺子。因此它只能作为某些顺子的起点。

实现时先判断总牌数是否能被 groupSize 整除。然后用 TreeMap<Integer, Integer> 记录每个牌面的数量,每轮取最小 key。如果最小 key 有 need 张,就从连续的 groupSize 个牌面中各扣 need 张,扣到 0 就删除。

boolean isNStraightHand(int[] hand, int groupSize) {
    if (hand.length % groupSize != 0) return false;
    TreeMap<Integer, Integer> map = new TreeMap<>();
    for (int x : hand) map.put(x, map.getOrDefault(x, 0) + 1);

    while (!map.isEmpty()) {
        int start = map.firstKey();
        int need = map.get(start);
        for (int x = start; x < start + groupSize; x++) {
            int c = map.getOrDefault(x, 0);
            if (c < need) return false;
            if (c == need) map.remove(x);
            else map.put(x, c - need);
        }
    }
    return true;
}

完整版教学

一、为什么最小牌必须作为起点

假设当前剩下的最小牌是 3。它如果不作为某组顺子的起点,就必须出现在 1,2,32,3,4 这类顺子里。但比 3 小的牌已经没有了,所以这些方案不可能成立。于是 3 唯一的合法位置就是某个顺子的第一张。

这就是本题贪心正确性的根。我们不是随便选一个局部方案,而是发现最小元素被约束到只有一种角色。只要当前最小牌无法向右凑出完整连续段,任何全局方案都不可能存在。

二、为什么一次要扣 need 张

如果最小牌 xneed 张,那么每一张 x 都必须开启一组顺子。也就是说需要 needx+1needx+2,一直到 x+groupSize-1。只扣一张再循环当然也能做,但会重复扫描,效率更差。

例如 hand = [1,1,2,2,3,3]groupSize=3。最小牌 1 有 2 张,所以必须同时扣掉两组:

需要 2 份: 1,2,3
扣完后: 1:0, 2:0, 3:0

如果 23 少于 2 张,就一定无法分成两组顺子。

三、有序映射的作用

贪心每轮都需要找到当前剩余最小牌,所以需要有序结构。Java 可以用 TreeMap,Python 可以先排序数组再配合计数表,C++ 可以用 map。关键不是具体容器,而是每轮处理时必须从最小剩余牌开始。

实现方式找最小牌适用场景
TreeMap / map自动有序代码直观,删除方便
排序数组 + HashMap按排序顺序遍历常见 LeetCode 写法
小根堆可以找最小删除和同步计数略繁琐

面试时用 TreeMap 最容易把贪心意图讲清楚。

四、流程图看清扣减过程

hand=[1,2,3,6,2,3,4,7,8]groupSize=3 为例:

计数: 1:1 2:2 3:2 4:1 6:1 7:1 8:1
最小 1,需要 1 组: 扣 1,2,3
剩余: 2:1 3:1 4:1 6:1 7:1 8:1
最小 2,需要 1 组: 扣 2,3,4
剩余: 6:1 7:1 8:1
最小 6,需要 1 组: 扣 6,7,8
剩余为空,成功

这个过程每一步都消灭当前最小牌,因此不会留下无法处理的低牌。

五、复杂度与边界

如果使用 TreeMap,设不同牌面数量为 m,每次访问和更新有 O(log m) 成本,总体接近 O(n log m)。如果先排序,排序成本是 O(n log n),之后每张牌被扣减有限次,整体也是常见可接受复杂度。

边界上,hand.length % groupSize != 0 可以提前失败。groupSize=1 时任何牌都能单独成组,答案为 true。牌面可能不连续、可能有大量重复,算法都通过计数表自然处理。

六、和区间覆盖类贪心的区别

这题不是选择「结束最早」或「覆盖最远」,而是选择「最小剩余元素的唯一归宿」。它和分发饼干、跳跃游戏的贪心证明不同,属于构造型贪心:先找出一个元素在任何合法解中必须怎么放,再把这个必须动作执行掉。

记忆钩子:顺子分组看最小牌,最小牌不能往左补,只能往右开组;开几组由它的张数决定。

七、常见误区与追问

  • 误区:随便从某张牌开始凑顺子。 如果不优先处理最小牌,可能把小牌需要的后继牌提前用掉。
  • 误区:最小牌有多张时只扣一张。 逻辑上可以但效率差,也更容易写出重复处理 bug。
  • 误区:只检查是否存在连续牌面。 还要检查每个牌面的数量是否够用。
  • 追问:为什么最小牌不能放在中间? 因为中间位置需要更小的前驱牌,而当前它已经是最小剩余牌。
  • 追问:复杂度是多少? TreeMap 写法约 O(n log m),排序写法约 O(n log n)
  • 追问:如果牌面范围很小能优化吗? 可以用数组计数并从小到大扫描,省掉平衡树开销。

八、加强记忆

一手顺子的贪心锚点是「最小牌没有选择」。每次拿当前最小牌 x,它只能作为顺子起点;如果它有 need 张,就必须从 xx+groupSize-1 每个数都扣 need 张。这个动作不是试探,而是任何合法解都必须包含的动作。记住「最小牌开组,数量决定组数,连续扣完再看下一个最小牌」,这题就不会写成回溯或乱序模拟。