题目描述

✅ 1090. 受标签影响的最大值

image-20260928225659506

image-20260928225659507

题意分析

第 i 项的分值和标签分别是 values[i]、labels[i]。选择的总数不能超过 numWanted,同一个标签的选取数不能超过 useLimit,要求分值总和最大,不要求必须凑满名额。

分值都非负,因此只要还有合法名额,多选一项不会让答案变小。标签限制只影响同类中的选取数量,可以先按分值从大到小考虑,再跳过已经用满标签名额的项。

解法:排序 + 贪心

核心思路

[!blue]

对任意一个标签,只需考虑分值最高的 useLimit 项:如果方案选了较低分的一项,却没选更高分的同标签项,直接替换后数量限制不变,总分不会减少。因此总有一个最优方案只使用这些候选;该标签不足 useLimit 项时则全部保留。

将所有标签的候选放在一起,每个标签至多已有 useLimit 项,所以从中任选子集都满足标签限制。剩下只需取全局分值最大的至多 numWanted 项;若候选不足,全部取走就是最优解。

代码把这两步合为一次降序扫描。idx 保存排序后的原下标,保证每个分值仍能找到对应标签;cnt[label] 记录该标签已选数量,picked 记录总数量,sum 记录总分。遇到未满额标签就选取,遇到已满额标签就跳过;降序保证被选中的正是每个标签最有价值的那些项。

picked 达到总上限时,后面的分值都不更大,可以结束;某个标签满额时,其他标签仍可能有可选项,因此只能跳过当前项。相同分值之间任选顺序都不会降低最优总和。

解题步骤

  • 排序下标,保持分值与标签配对。
  • 标签已满就跳过,否则累计分值与两种计数。
  • 达到总数上限或扫描完毕后返回。

代码实现

class Solution {
    public int largestValsFromLabels(int[] values, int[] labels, int numWanted, int useLimit) {
        Integer[] idx = new Integer[values.length];

        for (int i = 0; i < values.length; i++) {
            idx[i] = i;
        }

        // 按下标排序,保持分值与标签的对应。
        Arrays.sort(idx, (a, b) -> values[b] - values[a]);

        Map<Integer, Integer> cnt = new HashMap<>();
        int picked = 0;
        int sum = 0;

        for (int id : idx) {
            // 总名额用满才结束,单个标签用满只跳过。
            if (picked == numWanted) {
                break;
            }

            int label = labels[id];
            int used = cnt.getOrDefault(label, 0);

            if (used == useLimit) {
                continue;
            }

            // 选中后同时维护标签用量、总分和已选数量。
            cnt.put(label, used + 1);
            sum += values[id];
            picked++;
        }

        return sum;
    }
}
import "sort"

func largestValsFromLabels(values []int, labels []int, numWanted int, useLimit int) int {
    idx := make([]int, len(values))
    for i := 0; i < len(values); i++ {
        idx[i] = i
    }

    // 按下标排序,保持分值与标签的对应。
    sort.Slice(idx, func(i, j int) bool {
        return values[idx[i]] > values[idx[j]]
    })

    cnt := make(map[int]int)
    picked := 0
    sum := 0

    for _, id := range idx {
        // 总名额用满才结束,单个标签用满只跳过。
        if picked == numWanted {
            break
        }
        label := labels[id]
        used := cnt[label]
        if used == useLimit {
            continue
        }

        // 选中后同时维护标签用量、总分和已选数量。
        cnt[label] = used + 1
        sum += values[id]
        picked++
    }

    return sum
}

复杂度分析

  • 时间复杂度:期望 $O(n\log(n+1))$。排序下标需要 $O(n\log(n+1))$,之后扫描一次,每次标签计数查询和更新的期望时间为 $O(1)$。
  • 空间复杂度:$O(n)$,下标数组与标签计数。

关键点总结

[!green]

  • 全局已满结束全部扫描,单个标签已满只跳过当前项。
  • 排序的是下标,不会打散值与标签关系。

易错点总结

[!yellow]

  • 只排序 values 会读到不对应的标签。
  • 某标签满了就退出,会漏掉后续其他标签。
  • 选中时忘记增加任一计数,会超过相应限制。
  • 合法候选不足 numWanted 时直接返回已有总和,不能为了凑满数量突破某个标签的上限。

相似题目

题目 难度 关联与区别
215. 数组中的第K个最大元素 中等 本题不能直接选全局前k大,还要在贪心扫描时维护每种标签已使用的配额。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2023/68910710
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!