NOI C++ 课程体系 / 数学基础篇 / 第2讲
CSP-J 入门级 · 数学基础 · m6-02

初等数学
与 ASCII 码

字符在计算机里其实是数字 —— 掌握数位拆分、字符编码与大小写转换,打通"数学计算"与"文本处理"两大赛道。

6 大模块 128 个 ASCII 字符 3 个经典案例 5 大易错点 11 道分层练习
本讲 · 第 52/56 讲初等数学与 ASCII 码
模块 01

情景导航

用故事建立直觉,用类比理解抽象概念。

故事间谍的密电

二战期间,情报员小王要传送密信:"ATTACK"。敌方电台监听所有明文,于是他使用凯撒密码:每个字母向后移 3 位,A→D、T→W……密文变成 "DWWDFN"。

可计算机怎么帮他把字母"移位"?计算机并不认识字母 A,它认识的只是数字!在内存里,字母 'A' 存的是 65,'B' 存 66……"移位"其实就是加法:'A' + 3 就得到 'D'。

字符即数字:掌握 ASCII 编码,文本处理就变成了数学运算!

本讲我们学习初等数学的编程工具(数位拆分、取余)和 ASCII 编码(字符与数字的相互转换),两者结合能解决一大类竞赛题。

①生活类比:编号无处不在
🎟️

电影票座位号

"5 排 3 座"用两个数字定位一个座位。

→ 数位拆分定位每个数字
📞

电话键盘

按键 2 对应 ABC,每个字母都有编号。

→ 字符与编号的映射
🎂

生日数字

20120601 拆成 2,0,1,2,0,6,0,1 可求数字和。

→ %10 取末位、/10 去末位
🔠

字母表顺序

A 是第 1 个字母,D 是第 4 个。

→ 'D' - 'A' + 1 = 4
②编程定义:两大知识板块

初等数学运算:整除(/)、取余(%)、数位拆分,是所有数字类题目的基本功;ASCII 码:美国信息交换标准代码,给 128 个常用字符各编了一个 0~127 的号码。

📌 本讲核心知识地图

板块核心内容对应大纲
初等数学整除、取余、奇偶判断、数位和数及其运算
数位拆分n%10 取末位、n/10 去末位数及其运算
ASCII 编码字符↔整数、三个核心区段ASCII 码
字符运算大小写转换、数字字符转数值ASCII 码
综合应用大小写转换、字符统计、回文数综合
③核心口诀
🔑 口诀一:数位拆分

取余得末位,整除去末位
循环到为零,数位全到手

🔑 口诀二:ASCII 三区段

数字 48 起步跑,大写 65 来报到
小写 97 才登场,小写大写差 32

🔑 口诀三:字符转换

减 '0' 变数值,加 '0' 变字符
小写减 32 变大写,大写加 32 变小写

模块 02

知识探究

先掌握数学工具,再走进字符的世界。

①初等数学工具箱

C++ 中两个最重要的数学运算符:整除 /(两整数相除舍去小数部分)和 取余 %(取整除后的余数)。它们是处理"数字"问题的瑞士军刀。

需求写法例子
判断奇偶n % 2:0 偶 1 奇7 % 2 = 1 → 奇数
取末位n % 10153 % 10 = 3
去掉末位n / 10153 / 10 = 15
判断能否整除n % k == 012 % 3 == 0 → 能
C++基础运算演示
#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;
    cout << "奇偶: " << (n % 2 == 0 ? "偶数" : "奇数") << endl;
    cout << "末位: " << n % 10 << endl;        // 取余得末位
    cout << "去末位: " << n / 10 << endl;       // 整除去末位
    return 0;
}
②数位拆分

把一个整数的每一位数字"抠"出来,靠的就是%10 和 /10 的组合拳:每次取末位、去末位,循环到 n 变成 0 为止。

C++统计位数与数位和
#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;
    int sum = 0;              // 数位和
    int digits = 0;           // 位数
    while (n > 0) {
        sum += n % 10;        // 取出末位累加
        n /= 10;              // 去掉末位
        digits++;
    }
    cout << "位数: " << digits << endl;
    cout << "数位和: " << sum << endl;
    return 0;
}

手算模拟:n = 153

轮次nn%10(取走)n/10(剩余)
1153315
21551
3110 → 结束

位数 = 3,数位和 = 3+5+1 = 9。注意取出的顺序是从低位到高位(3、5、1)。

③ASCII 编码表

ASCII 码给每个常用字符编了一个 0~127 的号码。char 类型存的就是这个号码:(int)c 可查看,(char)n 可转回。竞赛中最常用的是下面三个连续区段:

区段编码范围特点
控制字符0 ~ 31不可见,如换行 \n = 10
数字字符 '0'~'9'48 ~ 57连续 10 个,'0'=48,'9'=57
大写字母 'A'~'Z'65 ~ 90连续 26 个,'A'=65,'Z'=90
小写字母 'a'~'z'97 ~ 122连续 26 个,'a'=97,'z'=122

三条黄金规律:① 同区段内编码连续('A'=65, 'B'=66, ...);② 排序上 数字 < 大写 < 小写;③ 小写字母比对应大写字母大 32(97 − 65 = 32)。

④字符运算

字符可以直接参与加减运算,结果按编码计算。四个必会套路:数字字符→数值(c−'0')、数值→数字字符(n+'0')、大写→小写(+32)、小写→大写(−32)。

C++字符运算演示
#include <iostream>
using namespace std;

int main() {
    char c = 'a';
    cout << (int)c << endl;          // 97:字符转编码
    cout << c - 32 << endl;          // 65:'a' - 32 即 'A' 的编码
    cout << (char)(c - 32) << endl;  // A:编码再转回字符
    cout << '5' - '0' << endl;       // 5:数字字符转数值
    cout << 'D' - 'A' + 1 << endl;   // 4:D 是第 4 个字母
    return 0;
}
模块 03

实践应用

三个经典案例,覆盖字符处理最常见的题型。

案例1大小写转换(保留空格)

题目:输入一行文字,把其中的小写字母变成大写,其他字符(包括空格)原样输出。

关键坑点:用 cin >> c 读字符会自动跳过空格和换行,空格就丢了!必须用 cin.get(c) 逐字符读取,任何字符都不放过。

C++大小写转换
#include <iostream>
using namespace std;

int main() {
    char c;
    while (cin.get(c)) {              // 逐字符读取,空格换行也会读到
        if (c >= 'a' && c <= 'z')
            cout << (char)(c - 32);   // 小写 → 大写
        else
            cout << c;                // 其他字符原样输出
    }
    return 0;
}

✅ 验证:输入 hello world 123,输出 HELLO WORLD 123——空格保留,数字不受影响('1'~'9' 不在 a~z 区间)。

案例2字符统计

题目:输入一行文字,分别统计其中英文字母、数字字符和其他字符的个数。判断某字符属于哪个区段,就是判断编码落在哪个区间。

C++字符统计
#include <iostream>
#include <string>
using namespace std;

int main() {
    string line;
    getline(cin, line);               // 读入一整行(含空格)
    int letter = 0, digit = 0, other = 0;
    for (int i = 0; i < line.size(); i++) {
        char c = line[i];
        if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
            letter++;
        else if (c >= '0' && c <= '9')
            digit++;
        else
            other++;
    }
    cout << "字母:" << letter << " 数字:" << digit << " 其他:" << other << endl;
    return 0;
}

✅ 验证:输入 abc 123!,输出 字母:3 数字:3 其他:2(空格和 ! 算其他)。

案例3回文数判断

题目:判断一个正整数是否为回文数(正着读和倒着读一样,如 12321)。技巧:把 n 倒序拼成 rev——每次 rev = rev*10 + m%10,最后比较 rev 与 n。

C++回文数判断
#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;
    int m = n, rev = 0;
    while (m > 0) {
        rev = rev * 10 + m % 10;      // 末位接到 rev 的尾部
        m /= 10;
    }
    if (n == rev) cout << "YES" << endl;
    else cout << "NO" << endl;
    return 0;
}

手算模拟:n = 123

轮次mm%10rev = rev*10 + 末位
112330×10+3 = 3
21223×10+2 = 32
31132×10+1 = 321

rev = 321 ≠ 123 → 输出 NO。若输入 121,rev = 121 = n → YES ✅

模块 04

总结提升

把零散的知识点串成体系,再向前多看一步。

总结核心要点
知识点核心写法易忘点
取末位/去末位n%10 / n/10while (n>0) 循环
数位和/位数循环中累加与计数取出的顺序是从低位开始
ASCII 三区段'0'=48,'A'=65,'a'=97数字 < 大写 < 小写
字符转数值c - '0'存的是 ASCII 码,不是数字本身
大小写转换±32 或 ±('a'-'A')先判断区间再转换
拓展ASCII 之外的世界

ASCII 只有 128 个编码,装不下中文。中文采用更庞大的编码体系:GB2312/GBK(国家标准,一个汉字占 2 个字节)和全球通用的 Unicode(UTF-8 是最常用的存储方式,常用汉字占 3 个字节)。这就是为什么处理中文文本时 char 数组常常不够用,C++ 中要借助 string 或宽字符类型。竞赛题一般只考 ASCII 字符处理,但了解编码背景能帮你理解"乱码"从何而来。

思考思考与启发

🤔 思考一:为什么"小写 − 32 = 大写"能成立?——因为两区段起点差 32(97−65),而区段内编码连续,所以任意对应字母都恰好差 32。

🤔 思考二:数位拆分取出的是"倒序"的数位(先个位),什么时候有影响?——求数位和、数位数无所谓;但拼倒序数(回文判断)、按序输出每位时就要注意顺序。

💡 启发:计算机世界里没有"字符"和"数字"的本质区别——都是二进制数。编码表就是一张"翻译对照表",学会查表、用表,文本问题立刻变成数学问题。

模块 05

学习检测

五大易错点 + 速查表 + 笔记卡,考前快速过一遍。

⚠️5 大易错点

易错点 1:把字符当数字用。'7' 的值是 55 不是 7!参与数值运算前必须 c - '0' 转换。

易错点 2:cin >> 丢空格。逐字符处理一行文本时用 cin >> c 会跳过所有空白字符,要用 cin.get(c) 或 getline。

易错点 3:大小写转换不判区间。对数字、空格也减 32 会得到乱码,必须先确认 c 在 'a'~'z'(或 'A'~'Z')之间。

易错点 4:while(n>0) 漏了 n=0。n 输入 0 时循环一次都不执行,位数算成 0。若题目允许 n=0,需特判。

易错点 5:char 溢出。char 范围通常为 −128~127,给 char 赋超过 127 的值会出错;'z'+1 之后的运算结果要转回 char 再输出。

📋速查表
操作代码片段结果
字符→编码(int)c如 'A' → 65
编码→字符(char)n如 66 → 'B'
数字字符→数值c - '0''8' → 8
数值→数字字符n + '0'8 → '8'
小写→大写c - 32(先判区间)'d' → 'D'
大写→小写c + 32(先判区间)'D' → 'd'
取末位/去末位n % 10 / n / 10数位拆分组合拳
📌笔记卡

📐 三个锚点编码:'0'=48、'A'=65、'a'=97,记住锚点即可推算全区段。

📐 差 32 定律:'a' − 'A' = 32,大小写互转就靠它。

📐 逐字符读:cin.get(c) 不跳空白;getline(cin, s) 读整行。

📐 回文套路:rev = rev*10 + m%10 拼倒序数,与原数比较。

课后练习

分层练习作业

A基础 / B提高 / C挑战,难度递增,点击展开答案与解析。

🟢 A组 · 基础(4题)

A1选择题

字符 'D' 的 ASCII 码是 68,则字符 'F' 的 ASCII 码是()

A. 69   B. 70   C. 71   D. 72

查看参考答案

答案:B. 70

大写字母区段连续:'E' = 69,'F' = 70。

考点:ASCII 表中 26 个大写字母是连续排列的,从 A=65 起按字母表顺序依次递增,不必死记每个字符的编码。

易错:把大小写相差 32 的规律套反,把小写 'f'=102 当成 'F' 的编码。

A2选择题

表达式 'b' - 'a' 的值是()

A. 1   B. 32   C. 98   D. 'b'

查看参考答案

答案:A. 1

'b' = 98,'a' = 97,98 − 97 = 1。同区段相减得到字母表中的偏移量。

考点:char 参与算术运算时会自动转成对应的整型编码,同区段字符相减得到的是它们在字母表中的偏移量之差。

易错:以为结果是字符 '1' 或 'b',忽略 char 在运算中已被提升为整数编码。

A3选择题

要判断 char 型变量 c 是数字字符,正确的条件是()

A. c >= 0 && c <= 9   B. c >= '0' && c <= '9'   C. c >= 48 || c <= 57   D. c - '0' > 0

查看参考答案

答案:B

判断的是字符 '0'~'9'(编码 48~57),不是数值 0~9。选项 D 在 c='0' 时得 0,会漏判。

考点:数字字符 '0'~'9' 的编码是连续的 48~57,所以用字符常量做区间比较最稳妥,也便于理解字符与编码的对应。

易错:写成 c >= 0 && c <= 9,拿编码和数值比;或把边界写成开区间,漏判 '0' 与 '9'。

A4计算题

写出整数 4096 的数位和。

查看参考答案

答案:19

4 + 0 + 9 + 6 = 19。程序流程:4096%10=6 → 409%10=9 → 40%10=0 → 4%10=4 → 结束。

思路:反复用 n % 10 取出末位数字累加,再用 n /= 10 丢掉末位,直到 n 变为 0,即可遍历每一位。

易错:在循环里用 n / 10 的商当数位累加,把「取商」和「取余」两个操作弄反。

🟡 B组 · 提高(4题)

B1编程题

读入一个大写字母,输出它在字母表中的序号(A 输出 1,B 输出 2……)。

查看参考答案
C++完整参考代码
#include <iostream>
using namespace std;

int main() {
    char c;
    cin >> c;                      // 读入一个大写字母,例如 D
    int idx = c - 'A' + 1;         // 同区段相减得偏移量,再 +1 变成序号
    cout << idx << endl;           // 输入 D 输出 4
    return 0;
}

答案:A 输出 1、D 输出 4,核心一行 c - 'A' + 1。

思路:字母在 ASCII 表里是连续排列的,'A'=65、'B'=66……所以 c - 'A' 刚好得到"从 A 数起偏移了几位"(0 起始),再 +1 就是从 1 开始的序号。验证:'D' 是 68,68 − 65 + 1 = 4 ✓;'Z' 是 90,90 − 65 + 1 = 26 ✓。

考点:char 与 int 的自动转换、字符在编码表中"连续"这一性质、同区段相减求偏移。

易错:① 别写 c - 64 这类"魔数",虽然结果一样但看不出含义,也容易记错;② 记住 'A' 是 65、'a' 是 97,别把字母当数字直接减;③ 想输出字母本身而不是它的编码时,要强转回 char。

B2编程题

读入一个整数 n(1 ≤ n ≤ 10⁹),输出它的位数。

查看参考答案
C++完整参考代码
#include <iostream>
using namespace std;

int main() {
    long long n;
    cin >> n;                      // 例如 1000000000

    if (n == 0) {                  // 0 也占 1 位,单独处理
        cout << 1 << endl;
        return 0;
    }

    long long m = n;               // 复制一份,保护原数
    int digits = 0;
    while (m > 0) {
        m /= 10;                   // 去掉最后一位
        digits++;                  // 去一次就多一位
    }
    cout << digits << endl;        // 1000000000 输出 10
    return 0;
}

答案:n = 1000000000(109)输出 10;特判 n = 0 输出 1。

思路:一个整数每除以 10 就少掉最后一位,所以"除到 0 为止,除了几次就是几位数"。把 n 复制到 m 上再除,原数 n 就不会被破坏。0 比较特殊——它当然只有 1 位,所以提前返回。

考点:while 循环 + 整除拆位、"数位拆分"的基本套路、数据范围与类型选择。

易错:① 忘记特判 n = 0,循环一次不进会输出 0;② 直接用 n 反复 /= 10 会毁掉原值,后面想再用就没有了,建议用副本 m;③ 109 用 int 够(int 大约能到 2.1×109),但到 1018 必须换 long long。

B3编程题

读入一行文字(可含空格),把其中的大写字母转成小写后输出,其余字符不变。

查看参考答案
C++完整参考代码
#include <iostream>
using namespace std;

int main() {
    char c;
    while (cin.get(c)) {                     // 逐字符读入,空格和换行都保留
        if (c >= 'A' && c <= 'Z')            // 只处理大写区间
            cout << (char)(c + 32);          // 大写转小写:编码 +32
        else
            cout << c;                       // 其他字符原样输出
    }
    return 0;                                // 输入 NOI 2025 输出 noi 2025
}

答案:输入 NOI 2025,输出 noi 2025(注意空格也原样保留)。

思路:逐个字符读进来判断:落在 ['A','Z'] 区间里就 +32 变成对应小写字母,否则(小写、数字、空格、标点)原样吐出去。读入必须用 cin.get(c),它会把空格和换行也读进来,这样才能"整行照原样还原"。

考点:cin.get(c) 与 cin >> c 的区别、大写与小写相差 32 的 ASCII 规律、<cctype> 里的 isupper / tolower 等价写法。

易错:① 用 cin >> c 时它自动跳过空格和换行,空格就被"吃掉"了,输出不再是原句;② 大小写相差 32,别写成 26 或 29;③ c + 32 的结果是 int,要强转 (char) 才是字符。

B4计算题

已知 'G' 的编码是 71,求小写字母 'g' 的编码,并写出推导过程。

查看参考答案

答案:103

'g' = 'G' + 32 = 71 + 32 = 103。也可从锚点推:'a' = 97,'g' 是第 7 个字母,97 + 6 = 103 ✓(两条路互相验证)。

思路:利用大小写字母固定相差 32 这一规律由 'G' 推 'g';再用锚点 'a'=97 按字母表偏移 6 位独立算一遍,两条路互相验证。

易错:把偏移量算成 7(误以为 'a' 的偏移是 1),得到 104。

🔴 C组 · 挑战(3题)

C1编程题

数字黑洞 6174:任取一个四位不完全相同的数,把组成它的四个数字从大到小和从小到大各排一次,用大减小,反复进行,最终必得 6174。编程模拟这一过程并输出每步算式。

查看参考答案
C++完整参考代码
#include <iostream>
#include <algorithm>
using namespace std;

int main() {
    int n;
    cin >> n;                              // 例如 4321
    int steps = 0;

    while (n != 6174) {                    // 一直迭代到数字黑洞
        int d[4] = { n / 1000, n / 100 % 10, n / 10 % 10, n % 10 };  // 拆出四个数位
        sort(d, d + 4);                                            // 从小到大排序

        int small = d[0] * 1000 + d[1] * 100 + d[2] * 10 + d[3];   // 升序重组的数
        int big   = d[3] * 1000 + d[2] * 100 + d[1] * 10 + d[0];   // 降序重组的数

        cout << big << " - " << small << " = " << big - small << endl;
        n = big - small;                   // 用差继续迭代
        steps++;
    }
    cout << "共 " << steps << " 步到达 6174" << endl;   // 4321 → 共 3 步
    return 0;
}

答案:以 4321 为例,3 步到达 6174:4321−1234=3087 → 8730−0378=8352 → 8532−2358=6174。任意四位"不完全相同"的数最多 7 步也一定会掉进 6174 这个黑洞。

思路:把当前数拆成 4 个数位 → 排序 → 用这 4 个数字拼出"最大数"和"最小数" → 相减 → 拿结果继续重复,直到结果正好等于 6174 为止。拆位用整除和取余:千位 n/1000、百位 n/100%10、十位 n/10%10、个位 n%10。

考点:整数数位拆分、sort 排序、while 循环模拟迭代过程、数字黑洞(Kaprekar 常数)。

易错:① 拆位的权值 1000 / 100 / 10 写错,数位就全乱了;② 最小数要按升序重组,像 0378 这种前导 0 在整数里会显示成 378,写成 378 或 0378 都对;③ 如果输入本来就是 6174,循环一次都不执行、步数为 0,这是正常结果;④ 若 4 个数字全部相同(如 1111),差恒为 0,会永远推不到 6174 —— 题目已用"不完全相同"排除。

C2编程题

加密解密:凯撒密码把每个大写字母向后移 3 位(X→A 环绕)。编写解密程序,把密文还原为明文。

查看参考答案
C++完整参考代码
#include <iostream>
#include <string>
using namespace std;

int main() {
    string s;
    getline(cin, s);                       // 读入一整行密文(含空格)

    for (char c : s) {
        if (c >= 'A' && c <= 'Z') {
            // 解密 = 每个字母前移 3 位;+26 保证括号里的数不为负
            cout << (char)((c - 'A' - 3 + 26) % 26 + 'A');
        } else {
            cout << c;                     // 非大写字母原样输出
        }
    }
    cout << endl;                          // 输入 KDSSB 输出 HAPPY
    return 0;
}

答案:前移 3 位解密:'D'→'A'、'A'→'X'、'W'→'T';密文 KDSSB 解密后是 HAPPY。

思路:解密就是加密的逆运算。加密时 +3,解密就 −3。先把字母换算成 0~25 的下标(c - 'A'),减 3 后 +26 再对 26 取余,就自动实现了"越过 A 就从 Z 绕回来"的循环移位,最后加回 'A' 变回字符。例如 'A':(0−3+26)%26 = 23,'A'+23 = 'X' ✓。

考点:模运算实现循环移位、字符与整数互转、getline 读整行。

易错:① 千万不要漏掉 +26:C++ 里负数取余结果仍是负数,'A'−3 会变成 −3,直接算出来是个乱七八糟的字符;② 只处理大写区间,空格和标点要原样输出;③ 加密是 +3、解密是 −3,方向别写反;④ 用 cin >> s 读到空格就停,整行输出要用 getline。

C3编程题

统计一行文字中出现次数最多的小写字母(数据保证唯一),输出该字母及其次数。

查看参考答案
C++完整参考代码
#include <iostream>
#include <string>
using namespace std;

int main() {
    string line;
    getline(cin, line);                    // 读一整行,保留空格

    int cnt[26] = {0};                     // 下标 0~25 依次对应 a~z
    for (char c : line) {
        if (c >= 'a' && c <= 'z')          // 只统计小写字母
            cnt[c - 'a']++;                // 计数
    }

    int best = 0;                          // 先假设 a 最多
    for (int i = 1; i < 26; i++) {
        if (cnt[i] > cnt[best])            // 打擂台找最大值
            best = i;
    }

    cout << (char)('a' + best) << " " << cnt[best] << endl;
    return 0;                              // apple banana → a 4
}

答案:输入 apple banana,输出 a 4。各字母次数:a 4 次、p 和 n 各 2 次、l / e / b 各 1 次,a 最多。

思路:开一个长度 26 的计数数组 cnt,下标 0~25 依次代表 a~z。扫一遍字符串,遇到小写字母就 cnt[c-'a']++;再扫一遍数组,用"打擂台"的办法找出最大值对应的下标,输出字母和次数。

考点:计数数组(桶思想)、字符到数组下标的映射、打擂台求最值。

易错:① 忘记过滤非小写字符(空格、数字、大写)会导致下标越界或统计错误;② 必须用 getline 读行,cin >> 会停在第一个空格,只统计到 apple;③ 输出字母时要 (char)('a' + best) 转回字符,直接输出 best 会变成数字;④ 题目保证最多者唯一,所以不需要处理并列。