每日一题——Python实现PAT甲级1063 Set Similarity(举一反三+思想解读+逐步优化)


一个认为一切根源都是“自己不够强”的INTJ

个人主页:用哲学编程-CSDN博客
专栏:每日一题——举一反三
Python编程学习
Python内置函数

Python-3.12.0文档解读

目录

我的写法

优点

改进建议

时间复杂度分析

空间复杂度分析

总结

我要更强

优化方法

优化后的代码

优化分析

哲学和编程思想

1. 懒惰求值(Lazy Evaluation)

2. 缓存(Memoization)

3. 最小化冗余(Minimization of Redundancy)

4. 对称性和不变性(Symmetry and Invariance)

5. 空间换时间(Space-Time Tradeoff)

6. 分治思想(Divide and Conquer)

7. 避免重复工作(Avoiding Duplication of Work)

总结



题目链接

我的写法

N = int(input())  # 读取整数N,表示集合的数量
sets = []  # 初始化一个空列表,用于存储集合

def similarity(set1, set2):
    # 定义一个函数 similarity,它接收两个集合 set1 和 set2 作为参数
    # 计算两个集合的相似度(交集大小除以并集大小,并乘以100得到百分比形式)
    return f"{len(set1 & set2) / len(set1 | set2) * 100:.1f}%"

for i in range(N):
    # 循环N次,读取每个集合
    # 使用 split() 分割输入,忽略第一个元素(因为它是集合的序号),将剩余元素转换为集合并添加到 sets 列表中
    sets.append(set(input().split()[1:]))

K = int(input())  # 读取整数K,表示查询的数量
for _ in range(K):
    # 循环K次,读取每个查询
    i, j = map(int, input().split())  # 读取两个整数 i 和 j,表示查询的两个集合的编号
    # 调用 similarity 函数计算 sets[i-1] 和 sets[j-1] 的相似度(集合编号从1开始,所以要减1)
    print(similarity(sets[i - 1], sets[j - 1]))

 这段代码的功能是读取多个集合并存储,计算和输出指定集合对的相似度。以下是对这段代码的专业点评和复杂度分析:

优点

  1. 简洁明了:代码逻辑清晰,结构简单,方便理解和维护。
  2. 函数封装:使用了similarity函数进行相似度计算,使代码模块化且易于复用。
  3. 直接输入处理:通过input().split()处理输入,简化了数据的读取过程。

改进建议

  1. 错误处理:代码没有处理输入的合法性和范围问题,比如负数或超出集合数量的查询。这可能导致运行时错误。
  2. 性能优化:当集合非常大时,交集和并集的计算可能会导致性能问题,可以考虑使用更高效的数据结构或算法。
  3. 注释和文档:虽然已经逐行加注释,但在实际应用中,详细的文档和函数说明也是必要的。

时间复杂度分析

  1. 读取集合:读取每个输入集合的时间复杂度为O(M),其中M是每个集合的平均大小。因此,读取N个集合的总时间复杂度为O(N * M)。
  2. 计算相似度:
    • 交集操作set1 & set2的时间复杂度为O(min(len(set1), len(set2)))。
    • 并集操作set1 | set2的时间复杂度为O(len(set1) + len(set2))。
    • 因此,单次相似度计算的时间复杂度为O(len(set1) + len(set2)),在最坏情况下是O(2M) = O(M)。
  3. 总相似度计算:有K次查询,因此所有查询的总时间复杂度为O(K * M)。

综上,总时间复杂度为: [ O(N * M + K * M) ] 在最坏情况下,N和K都非常大时可以简化为: [ O((N + K) * M) ]

空间复杂度分析

  1. 存储集合:每个集合的空间复杂度为O(M),N个集合的总空间复杂度为O(N * M)。
  2. 临时变量:交集和并集操作会创建临时集合,其空间复杂度为O(len(set1) + len(set2)),即最坏情况下是O(2M) = O(M)。

因此,总空间复杂度为: [ O(N * M) ] 这包括了存储所有集合所需的空间和计算交集并集时的临时空间。

总结

这段代码在功能实现上是正确的,但在处理大数据集时,性能和空间利用上可能需要进一步优化。可以考虑引入错误处理机制和优化集合操作的方法以提高效率。


我要更强

优化代码的时间复杂度和空间复杂度是非常重要的,对于处理大数据集尤其如此。以下是一些可能的优化方法:

优化方法

  1. 预计算:可以在读取集合时预先计算并存储每个集合的大小,这样在计算并集大小时就不需要重新计算集合的长度。
  2. 哈希表存储大小:使用哈希表存储每个集合的大小和每对集合的交集大小,可以避免重复计算。

优化后的代码

N = int(input())  # 读取整数N,表示集合的数量
sets = []  # 初始化一个空列表,用于存储集合
sizes = []  # 初始化一个空列表,用于存储每个集合的大小
intersection_sizes = {}  # 初始化一个字典,用于存储集合对的交集大小

def similarity(size1, size2, intersection_size):
    # 定义一个函数 similarity,用于计算两个集合的相似度
    union_size = size1 + size2 - intersection_size
    return f"{intersection_size / union_size * 100:.1f}%"

for i in range(N):
    # 循环N次,读取每个集合
    current_set = set(input().split()[1:])  # 忽略第一个元素(因为它是集合的序号)
    sets.append(current_set)  # 将集合添加到sets列表中
    sizes.append(len(current_set))  # 存储集合的大小

K = int(input())  # 读取整数K,表示查询的数量
queries = [tuple(map(int, input().split())) for _ in range(K)]  # 预先读取所有查询并存储为元组列表

for i, j in queries:
    idx1, idx2 = min(i - 1, j - 1), max(i - 1, j - 1)  # 确保idx1 < idx2
    if (idx1, idx2) not in intersection_sizes:
        # 计算交集大小并存储在字典中
        intersection_size = len(sets[idx1] & sets[idx2])
        intersection_sizes[(idx1, idx2)] = intersection_size
    else:
        intersection_size = intersection_sizes[(idx1, idx2)]
    
    # 计算并打印相似度
    print(similarity(sizes[idx1], sizes[idx2], intersection_size))

优化分析

优化后,总时间复杂度仍为: [ O(N * M + K * M) ] 但是在查询过程中,利用了缓存的交集大小,减少了重复计算。

  1. 时间复杂度:
    • 读取集合:与原始代码相同,时间复杂度为O(N * M)。
    • 预计算和存储大小:每个集合的大小存储一次,时间复杂度为O(N)。
    • 查询相似度:每次查询时,只需计算一次交集并存储,后续查询相同对时直接读取,时间复杂度为O(K * M)。
  2. 空间复杂度:
  • 存储集合:与原始代码相同,空间复杂度为O(N * M)。
  • 存储集合大小和交集大小:额外使用了O(N)的空间存储每个集合的大小,以及O(K)的空间存储交集大小(最坏情况下每对集合都被查询一次)。

优化后,总空间复杂度为: [ O(N * M + N + K) ]

通过这些优化,代码在处理大数据集时的性能得到了显著提升,尤其是在查询阶段通过缓存交集大小减少了重复计算,提高了效率。


哲学和编程思想

在优化代码以及改善时间复杂度和空间复杂度的过程中,我们应用了多种哲学和编程思想。以下是一些关键思想及其具体应用:

1. 懒惰求值(Lazy Evaluation)

懒惰求值是一种编程技术,它推迟计算直到真正需要结果时才进行计算。在我们的代码中,我们通过以下方式应用了懒惰求值的思想:

具体应用:

  • 预计算交集大小:只有在需要计算相似度时才计算交集大小,而不是在读取集合时就计算。
  • 缓存结果:存储已经计算过的交集大小,以避免重复计算。

示例:

if (idx1, idx2) not in intersection_sizes:
    intersection_size = len(sets[idx1] & sets[idx2])
    intersection_sizes[(idx1, idx2)] = intersection_size
else:
    intersection_size = intersection_sizes[(idx1, idx2)]

2. 缓存(Memoization)

缓存是一种优化技术,通过存储函数的计算结果以避免重复计算。我们的代码利用字典缓存交集大小,从而减少重复计算,提升性能。

具体应用:

  • 缓存交集大小:使用字典存储每对集合的交集大小,避免相同计算的重复。

示例:

intersection_sizes = {}  
# 初始化一个字典,用于存储集合对的交集大小

3. 最小化冗余(Minimization of Redundancy)

减少冗余是优化的一种基本原则,通过消除重复计算和数据存储,提升代码的效率。这在我们的代码中通过预计算和缓存体现出来。

具体应用:

  • 预计算集合大小:在读取集合时预先计算并存储每个集合的大小,避免在计算相似度时重复计算集合大小。

示例:

sizes.append(len(current_set))  # 存储集合的大小

4. 对称性和不变性(Symmetry and Invariance)

在处理集合对时利用对称性特性,即集合 A 与集合 B 的交集与集合 B 与集合 A 的交集是相同的,从而减少计算和存储的复杂度。

具体应用:

  • 保持索引顺序一致:通过保证 idx1 < idx2,我们利用了交集运算的对称性,简化了逻辑。

示例:

idx1, idx2 = min(i - 1, j - 1), max(i - 1, j - 1)  # 确保 idx1 < idx2

5. 空间换时间(Space-Time Tradeoff)

通过使用更多的内存来缓存计算结果,从而减少计算时间。这种技术在我们的代码中通过使用字典缓存交集大小得以体现。

具体应用:

  • 缓存交集大小:以空间换时间,通过使用字典存储交集大小提升查询效率。

示例:

intersection_sizes[(idx1, idx2)] = intersection_size

6. 分治思想(Divide and Conquer)

分治思想是将复杂问题分解成较小的子问题逐个解决。虽然在我们的具体代码中没有直接使用这种思想,但整体优化的过程包含了将读取集合、预计算、查询分解开来分别处理的思路。

具体应用:

  • 分阶段处理:将读取集合、缓存信息和查询相似度分开处理,每个阶段专注于特定任务。

示例:

for i in range(N):  # 读取集合并存储大小
for i, j in queries:  # 处理查询

7. 避免重复工作(Avoiding Duplication of Work)

通过缓存和预计算来避免重复计算,是一种高效的程序设计策略。

具体应用:

  • 预计算和缓存:上述提到的多次预计算和缓存实际应用了避免重复工作的思想。

示例:

if (idx1, idx2) not in intersection_sizes:
    intersection_size = len(sets[idx1] & sets[idx2])
    intersection_sizes[(idx1, idx2)] = intersection_size
else:
    intersection_size = intersection_sizes[(idx1, idx2)]

总结

这些哲学和编程思想共同作用,能够使人编写出更高效、更易维护的代码。通过懒惰求值、缓存、最小化冗余、对称性、空间换时间、分治及避免重复工作等思想,有效地优化了代码的性能。


感谢阅读。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

用哲学编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值