
本文探讨如何生成一组独特的组合,其中包含m个对象,每组n个对象,且每个对象对仅出现一次,无重复或剩余。我们将深入研究这一问题与Steiner系统的关联,介绍其存在性条件,并展示一个基于启发式和回溯的Python实现方法。尽管没有通用的生成算法,但通过本文的探讨,读者将理解其核心挑战及一种可行的解决方案。
在组合数学中,存在一类特殊的问题:给定 m 个对象,需要将其分组,每组包含 n 个对象,并满足以下严格条件:
这个问题的本质是构造一个 Steiner 系统,具体来说是 S(2, n, m)。一个 S(t, k, v) 系统定义为:在一个包含 v 个元素的集合中,选取一些 k 元素的子集(称为块),使得集合中任意 t 个元素的子集恰好包含在一个块中。对于我们当前的问题,t=2 (任意两个对象恰好出现一次),k=n (组大小),v=m (总对象数)。
并非所有 m 和 n 的组合都能形成一个有效的Steiner系统。存在一些必要的数学条件。如果这些条件不满足,则系统不可能存在。然而,需要强调的是,这些条件是 必要而非充分 的,即使满足这些条件,系统也可能不存在。
对于 S(2, n, m) 系统,其存在的两个主要必要条件是:
这两个条件可以用于计算如果系统存在,将有多少个这样的组。以下Python函数 valid_combos 实现了这些检查:
def valid_combos(m, n):
"""
检查给定m和n是否满足Steiner系统S(2, n, m)的必要存在条件,
并返回所需组合的数量。
"""
num_pairs_total = m * (m - 1) // 2 # 总共可能的两两配对数 C(m, 2)
num_pairs_per_group = n * (n - 1) // 2 # 每组中的两两配对数 C(n, 2)
if n <= 1 or m <= 1: # n或m小于等于1没有意义
return False
if (m - 1) % (n - 1) != 0: # 条件1
return False
if num_pairs_total % num_pairs_per_group != 0: # 条件2
return False
# 如果满足条件,计算所需的组数
return num_pairs_total // num_pairs_per_group
# 示例
print(f"valid_combos(9, 3): {valid_combos(9, 3)}") # 输出 12
print(f"valid_combos(6, 3): {valid_combos(6, 3)}") # 输出 False由于没有通用的算法来构造任意 S(2, n, m) 系统,我们通常需要依赖启发式方法和回溯搜索。一个直接的贪婪方法往往会失败,因为它可能在早期做出导致无解的选择。
例如,对于 m=9, n=3,如果算法首先生成了 [1, 2, 3]、[1, 4, 5]、[1, 6, 7]、[1, 8, 9]、[2, 4, 6]、[2, 5, 7],那么接下来尝试生成 [2, 8, 9] 时就会发现 [8, 9] 已经和 1 配对过,导致冲突。此时,算法需要回溯并尝试不同的组合。
为了解决这个问题,我们可以设计一个带有回溯机制的启发式算法。
首先,定义一个 id 类来表示每个对象,并跟踪它已经与哪些其他对象进行过配对。
import random
class id:
def __init__(self, name):
self.name = name
self.comparisons = [] # 存储已配对的对象名称列表
def update_comparisons(self, id_list, mode='add'):
"""
更新对象的配对列表。
mode='add': 添加新的配对。
mode='del': 移除配对。
mode='reset': 清空所有配对。
"""
# 移除重复项
for item in id_list:
if item in self.comparisons:
self.comparisons.remove(item)
if mode == 'add':
self.comparisons.extend(id_list)
self.comparisons.sort()
# 确保自身不在配对列表中
if self.name in self.comparisons:
self.comparisons.remove(self.name)
elif mode == 'del':
for item in id_list:
if item in self.comparisons:
self.comparisons.remove(item)
self.comparisons.sort()
elif mode == 'reset':
self.comparisons.clear()
return self.comparisons
def get_ids(n):
"""生成n个id对象"""
ids = []
for i in range(1, n + 1):
ids.append(id(i))
return ids核心算法通过循环尝试构建组。当遇到冲突或无法完成当前组时,它会回溯,撤销最近的一些选择,并尝试新的路径。
# 设定m和n的值
m = 9
n = 3
# 创建id对象列表
ids_master = get_ids(m)
ids = ids_master.copy()
comparisons = [] # 存储已成功生成的组合
invalid = [] # 存储导致死胡同的无效组合(用于剪枝)
# 获取所需的组合数量
combos_required = valid_combos(m, n)
if not combos_required:
print(f"对于 m={m}, n={n},不存在有效的Steiner系统S(2, n, m)。")
else:
print(f"对于 m={m}, n={n},需要 {combos_required} 个组合。")
while len(comparisons) < combos_required:
temp_group = [] # 临时存储当前正在构建的组
try:
# 优先处理第一个对象,确保其被充分利用
if len(comparisons) < (m - 1) / (n - 1): # 第一个对象需要参与的组数
if not temp_group: # 如果是组的第一个元素,固定为ids[0]
temp_group.append(ids[0])
else: # 否则从剩余对象中随机选择
# 从除了ids[0]之外的ids中随机选择
# 注意:这里ids[1:]应该过滤掉已经在temp_group中的元素
available_ids = [obj for obj in ids[1:] if obj not in temp_group]
if not available_ids:
raise Exception("没有可用的ID来完成当前组。")
id_a = random.choice(available_ids)
# 检查id_a是否已与temp_group中的任何元素配对
is_valid_candidate = True
for id_b in temp_group:
if id_b.name in id_a.comparisons or id_b.name == id_a.name:
is_valid_candidate = False
break
if is_valid_candidate:
temp_group.append(id_a)
else:
# 如果随机选择的id_a无效,尝试其他id
# 简单地跳过当前循环,让while len(temp_group) < n 再次尝试
continue
else: # 对于后续的组,从ids列表中按顺序选择
pos = 0 # 遍历ids列表的索引
while len(temp_group) < n: # 继续构建组直到达到n个元素
if pos >= len(ids): # 如果遍历完所有ids仍未完成组,说明当前路径有问题
raise Exception("无法找到足够的ID来完成当前组。")
id_a = ids[pos]
# 检查id_a是否已在temp_group中
if id_a in temp_group:
pos += 1
continue
# 检查id_a是否已与temp_group中的任何元素配对
counter = 0
for id_b in temp_group:
if id_b.name in id_a.comparisons or id_b.name == id_a.name:
counter += 1
break # 发现冲突,无需继续检查
# 检查id_a是否已与所有其他m-1个对象配对(仅当它是组的第一个元素时需要此检查)
# 实际上,这个条件在这里可能过于严格,因为它阻止了id_a继续参与新的组
# 更好的做法是,如果id_a的配对数达到m-1,则它不能再作为新组的成员
# 但在这里,我们是构建一个组,只要它能与组内其他成员形成新的有效对即可
# if len(id_a.comparisons) == m - 1:
# counter += 1 # 标记为无效
# 检查当前组(temp_group + id_a)是否是已知的无效组合
if counter == 0:
potential_group = temp_group.copy()
potential_group.append(id_a)
potential_group_names = sorted([x.name for x in potential_group])
for iv_names in invalid: # invalid存储的是name列表
if potential_group_names == iv_names:
counter += 1 # 标记为无效
break
if counter == 0:
temp_group.append(id_a)
pos += 1
except Exception as e:
# print(f"发生异常或无法完成组:{e}。开始回溯。")
# 清空当前临时组
temp_group.clear()
# 确定要回溯(移除)多少个已生成的组合
# 这里的逻辑比较复杂,目标是移除导致当前死胡同的最近一组或多组
# 简单起见,可以尝试移除最后一个生成的组合,或者根据启发式判断
# 记录导致问题的组合
if comparisons:
last_comparison = comparisons[-1]
last_comparison_names = sorted([x.name for x in last_comparison])
if last_comparison_names not in invalid: # 避免重复添加
invalid.append(last_comparison_names)
# 回溯:移除最近的几个组合
# 具体的移除数量需要根据实际情况调整,这里只是一个示例
num_to_remove = 1 # 默认回溯一步
if len(comparisons) > 0:
# 尝试移除最后一个组合
comparisons.pop()
# 重置所有id的比较记录
for obj_id in ids_master:
obj_id.update_comparisons([], mode='reset')
# 重新构建已成功组合的比较记录
for comp_group in comparisons:
names = [x.name for x in comp_group]
for obj_id in comp_group: # comp_group中的是id对象
obj_id.update_comparisons(names, mode='add')
continue # 继续外层while循环,尝试重新生成组合
# 如果成功构建了一个完整的组
if len(temp_group) == n:
comparisons.append(temp_group)
# 更新所有相关id的比较记录
current_group_names = sorted([x.name for x in temp_group])
for obj_id in temp_group:
obj_id.update_comparisons(current_group_names, mode='add')
# 提取所有组合的名称
final_comparison_names = []
for comp_group in comparisons:
final_comparison_names.append(sorted([x.name for x in comp_group]))
print("\n生成的组合:")
for group in final_comparison_names:
print(group)代码说明:
使用上述启发式算法,我们可以为一些 m 和 n 的组合生成Steiner系统:
m = 7, n = 3
[[1, 2, 5], [1, 7, 4], [1, 3, 6], [2, 3, 4], [2, 6, 7], [3, 5, 7], [4, 5, 6]]
m = 9, n = 3
[[1, 8, 4], [1, 3, 2], [1, 9,
以上就是Steiner系统:构建无重复、无余数的独特组合算法的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号