✏️
📎
DAY 01
模拟 · 高精 · 枚举 · 贪心 · 字符串
CSP-J/S 暑期集训 · B班
🔄
模拟
按规则逐步执行
🔢
高精度
BigInt 大数运算
🔍
枚举
不重不漏穷举
💡
贪心
每步选最优
📝
字符串
string 操作与应用
什么是模拟? 按照题目描述的规则,用代码一步一步地执行,不做任何优化——这就是模拟。核心能力:读懂题意 → 转化为代码逻辑 → 逐步实现
模拟的核心思路
读题理解 建模转化 逐步实现 验证输出 数据 → 变量/数组
常见模拟类型
类型描述例题
直接模拟按题意直接写,无技巧P1003 铺地毯
过程模拟模拟某个过程的每一步P1181 数列分段
复杂模拟规则复杂,需要仔细实现P1067 多项式输出
大数模拟配合高精度运算P1009 阶乘之和
💡模拟的核心:不要急于优化!先把正确性保证,再考虑效率。模拟题的关键是"细心"——不遗漏任何细节。
📋题意:在地板上按顺序铺 n 张矩形地毯(编号 1~n),后铺的会覆盖先铺的。给定一个点 (x,y),求最上面能看到的毯子编号。若没有输出 -1。
思路:倒序检查法
既然后铺的覆盖先铺的,那就从最后一张往前检查,第一张覆盖该点的就是答案。
SVG 示意图 — 地毯覆盖
地板坐标平面 地毯1 地毯2 地毯3 (x,y) a1,b1a2,b2a3,b3 从地毯3→2→1倒序检查:地毯3覆盖(x,y)✅ → 答案=3
步骤模拟
🔍步骤:读入所有地毯 a[i],b[i],g[i],k[i] → 读入目标点 x,y → 令 ans=-1 → 从 i=n 到 1:若 a[i]≤x≤a[i]+g[i] 且 b[i]≤y≤b[i]+k[i],ans=i 并 break → 输出 ans
代码
#include <iostream> using namespace std; int a[10005],b[10005],g[10005],k[10005]; int main() { int n; cin >> n; for(int i=1;i<=n;i++) cin>>a[i]>>b[i]>>g[i]>>k[i]; int x,y; cin>>x>>y; int ans = -1; for(int i=n;i>=1;i--) { if(x>=a[i] && x<=a[i]+g[i] && y>=b[i] && y<=b[i]+k[i]) { ans = i; break; } } cout << ans << endl; return 0; }
🎯小结:倒序遍历 + 第一个命中即答案。时间 O(n),空间 O(n)。关键是理解"后铺的覆盖先铺的"。
📋题意:将一段正整数数列分成若干连续段,使得每段之和不超过 M,求最少段数。
思路:贪心分段
从左到右扫描,不断往当前段累加。一旦加上当前数会超过 M,就新开一段。
SVG 示意图 — 数列分段
数列:4 2 3 5 1 6 2 4,M=10 4 2 3和=9≤10 ✓ 5 1和=6≤10 ✓ 6 2和=8≤10 ✓ 4和=4≤10 ✓ 段1 段2 段3 段4 答案:4 段
步骤模拟
🔍步骤:sum=0, cnt=1 → 读4: sum=4 → 读2: sum=6 → 读3: sum=9 → 读5: sum+5=14>10 → cnt=2, sum=5 → 读1: sum=6 → 读6: sum+6=12>10 → cnt=3, sum=6 → 读2: sum=8 → 读4: sum+4=12>10 → cnt=4, sum=4 → 结束,ans=4
代码
#include <iostream> using namespace std; int main() { int n, m; cin >> n >> m; int sum = 0, cnt = 1; for(int i = 0; i < n; i++) { int x; cin >> x; if (sum + x > m) { cnt++; sum = x; } else sum += x; } cout << cnt << endl; return 0; }
🎯小结:贪心分段策略——尽量多装,装不下就新开。时间 O(n),一次扫描即可。
为什么需要高精度? long long 最大只能表示约 9×1018(19位)。当题目要求计算 100! 这样的超大数时,内置类型完全不够。我们需要用数组模拟手工运算
核心思想:用数组存储大数的每一位
以 12345 为例 数字: 1 2 3 4 5 高位 ←————————————→ 低位 存储方式:低位在前(倒序存储) d[] : 5 4 3 2 1 d[0]d[1]d[2]d[3]d[4] 低位在前方便进位!加法时 d[0] 先加,进位自然往后传递。
💡为什么低位在前?手工加法从个位算起,进位往高位传。数组 d[0] 存个位、d[1] 存十位……这样下标 i 对齐,进位只需 d[i+1]++,代码最简洁。
5 种构造函数
BigInt() 默认构造空对象  |  BigInt(long long v) 从整数构造  |  BigInt(string s) 从字符串构造  |  BigInt(char c) 从单个字符构造  |  BigInt(const BigInt& o) 拷贝构造
struct BigInt { vector<int> d; // d[0]=个位, d[1]=十位, ... BigInt() {} // 默认 BigInt(long long v) { if (v == 0) d.push_back(0); while (v > 0) { d.push_back(v % 10); v /= 10; } } BigInt(string s) { for (int i = s.size()-1; i >= 0; i--) d.push_back(s[i] - '0'); } BigInt(char c) { d.push_back(c - '0'); } BigInt(const BigInt& o) : d(o.d) {} };
输出函数 out()
void out() const { if (d.empty()) { cout << 0; return; } for (int i = d.size()-1; i >= 0; i--) cout << d[i]; }
最高位(d 的末尾)开始输出到个位(d[0]),和读数字的自然顺序一致。
比较运算符
// 小于:先比位数,再从高位逐位比 bool operator<(const BigInt& o) const { if (d.size() != o.d.size()) return d.size() < o.d.size(); for (int i = d.size()-1; i >= 0; i--) if (d[i] != o.d[i]) return d[i] < o.d[i]; return false; // 相等 } // 小于等于:逐位比较(和 < 类似,但允许相等) bool operator<=(const BigInt& o) const { if (d.size() != o.d.size()) return d.size() < o.d.size(); for (int i = d.size()-1; i >= 0; i--) if (d[i] != o.d[i]) return d[i] < o.d[i]; return true; // 完全相等 } // 等于:vector 直接 == bool operator==(const BigInt& o) const { return d == o.d; }
🎯关键点:<= 独立实现逐位比较,与 < 逻辑一致但相等时返回 true。比较逻辑:位数不同 → 位数小的更小;位数相同 → 从高位到低位逐位比较。
原理:模拟竖式加法
347 + 586 = 933 347 + 586 933 进1 进1 d[0]: 7+6=13 → d[0]=3, carry=1 → d[1]: 4+8+1=13 → d[1]=3, carry=1 → d[2]: 3+5+1=9 → d[2]=9 每轮:sum = d[i] + o.d[i] + carry → 结果位 = sum%10 → 新进位 = sum/10
代码
BigInt operator+(const BigInt& o) const { BigInt res; int carry = 0; for (int i = 0; i < d.size() || i < o.d.size() || carry; i++) { int sum = carry; if (i < d.size()) sum += d[i]; if (i < o.d.size()) sum += o.d[i]; res.d.push_back(sum % 10); carry = sum / 10; } return res; }
💡要点:循环条件包含 carry,确保最后的进位也被加入结果。i 可能超过两个操作数的长度,所以用 i < size 保护访问。
减法 — 先比较大小,再模拟借位
503 - 287 = 216 503 - 287 216 d[0]: 3-7<0 → 13-7=6, borrow=1 d[1]: 0-8-1<0 → 10-9=1, borrow=1 d[2]: 5-2-1=2
BigInt operator-(const BigInt& o) const { // 先比较大小,保证大减小 bool a_ge_b = !(*this < o); const BigInt &big = a_ge_b ? *this : o; const BigInt &sml = a_ge_b ? o : *this; BigInt res; int borrow = 0; for (int i = 0; i < big.d.size(); i++) { int diff = big.d[i] - borrow; if (i < sml.d.size()) diff -= sml.d[i]; if (diff < 0) { diff += 10; borrow = 1; } else borrow = 0; res.d.push_back(diff); } while (res.d.size() > 1 && res.d.back() == 0) res.d.pop_back(); return res; }
乘法 — 逐位相乘再累加
12 × 34 = 408 12 34 × 48 360 408 ← 4 × 12 = 48 ← 3 × 12 = 36(左移一位,即 360) 48 + 360 = 408 res 初始化为 d.size()+o.d.size() 个 0 双重循环:res.d[i+j] += d[i] × o.d[j] 进位:res.d[i+j+1] += res.d[i+j] / 10 取模:res.d[i+j] %= 10
BigInt operator*(const BigInt& o) const { BigInt res; res.d.assign(d.size() + o.d.size(), 0); for (int i = 0; i < d.size(); i++) for (int j = 0; j < o.d.size(); j++) { res.d[i+j] += d[i] * o.d[j]; res.d[i+j+1] += res.d[i+j] / 10; res.d[i+j] %= 10; } while (res.d.size() > 1 && res.d.back() == 0) res.d.pop_back(); return res; }
💡注意:减法先用 < 比较大小(先比位数,再逐位比),确保大减小,无论谁大都不会出错。乘法结果数组预留 d.size()+o.d.size() 位,足够容纳最大积。
除法 — 高精除以低精度
933 ÷ 3 = 311(从高位到低位逐位除) 9 3 3 ÷ 3 rem=9, 9/3=3 rem=0, 3/3=1 rem=0, 3/3=1 从最高位往最低位:rem = rem*10 + d[i],res.d[i] = rem / v,rem %= v
BigInt operator/(int v) const { BigInt res; res.d.resize(d.size()); long long rem = 0; for (int i = d.size()-1; i >= 0; i--) { rem = rem * 10 + d[i]; res.d[i] = rem / v; rem %= v; } while (res.d.size() > 1 && res.d.back() == 0) res.d.pop_back(); return res; }
复合赋值运算符
复合赋值运算符内部调用对应的二元运算符,然后赋值给自身,返回 *this 的引用。
BigInt& operator+=(const BigInt& o) { *this = *this + o; return *this; } BigInt& operator-=(const BigInt& o) { *this = *this - o; return *this; } BigInt& operator*=(const BigInt& o) { *this = *this * o; return *this; } BigInt& operator/=(int v) { *this = *this / v; return *this; }
🎯至此 BigInt 完整实现:5种构造 + out + 比较(<,≤,=) + 四则运算 + 复合赋值,共约 60 行。
📋题意:计算 S = 1! + 2! + 3! + … + n!,n ≤ 50。结果非常大,必须用高精度。
🔢50! 有多大?50! ≈ 3.04 × 10⁶⁴,共 65 位数字!远超 long long 的范围(约 18 位)。必须用高精度(BigInt)来存储和计算。即使用递推 fact(i) = fact(i-1) × i,中间结果也会迅速超过任何内置整数类型。
思路:递推 + BigInt
递推关系:fact(i) = fact(i-1) × i,sum += fact(i) i=1fact=1, sum=1 i=2fact=2, sum=3 i=3fact=6, sum=9 i=4fact=24, sum=33 i=5fact=120, sum=153 n=5 时 S=153。n=50 时需要 BigInt!
代码
#include <iostream> #include <vector> #include <string> using namespace std; struct BigInt { /* ... 完整实现 ... */ }; int main() { int n; cin >> n; BigInt fact(1), sum(0); for (int i = 1; i <= n; i++) { fact *= BigInt((long long)i); sum += fact; } sum.out(); cout << endl; return 0; }
🎯总结:利用递推 fact(i)=fact(i-1)×i 避免重复计算,配合 BigInt 的 *= 和 += 运算符,代码极简。复杂度 O(n²)(n 次 BigInt 乘法)。
枚举 = 不重不漏地穷举所有可能。 暴力但可靠!关键:确定枚举范围和检验条件。
子集枚举 — 二进制对应
P1036 选数:从 {2, 3, 5, 7, 11} 中选 3 个,和为素数的方案 maskbit4(11)bit3(7)bit2(5)bit1(3)bit0(2) 001110011110 010110101112 011010110114 011100111015 100111001116 101011010118 101101011019 ✓ 110011100120 110101101021 111001110023 ✓ 枚举所有 C(5,3)=10 种选法,统计 popcount(mask)==k 且 sum 为素数的方案数 → 答案: 2
// 子集枚举模板 for (int mask = 0; mask < (1 << n); mask++) { for (int j = 0; j < n; j++) { if (mask & (1 << j)) { // 第 j 个元素被选中 } } }
💡枚举的核心:1. 明确枚举什么(变量范围)2. 明确检验条件(如何判断合法)3. 不重不漏。
📋题意:将 1~9 分成三组,每组 3 个数字,使每组组成的三位数之比为 1:2:3。输出所有方案。
思路:全排列枚举
把 1~9 的全排列(9! = 362880 种)枚举出来,每种排列拆成前3位、中3位、后3位组成三个三位数 a、b、c,检查是否满足 b=2a 且 c=3a。
全排列 → 拆分三组 → 验证比例 枚举 1~9 全排列next_permutation → 9! 种 拆成三组各3位p[0..2] → a, p[3..5] → b, p[6..8] → c 检查 b==2a && c==3a满足则输出 a, b, c 例如排列 {1,9,2,3,8,4,5,7,6} → a=192, b=384, c=576 → 1:2:3 ✓
代码
#include <iostream> #include <algorithm> using namespace std; int main() { int p[] = {1,2,3,4,5,6,7,8,9}; do { int a = p[0]*100+p[1]*10+p[2]; int b = p[3]*100+p[4]*10+p[5]; int c = p[6]*100+p[7]*10+p[8]; if(b==2*a && c==3*a) cout<<a<<" "<<b<<" "<<c<<endl; } while(next_permutation(p, p+9)); return 0; }
🎯总结:全排列枚举 → 拆分三组 → 验证比例。next_permutation 自动按字典序生成,不重不漏。复杂度 O(9! × 9),但对计算机来说瞬间完成。
贪心策略:每一步都做出当前看起来最优的选择,不考虑后续。关键问题——局部最优能否导致全局最优?需要证明
证明三步走
第一步:假设最优解设最优解为 O,贪心解为 G 第二步:交换论证若 G≠O,交换 O 中某步使之与 G 一致 第三步:不劣论证交换后答案不会更差 → G 也是最优 核心:证明"贪心选择"至少和任何其他选择一样好
证明方法对比
方法思路适用场景
交换论证假设最优解与贪心不同,交换后不更差排序类贪心(最常用)
排序不等式正序和 ≥ 乱序和 ≥ 逆序和排队、匹配类问题
贪心选择性质证明第一步贪心选择一定在某最优解中活动选择、区间调度
数学归纳对 n 归纳,假设前 k 步贪心最优递推相关贪心
📋题意:n 个活动,每个有开始和结束时间。同一时间只能参加一个活动,求最多能参加几个。
贪心策略:按结束时间升序排序
证明:选结束最早的活动,留给后续的时间最多。若最优解第一步不选结束最早的活动,可替换为结束最早的,不会更差(结束论证)。
按结束时间排序后的时间轴 t 活动A [2,6] ✓选中 活动B [8,12] ✓选中 活动C [10,14] ✗冲突 活动D [14,20] ✓选中 2681220 选中 A→B→D 共 3 个活动(每次选结束最早且不冲突的)
代码
#include <iostream> #include <algorithm> using namespace std; struct Act { int s, e; }; int main() { int n; cin >> n; vector<Act> a(n); for(auto& x : a) cin >> x.s >> x.e; sort(a.begin(), a.end(), [](const Act& a, const Act& b){ return a.e < b.e; }); int cnt = 0, last = 0; for(auto& x : a) if(x.s >= last) { cnt++; last = x.e; } cout << cnt << endl; }
🎯总结:贪心策略——按结束时间排序,每次选不冲突的。时间 O(n log n)。这就是经典的"区间调度"问题。
📋题意:n 个人排队接水,第 i 人接水需 t[i] 时间。求一种排列使所有人的等待总时间最小。
贪心策略:短作业优先(SJF)
排序不等式:按接水时间升序排列。证明——若存在相邻两人 i, j 且 t[i]>t[j],交换后 j 少等 t[i]-t[j],i 多等 t[j]-t[i],总和减少。故任何逆序都能通过交换改进。
排队接水示意(短作业优先) t=1等: 0 t=3等: 1 t=4等: 4 t=5等: 8 t=7等: 13 接水时间升序 → 总等待最小 总等待 = 0+1+4+8+13 = 26 每人的等待 = 前面所有人的接水时间之和 越短的任务越先做减少后续人的等待
代码
#include <iostream> #include <algorithm> using namespace std; struct P { int t, id; }; int main() { int n; cin >> n; vector<P> a(n); for(int i=0;i<n;i++) { cin>>a[i].t; a[i].id=i+1; } sort(a.begin(), a.end(), [](const P& a, const P& b){ return a.t < b.t; }); long long sum = 0, wait = 0; for(auto& x : a) { sum += wait; wait += x.t; } for(auto& x : a) cout << x.id << " "; cout << endl; printf("%.2f\n", (double)sum / n); }
🎯总结:短作业优先(SJF) + 排序不等式证明。时间 O(n log n)。输出原编号和平均等待时间。
C++ string:可变长字符序列,支持随机访问、拼接、查找等操作。底层是字符数组,自动管理内存。
常用操作速查表
操作语法说明复杂度
访问s[i]第 i 个字符O(1)
长度s.size() / s.length()字符个数O(1)
拼接s1 + s2 / s1 += s2字符串连接O(n)
子串s.substr(pos, len)从 pos 取 len 个字符O(len)
查找s.find(t)返回首次出现位置,找不到返回 string::nposO(n·m)
比较s1 == s2 / s1 < s2字典序比较O(n)
插入s.insert(pos, t)在 pos 处插入 tO(n)
删除s.erase(pos, len)从 pos 删 len 个O(n)
反转reverse(s.begin(), s.end())原地翻转O(n)
SVG 可视化
string s = "hello world" h e l l o w o r l d [0][1][2][3][4][5][6][7][8][9][10] s.substr(1, 4) = "ello" s.find("world") = 6
P1308 统计单词数
📋题意:给定一个单词 t 和一行文本 s,求 t 在 s 中作为独立单词出现的次数及首次位置(不区分大小写)。
🔑关键点:1. 统一转小写比较 2. "独立单词"意味着前后必须是空格或首尾边界 3. 用 find 或手动匹配
举例:单词 t="To",文本 s="to be or not to be"。统一转小写后 t="to",在 s 中搜索 " to "(前后加空格),找到 2 次,首次出现在位置 14(原串下标)。
#include <iostream> #include <string> using namespace std; int main() { string t; getline(cin, t); string s; getline(cin, s); for(auto& c:t) c=tolower(c); for(auto& c:s) c=tolower(c); s = " " + s + " "; t = " " + t + " "; int cnt=0, pos=-1; size_t p = s.find(t); while(p != string::npos) { cnt++; if(pos==-1) pos = p; p = s.find(t, p+1); } if(cnt) cout<<cnt<<" "<<pos-1<<endl; else cout<<-1<<endl; }
🎯技巧:前后加空格,用 find(" t ")确保是独立单词。
P1603 斯诺登的密码
📋题意:从一段英文文本中提取所有英文数字单词(one~nineteen, twenty, thirty...ninety),转为对应数字,排序后拼接输出(去掉前导零)。
🔑思路:1. 逐词读取,去掉标点 2. 用 map 把英文单词映射为数字 3. 收集所有数字排序 4. 拼接输出
#include <iostream> #include <string> #include <map> #include <vector> #include <algorithm> using namespace std; int main() { map<string,int> mp; mp["one"]=1; mp["two"]=2; mp["three"]=3; mp["four"]=4; mp["five"]=5; mp["six"]=6; mp["seven"]=7; mp["eight"]=8; mp["nine"]=9; mp["ten"]=10; mp["eleven"]=11; mp["twelve"]=12; mp["thirteen"]=13; mp["fourteen"]=14; mp["fifteen"]=15; mp["sixteen"]=16; mp["seventeen"]=17; mp["eighteen"]=18; mp["nineteen"]=19; mp["twenty"]=20; mp["thirty"]=30; mp["forty"]=40; mp["fifty"]=50; mp["sixty"]=60; mp["seventy"]=70; mp["eighty"]=80; mp["ninety"]=90; vector<int> nums; string w; while(cin>>w) { while(w.size()&&!isalpha(w.back())) w.pop_back(); while(w.size()&&!isalpha(w.front())) w.erase(w.begin()); for(auto&c:w) c=tolower(c); if(mp.count(w)) nums.push_back(mp[w]); } sort(nums.begin(), nums.end()); string res; for(int x:nums) res += to_string(x); int st=0; while(st<res.size()-1&&res[st]=='0') st++; cout<<res.substr(st)<<endl; }
🎯总结:字符串题核心——清洗(去标点、统一大小写)+ 匹配(find/map)+ 构造结果。
📋题意:给定字符串 s,找出所有将 s 在某处分割为左右两部分,左半部分反转后拼到右半部分前面,使得结果是回文串的方案数。
思路:枚举分割点
枚举分割点 i(左=s[0..i-1], 右=s[i..n-1]) i=0: 左="", 右="abcba" → 拼="abcba" → 回文? ✓ i=2: 左="ab", 右="cba" → 拼="ba"+"cba"="bacba" → 回文? ✗ i=3: 左="abc", 右="ba" → 拼="cba"+"ba"="cbaba" → 回文? ✗ 拼接规则:new_s = reverse(left) + right 检查 new_s 是否等于 reverse(new_s)
代码
#include <iostream> #include <string> #include <algorithm> using namespace std; int main() { string s; cin >> s; int n = s.size(), ans = 0; for(int i = 0; i <= n; i++) { string L = s.substr(0, i); string R = s.substr(i); reverse(L.begin(), L.end()); string t = L + R; string rev = t; reverse(rev.begin(), rev.end()); if(t == rev) ans++; } cout << ans << endl; }
字符串解题套路总结
套路适用场景例题
清洗+匹配大小写/标点干扰的查找P1308
映射+构造单词转数字、格式化P1603
枚举分割子串拼接、回文判定B4039
双指针回文检测、子串匹配通用
以下题目覆盖本次课程全部知识点,难度标注为洛谷官方难度:入门  普及−  普及
#题号题名知识点难度分层建议
1P1067多项式输出模拟普及−必做
2P1109学生分组模拟普及−必做
3P1003铺地毯模拟普及−必做
4P1181数列分段模拟/贪心普及−必做
5P1009阶乘之和高精度普及−必做
6P1618三连击(升级版)枚举普及−必做
7P1008三连击枚举普及−必做
8P1803凌乱的yyy / 活动安排贪心普及−必做
9P1190排队接水贪心普及−必做
10P4995跳跳贪心普及选做
11P1106删数问题贪心/字符串普及选做
12B3928贪心练习贪心普及−选做
13P1515旅行贪心普及−挑战
14P1079Vigenere密码字符串普及−必做
15P1098字符串的展开字符串普及−挑战
16P1308统计单词数字符串普及−必做
17B4039回文拼接字符串普及−选做
18B3927字符串练习字符串普及−选做
19B3680综合题1综合普及−选做
20B3979综合题2综合普及−挑战
📌完成建议:必做 11 题 → 选做 6 题 → 挑战 3 题。每题先独立思考 15 分钟再看提示。做完后对照课堂模板总结套路。
🎉
Day 01 课程结束
模拟 · 高精 · 枚举 · 贪心 · 字符串
今日收获
✅ 模拟:按规则逐步执行,细心是核心
✅ 高精度:BigInt 模板,数组模拟运算
✅ 枚举:不重不漏,确定范围与检验
✅ 贪心:每步最优 + 证明(交换论证/排序不等式)
✅ 字符串:清洗、匹配、构造三板斧
📖 明日预告 · Day02 二分与倍增
🔍 二分查找:边界处理与答案验证
📐 二分答案:把"求最优"转化为"判可行"
🚀 倍增思想:从 1 到 n 的跳跃加速
💡 经典应用:查找/答案/倍增三合一实战
讲师:黄老师信奥
CSP暑期集训 · 加油!💪