分类 OI 下的文章

字符串及文件读写

-1 检查浏览器渲染数学公式情况

因为笔者发布文章当天,本博客仍然存在 Latex 可能无法渲染的情况,下面是一个 Latex 示范:

$x$

  • 如果正常显示为衡水体 x,则代表读者的浏览器已经成功渲染了 Latex
  • 如果显示为 带有美元符号的 x 即 $x$(已除去换行符),则代表读者的浏览器没有成功渲染 Latex,读者可能需要刷新重试,如果刷新多次仍然无法渲染,请尝试换一个浏览器/网络环境

0 框架

你好,这里是 https://www.tropical-fish.cn/,本专题主要讲解 字符串及文件读写,感谢 深入浅出程序设计竞赛(基础篇) 提供的重要支持

这是 C++ 语法基础 的重要补充部分,包括了在 CCF 及其他系列比赛 获得 $\text{非}0$ 分数的方法,注意,这不是危言耸听

$$ \begin{aligned} &\textbf{字符串} \begin{cases} &\text{char}\\ &\text{string}\\ &\text{freopen} \end{cases} \end{aligned} $$

如上面的框架,笔者会介绍

  • 字符串的存储
  • 字符串的处理方法
  • STL 字符串
  • 文件输入输出

不废话了,现在开始!

1 char 字符数组

字符数组的 ASCII 本质与常用字符表

字符数组本质上和整数数组并没什么太大的区别,整数数字每一个下标存的是数字,字符数字存的是 字符(其实是 ASCII 码对应的数字),将这些字符存到数组里,便成为了一串字符,即 字符串,下面这张表是 ASCII 表,它们分别将 数字 对应了 字符,读者并不需要记忆全部的内容,仅需要记忆关键的键值,例如 0、A、a、 (空格),它们相对重要

DecCharDecCharDecCharDecCharDecCharDecCharDecCharDecChar
1SOH17DLE33!49165A81Q97a113q
2STX18DC234"50266B82R98b114r
3ETX19DC335#51367C83S99c115s
4EOT20DC436$52468D84T100d116t
5ENQ21NAK37%53569E85U101e117u
6ACK22SYN38&54670F86V102f118v
7BEL23ETB39'55771G87W103g119w
8BS24CAN40(56872H88X104h120x
9TAB25EM41)57973I89Y105i121y
10LF26SUB42*58:74J90Z106j122z
11VT27ESC43+59;75K91[107k123{
12FF28FS44,60<76L92\108l124``
13CR29GS45-61=77M93]109m125}
14SO30RS46.62>78N94^110n126~
15SI31US47/63?79O95_111o127DEL
16DLE32 48064@80P96 ` 112p0NUL

图片

其中的第 $0 \sim 31 \& 127$ 号字符是控制字符,第 $32$ 号字符是空格

在 OI 的场景下,一般只有第 $32 \sim 126$ 号元素起到了相对关键的作用

【题目样本 1.1】P5733 自动修正

题目描述

https://www.luogu.com.cn/problem/P5733

大家都知道,一些办公软件有自动将小写字母转换为大写的功能

输入一个长度不超过 $100$ 且不包括空格的字符串。要求将该字符串中的所有小写字母转换成大写字母并输出

例如输入 Luogu4!, 输出 LUOGU4!

做法 1

分析

既然单个字符可以使用 char 类型存储,那么存储一串字符就可以使用数组,我们可以定义一个数组 $s[]$,其中每一个下标都是字符类型,即 char s[100];,这样的 字符数组 就叫 字符串

读入字符串的办法大同小异

  1. 使用 scanf("%s",s);读入一个字符串,其中 %s 代表数据类型,即字符串,s 是定义的字符数组的名字
    至于为什么不需要取地址符 &,因为 s 是一个数组,这里的 s 在大多数表达式中会 自动转换为指向数组首元素的指针,即 &s[0],类型是 char*,仅此而已
  2. 使用 cin>>s;

请 特别注意 这一点,这两种读入方法只能读到空格、换行符、EOF(文章后面会有讲解),至于输入想要包含空格、换行符又想读入到同一个字符数组中,需要使用其他办法,文章后面同样会有讲解

参考代码

这段代码摘取至 深入浅出

#include <iostream>
#include <cstdio>
using namespace std;
int main() {
    char s[110];
    scanf("%s", s); // 读入这个字符串,还可以使用 cin>>s; 语句
    for (int i = 0; s[i] != '\0'; i++) // s[i] != '\0' 也有别的方法获取,具体会在文章后面提到
        if ('a' <= s[i] && s[i] <= 'z') /* 如果这个字符在'a'到'z'中间,说明是小写字母 */
            s[i] -= 'a' - 'A'; // 把这个字母转换成对应的大写字母,减去偏移量
    printf("%s\n", s); // 输出这个字符串,还可以使用 cout<<s<<endl; 语句
    return 0;
}
ASCII 码偏移量

这本身并不是一个特有名词,反倒是一个通俗的用语,它的意思由 Deepseek V4 Pro 提供:利用 ASCII 码表中字母的连续编码,通过加减一个固定值(偏移量)来实现大小写转换或字符映射

至于能否听懂,那就因人而异了,下面是一段解释:

如果你认真看了上面的 ASCII 表,你应该会发现 小写字母的顺序和大写字母的顺序分别是按照字母表的顺序排列的,a-A 是小写字母和对应大写字母的 ASCII,这被通俗地认为为 偏移量,这在其他的场景中也有体现,为了缩短篇幅,这里不再阐释,请 特别注意,小写字母的 ASCII 码相对于大写字母大

那么就很好说了,上面的代码利用 if ('a' <= s[i] && s[i] <= 'z') 判断是否是小写字母,如果是,减去偏移量,这样得到的字母就是大写字母了

字符串存储与 \0 结束标记

这段字符串在字符数组中的存储方式如下图

s[0]s[1]s[2]s[3]s[4]s[5]s[6]s[7]
76 'L'117 'u'111 'o'103 'g'117 'u'52 '4'33 '!'0 '\0'

$s[]$ 中的每一个元素,都存储了一个不超过 $127$ 的整数,它们分别对应了 ASCII 编码中的字符

最开始的字符存在 $s[0]$,这个字符串虽然有且仅有 $7$ 个字符,但它却占用了 $8$ 个下标,即 $s[0] \sim s[7]$,字符串的末尾,存了一个特殊的字符 结束标记字符,它是 \0 ,也可能会有 \n 换行符,划掉的原因是因为笔者暂时没有找到任何明确的证据,这个结束标记被用于提示一个字符串的结束位置,它会在修改字符串的时候自动调整,一个例子,我们在用 cout<<s 的时候,结束标记就会告诉 cout 字符串已经没了,提一嘴,类似这样的 “特殊字符”,还有好几个,例如 \n(ASCII 表中可能被表示为 10,划掉的原因同上)

[collapse status="true" title="如何表示 \?"]

#include <bits/stdc++.h>
using namespace std;
int main(){
    cout<<"\\";
}

cout 内的第一个 \ 是转义符,转义后面的字符,因为第二个 \ 转义了 ",为了避免第二个 \ 被转义,所以只好用第一个 \ 转义第二个 \,这样第二个 \ 就不会转义 " 了

控制台输出:

PS D:\Users\Lenovo\Downloads\output> & .\'test.exe'
\

[/collapse]

做法 2

分析

当然,也不用一次直接读入整个字符串,可以每次只读入一个字符,判断是否需要处理,(处理后)输出这个字符即可

这里可以使用 getchar() 函数获取输入中的一个字符,如果你具有好奇心,你可以打开下面的折叠框

[collapse status="true" title="getchar() 的返回类型"]
getchar() 被定义于 cstdio 头文件,因此你需要在使用的时候导入这个头文件

答案:getchar() 返回 int 类型

至于为什么不返回 char,这是因为它要正确处理文件结尾标识符 EOF,后面你就能看见了,马上

因此,尽管你定义了一个 char 类型的变量,调用 getchar() 的时候他会自动根据 ASCII 表转换成 char 类型

这是一个例子:

#include <bits/stdc++.h>
using namespace std;
int main(){
    char s;
    cout<<"请键入一个字符:";
    s=getchar();
    cout<<"您键入的字符是:"<<s;
}
PS D:\Users\Lenovo\Downloads\output> & .\'test.exe'
请键入一个字符:s
您键入的字符是:s

[/collapse]

相应的,putchar() 用于输出一个字符

参考代码
#include <iostream>
#include <cstdio>
using namespace std;
int main() {
    char s;
    while (1) {
        s = getchar(); // 每次调用 getchar() 函数,读入一个字符
        if (s == EOF)
            break;
        if ('a' <= s && s <= 'z') // 如果这个字符是小写字母
            s += 'A' - 'a'; // 把它转换成大写字母,这样写和上面是一样的
        putchar(s); // 调用 putchar() 函数,输出一个字符
    }
    return 0;
}
输入结束和 EOF

运行程序,结果发现无论输入什么,程序都没有反应,这是因为程序不认为输入已经结束了,继续在等待输入

遇到这种情况,输入完字符串后,按一下 Ctrl+Z 组合键,再按一次回车,就可以完成读入了

程序中读入一个字符都会判断是否读完了整个文件,如果文件被读完了,那么 getchar() 函数会返回 EOF(一个特殊的常量),即 End of File,这标志着读入已经结束了

在控制台中可以使用 Ctrl+Z 组合键(Windows 下)或者 Ctrl+D 组合键(Linux 下)来输入 EOF 标记提示程序输入已经完毕

至于一些教材使用的 gets() 函数将字符串读入字符数组,由于存在字符数组越界的风险, 已经不再建议使用,新的 C++11 标准更是删除了这个函数

而输出一个字符串还可以使用 puts() 方法,同时会自动输出换行,这倒是还能使用

如果您了解 HUSTOJ,您应该会遇到 #define gets(S) fgets(S,sizeof(S),stdin),仅仅提一嘴

【题目样本 1.2】P1914 凯撒密码

题目描述

https://www.luogu.com.cn/problem/P1914

凯撒密码是由原文字符串(由不超过 $50$ 个小写字母组成)中每个字母向后移动 $n$ 位形成的

z 的下一个字母是 a,如此循环

给出 $n$ 和移动前的原文字符串,请求出密码

分析

你可以导入这个字符串,一个一个处理,然后输出

你需要注意,你不能直接给每一位加上 $n$ 然后输出,因为可能会溢出,举一个例子,一个字符串是 $\text{z}$,$n=5$,如果直接加上答案就是 $\text{DEL}$($127$),如果不清楚这是什么,请翻阅前面的 ASCII 表,很明显答案是错的

所以,我们应该使用 s[i]-'a' 来计算和 $\text{a}$ 的偏移量,然后加上 $n$,得到目标字母的位置,一个例子,$\text{b}$ 这个字母移动 $4$ 位,就是第 $1$ 个字母($\text{a}$ 是第 $0$ 个字母)向右移动 $4$ 位,是第五个字母,即 $\text{f}$

为了要求这个位置始终在 $0 \sim 25$ 之间,我们应当把上一段计算除的结果对 $26$ 取模,后面还需要再加上 $\text{a}$,以还原为字母,记 s[i]-'a' 为 $t$,则计算的答案为 $(t \bmod 26)+\text{a}$

警告:读者应当理解 $a$ 与 $\text{a}$ 的区别,第一个指的是字母,第二个指的是值

参考代码

#include <iostream>
#include <cstdio>
using namespace std;
int main() {
    int n;
    char s[60];
    scanf("%d %s", &n, s);    // 读入字符串
    for (int i = 0; s[i] != '\0'; i++)
    putchar((s[i] - 'a' + n) % 26 + 'a');    // 计算偏移量并还原
    return 0;
}

【题目样本 1.3】P1125 [NOIP 2008 提高组] 笨小猴

题目描述

给出一个单词(由不超过 $100$ 个小写字母组成),假设 $maxn$ 是单词中出现次数最多的字母的出现次数,$minn$ 是单词中出现次数最少的字母的出现次数,如果 $maxn-minn$ 是一个质数,那么笨小猴就认为这是个 $\text{Lucky Word}$,输出 Lucky Word,然后在第二行输出 maxn-minn 的值;否则输出 No Answer,第二行输出 0

分析

考虑 计数排序思想,读入一个字母,用 $f[]$ 数组记录 $a \sim z$ 字母出现的数量,可以转换成 $0 \sim 25$ 的数字但是直接存更方便,$f[]$ 数组的大小为 $129$,这是 ASCII 表 非拓展字符的值域,随后使用 打擂台 思想寻找出现 次数最多的字母 和 非 $0$ 的最少,判断差是否为质数

bool isPrime(int n) { // 判断质数
    // 小于2的数不是质数
    if (n < 2) {
        return false;
    }
    
    // 从2到n-1逐个试除
    for (int i = 2; i < n; i++) {
        if (n % i == 0) {
            return false;  // 能被整除,是合数
        }
    }
    
    return true;  // 都不能整除,是质数
}

[collapse status="false" title="判断质数极限方法"]

inline uint64_t isqrt_u64(uint64_t n) { // 判断质数:小数字用试除(快),大数字用 Miller-Rabin(更快)
    if (n < 2) return n;
    uint64_t x = n;
    uint64_t y = (x + 1) >> 1;
    while (y < x) {
        x = y;
        y = (x + n / x) >> 1;
    }
    return x;
}

inline uint64_t mod_mul(uint64_t a, uint64_t b, uint64_t mod) {
    return (__uint128_t)a * b % mod;
}

inline uint64_t mod_pow(uint64_t a, uint64_t d, uint64_t mod) {
    uint64_t res = 1;
    while (d) {
        if (d & 1) res = mod_mul(res, a, mod);
        a = mod_mul(a, a, mod);
        d >>= 1;
    }
    return res;
}

bool isPrime(uint64_t n) {
    if (n < 2) return false;
    if (n == 2 || n == 3 || n == 5 || n == 7) return true;
    if ((n & 1) == 0 || n % 3 == 0 || n % 5 == 0 || n % 7 == 0) return false;
    
    // 小数字用试除(更快)
    if (n < 1000000000ULL) {
        uint64_t limit = isqrt_u64(n);
        for (uint64_t i = 11; i <= limit; ) {
            if (n % i == 0) return false;
            if (n % (i + 2) == 0) return false;
            if (n % (i + 6) == 0) return false;
            if (n % (i + 8) == 0) return false;
            if (n % (i + 12) == 0) return false;
            if (n % (i + 18) == 0) return false;
            if (n % (i + 20) == 0) return false;
            if (n % (i + 26) == 0) return false;
            i += 30;
        }
        return true;
    }
    
    // 大数字用 Miller-Rabin
    uint64_t d = n - 1;
    int s = 0;
    while ((d & 1) == 0) {
        d >>= 1;
        s++;
    }
    
    static const uint64_t bases[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};
    for (uint64_t a : bases) {
        if (a >= n) continue;
        uint64_t x = mod_pow(a, d, n);
        if (x == 1 || x == n - 1) continue;
        bool composite = true;
        for (int r = 1; r < s; r++) {
            x = mod_mul(x, x, n);
            if (x == n - 1) {
                composite = false;
                break;
            }
        }
        if (composite) return false;
    }
    return true;
}

具体内容不解释,自行搜索

上述内容取自 Deepseek V4 Pro
[/collapse]

如果有需要,可以封装成 namespace

参考代码

同样取自 深入浅出

#include <stdio.h>
#include <iostream.h>
#include <string.h>
using namespace std;

int main() {
    char a[110];
    int ans[26] = {0};              // ans[0] 到 ans[25] 分别代表 'a' 到 'z' 出现的次数,注意要初始化
    int l, mmax, mmin, delta;       // 字符长度,出现次数最多的字母出现次数和出现次数最少的字母出现次数,以及差值

    scanf("%s", a);
    l = strlen(a);

    for (int i = 0; i < l; i++) {
        ans[a[i] - 'a']++;          // 统计增加某个字母的数量
    }

    mmax = 0;
    mmin = 10000;                   // 最大最小值初始化

    for (int i = 0; i < 26; i++) {  // 寻找每个字母的最大值和最小值
        if (ans[i] > mmax) {
            mmax = ans[i];          // 如果超过最大值
        }
        if (ans[i] != 0 && ans[i] < mmin) {
            mmin = ans[i];          // 如果小于最小值,但是不能为 0
        }
    }

    delta = mmax - mmin;

    if (delta == 0 || delta == 1) { // 质数特判
        printf("No Answer\n0\n");
        return 0;
    }

    for (int h = 2; h * h <= delta; h++) {  // 枚举质数
        if (delta % h == 0) {
            printf("No Answer\n0\n");       // 直接输出答案并退出程序
            return 0;
        }
    }

    printf("Lucky Word\n%d\n", mmax - mmin);
    return 0;
}

string 头文件

这是一个前所未有的新头文件,它包含了一些新的头文件

这里有一些新的用法,供读者参考:

  • size_t strlen(const char *s); 求 char 数组的长度,将要求长度的数组放进 () 之中,在大部分情况下被 typedef 定义为 unsigned int,这也是为什么你在用 for(int i=0;i<strlen(s);i++) 会报 Warn 的原因,类型不同
  • char *strcpy(char *dest, const char *src); 将 *str 复制到 *dest,即复制字符串,返回值一般无用
  • int strcmp(const char *s1, const char *s2); 判断两个字符数组是否相同,下面是返回值及意义

    • $=0$ 是 $s1$ 和 $s2$ 完全相同
    • $\lt 0$ 是 $s1$ 小于 $s2$
    • $\gt 0$ 是 $s1$ 大于 $s2$

字符数组不能直接复制一个字符串,因为字符数组中的数组名也只是一个数组名,上面提供的函数可能会有作用

但是 char a[100]="TropicalFish"; 是合法的

【题目样本 1.4】P1957 口算练习题

题目描述

https://www.luogu.com.cn/problem/P1957

王老师收集了 $i(i \leq 50)$ 道学生经常做错的口算题,并且想整理编写成一份练习

王老师希望尽量减少输入的工作量,比如 $5+8$ 的算式最好只输入 5 和 8,输出的结果要尽量详细以方便后期排版使用

对于上述输入进行处理后,输出 5+8=13 以及该算式的总长度 6

输入数据第 $1$ 行是 $i$,接着的 $i$ 行是需要输入的算式,每行可能有 $3$ 个数据或两个数据

  • 若该行是 $3$ 个数据,则第一个数据表示运算类型,a 表示加法运算,b 表示减法运算,c 表示乘法运算,接着的两个数据表示参加运算的运算数
  • 若该行是两个数据,则表示本题的运算类型与上一题的运算类型相同,而这两个数据为运算数

分析

笔者写到这里的时候,就比较随意了,因为本题没有多大的实际意义,一般地,它只是作为一道相对复杂的应用题/模拟题

在本题中 switch-case 相对于 if-else 更加方便

sscanf 与 sprintf

他们的作用都是从字符串中读入/写出

请区别 scanf 与 printf

参考代码

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int main() {
    int n, a, b, c;
    char last, s[20], ans[20];
    scanf("%d\n", &n);
    while (n--) {
        fgets(s, sizeof(s), stdin); // 读入一行
        if (s[0] == 'a' || s[0] == 'b' || s[0] == 'c')
            last = s[0], s[0] = ' '; // 获取计算符号,并替换为空格
        sscanf(s, "%d %d", &a, &b); // 从这个字符串里面读入两个数 a 和 b 
        switch (last) {
            case'a': c = a + b; sprintf(ans, "%d+%d=%d", a, b, c); break; // +
            case'b': c = a - b; sprintf(ans, "%d-%d=%d", a, b, c); break; // -
            case'c': c = a * b; sprintf(ans, "%d*%d=%d", a, b, c); break; // ×
        }
        printf("%s\n%d\n", ans, strlen(ans)); // 输出
    }
    return 0;
}

上面的代码同样改编于 深入浅出

2 string 字符串

从 C 风格字符串到 C++ STL 的 string 类型

$$ \left\{ \begin{array}{l} \textbf{C++ 标准库} \\ \left\{ \begin{array}{l} \textbf{STL(标准模板库)} \\ \left\{ \begin{array}{l} \text{容器} \left\{ \begin{array}{l} \text{vector} \\ \text{list} \\ \text{map} \\ \boxed{\text{string}} \quad \text{(核心问题)} \end{array} \right. \\ \text{迭代器} \\ \text{算法} \\ \text{适配器} \\ \text{分配器} \\ \text{函数对象} \end{array} \right. \\ \text{其他组件} \left\{ \begin{array}{l} \text{I/O 流} \\ \text{异常处理} \\ \text{内存管理} \end{array} \right. \end{array} \right. \end{array} \right. $$

上面的思维导图由 Deepseek V4 Pro 生成,它体现了 string 与 STL 的主要关系

很明显的,使用 C 语言的字符数字有很多不便(char 是属于 C 语言的),比如 不能:

  • 弹性变化长度
  • 直接赋值
  • 直接复制
  • 有数组越界的风险(gets)

好消息是,C++ 中提出了 STL 这一概念,即 标准模板库,将很多有用的功能进行了封装,直接就可以用,不需要重新开发这些功能,我们为 2017 年的 OI 选手默哀,已经封装的功能包括但不限于:

  • 栈(stack)
  • 队列(queue)
  • 排序(sort)

总的来说,封装了容器、算法、其他功能,现在,我们会使用 string 解决字符串问题

【题目样本 2.1】P5015 [NOIP 2018 普及组] 标题统计

题目描述

https://www.luogu.com.cn/problem/P5015

凯凯刚写了一篇美妙的作文,请统计这篇作文的标题中有多少个字符

注意:标题中可能包含大、小写英文字母、数字字符、空格和换行符,且字符串中的字符和空格数总和不超过 $5$

统计标题字符数时,空格和换行符不计算在内

分析

因为使用 cin 读入字符串时会忽略空格,并且读到 空格 或者 换行符 就停止了,所以可以将读入写进 while() 中,每次读入一个字符串,把长度加入答案即可

至于读入整行,会在 【题目样本 2.3】 中给出方法

参考代码

#include <iostream>
#include <string>
using namespace std;

int main() {
    string s;
    int ans = 0;
    while (cin >> s)
        ans += s.length();
    cout << ans << endl;
    return 0;
}

深入浅出

string:字符串的加强版数据类型

这里没使用 char,而是使用了一种新的数据类型 string,一个 string 类型的变量可以用来存储一个字符串,还可以把这个字符串当成一个整体的处理,string 的功能包括但不限于:

  • 赋值
  • 拼接
  • 裁剪

char 毕竟是个数组,能做到这些就很烦了

输人时使用 cin 语句,不断读入字符串

当发现读入文件读完后(遇到 EOF,可以按 Ctrl+Z 组合键),cin>>s 本身就会返回 $0$,中断 while 语句,结束读入

这里的 $s$ 变量可以被认为是一个“加强版“的字符数组,可以使用 s.length()(s.size()) 来直接查询字符串 $s$ 的长度,也可以和字符数组一样使用 $s[0]$ 来查询这个字符串最开头的字符是什么

更厉害的是,string 类型的字符串可以直接拿来赋值、拼接操作,比如 s=s+s 就是将两个字符串 $s$ 拼接在一起,其结果赋值回 $s$ 的意思,请注意,这里提到的代码操作的时间复杂度是 $O(n)$,建议写 s+=s(s.append(s) 会在 【题目样本 2.2】 提到)

上述描述操作的便利性可不是字符数组可以比较的

【题目样本 2.2】P5734 文字处理软件

题目描述

https://www.luogu.com.cn/problem/P5734

现在需要开发一款文字处理软件

最开始时输入一个字符串(不超过 $100$ 个字符)作为初始文档

可以认为文档开头是第 $0$ 个字符,需要支持以下操作

  1. 1 str: 后接插入,在文档后面插入字符串 $\text{str}$,并输出文档的字符串
  2. 2 a b: 截取文档部分,只保留文档中从第 $a$ 个字符起 $b$ 个字符,并输出文档的字符串
  3. 3 a str: 插入片段,在文档中第 $a$ 个字符前面插入字符串 $\text{str}$,并输出文档的字符串
  4. 4 str: 查找子串,查找字符串 $\text{str}$ 在文档中最先出现的位置并输出;如果找不到输出 -1

为了简化问题,规定初始的文档和每次操作中的 $\text{str}$ 都不含有空格或换行

最多会有 $q (q \leq 100)$ 次操作

例如输入数据是:

4
ILove
1 Luogu
2 5 5
3 3 guGugu
4 gu

那么输出数据是:

ILoveLuogu
Luogu
LuoguGuuguugu
3

保证每次操作输入的字符串长度不超过 $100$ 且输入合法(($2$) 和 ($3$) 操作不会越界)

分析

string 需要头文件 string,方法包括但不限于:

  1. string s: 定义一个名字为 $s$ 的字符串变量
  2. s += str 或 s.append(str): 在字符串 $s$ 后面拼接字符串 $\text{str}$
  3. s < str: 比较字符串 $s$ 的字典序是否在字符串 $\text{str}$ 的字典序之前
  4. s.size() 或 s.length(): 得到字符串 $s$ 的长度
  5. s.substr(pos, len): 截取字符串 $s$,从第 $pos$ 个位置开始 $len$ 个字符,并返回这个字符串
  6. s.insert(pos, str): 在字符串 $s$ 的第 $pos$ 个字符之前,插入字符串 $\text{str}$,并返回这个字符串
  7. s.find(str, [pos]): 在字符串 $s$ 中从第 $pos$ 个字符开始寻找 $\text{str}$,并返回位置,如果找不到返回 $-1$,$pos$ 可以省略,默认值是 $0$

灵活地运用上面的这些办法,可以使笔者更好地玩此题目

额外需要注意:s.find() 在 无法找到 时,会返回 string::npos,因此,你可以利用 s.find("bqiu")!=string::npos 判断 $s$ 中是否包含 $\text{bqiu}$,如果包含,返回 $1$,反之 $0$

string 的赋值

string a, b;
a="bqiu";
b=a;

上面的代码完全可以通过编译

但是 char 不行

【题目样本 2.3】P1308 [NOIP 2011 普及组] 统计单词数

题目描述

https://www.luogu.com.cn/problem/P1308

给定一个单词,请你输出它在给定的文章中出现的次数和第一次出现的位置

注意:匹配单词时,不区分大小写,但要求完全匹配,即给定单词必须与文章中的某一独立单词在不区分大小写的情况下完全相同,如果给定单词仅是文章中某一单词的一部分则不算匹配

分析

s.find() 是一个好东西

请注意,若 $s$ 为 $\text{to be or not to be is a question}$,在搜索 $\text{tion}$ 时,是否会检索到包含单词 question?

因此,你可以搜索 tion ,全字匹配

如果你这样做,你可能会意识到这个问题,如果 tion 在文章的结尾怎么办?

我们可以将 $s$ 的前后加上空格,即 s=' '+s+' ',这样就可以方便地解决这个问题

你太牛逼了

至于统计次数,你可以记下 find() 每次的返回值(位置),然后将位置作为参数继续查找,直到找到了 string::npos 就可以完成,这就是找到了多少次

getline:整行读入

为了方便地读取整行字符串,不用被 cin 的傻逼特性干扰,我们可以使用 getline() 函数,它的作用是将完整的一行的输入数据读入(到字符串中),一般地,它的用法是 getline(cin,<string_name>),<string_name> 指 string 字符串的名字

string:其他

下面的这一个代码,体现了 string 具有强大的扩展性

// string 转字符数组
char arr[10];
string s = "LUOGU";
int len = s.copy(arr, 9); // 最多允许复制 9 个字符,否则就越界了
arr[len] = '\0'; // 在末尾增加结束标记

// 或者
char arr[10];
string s = "LUOGU";
strcpy(arr, s.c_str()); // strncpy(arr, s.c_str(), 10);

// 字符数组转 string 就更简单了
char arr[10];
strcpy(arr, "LUOGU");
string s;
s = arr;

在这里表示对 DeepSeek 的尊敬!

3 freopen 文件操作

废话

直到现在为止,绝大部分的输入输出方式都是 标准输入输出,即 stdin 与 stdout

但在很多程序设计竞赛中,例如万恶的 CCF,它们要求使用文件输入输出,这种输入输出的方式,可以将硬盘上的文件读入到程序,将程序中的输出写入到硬盘,Linux 中的 < 和 > 重定向符号和这个是一个道理,如果你没有在竞赛中使用文件读写,你会获得一个最小的自然数的分数

题目样本

题目描述

https://www.tropical-fish.cn/usr/uploads/2026/08/1163827895.pdf

分析

这是 $\text{2024 CCF 非专业级软件能力认证 CSP-J/S 2024 第二轮认证}$ 的真题,请看试题卷 题目 A poker

这道题是一道非常简单的模拟题,参考代码如下

#include <bits/stdc++.h>
using namespace std;
int T,ans;
string s;
map <string,int> f;
int main(){
    cin>>T;
    while(T--){
        cin>>s;
        if(!f[s]) ans++,f[s]=1;
    }
    cout<<52-ans;
}

但是,如果读者直接将这个代码作为你的程序提交,你会获得 0 分的好成绩,请看试题卷第一页,它给出了 英文题目与子目录名、提交源程序文件名、可执行文件名、输入文件名、输出文件名,我们必须按照这个要求使用,虽然其他信息也相对重要,但是就文章标题而言,没那么重要

根据要求,我们应当将文件保存为 /<英文题目与子目录名>/<提交源程序文件名>,也就是 /poker/poker.cpp,然后,我们需要学会使用 freopen,下面是一个比较基础的用法

freopen("<read_file_name>","r",stdin);
freopen("<write_file_name>","w",stdout);

一般它会放在 main 函数的下面

也就是说,这题的代码需要改成:

#include <bits/stdc++.h>
using namespace std;
int T,ans;
string s;
map <string,int> f;
int main(){
    freopen("poker.in","r",stdin);
    freopen("poker.out","w",stdout);
    cin>>T;
    while(T--){
        cin>>s;
        if(!f[s]) ans++,f[s]=1;
    }
    cout<<52-ans;
}

本地运行后,发现程序一闪而过,啥也没有,反倒程序文件目录出现了一个 poker.out

既然它有读入文件,我们就应当给程序指定输入文件 poker.in,可以将题目的输入样例填写进去,运行,就发现 poker.out 有了答案

特别的,当程序读到 EOF 时,就会停止输入(相当于 ^Z)

如果是在一般的 Online Judge 提交上面的代码,会判为 WA,如果 OJ 没有要求指定输入输出文件名(一般没写就是没有),所以,我们应当删除 freopen 或者设为注释行

重要:文件名等信息必须与题目要求的一模一样!复制粘贴是一个好东西

Better

如果你为了在撰写代码时的快速,你可以只注释 输出文件 的 freopen,这样,在 输入文件 填写好测试数据之后,运行即可看到结果,相对于下面的方法快多了

  • 都不注释,你需要每次看 输出文件,很烦
  • 都注释,你需要每次粘贴样例,而且对于多组测试数据的试题更加繁琐

4 总结

$Thanks \space for \space reading.$

排序(Round 1)

-1 检查浏览器渲染数学公式情况

因为笔者发布文章当天,本博客仍然存在 Latex 可能无法渲染的情况,下面是一个 Latex 示范:

$x$

  • 如果正常显示为衡水体 x,则代表读者的浏览器已经成功渲染了 Latex
  • 如果显示为 带有美元符号的 x 即 $x$(已除去换行符),则代表读者的浏览器没有成功渲染 Latex,读者可能需要刷新重试,如果刷新多次仍然无法渲染,请尝试换一个浏览器/网络环境

0 框架

在本文中,我将重点介绍 排序,感谢 深入浅出程序设计竞赛(基础篇) 提供重要支持,本文有一部分的内容是基于本书修改

排序 是在 OI 算法中重要的一部分,虽然大部分场景下都可以用 sort 解决(限于 bits/stdc++.h),但是掌握具体的内容还是必不可少的,相对于初赛而言

$$ \begin{aligned} &\textbf{排序(Round 1)} \begin{cases} &\text{计数排序}\\ &\text{选择排序、冒泡排序、插入排序}\\ &\text{快速排序}\\ &\text{排序算法的应用}\\ &\text{汇总}\\ \end{cases} \end{aligned} $$

1 计数排序

【题目样本 1.1】P1271【模板】排序

题目描述

https://www.luogu.com.cn/problem/P1271

学校正在选举学生会成员,有 $n(n \leq 999)$ 名候选人,每名候选人编号分别从 $1$ 到 $n$, 现在收集到了 $m(m \leq 200000)$ 张选票,每张选票都写了一个候选人编号

现在想把这些堆积如山的选票按照投票数字从小到大排序。输入 $n$ 和 $m$ 以及 $m$ 个选票上的数字,求出排序后的选票编号。例如,当有 $5$ 个候选人,$10$ 张选票分别是 $2、5、2、2、5、2、2、2、1、2$ 时,需要输出 1222222255

分析

这题本质有很多方法,但是由于是 计数排序,因此:

在投票区放上 $n$ 个投票箱,投票人支持谁就把票投入对应的投票箱中,投票后只须统计每个投票箱有几张选票,直接按照候选人编号取出来就可以完成排序操作

也不需要排序了

参考代码

#include <bits/stdc++.h>
using namespace std;
int n,m,i,j,x,f[100005];
int main(){
    cin>>n>>m;
    for(i=1;i<=m;i++) cin>>x,f[x]++;
    for(i=1;i<=n;i++) for(j=1;j<=f[i];j++) cout<<i<<" ";
}

这里倡导数组从 $1$ 开始存

只需要开一个大小不小于 $n$ 的数组作为票箱,依次读入选票,然后将选票号数加到对应的票箱中,最后按照每个票箱的数量输出候选人编号即可

在这个过程中,甚至不需要存储下每一张选票

在完成输入并计算时,$f[]$ 状态如下

$f$12345
17002

计数排序

这种排序方法被称为 计数排序

读入选票并统计的时间复杂度是 $O(m)$,输出选票的时间复杂度是 $O(m+n)$(第一层循环做 $n$ 次,$f[i]$ 至多为 $m$),空间复杂度是 $O(n)$(因为 $f[]$)

所以计数排序只能用于排序编号范围不是很大的数字,如果需要排序的数字要到 $10^9$,别说运行时间了,内存都无法存得下这么大的范围的数组

此外,如果希望将一些浮点数或者字符串进行排序,就没办法去建造合适的 投票箱,所以就不能使用这种算法了(把下标存成 $\text{浮点数} \times 10^\text{浮点数位数}$ 也是一个不错的选择)

也有改进方法,即 桶排序&基数排序,思路类似,但是复杂

无论是上段和计数,这些排序都是基于分类,并非比较

2 选择排序、冒泡排序、插入排序

【题目样本 2.1】数列排序

题目描述

输入 $n(n \leq 1000)$ 个数字 $a_i(a_i \leq 10^9)$, 将其从小到大排序后输出

分析

设有 $5$ 张纸牌,他们是 $4,1,9,5,1$,打牌时如何排序?

解法 1:选择排序

分析

求最大值:max()

求最小值:min()

求一序列最值:for(int i=1;i<n;i++) ma=max(ma,a[i]);,a[] 为序列,$n$ 为数量,$ma$ 存放最大值

核心思想:打擂台

如何联想到本题?

[collapse status="false" title="答案"]
从第一张牌到最后一张牌中找到最小的一张,放在最前面的位置;然后从第二张牌到最后一张牌中继续找到最小的一张,放到第二位……如此反复,就可以得到从小到大的序列
[/collapse]

这里有一张不错的图,来自 深入浅出,因此贴在这

若遇到大小相等的牌,continue

参考代码片段
for (int i = 0; i < n - 1; i++) {
    for (int j = i + 1; j < n; j++)
        if (a[j] < a[i]) {
            int p = a[i];
            a[i] = a[j];
            a[j] = p;
        }
}

解法 2:冒泡排序

分析

如果尝试把最大的一项放在后面,虽然可以倒过来使用选择排序,但是可以试试别的办法

这里有一张图,来自 豆包,它打了一个类似于文章的比方,讲述了 选择排序 与 冒泡排序 的区别,前情提要是 现有扑克牌:9、5,要排成5、9

首先比较第 $1$ 张牌和第 $2$ 张牌,如果后面的牌比前面的牌小,那么就交换位置;然后比较第 $2$ 张牌和第 $3$ 张牌,同样保证第 $3$ 张牌要大于第 $2$ 张牌……依此类推, 直到比较最后一张牌和倒数第二张牌,这样就能保证最后面的一张牌是最大的一张了

继续进行新的一轮,利用上面一样的交换,但是需要注意的是,由于最后一张牌已经是手牌中最大的一张了,所以只需要比较到倒数第二张就行了

这一轮后结束后倒数第二张牌就是第二大的牌(当然,也有可能是并列最大)。经过 $n-1$ 轮排序后,就得到了有序的手牌

下图同样来自 深入浅出,用于理解

参考代码片段
for (int i = 0; i < n - 1; i++) {
    for (int j = 0; j < n - i - 1; j++)
        if (a[j] > a[j + 1]) {
            int p = a[j];
            a[j] = a[j + 1];
            a[j + 1] = p;
        }
}

[scode type="green"]
int p=a[j];a[j]=a[j+1];a[j+1]=p; 推荐用 swap(a[j],a[j+1]); 来取代,用于交换
[/scode]

解法 3:插入排序

分析

一种很形象的比喻

你在玩纸牌游戏的时候,从牌堆里面抽牌,抽到一张牌,放到你的手牌里,你是怎么保证手牌内的牌是有序的呢?

如果你能通过 Cloudflare 的人机验证,你应该会看看抓到牌的大小,再看看手牌,定位到合适的位置,插入

上面描述的过程,即 插入排序

  1. 把手牌分为有序的和无序的两部分
  2. 最开始有序的就只有一张牌,也就是第一张
  3. 要把接下来的 无序区 中的一张牌(称为 待插牌 )插入到 有序区 中,就是从有序区的末尾开始往前比较,如果待插牌比正在比较的牌小,那么就把有序区的正在比较的牌往后面放一格,然后继续往前面进行比较,直到待插牌遇到不大于自己的牌或者成为第一个为止
  4. 这时,待插牌就可以填入留出来的缺口中。反复将无序区中的待插牌插入到有序区中,直到所有的牌都在有序区

同样的,这里有一张非常生动的图,供读者查阅

下面这是来自 深入浅出 的 插入排序 实现代码,进行了注释的补充

for(int i = 1; i < n; i++) {
    int now = a[i], j;          // 记录待插入元素,稍候会放进去
    for(j = i - 1; j >= 0; j--) // 一个很经典的模板:元素后移
        if(a[j] > now)
            a[j + 1] = a[j];    // 大于now的元素后移
        else
            break;
    a[j + 1] = now;             // 插入到正确位置
}

选择排序

为了加强读者的记忆,这里重新梳理一遍

解法 1 这种算法被称为 选择排序

从代码中可以看出有两重循环,读者可以自己计算一下已知 $n$ 的情况下一共需要比较的次数

选择排序的算法时间复杂度是 $O(n^2)$;由于只需要一个数组存下这些数字即可,空间复杂度是 $O(n)$

[collapse status="false" title="答案"]

$$ \frac{n(n-1)}{2} $$

[/collapse]

冒泡排序

解法 2 这种算法被称为 冒泡排序,在一轮中,最大的那张牌会一点一点地从左移动到右边,像冒泡泡一样

读者也可以计算一下排序的过程中需要进行几次比较

冒泡排序算法的时间复杂度也是 $O(n^2)$,空间复杂度也是 $O(n)$

冒泡排序相比于选择排序并没有实质性的改进

有一种冒泡排序的改良版叫作希尔排序,不是相邻位置的两个数字比较,而是比较距离更远的两个数字,虽然能略微优化一些复杂度,但依然不实用

猴子排序同样也是一种奇怪的办法

插入排序

这种算法叫作 插入排序

如果序列本来已经从小到大排序,那么每次无序区的待插牌都不会插进有序区中,而是紧贴在有序区后使有序区一直扩大,那么只需要 $n-1$ 次比较就可以完成算法流程

然而,如果序列是从大往小排序,那么每次插入都会将有序区中的所有牌往后面挪动一次,这样速度就非常慢了

讲时间复杂度一般会考虑最悲观的情况,所以插入排序的算法复杂度还是 $O(n^2)$,和选择排序、冒泡排序的复杂度一致,它们空间复杂度也是一样的

不过,如果能保证序列 基本有序 , 那么使用插入排序的效率可能会不错

不推荐在 Contest 中使用这些方法,耗时过久

笔者认为,学习这些是为了掌握原理,在第一轮比赛中获得较好的成绩,在历年的 OI 比赛中,有相当一部分的套题涉及了这个内容

3 快速排序

【题目样本 3.1】P1177 快速排序

题目描述

https://www.luogu.com.cn/problem/P1177

输入 $n(n \lt 10^5)$ 个数字 $a_i(a_i \lt 10^9)$,将其从小到大排序后输出

分析

这个数据量就很大了,插入排序、选择排序、冒泡排序都无法完成。这次将介绍在 OI 中最普遍使用的排序算法 快速排序,简称快排,顾名思义

快速排序说起来其实也挺简单的,但是需要读者能够理解递归

[collapse status="false" title="递归"]
递归就是函数调用自己。

你的快速排序就是递归:

void Qsort(int x, int y) {
    if (x >= y) return;          // 终止条件
    // ... 分区操作 ...
    Qsort(x, i);                 // 自己调用自己,排左边
    Qsort(i+2, y);               // 自己调用自己,排右边
}

执行过程:先把数组分成左右两部分,然后分别对两部分再调用同样的函数继续分,直到每部分只剩一个元素。

每次调用会占用栈内存,递归太深会栈溢出。你的代码最坏情况(已有序数据)递归深度达 $200000$,风险很大。

上述内容来自 Deepseek V4 Pro,请注意辨别
[/collapse]

算法的大致过程是对于一个无序序列,找到一个 哨兵数 ,将序列中所有比哨兵数小的数字都在哨兵数的左边,所有比哨兵数大的数字都在哨兵数的右边;然后分别对哨兵数左边和右边再使用同样的方法找到新的哨兵数,并再次进行分类,直到集合不可分割为止

怎么选择哨兵呢?随便选,可以是第一个,可以是中间那个,也可以在序列中随机选择

选择好哨兵,然后从序列左端开始寻找第一个比哨兵大的数字,从右边选择第一个比哨兵小的数字,然后交换这两个数;接着继续从左边找到比哨兵大的数字,右边比哨兵小的数字并交换……直到将序列分为两组,左边序列都不大于哨兵,右边序列都不小于哨兵,就可以分别对左边和右边进行排序了

下面同样还有一张生动的图,建议配合代码食用

参考代码

有多种办法可以实现,这是一种实现办法,也基于书本内容进行了适当的补充更多的办法请前往 排序的应用 这一章节

void qsort(int a[], int l, int r) { // a[] 传递数组
    int i = l, j = r, flag = a[(l + r) / 2], tmp;  // flag是基准值(哨兵)
    do {
        while (a[i] < flag) i++;   // 左指针右移,跳过比基准小的
        while (a[j] > flag) j--;   // 右指针左移,跳过比基准大的
        if (i <= j) {              // 找到一对需要交换的数
            tmp = a[i]; a[i] = a[j]; a[j] = tmp;
            i++; j--;              // 继续移动指针
        }
    } while (i <= j);              // 两指针未交错就继续
    if (l < j) qsort(a, l, j);     // 递归排左半部分
    if (i < r) qsort(a, i, r);     // 递归排右半部分
}

快速排序

虽然在极端情况下(比如数列已经有序,且每次选择哨兵都会分出长度为 $1$ 的小段),快速排序的时间复杂度是 $O(n^2)$,但是如果随机选择哨兵,则很难出现这种情况

实际上,随机化快速排序的算法复杂度是 $O(n \log n)$,空间复杂度是 $O(n)$,而且不需要额外的辅助空间,所以是一种最为实用的排序算法

快速排序利用了 分治 思想,后面可能会出一篇 排序 这一专题的补充篇,介绍了更多的非基础排序

额外的,二分查找法 也利用了 分治思想,这个方法也可能会在未来单独出一篇专题进行讲解

【题目样本 3.2】 P1923 求第 $k$ 小的数

题目描述

https://www.luogu.com.cn/problem/P1923

输入 $n(n \lt 10^6 \text{ 且 } n \text{ 为奇数})$ 个数字 $a_i(a_i \lt 10^9)$,输出这些数字的第 $k$ 小的数,最小的数是第 $0$ 小

分析

如上 URL 中的 题目描述,如果你是真的要训练 快排,请 不要 使用 nth_element STL,当然我估计你也不知道

虽然可以直接排序,然后取数组中的第 $k$ 个作为答案进行输出,但是直接排序一遍的复杂度是 $O(n \log n)$,不适合这道头目,我们可以借鉴 快排 的 分治思想 把时间复杂度降至 $O(n)$

[collapse status="false" title="可以降低时间复杂度的原因"]

核心原因在于每次只需要递归处理一边,而不是两边

更多的:

  1. 快速排序的分治过程
    每次选一个基准(pivot),将数组分成三部分:

    • 左边:所有元素 ≤ pivot
    • 中间:pivot 本身
    • 右边:所有元素 > pivot

    然后递归地对左右两边分别排序,所以总复杂度是 (O(n \log n))。

  2. 求第 k 小数的优化
    在快速选择(Quick Select)中,不需要对两边都排序,只需要判断第 k 小的数在哪一边:

    • 如果 (k) 小于左边元素个数,就只在左边递归查找
    • 如果 (k) 等于左边元素个数 + 1,那么 pivot 就是答案
    • 否则,只在右边递归查找,并且调整 k 的值
  3. 复杂度分析

    • 每次递归处理的数组大小大约是上一次的一半(平均情况)
    • 总比较次数:

      $$ n + \frac{n}{2} + \frac{n}{4} + \cdots = 2n $$

    • 因此平均时间复杂度是 (O(n))

上述内容来源 Deepseek V4 Pro 修改而得
[/collapse]

和快速排序的方法一样,任意取一个哨兵,将序列分为两部分,左边部分的所有数字都不大于右边的数字

  • 如果 $k$ 在左边的范围,则递归求解左边的部分
  • 如果在右边,则递归求解右边的部分
  • 但是也有可能两边都不属于(比如说 $j=4$ 而 $i=6$ , 但刚好 $k=5$),那么直接就可以给出答案

参考代码

这个代码是 深入浅出 提供的,由 Deepseek V4 Pro 和 笔者 改写而得

int ans = 0, k; // 记录答案,以及求第 k 小
void findkth(int a[], int l, int r) { // 引入数组的地址
    if(l == r) {
        ans = a[l]; // 区间长度为 1 时,记录答案
        return;
    }

    int i = l, j = r, flag = a[(l + r) / 2], tmp;
    do {
        while (a[i] < flag) i++; // 从左找比哨兵大的数
        while (a[j] > flag) j--; // 从右找比哨兵小的数
        if (i <= j) { // 交换
            tmp = a[i]; a[i] = a[j]; a[j] = tmp;
            i++; j--;
        }
    } while (i <= j);

    if (k <= j) findkth(a, l, j); // 第 k 小的数字在左区间
    else if (i <= k) findkth(a, i, r); // 第 k 小的数字在右区间
    else findkth(a, j + 1, i - 1); // 第 k 小的数字既不在左区间,也不在右区间
}

尽管 此参考代码片段(快速选择) 相对于 快排(模板代码)类似,但是每次处理的区间长度期望下都会被减半,复杂度是线性的

快速排序 - 快速选择

这个概念是在书中没有的,是由 Deepseek V4 Pro 提出

它基于 快速排序 的分治思想,但并不是完整的排序算法,而是一种 选择算法 ——专门用来查找数组中第 $k$ 小(或第 $k$ 大)的元素

下面是一张表格,阐释了区别

快速排序快速选择
目的将整个数组排好序只找出第 $k$ 小的元素
递归方向同时递归左右两边只递归包含答案的那一边
时间复杂度(O(n \log n))平均 (O(n))
是否完全排序是否

所以,这种方法叫做 快速选择,也可以理解为"快速排序的简化版"或"基于快排分治的选择算法"

[collapse status="false" title="函数图像(可能有误)"]

这个部分由 Deepseek V4 Pro 与 豆包 提供,它们分别为这个部分 提供函数生成代码 和 提供函数图,在这里表示对他们最诚挚的感谢

但是目前看起来它们携手合作画出来的图有误,作为初二学生的笔者,无法验证这张图的真伪,所以这个问题抛给读者,抱歉

[/collapse]

4 排序的应用

这里提供了一些 排序 的拓展用法,在第二轮比赛时可能有用

【题目样本 4.1】P1059 [NOIP 2006 普及组] 明明的随机数

题目描述

https://www.luogu.com.cn/problem/P1059

给出 $N(N \lt 10^2)$ 个 $1 \sim 10^3$ 的数字,输出去重后剩余数字的个数以及去重排序后的序列

解法 1:计数排序

在拿到新的选票时发现投票箱中已经有这个选票了,就扔掉这张选票,保持每个票箱中最多只有一张票

最后将有票的票箱编号输出即可

这里要求先输出不同数字的个数,可以先枚举一遍票箱记录,输出剩余数字的个数,然后再枚举一遍票箱,输出序列

也可以创建一个新的数组,在枚举票箱的时候将有票的编号复制进新的数组中

解法 2:STL

当然也可以直接对这些数据进行排序,然后从小到大每个数字只输出一个

考虑数据规模,可以使用上面提到的所有排序方式

不过,这次介绍排序的 STL, 这样就不需要去亲自实现排序算法了

关于 STL,后面可能会出一个专门的专题,STL = Standard Template Library(标准模板库)

有了这个就不用自己手搓了

排序 STL 1:sort
分析

std::sort 被定义于 algorithm 头文件中,因此,你在使用前需要先导入这个头文件,为了方便笔者,笔者会尽可能地在后文省略较多的不影响理解的内容

sort 的基本用法如下

// 形式1:默认升序排序
sort(起始迭代器, 结束迭代器);

// 形式2:自定义比较规则
sort(起始迭代器, 结束迭代器, 比较函数/仿函数/Lambda); // 比较函数即 cmp 用的较多

一个例子,sort(a,a+n,cmp),对 $a[]$ 数组从 $a[0]$ 到 $a[n-1]$ 进行排序,cmp 是指自定义排序函数,如果是将数组 $a[]$ 从小到大排序,那么这一项可以省略

参考代码
#include <iostream>
#include <algorithm>
using namespace std;
int const MAXN = 1010;
int a[MAXN], ans[MAXN], n, cnt = 0, tmp = -1;
int main() {
    cin >> n;
    for (int i = 0; i < n; i++) cin >> a[i];
    sort(a, a + n);
    for (int i = 0; i < n; i++) {
    if (a[i] != tmp) ans[cnt++] = a[i]; /* 当前的和上一个不同才复制到新数组 */
    tmp = a[i];
    }
    cout << cnt << endl;
    for (int i = 0; i < cnt; i++) cout << ans[i] << ' ';
    return 0;
}
sort

这里,sort 的时间复杂度是 $O(n \log n)$

如果要对数组 $a[1]$ 到 $a[n]$ 排序,就要用 sort(a+1, a+n+1)

那么,如果要求从大到小排序,只需要定义一个名字是 cmp 的自定义比较函数即可,这个函数的输入是两个元素,如果第一个在第二个之前,则返回 true, 否则返回 false。代码如下:

bool cmp(int q,int h){ // q 前 h 后
    return q>h;
}
排序 STL 2:unique
分析

std::unqiue 同样被定义于 algorithm 头文件中

unique(a,a+n):对 $a[]$ 数组从 $a[0]$ 到 $a[n-1]$ 进行去重,要求 $a[]$ 数组已经有序,返回去重后最后一个元素对应的指针

如果使用 unique, 就可以不用 ans 新数组了,可以直接获得去重后的数组和最终元素个数

参考代码
sort(a,a+n);
cnt=unique(a,a+n)-a; // 剩下的数总数
cout<<cnt<<"\n";
for(int i=0;i<cnt;i++) cout<<a[i]<<" ";

【题目样本 4.2】P1093 [NOIP 2007 普及组] 奖学金

题目描述

https://www.luogu.com.cn/problem/P1093

有 $n$($n \le 300$)名学生的语文、数学、英语成绩,这些学生的学号依次是从 $1$ 到 $n$。需要对这些学生进行排序。如果总分相同,则语文分数高者名次靠前;如果语文成绩还相同,学号小者靠前。输出排名前 $5$ 的学生学号和总分

解法 1:STL

分析

定义一个 struct 结构体,命名为 student 存储学生各项信息,使用 sort 附带 cmp 比较函数进行排序,最后输出排序结果,下面是排序优先级

  1. 总分,高者优先
  2. 语文,高者优先
  3. 学好,小者优先
参考代码
#include <algorithm>
#include <iostream>
using namespace std;

int const MAXN = 310;
int n;

struct student {
    int id, chinese, total;
} a[MAXN];

int cmp(student a, student b) {
    if (a.total != b.total) return a.total > b.total;      // 总分先定胜负
    if (a.chinese != b.chinese) return a.chinese > b.chinese; // 然后比语文
    return a.id < b.id;                                    // 最后比学号
}

int main() {
    cin >> n;
    for (int i = 0; i < n; i++) {
        int math, english;
        cin >> a[i].chinese >> math >> english;
        a[i].total = a[i].chinese + math + english;
        a[i].id = i + 1;
    }

    sort(a, a + n, cmp);

    for (int i = 0; i < 5; i++) {
        cout << a[i].id << " " << a[i].total << endl;
    }

    return 0;
}

解法 2:选择排序

分析

时间复杂度 $O(kn)$,此题 $k=5$,即 $O(n)$

解法 3:插入排序

分析

维护排名前 $5$ 数组作为比较对象(手牌)

时间复杂度 $O(n)$,运行效率不高

【题目样本 4.3】P1781 宇宙总统

题目描述

https://www.luogu.com.cn/problem/P1781

共有 ( n )(( n \le 20 ))个非凡拔尖的人竞选总统,现在票数已经统计完毕,请算出谁能够当上总统。第一行输出候选人编号,第二行输出选票数量。票数可能很大,最大会有 100 位

解法 1:计数排序

分析

由于本题只需要求出最大的那一个数字,所以用“打擂台”法

但是数字很大,没办法直接存下来(long long 也存不下)

但是需要高精度,不考虑单开一个话题

解法 2:结构体

分析

存进字符串。不过不能直接对这些字符串进行大小比较,因为字符串比较是比较字典序(第一位小的在前面,如果相同则比较第二位,以此类推),例如 $10000$ 小于 $1200$ 小于 $200$

得手搓 cmp

参考代码

struct node {
    string x;    // 票数
    int num;     // 候选人编号
} s[MAXN];

bool cmp(node a, node b) {
    if (a.x.length() != b.x.length()) {
        return a.x.length() > b.x.length(); // a 比 b 位数多时 a 在前面
    }
    return a.x > b.x; // 位数相同,但 a 字典序排列比 b 大
}

5 汇总

练习

笔者为读者提供了一些洛谷上没有的题目,供练习

C++ 常用排序算法一览表

额外的,这里有一张特别大的表格,供读者查阅

排序算法平均时间复杂度最好时间复杂度最坏时间复杂度空间复杂度最坏空间复杂度稳定性特点
冒泡排序$O(n^2)$$O(n)$$O(n^2)$$O(1)$$O(1)$✅ 稳定相邻元素两两比较,大数后移;优化版可提前结束
选择排序$O(n^2)$$O(n^2)$$O(n^2)$$O(1)$$O(1)$❌ 不稳定每轮选最小放到前面;交换次数最少(n次)
插入排序$O(n^2)$$O(n)$$O(n^2)$$O(1)$$O(1)$✅ 稳定将元素插入已排序区;数据量小或基本有序时极快
希尔排序$O(n \log n) \sim O(n^2)$$O(n \log n)$$O(n^2)$$O(1)$$O(1)$❌ 不稳定分组插入排序;增量序列决定效率
归并排序$O(n \log n)$$O(n \log n)$$O(n \log n)$$O(n)$$O(n)$✅ 稳定分治+合并;适合链表和外部排序
快速排序$O(n \log n)$$O(n \log n)$$O(n^2)$$O(\log n)$$O(n)$❌ 不稳定分治+分区;实际最快,但最坏退化为 $O(n^2)$
堆排序$O(n \log n)$$O(n \log n)$$O(n \log n)$$O(1)$$O(1)$❌ 不稳定基于堆数据结构;适合求 Top K
计数排序$O(n + k)$$O(n + k)$$O(n + k)$$O(k)$$O(k)$✅ 稳定非比较排序;适用于整数且范围 $k$ 不大
桶排序$O(n + k)$$O(n + k)$$O(n^2)$$O(n + k)$$O(n + k)$✅ 稳定分桶+桶内排序;数据分布均匀时极快
基数排序$O(d \times (n + k))$$O(d \times (n + k))$$O(d \times (n + k))$$O(n + k)$$O(n + k)$✅ 稳定按位排序(LSD/MSD);适合整数或字符串

[collapse status="false" title="C++ 常用排序算法一览表(无特点)"]

排序算法平均时间复杂度最好时间复杂度最坏时间复杂度空间复杂度最坏空间复杂度稳定性
冒泡排序$O(n^2)$$O(n)$$O(n^2)$$O(1)$$O(1)$✅ 稳定
选择排序$O(n^2)$$O(n^2)$$O(n^2)$$O(1)$$O(1)$❌ 不稳定
插入排序$O(n^2)$$O(n)$$O(n^2)$$O(1)$$O(1)$✅ 稳定
希尔排序$O(n \log n) \sim O(n^2)$$O(n \log n)$$O(n^2)$$O(1)$$O(1)$❌ 不稳定
归并排序$O(n \log n)$$O(n \log n)$$O(n \log n)$$O(n)$$O(n)$✅ 稳定
快速排序$O(n \log n)$$O(n \log n)$$O(n^2)$$O(\log n)$$O(n)$❌ 不稳定
堆排序$O(n \log n)$$O(n \log n)$$O(n \log n)$$O(1)$$O(1)$❌ 不稳定
计数排序$O(n + k)$$O(n + k)$$O(n + k)$$O(k)$$O(k)$✅ 稳定
桶排序$O(n + k)$$O(n + k)$$O(n^2)$$O(n + k)$$O(n + k)$✅ 稳定
基数排序$O(d \times (n + k))$$O(d \times (n + k))$$O(d \times (n + k))$$O(n + k)$$O(n + k)$✅ 稳定

[/collapse]

C++ STL 中的排序一览表

同时,这里还有一张关于 STL 的表

函数时间复杂度空间复杂度最坏空间复杂度稳定性特点
std::sort$O(n \log n)$$O(\log n)$$O(\log n)$❌ 不稳定混合排序(快排+堆排+插入),最常用
std::stable_sort$O(n \log^2 n) \sim O(n \log n)$$O(n)$$O(n)$✅ 稳定保证相等元素顺序不变
std::partial_sort$O(n \log k)$$O(1)$$O(1)$❌ 不稳定取前 $k$ 个最小/最大
std::nth_element$O(n)$ 平均$O(1)$$O(1)$❌ 不稳定求第 $k$ 小/大,不完全排序

NOIP 信息学:搜索及其优化 完整知识点文档

https://www.tropical-fish.cn/usr/uploads/2026/07/3625185257.pptx

一、前言:搜索算法定位

  1. 算法解题优先级:优先数学推导、递推、贪心、DP;无高效数学解法时,使用搜索。
  2. 搜索特点:通用性强、适用范围广;纯暴力搜索效率极低,必须配合各类优化才能通过大数据。
  3. 基础搜索分类:穷举、深度优先搜索(DFS)、广度优先搜索(BFS)。

二、核心基础概念

2.1 状态

  • 定义:搜索的最小单元,决定搜索效率、思维难度、代码复杂度。
  • 设计要点:靠刷题积累经验,状态定义优劣直接决定代码能否过时限。

2.2 搜索树

所有搜索过程均可抽象为一棵搜索树:

  • DFS:纵向优先遍历整棵树,适合求全部可行解、构造方案;缺点是深度过大易栈溢出。
  • BFS:逐层横向遍历,天然适合求最短/最小代价最优解;自带分层,第一次到达目标即为最优。

三、基础搜索算法实现

3.1 深度优先搜索 DFS(回溯)

通用模板

void dfs(int dep, [其他状态参数])
{
    // 边界:到达目标状态
    if(当前为目标状态){
        记录答案/输出/计数;
        return;
    }
    // 枚举所有可拓展状态
    for(枚举所有下一步可能){
        保存现场(修改全局/局部状态);
        if(该拓展合法) dfs(dep + 1, 新参数);
        恢复现场(回溯,还原状态);
    }
}

特点

  1. 使用系统栈/手动栈,递归实现;
  2. 不自动保证最优,需手动记录最优值;
  3. 深度过大时栈溢出(如POJ3278裸DFS会爆栈)。

3.2 广度优先搜索 BFS(队列)

通用模板

初始状态入队;
while(队列不为空){
    取出队首元素,队首出队;
    if(当前是目标状态){输出答案; return;}
    // 拓展所有子状态
    for(所有拓展方向){
        if(状态未访问 && 拓展合法){
            标记已访问(判重);
            新状态入队;
        }
    }
}

特点

  1. 借助队列实现分层遍历;
  2. 第一次抵达终点一定是最小步数/最小代价,无需额外比较最优;
  3. 必须判重,否则重复入队无限循环。

四、经典入门例题:POJ3278 抓住那头牛

题目简述

农夫在数轴N,牛在K,三种移动方式,每次耗时1分钟:

  1. $X \to X-1$
  2. $X \to X+1$
  3. $X \to 2X$
    求到达牛位置的最少时间。样例输入5 17,输出4。

算法选择分析

  1. 裸DFS:搜索树深度极大,系统栈溢出,且无法保证最优;
  2. BFS最优:分层遍历,首次走到K即为最小步数,是本题标准解法。

五、NOIP真题案例详解

5.1 NOIP2017提高组Day2 T1 奶酪(连通性DFS)

题意

无限大奶酪,高度$h$,内部有等半径球形空洞;空洞相交/相切可互通;空洞接触下表面$z=0$可进入,接触上表面$z=h$可逃出。判断老鼠能否从底部走到顶部。

核心思路

  1. 模型转化:无向图连通性,每个空洞是节点;
  2. 连通条件:两球心距离 $\le 2r$;

    • 距离公式避免sqrt防止精度丢失:平方比较
      $$(x_1-x_2)^2+(y_1-y_2)^2+(z_1-z_2)^2 \le (2r)^2$$
    • 全部变量使用long long,防止大数溢出;
  3. 起点集合:$z \le r$ 的空洞(接触下表面);
  4. 终止条件:搜到任意满足 $z \ge h-r$ 的空洞(接触上表面);

核心代码框架

void dfs(int id){
    vis[id] = true;
    // 判断是否连通上表面
    if(hole[id].z + r >= h) flag = true;
    for(int i = 1; i <= n; i++){
        if(!vis[i] && 两球连通(id,i)) dfs(i);
    }
}

5.2 NOIP2014提高组Day2 T2 寻找道路(反向DFS+BFS最短路)

题意

有向图,边权均为1,求起点s到终点t的最短路径,约束:路径上每个点的所有出边指向的点,都能间接到达终点t。

分步解法

  1. 建两张图:正向图(原图)、反向图(所有边反转);
  2. 反向图DFS:从终点t出发,标记所有能到达t的点vis1[];
  3. 筛选合法点:一个点u合法当且仅当u所有出边指向的v都满足vis1[v]=true;
  4. 正向图BFS:仅走合法点,求s到t最短路;

数据注意

$n\le10^4,m\le2\times10^5$,稀疏图用vector邻接表;输入量大,禁用cin,改用scanf防超时。

5.3 NOIP2017普及组T3 棋盘(带状态记忆化DFS)

题意

$m\times m$棋盘,格子红/黄/无色;四向行走,异色花费1金币;可花2金币临时将无色格子染色,魔法不可连续释放。求左上角到右下角最小花费。

状态设计(三维记忆化)

f[x][y][can]:坐标$(x,y)$,can=0/1代表当前能否使用魔法,存储到达该状态最小金币;

状态转移

  1. 下一格有颜色:根据颜色是否相同累加金币,魔法重置为可用;
  2. 下一格无色:仅当魔法可用时,花费2金币,魔法置为不可用;

优化梯度

  • 裸DFS:40分;
  • 最优性剪枝:60分;
  • 记忆化剪枝(记录每个状态最小代价):100分。

5.4 NOIP2009提高组T4 靶形数独(DFS+搜索顺序优化)

题意

9×9数独,每个格子有分值,数字×格子分值总和最大化;无解输出-1。

优化梯度

  1. 暴力DFS顺序填格:40分;
  2. 预处理空白格、行/列/九宫格数字判重:75分;
  3. 最优优化(100分):搜索顺序优化
    每次优先选择可填数字最少的空白格填充,大幅剪枝无效分支;

    • 若某格无合法数字:可行性剪枝,直接回溯;
    • 若只剩1种数字:直接填充,减少循环。

六、DFS核心优化:剪枝

6.1 可行性剪枝

当前状态已经不满足题目约束,后续无论怎么走都无解,直接回溯。
例:数独格子无可用数字、数轴坐标超出0~100000范围。

6.2 最优性剪枝

全局记录当前已知最优解;若当前已消耗代价 ≥ 最优解,后续路径只会更差,直接返回。
例:抓牛DFS记录当前最小步数,当前步数超过最优则剪枝。

6.3 记忆化剪枝(全局最优剪枝)

记录每个状态到达时的最小代价;再次走到同一状态时,若当前花费 ≥ 记录值,直接剪枝;否则更新记录。
适用:棋盘、最短代价类搜索问题。

6.4 搜索顺序优化

人为调整拓展节点的先后顺序,优先搜索分支少、更容易出解的路径,提前触发剪枝。
代表例题:靶形数独(优先填可选数字最少的格子)。

七、高阶搜索优化算法

7.1 迭代加深搜索 IDDFS

  1. 思路:限制DFS最大深度,从小到大逐层放宽深度限制;
  2. 优势:兼具DFS空间小、BFS保证最优的特点;解决深搜栈溢出问题;
  3. 适用:求最小步数、深度不确定的最优解问题。

7.2 双向BFS

  1. 思路:起点BFS、终点BFS同时向外拓展,两边相遇即得到最优解;
  2. 优势:搜索范围指数级缩小,大幅降低时间复杂度;
  3. 适用:起点终点明确、状态空间巨大的最短路问题(抓牛、迷宫最短路径)。

7.3 BFS配套优化:哈希判重

状态复杂(字符串、多维数组)无法开数组标记访问时,用哈希表存储已走过状态,避免重复入队。

八、搜索算法选择总结

算法适用场景核心优势短板
DFS求全部方案、连通性、构造解代码简单、空间小无法保证最优、深度大易栈溢出
BFS最短步数、最小代价最优解天然最优、分层遍历队列占用空间大
DFS+剪枝最优值、代价最小类搜索灵活,多重剪枝提速需要手动维护最优答案
IDDFS深度未知、需要最优、怕栈溢出兼顾DFS与BFS优点重复浅层搜索,轻微冗余
双向BFS超大状态空间最短路搜索量指数级减少代码逻辑更复杂

九、易错点汇总

  1. 距离比较时禁用sqrt,平方后用long long避免精度、溢出问题;
  2. 大数据输入使用scanf/printf,cin关闭同步或直接淘汰;
  3. DFS回溯必须恢复现场,否则状态污染;
  4. BFS必须判重,否则死循环;
  5. 连通性问题分清有向图/无向图,按需建反向图;
  6. 记忆化数组初始化无穷大,到达同一状态代价更小时才更新;
  7. 递归DFS注意递归深度上限,深度极大改用迭代加深或BFS。

十、配套上机习题

  1. 奶酪:基础连通DFS
  2. 飞越原野:BFS+记忆判重
  3. 寻找道路:反向DFS+正向BFS综合
  4. 棋盘:DFS+记忆化最优剪枝
  5. 靶形数独:DFS+搜索顺序优化
  6. 小木棍:DFS多重可行性/最优性剪枝

感受

我也不是打 noip 的货,也很菜

题解

梦熊 NOIP 2025 模拟赛 3 题解

T1 战争游戏

题目描述

游戏在一行 $n$ 个城市上进行,城市 $i$ 初始有士兵 $a_i$ 人。前 $m$ 个城市属于小L,其余属于小K,两人交替操作,小L先手。操作包含三种:

  1. 转移:选择己方地盘上相邻($|x-y|=1$)的两个城市 $x$、$y$,将 $x$ 的所有士兵转移到 $y$,$a_x$ 变为 0。
  2. 攻占:选择己方城市 $x$ 和对方相邻城市 $y$,小L操作需满足 $a_x > a_y$,小K操作需满足 $a_x \geq a_y$,攻占后 $y$ 归己方,士兵转移方式同转移操作。
  3. 不进行任何操作。
    当一方占据所有城市时获胜,需对 $m=1,2,...,n-1$ 分别输出获胜方。

关键思路

  1. 特殊性质B:若先手第一步无法攻占,双方会优先集中兵力,最终比较分界线两侧总兵力。
  2. 特殊性质C:若先手第一步可攻占对方城市且后手无法反制,比较攻占后两侧总兵力。
  3. 正解:先手攻占后需判断对手是否能反制,若能则选择转移兵力,否则执行攻占,核心是比较关键状态下的兵力总和。

T2 加减乘除

题目描述

给定一棵以 1 为根的树,每个节点 $u$ 有运算符号 $op_u$(加或减)和数值 $a_u$。$q$ 次询问,每次给定 $x$,从根节点出发,需满足边的范围条件 $[l_v, r_v]$(即经过节点 $u$ 运算后的 $x'$ 需在子节点 $v$ 的边范围内)才能走向子节点,求能到达的节点个数。

正解

  1. 区间预处理:每个节点 $u$ 对应可到达的 $x$ 区间 $[L_u, R_u]$,初始根节点区间为 $[-\infty, +\infty]$。
  2. 区间更新:向下遍历树时,运算操作会平移区间,边的范围条件会取区间交集,以此递推得到所有节点的 $[L_u, R_u]$。
  3. 高效查询:对所有区间端点离散化,用差分法给每个区间 $[L_u, R_u]$ 加 1,查询时二分找到 $x$ 对应的区间,即可得到答案。
  4. 时间复杂度:$O((n+q) \log n)$。

T3 空之碎物

题目描述

定义二进制不退位减法 $\ominus$,给定序列 $a$,对于区间 $[l, r]$,$f(l, r)$ 表示将该区间元素作为多重集,经若干次合并操作(选两个数合并为其不退位减法结果)后剩余的最大值。求 $\sum_{i=1}^{n} \sum_{j=i+1}^{n} f(i, j)$ 对 998244353 取模的值。

核心性质

  1. 性质1:最终操作结果可表示为 $x \ominus(y \ominus a_1 \ominus a_2 \ominus ... \ominus a_{n-2})$($a_1~a_{n-2}$ 为除 $x,y$ 外的元素)。某二进制位无1时无影响,恰好一个1时贡献由 $y$ 选择决定,至少两个1时贡献为0。
  2. 性质2:若区间长度超过 $\log_2 V + 2$($V$ 为元素最大值,此处 $V < 2^{25}$),则 $f(l, r)$ 等于区间最大值。

正解

  1. 处理长区间:直接累加所有长度超过 $\log_2 V + 2$ 的区间的最大值。
  2. 处理短区间:枚举每个二进制位,对该位有两个1的元素建立连边,通过枚举连边计算贡献,时间复杂度 $O(n \log_2^2 V)$。

T4 Ice Drop

题目描述

一个正整数序列是“好的”,当且仅当对任意正整数 $k$,序列中所有 $k$ 的倍数位置连续。给定序列 $a$,$f(b)$ 表示满足以下条件的“好的”序列 $b'$ 的数量:

  1. $|b'| = |b|$;
  2. 若 $b_i > 1$,则 $b_i' = b_i$;
  3. $LCM(b_1,...,b_m) = LCM(b_1',...,b_m')$($m$ 为 $b$ 的长度)。
    $q$ 次询问,每次给定 $l, r$,求 $\sum_{x=l}^{r} \sum_{y=x}^{r} f(a[x,y])$ 对 $10^9+7$ 取模的值。

关键思路

  1. 核心等价:序列是“好的”等价于每个质因子的指数序列是单峰的。
  2. 特殊性质A($q=1$):不能新增质因子,也不能超过原质因子最大指数,通过组合数计算合法填充方式,答案为各质因子贡献乘积。
  3. 特殊性质B:若只有一个质因子,合法区间是若干不交区间的子集,答案非0即1。
  4. 正解:用扫描线结合线段树,维护全局乘、单点修、查历史和,对每个不交区间单独处理,时间复杂度 $O(n \log n)$。

个人代码

T1

主要思路:骗分

#include <bits/stdc++.h>
using namespace std;
int T,n,i,m,ma,t,a[100010];
string ans;
int main(){
    cin>>T;
    while(T--){
        cin>>n;t=ma=0;ans="";
        for(i=1;i<=n;i++)
            cin>>a[i],ma+=a[i];
        for(m=1;m<n;m++){
            t+=a[m];
            if(2*t>ma) ans+='1';
            else ans+='0';
        }
        cout<<ans<<"\n";
    }
}

太好力,又水了一篇 blog
MX-S11

【知识要点】栈、队列、堆、优先队列

1. 栈(Stack)和队列(Queue)基础

1.1 基本概念对比

数据结构特性操作位置C++声明插入操作删除操作
栈(Stack)LIFO (后进先出)同一端(栈顶)stack<T> s;s.push()s.pop()
队列(Queue)FIFO (先进先出)一端插入(队尾),另一端删除(队头)queue<T> q;q.push() (enqueue)q.pop() (dequeue)

1.2 队列的详细定义

  • 队列是C++标准库中的重要数据结构
  • 允许删除的一端称为队头(Front)
  • 允许插入的一端称为队尾(Rear)
  • 空队列:队列中没有元素的状态
  • 操作原则:先进先出(FIFO)

1.3 C++队列基本操作示例

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

int main() {
    queue<int> q; // 定义一个空队列
  
    for(int i=0; i<5; i++) {
        q.push(i); // 将i插入到队尾
    }
  
    q.emplace(32); // 将32放置到队尾,作用和push一样
  
    cout << q.size() << endl; // 输出队列中元素的个数
  
    while(!q.empty()) { // 当队列不为空时,继续循环
        cout << q.front() << endl; // 输出队列中的首元素
        q.pop(); // 将元素从队头删除
    }
  
    return 0;
}

1.4 队列queue的完整函数列表

  • q.pop() - 删除queue的队头元素
  • q.front() - 返回队列的队头元素,但不删除
  • q.back() - 返回队列的队尾元素,但不删除
  • q.push(arg) - 将元素arg插入到队列的队尾
  • q.emplace(arg) - 将元素arg放置到队列的尾部(效果同push)
  • q.size() - 返回队列中元素的个数
  • q.empty() - 队列为空时返回true,否则false
  • q.swap(q1) - 交换q和q1中的元素(交换底层数据结构)
  • swap(q,q1) - 非成员函数,效果同成员函数swap

1.5 队列的应用场景

  • BFS(广度优先搜索)算法
  • 单调队列
  • 缓冲区管理
  • 任务调度

2. 堆(Heap)与优先队列(Priority Queue)

2.1 堆的基本概念

  • 堆是一类特殊的数据结构,常用的是二叉堆
  • 形式上是一个数组,本质上是一棵完全二叉树
  • 分为大根堆和小根堆:

    • 大根堆:除根节点外,每个节点的值都大于等于其子节点的值

      • 公式表示:A[parent(i)] >= A[i]
    • 小根堆:除根节点外,每个节点的值都小于等于其子节点的值

      • 公式表示:A[parent(i)] <= A[i]
  • 堆中的任一子树也还是堆

2.2 堆的基本操作

  • push操作(插入):往堆尾加入元素,并通过从下往上调整保持堆性质
  • get操作(删除):取出堆顶元素,用堆尾元素覆盖堆顶,再从上往下调整

2.3 优先队列的实现

C++中用 priority_queue实现堆的功能:

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

int main() {
    // 默认大根堆
    priority_queue<int> max_heap;
  
    // 小根堆定义方式
    priority_queue<int, vector<int>, greater<int>> min_heap;
  
    // 自定义结构体的优先队列
    struct Node {
        int u, len;
        bool operator < (const Node &x) const {
            return x.len < len; // 注意这里是反向定义,实现小根堆效果
        }
    };
  
    priority_queue<Node> custom_heap;
  
    return 0;
}

2.4 优先队列的操作函数

  • push(Elem e) - 插入元素,时间复杂度O(log n)
  • pop() - 删除顶部元素,O(log n)
  • top() - 返回顶部元素,O(1)
  • size() - 返回元素数量
  • empty() - 判断是否为空

2.5 优先队列的特殊注意事项

  1. 不支持除顶部外其他元素的访问和操作
  2. 没有内置的clear()操作,需要手动实现:
void clear(priority_queue<int> &q) {
    while (!q.empty()) q.pop();
}
  1. 访问top()前必须检查队列是否为空

3. 经典问题与解决方案

3.1 合并果子问题

问题描述:有n堆果子,每次合并两堆消耗体力值为两堆果子数之和,求最小总消耗。

解决方案:

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

int main() {
    int n, ans = 0;
    cin >> n;
    priority_queue<int, vector<int>, greater<int>> q;
  
    for(int i = 0; i < n; i++) {
        int x; cin >> x;
        q.push(x);
    }
  
    while(q.size() > 1) {
        int x = q.top(); q.pop();
        int y = q.top(); q.pop();
        ans += x + y;
        q.push(x + y);
    }
  
    cout << ans;
    return 0;
}

时间复杂度分析:O(n log n)

3.2 互数问题

问题描述:给定素数集合S,生成由S中素数乘积构成的"互数集合",求第n小的互数。

解决方案:

typedef long long ll;

int main() {
    ll k, n;
    cin >> k >> n;
    vector<ll> primes(k);
    priority_queue<ll, vector<ll>, greater<ll>> heap;
    set<ll> seen;
  
    for(int i = 0; i < k; i++) {
        cin >> primes[i];
        heap.push(primes[i]);
        seen.insert(primes[i]);
    }
  
    ll humble = 1;
    for(int i = 0; i < n; i++) {
        humble = heap.top(); heap.pop();
        for(auto p : primes) {
            ll num = humble * p;
            if(seen.find(num) == seen.end()) {
                seen.insert(num);
                heap.push(num);
            }
        }
    }
  
    cout << humble;
    return 0;
}

时间复杂度分析:O(nk log n)

3.3 两个序列的最小N个和

问题描述:有两个长度为N的有序序列A和B,求A和B中各取一个数相加得到的N²个和中最小的N个。

解决方案:

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

const int maxn = 1e5 + 10;

struct HeapNode {
    int x, val;
    bool operator < (const HeapNode &other) const {
        return val > other.val; // 小根堆
    }
};

int main() {
    int n, a[maxn], b[maxn], pos[maxn] = {0};
    priority_queue<HeapNode> q;
  
    cin >> n;
    for(int i = 0; i < n; i++) cin >> a[i];
    for(int i = 0; i < n; i++) cin >> b[i];
  
    // 初始时每个a[i]配b[0]
    for(int i = 0; i < n; i++) {
        q.push({i, a[i] + b[0]});
    }
  
    for(int i = 0; i < n; i++) {
        HeapNode tmp = q.top(); q.pop();
        cout << tmp.val << " ";
        int x = tmp.x;
        if(++pos[x] < n) {
            q.push({x, a[x] + b[pos[x]]});
        }
    }
  
    return 0;
}

时间复杂度分析:O(n log n)

4. 高级应用提示

  1. Dijkstra算法优化:堆/优先队列可用于优化Dijkstra最短路径算法,将时间复杂度从O(V²)降低到O(E + V log V)
  2. 单调队列:一种特殊的队列,可以高效解决滑动窗口最值问题
  3. 多路归并:优先队列可以高效解决多路归并问题,如上面的最小N个和问题
  4. Huffman编码:优先队列可用于构建最优前缀编码树

5. 总结

数据结构特点主要操作时间复杂度典型应用
栈(Stack)LIFOpush, pop, topO(1)函数调用、表达式求值
队列(Queue)FIFOpush, pop, frontO(1)BFS、缓冲区
优先队列(Priority Queue)自动排序push, pop, toppush/pop: O(log n), top: O(1)Dijkstra、Huffman编码、合并果子

掌握这些基础数据结构及其应用场景,对于算法学习和编程竞赛至关重要。优先队列作为堆的高级抽象,在实际应用中更为方便,但理解其底层堆的原理同样重要。

【熟能生巧】

P9588 「MXOI Round 2」队列

题目描述

小 C 有一个队列,他要对这个队列进行 $q$ 次操作。操作共四种,参数分别如下:

$1\ x$:这是第一种操作,表示从队尾依次插入 $1,2,3,\cdots,x$;

$2\ y$:这是第二种操作,表示弹出队头的前 $y$ 个元素;

$3\ z$:这是第三种操作,表示查询队列中的第 $z$ 个元素;

$4$:这是第四种操作,表示查询队列中所有元素的最大值。

你需要帮助他维护这个队列,并对于每个第三种操作和第四种操作,输出查询的答案。

输入格式

第一行两个整数 $c,q$,其中 $c$ 表示测试点编号。$c=0$ 表示该测试点为样例。

接下来 $q$ 行,每行 $1 \sim 2$ 个整数,表示一个操作,格式见【题目描述】。

输出格式

对于每个第三种操作和第四种操作,输出一行一个整数,表示查询的答案。

输入输出样例 #1

输入 #1

0 9
1 5
1 3
2 2
1 4
3 6
3 8
2 4
4
3 3

输出 #1

3
2
4
1

说明/提示

【样例解释 #1】

在进行第四次操作后,队列中的元素依次为 $3,4,5,1,2,3,1,2,3,4$。

在进行第七次操作后,队列中的元素依次为 $2,3,1,2,3,4$。

【数据范围】

设 $\sum x$ 表示单个测试点内 $x$ 之和。

对于 $100\%$ 的数据,$1 \le q \le 2\times 10^5$,$1 \le x,y,z \le 10^9$,$0 \le \sum x \le 2\times10^{14}$,保证在进行第二种操作前队列内元素个数不小于 $y$,在进行第三种操作前队列内元素个数不小于 $z$,在进行第四种操作前队列内元素个数大于 $0$。

测试点编号$q \le$$x \le$$\sum x \le$特殊性质
$1\sim3$$500$$500$$2\times10^5$C
$4\sim8$$5000$$5000$$2\times10^7$无
$9\sim10$$2\times10^5$$10^9$$2\times10^{14}$AB
$11\sim12$$2\times10^5$$10^9$$2\times10^{14}$B
$13\sim14$$2\times10^5$$10^9$$2\times10^9$AC
$15\sim16$$2\times10^5$$10^9$$2\times10^9$C
$17\sim18$$2\times10^5$$500$$2\times10^7$无
$19$$2\times10^5$$10^9$$2\times10^9$无
$20$$2\times10^5$$10^9$$2\times10^{14}$无

特殊性质 A:没有第二种操作。

特殊性质 B:没有第三种操作。

特殊性质 C:没有第四种操作。

P2723 [USACO3.1] 丑数 Humble Numbers

题目描述

对于一给定的素数集合 $S = \{ p_1, p_2, ..., p_k \}$, 考虑一个正整数集合,该集合中任一元素的质因数全部属于 $S$。这个正整数集合包括,$p_1$、$p_1 \times p_2$、$p_1 \times p_1$、$p_1 \times p_2 \times p_3$ ...(还有其它)。该集合被称为 $S$ 集合的“丑数集合”。注意:我们认为 $1$不是一个丑数。

你的工作是对于输入的集合 $S$ 去寻找“丑数集合”中的第 $n$ 个“丑数”。保证答案可以用 32 位有符号整数表示。

补充:丑数集合中每个数从小到大排列,每个丑数都是素数集合中的数的乘积,第 $n$ 个“丑数”就是在能由素数集合中的数相乘得来的(包括它本身)第 $n$ 小的数。

输入格式

输入的第一行是两个的整数,分别代表集合 $S$ 的大小 $k$ 和给定的参数 $n$。

输入的第二行有 $k$ 互不相同的整数,第 $i$ 个整数代表 $p_i$。

输出格式

输出一行一个整数,代表答案。

输入输出样例 #1

输入 #1

4 19
2 3 5 7

输出 #1

27

说明/提示

数据规模与约定

对于 $100\%$ 的数据,保证:

  • $1 \leq k \leq 100$。
  • $1 \leq n \leq 10^5$。
  • $2 \leq p_i < 2^{31}$,且 $p_i$ 一定为质数。

说明

题目翻译来自 NOCOW。

USACO Training Section 3.1

P1323 删数问题

题目描述

一个集合有如下元素:$1$ 是集合元素;若 $P$ 是集合的元素,则 $2\times P+1$,$4\times P+5$ 也是集合的元素。

取出此集合中最小的 $k$ 个元素,按从小到大的顺序组合成一个多位数,现要求从中删除 $m$ 个数位上的数字,使得剩下的数字最大,编程输出删除前和删除后的多位数字。

注:不存在所有数被删除的情况。

输入格式

只有一行两个整数,分别代表 $k$ 和 $m$。

输出格式

输出为两行两个整数,第一行为删除前的数字,第二行为删除后的数字。

输入输出样例 #1

输入 #1

5  4

输出 #1

137915
95

说明/提示

数据规模与约定

  • 对于 $30\%$ 的数据,保证 $1\le k,m\le300$。
  • 对于 $100\%$ 的数据,保证 $1\le k,m\le3\times10^4$。

P2085 最小函数值

题目描述

有 $n$ 个函数,分别为 $F_1,F_2,\dots,F_n$。定义 $F_i(x)=A_ix^2+B_ix+C_i(x\in\mathbb N*)$。给定这些 $A_i$、$B_i$ 和 $C_i$,请求出所有函数的所有函数值中最小的 $m$ 个(如有重复的要输出多个)。

输入格式

第一行输入两个正整数 $n$ 和 $m$。

以下 $n$ 行每行三个正整数,其中第 $i$ 行的三个数分别为 $A_i$、$B_i$ 和 $C_i$。

输出格式

输出将这 $n$ 个函数所有可以生成的函数值排序后的前 $m$ 个元素。这 $m$ 个数应该输出到一行,用空格隔开。

输入输出样例 #1

输入 #1

3 10
4 5 3
3 4 5
1 7 1

输出 #1

9 12 12 19 25 29 31 44 45 54

说明/提示

数据规模与约定

对于全部的测试点,保证 $1 \leq n,m\le10000$,$1 \leq A_i\le10$,
$0 \leq B_i\le100$,
$0 \leq C_i\le10^4$。

【答案校对】

队列

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=800010;
int c,T,t,i,cnt,op,x,del,qwq,f[N],l[N],r[N],a[N*2],h=1,n=400005;
int read(){
    int f=1,x=0;char ch=getchar();
    while(ch<'0'||ch>'9'){ 
        if(ch=='-') f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9') x=x*10+ch-'0',ch=getchar();
    return f*x;
}
void update(int x,int l,int r,int ql,int qr,int k){
    int mid=(l+r)>>1;
    if(ql<=l&&r<=qr){
        a[x]=k;return;
    }
    if(ql<=mid) update(x*2,l,mid,ql,qr,k);
    if(qr>mid) update(x*2+1,mid+1,r,ql,qr,k);
    a[x]=max(a[x*2],a[x*2+1]);
}
int query(int x,int l,int r,int ql,int qr){
    int mid=(l+r)>>1,sum=0;
    if(ql<=l&&r<=qr){
        return a[x];
    }
    if(ql<=mid) sum=max(sum,query(x*2,l,mid,ql,qr));
    if(qr>mid) sum=max(sum,query(x*2+1,mid+1,r,ql,qr));
    return sum;
}
signed main(){
    cin>>c>>T;
    while(T--){
        op=read();
        if(op==1)
            x=read(),l[++t]=1,r[t]=x,update(1,1,n,t,t,x),f[t]=f[t-1]+x;
        else if(op==2){
            x=read();cnt=0;
            for(i=h;i<=t;++i){
                cnt+=r[i]-l[i]+1;
                if(cnt>=x){
                    cnt-=r[i]-l[i]+1;
                    l[i]+=x-cnt;del+=x-cnt;
                    if(l[i]>r[i])update(1,1,n,i,i,0),++h;
                    break;
                }
                del+=r[i]-l[i]+1;r[i]=l[i]=0;++h;
                update(1,1,n,i,i,0);
            }
        }
        else if(op==3){
            x=read();
            qwq=lower_bound(f+h,f+t+1,del+x)-f;
            printf("%lld\n",del+x-f[qwq-1]);
        }
        else printf("%lld\n",query(1,1,n,h,t));
    }
}

丑数

#include <bits/stdc++.h>
#define int long long
using namespace std;
int n,m,i,j,a[105],b[105],s[100005],zx;
signed main(){
    cin>>n>>m;
    for(i=1;i<=n;i++)
        cin>>a[i];
    s[0]=1;
    for(i=1;i<=m;i++){
        zx=pow(2,31)-1;
        for(j=1;j<=n;j++){
            while(a[j]*s[b[j]]<=s[i-1])
                b[j]++;
            if(a[j]*s[b[j]]<zx)
                zx=a[j]*s[b[j]];
        }
        s[i]=zx;
    }
    cout<<s[m];
}

删数问题

#include <bits/stdc++.h>
#define int long long
using namespace std;
priority_queue<int,vector<int>,greater<int> > q;
string p;
int k,m,s;
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(NULL);cout.tie(NULL);
    cin>>k>>m;
    q.push(1);
    while(k){
        int s=q.top();
        p=p+to_string(s);
        q.pop();
        q.push(s*2+1);
        q.push(s*4+5);
        k--;
    }
    cout<<p<<"\n";
    while(true){
        for(int i=0;i<p.size();i++){
            if(p[i+1]>p[i]){
                s++;
                p.erase(i,1);
                if(s>=m){
                    cout<<p;
                    exit(0);
                }
                break;
            }
        }
    }
}

最小函数值

#include <bits/stdc++.h>
#define int long long
using namespace std;
int a[10001],b[10001],c[10001],n,m,s[10000001],i,j,t;
signed main(){
    cin>>n>>m;
    for(i=1;i<=n;i++) cin>>a[i]>>b[i]>>c[i];
    for(i=1;i<=n;i++) for(j=1;j<=100;j++) s[++t]=a[i]*j*j+b[i]*j+c[i];
    sort(s+1,s+1+t);
    for(i=1;i<=m;i++) cout<<s[i]<<" ";
}

T1

【题面】T678583 A美丽数 (beauty)

题目描述

小 B 对数字有着独到的见解,他认为一个数是美丽数当且仅当这个数的相邻两位都不相同。

例如12345、12121是美丽数,而123321、11不是美丽数。
现在小B想知道,对于所有满足以下条件的数中,最小的那个数是什么?如果不存在这样的美丽数,则输出-1。

· 对 于i(0≤i≤9), 这个数中是i 的位数一共有ai 个。

· 这个数不能有前导0,即012是不合法的。
有多组测试点,每组测试点现在给你a0~a9, 请你回答小B 的问题。

输入格式

第一行包含一个整数T。

接下来T 行,每行10个整数, a0~a9, 含义如题。

输出格式

输出T, 每行一个整数,表示答案。

输入输出样例 #1

输入 #1

12
6 0 0 0 0 0 0 0 0 5
0 1 0 2 0 0 0 0 0 0
3 0 0 0 2 0 0 1 0 0
0 1 2 3 4 5 6 7 8 9
1 2 0 1 5 0 1 0 0 0
2 1 1 0 1 5 0 0 0 0
6 0 1 0 2 0 0 0 1 0
8 1 0 0 1 0 0 0 0 0
5 2 0 0 2 0 1 0 0 0
3 0 0 0 0 3 0 2 1 1
3 1 0 5 0 0 1 0 0 0
0 1 2 0 0 1 0 1 0 5

输出 #1

-1
313
404070
123234345454565656767676787897979898989898989
1404143464
1505052545
-1
-1
1010404060
5050507879
1303030363
1929295979

说明/提示

对于100%的数据,保证:T≤10,1≤n,ai≤ 10⁵, 其中$n=∑ai(0≤i≤9)$
测试点编号数据范围特殊性质
1~2n≤10无
3~4n≤10³无
5~6无限制A
7~8无限制B
9~10无限制无

A: 保证最多只有2个ai>0。

B: 保证最多只有3个ai>0。

【代码】T678583 A美丽数 (beauty)

#include <bits/stdc++.h>
using namespace std;
int t,i,d,p,m,c,w,l,g,a[11],cnt[11],f;
string s;
bool j(int t,int b=-1){
    int mx=0;
    for(i=1;i<=10;i++){
        if(i-1==b) continue;
        if(cnt[i]>mx) mx=cnt[i];
    }
    return mx\*2<=t+1;
}
int main(){
    cin>>t;
    while(t--){
        for(i=1;i<=10;i++) cin>>a[i];
        m=c=0;l=-1;s="";
        for(i=1;i<=10;i++) m+=a[i];
        if(m==0){cout<<-1<<"\\n";continue;}
        for(i=1;i<=10;i++) if(a[i]>c) c=a[i];
        if(c>(m+1)/2){cout<<-1<<"\\n";continue;}
        for(i=1;i<=10;i++) cnt[i]=a[i];
        for(p=0;p<m;p++){
            f=0;
            for(d=1;d<=10;d++){
                g=d-1;
                if(cnt[d]==0) continue;
                if(g==l) continue;
                if(p==0&&g==0) continue;
                cnt[d]--;
                if(j(m-p-1,g)){
                    s+=char('0'+g);
                    l=g;
                    f=1;
                    break;
                }
                cnt[d]++;
            }
            if(!f){cout<<-1<<"\\n";break;}
        }
        if(f) cout<<s<<"\\n";
    }
}

试题

上海市2025CSP-J十连测Round 7.pdf

大感

我们这一次并没有批分数,所以关于上海市2025CSP-J十连测Round 7的内容我们会分为$Part 1$和$Part 2$
这一次我估计很难

上海市2025CSP-J十连测Round 7 题干

一、单项选择题

共15题、每题2分,共计30分,每题有且仅有一个正确选项。

  1. 如果开始时计算机处于小写输入状态,现在有一只小老鼠反复按照CapsLock、字母键A、字母键S和字母键D的顺序循环按键,即CapsLock、A、S、D、CapsLock、A、S、D……则屏幕上输出的第2025个字符是字母()。
    A. A
    B. S
    C. D
    D. a
  2. 在8位二进制补码中,10101011表示的是十进制下的()。
    A. -85
    B. -43
    C. 43
    D. 85
  3. 给定一个含N个不相同数字的数组,在最坏情况下,找出其中最大或最小的数,至少需要$N-1$次比较操作。则最坏情况下,在该数组中同时找最大与最小的数至少需要()次比较操作。(「」表示向上取整,⌊⌋表示向下取整)
    A. $\left\lceil\frac{3N}{2}\right\rceil - 2$
    B. $\left\lfloor\frac{3N}{2}\right\rfloor - 2$
    C. $2N - 2$
    D. $2N - 4$
  4. 表达式$a*(b+c)-d$的后缀表达形式为()。
    A. $abc d*+-$
    B. $abc+*d-$
    C. $abc*+d-$
    D. $-+*abcd$
  5. 约定二叉树的根节点高度为1。一棵结点数为2025的二叉树最少有()个叶子结点;一棵结点数为2025的二叉树最小的高度值是()。
    A. 1,11
    B. 1,12
    C. 2,11
    D. 2,12
  6. 向一个栈顶指针为$hs$的链式栈中插入一个指针s指向的结点时,应执行()。
    A. $hs \to next = s;$
    B. $s \to next = hs; hs = s;$
    C. $s \to next = hs \to next; hs \to next = s;$
    D. $s \to next = hs; hs = hs \to next$
  7. 2-3树是满足两个条件的树:①所有叶结点到根的路径长度相同;②所有非叶子结点有两个或三个子结点。如果一棵2-3树有10个叶结点,那么它可能有()个非叶结点。
    A. 3
    B. 4
    C. 6
    D. 8
  8. 由四个没有区别的点构成的简单无向连通图的个数是()。
    A. 6
    B. 7
    C. 8
    D. 9
  9. 设含有10个元素的集合的全部子集数为S,其中由7个元素组成的子集数为T,则$\frac{T}{S}$的值()。
    A. $\frac{5}{32}$
    B. $\frac{15}{128}$
    C. $\frac{1}{8}$
    D. $\frac{21}{125}$
  10. 如下图所示,共有13个格子。对任何一个格子进行一次操作,会使得它自己以及与它上下左右相邻的格子中的数字改变(由1变0,或由0变1)。现在要使得所有的格子中的数字都变为0,至少需要()次操作。
    A. 3
    B. 4
    C. 5
    D. 6
  11. 一个1×8的方格图形(不可旋转)用黑、白两种颜色填涂每个方格。如果每个方格只能填涂一种颜色,且不允许两个黑格相邻,共有()种填涂方案。
    A. 8
    B. 55
    C. 56
    D. 64
  12. 设G是有n个结点、m条边($n \leq m$)的连通图,必须删去G的()条边,才能使得G变成一棵树。
    A. $m - n + 1$
    B. $m - n$
    C. $m + n + 1$
    D. $n - m + 1$
  13. 有7个一模一样的苹果,放到3个一样的盘子中,一共有()种放法。
    A. 7
    B. 8
    C. 21
    D. 2187
  14. 若$f_0 = 0$,$f_1 = 1$,$f_{n+1} = \frac{f_n + f_{n-1}}{2}$,则随着i的增大,$f_i$将接近于()。
    A. $\frac{1}{2}$
    B. $\frac{2}{3}$
    C. $\frac{\sqrt{5} - 1}{2}$
    D. 1
  15. 以下是32位机器和64位机器的区别是()。
    A. 显示器不同
    B. 硬盘大小不同
    C. 寻址空间不同
    D. 输入法不同

二、阅读程序

判断题1分,选择题3分,共计40分;判断题正确填T,错误填F。

第1题

int solve1(int n) {
    n++;
    int size = 0;
    int digit[16];
    int pow = 1;
    int s = 0;
    while (n > 0) {
        digit[size] = n % 10;
        size++;
        n /= 10;
        s += pow;
        pow *= 5;
    }
    while (size > 0) {
        --size;
        pow /= 5;
        s += (digit[size] / 2) * pow;
        if (digit[size] % 2 == 0) break;
    }
    return s - 1;
}

int solve2(int n) {
    int s = 0;
    for (int i = 1; i <= n; i += 2) {
        int t = i;
        bool pass = true;
        while (t > 0) {
            int x = t % 10;
            if (x % 2 == 0) {
                pass = false;
                break;
            }
            t /= 10;
        }
        if (pass) s++;
    }
    return s;
}

保证solve1与solve2的参数n是非负整数。

判断题

  1. solve1(11)的返回值为6()。
  2. solve2(12345)的返回值为906()。
  3. 对所有的$n > 0$,必有solve1(n) <= n()。
  4. solve1(0)的返回值为0()。

选择题

  1. 关于solve1(n)与solve2(n)的大小关系,当$n \geq 0$时,下列说法正确的是()。
    A. 必然有solve1(n) < solve2(n)
    B. 必然有solve1(n) == solve2(n)
    C. 只有当n为偶数时,才有solve1(n) = solve2(n)
    D. 只有当n为奇数时,才有solve1(n) == solve2(n)
  2. 若将solve1函数中第一句n++去掉,将solve2函数中for循环的i += 2改为i += 1,则新返回值与原来相比()。
    A. solve1不变,solve2变大
    B. solve1不变,solve2变小
    C. solve1变小,solve2不变
    D. solve1变大,solve2不变
  3. solve1(n)与solve2(n)的时间复杂度为()。
    A. $\Theta(logn)$、$\Theta(logn^2)$
    B. $\Theta(logn)$、$\Theta(n)$
    C. $\Theta(logn)$、$\Theta(nlogn)$
    D. $\Theta(n)$、$\Theta(n^2)$

第2题

int a[maxn];
int b[maxn];
int n, m;
const long long mod = 1000000007;
bool filled[maxn][maxn];
long long mem[maxn][maxn];

long long solve(int i, int j) {
    if (filled[i][j]) return mem[i][j];
    if (i == n) return 1;
    if (j == m) return 1;
    filled[i][j] = true;
    if (a[i] == b[j]) {
        long long sum = solve(i + 1, j) + solve(i, j + 1);
        return mem[i][j] = sum % mod;
    } else {
        return mem[i][j] = (solve(i + 1, j) - solve(i + 1, j + 1)) % mod;
    }
}

int main() {
    std::cin >> n >> m;
    for (int i = 0; i < n; ++i) std::cin >> a[i];
    for (int i = 0; i < m; ++i) std::cin >> b[i];
    std::cout << (solve(0, 0) + mod) % mod << "\n";
}

判断题

  1. 若依次输入数据2 2 1 3 3 1,程序返回的结果是3()。
  2. 如果$n = 0$或$m = 0$,程序返回的结果是1()。
  3. 如果两个序列完全相同,程序返回的结果是$2^n$()。
  4. 输出时,执行solve(0,0)+mod是多余的运算()。

选择题

  1. 该程序的主要功能是()。
    A. 计算两个序列的最长公共子序列的长度
    B. 计算两个序列的公共子序列的数量
    C. 计算两个序列的公共子串的数量
    D. 计算两个序列的相同元素个数
  2. 该程序的时间复杂度为()。
    A. $\Theta(n + m)$
    B. $\Theta(n \cdot m)$
    C. $\Theta(2^{m + n})$
    D. $\Theta(nlogm)$
  3. 当$a[i] = b[j]$时,返回值需要减去solve(i+1,j+1)的原因是()。
    A. 排除重复情况
    B. 排除错误情况
    C. 优化程序的空间效率
    D. 优化程序的时间效率

第3题

long long k;
int n;
int p[20];
bool used[20] = {false};
long long frac[20];

void gen(int i) {
    if (i > n) {
        for (int i = 1; i <= n; ++i) std::cout << p[i] << " ";
        return;
    }
    for (int a = 1; a <= n; ++a) if (!used[a]) {
        if (k <= frac[n - i]) {
            p[i] = a;
            used[a] = true;
            gen(i + 1);
            return;
        } else {
            k -= frac[n - i];
        }
    }
}

int main() {
    n = 1;
    std::cin >> k;
    frac[0] = frac[1] = 1;
    while (k > frac[n]) {
        k -= frac[n];
        frac[n + 1] = frac[n] * (n + 1);
        n++;
    }
    gen(1);
}

判断题

  1. 当输入k为1时候,输出1()。
  2. 当输入k为1000时候,输出1 5 2 4 3()。

选择题

  1. 关于程序输出的序列所满足的性质,错误的是()。
    A. 输出的每个数字各不相同
    B. 输出的数字都在1到n之间
    C. 输出的相邻的数字差距不会超过1
    D. 最先输出的数字在程序中是最先被确定的
  2. 下列说法正确的是()。
    A. 越大的k一定会输出越长的序列
    B. k若是奇数,输出序列的长度一定也是奇数
    C. 程序的时间复杂度为(k!)
    D. gen递归是树形递归
  3. 若程序输出的第一个数字为2,第二数字为4,剩下还有三个数字,则输入的k()。
    A. 最小值是60,最大值是65
    B. 最小值是65,最大值是70
    C. 最小值是70,最大值是75
    D. 最小值是75,最大值是80
  4. 如果使得输出序列出现7,则输入k最少需要()。
    A. 343
    B. 874
    C. 2170
    D. 5040

三、完善程序

单选题,每小题3分,共计30分。

第1题

有一个用户,在连续的n天里,都会收到积分,也会消费积分。积分在获得后的m天内有效(m为一个给定的整数),过期失效。在第i天,用户将会获得$p_i$分,他需要消费$c_i$分。若积分不足,则用掉全部积分后用其他方式消费。消费积分时,先用最早的。当天获取的积分可以当天消费。请计算这个用户一共消费了多少积分。

const int max_size = 100000;
int queue[max_size];
int head = 0;
int tail = 0;

int main() {
    int n, m;
    std::cin >> n >> m;
    int sum = 0;
    for (int i = 0; i < n; ++i) {
        int p, c;
        std::cin >> p >> c;
        queue[_(1)_] = p;
        while (_(2)_) {
            if (_(3)_) {
                queue[head] -= c;
                sum += c;
                c = 0;
            } else {
                int amount = _(4)_;
                c -= amount;
                sum += amount;
            }
        }
        if (_(5)_ > m) {
            head++;
        }
        std::cout << sum << "\n";
    }
}
  1. (1)处应填()。
    A. tail
    B. tail + 1
    C. i + head
    D. tail++
  2. (2)处应填()。
    A. head < tail || c > 0
    B. head < tail || p > 0
    C. head < tail && c > 0
    D. head < tail && p > 0
  3. (3)处应填()。
    A. queue[head] > c
    B. queue[tail] > c
    C. queue[head + 1] > c
    D. queue[tail - 1] > c
  4. (4)处应填()。
    A. queue[head++]
    B. queue[head]
    C. queue[tail--]
    D. queue[tail]
  5. (5)处应填()。
    A. head - tail
    B. tail - head
    C. tail + head
    D. i - head + 1

第2题

给定一个1到n的排列$p_1, p_2, ..., p_n$。请统计排列中所有长度大于等于2的连续子序列的次大数之和。定义$max_2(a_i, a_{i+1}, ..., a_j)$表示从$a_i$开始到$a_j$结束的连续子序列中,排名第二大的数,这个数就是一个连续子序列的次大数之和。题目就是要求:$\sum_{1 \leq i < j \leq n} max_2(a_i, a_{i+1}, ..., a_j)$。solve用于解决这个问题。

int q[maxn];
int prev[maxn];
int next[maxn];

long long solve(int n, int p[]) {
    for (int i = 1; i <= n; ++i) {
        _(1)_;
    }
    p[0] = q[0] = prev[0] = 0;
    p[n + 1] = q[n + 1] = next[n + 1] = n + 1;
    for (int i = 1; i <= n; ++i) {
        int num = _(2)_;
        int prev_num = p[i - 1];
        int next_num = p[i + 1];
        prev[num] = _(3)_;
        next[num] = _(4)_;
    }
    long long sum = 0;
    for (int num = 1; num <= n; ++num) {
        int prev_num = prev[num];
        int prev_prev_num = prev[prev_num];
        int next_num = next[num];
        int next_next_num = next[next_num];
        sum += (long long)num * (q[num] - q[prev_num]) * _(5)_;
        sum += (long long)num * (q[next_num] - q[num]) * _(6)_;
        _(7)_ = prev_num;
        _(8)_ = next_num;
    }
    return sum;
}
  1. (1)处应填()。
    A. $q[p[i]] = i$
    B. $q[i] = p[i]$
    C. $q[i] = i$
    D. $q[p[i]] = p[i]$
  2. (2)(3)(4)处应填()。
    A. p[i],prev_num,next_num
    B. q[i],next_num,prev_num
    C. p[i],next_num,prev_num
    D. q[i],prev_num,next_num
  3. (5)处应填()。
    A. (q[prev_num] - q[prev_prev_num])
    B. (q[next_next_num] - q[prev_num])
    C. (q[next_num] - q[prev_prev_num])
    D. (q[next_next_num] - q[next_num])
  4. (6)处应填()。
    A. (q[prev_num] - q[prev_prev_num])
    B. (q[next_next_num] - q[prev_num])
    C. (q[next_num] - q[prev_prev_num])
    D. (q[next_next_num] - q[next_num])
  5. (7)(8)处应填()。
    A. prev[prev_num],next[next_num]
    B. prev[next_num],next[prev_num]
    C. next[prev_num],prev[next_num]
    D. next[next_num],prev[prev_num]

试题

2023CSP-J.pdf

大感

说实话,我当时参加2023CSP-J还是五年级吧?当时只考了65分,这次再考,考了78.5分,分数线是72分(浙江)

错题大纲

一 单项选择

T6

小明在某一天中依次有七个空闲时间段,他想要选出至少一个空闲时间段来练习唱歌,但他希望任意两个练习的时间段之间都有至少两个空闲的时间段让他休息。则小明一共有( )种选择时间段的方案
A:31
B:18
C:21
D:33

这道题,是一道经典的排列组合题
这实际上是一个组合问题,但带有约束:选取的子集中任意两个元素至少相差3
我们可以考虑用递推或组合计数的方法。这里我们采用递推(动态规划)的方法来计数所有非空子集满足约束的方案数
定义:
令 $a_n$​ 表示在n个时间段中,选择至少一个时间段满足任意两个被选的时间段之间至少间隔2个(即索引差至少为3)的总方案数
注意:问题要求“至少一个”,即非空子集
实际上,我们可以先计算所有满足约束的子集(包括空集)的数量,然后减去1(空集)即可
设$f(n)$为在n个时间段中,满足任意两个被选时间段索引差至少为3的子集个数(包括空集)
考虑第n个时间段是否被选:

  • 如果不选第n个,那么方案数就是前n-1个的满足约束的子集数:$f(n−1)$
  • 如果选第n个,那么第n-1和n-2个都不能被选(因为要求至少间隔两个),所以前n-3个可以任意选(满足约束),因此方案数为 $f(n−3)$(注意:这里f包括空集,所以当n<3时需要小心)

因此,递推式子为:
$f(n)=f(n−1)+f(n−3)对于n≥3$
经过计算,非空为18
则答案为B

T8

后缀表达式6 2 3 + - 3 8 2 / + * 2 ^ 3 +对应的中缀表达式为( )
A ( ( 6 - ( 2 + 3 ) ) * ( 3 + 8 / 2 ) ) ^ 2 + 3
B 6 - 2 + 3 * 3 + 8 / 2 ^ 2 + 3
C ( 6 - ( 2 + 3 ) ) * ( ( 3 + 8 / 2 ) ^ 2 ) + 3
D 6 - ( ( 2 + 3 ) * ( 3 + 8 / 2 ) ) ^ 2 + 3

这种题,有一种很好的方法,就是加括号,然后移符号
例如下面这个中缀表达式
3 + 8 / 2
先给运算加括号
( 3 + ( 8 / 2 ) )
然后将符号移动到当前括号的最右侧(后缀表达式)
3 8 2 / +
记得删括号

所以,这道题先加括号,再转运算符后是
6 2 3 + - 3 8 2 / + * 2 ^ 3 +
选A

T10

假设有一组字符${a,b,c,d,e,f}$,对应的频率分别为$5%,9%,12%,13%,16%,45%$。请问以下哪个选项是字符$a,b,c,d,e,f$分别对应的一组哈夫曼编码?( )
A 1111 1110 101 100 110 0
B 1010 1001 1000 011 010 00
C 000 001 010 011 10 11
D 1010 1011 110 111 00 01

哈夫曼编码的原理是:让频率小的字母编码长度长,频率大的字母编码长度短,因此达到文章总体编码长度更短的效果,具体过程如下,但不是题目样例
image.png
image.png

T14

一个班级有10个男生和12个女生。如果要选出一个3人的小组,并且小组中必须至少包含一个女生,那么有多少种可能的组合?( )
A 1420
B 1770
C 1540
D 2200

根据题意,列出:
总共有22人(10男 + 12女),选出3人的总组合数:$C(22, 3) = \frac{22 \times 21 \times 20}{3 \times 2 \times 1} = 1540$
小组中没有任何女生(即全为男生)的组合数:$C(10, 3) = \frac{10 \times 9 \times 8}{3 \times 2 \times 1} = 120$
因此,至少包含一个女生的组合数:$\text{总组合数} - \text{全男生的组合数} = 1540 - 120 = 1420$
所以,选A

二 阅读程序

第一大题

#include <iostream>
#include <cmath>
using namespace std;

double f(double a, double b, double c) {
    double s = (a + b + c) / 2;
    return sqrt(s * (s - a) * (s - b) * (s - c));
}

int main() {
    cout.flags(ios::fixed);
    cout.precision(4);

    int a, b, c;
    cin >> a >> b >> c;
    cout << f(a, b, c) << endl;
    return 0;
}

第二大题

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int f(string x, string y) {
    int m = x.size();
    int n = y.size();
    vector<vector<int>> v(m + 1, vector<int>(n + 1, 0));
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (x[i - 1] == y[j - 1]) {
                v[i][j] = v[i - 1][j - 1] + 1;
            } else {
                v[i][j] = max(v[i - 1][j], v[i][j - 1]);
            }
        }
    }
    return v[m][n];
}

bool g(string x, string y) {
    if (x.size() != y.size()) {
        return false;
    }
    return f(x + x, y) == y.size();
}

int main() {
    string x, y;
    cin >> x >> y;
    cout << g(x, y) << endl;
    return 0;
}

这题是LCS

第三大题

#include <iostream>
#include <cmath>
using namespace std;

int solve1(int n) {
    return n * n;
}

int solve2(int n) {
    int sum = 0;
    for (int i = 1; i <= sqrt(n); i++) {
        if (n % i == 0) {
            if (n / i == i) {
                sum += i * i;
            } else {
                sum += i * i + (n / i) * (n / i);
            }
        }
    }
    return sum;
}

int main() {
    int n;
    cin >> n;
    cout << solve2(solve1(n)) << " " << solve1(solve2(n)) << endl;
    return 0;
}

三 完善程序

第一大题

(寻找被移除的元素)问题:原有长度为n+1、公差为1的等差升序数列;将数列输入到程序的数组时移除了一个元素,导致长度为n的升序数组可能不再连续,除非被移除的是第一个或最后一个元素。需要在数组不连续时,找出被移除的元素
试补全程序

#include <iostream>
#include <vector>

using namespace std;

int find_missing(vector<int>& nums) {
    int left = 0, right = nums.size() - 1;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == mid + ①) {
            ②
        } else {
            ③
        }
    }
    return ④;
}

int main() {
    int n;
    cin >> n;
    vector<int> nums(n);
    for(int i = 0; i < n; i++)cin >> nums[i];
    int missing_number = find_missing(nums);
    if (missing_number == ⑤) {
        cout << "Sequence is consecutive" << endl;
    } else {
        cout << "Missing number is " << missing_number << endl;
    }
    return 0;
}

T33

选B,
为什么不选A?
因为题目说了,除非被移除的是第一个或最后一个元素,则代表输入的第一个数可能不是1,所以需要用num[0]

T35

选C,自行模拟

第二大题

​(编辑距离)​给定两个字符串,每次操作可以选择删除Delete、 插入insert、替换Replace个字符,求将第一个字符串转换为第二个字符串所需要的最少操作次数

试 补 全 动 态 规 划 算 法

#include <iostream>
#include <string>
#include <vector>
using namespace std;

int min(int x, int y, int z) {
    return min(min(x, y), z);
}

int edit_dist_dp(string str1, string str2) {
    int m = str1.length();
    int n = str2.length();
    vector<vector<int>> dp(m + 1, vector<int>(n + 1));

    for (int i = 0; i <= m; i++) {
        for (int j = 0; j <= n; j++) {
            if (i == 0)
                dp[i][j] = ①;
            else if (j == 0)
                dp[i][j] = ②;
            else if (③)
                dp[i][j] = ④;
            else
                dp[i][j] = 1 + min(dp[i][j - 1], dp[i - 1][j], ⑤);
        }
    }
    return dp[m][n];
}

int main() {
    string str1, str2;
    cin >> str1 >> str2;
    cout << "Mininum number of operation:"
         << edit_dist_dp(str1, str2) << endl;
    return 0;
}

T40

选A
image.png

完

致谢

题目

上海市2025CSP-J十连测Round 6.pdf

大感

难度感觉很难,我只有69分,全班排No 3的状态

错题大纲

一 单项选择(错4题)

T4

如果对一个已经升序的数组进行排序,下列算法中可能花费时间反而比乱序数组多的是( )

A.堆排序
B.插入排序
C.冒泡排序
D.快速排序

先来一张统计表

排序算法平均时间复杂度最优时间复杂度最坏时间复杂度备注
A. 堆排序$O(n log n)$$O(n log n)$$O(n log n)$不稳定排序
B. 插入排序$O(n²)$$O(n)$$O(n²)$稳定排序,对小数据集高效
C. 冒泡排序$O(n²)$$O(n)$$O(n²)$稳定排序,效率较低
D. 快速排序$O(n log n)$$O(n log n)$$O(n²)$不稳定排序,平均性能最好

答案为D

原因如下:快速排序依赖于基准,如果数组已经有序,且选择第一个或最后一个元素作为基准(常见实现方式),则每次划分都会极度不平衡(因为基准是当前子数组的最小或最大值),导致递归深度达到最大($O(n)$),并且每次划分只能减少一个元素。因此,最坏时间复杂度为​$O(n²)​$,比乱序数组的平均情况($O(n log n)$)要慢得多

例如:
初始数组:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

第一次排序(以 1 为基准):
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

对右半部分 [2, 3, 4, 5, 6, 7, 8, 9, 10] 排序(以 2 为基准):
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

对右半部分 [3, 4, 5, 6, 7, 8, 9, 10] 排序(以 3 为基准):
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

对右半部分 [4, 5, 6, 7, 8, 9, 10] 排序(以 4 为基准):
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

对右半部分 [5, 6, 7, 8, 9, 10] 排序(以 5 为基准):
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

对右半部分 [6, 7, 8, 9, 10] 排序(以 6 为基准):
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

对右半部分 [7, 8, 9, 10] 排序(以 7 为基准):
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

对右半部分 [8, 9, 10] 排序(以 8 为基准):
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

对右半部分 [9, 10] 排序(以 9 为基准):
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

对右半部分 [10] 排序(只有一个元素,无需排序):
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

代码实现:

#include <iostream>
using namespace std;

// 划分函数,返回基准位置
int partition(int arr[], int low, int high) {
    int pivot = arr[low];  // 选择第一个元素为基准
    int i = low, j = high;
    while (i < j) {
        while (i < j && arr[j] >= pivot) j--; // 从右找小于基准的
        while (i < j && arr[i] <= pivot) i++; // 从左找大于基准的
        if (i < j) swap(arr[i], arr[j]);
    }
    swap(arr[low], arr[i]); // 将基准放到正确位置
    return i;
}

// 快速排序递归函数
void quickSort(int arr[], int low, int high) {
    if (low < high) {
        int pi = partition(arr, low, high); // 划分
        quickSort(arr, low, pi - 1);  // 排序左半
        quickSort(arr, pi + 1, high); // 排序右半
    }
}

// 打印数组
void printArray(int arr[], int n) {
    for (int i = 0; i < n; i++)
        cout << arr[i] << " ";
    cout << endl;
}

int main() {
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 待排序
    int n = sizeof(arr) / sizeof(arr[0]);
    
    printArray(arr, n);
    
    quickSort(arr, 0, n - 1);
    
    printArray(arr, n);
    
    return 0;
}

T6

题目:设一个栈与一个队列的初始状态均为空。元素 1、2、3、4、5、6依次进入栈,且每个元素出栈后即进入队列,若出队的顺序为2、4、3、6、5、1,则栈的容量至少应该为( )。

A 2
B 3
C 4
D 5

答案为B,模拟过程如下:

1入栈,容量为1
2入栈,容量为2
2出栈,容量为1
3入栈,容量为2
4入栈,容量为3
4出栈,容量为2
3出栈,容量为1
5入栈,容量为2
6入栈,容量为3
6出栈,容量为2
5出栈,容量为1
1出栈,容量为0

期间最大容量为3,所以栈容量至少为3

T7

题目:整型数组a中有n个元素,能计算a中有多少个数字大于lower且小于upper的函数,应该将下划线依次替换为( )

int solve(int a[], int n, int lower, int upper)
{
    std::sort(a, a + n);
    auto begin = std::_____(a, a + n, lower);
    auto end = std::_____(a, a + n, upper);
    return end - begin;
}
A lower_bound、lower_bound
B lower_bound、upper_bound
C upper_bound、lower_bound
D upper_bound、upper_bound

答案为B

原因:

先说好,lb和up是C++的STL容器

  • lower_bound:返回指向第一个大于或等于目标值的元素的迭代器
  • upper_bound:返回指向第一个大于目标值的元素的迭代器

T11

题目:设有一个含有13个元素的哈希表(0 12),哈希函数是H(key)=key%13。用线性探查法解决冲突,则对于序列{2,8,31,20,19,18,53,27},18应放在下标为( )的位置

A 0
B 4
C 5
D 9

答案为D
核心规则回顾

  1. 哈希函数:H(key) = key % 13,计算元素的初始哈希位置。
  2. 线性探查法:若初始位置已被占用,依次检查下一个位置(下标+1,超过12则循环到0),直到找到空位置。
  3. 哈希表下标范围:0~12,共13个位置。
    逐元素计算存储位置(重点跟踪18)

1. 元素2

  • 初始哈希位置:H(2) = 2 % 13 = 2
  • 位置2为空,存储2 → 哈希表:[空, 空, 2, 空, 空, 空, 空, 空, 空, 空, 空, 空, 空]

2. 元素8

  • 初始哈希位置:H(8) = 8 % 13 = 8
  • 位置8为空,存储8 → 哈希表:[空, 空, 2, 空, 空, 空, 空, 空, 8, 空, 空, 空, 空]

3. 元素31

  • 初始哈希位置:H(31) = 31 % 13 = 5(13×2=26,31-26=5)
  • 位置5为空,存储31 → 哈希表:[空, 空, 2, 空, 空, 31, 空, 空, 8, 空, 空, 空, 空]

4. 元素20

  • 初始哈希位置:H(20) = 20 % 13 = 7(13×1=13,20-13=7)
  • 位置7为空,存储20 → 哈希表:[空, 空, 2, 空, 空, 31, 空, 20, 8, 空, 空, 空, 空]

5. 元素19

  • 初始哈希位置:H(19) = 19 % 13 = 6(13×1=13,19-13=6)
  • 位置6为空,存储19 → 哈希表:[空, 空, 2, 空, 空, 31, 19, 20, 8, 空, 空, 空, 空]

6. 元素18(目标元素)

  • 初始哈希位置:H(18) = 18 % 13 = 5(13×1=13,18-13=5)
  • 冲突:位置5已存储31,按线性探查检查下一个位置6;
  • 再次冲突:位置6已存储19,继续检查下一个位置7;
  • 再次冲突:位置7已存储20,继续检查下一个位置8;
  • 再次冲突:位置8已存储8,继续检查下一个位置9;
  • 位置9为空,存储18 → 18的最终下标为9。

(后续元素验证,不影响18的结果)

  • 元素53:H(53)=53%13=1(13×4=52,53-52=1),位置1为空,存储53。
  • 元素27:H(27)=27%13=1,位置1已存53,探查位置2(存2)→ 位置3(空),存储27。

结论

18应放在下标为 9 的位置,答案选 D。

二 阅读程序

第一大题

int solve1(int n)
{
    int s = 0;
    for (int i = 1; i <= n; ++i) {
        int f = 1;
        for (int j = i; j >= 1; --j) {
            f = f * j;
        }
        s = s + f;
    }
    return s;
}

int solve2(int n)
{
    int s = 0;
    for (int i = n; i >= 1; --i)
    {
        s = s + 1;
        s = s * i;
    }
    return s;
}

第二大题

bool move(int b[], int n)
{
    for (int i = 0; i < n; ++i) {
        if (b[i] == 1) {
            b[i] = 0;
        } else {
            b[i] = 1;
            return true;
        }
    }
    return false;
}

void print(int n)
{
    int b[n];
    for (int i = 0; i < n; ++i) b[i] = 0;
    do {
        for (int i = 0; i < n; ++i) std::cout << b[i];
        std::cout << "\n";
    } while (move(b, n));
}

int count(int n)
{
    int b[n];
    for (int i = 0; i < n; ++i) b[i] = 0;
    int c = 0;
    do {
        c++;
    } while (move(b, n));
    return c;
}

T22

题目:print输出的0与1必然一样多

什么是必然?
必定这样。必然是指客观事物联系和发展的合乎规律的、确定不移的趋势,是在一定条件下的不可避免性和确定性。

所以,√

模拟即可

T27

题目:以下说法错误的是( )

A.print(n)前一半输出全是偶数,后一半输出全是奇数
B.若将print(n)输出的每行内容看成一个数字,则他们是依次递增的
C.print(n)输出的每一行内容都是不同的
D.print(n)输出的每一列内容都是不同的

选B

不是依次递增

是有位数为int n的二进制,但是倒着的,也就是说,如果调用print(2),则模拟过程如下:

0 0
1 0
0 1
1 1
0 0

把他旋转一下:

0 0 》0
0 1 》1
1 0 》2
1 1 》3
0 0 》0

所以本题选B

第三大题

struct flat_map {
    struct {
        int key;
        int value;
    } bucket[65536];
    int size = 0;

    struct result {
        int index;
        bool hit;
    };

    result find(int begin, int end, int key) {
        if (begin == end)
            return {begin, false};
        else {
            int mid = begin + (end - begin) / 2;
            if (key < bucket[mid].key)
                return find(begin, mid, key);
            else if (bucket[mid].key < key)
                return find(mid + 1, end, key);
            else
                return {mid, true};
        }
    }

    int get(int key) {
        result p = find(0, size, key);
        if (p.hit)
            return bucket[p.index].value;
        else
            return 0;
    }

    void put(int key, int value) {
        result p = find(0, size, key);
        for (int i = size; i > p.index; --i) {
            bucket[i] = bucket[i - 1];
        }
        size++;
        bucket[p.index].key = key;
        bucket[p.index].value = value;
    }
};

T30

题目:若数组bucket的下标在[b,e)范围内存在键k,find(b,e,k)函数返回的hit为false

答案为×

find 函数通过二分查找,在数组 bucket 的区间 [b, e) 内查找键 k:

  • 如果找到键 k,返回 {索引, true}(hit = true)。
  • 如果没找到键 k,返回 {应该插入的位置, false}(hit = false)。

找到hit后是true,所以说法错误

T32

题目:记$s$表示容器的大小size,调用get(key)的最坏时间复杂度为( )

A $O(s)$
B $O(log s)$
C $O(s log s)$
D $O(s^2)$

根据题意,选B

log 通常指的是以 2 为底的对数(log₂)

T35

题目:若调用put(k,v)时已经存在相同的键k时,以下哪一种处理策略最符合上述代码的逻辑( )

A. 熔断 (Breaker):掷出异常,向系统报告错误
B. 回滚 (Rollback):不做任何修改,撤销put操作
C. 覆盖 (Rewrite):将键所对应的老值覆盖成新值
D. 忽略 (lgnore):对有可能出错的操作置之不理

答案为D,程序没有做特别的判断

三 完善程序

第一大题

给定含有n个顶点的有向完全图(顶点编号为$0$到$n-1$)。顶点$x$到$y$的边的权重为g[x][y]了。
请找出一条不重复经过任何点的路径,从顶点$0$出发到顶点$n-1$结束,路径上所有边的权重的异或值尽可能大。

int n, m;
long long g[MAXN][MAXN];
bool visited[MAXN] = {false};

int dfs(int node, int path) {
    if (____(1)____)
    {
        return ____(2)____;
    }

    int best = 0;
    visited[node] = true;
    for (int next = 0; next < ____(3)____; ++next)
    {
        if (____(4)____)
        {
            best = std::max(best, ____(5)____);
        }
    }
    visited[____(6)____] = ____(7)____;
}

int solve()
{
    return ____(8)____;
}

T40

node为节点,path为id

所以选A

第二大题

$n$个岛屿由$n$座桥连成环。岛的编号为$0$到$n-1$
,第$i$座桥连第$i$号岛屿第$(i+1) mod n$号岛
某旅行团从第$x_1$号岛出发,依次访问的岛编号为$x_2,...,x_m$
请选择拆掉一座桥,使得旅行团的过桥次数达到最小,令solve函数返回这个最小值

int solve(int n, int m, int x[])
{
    int diff[n];

    for (int i = 0; i < n; ++i) diff[i] = 0;
    int base = 0;
    for (int i = 1; i < m; ++i)
    {
        int prev = x[i - 1];
        int next = x[i];
        int begin, end;
        if (prev < next) {
            begin = prev;
            end = next;
        }
        else {
            begin = next;
            end = prev;
        }

        base += ____(1)____;
        int inc = ____(2)____ ;
        diff[____(3)____] += inc;
        diff[____(4)____] -= inc;
        prev = next;
    }
    int best = n * m;
    int sum = 0;
    for (int i = 0; i < n; ++i)
    {
        ____(5)____;
        if (best > sum) best = sum;
    }
    return ____(6)____;
}

T42

联系上下文即可

T44

联系上下文即可

T45

联系上下文即可

完

致谢