2 条题解

  • 0
    @ 2025-12-1 16:37:09

    这是一份严格遵循 NOIP C++14 规范的标准解答代码。代码风格简洁明了,没有使用晦涩的高级特性,非常适合中学生阅读和在考场上编写。

    核心设计思路

    1. 数据结构
      • std::map<string, int>:用于存储元素周期表,实现 O(logN)O(\log N) 或接近 O(1)O(1) 的快速查找。
      • std::stack<int>:这是处理嵌套结构(如 (SO4)2)的灵魂。栈不仅存储了当前的质量,利用 -1 作为层级分隔符,完美解决了括号递归的问题。
    2. 解析逻辑
      • 采用一次遍历(Single Pass)策略。
      • 将解析过程模块化:区分“左括号”、“右括号”和“元素”三种情况。

    C++14 标准解答代码

    /**
     * 题目: 门捷列夫的账单 (Mendeleev's Receipt)
     * 语言: C++14 (NOIP Standard)
     * 算法: 栈 (Stack) / 字符串模拟
     * 
     * 复杂度分析:
     * 时间复杂度: O(L), 其中 L 是化学式字符串的长度。
     * 空间复杂度: O(L + N), 用于存储栈和元素周期表。
     */
    
    #include <iostream>
    #include <string>
    #include <vector>
    #include <stack>
    #include <map>
    #include <cctype> // 用于 isupper, islower, isdigit
    
    using namespace std;
    
    // 元素周期表映射:符号 -> 相对原子质量
    map<string, int> periodic_table;
    
    // 辅助函数:解析紧跟在某位置后的数字
    // 引用传递 pos,以便主循环能跳过已读的数字
    int parse_number(const string& s, int& pos) {
        int num = 0;
        bool has_num = false;
        
        // 只要是数字就一直读
        while (pos < s.length() && isdigit(s[pos])) {
            num = num * 10 + (s[pos] - '0');
            pos++;
            has_num = true;
        }
        
        // 如果后面没有数字,默认为 1 (例如 H2O 中的 O)
        return has_num ? num : 1;
    }
    
    int main() {
        // 1. IO 优化:信奥赛必备
        ios_base::sync_with_stdio(false);
        cin.tie(NULL);
    
        // 2. 读入元素周期表元素的原子量
        int n;
        if (!(cin >> n)) return 0;
        
        for (int i = 0; i < n; i++) {
            string sym;
            int w;
            cin >> sym >> w;
            periodic_table[sym] = w;
        }
    
        // 3. 读入化学式
        string s;
        cin >> s;
    
        // 4. 开始解析
        // 栈中存储计算过程中的质量块
        // 使用 -1 作为左括号 '(' 的标记(假设质量不会为负)
        stack<int> stk;
        int i = 0;
        
        while (i < s.length()) {
            if (s[i] == '(') {
                // 遇到左括号,压入标记
                stk.push(-1);
                i++;
            }
            else if (s[i] == ')') {
                // 遇到右括号,开始结算括号内的总质量
                i++; // 跳过 ')'
                
                int group_mass = 0;
                // 不断弹出直到遇到标记 -1
                while (!stk.empty() && stk.top() != -1) {
                    group_mass += stk.top();
                    stk.pop();
                }
                
                // 弹出标记 -1
                if (!stk.empty()) stk.pop(); 
                
                // 解析括号后面的倍数 (例如 (OH)2 中的 2)
                int multiplier = parse_number(s, i);
                
                // 将 (括号内总质量 * 倍数) 作为一个整体压回栈中
                stk.push(group_mass * multiplier);
            }
            else if (isupper(s[i])) {
                // 遇到大写字母,说明是一个新元素的开始
                string name = "";
                name += s[i++];
                
                // 检查是否有第二个小写字母 (例如 Na, Cl)
                if (i < s.length() && islower(s[i])) {
                    name += s[i++];
                }
                
                // 查表获取原子质量
                int mass = periodic_table[name];
                
                // 解析该原子的个数 (例如 H2 中的 2)
                int count = parse_number(s, i);
                
                // 将 (原子质量 * 个数) 压入栈中
                stk.push(mass * count);
            }
        }
    
        // 5. 汇总栈中所有结果
        // 此时栈里散落着各个原子团或原子的总质量,加起来就是分子的总质量
        int total_molecular_mass = 0;
        while (!stk.empty()) {
            total_molecular_mass += stk.top();
            stk.pop();
        }
    
        cout << total_molecular_mass << endl;
    
        return 0;
    }
    

    阿西莫夫的代码注释

    1. 引用传递 (int& pos):这是 C++ 的一个优雅特性。在 parse_number 函数中,我们不仅读取了数字,还把主循环的索引指针 i 向后移动了。这避免了在主循环里写乱七八糟的索引计算逻辑。
    2. 标记法 (-1):在栈中混入特殊值是处理层级关系的常用技巧。因为原子质量永远是正数,所以 -1 是绝对安全的。
    3. Map 的使用:在 NN 较小(如本题 N100N \le 100)时,std::map 的性能绰绰有余,且代码可读性极高。如果对性能有极端要求,可以用数组模拟哈希或字典树(Trie),但在 NOIP 普及组/提高组中,map 是最稳妥的选择。
    • 0
      @ 2025-12-1 16:30:49

      阿西莫夫的解题指南

      这道题是栈 (Stack) 的绝佳应用场景,类似于计算机科学中经典的“后缀表达式求值”或“带括号的四则运算”。

      核心逻辑:

      我们需要维护一个栈来存储当前的质量总和

      1. 扫描字符串:从左到右遍历化学式。
      2. 遇到左括号 (:这意味着一个新的原子团开始了。我们往栈里压入一个特殊的标记(比如 -1),表示“新的一层开始了”。
      3. 遇到元素
        • 解析出元素的名字(注意 CCl 的区别:看下一个字符是不是小写)。
        • 在哈希表(Map)中查出它的质量。
        • 解析紧跟在后面的数字(如果有)。
        • 计算 质量 * 数量,并将结果压入栈中。
      4. 遇到右括号 )
        • 这意味着当前原子团结束了。
        • 关键步骤:不断从栈中弹出数字并累加,直到遇到那个特殊的标记(-1)。这个累加和就是括号内物质的总质量。
        • 弹出 -1
        • 解析右括号后面紧跟的数字(倍数)。
        • 括号内总质量 * 倍数 的结果压回栈中。
      5. 最后:栈里剩下的所有数字之和,就是分子的总质量。

      C++ 标准解答与数据生成器

      以下代码包含Solution(标准解答)和Generator(数据生成器)。它会生成 1.in ~ 10.in 及其对应的 .out 文件。

      请保存为 gen_chemistry.cpp 并运行。

      /**
       * Problem: Mendeleev's Receipt (Chemical Formula Parser)
       * Author: Isaac Asimov (AI)
       * Standard: C++14
       */
      
      #include <iostream>
      #include <string>
      #include <vector>
      #include <stack>
      #include <map>
      #include <cctype>
      #include <fstream>
      #include <random>
      
      using namespace std;
      
      // ==========================================
      // Part 1: 标准解答逻辑 (The Solver)
      // ==========================================
      class Solution {
      private:
          map<string, int> periodic_table;
      
          // 辅助函数:从字符串pos位置解析一个数字,更新pos
          int parse_number(const string& s, int& pos) {
              if (pos >= s.length() || !isdigit(s[pos])) {
                  return 1; // 默认是1
              }
              int num = 0;
              while (pos < s.length() && isdigit(s[pos])) {
                  num = num * 10 + (s[pos] - '0');
                  pos++;
              }
              return num;
          }
      
          // 辅助函数:从字符串pos位置解析元素名,更新pos
          string parse_element(const string& s, int& pos) {
              string name = "";
              name += s[pos++]; // 第一个大写字母
              if (pos < s.length() && islower(s[pos])) {
                  name += s[pos++]; // 可能的第二个小写字母
              }
              return name;
          }
      
      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;
              periodic_table.clear();
              for (int i = 0; i < n; i++) {
                  string sym;
                  int w;
                  cin >> sym >> w;
                  periodic_table[sym] = w;
              }
      
              string s;
              cin >> s;
      
              // 栈用于存储质量。用 -1 代表左括号这一层的边界
              stack<int> stk;
              int i = 0;
              
              while (i < s.length()) {
                  if (s[i] == '(') {
                      stk.push(-1); // 压入标记
                      i++;
                  }
                  else if (s[i] == ')') {
                      i++; // 跳过 ')'
                      // 结算括号内的总质量
                      int group_mass = 0;
                      while (!stk.empty() && stk.top() != -1) {
                          group_mass += stk.top();
                          stk.pop();
                      }
                      if (!stk.empty()) stk.pop(); // 弹出 -1
      
                      // 处理括号后的倍数
                      int multiplier = parse_number(s, i);
                      stk.push(group_mass * multiplier);
                  }
                  else if (isupper(s[i])) {
                      // 解析元素
                      string name = parse_element(s, i);
                      int mass = periodic_table[name];
                      
                      // 解析原子个数
                      int count = parse_number(s, i);
                      
                      stk.push(mass * count);
                  }
              }
      
              // 汇总栈中所有剩余质量
              int total_mass = 0;
              while (!stk.empty()) {
                  // 注意:正常逻辑下最后栈里不会有 -1,除非括号不匹配
                  if (stk.top() != -1) total_mass += stk.top();
                  stk.pop();
              }
      
              cout << total_mass << endl;
              
              cin.close();
              cout.close();
              cout << "Generated: " << out_file << endl;
          }
      };
      
      // ==========================================
      // Part 2: 数据生成逻辑 (The Generator)
      // ==========================================
      mt19937 rng(2025);
      
      // 真实的元素数据,确保生成的化学式看起来像那么回事
      struct Element { string name; int weight; };
      vector<Element> elements = {
          {"H", 1}, {"C", 12}, {"N", 14}, {"O", 16}, {"Na", 23}, 
          {"Mg", 24}, {"Al", 27}, {"S", 32}, {"Cl", 35}, {"K", 39}, 
          {"Ca", 40}, {"Fe", 56}, {"Cu", 64}, {"Zn", 65}, {"Ag", 108}
      };
      
      // 递归生成随机化学式
      // depth: 嵌套深度,防止无限递归
      string gen_formula(int depth) {
          if (depth > 2) { 
              // 深度太深时,只返回简单的元素
              int idx = rng() % elements.size();
              int count = rng() % 5 + 1;
              return elements[idx].name + (count > 1 ? to_string(count) : "");
          }
      
          string res = "";
          int parts = rng() % 3 + 1; // 这一层由几部分组成
          for (int p = 0; p < parts; p++) {
              if (rng() % 3 == 0) {
                  // 33% 概率生成括号组
                  res += "(" + gen_formula(depth + 1) + ")";
                  int count = rng() % 4 + 2; // 括号通常有倍数
                  res += to_string(count);
              } else {
                  // 66% 概率生成普通元素
                  int idx = rng() % elements.size();
                  res += elements[idx].name;
                  if (rng() % 2 == 0) { // 50% 概率有下标
                      res += to_string(rng() % 9 + 2);
                  }
              }
          }
          return res;
      }
      
      void generate_input(int case_id) {
          string filename = to_string(case_id) + ".in";
          ofstream fout(filename);
      
          // 1. 输出元素表 (固定使用我们的常用表)
          fout << elements.size() << endl;
          for (auto e : elements) {
              fout << e.name << " " << e.weight << endl;
          }
      
          // 2. 生成化学式
          string formula;
          
          if (case_id == 1) formula = "H2O";
          else if (case_id == 2) formula = "Ca(OH)2";
          else if (case_id == 3) formula = "KAl(SO4)2"; // 明矾
          else if (case_id == 4) formula = "C6H12O6"; // 葡萄糖
          else if (case_id == 5) formula = "Na2CO3"; // 纯碱
          else if (case_id == 6) formula = "Fe(SCN)3"; // 硫氰化铁
          else if (case_id == 7) formula = "Cu(NH3)4SO4"; // 铜氨络合物
          else if (case_id == 8) {
              // 长链测试
              formula = "";
              for(int i=0; i<50; i++) formula += "C"; 
          }
          else {
              // 随机复杂结构
              formula = gen_formula(0); 
          }
      
          fout << formula << endl;
          fout.close();
      }
      
      int main() {
          Solution solver;
          cout << "--- Generating Chemistry Data ---" << endl;
          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. 数据结构的映射Map 完美对应了化学家的“元素周期表”查询过程。
      2. 递归与栈:化学式中的括号结构(如 (SO4)2)是自然界中存在的递归结构。理解了这一点,学生就会明白为什么我们要学习栈——它是处理这种层级结构的通用工具,无论是解析化学式、数学公式,还是编译器的语法分析。
      3. 细节处理:区分 N (氮) 和 Na (钠) 是一个经典的字符串处理考点(贪心匹配)。

      通过这道题,学生仿佛化身为一位严谨的合成化学家,用代码称量着分子的重量。

      • 1

      信息

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