从 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)。
相关推荐
起风了___5 小时前
Flask生产级模板:统一返回、日志、异常、JSON编解码,开箱即用可扩展
后端·python
骑着bug的coder5 小时前
第4讲:现代SQL高级特性——窗口函数与CTE
后端
Dwzun5 小时前
基于SpringBoot+Vue的农产品销售系统【附源码+文档+部署视频+讲解)
数据库·vue.js·spring boot·后端·毕业设计
y1y1z5 小时前
Spring Security教程
java·后端·spring
小橙编码日志5 小时前
分布式系统推送失败补偿场景【解决方案】
后端·面试
程序员根根5 小时前
Maven 核心知识点(核心概念 + IDEA 集成 + 依赖管理 + 单元测试实战)
后端
想用offer打牌5 小时前
RocketMQ如何防止消息丢失?😯
后端·面试·rocketmq
民乐团扒谱机5 小时前
【微实验】谱聚类之大规模数据应用——Nyström 方法
人工智能·算法·机器学习·matlab·数据挖掘·聚类·谱聚类
CoderYanger5 小时前
A.每日一题——3606. 优惠券校验器
java·开发语言·数据结构·算法·leetcode