Steiner系统:构建无重复、无余数的独特组合算法

聖光之護
发布: 2025-12-01 11:30:12
原创
854人浏览过

steiner系统:构建无重复、无余数的独特组合算法

本文探讨如何生成一组独特的组合,其中包含m个对象,每组n个对象,且每个对象对仅出现一次,无重复或剩余。我们将深入研究这一问题与Steiner系统的关联,介绍其存在性条件,并展示一个基于启发式和回溯的Python实现方法。尽管没有通用的生成算法,但通过本文的探讨,读者将理解其核心挑战及一种可行的解决方案。

引言:组合生成问题与Steiner系统

在组合数学中,存在一类特殊的问题:给定 m 个对象,需要将其分组,每组包含 n 个对象,并满足以下严格条件:

  1. 唯一性 (Unique Combinations):生成的每组组合都是独特的。
  2. 无重复对 (No Repetitions):任意两个对象仅能在恰好一个组中同时出现一次。例如,如果 [1, 2, 3] 已经存在,则 [1, 2, 4] 是不允许的,因为 [1, 2] 这个对重复了。
  3. 无余数 (No Remainders):所有生成的组都必须严格包含 n 个对象,不能有任何对象被遗漏或形成不完整的组。

这个问题的本质是构造一个 Steiner 系统,具体来说是 S(2, n, m)。一个 S(t, k, v) 系统定义为:在一个包含 v 个元素的集合中,选取一些 k 元素的子集(称为块),使得集合中任意 t 个元素的子集恰好包含在一个块中。对于我们当前的问题,t=2 (任意两个对象恰好出现一次),k=n (组大小),v=m (总对象数)。

Steiner系统的存在性条件

并非所有 m 和 n 的组合都能形成一个有效的Steiner系统。存在一些必要的数学条件。如果这些条件不满足,则系统不可能存在。然而,需要强调的是,这些条件是 必要而非充分 的,即使满足这些条件,系统也可能不存在。

对于 S(2, n, m) 系统,其存在的两个主要必要条件是:

  1. m - 1 必须能被 n - 1 整除。
  2. m * (m - 1) 必须能被 n * (n - 1) 整除。

这两个条件可以用于计算如果系统存在,将有多少个这样的组。以下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 配对过,导致冲突。此时,算法需要回溯并尝试不同的组合。

Remove.bg
Remove.bg

AI在线抠图软件,图片去除背景

Remove.bg 174
查看详情 Remove.bg

为了解决这个问题,我们可以设计一个带有回溯机制的启发式算法。

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
登录后复制

2. 启发式生成与回溯机制

核心算法通过循环尝试构建组。当遇到冲突或无法完成当前组时,它会回溯,撤销最近的一些选择,并尝试新的路径。

# 设定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)
登录后复制

代码说明:

  • id 类:每个对象 id 实例维护一个 comparisons 列表,记录它已经和哪些其他对象一起出现在某个组中。这对于检查“无重复对”至关重要。
  • valid_combos 函数:作为预检,快速判断 m 和 n 是否可能存在解。
  • 主循环 (while len(comparisons) < combos_required):持续尝试生成组,直到达到所需数量。
  • temp_group:用于构建当前正在尝试的组。
  • 第一个对象的特殊处理:在生成最初的几组时,通常会优先选择第一个对象(ids[0]),确保它能与其他对象充分配对。random.choice 的引入是为了增加探索路径的多样性,减少陷入局部最优解的概率。
  • 冲突检测 (counter):在将 id_a 添加到 temp_group 之前,会检查 id_a 是否已经与 temp_group 中已有的任何 id_b 配对过。如果配对过,则 id_a 不能加入当前组。
  • 无效组合剪枝 (invalid):invalid 列表存储了已被证明无法导致完整解的组合路径。当算法回溯时,会将当前导致问题的组合标记为无效,在后续尝试中避免再次生成。
  • 回溯机制 (try-except 块):当算法在 while len(temp_group) < n 循环中无法找到合适的 id 来完成当前组,或者 pos 超出 ids 列表范围时,会抛出异常。except 块捕获此异常,执行回溯操作:
    • 清空 temp_group。
    • 将导致问题的最后一个已生成组合(或其名称)添加到 invalid 列表中。
    • 移除 comparisons 列表中最近的一个或多个组合(这里示例是移除最后一个)。
    • 重置所有 id 对象的 comparisons 列表,然后根据 comparisons 中剩余的有效组合重新构建 comparisons 记录。这确保了状态的一致性。
    • continue 语句使算法重新开始外层循环,尝试从新的起点构建组合。

示例输出

使用上述启发式算法,我们可以为一些 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中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号