3 条题解

  • 1
    @ 2026-1-17 22:35:51

    C++ STL unordered_map 方法和用法指南

    概述

    unordered_map 是 C++ STL 中的哈希表实现,提供平均 O(1) 的查找、插入和删除性能。


    主要方法

    1. 构造和析构

    #include <unordered_map>
    
    unordered_map<int, string> map1;                    // 默认构造
    unordered_map<int, string> map2(map1);              // 拷贝构造
    unordered_map<int, string> map3 = {{1, "a"}, {2, "b"}};  // 初始化列表构造
    

    2. 元素访问

    unordered_map<int, string> map;
    map[1] = "apple";
    map[2] = "banana";
    
    // at() - 访问,键不存在时抛出 out_of_range 异常
    string val = map.at(1);  // "apple"
    // string val2 = map.at(3);  // 抛出异常
    
    // operator[] - 访问或插入
    string val = map[1];     // "apple"
    map[3] = "cherry";       // 如果键不存在,则插入默认值后赋值
    

    3. 查找元素

    unordered_map<int, string> map = {{1, "a"}, {2, "b"}};
    
    // find() - 返回迭代器
    auto it = map.find(1);
    if (it != map.end()) {
        cout << it->first << ": " << it->second << endl;  // 1: a
    } else {
        cout << "Not found" << endl;
    }
    
    // count() - 返回 0 或 1(unordered_map 中无重复键)
    if (map.count(2) > 0) {
        cout << "Key 2 exists" << endl;
    }
    
    // contains() - C++20 标准
    if (map.contains(1)) {
        cout << "Key 1 exists" << endl;
    }
    

    4. 插入元素

    unordered_map<int, string> map;
    
    // insert() - 插入单个键值对
    map.insert({1, "apple"});
    map.insert(make_pair(2, "banana"));
    
    // insert() - 插入范围
    unordered_map<int, string> map2 = {{3, "cherry"}, {4, "date"}};
    map.insert(map2.begin(), map2.end());
    
    // emplace() - 就地构造插入,通常更高效
    map.emplace(5, "elderberry");
    
    // 检查插入是否成功
    auto result = map.insert({1, "new_apple"});
    if (!result.second) {
        cout << "Key 1 already exists, insertion failed" << endl;
    }
    

    operator[] vs insert() 的区别

    特性 operator[] insert()
    键不存在时 创建新项,值初始化为默认值 返回 pair,可判断是否插入成功
    键已存在时 覆盖旧值 不覆盖,保持原值
    返回值 引用(对值的引用) pair<iterator, bool>
    能否判断是否成功
    效率 快(直接访问或插入) 快(但需要检查返回值)
    适用场景 赋值、更新 仅插入新元素、批量插入、判断插入结果

    实际代码对比

    unordered_map<int, string> map;
    
    // 场景 1:想插入,但不想覆盖已有的值
    // 用 operator[] 实现:无法判断是否覆盖
    map[10] = "first";
    map[10] = "second";  // 覆盖了原值,但你无法察觉
    
    // 用 insert() 实现:可以明确知道
    auto result = map.insert({10, "first"});
    if (result.second) {
        cout << "插入成功" << endl;
    } else {
        cout << "键已存在,插入失败,原值为: " << result.first->second << endl;
    }
    
    // 场景 2:想批量插入
    vector<pair<int, string>> data = {{1, "a"}, {2, "b"}, {3, "c"}};
    // operator[] 无法直接批量操作
    // insert() 可以 :
    map.insert(data.begin(), data.end());
    
    // 场景 3:只想访问,不想创建新元素
    int key = 999;
    // 用 operator[] 会创建新元素,改变 map 的大小
    // map[999];  // 这会在 map 中创建 999 这个键
    
    // 用 find() 或 count() :
    if (map.count(999)) {
        cout << map[999] << endl;
    } else {
        cout << "键不存在" << endl;
    }
    

    总结

    • operator[]: 适合做值的赋值和更新,简洁直观
    • insert() / emplace(): 适合仅插入新元素,且需要判断是否插入成功的场景
    • find() / count() / at(): 适合仅查询,不想修改 map 的场景

    5. 删除元素

    unordered_map<int, string> map = {{1, "a"}, {2, "b"}, {3, "c"}};
    
    // erase() - 按键删除
    map.erase(1);
    
    // erase() - 按迭代器删除
    auto it = map.find(2);
    if (it != map.end()) {
        map.erase(it);
    }
    
    // erase() - 删除范围
    auto it1 = map.find(1);
    auto it2 = map.find(3);
    map.erase(it1, it2);
    
    // clear() - 清空所有元素
    map.clear();
    

    6. 容量和大小

    unordered_map<int, string> map = {{1, "a"}, {2, "b"}, {3, "c"}};
    
    // size() - 返回元素个数
    cout << map.size() << endl;  // 3
    
    // empty() - 检查是否为空
    if (map.empty()) {
        cout << "Map is empty" << endl;
    } else {
        cout << "Map is not empty" << endl;
    }
    
    // max_size() - 返回理论最大元素个数
    cout << map.max_size() << endl;
    

    7. 迭代器

    unordered_map<int, string> map = {{1, "a"}, {2, "b"}, {3, "c"}};
    
    // begin() 和 end()
    for (auto it = map.begin(); it != map.end(); ++it) {
        cout << it->first << ": " << it->second << endl;
    }
    
    // 范围 for 循环(推荐)- C++17 结构化绑定
    for (auto& [key, value] : map) {
        cout << key << ": " << value << endl;
    }
    
    // C++14 及以下兼容写法
    for (auto& p : map) {
        cout << p.first << ": " << p.second << endl;
    }
    
    // cbegin() 和 cend() - 常量迭代器
    for (auto it = map.cbegin(); it != map.cend(); ++it) {
        cout << it->first << ": " << it->second << endl;
    }
    

    C++14 兼容性说明

    CSP/NOI比赛限制只能用C++14标准

    写法 C++14 C++17+ 说明
    for (auto it = map.begin(); ...) 传统迭代器遍历,所有版本都支持
    for (auto& p : map) 范围 for 循环,C++11 起支持
    for (auto& [key, value] : map) 结构化绑定,需要 C++17

    不同 C++ 版本的写法对比

    unordered_map<int, string> map = {{1, "a"}, {2, "b"}};
    
    // C++14 及以下 - 推荐写法
    for (auto& p : map) {
        int key = p.first;
        string value = p.second;
        cout << key << ": " << value << endl;
    }
    
    // C++14 - 另一种写法(不推荐,冗长)
    for (const auto& pair : map) {
        cout << pair.first << ": " << pair.second << endl;
    }
    
    // C++17+ - 最简洁写法(使用结构化绑定)
    for (auto& [key, value] : map) {
        cout << key << ": " << value << endl;
    }
    
    // C++11 - 传统迭代器写法
    for (auto it = map.begin(); it != map.end(); ++it) {
        cout << it->first << ": " << it->second << endl;
    }
    

    8. 桶相关操作

    unordered_map<int, string> map = {{1, "a"}, {2, "b"}};
    
    // bucket_count() - 返回桶的数量
    cout << "Bucket count: " << map.bucket_count() << endl;
    
    // load_factor() - 返回负载因子 (元素数 / 桶数)
    cout << "Load factor: " << map.load_factor() << endl;
    
    // max_load_factor() - 获取或设置最大负载因子
    cout << "Max load factor: " << map.max_load_factor() << endl;
    map.max_load_factor(0.75);
    
    // rehash() - 重新哈希,指定桶的数量
    map.rehash(100);
    

    完整示例

    #include <iostream>
    #include <unordered_map>
    using namespace std;
    
    int main() {
        unordered_map<int, string> students;
        
        // 添加元素
        students[1001] = "Alice";
        students[1002] = "Bob";
        students.emplace(1003, "Charlie");
        
        // 遍历
        cout << "All students:" << endl;
        for (auto& [id, name] : students) {
            cout << "ID: " << id << ", Name: " << name << endl;
        }
        
        // 查找
        if (students.count(1002)) {
            cout << "Student 1002: " << students[1002] << endl;
        }
        
        // 更新
        students[1001] = "Alice (updated)";
        
        // 删除
        students.erase(1003);
        
        cout << "Remaining students: " << students.size() << endl;
        
        return 0;
    }
    

    输出:

    All students:
    ID: 1003, Name: Charlie
    ID: 1001, Name: Alice
    ID: 1002, Name: Bob
    Student 1002: Bob
    Remaining students: 2
    

    常见用途

    用途 代码
    频率统计 unordered_map<char, int> freq; freq[ch]++;
    键值映射 unordered_map<string, int> mapping; mapping["age"] = 25;
    缓存 if (cache.count(key)) return cache[key];
    去重 unordered_map<int, bool> seen; if (!seen[x]) seen[x] = true;

    频率:数学统计术语,指某个值出现的次数

    • 0
      @ 2025-12-24 17:23:56

      作为教练,制作数据最重要的一点是确保唯一性覆盖极端边界

      在“两数之和”中,虽然逻辑简单,但数据生成需要特别注意:

      1. 答案的唯一性:随机生成大数据时,极低概率会出现第二组解。生成后必须用标程验证。
      2. 大整数处理targettarget 可能达到 2×1092 \times 10^9,必须使用 long long 逻辑处理。
      3. 相同数值的陷阱:例如 target=6target=6,数组中可能出现多个 3,或者只有一个 3(不能自加)。

      以下是为你准备的自动化数据生成器。它集成了 O(n)O(n) 标程逻辑,会自动生成符合 NOI 规范的 1.in10.out

      一、 数据生成器代码 (C++14 标准)

      #include <iostream>
      #include <fstream>
      #include <vector>
      #include <string>
      #include <unordered_map>
      #include <random>
      #include <ctime>
      #include <algorithm>
      #include <set>
      
      using namespace std;
      
      typedef long long ll;
      
      // ================= 标准标程逻辑 (用于生成 .out) =================
      pair<int, int> solve_standard(int n, ll target, const vector<int>& nums) {
          unordered_map<ll, int> dic;
          for (int i = 0; i < n; ++i) {
              ll complement = target - nums[i];
              if (dic.count(complement)) {
                  return {dic[complement], i};
              }
              dic[nums[i]] = i;
          }
          return {-1, -1};
      }
      
      // ================= 数据构造逻辑 =================
      void write_test_case(int id, int n, ll target, vector<int>& nums) {
          string in_name = to_string(id) + ".in";
          string out_name = to_string(id) + ".out";
      
          // 写入输入文件
          ofstream fout(in_name);
          fout << n << " " << target << "\n";
          for (int i = 0; i < n; ++i) {
              fout << nums[i] << (i == n - 1 ? "" : " ");
          }
          fout << endl;
          fout.close();
      
          // 计算并写入标准输出
          pair<int, int> res = solve_standard(n, target, nums);
          ofstream fans(out_name);
          fans << res.first << " " << res.second << endl;
          fans.close();
      
          cout << "Testcase " << id << " generated. (n=" << n << ")" << endl;
      }
      
      int main() {
          mt19937 rng(time(0));
          // 随机值范围:-10^9 到 10^9
          uniform_int_distribution<int> val_dist(-1000000000, 1000000000);
      
          for (int i = 1; i <= 10; ++i) {
              int n;
              ll target;
              vector<int> nums;
      
              if (i == 1) { // 样例
                  n = 4; target = 9;
                  nums = {2, 7, 11, 15};
              } 
              else if (i == 2) { // 最小规模
                  n = 2; target = 100;
                  nums = {40, 60};
              }
              else if (i == 3) { // 目标值由两个相同数字组成
                  n = 1000; target = 200;
                  nums.assign(n, 555); // 填充干扰
                  int idx1 = rng() % n;
                  int idx2 = rng() % n;
                  while(idx1 == idx2) idx2 = rng() % n;
                  nums[idx1] = 100; nums[idx2] = 100;
              }
              else if (i == 4) { // 包含负数
                  n = 5000; target = -500;
                  for(int j=0; j<n; j++) nums.push_back(val_dist(rng));
                  int idx1 = 0, idx2 = n-1;
                  nums[idx1] = -200; nums[idx2] = -300;
              }
              else if (i <= 6) { // 大规模随机
                  n = 10000; target = val_dist(rng);
                  set<int> used;
                  for(int j=0; j<n; j++) {
                      int v = val_dist(rng);
                      nums.push_back(v);
                  }
                  // 强制构造一个解
                  int idx1 = rng() % n, idx2 = rng() % n;
                  while(idx1 == idx2) idx2 = rng() % n;
                  nums[idx1] = 1000000; 
                  nums[idx2] = (int)(target - 1000000);
              }
              else { // 极限边界:答案在头尾
                  n = 10000; target = 123456789;
                  for(int j=0; j<n; j++) nums.push_back(val_dist(rng));
                  nums[0] = 100000000;
                  nums[n-1] = 23456789;
              }
              
              // 简单打乱,防止答案位置过于固定(除了 Case 10 这种特意测试头尾的)
              if(i != 1 && i != 10) {
                  // 注意:打乱前需记录构造好的值,这里我们采用直接在随机位置插入
              }
      
              write_test_case(i, n, target, nums);
          }
          return 0;
      }
      

      二、 测试点设计思路(创建 OJ 必读)

      针对“两数之和”,我设计的 10 组测试点涵盖了以下维度:

      测试点 数据特征 考察重点
      1 官方样例 确保基础逻辑符合题目示例。
      2 n=2n=2 最小边界。检查数组下标越界和基础读取。
      3 target=2xtarget = 2x 高频易错点:数组中有两个相同的数(如 3+3=63+3=6)。考察哈希表是否能正确存取同一数值的不同下标。
      4 负数数据 考察对负数运算和负数键值在哈希表中存储的稳健性。
      5-6 大规模随机 压力测试:n=104n=10^4,考察 O(n)O(n) 时间复杂度的实现。
      7-8 大数值 targettarget 考察是否使用了 long long 接收 targettarget 及其减法运算。
      9 答案位于中间 考察哈希表在中间匹配时的即时退出。
      10 答案位于首尾 考察完整扫描数组的能力。

      三、 考场避坑建议(教练寄语)

      1. 哈希表的选择: 在 C++14 中,请务必使用 unordered_map 而不是 map

        • unordered_map 是哈希表,O(1)O(1)
        • map 是红黑树,O(logn)O(\log n)。在 10510^5 数据量下,两者的常数差异会决定是否超时。
      2. 同一元素不能重复使用: 代码逻辑中,一定要find(查找),再 insert(入表)

        • 错误逻辑:先把所有数放进表。如果 target=6target=6,数组里只有一个 3,程序会找到自己并返回 (0, 0)
        • 正确逻辑:遍历到当前数时,去表里找它的“另一半”。没找到,再把自己放进去。
      3. 非递归安全: 本题为线性扫描,不涉及递归,无爆栈风险。生成器生成的 nn 严格控制在 10410^4 以内,标程运行速度极快。

      4. 无除零风险: 本题仅涉及加减法,完全避开了除法运算,生成的 nn 保证 2\ge 2

      你可以编译并运行此生成器,它会自动在当前文件夹下输出所有的 .in.out 文件。祝你的学生在练习中更进一步!

      • 0
        @ 2025-12-24 17:19:12

        你好,同学。作为教练,我为你整理了这道题的 NOIP 竞赛级标准程序。

        在 NOI 竞赛中,追求的不仅是“能跑通”,更是“在大数据量下的稳健与高效”。虽然本题规模 10410^4 暴力可过,但我们必须掌握 O(n)O(n) 的最优解法,以应对未来可能出现的 10510^5 甚至 10610^6 级别的数据。


        一、 两数之和 标准题解 (C++14)

        #include <iostream>
        #include <vector>
        #include <unordered_map> // NOI 竞赛中常用哈希表
        
        using namespace std;
        
        /**
         * 核心逻辑:一遍哈希扫描法
         * 1. 扫描到一个数 x 时,我们目标明确:寻找 target - x。
         * 2. 如果哈希表里已经存了 target - x,说明匹配成功。
         * 3. 如果没存,就把 x 和它的下标存进去,给后面的数“当另一半”。
         */
        
        void solve() {
            int n;
            long long target; // 使用 long long 防止 target 计算溢出
            if (!(cin >> n >> target)) return;
        
            vector<int> nums(n);
            // unordered_map 在 C++11/14 中是基于哈希表实现的,查找期望时间为 O(1)
            // key: 数值, value: 对应的下标
            unordered_map<int, int> dic;
        
            for (int i = 0; i < n; ++i) {
                cin >> nums[i];
            }
        
            for (int i = 0; i < n; ++i) {
                int complement = target - nums[i];
        
                // 易错点:必须在将当前数放入哈希表之前查找
                // 这样可以自动避免“同一个元素重复出现”的问题
                if (dic.count(complement)) {
                    // 找到了!输出存下的下标和当前下标
                    cout << dic[complement] << " " << i << endl;
                    return; // 题目保证只有一个答案,直接结束
                }
        
                // 如果没找到,将当前数值及其下标存入哈希表
                dic[nums[i]] = i;
            }
        }
        
        int main() {
            // NOI 竞赛必备优化:加速 I/O。在大规模数据输入时,比 scanf 还要快。
            ios::sync_with_stdio(false);
            cin.tie(0);
        
            solve();
        
            return 0;
        }
        

        二、 复杂度分析思考过程

        1. 时间复杂度:O(n)O(n)

        • 推导过程
          • 我们只对数组进行了一次线性遍历(for 循环 nn 次)。
          • 在循环内部,unordered_map 的查找(count)和插入(operator[])操作在平均情况下的时间复杂度均为 O(1)O(1)
        • 结论:总时间复杂度为 O(n)O(n)。在 n=104n=10^4 甚至 10510^5 时,该程序都能在数毫秒内完成。

        2. 空间复杂度:O(n)O(n)

        • 推导过程
          • 我们需要一个哈希表来存储遍历过的元素。在最坏情况下(即最后两个数才匹配成功),哈希表会存储 n1n-1 个元素。
        • 结论:额外空间复杂度为 O(n)O(n)。在 NOI 256MB 的内存限制下,存储 10510^5int 仅占用约 2-4MB,非常安全。

        三、 时间复杂度优化建议

        虽然上述代码已经是期望 O(n)O(n) 的最优解,但在 NOI 竞赛中,如果遇到极端的“哈希冲突测试数据”(Anti-Hash Test Cases),unordered_map 的效率可能会退化到 O(n2)O(n^2)。以下是进阶优化建议:

        1. 预分配空间 (Reserve): 如果你知道数组长度 nn,可以调用 dic.reserve(n)。这能减少哈希表动态扩容时的内存分配次数,显著提升速度。

        2. 防御性哈希 (Custom Hash): 为了防止出题人故意构造让哈希冲突的数据,可以手写一个简单的随机扰动函数给哈希表。

        3. 排序 + 双指针方案 (O(nlogn)O(n \log n)): 如果内存限制极其严苛,不允许使用哈希表,可以先对数组进行排序,然后使用左、右两个指针向中间靠拢。

          • 优点:不需要额外哈希空间,且时间复杂度非常稳定。
          • 缺点:排序后原下标会乱,需要使用 pair<int, int> 同时存储数值和原下标。
        4. 快读 (Fast I/O): 如果 nn 达到 10610^6 级别,可以使用 getchar() 手写读取函数。

        教练点评: 这道题是学习**“利用额外空间换取查找速度”**的入门教科书。在考场上,请务必注意 targetnums[i]target - nums[i] 的计算是否会超出 int 范围,以及哈希表容器是否正确使用。加油!

        • 1

        信息

        ID
        19385
        时间
        1000ms
        内存
        128MiB
        难度
        10
        标签
        (无)
        递交数
        5
        已通过
        4
        上传者