例题给全套:题面、样例、参考代码、以及一步步的讲解。先自己读题想一遍, 再看代码,最后对着讲解核一遍自己想的对不对。
练习只给题面、样例和一句提示,答案是折叠起来的 —— 请先自己写一版跑通了再展开。写不出来也别直接看答案,先回速查那一组找找。
每份代码都能直接点运行:输入框里已经填好了样例输入, 把它改一改再跑,是这一页最值钱的用法。
先把地基摆一遍
先说那个 &。C++ 传参数默认是传值:函数拿到的是一份复制品,在里面怎么改都影响不到外面。参数类型后面加一个 & 就变成传引用:函数拿到的就是外面那个东西本身。
★ 而对数组、字符串、结构体来说,这个 & 不只是「能不能改」的问题,更是快慢的问题 —— 不写 &,每次调用都要把整个数组复制一遍。一百万个 int 实测差 6.7 倍,而递归里调用几十万次就是几十万次复制。⇒ 参数是容器时一律写 const T &(const 表示「我只读不改」,顺带防手滑)。
★★ 再说递归。递归函数每进一层,它的局部变量都要在栈上占一块地方,而栈通常只有 8 MB。所以两件事要当心:一是必须有出口(否则无限递归,几万层就崩);二是别在递归函数里开大数组(每层都占一份)。
⚠ 本书量过一个反直觉的数:同一台机器上,递归函数体里多几个变量,能递归的层数就从 17.4 万掉到 2.4 万 —— 能递归多深不是机器的属性,是「你在那个函数里写了什么」的属性。
例题(2 道)
每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。
例题 ① 区间和
给 n 个整数 a₁ … aₙ,再给 q 个询问,每个询问两个数 l、r,请输出 a_l + a_{l+1} + … + a_r。
⚠ 请把「求区间和」写成一个函数,参数里带上那个数组。
n、q(1 ≤ n, q ≤ 1000);第二行 n 个整数(|aᵢ| ≤ 10⁹);接下来 q 行,每行两个整数 l、r(1 ≤ l ≤ r ≤ n)。q 行,每行一个整数。输入
5 2 1 2 3 4 5 1 3 2 5
输出
6 14
1+2+3 = 6;2+3+4+5 = 14。下标从 1 开始数。算法本身是一个循环。这道题要练的是函数的参数怎么写 ——如果写成 long long rangeSum(vector<int> a, int l, int r),会发生什么?
// 例题 ①「区间和」—— 函数、引用与递归组//// 这道题真正要练的是那个参数写法:const vector<int> &a。// 少了 & ⇒ **每次调用都把整个数组复制一遍**(q 次询问就复制 q 遍);// 少了 const ⇒ 编译器不会拦你手滑改掉调用方的数据。// ⇒ 参数是 vector / string / struct 时,一律写 const T &。
#include <bits/stdc++.h>using namespace std;
// a 用的是 1 基下标:a[1] 是第一个数long long rangeSum(const vector<int> &a, int l, int r) { long long s = 0; for (int i = l; i <= r; i++) s += a[i]; return s;}
int main() { int n, q; cin >> n >> q;
vector<int> a(n + 1); // 多开一格,好让下标从 1 开始 for (int i = 1; i <= n; i++) cin >> a[i];
while (q--) { int l, r; cin >> l >> r; cout << rangeSum(a, l, r) << "\n"; } return 0;}点「运行 ▶」看结果
- C++ 传参数默认是传值:函数拿到的是一份复制品。⇒ 写成
vector<int> a的话,每次调用都要把整个数组复制一遍,q次询问就复制q遍。 - 参数类型后面加一个
&就变成传引用:函数拿到的就是外面那个数组本身,一个字节都不复制。本书实测过:一百万个 int 的数组,差 6.7 倍。 - 再加一个
const表示「我只读不改」—— 顺带防手滑,也让读代码的人一眼看出这个函数不会动你的数据。⇒ 参数是 vector / string / struct 时,一律写const T &。 - ⚠ 这条对比较器 lambda 同样成立:
[](const Node &a, const Node &b)——sort 会调用它几百万次,每次复制一个结构体不是小钱。 - 这道题的下标从 1 开始 ⇒ 数组开
n + 1格(vector<int> a(n + 1)),a[0]空着不用。⚠ 开n格的话a[n]就越界了。
⚠ 顺带一笔账:q 次询问、每次最多扫 n 个数 ⇒ 最坏 10⁶ 次加法,这道题的规模下没问题。真要 n, q ≤ 10⁵,就得上第 6 章的前缀和了。
例题 ② 递归:从个位开始逐位打印
给一个非负整数 n,请把它的每一位从个位开始逐个打印出来,每行一个。
⚠ 要求用递归写,不许用循环 —— 这道题练的就是递归的形状。
n(0 ≤ n ≤ 10⁹)。输入
1305
输出
5 0 3 1
写递归永远是两步,顺序不能反:① 先写出口,② 再写递推。这道题的出口是什么?(什么情况下,不用再往下递了?)
// 例题 ②「递归:从个位开始逐位打印」—— 函数、引用与递归组//// 写递归永远是两步,顺序不能反:// ① 先写**出口** —— 什么情况下不用再往下递了、答案直接是什么;// ② 再写**递推** —— 假设「小一号的问题」已经算对了,我怎么用它拼出答案。// ⚠ 第 ② 步的关键是**相信它会算对**,不要在脑子里展开执行。
#include <bits/stdc++.h>using namespace std;
void printDigits(int n) { if (n < 10) { // ① 出口:只剩一位了,打出来就收工 cout << n << "\n"; return; } cout << n % 10 << "\n"; // 先把个位打掉 printDigits(n / 10); // ② 递推:剩下那个更小的数,交给它自己去打}
int main() { int n; cin >> n; printDigits(n); // 忘了写出口会怎样:n 一路除到 0 之后**一直是 0**,函数无限递归下去, // 几万层之后栈就满了 —— 表现是跑一两秒然后 Segmentation fault。 return 0;}点「运行 ▶」看结果
- ① 出口:
n < 10时只剩一位了,直接打出来就收工。⇒ 出口的标志是「这一层自己就能给出答案,不需要问更小的问题」。 - ② 递推:先把个位
n % 10打掉,剩下的n / 10是一个更小的同类问题 ——把它交给函数自己。 - ⚠ 第 ② 步的关键是相信它会算对,不要在脑子里展开执行。「假设
printDigits(130)已经能把 130 的每一位打对,那我该做什么」—— 只问这一句。 - 忘了写出口会怎样:
n一路除到 0 之后一直是 0,函数无限递归下去,几万层之后栈就满了 ⇒ 跑一两秒然后 Segmentation fault。⚠ 而编译器对此一个字都不会说(有时会给一条infinite recursion的警告,但别指望它)。
第 1 章讲的就是这件事,而它是往后三十章(搜索、树、DP)的地基。
用到的写法:递归的出口 void f(int x)
练习(3 道)
答案折叠着,先自己写一版。
练习 ① 递归求最大公约数
给两个正整数 a、b,求它们的最大公约数(gcd)。
★ 用这条性质:gcd(a, b) = gcd(b, a mod b),而 gcd(a, 0) = a。
a、b(1 ≤ a, b ≤ 10¹⁸)。输入
84 30
输出
6
题面已经把出口和递推都告诉你了 —— 照着「① 先写出口、② 再写递推」抄一遍就行。⚠ 注意数据范围。
这就是第 40 章的辗转相除法。⚠ 它的递归深度是 O(log) 级的(几十层),所以完全不用担心爆栈 ——而下一道题就不是这样了。
用到的写法:递归的出口 void f(int x)
练习 ② 递归求 1 + 2 + … + n
给一个正整数 n,用递归求 1 + 2 + … + n。
n(1 ≤ n ≤ 10⁵)。输入
100
输出
5050
出口:n = 0 时答案是 0。递推:sumTo(n) = n + sumTo(n − 1)。⚠ 写完之后先别急着交,看看下面那一句。
⚠⚠ 这个写法要递归 n 层。本书实测过:一个精简的递归函数每层约 48 字节,栈上限 8 MB ⇒ 大约 17.4 万层就崩了。⇒ n = 10⁵ 勉强活着,n = 10⁶ 必炸。能递归多深不是机器的属性,是「你在那个函数里写了什么」的属性 ——同一台机器上,函数体里多几个变量,能递归的层数就从 17.4 万掉到 2.4 万。⇒ 顶格的题一律写循环(这道题更是直接有公式 n(n+1)/2),别赌。
用到的写法:递归的出口 别在递归函数里开大数组
练习 ③ 让函数真的改掉外面那个数组
给 n 个整数,请写一个函数把数组里每个元素翻倍,然后在 main 里输出结果。
⚠ 翻倍这件事必须在函数里做,main 里只负责读入、调用和输出。
n(1 ≤ n ≤ 10⁵);第二行 n 个整数。n 个整数,用空格隔开,行末不留空格。输入
4 1 2 3 4
输出
2 4 6 8
1 2 3 4 翻倍之后是 2 4 6 8。这道题有两个地方要加 &,少任何一个,程序都照样编译、照样跑,只是什么都没变。(一个在参数表里,另一个在函数体里。)
⚠ 而这里不能加 const —— 我们就是要改它。⇒ const T & 是「只读」,T & 是「要改」,两者的区别在这道题上是生死。