bluecode-优惠券优先级恢复

24 阅读7分钟

问题描述

在一次热闹的促销活动中,小C策划了一项优惠券发放计划,准备了 nn 种不同的优惠券,编号为 11 到 nn。为了确保活动顺利进行,小C为优惠券制定了几条规则:

  • 每位顾客最多只能领取其中一种优惠券。
  • 每种优惠券都有一个独特的优先级,优先级之间没有重复。
  • 只有满足某些特定条件的顾客才能领取对应的优惠券。
  • 如果一位顾客符合多个优惠券的发放条件,他只会领取其中优先级最高的优惠券。

活动结束后,小C发现用于分析的优惠券优先级记录丢失了。他只能依靠活动期间留下的 mm 条顾客发放记录进行恢复。每条记录包含了顾客满足发放条件的所有优惠券类型及他最终获得的优惠券。现在小C希望你能帮忙恢复这些优惠券的优先级顺序。

如果存在多个可能的解,输出字典序最小的解。

需要注意的是,array表中,每个列表的第一个元素表示后面的抽奖券数,第二个元素表示顾客实际拿到的抽奖券。


测试样例

样例1:

输入:n = 5, m = 3, array = [[3, 5, 4, 2], [3, 1, 3, 5], [2, 2, 4]]
输出:[1, 3, 5, 2, 4]

样例2:

输入:n = 4, m = 2, array = [[2, 3, 4], [3, 2, 1, 3]]
输出:[2, 1, 3, 4]

样例3:

输入:n = 3, m = 3, array = [[2, 2, 1], [2, 3, 1], [2, 3, 2]]
输出:[3, 2, 1]

#include <iostream>   // 用于输入输出流
#include <map> // 如果需要,可以用映射实现邻接表 (这里用 vector)
#include <queue>   // 用于使用优先队列 priority_queue (实现最小堆)
#include <set>     // 用于存储已添加的边,防止重复处理
#include <utility> // 用于 std::pair (在 set 中存储边)
#include <vector>  // 用于使用动态数组 vector

// 在这个特定上下文中,为了简洁使用 std 命名空间,
// 但在大型项目中通常建议使用显式限定 (例如 std::vector)。
using namespace std;

// 函数:根据给定的优惠券发放记录,计算并返回优惠券的优先级顺序(字典序最小)
// 参数:
//   n: 优惠券的总种数 (编号 1 到 n)
//   m: 顾客发放记录的数量
//   array: 包含 m 条记录的二维向量,每条记录格式为 {k, received_coupon,
//   eligible1, eligible2, ...}
//          k 是顾客满足条件的优惠券数量 (在此实现中未使用k,依赖于列表内容)
//          received_coupon 是顾客实际领取的优惠券 (优先级最高)
//          eligible1, eligible2, ... 是顾客满足条件的所有优惠券
// 返回值:
//   一个包含 n
//   个整数的向量,表示按优先级从高到低排列的优惠券编号(字典序最小的解)
//   如果输入导致优先级冲突(循环),则行为取决于问题保证(假设输入有效)
vector<int> solution(int n, int m, vector<vector<int>> array) {
  // 处理基本情况:m = 0 (没有记录,没有约束)
  // 此时字典序最小的排列就是 1, 2, ..., n
  if (m == 0 || array.empty()) {
    vector<int> default_result; // 创建一个空的默认结果向量
    for (int i = 1; i <= n; ++i) {
      default_result.push_back(i); // 填充 1 到 n
    }
    return default_result; // 返回默认顺序
  }

  // 图的表示:邻接表 (coupon -> list of lower priority coupons)
  // 使用 vector<vector<int>> 作为邻接表 (adj[i] 存储优先级低于 i 的优惠券列表)
  vector<vector<int>> adj(n + 1); // 大小为 n+1,用于 1-based 索引 (下标 1 到 n)

  // 每个优惠券(节点)的入度计数
  vector<int> in_degree(n + 1, 0); // 大小为 n+1,并初始化为 0

  // 使用集合 (Set) 来跟踪已添加的边,以避免重复增加 'in_degree'
  set<pair<int, int>> added_edges;

  // 根据顾客记录构建图
  for (const auto &record : array) { // 遍历每一条记录
    // 记录结构:[k, received_coupon, eligible1, eligible2, ...]
    // k 在索引 0, received_coupon 在索引 1
    if (record.size() < 2)
      continue; // 跳过格式错误的记录 (至少需要 k 和 received_coupon)

    int received_coupon = record[1]; // 获取顾客实际收到的优惠券

    // 确保收到的优惠券编号有效 (可选,但良好的编程实践)
    if (received_coupon < 1 || received_coupon > n) {
      // 如果需要,处理无效数据,此处假设输入有效
      continue;
    }

    // 遍历所有符合条件的优惠券 (从索引 2 开始)
    for (size_t i = 2; i < record.size(); ++i) {
      int eligible_coupon = record[i]; // 获取一个符合条件的优惠券

      // 确保符合条件的优惠券编号有效 (可选)
      if (eligible_coupon < 1 || eligible_coupon > n) {
        continue;
      }

      // 如果一个符合条件的优惠券与实际收到的不同,
      // 这意味着实际收到的优惠券有更高的优先级。
      // 添加一条有向边:received_coupon -> eligible_coupon
      if (eligible_coupon != received_coupon) {
        // 仅当这条边之前未被添加时才添加它
        // 使用 pair {received_coupon, eligible_coupon} 作为边的唯一标识
        if (added_edges.find({received_coupon, eligible_coupon}) ==
            added_edges.end()) {
          adj[received_coupon].push_back(
              eligible_coupon); // 在 received_coupon 的邻接表中添加
                                // eligible_coupon
          in_degree[eligible_coupon]++; // 将优先级较低的 eligible_coupon
                                        // 的入度加 1
          added_edges.insert(
              {received_coupon, eligible_coupon}); // 标记这条边已被添加
        }
      }
    }
  }

  // 使用 Kahn 算法进行拓扑排序,并结合最小堆以获得字典序最小的结果
  // 最小优先队列存储入度为 0 的节点
  priority_queue<int, vector<int>, greater<int>>
      pq; // 声明一个最小堆 (存储 int, 底层容器 vector<int>, 比较器
          // greater<int>)

  // 初始化队列:将所有入度为 0 的节点(优惠券)加入最小堆
  for (int i = 1; i <= n; ++i) { // 遍历所有优惠券编号 1 到 n
    if (in_degree[i] == 0) {     // 如果入度为 0
      pq.push(i); // 将其加入最小堆 (这些是当前优先级最高的)
    }
  }

  vector<int> result; // 用于存储拓扑排序结果(优先级列表)
  result.reserve(n);  // 预分配空间以提高效率

  // 处理优先队列中的节点
  while (!pq.empty()) { // 当队列不为空时
    // 获取队列中值最小的节点(在当前可选节点中优先级最高且编号最小,保证字典序)
    int u = pq.top();    // 获取堆顶元素
    pq.pop();            // 移除堆顶元素
    result.push_back(u); // 将该节点添加到结果列表中

    // 遍历节点 u 的所有邻居(优先级低于 u 的优惠券 v)
    // adj[u] 包含了所有 u 指向的节点 v
    for (int v : adj[u]) {
      // 确保邻居 v 有效 (在建图时已检查,此处为保险起见)
      if (v >= 1 && v <= n) {
        in_degree[v]--; // 将邻居 v 的入度减 1 (相当于逻辑上移除了 u->v 这条边对
                        // v 的依赖)
        // 如果邻居 v 的入度变为 0,说明它的所有前驱(更高优先级的券)都已被处理
        if (in_degree[v] == 0) {
          pq.push(v); // 将 v 加入优先队列,v 现在成为可选的最高优先级节点之一
        }
      }
    }
  }

  // 检查是否找到了覆盖所有节点的有效拓扑排序
  // 如果 result.size() != n,说明图中存在环(优先级冲突)。
  // 问题描述暗示对于有效输入总会存在一个解。
  if (result.size() == n) {
    return result; // 返回计算得到的优先级顺序
  } else {
    // 这种情况意味着输入数据存在矛盾(优先级形成了环)。
    // 问题要求找到 *那个* 解,暗示解总是存在且唯一的(字典序最小)。
    // 如果无法完成完整的排序,返回空向量或默认向量可能更合适,
    // 但根据问题假设,我们期望 result.size() == n。
    return result; // 返回计算出的结果。如果 size != n,则表示存在环或输入有误。
                   // 假设题目保证输入有效且无环。
  }
}

int main() {
  // 测试样例1
  {
    std::vector<std::vector<int>> array = {
        {3, 5, 4, 2}, {3, 1, 3, 5}, {2, 2, 4}};
    std::vector<int> result = solution(5, 3, array);
    bool correct = (result == std::vector<int>{1, 3, 5, 2, 4});
    std::cout << (correct ? "true" : "false") << std::endl;
  }

  // 测试样例2
  {
    std::vector<std::vector<int>> array = {{2, 3, 4}, {3, 2, 1, 3}};
    std::vector<int> result = solution(4, 2, array);
    bool correct = (result == std::vector<int>{2, 1, 3, 4});
    std::cout << (correct ? "true" : "false") << std::endl;
  }

  // 测试样例3
  {
    std::vector<std::vector<int>> array = {{2, 2, 1}, {2, 3, 1}, {2, 3, 2}};
    std::vector<int> result = solution(3, 3, array);
    bool correct = (result == std::vector<int>{3, 2, 1});
    std::cout << (correct ? "true" : "false") << std::endl;
  }

  return 0;
}