
本文深入探讨了如何从 `m` 个对象中生成 `n` 个一组的独特组合,要求每个对象对仅出现一次,且无重复或剩余。我们将此问题与组合数学中的 steiner 系统 `s(2, n, m)` 关联,阐述其存在性条件。鉴于缺乏通用算法,文章重点介绍了一种基于 python 的回溯搜索与剪枝策略的实现方法,并讨论了其原理、挑战及局限性,旨在为解决此类复杂组合问题提供实践指导。
在许多场景中,我们需要从一个包含 m 个元素的集合中,生成一系列大小为 n 的子集(组合),并满足以下严格条件:
例如,给定 m=9 个对象和 n=3 的分组大小,我们期望得到 12 个独特的组合,其中每个对象对(如 [1,2])只出现一次。一个可能的输出示例如下:
[1, 2, 3] [1, 4, 5] [1, 6, 7] [1, 8, 9] [2, 4, 9] [2, 5, 7] [2, 6, 8] [3, 4, 6] [3, 7, 9] [3, 5, 8] [4, 7, 8] [5, 6, 9]
上述问题在组合数学中被称为 Steiner 系统。一个 Steiner 系统通常表示为 S(t, k, v),它指的是一个包含 v 个元素的集合,其 k 元素子集(称为“块”)的集合,使得任何 t 个元素的子集恰好包含在一个块中。
对于我们提出的问题:
因此,我们的目标是构建一个 S(2, n, m) 型的 Steiner 系统。当 k=3 时,它被称为 Steiner 三元系(Steiner Triple Systems, STS);当 k=4 时,被称为 Steiner 四元系(Steiner Quadruple Systems, SQS)。
并非所有 m 和 n 的组合都能形成一个 Steiner 系统。存在一些必要的(但非充分的)条件来判断一个 Steiner 系统 S(t, k, v) 是否可能存在:
条件一: (v - 1) 必须能被 (k - 1) 整除。 这对应于在 v 个元素中,选择一个特定元素 x,它必须与 v-1 个其他元素配对。每个块中包含 k-1 个其他元素与 x 配对。因此,x 必须出现在 (v-1) / (k-1) 个块中。
条件二: v * (v - 1) 必须能被 k * (k - 1) 整除。 这对应于在 v 个元素中选择任意两个元素 (v choose 2),它们必须恰好出现在一个块中。每个块包含 (k choose 2) 个这样的对。因此,总的对数 v * (v - 1) / 2 必须是每个块中对数 k * (k - 1) / 2 的整数倍。
这些条件可以用于预先判断一个 Steiner 系统是否存在。如果这些条件不满足,则系统不可能存在。
当 Steiner 系统存在时,我们可以计算出需要生成的组合(块)的总数。这个数量等于总的可能配对数除以每个组合能提供的配对数。
其计算函数 valid_combos(m, n) 如下所示:
def valid_combos(m, n):
"""
计算 m 个对象,n 个一组的 Steiner 系统 S(2, n, m) 所需的组合数。
同时检查 Steiner 系统 S(2, n, m) 的必要存在条件。
"""
if n <= 1 or m <= 1:
return False # 无效的 m 或 n 值
# 条件一:(m-1) 必须能被 (n-1) 整除
if (m - 1) % (n - 1) != 0:
return False
# 条件二:m*(m-1) 必须能被 n*(n-1) 整除
# 这等价于 C(m, 2) / C(n, 2)
if (m * (m - 1)) % (n * (n - 1)) != 0:
return False
# 如果条件满足,计算组合数
return (m * (m - 1)) // (n * (n - 1))例如,valid_combos(9, 3) 返回 12,而 valid_combos(6, 3) 返回 False,因为 (6-1) % (3-1) 即 5 % 2 不为 0,不满足条件一。
尽管 Steiner 系统的概念清晰,但为任意 t, k, v 构造 Steiner 系统的通用算法至今仍是一个未解决的数学难题。对于 S(2, k, v) 这种特定类型,虽然 k=3 和 k=4 的情况得到了广泛研究,并存在一些特定的构造方法,但对于任意 k 和 v,并没有一个通用的、高效的算法。
这意味着,我们不能仅仅依靠简单的迭代或贪婪策略来生成这些组合。例如,在 m=9, n=3 的情况下,如果算法先生成了 [1, 8, 9] 和 [2, 4, 6],接着尝试生成 [2, 8, 9],就会发现 [8, 9] 这个对已经在 [1, 8, 9] 中出现过,导致无法继续。这种“死胡同”现象是算法必须处理的核心挑战。
因此,解决这类问题通常需要采用启发式方法,最常见的是回溯搜索(Backtracking Search)结合剪枝(Pruning)策略。
为了应对没有通用算法的挑战,我们可以设计一个基于回溯的启发式算法。其核心思想是:逐步构建组合,如果当前选择导致无法完成所有组合(即进入死胡同),则撤销最近的选择,尝试其他路径。
首先,我们需要一个机制来跟踪每个对象已经参与了哪些配对。为此,可以定义一个 id 类,每个对象实例存储其名称和已配对的伙伴列表。
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 other_id_name in id_list:
if other_id_name in self.comparisons:
self.comparisons.remove(other_id_name)
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 other_id_name in id_list:
if other_id_name in self.comparisons:
self.comparisons.remove(other_id_name)
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算法的核心是一个 while 循环,它不断尝试生成新的组合,直到达到预期的总组合数。在构建每个组合时,它会从可用对象中选择,并进行多重校验以确保满足 Steiner 系统的条件。
当算法发现无法构建一个有效组合,或者当前路径导致后续组合无法生成时,它会触发一个回溯机制。这通常通过 try-except 块来实现,捕获异常并撤销之前的操作。
以下是主生成逻辑的关键部分及其解释:
import random
# 假设 m, n 已设定,ids_master, ids, get_ids, valid_combos 已定义
# m = 9
# n = 3
# ids_master = get_ids(m)
# ids = ids_master.copy()
comparisons = [] # 存储所有已生成的有效组合(id 对象列表)
comparison_names = [] # 存储所有已生成的有效组合(名称列表)
invalid = [] # 存储已尝试但被判定为无效的组合路径
combos_required = valid_combos(m, n) # 需要生成的总组合数
while len(comparisons) < combos_required:
temp_group = [] # 当前正在构建的组合
current_pos = 0 # 用于遍历 ids 列表
# 内层循环:构建一个大小为 n 的组合
while len(temp_group) < n:
try:
# 启发式:在生成最初几组时,可能选择第一个ID作为固定点,
# 之后随机选择以增加探索性。
# (m - 1) / (n - 1) 是每个元素需要出现的组合次数
if len(comparisons) < (m - 1) / (n - 1):
if len(temp_group) == 0:
# 如果是组的第一个元素,选择第一个ID
temp_group.append(ids[0])
else:
# 否则,从剩余ID中随机选择一个
candidate_id = random.choice(ids[1:])
is_valid_candidate = True
for member_in_group in temp_group:
# 检查是否已配对或重复
if member_in_group.name in candidate_id.comparisons or \
member_in_group.name == candidate_id.name:
is_valid_candidate = False
break
if is_valid_candidate:
temp_group.append(candidate_id)
else:
# 正常情况:顺序选择 ID 并进行严格校验
candidate_id = ids[current_pos]
is_valid_candidate = True
# 检查与 temp_group 中现有元素的配对情况
for member_in_group in temp_group:
if member_in_group.name in candidate_id.comparisons or \
member_in_group.name == candidate_id.name:
is_valid_candidate = False
break
# 检查当前 candidate_id 是否已与所有其他 (m-1) 个对象配对
if len(candidate_id.comparisons) == m - 1:
is_valid_candidate = False
# 进一步校验:避免生成已被标记为“无效”的组合
if is_valid_candidate:
# 创建一个临时组名列表进行比较
v_check_names = [x.name for x in temp_group]
v_check_names.append(candidate_id.name)
v_check_names.sort() # 规范化顺序
for invalid_group_names in invalid_names: # invalid_names 需要在外部维护
if v_check_names == invalid_group_names:
is_valid_candidate = False
break
if is_valid_candidate:
temp_group.append(candidate_id)
current_pos += 1
# 如果 current_pos 超出 ids 范围,说明无法找到有效元素,需要回溯
if current_pos >= len(ids) and len(temp_group) < n:
raise Exception("无法找到下一个有效元素,需要回溯。")
except Exception as e:
# 发生异常,表示当前路径无法完成,需要回溯
# 清空当前临时组
temp_group.clear()
# 回溯策略:识别并移除导致问题的最近组合
num_to_remove = 0以上就是生成无重复无余数独特组合:Steiner 系统与回溯算法实践的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号