Python实现带最小命中数与百分比条件的组合生成问题
彩票组合覆盖问题的正确实现与优化
现有代码的错误分析
你的代码逻辑完全偏离需求:
- 现有代码遍历每个t元子集,找到第一个能包含它的k元组合并加入结果,最后去重。这会把所有能覆盖至少一个t元子集的k元组合都纳入结果,但你需要的是最小(或满足条件)的组合集合,确保所有t元子集都被集合中的至少一个k元组合覆盖,而非所有能覆盖单个t元子集的组合。
- 以
n=5, k=3, t=2为例,现有代码会把(1,2,4)这类多余的组合加入,但实际上(1,2)已被(1,2,3)覆盖,(2,4)已被(2,3,4)覆盖,(1,4)被(1,4,5)覆盖,(1,2,4)完全不需要。
正确实现思路
这本质是集合覆盖问题:全集是所有t元号码子集,每个k元组合对应它能包含的所有t元子集,我们需要选最少的k元组合,使得它们的子集并集等于全集。该问题是NP-hard问题,小规模场景可用精确枚举,大规模场景需用贪心算法近似求解。
精确实现(适用于小规模n)
from itertools import combinations def find_min_cover(n, k, t): # 生成所有t元子集作为全集 all_t_subsets = set(combinations(range(1, n+1), t)) # 生成所有k元组合作为候选集合 all_k_combos = list(combinations(range(1, n+1), k)) # 初始最优解设为所有组合(最坏情况) best_cover = all_k_combos.copy() # 从最小组合数开始枚举,找到第一个全覆盖的集合 for cover_size in range(1, len(all_k_combos)+1): for candidate_cover in combinations(all_k_combos, cover_size): covered = set() for combo in candidate_cover: # 统计当前候选组合能覆盖的所有t元子集 covered.update(combinations(combo, t)) if covered == all_t_subsets: best_cover = candidate_cover return sorted(best_cover) return sorted(best_cover) # 测试n=5, k=3, t=2 for combo in find_min_cover(5, 3, 2): print(', '.join(map(str, combo)))
运行结果与你给出的正确输出一致:
1, 2, 3 1, 4, 5 2, 3, 4 2, 3, 5
贪心实现(适用于较大n,如n=49)
精确枚举在n=49时完全不可行,贪心算法每次选择能覆盖最多未被覆盖t元子集的k元组合,效率更高且结果接近最优:
from itertools import combinations from collections import defaultdict def greedy_set_cover(n, k, t): all_t_subsets = set(combinations(range(1, n+1), t)) remaining = all_t_subsets.copy() cover = [] all_k_combos = list(combinations(range(1, n+1), k)) # 预计算每个k元组合能覆盖的t元子集 combo_to_covered = {} for combo in all_k_combos: combo_to_covered[combo] = set(combinations(combo, t)) while remaining: # 找到覆盖最多剩余子集的组合 best_combo = None max_covered = 0 for combo, covered in combo_to_covered.items(): current_covered = len(remaining & covered) if current_covered > max_covered: max_covered = current_covered best_combo = combo # 更新覆盖集合与剩余子集 cover.append(best_combo) remaining -= combo_to_covered[best_combo] del combo_to_covered[best_combo] return sorted(cover) # 测试n=5, k=3, t=2 for combo in greedy_set_cover(5, 3, 2): print(', '.join(map(str, combo)))
处理非100%覆盖的情况
若只需覆盖指定百分比的t元子集,可修改贪心算法,达到目标覆盖比例后提前停止:
from itertools import combinations from collections import defaultdict def greedy_set_cover_with_percent(n, k, t, target_percent=0.95): all_t_subsets = set(combinations(range(1, n+1), t)) total = len(all_t_subsets) target_covered = int(total * target_percent) remaining = all_t_subsets.copy() cover = [] all_k_combos = list(combinations(range(1, n+1), k)) combo_to_covered = {} for combo in all_k_combos: combo_to_covered[combo] = set(combinations(combo, t)) covered_count = 0 while covered_count < target_covered and remaining: best_combo = None max_additional = 0 for combo, covered in combo_to_covered.items(): additional = len(remaining & covered) if additional > max_additional: max_additional = additional best_combo = combo # 更新覆盖状态 cover.append(best_combo) newly_covered = remaining & combo_to_covered[best_combo] covered_count += len(newly_covered) remaining -= newly_covered del combo_to_covered[best_combo] return sorted(cover) # 示例:覆盖n=5, k=3, t=2的80%子集 for combo in greedy_set_cover_with_percent(5, 3, 2, 0.8): print(', '.join(map(str, combo)))
原彩票场景的适配
针对你提到的6/49玩法:从49个号码中选27个重点号码,要求任意6个重点号码的子集(开奖命中),所选彩票中至少有一张包含这6个中的至少3个。可调整为击中集逻辑实现:
from itertools import combinations def lottery_hit_cover(pool_size=27, ticket_size=6, hit_min=3): # 生成27个重点号码的所有6元子集(目标命中集合) focus_numbers = list(range(1, pool_size+1)) target_subsets = set(combinations(focus_numbers, 6)) remaining = target_subsets.copy() cover = [] # 仅从重点号码中生成候选彩票(减少计算量) candidate_tickets = list(combinations(focus_numbers, ticket_size)) while remaining: best_ticket = None max_hit = 0 for ticket in candidate_tickets: # 统计该彩票能击中的剩余目标子集数(交集>=3) hit_count = 0 for subset in remaining: if len(set(ticket) & set(subset)) >= hit_min: hit_count += 1 if hit_count > max_hit: max_hit = hit_count best_ticket = ticket # 更新覆盖集合与剩余目标子集 cover.append(best_ticket) new_remaining = set() for subset in remaining: if len(set(best_ticket) & set(subset)) < hit_min: new_remaining.add(subset) remaining = new_remaining candidate_tickets.remove(best_ticket) return sorted(cover)
内容的提问来源于stack exchange,提问作者YoYoYo
相关产品推荐
相关产品推荐

