数组
P5716 [深基3.例9] 月份天数
参考代码
#include <cstdio>
int main()
{
// 定义一个整型数组,预先存储平年时每个月的天数
// days[0] 对应 1 月,days[1] 对应 2 月,以此类推
int days[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int y, m;
scanf("%d%d", &y, &m);
// 判断是否为闰年
if (y % 4 == 0 && y % 100 != 0 || y % 400 == 0) {
days[1] = 29; // 如果是闰年,将 2 月份的天数(数组索引为 1)更新为 29
}
// 输出对应月份的天数
// 因为月份 m 是从 1 开始的,而数组索引是从 0 开始的,所以需要 m-1 作为索引
printf("%d\n", days[m - 1]);
return 0;
}
P1046 [NOIP2005 普及组] 陶陶摘苹果
参考代码
#include <cstdio>
int main()
{
int h[10]; // 创建一个大小为 10 的数组,用于存储 10 个苹果的高度
for (int i = 0; i < 10; i++) { // 循环 10 次,读取每个苹果的高度
scanf("%d", &h[i]); // 读入一个整数,并存入数组 h 的相应位置
}
int t;
scanf("%d", &t);
int ans = 0; // 记录能摘到的苹果数量
for (int i = 0; i < 10; i++) { // 循环 10 次,检查每个苹果是否能被摘到
if (t + 30 >= h[i]) ans++;
}
printf("%d\n", ans);
return 0;
}
P1089 [NOIP2004 提高组] 津津的储蓄计划
参考代码
#include <cstdio>
int main()
{
int b[13]; // 定义一个大小为13的数组,b[1]到b[12]用于存储1到12月的预算
for (int i = 1; i <= 12; i++) { // 循环读入12个月的预算
scanf("%d", &b[i]);
}
int save = 0; // 记录存在妈妈那里的总钱数
int money = 0; // 记录津津手中当前的现金
for (int i = 1; i <= 12; i++) { // 循环模拟1月到12月
money += 300; // 月初,妈妈给300元
money -= b[i]; // 月末,减去当月预算
if (money < 0) { // 判断钱是否够用
printf("%d\n", -i);
// 结束程序
return 0; // 使用 return 0 可以直接终止 main 函数
}
// 如果钱够用,计算可以存的整百元
// money / 100 得到百元的数量,再乘以 100
save += money / 100 * 100;
money = money % 100; // 更新手中的现金,只保留不足100元的部分
}
// 如果循环正常结束(12个月都没问题)
// 计算年末总金额
// money 是12月底手里剩下的钱
// save / 5 * 6 是妈妈返还的本金加20%的利息,避免了精度问题
int final_money = money + save / 5 * 6;
printf("%d\n", final_money); // 输出最终金额
return 0;
}
P1428 小鱼比可爱
可以使用两层循环来解决这个问题。外层循环遍历每一条鱼,从左到右(假设索引从 \(1\) 到 \(n\)),这个循环是用来确定当前正在为哪条鱼计算结果。内层循环对于外层循环选定的第 \(i\) 条鱼,需要检查它左边的所有鱼,所以内层循环需要遍历从第 \(1\) 条到第 \(i-1\) 条鱼。在内层循环中,将第 \(j\) 条鱼的可爱程度 \(a_j\) 与第 \(i\) 条鱼的可爱程度 \(a_i\) 进行比较。如果 \(a_j \lt a_i\),说明第 \(j\) 条鱼不如第 \(i\) 条鱼可爱,就用一个计数器来记录。当内层循环(对第 \(i\) 条鱼的检查)结束后,计数器的值就是第 \(i\) 条鱼左边比它可爱的鱼的数量,将这个值输出。外层循环继续,直到为所有的鱼都计算并输出了结果。
参考代码
#include <cstdio>
int main()
{
int n;
scanf("%d", &n);
// 定义一个大小为105的数组a,用于存储每条鱼的可爱程度
// 数组大小比题目数据范围(n<=100)稍大,是良好的编程习惯
int a[105];
// 循环读入n条鱼的可爱程度,并存入数组a
// 注意这里的循环是从1到n,所以数组的a[0]没有使用
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i <= n; i++) { // 外层循环:遍历每一条鱼,从第1条到第n条
int x = 0; // 为每条鱼i,初始化一个计数器x,用于记录它左边比它不可爱的鱼的数量
for (int j = 1; j < i; j++) { // 内层循环:遍历在鱼i左边的所有鱼,从第1条到第i-1条
if (a[j] < a[i]) { // 比较左边的鱼j和当前的鱼i的可爱程度
x++; // 如果左边的鱼j更不可爱,计数器x加1
}
}
// 内层循环结束后,x的值就是鱼i左边比它不可爱的鱼的总数
printf("%d ", x);
}
return 0;
}
P1427 小鱼的数字游戏
由于需要先接收完所有数字才能知道倒序是什么,所以不能边读边输出,正确的思路是“先存储,后输出”。
需要一个容器来存储所有输入的数字,数组是这里最简单直接的选择,可以声明一个足够大的数组(例如 a[105],因为题目保证数字个数不超过 \(100\))。
需要循环读取用户输入的数字,直到遇到 0 为止。可以使用一个 while 循环,每次循环读入一个数字。在循环内部,判断读入的数字是否为 0,如果是 0,说明循环结束,就跳出循环,如果不是 0,就将这个数字存入数组中,并用一个计数器记录当前已经存了多少个数字。
当输入循环结束后,已经将所有需要的数字按正序存储在了数组 a 中,并且通过计数器知道了有效数字的个数。接下来,再用一个 for 循环来输出。这个循环的迭代器 i 应该从最后一个有效元素的索引开始,一直递减到第一个有效元素的索引。在循环中,依次打印 a[i],这样就实现了倒序输出。
参考代码
#include <cstdio>
int main()
{
int x; // 临时变量,用于接收每次 scanf 读入的整数
int a[105]; // 定义一个数组,用于存储输入的数字,大小105足够,因为题目说不超过100个
int len = 0; // 计数器,记录存入数组的有效数字的个数
// 循环读取输入的整数
// scanf("%d", &x) 在成功读取一个整数时返回1,可以驱动while循环
while (scanf("%d", &x)) {
if (x == 0) { // 判断输入是否为结束标志 0
break; // 如果是0,则跳出while循环,结束输入阶段
}
// 如果不是0,则将该数字存入数组
// a[++len] = x; 等价于 len = len + 1; a[len] = x;
// 这样,数字被依次存放在 a[1], a[2], a[3]... 中
a[++len] = x;
}
// 倒序输出数组中的元素
// 循环从最后一个有效元素的索引 len 开始,递减到 1
for (int i = len; i >= 1; i--) printf("%d ", a[i]);
return 0;
}
标记数组与“桶”思想
在编程中,经常会遇到一类问题:给定一堆数字,然后需要反复、快速地判断某个数字是否在这堆数字里。
一个最直观的想法是:对于每一个要查询的数字,都把那堆数字从头到尾看一遍。如果找到了,它就存在;如果看到最后也没找到,它就不存在。
这个方法当然是可行的,但是,如果这堆数字非常多,或者需要查询的次数非常多,那么每次都从头到尾地“扫”一遍,效率就会非常低,有没有其他方法?
答案是肯定的,这就是数组标记法,也常被称为“桶思想”。
“标记法”的核心思想非常巧妙:用数组的下标来代表要查找的“数值”,而用该下标位置上存储的“值”来做一个标记。
这听起来有点抽象,用一个生活中的例子来理解:
想象有一排储物柜,编号从 1 到 100。现在拿到一个号码牌,上面写着数字“25”。为了记住你拥有“25”这个号码,你不需要把它记在脑子里,也不需要把它写在纸上,你只需要走到 25 号储物柜前,在里面放一个东西。
过了一会儿,有人问你:“你有 25 号号码牌吗?”你不需要翻遍所有口袋,只需要直接走到 25 号储物柜,打开一看,里面有个东西。于是你立刻知道,你拥有 25 号。
如果他问你:“你有 60 号号码牌吗?”你走到 60 号储物柜前,发现里面空空如也。于是你立刻知道,你没有 60 号。
在这个例子里:
- 储物柜的编号(1~100)就相当于数组的下标。
- 号码牌上的数字(25, 60)就相当于要处理的数值。
- 储物柜里是否放了东西,就相当于数组对应位置上的标记(比如存 1 代表有,存 0 代表没有,当然也可以是计数信息)。
通过这种方式,把“查找”问题转换成了一次“直接访问”,效率大大提升。
使用条件:这种方法非常高效,但也有一个明显的限制,待标记的对象范围不能太大,并且要能映射成非负整数,因为数组的下标是从 0 开始的,且数组不能无限大。
例题:P2550 [AHOI2001] 彩票摇奖
彩票有 7 个号码,范围是 1~33。我们会得到 7 个中奖号码,以及 \(n\) 张我们自己买的彩票。需要对每一张彩票,统计它和中奖号码有几个数字相同,并根据相同数字的个数确定奖项。
对于买的每一张彩票,核心任务是:判断这张彩票上的 7 个号码,哪些是中奖号码,而这正是前面提到的“判断一个数是否存在”的问题。
中奖号码的范围是 1~33,可以创建一个大小足够的数组,用来充当“储物柜”或“桶”。
先读取 7 个中奖号码,每读到一个号码,就在数组的对应位置上做一个标记。比如,把其值设为 1。
当处理买的彩票时,每读到一个号码,不再需要去遍历 7 个中奖号码。只需要直接检查数组对应位置上的值是否为 1,如果是,说明是一个中奖号码,否则不是。
通过这种方式,对一张彩票的 7 个号码的检查,只需要 7 次数组访问,非常高效。
参考代码
#include <cstdio>
// num 数组用作桶,标记哪些号码是中奖号码
// 数组大小设为35,因为号码范围是1-33,这样可以直接用号码作为下标
int num[35] = {0};
// award 数组也用了桶思想,下标代表奖项,值代表该奖项的数量
int award[10] = {0};
int main()
{
int n;
scanf("%d", &n);
// 步骤1:读取中奖号码,并进行“标记”
for (int i = 0; i < 7; i++) {
int id;
scanf("%d", &id);
num[id] = 1; // 将中奖号码在 num 数组中对应的位置标记为 1
}
// 步骤2:逐张处理购买的彩票
for (int i = 0; i < n; i++) {
int hit = 0; // 初始化当前彩票的命中号码数量
// 步骤3:对于彩票上的每个号码,进行快速查询
for (int j = 0; j < 7; j++) {
int id;
scanf("%d", &id);
if (num[id] == 1) { // 直接访问数组下标,判断是否为中奖号码
hit++; // 如果是中奖号码,命中数加1
}
}
// 根据命中数量,更新奖项统计(这里也用到了桶思想)
if (hit > 0) {
// 命中 hit 个号码,对应 7-hit 等奖
award[7 - hit]++;
}
}
// 依次输出特等奖到六等奖的中奖张数
for (int i = 0; i <= 6; i++) printf("%d ", award[i]);
printf("\n");
return 0;
}
习题:P1047 [NOIP2005 普及组] 校门外的树
解题思路
可以创建一个大小为 \(l+1\) 的数组,用数组的索引 \(i\) 代表路上的坐标 \(i\)。数组元素的值可以用来表示该坐标上是否有树,例如 \(1\) 表示有树,\(0\) 表示没有树。
最初,从 \(0\) 到 \(l\) 的每个整数坐标上都有一棵树。所以,可以将数组从 \(0\) 到 \(l\) 的所有元素都初始化为 \(1\)。
题目给出了 \(m\) 个区域,需要将这些区域内的树移除。可以遍历这 \(m\) 个区域,对于每个区域 \([u, v]\),将数组中索引 \(u\) 到 \(v\) 的所有元素都设置为 \(0\),表示这些位置的树被移除了。
最后,再次遍历数组,从 \(0\) 到 \(l\)。统计数组中值为 \(1\) 的元素的个数,这个总数就是最后剩下的树木数量。
参考代码
#include <cstdio>
int main()
{
int l, m;
int road[10005]; // 使用数组模拟马路,下标代表坐标,值代表是否有树
scanf("%d%d", &l, &m);
// 初始化,假设所有位置都有树
// 数组下标从 0 到 l,代表马路上的坐标 0 到 l
for (int i = 0; i <= l; i++) road[i] = 1; // 1 表示有树
for (int i = 0; i < m; i++) { // 循环 m 次,处理 m 个区域
int u, v; // 区域的起始和结束坐标
scanf("%d%d", &u, &v);
// 将该区域内的树移除
for (int j = u; j <= v; j++) road[j] = 0; // 0 表示没有树
}
int ans = 0; // 记录剩余树木的数量
// 遍历马路,统计剩余的树
for (int i = 0; i <= l; i++) {
if (road[i] == 1) { // 如果该位置有树
ans++; // 数量加一
}
}
printf("%d\n", ans); // 输出结果
return 0;
}
习题:P11227 [CSP-J 2024] 扑克牌
解题思路
问题的核心是计算缺少多少种不同的牌,一副完整的牌有 52 种,如果已经拥有的牌覆盖了 \(k\) 种不同的牌,那么就需要再借 \(52-k\) 张牌来补全。
需要注意的是,小 Q 给的 \(n\) 张牌里可能有重复的牌,例如两张“方片 Q”。对于凑成一副完整牌的目标来说,拥有两张“方片 Q”和拥有一张“方片 Q”的效果是一样的,都算作拥有了“方片 Q”这一种牌。因此,需要做的是统计小 Q 给的牌中,一共有多少种不同的牌。
参考代码
#include <iostream>
using namespace std;
// c[s][r] 用于记录花色为 s,点数为 r 的牌是否已经拥有。
// s: 0-方片D, 1-草花C, 2-红桃H, 3-黑桃S
// r: 1-A, 2-2, ..., 9-9, 10-T, 11-J, 12-Q, 13-K
bool c[4][14];
int main()
{
int n;
cin >> n; // 读取小 Q 拥有的牌的数量
// ans 初始化为 52,表示最初需要借 52 张牌才能凑齐一副完整的牌。
// 每当发现一张小 Q 拥有的、且之前未统计过的牌,ans 就减 1。
int ans = 52;
// 循环 n 次,读取每一张牌
for (int i = 1; i <= n; i++) {
char st, rk;
cin >> st >> rk; // 读取花色和点数
int s, r;
// 将花色字符转换为对应的数字索引 (0-3)
if (st == 'D') s = 0; // 方片
else if (st == 'C') s = 1; // 草花
else if (st == 'H') s = 2; // 红桃
else s = 3; // 黑桃
// 将点数字符转换为对应的数字索引 (1-13)
if (rk == 'A') r = 1; // A
else if (rk == 'T') r = 10; // 10
else if (rk == 'J') r = 11; // J
else if (rk == 'Q') r = 12; // Q
else if (rk == 'K') r = 13; // K
else r = (rk - '0'); // 数字牌 (2-9)
if (!c[s][r]) {
ans--;
c[s][r] = true;
}
}
// 输出最终需要借的牌数
printf("%d\n", ans);
return 0;
}
P5728 [深基5.例5] 旗鼓相当的对手
参考代码
#include <cstdio>
// 定义三个数组,分别存储n名学生的语文、数学、英语成绩
// 数组大小设为1005,略大于题目给出的最大n值1000,以防越界
int c[1005], m[1005], e[1005];
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; ++i)
scanf("%d%d%d", &c[i], &m[i], &e[i]);
int ans = 0; // 旗鼓相当的对手的对数
// 使用双重循环遍历所有可能的学生对 (i, j)
// 外层循环从第一个学生开始
for (int i = 0; i < n; ++i) {
// 内层循环从第 i 的下一个学生开始,以避免重复比较(如 (1,2) 和 (2,1))和自我比较(如 (1,1))
for (int j = i + 1; j < n; ++j) {
// 计算学生 i 和学生 j 的各科成绩差
int dc = c[i] - c[j];
int dm = m[i] - m[j];
int de = e[i] - e[j];
int dsum = (c[i] + m[i] + e[i]) - (c[j] + m[j] + e[j]); // 计算总分差
if (dc >= -5 && dc <= 5)
if (dm >= -5 && dm <= 5)
if (de >= -5 && de <= 5)
if (dsum >= -10 && dsum <= 10)
ans++;
}
}
printf("%d\n", ans);
return 0;
}
P5729 [深基5.例7] 工艺品制作
可以直接使用三维数组来模拟这个立方体。
参考代码
#include <cstdio>
// 定义一个三维数组来模拟立方体
// 数组大小设为25,略大于题目给出的最大尺寸20,以防止因坐标从1开始而导致的越界
// 全局数组会自动初始化为0,a[i][j][k] = 0 表示小方块存在
int a[25][25][25];
int main()
{
int w, x, h;
scanf("%d%d%d", &w, &x, &h);
int q;
scanf("%d", &q);
int cur = w * x * h; //计算初始的总体积
while (q > 0) { // 循环处理每一次切割操作
q--; // 循环次数减一
int x1, y1, z1, x2, y2, z2;
// 读取本次切割区域对角点坐标
scanf("%d%d%d%d%d%d", &x1, &y1, &z1, &x2, &y2, &z2);
// 使用三层循环遍历切割区域内的所有小方块
for (int i = x1; i <= x2; ++i)
for (int j = y1; j <= y2; ++j)
for (int k = z1; k <= z2; ++k) {
if (a[i][j][k] == 0) { // 检查这个小方块是否还存在
// 如果存在(值为0),说明这是第一次被切割
// 那么剩余的体积就要减1
cur--;
// 将该位置标记为已切割(值为1),避免重复计算
a[i][j][k] = 1;
}
}
}
printf("%d\n", cur); // 输出最终剩余的体积
return 0;
}
P1152 欢乐的跳
高效的方法是使用一个“标记”数组来记录哪些差值已经出现过。
参考代码
#include <cstdio>
#include <cmath>
using namespace std;
const int N = 1005;
int a[N]; // 存储输入的整数序列
bool vis[N]; // 标记数组,vis[i] = true 表示差值 i 出现过
int main()
{
int n; scanf("%d", &n);
// 为了方便处理 a[i] 和 a[i-1],这里使用 1-based 索引
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 2; i <= n; i++) { // 循环从第二个元素开始,计算与前一个元素的差值
int d = abs(a[i] - a[i - 1]); // 计算与相邻的元素的差的绝对值
if (d >= 1 && d < n) { // 判断差值是否在有效范围 [1, n-1] 内
vis[d] = true; // 如果是有效差值,则在标记数组中记录下来
}
}
// 检查是否所有需要的差值都已出现
bool ans = true; // 先假设序列是 "Jolly"
for (int i = 1; i < n; i++) { // 循环检查 1 到 n-1 是否都作为差值出现过
if (!vis[i]) { // 如果 vis[i] 为 false,说明差值 i 没有出现过
ans = false; // 那么确定序列不是 "Jolly"
break; // 已经确定不是,无需继续检查,直接退出循环
}
}
// 根据检查结果输出
if (ans) {
printf("Jolly\n");
} else {
printf("Not jolly\n");
}
return 0;
}
P1554 梦中的统计
参考代码
#include <cstdio>
int main()
{
int m, n;
scanf("%d%d", &m, &n);
// 定义一个大小为10的整型数组 cnt,用于统计数字 0 到 9 出现的次数
// 初始化所有元素为 0
int cnt[10] = {0};
for (int i = m; i <= n; i++) { // 外层循环:遍历从 m 到 n 的每一个整数
// 将当前要处理的数字 i 赋值给一个临时变量 t
// 这样做是为了在内层循环中修改 t 的值,而不会影响外层循环的变量 i
int t = i;
while (t > 0) { // 内层循环:分解数字 t,直到 t 变为 0
// t % 10:取出 t 的个位数、
// cnt[t % 10]++:将对应数字的计数器加 1
cnt[t % 10]++;
t = t / 10; // 去掉 t 的个位数,为下一次循环准备
}
}
for (int i = 0; i <= 9; i++) {
printf("%d ", cnt[i]);
}
printf("\n");
return 0;
}
P1614 爱与愁的心痛
参考代码 1
#include <cstdio>
const int N = 3e3+5;
int a[N];
int main()
{
int n, m; scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
// a[1]+...+a[m]
// a[2]+...+a[m+1]
// ...
// a[n-m+1]+...+a[n]
int ans = 300000;
for (int i = 1; i <= n-m+1; i++) {
int sum = 0;
for (int j = 1; j <= m; j++) {
sum+=a[i+j-1];
}
if (sum<ans) ans=sum;
}
printf("%d\n", ans);
return 0;
}
参考代码 2
下一个区间的和等于上一个区间的和减上一个区间的第一个元素,再加下一个区间的新增元素。
#include <cstdio>
const int N = 3e3+5;
int a[N];
int main()
{
int n, m; scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
// a[1]+...+a[m]
// a[2]+...+a[m+1]
// ...
// a[n-m+1]+...+a[n]
int sum = 0;
for (int i = 1; i <= m; i++) sum+=a[i];
int ans = sum;
for (int i = 2; i <= n-m+1; i++) {
sum = sum-a[i-1]+a[i+m-1];
if (sum<ans) ans=sum;
}
printf("%d\n", ans);
return 0;
}
时间复杂度是 \(O(n)\),而前一个程序的时间复杂度是 \(O(nm)\)。在 \(n\) 和 \(m\) 较大时,这个程序的效率远高于上一个程序。
习题:P6568 [NOI Online #3 提高组] 水壶
解题思路
目标是让某一个水壶的水量最大化,假设选择第 \(i\) 号水壶作为最终汇集点。由于水只能向右流,只能将 \(i\) 左边的水壶(\(1, 2, \dots, i-1\))里的水汇集到 \(i\) 中。有 \(k\) 次操作,为了让 \(i\) 号水壶的水最多,应该汇集与它相邻的、左边的水壶,因为这样最“划算”(操作次数少)。要将 \(i-1\) 号的水倒入 \(i\) 号,需要 1 次操作。要将 \(i-2\) 号的水倒入 \(i\) 号,需要 \(2\) 次操作(\(i-2 \rightarrow i-1 \rightarrow i\))。要将 \(i-k\) 号的水倒入 \(i\) 号,需要 \(k\) 次操作。因此,利用最多 \(k\) 次操作,可以将 \(i\) 号水壶左边的 \(k\) 个水壶(即 \(i-k, i-k+1, \dots, i-1\) 号)的水全部汇集到 \(i\) 号水壶中。这样,就把 \(k+1\) 个连续水壶(从 \(i-k\) 到 \(i\))的水汇集到了一起。问题就转化成了:在 \(n\) 个数中,求连续 \(k+1\) 个数的最大和是多少,于是计算过程类似上一题。
参考代码
#include <cstdio>
const int N = 1000005;
int a[N];
int main()
{
int n, k; scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
// 计算 a[1] 到 a[k+1] 的总和
int sum = 0;
for (int i = 1; i <= k + 1; i++) {
sum += a[i];
}
int ans = sum; // 初始化最大和 ans 为前 k+1 个元素的和
for (int i = 2; i <= n - k; i++) {
// 高效更新 sum
// sum + a[i+k]:加上新进入的元素 a[i+k]
// - a[i-1]:减去离开的元素 a[i-1]
sum += a[i + k] - a[i - 1];
if (sum > ans) ans = sum; // 比较并更新最大和
}
printf("%d\n", ans);
return 0;
}
习题:P5638 【CSGRound2】光骓者的荣耀
解题思路
反向思考,无论如何,都需要从城市 1 走到城市 n,\(总时间 = (不使用传送器的总时间) - (使用传送器节省的时间)\)。为了让最终时间最短,必须让节省的时间最多。
问题转化为:如何节省最多时间?不使用传送器时,总时间是所有路段 \(a_1, a_2, \dots, a_{n-1}\) 的时间之和。当在城市 \(i\) 使用传送器,可以跳到城市 \(i+k\),这个过程跳过了从城市 \(i\) 到 \(i+1\),再到 \(i+2\),……,最后到 \(i+k\) 的所有路段。这些被跳过的路段是 \(a_i, a_{i+1}, \dots, a_{i+k-1}\),共 \(k\) 个连续的路段。因此,节省的时间就是这 \(k\) 个连续路段的时间之和。
问题就转化成了:在所有路段 \(a_i\) 中,找出连续 \(k\) 个路段的最大和,这个最大和就是能节省的最多时间,计算方法同上道题。
参考代码
#include <cstdio>
#include <algorithm>
using std::max;
using ll = long long; // 使用类型别名,ll 代表 long long,防止整数溢出
const int N = 1e6 + 5;
ll a[N];
int main()
{
int n, k; scanf("%d%d", &n, &k);
ll sum = 0;
for (int i = 1; i < n; i++) {
scanf("%lld", &a[i]); sum += a[i];
}
// 需要的时间=原来的总时间-传送省下的时间
// 传送省下的时间即为长度为k的连续区间总和
ll t = 0;
for (int i = 1; i <= k; i++) {
t += a[i];
}
ll save = t;
for (int i = k + 1; i < n; i++) {
t = t - a[i - k] + a[i];
save = max(save, t);
}
printf("%lld\n", sum - save);
return 0;
}
P2911 [USACO08OCT] Bovine Bones G
参考代码
#include <cstdio>
int main()
{
// 定义一个大小为 100 的数组 cnt,用于统计每个和出现的次数
// 数组索引代表和的值,元素值代表该和出现的频率
// s1+s2+s3 最大为 20+20+40=80,所以大小100足够
// 初始化所有元素为 0
int cnt[100] = {0};
int s1, s2, s3;
scanf("%d%d%d", &s1, &s2, &s3);
// 使用三层嵌套循环,模拟所有可能的掷骰子结果
for (int i = 1; i <= s1; ++i) // i:第一个骰子的点数
for (int j = 1; j <= s2; ++j) // j:第二个骰子的点数
for (int k = 1; k <= s3; ++k) // k:第三个骰子的点数
cnt[i + j + k]++; // 对于每一种组合 (i, j, k),将其和对应的计数器加一
// 初始化 ans 为可能的最小和 (1+1+1=3)
// 初始化 mx 为 0,用于记录当前找到的最大频率
int ans = 3, mx = 0;
for (int i = 3; i <= s1 + s2 + s3; ++i) { // 遍历所有可能的和,从最小的 3 到最大的 s1+s2+s3
if (cnt[i] > mx) { // 如果当前和 i 的出现次数 cnt[i] 大于已知的最大次数 mx
mx = cnt[i]; // 更新最大次数
ans = i; // 更新结果为当前和 i
}
}
printf("%d\n", ans);
return 0;
}
P5731 [深基5.习6] 蛇形方阵
可以采用“逐层填充”的方法来构建这个方阵,核心思想是一圈一圈地填数。
整个方阵可以看作是由多个同心方环组成的,像洋葱一样。从最外层的方环开始,顺时针填充数字,然后进入内一层方环,继续顺时针填充,直到所有数字填完。一个 \(n \times n\) 的方阵,总共需要填充 \(\lceil n/2 \rceil\) 个完整的方环。
对于第 \(i\) 个方环(从 \(1\) 开始,代表最外层),它的填充过程分为四步,对应四条边。可以通过四个独立的循环,并精确控制循环的起止边界,代码可以依次完成这四步,从而填满一整个方环。
参考代码
#include <cstdio>
const int N = 10;
int a[N][N];
int main()
{
int n; scanf("%d", &n);
int number = 0; // 计数器,用于生成要填充的数字 (1, 2, 3, ...)
// 一圈一圈填数,n/2圈
for (int i = 1; i <= n / 2; i++) {
// 左上角(i,i) 右下角(n-i+1,n-i+1)
// 向右 (i,i)->(i,n-i)
for (int j = i; j <= n - i; j++)
a[i][j] = ++number;
// 向下 (i,n-i+1)->(n-i,n-i+1)
for (int j = i; j <= n - i; j++)
a[j][n-i+1] = ++number;
// 向左 (n-i+1,n-i+1)->(n-i+1,i+1)
for (int j = n-i+1; j>=i+1; j--)
a[n-i+1][j] = ++number;
// 向上 (n-i+1,i)->(i+1,i)
for (int j = n-i+1; j>=i+1; j--)
a[j][i] = ++number;
}
// 如果n是奇数,还有一个中心点需要填数
if (n%2==1) a[n/2+1][n/2+1]=++number;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) printf("%3d", a[i][j]);
printf("\n");
}
return 0;
}
除了把方阵看作一层层的环,也可以模拟一个“画笔”在网格中行走并写下数字。画笔从左上角出发,一直向右走,直到撞到边界或已经画过的格子,然后顺时针转向,继续行走,如此反复,直到填满整个方阵。
参考代码
#include <cstdio>
int main()
{
int n;
scanf("%d", &n);
int s[10][10] = {0}; // 定义并初始化二维数组,0表示该格子未被填充
// (x, y) 是当前画笔的位置
int x = 1;
int y = 1;
// dx, dy 是方向向量
// dx[0], dy[0] -> 右;dx[1], dy[1] -> 下;dx[2], dy[2] -> 左;dx[3], dy[3] -> 上
int dx[4] = {0, 1, 0, -1};
int dy[4] = {1, 0, -1, 0};
int d = 0; // 右0下1左2上3,当前方向为右
s[1][1] = 1; // 将第一个数字 1 填入起始位置 (1,1)
int num = 2; // 准备要填入的下一个数字
while (num <= n * n) { // 当还有数字需要填充时,循环继续
// 计算预备要走的下一个格子的坐标 (xx, yy)
int xx = x + dx[d];
int yy = y + dy[d];
// 判断下一个格子是否合法:在边界内(1到n)且未被填充(值为0)
if (xx >= 1 && xx <= n && yy >= 1 && yy <= n && s[xx][yy] == 0) {
// 如果合法,则移动到该格子
x = xx;
y = yy;
s[x][y] = num; // 填入数字
num++; // 准备下一个数字
} else {
d = (d + 1) % 4; // 如果不合法(撞墙或已填充),则改变方向(顺时针转90度)
}
}
// 循环输出整个方阵
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
printf("%3d", s[i][j]); // "%3d" 格式化输出:每个数字占3个字符宽度,右对齐
}
printf("\n"); // 每行结束后换行
}
return 0;
}
P5732 [深基5.习7] 杨辉三角
杨辉三角是一个经典的数学结构,它的每一行都遵循特定的规律。
每一行的第一个数总是 1,每一行的最后一个数也总是 1。
对于第 \(i\) 行(\(i \gt 1\))的第 \(j\) 个数(\(j \gt 1\) 且 \(j \lt i\)),它的值等于它正上方的数(第 \(i-1\) 行的第 \(j\) 个数)与它左上方的数(第 \(i-1\) 行的第 \(j-1\) 个数)之和,也就是 \(a_{i,j}=a_{i-1,j-1}+a_{i-1,j}\)。、
参考代码
#include <cstdio>
int main()
{
int n;
// 定义一个二维数组 a,用于存储杨辉三角
// 数组大小设为 25×25,足够处理 n <= 20 的情况
int a[25][25] = {0};
scanf("%d", &n);
for (int i = 1; i <= n; i++) { // 外层循环:逐行生成杨辉三角,从第1行到第n行
a[i][1] = 1; // 设置边界条件:每行的第一个数是1
a[i][i] = 1; // 设置边界条件:每行的最后一个数是1
// 计算中间的数
// 内层循环:从第2列开始,到第 i-1 列结束
// a[i][j] 的值等于它上一行(i-1)的两个数之和
for (int j = 2; j <= i - 1; j++)
a[i][j] = a[i - 1][j] + a[i - 1][j - 1];
}
// 循环输出整个杨辉三角
for (int i = 1; i <= n; i++) { // 外层循环:控制行数
for (int j = 1; j <= i; j++) { // 内层循环:控制列数,第 i 行有第 i 个数
printf("%d ", a[i][j]);
}
printf("\n"); // 每行结束后换行
}
return 0;
}
P1789 [Mc生存] 插火把
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
int mp[105][105]; // 定义一个全局的二维数组来表示地图,0表示黑暗,1表示光明
int main()
{
int n, m, k;
scanf("%d%d%d", &n, &m, &k); // 读取地图大小、火把数量、萤石数量
// 处理火把
for (int i = 0; i < m; ++i) {
int x, y;
scanf("%d%d", &x, &y);
mp[x][y] = 1; // 标记火把本身的位置
// 标记火把的照明范围
for (int j = max(x - 2, 1); j <= min(x + 2, n); ++j) mp[j][y] = 1;
for (int j = max(y - 2, 1); j <= min(y + 2, n); ++j) mp[x][j] = 1;
for (int a = max(x - 1, 1); a <= min(x + 1, n); ++a)
for (int b = max(y - 1, 0); b <= min(y + 1, n); ++b)
mp[a][b] = 1;
}
// 处理萤石
for (int i = 0; i < k; ++i) {
int x, y;
scanf("%d%d", &x, &y);
// 标记萤石的照明范围,即以(x,y)为中心的5×5区域
for (int a = max(x - 2, 1); a <= min(x + 2, n); ++a)
for (int b = max(y - 2, 1); b <= min(y + 2, n); ++b)
mp[a][b] = 1;
}
int ans = 0;
// 统计黑暗格子的数量
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
if (mp[i][j] == 0) ++ans;
printf("%d\n", ans); // 输出最终答案
return 0;
}
习题:P2615 [NOIP2015 提高组] 神奇的幻方
参考代码
#include <cstdio>
int ans[50][50]; // 定义一个全局二维数组来存储幻方,大小50×50足够
int main()
{
int n;
scanf("%d", &n); // 读取幻方的大小
// 初始化:将 1 放在第一行的中间
int x = 1, y = (n + 1) / 2; // (x, y) 记录当前最后一个填充数字的位置
ans[x][y] = 1; // 填入数字 1
for (int i = 2; i <= n * n; ++i) { // 循环填充数字 2 到 n*n
// 根据题目描述的四条规则,判断下一个数字 i 的位置
if (x == 1 && y < n) { // 规则1:上一个数在第一行,但不在最后一列
x = n; // 移动到最后一行
++y; // 移动到右边一列
} else if (y == n && x > 1) { // 规则2:上一个数在最后一列,但不在第一行
y = 1; // 移动到第一列
--x; // 移动到上一行
} else if (x == 1 && y == n) { // 规则3:上一个数在第一行最后一列(右上角)
++x; // 移动到正下方
} else if (x != 1 && y != n) { // 规则4:上一个数在中间区域(不在第一行也不在最后一列)
// 检查右上方是否已被填充
if (ans[x - 1][y + 1] == 0) { // 0 表示未填充
// 子规则4a:右上方未填充,则移动到右上方
--x;
++y;
} else ++x; // 子规则4b:右上方已填充,则移动到正下方
}
ans[x][y] = i; // 在计算出的新位置 (x, y) 填入当前数字 i
}
// 循环输出整个幻方
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) printf("%d ", ans[i][j]);
printf("\n"); // 每行结束后换行
}
return 0;
}
P2141 [NOIP2014 普及组] 珠心算测验
注意题目问的是 “其中有多少个数,恰好等于集合中另外两个(不同的)数之和?”。
可以枚举每个数,去验证能否通过另外的两个数把它加出来。
参考代码
```cpp
#include <cstdio>
const int N = 105;
int a[N];
int main()
{
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
int ans = 0;
for (int i = 1; i <= n; i++) {
// 验证a[i]是否能被另外两个数加出来
bool ok = false;
for (int j = 1; j <= n; j++) {
for (int k = j + 1; k <= n; k++) {
if (a[i] == a[j] + a[k]) {
ok = true; break;
}
}
if (ok) break;
}
if (ok) ans++;
}
printf("%d\n", ans);
return 0;
}
</details>
更进一步地,由于给出的正整数取值范围在 $10000$ 以内,因此也可以先处理出任意取两个数能加出哪些值来,从而快速验证某个数是否能被另外两个数加出来。
<details>
<summary>参考代码</summary>
```cpp
#include <cstdio>
const int N = 105;
const int A = 10005;
bool vis[A];
int a[N];
int main()
{
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
int sum = a[i] + a[j];
if (sum < A) vis[sum] = true; // 超出值域的情况不需要考虑
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (vis[a[i]]) ans++;
}
printf("%d\n", ans);
return 0;
}
P1904 天际线
记录下轴上每个点的楼房高度的最大值,每个转折点的出现是由于最大高度的变化,所以最后将坐标从左往右枚举一遍,如果该点的高度与前一个点有差距,就说明是转折点,需要输出
考虑如下情况:
如果有两个相邻的楼房三元组,高度信息相同,前者的右端点坐标和后者的左端点坐标刚好差 1,此时这两个点高度没有发生变化,但这两个点都是转折点
针对这个问题,我们可以将原坐标体系放大到 2 倍,让原来的 x 和 x+1 在新坐标体系下分别为 2x 和 2x+2,这样 2x 和 2x+1 之间以及 2x+1 与 2x+2 之间都会存在高度差,从而解决了上面提到的问题。
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXN = 20005;
int H[MAXN];
int main()
{
int l, h, r;
while (scanf("%d%d%d", &l, &h, &r) != -1) {
for (int i = l * 2; i <= r * 2 ; i++) H[i] = max(H[i], h);
}
for (int i = 2; i < MAXN; i++)
if (H[i] != H[i - 1]) printf("%d %d ", i / 2, H[i]);
return 0;
}
习题:P9752 [CSP-S 2023] 密码锁
解题思路
题目给出了 \(n\) 个“结果状态”,要求找“初始状态”。可以反向思考:对于每一个给定的状态,通过一次操作能变成哪些“初始状态”?如果一个“初始状态”可以由所有 \(n\) 个给定的状态通过一次操作得到,那么这个“初始状态”就是一个可能的正确密码。
参考代码
#include <cstdio>
const int N = 10;
int cnt[N][N][N][N][N], a[N];
int main()
{
int n; scanf("%d", &n);
for(int i = 1; i <= n; i++) { // 循环读取 n 个给定的状态
for (int j = 1; j <= 5; j++) scanf("%d", &a[j]);
// 生成所有可能的“前驱”状态
for (int j = 1; j <= 9; j++) { // 遍历转动的幅度
// 1. 转动一个拨圈
for (int k = 1; k <= 5; k++) { // 遍历每个拨圈
a[k] = (a[k] + j) % 10; // 计算前驱状态的数字
cnt[a[1]][a[2]][a[3]][a[4]][a[5]]++;
a[k] = (a[k] + 10 - j) % 10; // 恢复原始数字
}
// 2. 转动两个相邻拨圈
for (int k = 1; k <= 4; k++) { // 遍历每对相邻的拨圈
a[k] = (a[k] + j) % 10; a[k + 1] = (a[k + 1] + j) % 10;
cnt[a[1]][a[2]][a[3]][a[4]][a[5]]++;
a[k] = (a[k] + 10 - j) % 10; a[k + 1] = (a[k + 1] + 10 - j) % 10; // 恢复原始数字
}
}
}
int ans = 0;
// 遍历所有可能的密码状态,统计符合条件的密码数量
for (int i1 = 0; i1 <= 9; i1++)
for (int i2 = 0; i2 <= 9; i2++)
for (int i3 = 0; i3 <= 9; i3++)
for (int i4 = 0; i4 <= 9; i4++)
for (int i5 = 0; i5 <= 9; i5++) {
// 如果一个密码是所有 n 个状态的前驱,它的计数值就等于 n
if (cnt[i1][i2][i3][i4][i5] == n) ans++;
}
printf("%d\n", ans);
return 0;
}
习题:CF1313C1 Skyscrapers (easy version)
解题思路
最终的高度序列从左往右看,一定是先非递减,然后达到一个峰值,再非递增,它不能先下降再上升。
那么整个高度序列 \(a\) 必然会有一个“最高点”(或者多个并列的最高点),称这个最高点的位置为 \(p\)。在 \(p\) 的左边(从 \(1\) 到 \(p\)),高度序列必须是非递减的,即 \(a_1 \le a_2 \le \cdots \le a_p\)。在 \(p\) 的右边(从 \(p\) 到 \(n\)),高度序列必须是非递增的,即 \(a_p \ge a_{p+1} \ge \cdots \ge a_n\)。为了使总高度最大,应该让每栋楼都尽可能高。对于左边的非递减序列,\(a_i\) 的高度不仅受 \(m_i\) 限制,还受它右边的 \(a_{i+1}\) 限制(\(a_i \le a_{i+1}\))。所以,\(a_i\) 的最大值是 \(\min(m_i, a_{i+1})\)。同理,对于右边的非递增序列,\(a_i\) 的最大值是 \(\min(m_i, a_{i-1})\)。
既然最终的建筑序列必然有一个最高点 \(p\),而不能确定这个最高点在哪里,干脆枚举所有可能的位置 \(i\)(从 \(1\) 到 \(n\))作为这个最高点。对于每一个假设的最高点 \(i\),可以计算出在这种情况下能达到的最大总高度。假设第 \(i\) 栋楼就是最高点,为了总高度最大,让它的高度达到上限,即 \(a_i = m_i\)。从 \(i-1\) 向 \(1\) 遍历,对于第 \(j\) 栋楼,用上面推出的式子计算出它的最大高度,最高楼右侧的楼的最大高度同理。对每个 \(i\) 都计算出高度总和,并记录下哪个 \(i\) 能够产生最大的总高度,这个 \(i\) 就是真正的最高点位置。
找到最优的最高点位置之后,重新构造一次最终的高度序列即可。
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
using ll = long long;
const int N = 1005;
int m[N];
int main()
{
int n; scanf("%d", &n); // 读取地的数量
for (int i = 1; i <= n; i++) scanf("%d", &m[i]); // 读取每块地的层数限制
ll ans = 0; // 用于存储找到的最大总高度
int maxi = 0; // 用于存储产生最大总高度时的最高点位置
for (int i = 1; i <= n; i++) { // 外层循环:枚举每一个位置 i 作为可能的最高点
ll sum = m[i]; // 当前总高度,初始化为假设的最高点的高度
// 向左扩展计算高度和
int l = m[i]; // 用于记录向左扩展时,右边楼的高度
for (int j = i - 1; j >= 1; j--) {
l = min(l, m[j]); // 第 j 栋楼的高度不能超过 m[j] 和它右边的楼高
sum += l; // 累加到总和
}
// 向右扩展计算高度和
int r = m[i]; // 用于记录向右扩展时,左边楼的高度
for (int j = i + 1; j <= n; j++) {
r = min(r, m[j]); // 第 j 栋楼的高度不能超过 m[j] 和它左边的楼高
sum += r; // 累加到总和
}
if (sum > ans) { // 如果当前假设产生的总高度 sum 更大,则更新最优解
ans = sum; // 更新最大总高度
maxi = i; // 记录这个最优的最高点位置
}
}
// 根据找到的最优最高点 maxi,重新构造最终的高度序列
// 注意:这里直接修改了原数组 m 来存储最终结果 a
for (int i = maxi - 1; i >= 1; i--) m[i] = min(m[i], m[i + 1]); // 从最高点向左修正高度
for (int i = maxi + 1; i <= n; i++) m[i] = min(m[i], m[i - 1]); // 从最高点向右修正高度
for (int i = 1; i <= n; i++) printf("%d%c", m[i], i == n ? '\n' : ' '); // 输出最终的高度序列
return 0;
}
STL vector
动态数组是大小可以在运行过程中改变的数组。在 C++ 中最流行的动态数据是 vector,可以像普通数组一样使用它。
下面的代码创建了一个空的 vector 然后向里面添加了三个元素:
vector<int> v;
v.push_back(3); // [3]
v.push_back(2); // [3,2]
v.push_back(5); // [3,2,5]
此时,可以像普通数组一样访问里面的元素:
printf("%d %d %d\n", v[0], v[1], v[2]); // 3 2 5
函数 size 返回 vector 的元素数量。下面的代码遍历整个 vector 并输出每个元素:
for (int i = 0; i < v.size(); i++) {
printf("%d\n", v[i]);
}
一种更简洁的遍历方式如下:
for (int x : v) {
printf("%d\n", x);
}
这种 for 循环被称为 range-based for,每次循环它会按顺序把容器中的元素赋值给循环变量。更进一步,如果不想手动写出容器中元素的类型,可以使用 auto 关键字自动推导元素类型:
for (auto x : v) { // 这里的v因为是vector<int>,所以auto会推导出int
printf("%d\n", x);
}
注意 size 的返回值类型是 size_t,是一种无符号整型,无符号整型不能表示负数。因此对于下面的代码:
#include <iostream>
#include <vector>
using std::cout;
using std::vector;
int main()
{
vector<int> v;
cout << v.size() << "\n"; // 0
cout << v.size() - 1 << "\n"; // 18446744073709551615
cout << int(v.size()) - 1 << "\n"; // -1
return 0;
}
至于 -1 为什么会变成 18446744073709551615,这和整数在计算机中的存储方式有关,会在后面讲到。这告诉我们如果想对 size 的返回结果涉及减法运算,可以先将其做类型转换,之后再做减法。这一点在倒序遍历 vector 时很重要:
for (int i = int(v.size()) - 1; i >= 0; i--) {
printf("%d\n", v[i]);
}
上面的代码如果没有将 v.size() 转换成 int 类型,就会在 vector 为空时导致死循环。
函数 back 返回 vector 中的最后一个元素,函数 pop_back 删除最后一个元素:
vector<int> v; v.push_back(5); v.push_back(2);
printf("%d\n", v.back()); // 2
v.pop_back();
printf("%d\n", v.back()); // 5
下面的代码在创建 vector 时用五个元素初始化:
vector<int> v = {2, 4, 2, 5, 1};
另一种创建 vector 的同时给元素赋初始值的方式:
vector<int> v1(10); // 大小为10,每个元素初始值为0
vector<int> v2(10, 5); // 大小为10,每个元素初始值为5
函数 resize(n) 用于将调整 vector 的大小调整至 n。如果 n 小于当前的大小,会保留前 n 个元素,销毁多出来的部分。如果 n 大于当前的大小,会在末尾补足若干个元素使得新的大小变成 n,如果调用的是 resize(n,val),会用 val 来补足,如果不指定 val,就用默认的初始化元素。
vector<int> v;
for (int i = 1; i < 10; i++) v.push_back(i);
v.resize(5); // [1,2,3,4,5]
v.resize(8, 100); // [1,2,3,4,5,100,100,100]
v.resize(12); // [1,2,3,4,5,100,100,100,0,0,0,0]
习题:P5727 【深基5.例3】冰雹猜想
解题思路
注意 \(n \le 100\) 只是一开始的 \(n\),变化过程中可能会涉及不止 \(100\) 个中间数,所以不能只开大小 \(100\) 的数组。这里可以使用 vector 来记录中间的数,输出时倒过来输出即可。
#include <cstdio>
#include <vector>
using std::vector;
int main()
{
int n; scanf("%d", &n);
vector<int> ans; // 定义一个里面元素是int类型的动态数组
// 一般不用vector<char> vector<bool>
// 此时是没有ans[0]...的
while (n!=1) {
// 把n添加进ans
ans.push_back(n);
if (n%2==0) {
n/=2;
} else {
n = 3 * n + 1;
}
}
// 把n添加进ans
ans.push_back(n);
// ans.size()
// 尽量不要对size()直接运算,比如 xxx.size()-1
// for (int i=0;i<ans.size();i++) { printf("%d ",ans[i]); }
// for (int i=ans.size()-1;i>=0;i--) { printf("%d ",ans[i]); }
// 尽量不要像上面这样写ans.size()-1
int len = ans.size();
for (int i=len-1; i>=0; i--) {
printf("%d ", ans[i]);
}
return 0;
}
例题:P3613 【深基15.例2】寄包柜
超市里有 \(n \ (n \le 10^5)\) 个寄包柜。每个寄包柜格子数量不一,第 \(i\) 个寄包柜有 \(a_i \ (a_i \le 10^5)\) 个格子,不过并不知道各个 \(a_i\) 的值。对于每个寄包柜,格子编号从 \(1\) 开始,一直到 \(a_i\)。现在有如下 \(q\) 次操作:
1)1 i j k:在第 \(i\) 个柜子的第 \(j\) 个格子存入物品 \(k \ (0 \le k \le 10^9)\)。当 \(k=0\) 时,说明清空该格子。
2)2 i j:查询第 \(i\) 个柜子的第 \(j\) 个格子中的物品是什么,保证查询的柜子存过东西。
已知超市里共计不会超过 \(10^7\) 个寄包格子,\(a_i\) 是确定然而未知的,但是保证一定不小于该柜子存物品请求的格子编号的最大值。当然,也有可能某些寄包柜中一个格子都没有。
分析:可以建立一个二维数组,s[i][j] 记录第 \(i\) 个柜子中第 \(j\) 个格子中的物品。根据本题的数据规模,需要定义一个大小为 \(10^5 \times 10^5\) 的 int 数组(\(4 \times 10^{10}\) 字节,大约 40GB),显然会超出内存限制。
这里可以用 vector 来解决这个问题。寄包柜最多 \(10^5\) 个,所以可以开一个 \(10^5\) 的数组,但是由于每个寄包柜的格子数不一定,因此数组中的每个元素(每个寄包柜)用一个 vector 来存储,这样就解决了空间的问题。
#include <cstdio>
#include <vector>
using std::vector;
const int N = 1e5 + 5;
vector<int> v[N];
int main()
{
int n, q; scanf("%d%d", &n, &q);
for (int i=1;i<=q;i++) {
int op; scanf("%d", &op);
if (op==1) {
int x,y,z; scanf("%d%d%d", &x,&y,&z);
// resize()调整动态数组大小
if (v[x].size()<=y) v[x].resize(y+1);
v[x][y]=z;
} else {
int x,y; scanf("%d%d",&x,&y);
printf("%d\n", v[x][y]);
}
}
return 0;
}
如果要定义由 10 个动态数组组成的一个二维数组,可以写为 vector<int> v[10]。甚至动态数组还可以嵌套,定义一个两个维度都不定长的二维数组 vector<vector<int>> v。

浙公网安备 33010602011771号