数组

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

posted @ 2023-07-24 13:37  RonChen  阅读(110)  评论(0)    收藏  举报