2 条题解

  • 0
    @ 2025-12-2 12:31:05

    “std::map<string, int>(基于红黑树,有序)或 std::unordered_map<string, int>(基于哈希表,无序)是最佳选择” 请说明这两个的区别?

    你好,我是阿西莫夫。

    这是一个非常棒的计算机科学基础问题。选择 map 还是 unordered_map,往往决定了你的程序是“优雅地慢”还是“狂野地快”。

    简单来说:std::map 是一个一丝不苟的图书管理员,而 std::unordered_map 是一个拥有瞬间移动魔法的快递员

    下面我从原理、性能、特性三个维度为你详细拆解它们的区别:

    1. 核心区别一览表

    特性 std::map (有序映射) std::unordered_map (无序映射)
    底层实现 红黑树 (Red-Black Tree) 哈希表 (Hash Table)
    内部顺序 有序 (按 Key 的大小自动排序) 无序 (杂乱无章,看似随机)
    查找速度 O(logN)O(\log N) (稳定,较快) O(1)O(1) (平均极快,最坏 O(N)O(N))
    插入/删除 O(logN)O(\log N) O(1)O(1) (平均)
    内存占用 较高 (每个节点存父/子指针和颜色) 更高 (需预分配桶数组,利用率低)
    Key的要求 需要定义 < 运算符 (可比较) 需要定义 hash 函数和 == 运算符
    引入标准 C++98 (老牌经典) C++11 (现代标准)

    2. 深度原理解析

    🤖 std::map:严谨的红黑树

    想象你在查一本纸质字典

    • 原理:字典里的单词是按 A-Z 严格排序的。
    • 查找:你想找 "Science",你不会从第一页翻到最后一页。你会先翻到中间,看是 "M",知道 "S" 在后面,再往后翻一半……这就是二分查找的逻辑。
    • 特点:无论数据怎么插入,它永远保持有序。
    • 优点
      • 有序性:如果你需要按顺序输出所有汉字(如“按拼音排序”),map 是唯一选择。
      • 稳定性:查询时间非常稳定,永远是 logN\log N。不会因为运气不好而变慢。

    ⚡ std::unordered_map:魔法的哈希表

    想象你有 1000 个标了号的魔法桶

    • 原理:你有一个数学公式(哈希函数)。给它一个词 "Science",公式算出 886。你就直接把数据扔进 886 号桶里。
    • 查找:下次你要找 "Science",公式一算还是 886,你直接把手伸进 886 号桶就能拿出来。不需要翻找,一步到位。
    • 特点:桶里数据的物理位置和字母顺序无关,完全取决于那个数学公式。
    • 缺点(哈希冲突)
      • 万一 "Science" 算出 886,"Math" 也算出 886 怎么办?它们会挤在同一个桶里,排成一条小链表。
      • 最坏情况:如果运气极差,所有词都算出了 886,哈希表就退化成了链表,查询速度变成最慢的 O(N)O(N)。但在 OI 竞赛中,除非出题人刻意构造数据攻击(卡哈希),一般情况都非常快。

    3. 代码直观对比

    为了让你看到“有序”和“无序”的直接区别,请看这段代码:

    #include <iostream>
    #include <map>
    #include <unordered_map>
    #include <string>
    
    using namespace std;
    
    int main() {
        // 1. 使用 std::map (红黑树)
        map<string, int> ordered;
        ordered["Zeus"] = 1;
        ordered["Apollo"] = 2;
        ordered["Athena"] = 3;
    
        cout << "--- std::map (自动排序) ---" << endl;
        for (auto const& [key, val] : ordered) {
            cout << key << ": " << val << endl;
        }
    
        // 2. 使用 std::unordered_map (哈希表)
        unordered_map<string, int> unordered;
        unordered["Zeus"] = 1;
        unordered["Apollo"] = 2;
        unordered["Athena"] = 3;
    
        cout << "\n--- std::unordered_map (乱序) ---" << endl;
        for (auto const& [key, val] : unordered) {
            cout << key << ": " << val << endl;
        }
    
        return 0;
    }
    

    运行结果可能如下:

    --- std::map (自动排序) ---
    Apollo: 2    <-- A 开头排第一
    Athena: 3
    Zeus: 1      <-- Z 开头排最后
    
    --- std::unordered_map (乱序) ---
    Zeus: 1      <-- 顺序完全不可预测
    Athena: 3
    Apollo: 2
    

    4. 什么时候用哪个?(做题经验)

    作为一名正在准备信息学竞赛的学生,你应该遵循以下**“阿西莫夫法则”**:

    1. 首选 unordered_map
      • 如果题目只要求你**“统计次数”“判断是否存在”**(如刚才的“虚词账本”题、或者“两数之和”)。
      • 因为它的速度通常比 map 快 2-3 倍,能帮你避免 TLE(超时)。
    2. 必须用 map 的情况
      • 如果题目要求**“按字典序输出结果”**。
      • 如果题目需要用到**“二分查找”相关功能**(如 lower_bound / upper_bound,比如查找“第一个大于等于某值的键”)。
      • 如果你的键(Key)是 pair<int, int>vector<int> 这种复杂类型,且你不想自己手写复杂的 Hash 函数(map 自带对 pair 的比较,直接能用)。

    总结

    • map:优雅、有序、稳定,但慢一点。
    • unordered_map:暴力、杂乱、极快,但有被“卡”的风险。

    在“虚词账本”这道题中,因为我们只关心“字出现了几次”,而不关心“字按拼音怎么排”,所以 unordered_map 是理论上的最佳选择

    • 0
      @ 2025-12-2 12:17:29

      C++ 标准解答 (NOIP C++14)

      这份代码使用了 std::unordered_map,这是 C++11 引入的标准哈希表,查询效率理论上为 O(1)O(1)

      /**
       * 题目: 书童的“虚词账本” (Word Frequency Counter)
       * 作者: Isaac Asimov (AI)
       * 知识点: std::unordered_map 的使用 (Hash Map)
       */
      
      #include <iostream>
      #include <string>
      #include <unordered_map> // 引入哈希表头文件 (C++11)
      // 如果编译器较老不支持 C++11,可以使用 <map>,用法完全一样,只是底层是树
      
      using namespace std;
      
      int main() {
          // 1. IO 优化
          ios_base::sync_with_stdio(false);
          cin.tie(NULL);
      
          int n;
          if (!(cin >> n)) return 0;
      
          // 2. 定义哈希表 (账本)
          // Key (键): string -> 汉字
          // Value (值): int -> 出现的次数
          unordered_map<string, int> word_counts;
      
          // 3. 读取文章并统计
          for (int i = 0; i < n; i++) {
              string word;
              cin >> word;
              
              // map 的神奇操作符 []: 
              // 如果 word 存在,返回对应的 value 的引用
              // 如果 word 不存在,自动插入 {word, 0} 并返回 value 的引用
              word_counts[word]++;
          }
      
          // 4. 处理查询
          int m;
          cin >> m;
          for (int i = 0; i < m; i++) {
              string query;
              cin >> query;
              
              // 直接输出 map 中记录的次数
              // 如果 query 从未出现过,word_counts[query] 会自动创建并返回 0
              // (注:在某些严格场景下,若不想污染 map,应用 .find() 或 .count(),
              // 但本题只求输出,直接用 [] 最简洁)
              cout << word_counts[query] << endl;
          }
      
          return 0;
      }
      

      数据生成器 (Generator)

      生成器会从一个预定义的“文言文常用字库”中随机抽取字符生成文章,并生成包含“存在”和“不存在”的查询。

      请保存为 gen_hashmap.cpp 并运行。

      /**
       * Project: The Scholar's Particle Counter (HashMap Data Generator)
       * Author: Isaac Asimov (AI)
       */
      
      #include <iostream>
      #include <fstream>
      #include <random>
      #include <string>
      #include <vector>
      #include <unordered_map>
      
      using namespace std;
      
      // ==========================================
      // Part 1: 标准解答逻辑
      // ==========================================
      class Solution {
      public:
          void solve(string in_file, string out_file) {
              ifstream cin(in_file);
              ofstream cout(out_file);
              if (!cin.is_open()) return;
      
              int n;
              cin >> n;
              unordered_map<string, int> counts;
              for(int i=0; i<n; i++) {
                  string s; cin >> s;
                  counts[s]++;
              }
              
              int m;
              cin >> m;
              for(int i=0; i<m; i++) {
                  string s; cin >> s;
                  cout << counts[s] << endl;
              }
              
              cin.close();
              cout.close();
              cout << "Generated: " << out_file << endl;
          }
      };
      
      // ==========================================
      // Part 2: 数据生成逻辑
      // ==========================================
      mt19937 rng(2025);
      
      int rand_int(int min, int max) {
          uniform_int_distribution<int> dist(min, max);
          return dist(rng);
      }
      
      // 文言文常用词库
      vector<string> dictionary = {
          "之", "乎", "者", "也", "矣", "焉", "哉", 
          "曰", "云", "子", "君", "臣", "民", "国",
          "仁", "义", "礼", "智", "信",
          "上", "下", "古", "今", "山", "川",
          "遂", "乃", "复", "安", "耳", "耶"
      };
      
      void generate_input(int case_id) {
          string filename = to_string(case_id) + ".in";
          ofstream fout(filename);
      
          int n, m;
      
          if (case_id <= 3) {
              n = 20; m = 5;
          } else if (case_id <= 7) {
              n = 1000; m = 100;
          } else {
              // 压力测试: 10万数据量
              n = 100000; m = 100000;
          }
      
          fout << n << endl;
      
          // 生成文章
          // 为了制造大量重复(让计数有意义),我们只从较小的字典中采样
          // 同时也生成一些随机字符串作为“生僻字”
          vector<string> article_words;
          for (int i = 0; i < n; i++) {
              string word;
              if (rand_int(1, 100) <= 80) {
                  // 80% 概率使用常用虚词
                  word = dictionary[rand_int(0, dictionary.size() - 1)];
              } else {
                  // 20% 概率生成随机生僻字 (用字母模拟,便于处理)
                  // 实际汉字是多字节,string 都能处理,这里为了生成器简单用字母代替部分生僻词
                  // 题目描述中说了是字符串,所以混用汉字和字母是合法的
                  word = "Word" + to_string(rand_int(1, 100));
              }
              fout << word << (i == n-1 ? "" : " ");
              article_words.push_back(word);
          }
          fout << endl;
      
          fout << m << endl;
          
          // 生成查询
          for (int i = 0; i < m; i++) {
              string query;
              int dice = rand_int(1, 100);
              if (dice <= 60) {
                  // 60% 查询文章中存在的词 (从字典里选)
                  query = dictionary[rand_int(0, dictionary.size() - 1)];
              } else if (dice <= 90) {
                  // 30% 查询肯定不存在的词
                  query = "MobilePhone"; 
              } else {
                   // 10% 查询随机生成的生僻字
                   query = "Word" + to_string(rand_int(1, 100));
              }
              fout << query << endl;
          }
      
          fout.close();
      }
      
      int main() {
          ios_base::sync_with_stdio(false);
          cout << "--- Generating HashMap Data ---" << endl;
          
          Solution solver;
          for (int i = 1; i <= 10; i++) {
              generate_input(i);
              string in = to_string(i) + ".in";
              string out = to_string(i) + ".out";
              solver.solve(in, out);
          }
          cout << "--- Done! ---" << endl;
          return 0;
      }
      

      阿西莫夫的点评

      1. 汉字与 String: 对于 C++ 的 std::string 来说,一个汉字(UTF-8 编码下占 3 字节)和一个英文字母并没有本质区别,它们都只是一串 char 数据。map 可以完美处理它们。这是对**“数据抽象”**概念的最好演示。
      2. 文言文的韵律: 通过统计“之乎者也”的频率,我们其实是在做最原始的自然语言处理(NLP)。你会发现,越是经典的古文,这些虚词的分布越有规律。这道题不仅考了算法,还稍微触碰了一下计算语言学的大门。
      • 1

      信息

      ID
      19249
      时间
      1000ms
      内存
      32MiB
      难度
      10
      标签
      (无)
      递交数
      1
      已通过
      1
      上传者