AB 实验用户反馈分类问题 | 豆包MarsCode AI 刷题

231 阅读6分钟

做题笔记: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)

处理步骤:

  1. 处理必需命中的实验

    • 对所有必需命中的实验,取这些实验对应用户集合的交集。只有同时命中所有这些实验的用户才满足条件。
    • 为了优化交集操作的效率,可以先按命中用户数对必需命中的实验排序,先处理用户数较少的实验,减少交集的规模。
  2. 处理必需未命中的实验

    • 对所有必需未命中的实验,取这些实验对应用户集合的并集。将这个并集从之前的结果集中减去,得到最终符合条件的用户集合。
  3. 特殊情况

    • 如果查询中没有必需命中的实验,则初始结果集为用户全集。
    • 如果查询中没有必需未命中的实验,则不需要进行减集操作。

3. 实现细节

  • 使用集合操作
    • Python 的 set 数据结构提供了高效的集合交集和差集操作,适合用于此问题。
  • 优化
    • 由于实验数量和用户数量可能较大(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]
    )

代码解析

  1. 预处理阶段

    • 使用 defaultdict(set) 来存储每个实验对应的用户集合。
    • 遍历每个用户,记录其命中的所有实验。
  2. 查询处理阶段

    • 对每个查询,分离出必需命中和必需未命中的实验。
    • 如果有必需命中的实验,先从这些实验中取用户集合的交集。
      • 通过按命中用户数排序,先处理用户数少的实验,优化交集效率。
    • 如果有必需未命中的实验,取这些实验对应的用户集合的并集,并从结果集中减去这些用户。
    • 记录最终符合条件的用户数量。
  3. 测试用例

    • 包含样例输入输出,确保代码的正确性。
    • 额外添加了一些测试用例,覆盖更多边界情况。

注意事项

  • 数据规模:由于 n、m、q 可能高达 105,需确保算法的时间和空间复杂度在可接受范围内。
  • 集合操作的效率:Python 的 set 提供了高效的交集和差集操作,但在极端情况下仍需注意性能。
  • 内存优化:避免不必要的数据复制,尽量在原地操作集合以节省内存。