讲师中心 微信公众号
AI工具推荐 视频效率加速

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

风强姑娘_7301

风强姑娘_7301

发布时间:2025-12-01 11:30:12

|

943人浏览过

|

来源于php中文网

原创

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 配对过,导致冲突。此时,算法需要回溯并尝试不同的组合。

361SCAN security check your skill python scripts
361SCAN security check your skill python scripts

扫描并分析已安装技能,用于查看指定技能目录的名称、描述和详情,或列出所有已安装的技能。

下载

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

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, 

热门AI工具

更多
立刻MV
立刻MV Hot

立刻MV是一款AI文本写作工具,AI 音乐视频(MV)创作工具。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

讯飞绘文

讯飞绘文是一款由科大讯飞推出的一站式 AIGC 内容运营平台。

DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

音述AI
音述AI Hot

一款AI音频处理工具,主要用于音述AI是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区,适合需要提升相关任务效率的用户。

Laper
Laper Hot

Laper是专为编剧、导演和制片人推出的 AI 原生剧本创作工具。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

UpDream
UpDream Hot

一款AI视频创作工具,主要用于哔哩哔哩推出的自研AI视频创作工具,适合需要提升相关任务效率的用户。

墨刀AI
墨刀AI Hot

一款AI图像与设计工具,主要用于产品经理的专属智能体,适合需要提升相关任务效率的用户。

相关专题

更多
while的用法
while的用法

while的用法是“while 条件: 代码块”,条件是一个表达式,当条件为真时,执行代码块,然后再次判断条件是否为真,如果为真则继续执行代码块,直到条件为假为止。本专题为大家提供while相关的文章、下载、课程内容,供大家免费下载体验。

354

2023.09.25

java break和continue
java break和continue

本专题整合了java break和continue的区别相关内容,阅读专题下面的文章了解更多详细内容。

787

2025.10.24

页面置换算法
页面置换算法

页面置换算法是操作系统中用来决定在内存中哪些页面应该被换出以便为新的页面提供空间的算法。本专题为大家提供页面置换算法的相关文章,大家可以免费体验。

6076

2023.08.14

Gradle插件开发入门教程
Gradle插件开发入门教程

本专题介绍Gradle插件开发的基本流程,涵盖插件类编写、自定义任务注册、扩展参数设计和插件应用方法,帮助你理解插件如何组织构建逻辑,减少多个项目之间的重复配置。

0

2026.10.22

Gradle Wrapper配置教程
Gradle Wrapper配置教程

本专题介绍Gradle Wrapper的基本作用,涵盖文件组成、生成方法、版本配置和任务调用,帮助你理解项目如何使用指定的Gradle版本,减少不同开发环境带来的构建差异。

0

2026.10.22

Golangci-lint检查器使用教程
Golangci-lint检查器使用教程

本专题整理Golangci-lint常用Linters的用途与配置方法,覆盖错误处理、无效代码、安全、复杂度、命名和依赖检查,帮助项目合理选规则。

0

2026.10.22

Gradle怎么构建Java项目
Gradle怎么构建Java项目

本专题介绍Gradle Java项目构建方法,涵盖Java插件配置、依赖声明、源码编译、测试执行和打包检查,帮助你理解各阶段的任务关系,逐步完成项目构建。

0

2026.10.22

PHP时间戳漏洞修复教程
PHP时间戳漏洞修复教程

本专题汇总PHP常见时间戳漏洞,包括2038年溢出、时区漂移、时间戳伪造、重放攻击、时间盲注与日志审计失效等,并给出升级64位环境、使用 DateTimeImmutable、统一UTC、严格超时校验、预处理绑定、hash_equals 比对等修复方案,帮助开发者快速排查和加固时间相关代码。

0

2026.10.22

Golangci-lint配置文件与规则设置
Golangci-lint配置文件与规则设置

本专题聚焦Golangci-lint YAML配置文件,讲清运行参数、规则启停、目录排除、问题过滤及严重级别设置,便于团队统一检查标准。

0

2026.10.22

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Uvicorn官方安装与入门手册
Uvicorn官方安装与入门手册

共0课时 | 0人学习

Python uv官方项目管理指南
Python uv官方项目管理指南

共0课时 | 0人学习

Python uv官方安装与入门文档
Python uv官方安装与入门文档

共0课时 | 0人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn