当前位置:首页 > 技术 > 正文内容

计算技能辅导关系的有序对数量

访客 技术 2026年8月30日 1

本技术文章探讨如何高效解决一个技能辅导匹配问题。假定存在 N 名学生和 S 项不同的技能,这些技能用 1 到 S 的整数表示。每位学生 i 掌握 c_i 项技能,其中 1 ≤ c_i ≤ 5。如果学生 i 掌握某项学生 j 不具备的技能,则称学生 i 可以辅导学生 j。我们的目标是计算满足此条件的所有有序学生对 (i, j) 的总数量。

问题分析与转化

直接计算所有满足"学生 i 可以辅导学生 j"的对数可能比较复杂。我们可以转而计算其补集:即学生 i 不能辅导学生 j 的对数,然后用总的可能对数 N * N 减去这个补集的数量。学生 i 不能辅导学生 j 的条件是:学生 i 掌握的所有技能,学生 j 也全部掌握。换言之,学生 i 的技能集合是学生 j 技能集合的子集。

考虑到问题的对称性,计算"学生 i 技能集是学生 j 技能集子集"的对数,与计算"学生 j 技能集是学生 i 技能集子集"的对数是等价的。因此,我们可以选择计算后者,即统计所有有序对 (i, j),使得学生 j 的技能集是学生 i 技能集的子集。最终的答案就是 N * N 减去这个统计值。

关键约束与优化思路

  • 学生总数 N 最高可达 5 × 10^4
  • 技能总数 S 最高可达 1000
  • 每位学生掌握的技能数量 c_i 非常小,仅为 15。这是解决此问题的关键。
  • 时间限制:20秒,内存限制:1GB。

由于 c_i 的上限非常小,每个学生的技能集合的非空子集数量非常有限 (最多 2^5 - 1 = 31 个)。这启发我们可以对每个学生的技能集合及其子集进行有效处理。

初始实现(效率较低)

一种直观的方法是:

  1. 首先,读取所有学生的技能集合,对每个集合内部的技能进行排序。
  2. 使用 std::map<std::vector<int>, int> 来统计每种独特技能集合出现的频率。
  3. 遍历每位学生 P_i (0N-1),获取其技能集合 S_i
  4. 生成 S_i 的所有非空子集 T
  5. 对于每个子集 T,查询 std::map 中有多少学生的技能集合恰好是 T。将这些数量累加到 total_subset_pairs 变量中。这个变量将累加所有 (i, j) 对,使得 S_jS_i 的非空子集。由于 c_j ≥ 1S_j 总是非空的,所以此计数涵盖了所有 S_jS_i 子集的情况。
  6. 最终结果是 N * N - total_subset_pairs

然而,这种方法在处理大量数据时效率低下。主要瓶颈在于 std::map 的键是 std::vector<int> 类型。每次对 map 的访问(插入或查找)都需要对 vector 进行比较,其时间复杂度取决于 vector 的大小。尽管 vector 大小上限为 5,但 N 值很大,导致大量的 vector 比较操作使得总体运行时间过长。

#include <iostream>
#include <vector>
#include <algorithm> // For std::sort
#include <map>       // For std::map

// 解决单个测试用例的函数
void solve_case_slow() {
    int num_people, total_skill_types;
    std::cin >> num_people >> total_skill_types;

    std::vector<std::vector<int>> all_person_skills;
    std::map<std::vector<int>, int> skill_set_frequencies;

    // 读取所有学生的技能集合,并统计频率
    for (int i = 0; i < num_people; ++i) {
        int skills_count;
        std::cin >> skills_count;
        std::vector<int> current_person_skills(skills_count);
        for (int j = 0; j < skills_count; ++j) {
            std::cin >> current_person_skills[j];
        }
        std::sort(current_person_skills.begin(), current_person_skills.end()); // 排序内部技能
        all_person_skills.push_back(current_person_skills);
        skill_set_frequencies[current_person_skills]++; // 统计该技能集合的频率
    }

    long long total_pairs_j_subset_of_i = 0; // 统计 (i, j) 对,其中 S_j 是 S_i 的子集

    // 遍历每个学生 i
    for (int i = 0; i < num_people; ++i) {
        int current_skills_size = all_person_skills[i].size();
        // 枚举学生 i 的技能集合 S_i 的所有非空子集
        // c_i ≥ 1 保证 current_skills_size ≥ 1
        // submask 从 1 开始,保证子集非空
        for (int submask = 1; submask < (1 << current_skills_size); ++submask) {
            std::vector<int> current_subset_skills;
            for (int j = 0; j < current_skills_size; ++j) {
                if ((submask >> j) & 1) { // 如果第 j 位技能在子集中
                    current_subset_skills.push_back(all_person_skills[i][j]);
                }
            }
            // 统计有多少学生的技能集合恰好等于 current_subset_skills
            total_pairs_j_subset_of_i += skill_set_frequencies[current_subset_skills];
        }
    }

    long long result = (long long)num_people * num_people - total_pairs_j_subset_of_i;
    std::cout << result << std::endl;
}

int main() {
    // 启用快速I/O
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int T_test_cases;
    std::cin >> T_test_cases;
    for (int t = 1; t <= T_test_cases; ++t) {
        // 如果竞赛平台要求特定输出格式,例如 "Case #1: ",可以在此处添加
        // std::cout << "Case #" << t << ": ";
        solve_case_slow();
    }
    return 0;
}

优化方案

为了提高效率,我们可以避免使用 std::map 进行频率统计和查找,转而利用 std::vector 的排序和二分查找功能:

  1. 将所有学生的技能集合(已内部排序)收集到一个 std::vector<std::vector<int>> 中。
  2. 对这个包含所有技能集合的 vector 进行整体排序。这样,相同的技能集合会相邻排列。
  3. 当需要统计某个特定子集 T 出现的频率时,我们可以使用 std::lower_boundstd::upper_bound 在已排序的 vector 中高效地查找 T 的出现范围,从而获得其频率。这种查找的时间复杂度为 O(C_max log N),其中 C_max 是技能集合的最大大小(此处为 5)。
  4. 在数据读取阶段,可以预先为 std::vector<std::vector<int>> 分配好空间,以避免频繁的内存重新分配。

这个优化方案将 map 查找的对数比较操作替换为 std::vector 的二分查找,显著提升了性能。

#include <iostream>
#include <vector>
#include <algorithm> // For std::sort, std::lower_bound, std::upper_bound
#include <utility>   // For std::move (C++11 and later)

// 解决单个测试用例的优化函数
void optimized_solve_case() {
    int num_students, total_skill_types;
    std::cin >> num_students >> total_skill_types;

    // 预分配空间,避免频繁的内存重新分配
    std::vector<std::vector<int>> all_student_skill_sets(num_students);

    // 读取所有学生的技能集合并排序
    for (int i = 0; i < num_students; ++i) {
        int student_skill_count;
        std::cin >> student_skill_count;
        // 使用局部变量 temp_skills 存储当前学生的技能,然后通过移动语义赋值
        std::vector<int> temp_skills(student_skill_count);
        for (int j = 0; j < student_skill_count; ++j) {
            std::cin >> temp_skills[j];
        }
        std::sort(temp_skills.begin(), temp_skills.end());
        all_student_skill_sets[i] = std::move(temp_skills); // (1) 优化点:使用移动赋值
    }

    // 对所有学生的技能集合进行整体排序,以便后续进行二分查找
    std::sort(all_student_skill_sets.begin(), all_student_skill_sets.end());

    long long count_j_subset_of_i = 0; // 统计 (i, j) 对,其中 S_j 是 S_i 的子集

    // 遍历每个学生 i
    for (int i = 0; i < num_students; ++i) {
        int current_student_skills_size = all_student_skill_sets[i].size();
        // 遍历当前学生 S_i 的所有非空子集
        // 题目限制 c_i ≥ 1,所以 current_student_skills_size ≥ 1。
        // 子集 submask 从 1 开始,确保生成的子集 generated_subset_skills 也非空。
        for (int submask = 1; submask < (1 << current_student_skills_size); ++submask) {
            std::vector<int> generated_subset_skills;
            for (int k = 0; k < current_student_skills_size; ++k) {
                if ((submask >> k) & 1) { // 检查位 k 是否被设置
                    generated_subset_skills.push_back(all_student_skill_sets[i][k]);
                }
            }
            // 使用二分查找统计有多少学生的技能集合与 generated_subset_skills 完全相同
            auto range_start = std::lower_bound(all_student_skill_sets.begin(), all_student_skill_sets.end(), generated_subset_skills);
            auto range_end = std::upper_bound(all_student_skill_sets.begin(), all_student_skill_sets.end(), generated_subset_skills);
            count_j_subset_of_i += std::distance(range_start, range_end);
        }
    }

    // 总对数减去不能辅导的对数,即为可辅导的对数
    long long total_tutoring_pairs = (long long)num_students * num_students - count_j_subset_of_i;
    std::cout << total_tutoring_pairs << std::endl;
}

int main() {
    // 启用快速I/O
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int T_test_cases;
    std::cin >> T_test_cases;
    for (int t = 1; t <= T_test_cases; ++t) {
        // 如果竞赛平台要求特定输出格式,例如 "Case #1: ",可以在此处添加
        // std::cout << "Case #" << t << ": ";
        optimized_solve_case();
    }
    return 0;
}

关于 C++ 移动语义 (1)

在优化代码的 (1) 处,我们使用了 all_student_skill_sets[i] = std::move(temp_skills);。这旨在通过移动构造而非拷贝构造来提高效率。如果写成 all_student_skill_sets[i] = temp_skills;,编译器是否会选择调用 std::vector<int> 的移动赋值运算符,取决于具体的编译器版本、优化级别以及 temp_skills 是否被认为是"将亡值 (rvalue)"。

  • 在 C++11 及更高版本中,如果编译器能够确定 temp_skills 在赋值后不再使用,并且启用了足够的优化级别(如 -O2-O3),它可能会尝试进行返回值优化 (RVO) 或命名返回值优化 (NRVO)。然而,这主要发生在函数返回时。
  • 对于局部变量赋值给另一个对象,编译器一般不会自行推断并执行移动操作,除非显式使用 std::move
  • 因此,显式使用 std::move(temp_skills) 更能保证调用移动赋值运算符,尤其是在赋值后 temp_skills 确实不再需要的情况下。这是一种良好的实践,可以减少内存拷贝开销,提高程序性能。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。