从 N 个商品中找出总价最小的 K 个方案

最近遇到一道有趣的算法题,想和大家分享一下:

问题描述:给定 N 个商品(每个商品只能选一次),从中找出总价最小的 K 个不同购买方案。每个方案是商品的一个子集,目标是返回总价格最小的前 K 个组合。

最直观的想法是暴力枚举------遍历所有 N! 种可能的子集,计算每种组合的总价,再排序取前 K 个。然而,当 N 较大时,这种方法在时间和空间上都不可行。

因此,我们需要一种更高效的策略。关键观察点在于:我们并不需要生成所有组合,而只需逐步"探索"出最小的 K 个方案 。这启发我们使用优先队列(最小堆)+ 剪枝 + 去重的方法,按需扩展最有希望的候选方案。

具体思路如下:

  1. 将每个单商品组合(即只买一个商品)作为初始方案加入最小堆;
  2. 每次从堆中取出当前总价最小的方案,将其加入结果集;
  3. 然后以该方案为基础,尝试加入尚未包含的商品,生成新的组合;
  4. 使用哈希集合记录已生成的组合(通过标准化表示去重),避免重复入队;
  5. 重复上述过程,直到收集到 K 个方案或队列为空。

这种方法避免了全量枚举,显著提升了效率,尤其适用于 K 远小于 2N 的场景。


优化后的 Java 代码
java 复制代码
import java.util.*;

/**
 * 最小花费商品组合问题
 * 给定 N 个商品的价格,找出总花费最小的 K 个不同购买方案。
 * 每个方案是商品的一个子集,每个商品最多被选择一次。
 */
public class MinCostCombinations {

    /**
     * 表示一个购买方案
     */
    static class Solution implements Comparable<Solution> {
        Set<Integer> indices;   // 商品编号集合(从 1 开始)
        long totalPrice;        // 方案总价格

        Solution(Set<Integer> indices, long totalPrice) {
            this.indices = new HashSet<>(indices);
            this.totalPrice = totalPrice;
        }

        @Override
        public int compareTo(Solution other) {
            if (this.totalPrice != other.totalPrice) {
                return Long.compare(this.totalPrice, other.totalPrice);
            }
            // 价格相同时,按集合的哈希值排序以保证比较一致性(虽非完美,但可接受)
            return Integer.compare(this.indices.hashCode(), other.indices.hashCode());
        }
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        try {
            int N = sc.nextInt();
            int K = sc.nextInt();
            int[] prices = new int[N];
            for (int i = 0; i < N; i++) {
                prices[i] = sc.nextInt();
            }

            List<Solution> results = findMinCostCombinations(prices, K);

            for (Solution sol : results) {
                List<Integer> sorted = new ArrayList<>(sol.indices);
                Collections.sort(sorted);
                for (int i = 0; i < sorted.size(); i++) {
                    if (i > 0) System.out.print(",");
                    System.out.print(sorted.get(i));
                }
                System.out.println(":" + sol.totalPrice);
            }
        } finally {
            sc.close();
        }
    }

    /**
     * 使用优先队列(最小堆)按需生成最小的 K 个组合
     * 
     * @param prices 商品价格数组
     * @param k      需要返回的方案数量
     * @return       按总价升序排列的前 K 个方案
     */
    private static List<Solution> findMinCostCombinations(int[] prices, int k) {
        PriorityQueue<Solution> pq = new PriorityQueue<>();
        Set<String> visited = new HashSet<>();
        List<Solution> result = new ArrayList<>();

        // 初始化:将每个单商品方案入队
        for (int i = 0; i < prices.length; i++) {
            Set<Integer> set = new HashSet<>();
            set.add(i + 1); // 编号从 1 开始
            Solution sol = new Solution(set, prices[i]);
            String key = getCanonicalKey(set);
            pq.offer(sol);
            visited.add(key);
        }

        while (result.size() < k && !pq.isEmpty()) {
            Solution current = pq.poll();
            result.add(current);

            // 扩展:尝试加入每一个未包含的商品
            for (int i = 0; i < prices.length; i++) {
                int idx = i + 1;
                if (!current.indices.contains(idx)) {
                    Set<Integer> newSet = new HashSet<>(current.indices);
                    newSet.add(idx);
                    String key = getCanonicalKey(newSet);
                    if (!visited.contains(key)) {
                        long newPrice = current.totalPrice + prices[i];
                        pq.offer(new Solution(newSet, newPrice));
                        visited.add(key);
                    }
                }
            }
        }

        return result;
    }

    /**
     * 生成方案的规范字符串表示,用于去重
     * 例如 {3, 1, 2} → "1 2 3"
     */
    private static String getCanonicalKey(Set<Integer> indices) {
        List<Integer> list = new ArrayList<>(indices);
        Collections.sort(list);
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < list.size(); i++) {
            if (i > 0) sb.append(' ');
            sb.append(list.get(i));
        }
        return sb.toString();
    }
}

补充说明

  • 时间复杂度:最坏情况下仍可能接近 O(K⋅Nlog(K⋅N)),但由于剪枝和去重,实际运行通常远优于暴力法。
  • 空间复杂度:主要由优先队列和去重集合决定,约为 O(K⋅N)。
相关推荐
乃瞻衡宇1 小时前
Agent Skills 完全指南:让你的 AI Agent 拥有超能力
算法
mit6.8241 小时前
pair<int, TreeNode*> dfs
算法
nil1 小时前
记录protoc生成代码将optional改成omitepty问题
后端·go·protobuf
superman超哥2 小时前
Rust 范围模式(Range Patterns):边界检查的优雅表达
开发语言·后端·rust·编程语言·rust范围模式·range patterns·边界检查
初晴や2 小时前
【C++】图论:基础理论与实际应用深入解析
c++·算法·图论
李泽辉_2 小时前
深度学习算法学习(五):手动实现梯度计算、反向传播、优化器Adam
深度学习·学习·算法
云上凯歌3 小时前
02 Spring Boot企业级配置详解
android·spring boot·后端
李泽辉_3 小时前
深度学习算法学习(一):梯度下降法和最简单的深度学习核心原理代码
深度学习·学习·算法
꧁Q༒ོγ꧂3 小时前
算法详解---大纲
算法
秋饼3 小时前
【手撕 @EnableAsync:揭秘 SpringBoot @Enable 注解的魔法开关】
java·spring boot·后端