小R的并集大小计算| 豆包MarsCode AI刷题

75 阅读6分钟

解题思路

这个问题的关键在于计算两个随机集合的并集大小的期望值。首先,我们需要理解并集的概念,即两个集合合并后的元素总数,但相同的元素只计算一次。由于每个集合中的元素都是唯一的且互不相同,我们可以利用集合的并集运算来计算并集的大小。

问题的输入包括一个整数 n 表示集合的数量,以及一个列表 st 包含 n 个集合。输出要求是随机选择两个集合并集大小的期望值,结果保留两位小数。

算法设计

  1. 组合计算:我们需要计算所有可能的集合对的组合数。对于 n 个集合,选择两个集合的组合数为 C(n, 2) = n * (n - 1) / 2
  2. 并集大小计算:对于每一对集合,计算它们的并集大小,并将所有并集大小的总和累加。
  3. 期望值计算:将所有并集大小的总和除以组合数,得到期望值。
  4. 结果格式化:将期望值保留两位小数并返回。

具体步骤如下:

  • 初始化一个变量 total_size 用于存储所有并集大小的总和。
  • 使用两层循环遍历所有可能的集合对,计算每对集合的并集大小并累加到 total_size
  • 计算期望值 expected_sizetotal_size 除以组合数 n * (n - 1) / 2
  • 格式化结果为保留两位小数的字符串。

代码分析

代码的主要部分如下:

def solution(n: int, st: list) -> str:
    total_size = 0
    for i in range(n):
        for j in range(i + 1, n):
            total_size += len(set(st[i]) | set(st[j]))
    expected_size = total_size / (n * (n - 1) / 2)
    return f"{expected_size:.2f}"
  • total_size 初始化total_size = 0 用于累加所有并集的大小。
  • 双层循环for i in range(n)for j in range(i + 1, n) 确保每对集合只被计算一次,并且不会重复计算自己与自己。
  • 并集计算len(set(st[i]) | set(st[j])) 计算两个集合的并集大小。set(st[i])set(st[j]) 将列表转换为集合,| 操作符计算并集。
  • 期望值计算expected_size = total_size / (n * (n - 1) / 2) 计算期望值,其中组合数 C(n, 2)n * (n - 1) / 2
  • 结果格式化f"{expected_size:.2f}" 将期望值格式化为保留两位小数的字符串。

可能出现的问题

  1. 数据类型转换:输入的集合是列表形式,需要转换为集合才能进行并集运算。如果输入的集合本身已经是集合类型,无需转换。
  2. 性能问题:对于较大的 n,双层循环的复杂度为 O(n^2),可能会导致性能下降。特别是当每个集合的大小也较大时,每次计算并集的复杂度为 O(m1 + m2),其中 m1m2 分别是两个集合的大小。
  3. 精度问题:计算期望值时,浮点数的精度可能会导致结果不准确。虽然 Python 的浮点数精度一般足够,但在某些情况下可能需要更高的精度。

个人的思考与改进

  1. 数据类型优化

    • 如果输入的集合已经是以集合类型提供的,可以直接使用,无需转换。这可以减少不必要的计算开销。
    • 代码中可以添加一个检查,如果输入是列表,再进行转换。
    if isinstance(st[0], list):
        st = [set(s) for s in st]
    
  2. 性能优化

    • 使用集合的并集运算 | 会重新创建一个新的集合,这在每次计算时可能会消耗较多时间。可以考虑使用集合的交集运算 & 和并集大小的公式来优化计算:
      • |A| + |B| - |A & B|,其中 |A||B| 分别是集合 AB 的大小,|A & B| 是它们的交集大小。
    • 这样可以减少每次计算并集时的开销。
    total_size = 0
    for i in range(n):
        for j in range(i + 1, n):
            total_size += len(st[i]) + len(st[j]) - len(st[i] & st[j])
    
  3. 代码结构优化

    • 代码可以通过函数封装来提高可读性和可维护性。例如,可以将并集大小的计算封装成一个单独的函数。
    • 这样可以使得主函数更加简洁,功能模块化,便于测试和扩展。
    def union_size(s1: set, s2: set) -> int:
        return len(s1) + len(s2) - len(s1 & s2)
    
    def solution(n: int, st: list) -> str:
        if isinstance(st[0], list):
            st = [set(s) for s in st]
        total_size = sum(union_size(st[i], st[j]) for i in range(n) for j in range(i + 1, n))
        expected_size = total_size / (n * (n - 1) / 2)
        return f"{expected_size:.2f}"
    
  4. 测试用例优化

    • 除了给定的测试样例,还可以增加一些边界情况和特殊情况的测试用例,例如集合为空的情况、所有集合相同的极端情况等。
    • 这有助于确保代码在各种输入下都能正确运行。
    if __name__ == '__main__':
        print(solution(2, [[1, 2], [1, 3, 5]]) == '4.00')
        print(solution(3, [[1, 4], [2, 5], [3, 6, 7]]) == '4.67')
        print(solution(2, [[10, 20, 30], [10, 30, 50, 70]]) == '5.00')
        print(solution(3, [[1], [2], [3]]) == '2.00')  # 边界情况:每个集合只有一个元素
        print(solution(3, [[1, 2, 3], [1, 2, 3], [1, 2, 3]]) == '6.00')  # 极端情况:所有集合相同
        print(solution(3, [[], [], []]) == '0.00')  # 集合为空的情况
    
  5. 算法复杂度分析

    • 当前算法的时间复杂度为 O(n^2 * (m1 + m2)),其中 n 是集合的数量,m1m2 是两个集合的大小。
    • 空间复杂度为 O(n * m),其中 m 是集合中元素的最大数量。
    • 对于较大的 nm,可以考虑使用更高效的算法或数据结构来优化性能,例如使用哈希表来快速查找交集。
  6. 进一步优化

    • 如果集合的元素范围已知且较小,可以使用位图(bitmap)来表示每个集合的元素,这样并集和交集的计算可以使用位运算来完成,进一步提高效率。
    • 位图的大小取决于元素的最大值,如果元素范围较大,则不适用。
  7. 多线程优化

    • 对于非常大的 n,可以考虑使用多线程或并行计算来加速并集大小的计算。
    • 例如,可以使用 Python 的 multiprocessing 模块来并行计算每对集合的并集大小。
  8. 数学优化

    • 从数学角度考虑,可以尝试推导出一个更高效的公式来计算期望值。
    • 例如,可以利用概率论中的期望值公式来减少计算量。

结论

通过上述分析和改进,我们可以看到,计算随机选择两个集合的并集大小的期望值是一个典型的组合问题。通过优化数据类型、性能、代码结构和测试用例,可以提高代码的效率和可靠性。进一步的数学优化和多线程优化也可以在特定情况下提供更好的性能。这些问题和改进方案不仅有助于解决当前问题,还可以为类似问题的解决提供参考。希望这篇读书笔记对理解和优化这个问题有所帮助。