做题笔记:AB 实验用户反馈分类问题
问题概述
在AB实验中,工程师们会不断上线新实验或下线无效策略。在迭代过程中,可能会遇到意外情况,导致部分服务崩溃。为了防止系统全面崩溃,工程师们设置了监控报警机制,确保能够及时响应。小M同学需要根据用户反馈和实验命中情况,对问题进行分类。
具体来说:
- 有 N 名用户,每位用户命中了若干实验。
- 进行 Q 次查询,每次查询给定一组实验的命中或未命中条件,要求统计符合这些条件的用户数。
输入描述
- n:用户数量(1 ≤ n ≤ 105)
- m:实验数量(1 ≤ m ≤ 105)
- q:查询次数(1 ≤ q ≤ 105)
- arrayN:长度为 n 的列表,每个元素为用户命中的实验列表,格式为
[k_i, a_i1, a_i2, ..., a_ik_i],表示第 i 个用户命中了 ki 个实验,实验编号为 ai1, ai2, ..., aik_i。 - arrayQ:长度为 q 的列表,每个元素为查询条件,格式为
[c_i, b_i1, b_i2, ..., b_ic_i],表示第 i 次查询有 ci 个条件,bij > 0 表示命中实验 |bij|,bij < 0 表示未命中实验 |bij|。
输出描述
对于每个查询,输出符合条件的用户数量。
解题思路
1. 数据预处理
为了高效地回答查询,我们需要对用户的实验命中情况进行预处理:
-
实验到用户的映射:对于每个实验,记录命中该实验的所有用户。这可以通过一个字典或列表来实现,其中键是实验编号,值是命中该实验的用户集合。
-
用户全集:初始化一个全集,包含所有用户,用于处理没有必需命中的实验条件的情况。
2. 处理查询
每个查询包含多个条件,条件分为两类:
- 必需命中的实验(bij > 0)
- 必需未命中的实验(bij < 0)
处理步骤:
-
处理必需命中的实验:
- 对所有必需命中的实验,取这些实验对应用户集合的交集。只有同时命中所有这些实验的用户才满足条件。
- 为了优化交集操作的效率,可以先按命中用户数对必需命中的实验排序,先处理用户数较少的实验,减少交集的规模。
-
处理必需未命中的实验:
- 对所有必需未命中的实验,取这些实验对应用户集合的并集。将这个并集从之前的结果集中减去,得到最终符合条件的用户集合。
-
特殊情况:
- 如果查询中没有必需命中的实验,则初始结果集为用户全集。
- 如果查询中没有必需未命中的实验,则不需要进行减集操作。
3. 实现细节
- 使用集合操作:
- Python 的
set数据结构提供了高效的集合交集和差集操作,适合用于此问题。
- Python 的
- 优化:
- 由于实验数量和用户数量可能较大(105),需要注意内存和时间的优化。
- 可以考虑使用位运算或布尔数组来进一步优化,但在 Python 中集合操作已经足够高效。
- 在处理必需命中的实验时,优先选择命中用户数最少的实验,以减少后续交集的计算量。
4. 复杂度分析
-
预处理:
- 时间复杂度:O(N * K),其中 K 是用户命中的平均实验数。
- 空间复杂度:O(M * U),其中 U 是实验命中的平均用户数。
-
每个查询:
- 时间复杂度:O(C * log U),其中 C 是查询条件数,U 是命中用户数的平均值。
-
总复杂度:
- 预处理时间加上所有查询的时间,总体可接受。
代码实现
以下是基于上述思路的 Python 实现:
def solution(n, m, q, arrayN, arrayQ):
# 导入所需模块
from collections import defaultdict
# 预处理:构建实验到用户的映射
experiment_users = defaultdict(set) # key: experiment number, value: set of user IDs
for user_id in range(1, n + 1):
experiments = arrayN[user_id - 1]
k_i = experiments[0]
for exp in experiments[1:k_i + 1]:
experiment_users[exp].add(user_id)
# 全部用户集合
all_users = set(range(1, n + 1))
results = []
for query in arrayQ:
c_i = query[0]
conditions = query[1:c_i + 1]
required_experiments = []
forbidden_experiments = []
# 分离必需命中和必需未命中的实验
for b in conditions:
if b > 0:
required_experiments.append(b)
else:
forbidden_experiments.append(-b)
# 处理必需命中的实验
if required_experiments:
# 按命中用户数排序,优化交集顺序
required_experiments.sort(key=lambda x: len(experiment_users[x]))
result_set = experiment_users[required_experiments[0]].copy()
for exp in required_experiments[1:]:
result_set &= experiment_users[exp]
if not result_set:
break # 提前终止,避免无效计算
else:
result_set = all_users.copy()
# 处理必需未命中的实验
if forbidden_experiments and result_set:
forbidden_set = set()
for exp in forbidden_experiments:
forbidden_set |= experiment_users[exp]
# 如果所有用户都被禁止,则结果为空
if len(forbidden_set) == n:
break
result_set -= forbidden_set
# 记录符合条件的用户数量
results.append(len(result_set))
return results
if __name__ == "__main__":
# 样例测试
# 样例1
print(
solution(
3,
3,
3,
[[2, 1, 2], [2, 2, 3], [2, 1, 3]],
[[2, 1, -2], [2, 2, -3], [2, 3, -1]],
)
== [1, 1, 1]
)
# 样例2
print(
solution(
5,
4,
2,
[[3, 1, 2, 3], [1, 2], [2, 1, 4], [3, 2, 3, 4], [2, 1, 3]],
[[3, 1, -4, 2], [2, -1, -3]],
)
== [1, 1]
)
# 样例3
print(
solution(
4,
3,
2,
[[1, 1], [2, 2, 3], [1, 3], [2, 1, 2]],
[[1, -3], [2, 2, 3]],
)
== [2, 1]
)
# 额外测试用例4:没有必需命中或未命中的实验
print(
solution(
2,
2,
1,
[[1, 1], [1, 2]],
[[0]],
)
== [2]
)
# 额外测试用例5:所有用户被禁止
print(
solution(
3,
2,
1,
[[1, 1], [1, 2], [2, 1, 2]],
[[2, -1, -2]],
)
== [0]
)
代码解析
-
预处理阶段:
- 使用
defaultdict(set)来存储每个实验对应的用户集合。 - 遍历每个用户,记录其命中的所有实验。
- 使用
-
查询处理阶段:
- 对每个查询,分离出必需命中和必需未命中的实验。
- 如果有必需命中的实验,先从这些实验中取用户集合的交集。
- 通过按命中用户数排序,先处理用户数少的实验,优化交集效率。
- 如果有必需未命中的实验,取这些实验对应的用户集合的并集,并从结果集中减去这些用户。
- 记录最终符合条件的用户数量。
-
测试用例:
- 包含样例输入输出,确保代码的正确性。
- 额外添加了一些测试用例,覆盖更多边界情况。
注意事项
- 数据规模:由于 n、m、q 可能高达 105,需确保算法的时间和空间复杂度在可接受范围内。
- 集合操作的效率:Python 的
set提供了高效的交集和差集操作,但在极端情况下仍需注意性能。 - 内存优化:避免不必要的数据复制,尽量在原地操作集合以节省内存。