📖 第 2.3 章 ⏱️ 约 65 分钟 🎯 入门

第 2.3 章:函数与数组

📝 前置条件: 第 2.1 章和第 2.2 章(变量、循环、if/else)

随着程序越来越大,你需要组织代码的方式(函数)和存储数据集合的方式(数组和向量)。本章介绍这两者——竞赛编程中最重要的两个工具。


2.3.1 函数——是什么,为什么需要

🍕 食谱类比

📄 查看代码:🍕 食谱类比
函数就像一份披萨食谱:

- 输入(参数):   食材——面粉、奶酪、番茄
- 过程(函数体):烹饪步骤
- 输出(返回值):做好的披萨

就像你可以用一份食谱做很多张披萨,
你可以用不同的输入多次调用一个函数。

pizza("薄底", "培根")  → 一张披萨
pizza("厚底", "蘑菇")  → 另一张披萨

没有函数,如果你需要在程序的五个地方计算「这个数是不是质数」,你就得把同样的 10 行代码复制粘贴五次。然后如果发现了 bug,就要在五个地方全部修复!

什么时候写函数

以下情况使用函数:

  1. 程序中某段逻辑重复了 3 次以上
  2. 某段代码做的是一件清晰命名的事(如「检查是否质数」「计算距离」)
  3. 你的 main 变得太长、不好读

函数的基本语法

返回类型 函数名(参数1类型 参数1, 参数2类型 参数2, ...) {
    // 函数体
    return 值;  // 必须与返回类型匹配;void 函数可省略
}

你的第一批函数

📄 查看代码:你的第一批函数
#include <bits/stdc++.h>
using namespace std;

// ---- 函数定义(必须在使用前定义,或使用前置声明)----

// 接收一个整数,返回它的平方
int square(int x) {
    return x * x;
}

// 接收两个整数,返回较大的那个
int maxOf(int a, int b) {
    if (a > b) return a;
    else return b;
}

// void 函数:执行某件事但不返回值
void printSeparator() {
    cout << "====================\n";
}

// ---- 主函数 ----
int main() {
    cout << square(5) << "\n";       // 以 x=5 调用 square,打印 25
    cout << square(12) << "\n";      // 以 x=12 调用 square,打印 144

    cout << maxOf(7, 3) << "\n";     // 打印 7
    cout << maxOf(-5, -2) << "\n";   // 打印 -2

    printSeparator();                // 打印分隔线
    cout << "完成!\n";
    printSeparator();

    return 0;
}

🤔 为什么函数要在 main 之前?

C++ 从上到下读取你的文件。当它看到 square(5) 这样的调用时,需要已经知道 square 是什么。如果 squaremain 之后定义,编译器会说「我从没听说过 square!」

方案一: 把所有函数定义在 main 之上(最简单)。

方案二: 使用函数原型——前置声明,告诉编译器「这个函数存在,我之后再定义」:

📄 C++ 完整代码
#include <bits/stdc++.h>
using namespace std;

int square(int x);       // 原型——只有签名,没有函数体
int maxOf(int a, int b); // 原型

int main() {
    cout << square(5) << "\n";   // OK!编译器知道 square 存在
    return 0;
}

// 完整定义可以在 main 之后
int square(int x) {
    return x * x;
}

int maxOf(int a, int b) {
    return (a > b) ? a : b;
}

2.3.2 void 函数 vs 返回值函数

void 函数:执行操作,不返回值

📄 查看代码:void 函数:执行操作,不返回值
// void 函数执行某个动作
void printLine(int n) {
    for (int i = 0; i < n; i++) {
        cout << "-";
    }
    cout << "\n";
}

// 调用 void 函数——直接调用,不要尝试接收返回值
printLine(10);    // 打印:----------
printLine(20);    // 打印:--------------------

返回值函数:计算并返回一个值

// 返回 x 的绝对值
int absoluteValue(int x) {
    if (x < 0) return -x;
    return x;
}

// 调用返回值函数——把结果存入变量或直接使用
int result = absoluteValue(-7);
cout << result << "\n";           // 7
cout << absoluteValue(-3) << "\n"; // 3(直接使用)

多个 return 语句

函数可以有多个 return 语句——执行到第一个就停止:

string classify(int n) {
    if (n < 0) return "负数";   // n < 0 时从这里退出
    if (n == 0) return "零";    // n == 0 时从这里退出
    return "正数";              // 其他情况从这里退出
}

cout << classify(-5) << "\n";   // 负数
cout << classify(0) << "\n";    // 零
cout << classify(3) << "\n";    // 正数

2.3.3 按值传递 vs 按引用传递

向函数传递变量时有两种方式。理解这一点至关重要。

按值传递(默认):函数得到一个副本

📄 查看代码:按值传递(默认):函数得到一个副本
void addOne_byValue(int x) {
    x++;  // 修改的是局部副本——原变量不变
    cout << "函数内部:" << x << "\n";  // 6
}

int main() {
    int n = 5;
    addOne_byValue(n);
    cout << "函数之后:" << n << "\n";   // 还是 5!原变量未改变
    return 0;
}

可以把它想成复印件:函数对复印件进行操作,对复印件的修改不影响原件。

按引用传递(&):函数操作原变量

📄 查看代码:按引用传递(&):函数操作原变量
void addOne_byRef(int& x) {  // & 表示「原变量的引用」
    x++;  // 直接修改原变量
    cout << "函数内部:" << x << "\n";  // 6
}

int main() {
    int n = 5;
    addOne_byRef(n);
    cout << "函数之后:" << n << "\n";   // 现在是 6!原变量被改变了
    return 0;
}

什么时候用哪种

用按传递,当……用按引用传递,当……
函数不应修改原变量函数需要修改原变量
小类型(int、double、char)需要返回多个值
你想要安全性(无副作用)大型对象(避免昂贵的复制)

通过引用返回多个值

C++ 函数只能 return 一个值。但可以通过引用参数「返回」多个值:

📄 C++ 函数只能 `return` 一个值。但可以通过引用参数「返回」多个值:
// 同时计算商和余数
void divmod(int a, int b, int& quotient, int& remainder) {
    quotient = a / b;
    remainder = a % b;
}

int main() {
    int q, r;
    divmod(17, 5, q, r);  // q 和 r 被函数修改
    cout << "17 / 5 = " << q << " 余 " << r << "\n";
    // 打印:17 / 5 = 3 余 2
    return 0;
}

2.3.4 递归

递归函数是调用自身的函数。它非常适合可以拆解成同类问题的更小版本的问题。

经典示例:阶乘

5! = 5 × 4 × 3 × 2 × 1 = 120
   = 5 × (4!)              ← 同样的问题,更小的输入!

💡 递归思维三步法:

  1. 找「自相似性」: 原问题能否拆解为同类型的更小问题?5! = 5 × 4!,4! 和 5! 是同一类型 ✓
  2. 确定边界条件: 最小的情况是什么?0! = 1,不能再拆解
  3. 写递推步骤: n! = n × (n-1)!,用更小的输入调用自身

这个思维过程在**图论算法(第 5.1 章)动态规划(第 6.1-6.3 章)**中会反复用到。

int factorial(int n) {
    if (n == 0) return 1;            // 边界条件:停止递归
    return n * factorial(n - 1);    // 递推步骤:缩小到更小的问题
}

追踪 factorial(4)

factorial(4)
= 4 * factorial(3)
= 4 * (3 * factorial(2))
= 4 * (3 * (2 * factorial(1)))
= 4 * (3 * (2 * (1 * factorial(0))))
= 4 * (3 * (2 * (1 * 1)))   ← 边界条件!
= 4 * (3 * (2 * 1))
= 4 * (3 * 2)
= 4 * 6
= 24  ✓

每个递归函数都需要:

  1. 边界条件 —— 停止递归(防止无限递归)
  2. 递推步骤 —— 用更小的输入调用自身

🐛 常见 Bug: 忘记边界条件 → 无限递归 → 「栈溢出」崩溃!


2.3.5 数组——固定大小的集合

🏠 邮箱类比

数组就像街道上一排邮箱:
- 所有邮箱大小相同(相同类型)
- 每个都有门号(下标,从 0 开始)
- 可以通过门号直接找到任意邮箱

Array Index Visual

图示:数组内存布局

Array Memory Layout

数组在内存中是连续存储的,每个元素紧挨着前一个,因此支持 O(1) 随机访问。

数组基础

📄 查看代码:数组基础
#include <bits/stdc++.h>
using namespace std;

int main() {
    // 声明一个有 5 个整数的数组(元素未初始化——是垃圾值!)
    int arr[5];

    // 逐一赋值
    arr[0] = 10;
    arr[1] = 20;
    arr[2] = 30;
    arr[3] = 40;
    arr[4] = 50;

    // 同时声明和初始化
    int nums[5] = {1, 2, 3, 4, 5};

    // 全部初始化为零
    int zeros[100] = {};          // 所有 100 个元素 = 0
    int zeros2[100];
    fill(zeros2, zeros2 + 100, 0); // 另一种方式

    // 访问和打印
    cout << arr[2] << "\n";       // 30

    // 遍历数组
    for (int i = 0; i < 5; i++) {
        cout << nums[i] << " ";   // 1 2 3 4 5
    }
    cout << "\n";

    return 0;
}

🐛 差一错误——头号数组 Bug

数组是从 0 开始索引的:如果你声明 int arr[5],合法下标是 0, 1, 2, 3, 4,不存在 arr[5]

📄 C++ 完整代码
int arr[5] = {10, 20, 30, 40, 50};

// 错误:循环从 i=0 到 i=5 包含——下标 5 不存在!
for (int i = 0; i <= 5; i++) {   // BUG:<= 5 应改为 < 5
    cout << arr[i];               // i=5 时崩溃或打印垃圾值
}

// 正确:循环从 i=0 到 i=4(i < 5 确保 i 永远到不了 5)
for (int i = 0; i < 5; i++) {    // i 的值:0, 1, 2, 3, 4 ✓
    cout << arr[i];               // 始终有效
}

这就是「差一错误」——越过末尾一个元素。这是竞赛编程中最常见的数组 bug。

🤔 为什么从 0 开始? C++ 从 C 继承了这个设计,而 C 的设计贴近硬件。下标实际上是从数组起点的偏移量。第一个元素在偏移量 0 处(从起点无偏移)。

大数组用全局变量

main 里的局部变量存在「栈」上,空间有限(约 1-8 MB)。竞赛编程中 N 最大可达 10^6,需要用全局数组(存在不同的内存区域,空间大得多):

📄 C++ 完整代码
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1000001;  // 最大长度 + 1(常见惯例)
int arr[MAXN];              // 全局声明——大数组安全
// 全局数组会自动初始化为 0!

int main() {
    int n;
    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> arr[i];
    }
    return 0;
}

专业技巧: 全局数组会自动初始化为 0。局部数组不会——它们在赋值前包含垃圾值!


2.3.6 常见数组算法

求和、最大值、最小值

📄 查看代码:求和、最大值、最小值
int n;
cin >> n;

vector<int> arr(n);    // 马上学 vector;用法与数组相同
for (int i = 0; i < n; i++) cin >> arr[i];

// 求和
long long sum = 0;
for (int i = 0; i < n; i++) sum += arr[i];
cout << "总和:" << sum << "\n";

// 最大值(初始化为第一个元素!)
int maxVal = arr[0];
for (int i = 1; i < n; i++) {
    if (arr[i] > maxVal) maxVal = arr[i];
}
cout << "最大值:" << maxVal << "\n";

// 最小值(同理)
int minVal = arr[0];
for (int i = 1; i < n; i++) {
    minVal = min(minVal, arr[i]);  // min() 是内置函数
}
cout << "最小值:" << minVal << "\n";

复杂度分析:

  • 时间:O(N)——每个算法只需一次遍历
  • 空间:O(1)——只需几个额外变量(不计输入数组本身)

翻转数组

int arr[] = {1, 2, 3, 4, 5};
int n = 5;

// 从两端向中间交换元素
for (int i = 0, j = n - 1; i < j; i++, j--) {
    swap(arr[i], arr[j]);  // swap() 是内置函数
}
// arr 现在是 {5, 4, 3, 2, 1}

复杂度分析:

  • 时间:O(N)——每对元素交换一次,共 N/2 次
  • 空间:O(1)——原地交换,不需要额外数组

二维数组

二维数组就像一张表格或网格,非常适合地图、棋盘、矩阵:

📄 二维数组就像一张表格或网格,非常适合地图、棋盘、矩阵:
int grid[3][4];  // 3 行 4 列

// 填入 i * 10 + j
for (int r = 0; r < 3; r++) {
    for (int c = 0; c < 4; c++) {
        grid[r][c] = r * 10 + c;
    }
}

// 打印
for (int r = 0; r < 3; r++) {
    for (int c = 0; c < 4; c++) {
        cout << grid[r][c] << "\t";
    }
    cout << "\n";
}

输出:

0   1   2   3
10  11  12  13
20  21  22  23

2.3.7 向量(vector)——动态数组

数组有一个大限制:大小必须在编译时确定(或事先声明得足够大)。向量解决了这个问题——在程序运行时可以按需增减大小。

数组 vs 向量对比

特性数组向量
大小编译时固定运行时可增减
读 N 个元素必须硬编码或用 MAXNpush_back(x) 自然适用
内存位置栈(快速,有限)堆(稍慢,无限制)
语法int arr[5]vector<int> v(5)
竞赛编程首选固定大小的简单情况大多数问题

向量基础

📄 查看代码:向量基础
#include <bits/stdc++.h>
using namespace std;

int main() {
    // 创建空向量
    vector<int> v;

    // 用 push_back 在末尾添加元素
    v.push_back(10);    // v = [10]
    v.push_back(20);    // v = [10, 20]
    v.push_back(30);    // v = [10, 20, 30]

    // 按下标访问(与数组相同,从 0 开始)
    cout << v[0] << "\n";     // 10
    cout << v[1] << "\n";     // 20

    // 常用函数
    cout << v.size() << "\n"; // 3(元素个数)
    cout << v.front() << "\n"; // 10(第一个元素)
    cout << v.back() << "\n";  // 30(最后一个元素)
    cout << v.empty() << "\n"; // 0(false——不为空)

    // 删除最后一个元素
    v.pop_back();   // v = [10, 20]

    // 清空所有元素
    v.clear();      // v = []
    cout << v.empty() << "\n"; // 1(true——现在为空)

    return 0;
}

创建带初始值的向量

vector<int> zeros(10, 0);       // 十个 0:[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
vector<int> ones(5, 1);         // 五个 1:[1, 1, 1, 1, 1]
vector<int> primes = {2, 3, 5, 7, 11};  // 从列表初始化
vector<int> empty;              // 空向量

遍历向量

📄 查看代码:遍历向量
vector<int> v = {10, 20, 30, 40, 50};

// 方法一:下标循环(与数组相同)
for (int i = 0; i < (int)v.size(); i++) {
    cout << v[i] << " ";
}
cout << "\n";

// 方法二:范围 for 循环(更简洁,推荐)
for (int x : v) {
    cout << x << " ";
}
cout << "\n";

// 方法三:引用范围 for(需要修改时使用)
for (int& x : v) {
    x *= 2;  // 将每个元素就地翻倍
}

🤔 为什么下标循环用 (int)v.size() v.size() 返回的是无符号整数。把 int i 和无符号值比较,C++ 有时会产生意外行为(尤其是 i 为负时)。转成 (int) 是安全习惯。

USACO 中使用向量的标准模式

int n;
cin >> n;

vector<int> arr(n);         // 创建大小为 n 的向量
for (int i = 0; i < n; i++) {
    cin >> arr[i];          // 读入每个位置
}

// 现在处理 arr...
sort(arr.begin(), arr.end());  // 升序排序

二维向量

int rows = 3, cols = 4;
vector<vector<int>> grid(rows, vector<int>(cols, 0));  // 3×4 的全 0 网格

// 访问:grid[r][c]
grid[1][2] = 42;
cout << grid[1][2] << "\n";  // 42

2.3.8 向函数传递数组和向量

数组

向函数传递数组时,函数收到的是第一个元素的指针。函数内部的修改会影响原数组:

📄 向函数传递数组时,函数收到的是第一个元素的指针。**函数内部的修改会影响原数组:**
void fillSquares(int arr[], int n) {  // 数组参数的 arr[] 语法
    for (int i = 0; i < n; i++) {
        arr[i] = i * i;   // 修改的是原数组!
    }
}

int main() {
    int arr[5] = {0};
    fillSquares(arr, 5);
    // arr 现在是 {0, 1, 4, 9, 16}
    for (int i = 0; i < 5; i++) cout << arr[i] << " ";
    cout << "\n";
    return 0;
}

向量

向量传递给函数时默认是复制(大向量很慢!)。用 & 按引用传递:

📄 向量传递给函数时默认是**复制**(大向量很慢!)。用 `&` 按引用传递:
// 按值传递——复制整个向量(大向量时很慢)
void printVec(vector<int> v) {
    for (int x : v) cout << x << " ";
}

// 按引用传递——不复制,可修改原向量(用于输出参数)
void sortVec(vector<int>& v) {
    sort(v.begin(), v.end());
}

// 按 const 引用传递——不复制,不可修改(只读时最佳)
void printVecFast(const vector<int>& v) {
    for (int x : v) cout << x << " ";
}

专业技巧: 对于只读(不修改)的向量参数,始终写 const vector<int>&。这既避免了复制,也向读者表明函数不会修改向量。


⚠️ 第 2.3 章常见错误

#错误示例错在哪里修复方法
1数组越界差一数组大小为 n 时访问 arr[n]合法下标是 0 到 n-1,arr[n] 越界i < n 而不是 i <= n
2忘记递归边界条件int f(int n) { return n*f(n-1); }永不停止,导致栈溢出崩溃加上 if (n == 0) return 1;
3递归传入非法参数(如负数)factorial(-1)边界条件只处理 n == 0;负值导致无限递归 → 栈溢出调用前确保输入在合法范围;或在函数入口加防御:if (n < 0) return -1;
4向量按值传递性能问题void f(vector<int> v)复制整个向量,N 大时极慢const vector<int>& v
5局部数组未初始化int arr[100]; sum += arr[50];局部数组不会自动清零,包含垃圾值= {} 初始化或使用全局数组
6main 内数组太大int main() { int arr[1000000]; }超过栈内存限制(通常 1-8 MB),程序崩溃把大数组放在 main 外(全局)
7函数定义在调用之后main 调用 square(5)square 定义在 main 下面编译器无法识别未定义的函数在 main 之前定义函数,或使用函数原型

本章总结

📌 核心要点

概念要点为什么重要
函数定义一次,随处调用减少重复代码,提高可读性
返回类型intdoubleboolvoid不同场景用不同返回类型
按值传递函数得到副本,原变量不变安全,无副作用
按引用传递(&函数操作原变量可修改原变量,避免复制大对象
递归函数调用自身,必须有边界条件分治、回溯、DP 的基础
数组固定大小,从 0 开始,O(1) 随机访问竞赛编程中最基本的数据结构
全局数组避免栈溢出,自动初始化为 0N 超过 10^5 时必须用全局数组
vector<int>动态数组,可变大小竞赛编程首选数据容器
push_back / pop_back在末尾增/删O(1) 操作,构建动态集合的主要方式
前缀和O(N) 预处理,O(1) 查询区间求和的核心技巧,第 3.2 章深入讲解

❓ 常见问题

Q1:数组和向量哪个更好?

A:竞赛编程中两者都常用。经验法则:大小固定且已知时,全局数组最简单;大小动态变化或需要传给函数时,用 vector。很多选手默认用 vector,因为它更灵活、更不容易出错。

Q2:递归深度有限制吗?会崩溃吗?

A:有。每次函数调用都在栈上分配空间,默认栈大小约 1-8 MB。实际上,约 10^4 到 10^5 层递归是安全的。超出后程序会以「栈溢出」崩溃。竞赛中如果递归深度可能超过 10^4,考虑改用迭代(循环)方式。

Q3:什么时候用按引用传递(&)?

A:两种情况:① 需要在函数内部修改原变量;② 参数是大对象(如 vectorstring),想避免复制开销。对于 intdouble 这样的小类型,复制开销可忽略不计,按值传递即可。

Q4:函数可以返回数组或向量吗?

A:数组不能直接返回,但 vector 可以!vector<int> solve() { ... return result; } 完全有效。现代 C++ 编译器会优化返回过程(称为 RVO),实际上不会复制整个向量。

Q5:为什么前缀和数组多一个索引?prefix[n+1] 而不是 prefix[n]

A:prefix[0] = 0 是一个「哨兵值」,使得公式 prefix[R+1] - prefix[L] 在所有情况下都有效。没有这个哨兵,查询 [0, R] 时 L=0 需要特殊处理。这是一个非常常见的编程技巧:用额外的哨兵值简化边界处理。

🔗 与后续章节的联系

  • 第 3.1 章(STL 核心用法)将介绍 sortbinary_searchpair 等工具,让你用一行代码完成本章手动实现的操作
  • 第 3.2 章(前缀和)将深入探讨题目 2.3.10 中引入的前缀和技术,包括二维前缀和和差分数组
  • 第 5.1 章(图的基础)将以 2.3.4 节的递归基础为起点,讲解 DFS 和 BFS 等图遍历算法
  • 第 6.1-6.3 章(动态规划):「把大问题拆解成小问题」的核心思想与递归密切相关;本章的递归思维是重要的基础
  • 本章学到的函数封装数组/向量操作将在后续每一章中持续使用

练习题


🌡️ 热身题


热身 2.3.1 — 平方函数 编写一个函数 int square(int x),返回 x²。在 main 中读取一个整数并打印其平方。

样例输入: 7样例输出: 49

💡 题解(点击展开)

思路: 把函数写在 main 之上,用输入调用它。

#include <bits/stdc++.h>
using namespace std;

int square(int x) {
    return x * x;
}

int main() {
    int n;
    cin >> n;
    cout << square(n) << "\n";
    return 0;
}

关键点:

  • 函数定义在 main 之上,编译器才能识别
  • return x * x;——C++ 计算 x * x 并返回结果
  • 若 x 可能很大(如最大 10^9),用 long long(x² 最大 10^18)

热身 2.3.2 — 两数之大 编写函数 int myMax(int a, int b),返回两个整数中较大的。在 main 中读取两个整数并打印较大值。

样例输入: 13 7样例输出: 13

💡 题解(点击展开)

思路: 比较 a 和 b,返回较大的那个。

#include <bits/stdc++.h>
using namespace std;

int myMax(int a, int b) {
    if (a > b) return a;
    return b;
}

int main() {
    int a, b;
    cin >> a >> b;
    cout << myMax(a, b) << "\n";
    return 0;
}

关键点:

  • C++ 有内置的 max(a, b) 函数——但自己写一遍有助于理解概念
  • 三目运算符替代方案:return (a > b) ? a : b;

热身 2.3.3 — 倒序数组 声明一个恰好有 5 个整数的数组:{1, 2, 3, 4, 5}。倒序打印(不需要输入)。

期望输出:

5 4 3 2 1
💡 题解(点击展开)

思路: 从下标 4 循环到 0(倒序)。

#include <bits/stdc++.h>
using namespace std;

int main() {
    int arr[5] = {1, 2, 3, 4, 5};

    for (int i = 4; i >= 0; i--) {
        cout << arr[i];
        if (i > 0) cout << " ";
    }
    cout << "\n";

    return 0;
}

关键点:

  • 从下标 n-1 = 4 循环到 0(包含),使用 i--
  • if (i > 0) cout << " " 避免末尾多一个空格——不过对 USACO 来说末尾空格通常可以接受

热身 2.3.4 — 向量求和 创建一个向量,用 push_back 依次加入 10、20、30、40、50,然后打印它们的总和。

期望输出: 150

💡 题解(点击展开)

思路: 创建空向量,push 5 个值,循环求和。

#include <bits/stdc++.h>
using namespace std;

int main() {
    vector<int> v;
    v.push_back(10);
    v.push_back(20);
    v.push_back(30);
    v.push_back(40);
    v.push_back(50);

    long long sum = 0;
    for (int x : v) {
        sum += x;
    }

    cout << sum << "\n";
    return 0;
}

关键点:

  • 范围 for for (int x : v) 遍历所有元素
  • 一行替代方案:accumulate(v.begin(), v.end(), 0LL)

热身 2.3.5 — Hello N 次 编写一个 void 函数 sayHello(int n),打印「Hello!」恰好 n 次。读取 n 后从 main 调用它。

样例输入: 3 样例输出:

Hello!
Hello!
Hello!
💡 题解(点击展开)

思路: 一个内含 for 循环的 void 函数。

#include <bits/stdc++.h>
using namespace std;

void sayHello(int n) {
    for (int i = 0; i < n; i++) {
        cout << "Hello!\n";
    }
}

int main() {
    int n;
    cin >> n;
    sayHello(n);
    return 0;
}

关键点:

  • void 表示函数不返回任何值——不需要 return 值;(可以用裸 return; 提前退出)
  • sayHello 参数中的 nmainn独立副本(按值传递)

🏋️ 核心练习题


题目 2.3.6 — 倒序输出 读取 N(1 ≤ N ≤ 100),然后读取 N 个整数,倒序打印。

样例输入:

5
1 2 3 4 5

样例输出: 5 4 3 2 1

💡 题解(点击展开)

思路: 存入向量,然后从最后一个下标到第一个倒序打印。

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n;
    cin >> n;

    vector<int> arr(n);
    for (int i = 0; i < n; i++) {
        cin >> arr[i];
    }

    for (int i = n - 1; i >= 0; i--) {
        cout << arr[i];
        if (i > 0) cout << " ";
    }
    cout << "\n";

    return 0;
}

关键点:

  • vector<int> arr(n) 创建大小为 n 的向量(初始全为零)
  • 就像数组一样读入 arr[i]
  • n-1 循环到 0(包含)打印

题目 2.3.7 — 动态平均值 读取 N(1 ≤ N ≤ 100),然后逐个读取 N 个整数。每读入一个整数后,打印当前已读所有整数的平均值(保留 2 位小数)。

样例输入:

4
10 20 30 40

样例输出:

10.00
15.00
20.00
25.00
💡 题解(点击展开)

思路: 维护累计和,每次新读入后除以已读个数。

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n;
    cin >> n;

    long long sum = 0;
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        sum += x;
        double avg = (double)sum / i;
        cout << fixed << setprecision(2) << avg << "\n";
    }

    return 0;
}

关键点:

  • 每次新元素读入后更新 sumi 是已读元素个数
  • (double)sum / i——先强转为 double 再除,得到小数结果
  • fixed << setprecision(2) 强制输出恰好 2 位小数

题目 2.3.8 — 频率统计 读取 N(1 ≤ N ≤ 100)个整数,每个整数在 1 到 10 之间。打印 1 到 10 中每个值出现的次数。

样例输入:

7
3 1 2 3 3 1 7

样例输出:

1 appears 2 times
2 appears 1 times
3 appears 3 times
4 appears 0 times
5 appears 0 times
6 appears 0 times
7 appears 1 times
8 appears 0 times
9 appears 0 times
10 appears 0 times
💡 题解(点击展开)

思路: 用数组(或向量)作为「计数器」——下标 1 到 10 存储对应值的计数。

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n;
    cin >> n;

    int freq[11] = {};  // 下标 0-10;我们用 1-10。全部初始化为 0。

    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        freq[x]++;    // 值 x 的计数加一
    }

    for (int v = 1; v <= 10; v++) {
        cout << v << " appears " << freq[v] << " times\n";
    }

    return 0;
}

关键点:

  • freq[x]++ 是非常常见的模式——用作为频率数组的下标
  • 声明 freq[11],下标 0-10,使 freq[10](表示值 10)有效
  • int freq[11] = {}——= {} 将所有元素初始化为零

题目 2.3.9 — 两数之和 读取 N(1 ≤ N ≤ 100)个整数和目标值 T。如果数组中任意两个不同元素相加等于 T,打印 YES,否则打印 NO

样例输入:

5 9
1 4 5 6 3

(N=5,T=9,然后是数组) 样例输出: YES(因为 4+5=9 或 3+6=9)

💡 题解(点击展开)

思路: 检查所有满足 i < j 的对 (i, j),若任意一对之和等于 T,打印 YES。

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n, t;
    cin >> n >> t;

    vector<int> arr(n);
    for (int i = 0; i < n; i++) cin >> arr[i];

    bool found = false;
    for (int i = 0; i < n && !found; i++) {
        for (int j = i + 1; j < n; j++) {
            if (arr[i] + arr[j] == t) {
                found = true;
                break;
            }
        }
    }

    cout << (found ? "YES" : "NO") << "\n";

    return 0;
}

关键点:

  • 内层循环从 j = i + 1 开始,避免同一元素使用两次,也避免重复检查相同的对
  • break + 外层循环的 && !found 条件确保找到匹配后立即停止
  • 这是 O(N²)——对 N ≤ 100 完全够用。N 最大 10^5 时,需要用集合(第 3.1 章)

题目 2.3.10 — 前缀和 读取 N(1 ≤ N ≤ 1000),然后读取 N 个整数。再读取 Q 个查询(1 ≤ Q ≤ 1000),每个查询有两个整数 L 和 R(0-indexed,包含两端)。对每个查询打印下标 L 到 R 的元素之和。

样例输入:

5
1 2 3 4 5
3
0 2
1 3
2 4

样例输出:

6
9
12
💡 题解(点击展开)

为什么不对每次查询直接求和? 暴力法:每次查询从 L 循环到 R,时间复杂度 O(N),所有查询总计 O(N×Q)。当 N=10^5,Q=10^5 时是 10^{10} 次操作——远超时限。

优化思路: 预处理一次,O(N);之后每次查询只需 O(1)。总计 O(N+Q),快得多!这就是前缀和的核心思想(第 3.2 章深入讲解)。

思路: 构建前缀和数组,prefix[i] = arr[0..i-1] 的和。则 L 到 R 的和 = prefix[R+1] - prefix[L],每次查询 O(1)。

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n;
    cin >> n;

    vector<long long> arr(n), prefix(n + 1, 0);

    for (int i = 0; i < n; i++) {
        cin >> arr[i];
        prefix[i + 1] = prefix[i] + arr[i];  // 构建前缀和
    }
    // prefix[0] = 0
    // prefix[1] = arr[0]
    // prefix[2] = arr[0] + arr[1]
    // prefix[i] = arr[0] + arr[1] + ... + arr[i-1]

    int q;
    cin >> q;
    while (q--) {
        int l, r;
        cin >> l >> r;
        // l 到 r(含)的和 = prefix[r+1] - prefix[l]
        cout << prefix[r + 1] - prefix[l] << "\n";
    }

    return 0;
}

关键点:

  • prefix[i] = 前 i 个元素之和(prefix[0] = 0 是哨兵)
  • arr[L..R] 的和 = prefix[R+1] - prefix[L]——减去 L 之前的部分
  • 用样例验证:arr=[1,2,3,4,5],prefix=[0,1,3,6,10,15]。查询 [0,2]:prefix[3]-prefix[0]=6-0=6 ✓

复杂度分析:

  • 时间:O(N + Q)——预处理 O(N) + 每次查询 O(1) × Q
  • 空间:O(N)——前缀和数组占 N+1 的空间

💡 暴力 vs 优化: 暴力 O(N×Q) vs 前缀和 O(N+Q)。N=Q=10^5 时,前者需 10^{10} 次操作(超时),后者只需 2×10^5 次操作(瞬间)。


🏆 挑战题


挑战 2.3.11 — 数组旋转 读取 N(1 ≤ N ≤ 1000)和 K(0 ≤ K < N),再读取 N 个整数。打印向旋转 K 位后的数组(最后 K 个元素移到最前面)。

样例输入:

5 2
1 2 3 4 5

样例输出: 4 5 1 2 3

💡 题解(点击展开)

思路: 新数组在位置 i 的元素原来在位置 (i - K + N) % N。等价地,从下标 N-K 开始打印,循环回绕。

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n, k;
    cin >> n >> k;

    vector<int> arr(n);
    for (int i = 0; i < n; i++) cin >> arr[i];

    // 从下标 (n - k) % n 开始打印 n 个元素,循环回绕
    for (int i = 0; i < n; i++) {
        int idx = (n - k + i) % n;
        cout << arr[idx];
        if (i < n - 1) cout << " ";
    }
    cout << "\n";

    return 0;
}

关键点:

  • 右旋 K 位:后 K 个元素先输出,再输出前 N-K 个
  • (n - k + i) % n 将新位置 i 映射到旧位置——% n 处理回绕
  • 验证:n=5,k=2。i=0: idx=(5-2+0)%5=3 → arr[3]=4。i=1: idx=4 → arr[4]=5。i=2: idx=0 → arr[0]=1。正确!

挑战 2.3.12 — 合并有序数组 读取 N₁,然后 N₁ 个已排序的整数;读取 N₂,然后 N₂ 个已排序的整数。打印合并后的有序数组。

样例输入:

3
1 3 5
4
2 4 6 8

样例输出: 1 2 3 4 5 6 8

💡 题解(点击展开)

思路: 双指针——一个指向每个数组的当前位置。每步取两个当前元素中较小的。

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n1;
    cin >> n1;
    vector<int> a(n1);
    for (int i = 0; i < n1; i++) cin >> a[i];

    int n2;
    cin >> n2;
    vector<int> b(n2);
    for (int i = 0; i < n2; i++) cin >> b[i];

    // 双指针合并
    int i = 0, j = 0;
    vector<int> result;

    while (i < n1 && j < n2) {
        if (a[i] <= b[j]) {
            result.push_back(a[i++]);  // 取 a,i 前进
        } else {
            result.push_back(b[j++]);  // 取 b,j 前进
        }
    }
    // 某个数组可能还有剩余元素
    while (i < n1) result.push_back(a[i++]);
    while (j < n2) result.push_back(b[j++]);

    for (int idx = 0; idx < (int)result.size(); idx++) {
        cout << result[idx];
        if (idx < (int)result.size() - 1) cout << " ";
    }
    cout << "\n";

    return 0;
}

关键点:

  • 双指针 ij 同步扫描数组 ab
  • 始终取当前较小的元素——维持有序性
  • while 循环结束后,某个数组可能还有剩余元素——直接追加

挑战 2.3.13 — 气味距离 (USACO Bronze 风格)

N 头奶牛站成一排,每头奶牛有位置 p[i] 和气味半径 s[i]。如果两头奶牛之间的距离不超过它们半径之和,它们就能互相闻到对方。读取 N,然后 N 对(位置,半径),打印能互相闻到的奶牛对数。

样例输入:

4
1 2
5 1
8 3
15 1

样例输出: 1

(对 (1,2):距离=|5-8|=3,半径和=1+3=4,3≤4,YES。其余对距离均超过半径和,总计 1。)

💡 题解(点击展开)

思路: 检查所有满足 i < j 的对,对每对计算距离并与半径和比较。

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n;
    cin >> n;

    vector<long long> pos(n), rad(n);
    for (int i = 0; i < n; i++) {
        cin >> pos[i] >> rad[i];
    }

    int count = 0;
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) {
            long long dist = abs(pos[i] - pos[j]);
            long long sumRad = rad[i] + rad[j];
            if (dist <= sumRad) {
                count++;
            }
        }
    }

    cout << count << "\n";
    return 0;
}

关键点:

  • 检查满足 i < j 的对,避免同一对计数两次
  • abs(pos[i] - pos[j]) 计算位置间的绝对距离
  • 使用 long long 以防位置和半径较大