【题解】【LGR-(-13) 】SCP 2021 第一轮(初赛)模拟
前言
由于自己太菜了,连初赛题都不会做了,所以写篇题解充充电。
UPD(2021.9.16):
在经历了十二天的咕咕咕打磨以后,这篇题解终于赶在 CSP2021 第一轮(9.19)前公开。(虽然还是有四道题不会做……)
祝各位 CSP2021 第一轮 RP++!
完整题面 & 标准答案
见比赛界面。
题解
一、单项选择题
-
答案:A
-
解析:由于
10110111 符号位为1 ,即:为负数,所以补码等于反码加一。因此反码为10110110 ,进一步得原码为11001001 ,即-73 。 -
补充:(有符号二进制数的表示,原码、补码、反码的定义)
- 有符号二进制数的表示:最高位为符号位,正为
0 ,负为1 ;其余位表示数值。 - 原码:最直观的一种表示方法。
- 例如
6 位二进制数010010 表示18 。 - 再例如
6 位二进制数101011 表示-11
- 例如
- 反码:若为正数,则与原码相同;若为负数,则在原码的基础上逐位取反(不包括符号位)。
- 例如
4 位原码0000 对应的反码为0000 。 - 再例如
4 位原码1011 对应的反码为1100 。 - 注意有符号二进制数要区分
+0 和-0 ,+0 的4 位反码为0000 ,而-0 的4 位反码为1111 。
- 例如
- 补码:若为正数,则与原码相同;若为负数,则为反码
+1 。- 例如
5 位原码00000 对应的补码为00000 。 - 再例如
5 位原码11100 对应的补码为10100 。 - 注意:
+0 与-0 的补码相同。
- 例如
-
小试牛刀(洛谷 CSP 2020 第一轮(初赛)模拟第 1 题):
答案请前往洛谷有题 - 1033 - CSP 2020 第一轮(初赛)模拟查看。
- 有符号二进制数的表示:最高位为符号位,正为
-
闲话:我到现在才终于搞清楚这个东西……
-
答案:C
-
解析:计算过程如下:
(1920 \times 1080) \times (32 \div 8) \times 30 \times (24 \times 60) \times 25\% \div 1024^3=83.42742919921875 说明:
32 \div 8 是把“位”转成“字节”,24 \times 60 是把“分”转成“秒”,\div 1024^3 是把“字节”转成“GiB”。(使用草稿纸计算时,应适当放缩,以简便计算)
-
闲话:赛时我以为答案要乘以
(1-\text{压缩率}) ,于是选了 B。(我完全不知道怎么算,只是觉得把所有东西都乘起来是不是太简单了……)
-
答案:B
-
解析:
-
编译器:编译器对源文件进行编译,就是把源文件中的文本形式存在的源代码翻译成机器语言形式的目标文件的过程,在这个过程中,编译器会进行一系列的语法检查。如果编译通过,就会把对应的CPP转换成OBJ文件。
-
链接器:
当链接器进行链接的时候,首先决定各个目标文件在最终可执行文件里的位置。然后访问所有目标文件的地址重定义表,对其中记录的地址进行重定向(加上一个偏移量,即该编译单元在可执行文件上的起始地址)。
然后遍历所有目标文件的未解决符号表,并且在所有的导出符号表里查找匹配的符号,并在未解决符号表中所记录的位置上填写实现地址。最后把所有的目标文件的内容写在各自的位置上,再作一些另的工作,就生成一个可执行文件。
-
-
闲话:赛时我不会这道题,盲猜了个 D……
- 答案:C
- 解析:向堆内加入和删除元素的次数为
O(n) ,将堆内一个元素变小的次数为O(m) ,遍历所有边的时间复杂度O(n+m) ,因此总时间复杂度为O(m+n\log{n}) 。
- 答案:B
- 解析:每个点只会被访问一次,因此相当于把整个邻接矩阵遍历一遍,时间复杂度为
O(n^2) 。
- 答案:D
-
解析:
- 归并排序每次将当前区间分成左右两半,把问题变成两个子问题,分别解决后再合并,属于分治。
- 二叉树的前序/中序/后序遍历,依次遍历了两个子树。由于每个子树都是与当前相同的子问题,所以属于分治。
- 快速排序每次任取一个分界值,将当前区间分成小于等于这个分界值和大于等于这个分界值的两部分,把问题变成两个子问题,属于分治;
- 二叉树的层次遍历属于 BFS(宽度优先搜索),不属于分治。
-
答案:A
-
解析:拿两个栈分别存储数值和运算符,从右到左模拟即可。注意中缀表达式也要从右往左写。(不然就会选成 D)
-
补充:
中缀表达式:就是我们平常写的那种数学式子。
前缀表达式:用两个栈分别存储数值和运算符,从右往左进行如下处理:
- 把当前的数值/运算符加入对应栈。
- 若数值
\ge 2 个且运算符\ge 1 个,则从栈顶取出两个数值和一个运算符,进行运算后将运算结果加入存储数值的栈。 - 重复步骤 2,直至无法执行,并继续考虑下一个数值/运算符。(回到步骤 1)
计算过程中记得从右向左写(越靠近栈顶越靠左,2022.9.18 修改)并且注意打括号,就可以得到中缀表达式。
后缀表达式:前缀表达式的逆表达式。(事实上,由于现代一般习惯为从左向右写,所以从后缀表达式到中缀表达式的转化,大概会比前缀表达式更为直观)
-
闲话:赛时我只会后缀表达式,连中缀表达式是啥都不知道……也不知道前缀转中缀要从右往左写……最后猜了个 B。
(怎么我一猜啥,啥不对呀)
- 答案:D
-
解析:
总方案数为
5!=5 \times 4 \times 3 \times 2 \times 1=120 。设
n 个元素的(完全)错排方案数为D_n ,则有递推式D_n=(n-1)(D_{n-1}+D_{n-2}) 。说明:考虑加入第
n 个元素,由于它至多会影响之前的一个元素的位置,所以之前的元素不能有两个及以上在原位置上(即与下标相同)。若都不在原位置上,则将第n 个元素与之前的n-1 个元素中的某一个交换(有n-1 种选择);若恰有一个在原位置上,则将第n 个元素与该元素交换(在原位置上的是哪个元素有n-1 种选择)。由
D_1=0 ,D_2=1 可知D_5=44 ,于是答案为\dfrac{44}{120}=\dfrac{11}{30} 。有关错排问题,可参考OI Wiki - 错位排列。
- 答案:C
- 解析:
\lnot 是逻辑“非”,\lor 是逻辑“或”,\land 是逻辑“与”。(后两个可结合集合运算中的\cup 和\cap 来记忆)然后算就完啦~(不会吧不会吧,不会真的有人不会吧)
- 答案:B
- 解析:
O(n)<O(n^{\log_2{3}}) ,由主定理知T(n)=O(n^{\log_2{3}}) 。 - 补充:主定理部分请参考百度百科 - 主定理。(主要是懒得打 LaTeX 了)
- 闲话:赛时连主定理听都没听过的我瑟瑟发抖,猜了个 A。(2022.9.18)其实可以暴力展开。
- 答案:C
- 解析:在长度为
1 的线段上随机取一个点,划分成的两条线段的期望长度均为\dfrac{1}{2} 。(以中点为对称点即可证明)本题要取两次,而第二次取的时候是相同形式的问题,所以答案为(\dfrac{1}{2})^2 。
- 答案:D
- 解析:前三个最好情况
O(n) ,最坏情况O(n^2) ;快速排序的平均时间复杂度为O(n\log{n}) 。(此处说的快速排序应该是选取分界值比较合理的快排,例如 STL 中的 sort,不然最坏情况下时间复杂度其实是O(n^2) 的) - 闲话 1:赛时手残选成 A 了……
- 闲话 2:只要你够欧,你就可以写 random_shuffle + 选择排序/冒泡排序/插入排序,时间复杂度
O(n) ,比快排还强。(bushi)
- 答案:A
- 解析:简单图是指无重边、无自环,于是有
6 条边可供选择,答案为\binom{6}{4}=\binom{6}{2}=\dfrac{6 \times 5}{2}=15 。(由于数据很小,可直接枚举)
- 答案:B
- 解析:我不会,只能丢百度百科链接:艾伦·麦席森·图灵,约翰·冯·诺依曼,克劳德·艾尔伍德·香农,罗伯特·塔扬。
- 闲话:去年 CSP-S 第一轮,我选了冯·诺伊曼,答案是克劳德·香农。今年模拟,我选了图灵,答案是冯·诺伊曼……希望今年 CSP-S 第一轮不要有这种东西。(
有的话我多半猜不对)(2022.9.18)突然发现这不是 NOI 笔试题库里的东西吗……
- 答案:D
- 解析:D 又叫打表。
是 OIer 传统艺能。 - 闲话 1:SSH 不是省选必备技能吗,关 CSP-S 啥事(doge)
- 闲话 2:修改输入文件不是 NOIP 必备技能吗,关 CSP-S 啥事(doge)
- 闲话 3:使用 U 盘拷贝题目、下发文件以及自己的代码不是 NOI 必备技能吗,关 CSP-S 啥事(doge)
- 闲话 4:C 操作似乎真的不会被处罚,所以大家一定要发扬这种精神!(2022.9.18)然而今年 NOI 禁止了这种行为。
二、阅读程序
题面注:程序输入不超过数组或字符串定义的范围。
#include <cstdio>
#include <cstring>
using namespace std;
char s[10000];
int cnt[26];
int main() {
scanf("%s", s);
for (int i = 0; i < strlen(s); ++i) {
if (cnt[s[i] - 'a'] <= 50) {
s[strlen(s)] = s[i];
}
++cnt[s[i] - 'a'];
}
printf("%s\n", s);
return 0;
}
P.S. 第 11 行即为 for 循环。
-
答案:TFFFBD(对的选 T,错的选 F,下同)
-
解析:函数
strlen()返回的是字符串的当前长度(遇到\0时停止,\0的 ASCLL 码为0 ),因此本代码的作用为:读入字符串s ,在字符串s 末尾按照出现顺序添加小写字母,每种出现过的小写字母添加恰好51 个。(注意是51 个,而不是50 个)每道题具体解析如下:
- 此处
++i和i++是等价的(前者常数会小一些),第 1 题正确。 - 在更改以后,
for循环只会遍历输入时字符串的那部分。若某种出现的小写字母出现次数<51 ,那么添加个数会不足51 。程序运行效率会得到提升,但程序运行结果也可能会改变,故第 2 题错误。 - 首先,“不小于
50 ” 就决定了这个命题是错误的。更何况,若将50 改成51 ,则该命题等价于:在满足条件的字符串b 的长度为51 \times 26 的后缀中,每种小写字母恰好出现51 次——这显然是错误的。故第 3 题错误。 - 注意添加的小写字母一定是出现过的小写字母,第 4 题错误。
- 根据一开始的分析,两个字符串长度之差必定为
51 \times \text{出现的小写字母的种类数} ,又因为s 输入时不是空串,所以必然有x<y ,故第 5 题选 B。 - 由于在输入字符串
s 中,w 原本出现两次,所以输出时w 在s 中出现2+51=53 次。
- 此处
-
闲话:为什么到了最后一题,
51 才实实在在地开始坑人啊……不得不说,扶咕咕太良心了!
#include <cstdio>
const int N = 5010;
const int M = 20010;
const int inf = 1073741823;
int e, bg[N], nx[M], to[M], wt[M];
inline void link(int u, int v, int w) {
to[++e] = v;
nx[e] = bg[u];
wt[e] = w;
bg[u] = e;
}
int n, m, u, v, w;
int f[N], h[N << 1];
void update(int x, int y) {
x += n - 1;
for (h[x] = y; x; x >>= 1)
h[x >> 1] = f[h[x]] < f[h[x^1]] ? h[x] : h[x^1];
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i != m; ++i) {
scanf("%d%d%d", &u, &v, &w);
link(u, v, w);
}
int nn = n << 1;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j != nn; ++j)
h[j] = 0;
for (int j = 0; j <= n; ++j)
f[j] = inf;
f[i] = 0;
update(i, i);
for (int j = i; true; j = h[1]) {
if (f[j] == inf) break;
for (int k = bg[j]; k; k = nx[k]) {
if (f[j] + wt[k] < f[to[k]]) {
f[to[k]] = f[j] + wt[k];
update(to[k], to[k]);
}
}
update(j, 0);
}
for (int j = 1; j <= n; ++j)
printf("%d%c", f[j], "\n "[j != n]);
}
return 0;
}
P.S. 第 34 行是给 f 数组赋初值的 for 循环。
-
答案:TFTFDC
-
解析:由函数
link()的内容,以及if (f[j] + wt[k] < f[to[k]])就更新f数组可以看出,这是一份跟图论中的最短路有关的代码。再定睛一看,可以发现h其实是一个手写的二叉堆(小根堆),update()则是更新堆中元素大小,并维护堆的结构的函数。综上可以看出,本代码的作用为:跑n 次堆优化 Dijkstra 算法,求出以每个点为起点的单源最短路。每道题具体解析如下:
- 这份代码中的所有
!=,都是在用于判断循环变量是否达到上界,因此可以改为<,判断题第 1 题正确。 - 边数组大小为
2010 ,且每次新边的编号是++e,因此输入的边数必须不大于2009 ,判断题第 2 题错误。 - 根据
printf的内容,可知判断题第 3 题正确。 - 若将第 34 行的
j=0改为j=1,则f(0)=0 ,那么h堆顶的元素永远为0 (当且仅当起点编号为二叉堆最靠右的儿子时,h堆顶的元素第一次不为0 ,但也只有第一次),第 38 行会出现死循环,故判断题第 4 题错误。 - 由于边权均为一个相同的正整数,Dijsktra 算法实际上变成了(用堆存储的)BFS,所以点一旦入队,在出队前都不会再进行
update。又因为连通块(更严谨地说,强连通分量)大小不会超过m+1 ,所以update被调用的次数为O(n \cdot \min(n,m)) 。(注意若没有这个选项,则答案应为O(n^2) ) - 最坏情况下,以每个点为起点的单源最短路都会遍历所有边,且每条边都会更新
f数组和二叉堆h,而二叉堆的update函数单次时间复杂度为O(\log{n}) ,因此总时间复杂度为O(nm\log{n}) 。
- 这份代码中的所有
-
闲话 1:由于过去了将近一个小时才想起有初赛模拟,赛时做得很匆忙,根本没看见这个程序的单选第 1 题有
O(n\min(n,m)) 的选项,选了O(n^2) 就跑了…… -
闲话 2:这初赛模拟一看就是组的卷——阅读程序一还是 1~6,阅读程序二就是 1~4 和 1~2 了。
-
闲话 3:为什么是“‘以下’程序的输入”啊
#include <bits/stdc++.h>
using namespace std;
#define N 105
#define INF 1e9
int dis1[N][N], dis2[N][N];
int mp[N][N], n, m;
void fun1(int dis[N][N]) {
static bool vis[N];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
dis[i][j] = mp[i][j];
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) vis[j] = 0;
for (int k = 1; k <= n; k++) {
int now = 0;
for (int j = 1; j <= n; j++) {
if (!vis[j] && (!now || dis[i][now] > dis[i][j]))
now = j;
}
vis[now] = 1;
for (int j = 1; j <= n; j++) {
if(!vis[j]&&dis[i][j] > dis[i][now]+mp[now][j]){
dis[i][j] = dis[i][now] + mp[now][j];
}
}
}
}
}
void fun2(int dis[N][N]) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
dis[i][j] = mp[i][j];
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
for (int k = 1; k <= n; k++) {
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
}
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (i == j) mp[i][j] = 0;
else mp[i][j] = INF;
}
}
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
mp[u][v] = w;
}
fun1(dis1);
fun2(dis2);
int ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (dis1[i][j] != dis2[i][j])
ans++;
}
}
cout << ans << endl;
return 0;
}
P.S. 第 19 行是函数 fun1() 中的第五个 for 循环。
- 答案:TTTFBC
-
解析:函数
fun1()从每个点出发,各跑了一遍O(n^2) 的 Dijkstra 算法。函数fun2()则跑了一遍O(n^3) 的 Floyd 算法。但请注意:- 由于只保证了不存在重边,即可能有自环,则
\mathit{mp}(i,i)=0 不一定成立;若不成立,则dis1(i,...)和dis2(i,...)会整体加上mp(i,i);进一步,dis1和dis2可能不是正确的最短路。 fun2()中的 Floyd 写假了——正确的写法是先枚举中间点,即循环变量应依次为k,i,j 。
每道题的具体解析如下:
- 由上述分析知,第 1 题正确。
-
当输入为如下所示的有向图时,
\mathit{ans}=1 (构造显然不唯一):故第 2 题正确。
-
fun1()中有三层从1 到n 的for循环,且循环中没有break,因此其时间复杂度恒为O(n^3) ,第 4 题错误。(注意题目问的是fun1(),而不是 dijkstra 算法)- 手玩可以发现:
\mathit{dis1}(2,1)=7 \neq \mathit{dis2}(2,1)=\infty ,\mathit{dis1}(2,5)=8 \neq \mathit{dis2}(2,5)=\infty ,\mathit{dis1}(5,1)=10 \neq \mathit{dis2}(5,1)=\infty ,dis1和dis2的其余位置对应相等。因此输出的结果(ans)为3 。(直接按照程序模拟可能导致时间不足,要理解写假了的 Floyd 的错误本质,才能快速手玩出答案) - 我不会,求会的神仙教教我 。
- 由于只保证了不存在重边,即可能有自环,则
- 闲话 1:
fun1()函数中怎么有混合码风啊…… - 闲话 2:我在读题目条件时想:“只保证不存在重边”,也就是有可能有自环!但不知为什么,我判断第 1 题选了错……
- 闲话 3:其实我觉得判断第 4 题有歧义——总觉得它在说 dijkstra 算法,但在一番挣扎后,我成功地选了错 。
- 闲话 4:赛时最后几分钟想要 rush 第 5 题(前面说过,晚了将近一小时才开始做),结果手玩错了,选了 C……(并且当时是几乎对着程序模拟……)
完善程序
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1005;
int n;
int a[maxn], b[maxn], c[maxn];
bool Comp(const int &x, const int &y) {
// 你可以简单地认为括号内的内容等价于 (int x, int y)
return ①;
}
bool check(int x) {
for (int i = 1; i <= n; ++i) {
int u = c[i];
if (②) {
x += b[u];
} else {
return false;
}
}
return true;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d", a + i);
for (int i = 1; i <= n; ++i) scanf("%d", b + i);
for (int i = 1; i <= n; ++i) c[i] = i;
sort(c + 1, c + 1 + n, Comp);
int ans = 1145141919;
for (int l=1, r=ans, mid=(l+r)/2; ③; mid=(l+r)/2)
if (check(mid)) {
ans = mid;
④;
} else {
⑤;
}
printf("%d\n", ans);
return 0;
}
-
答案:BDBAD
-
解析:
- 贪心原则为:按
a 从小到大进行穿戴,故选 B。(注意 CD 选项可能导致 RE) c存储了按a的值从小到大排序后的下标,结合题意可知选 D。- 二分的写法有两种,一种是
l<=r,l=mid+1和r=mid-1,另一种是l<r,l=mid+1和r=mid,故选 B。 - 题目所求的是最小值——当
check返回true时,应移动右端点,故选 A。 - 题目所求的是最小值——当
check返回false时,应移动左端点,故选 D。
- 贪心原则为:按
-
闲话 1:为什么要二分啊……我猜可能是为了凑知识点,毕竟不二分似乎还简单一些……
-
闲话 2:这道题有
1145141919,下面一道题又有1919810……
#include<iostream>
using namespace std;
int main()
{
int n,m;
cin>>n>>m;
if(m==n) {
cout<<①<<endl;
return 0;
}
long long M=10000000;
int ans=②;
int lst=0;
for(int i=1;i<=n;++i) {
for(int j=1;j>=0;--j) {
int lower=max(0,③);
int upper=i-j;
int base=④;
ans+=upper-lower+1;
if(lower+base<=lst) ans-=lst-(lower+base)+1;
lst=⑤;
}
}
cout<<ans<<endl;
return 0;
}
P.S. 第 5 题 D 选项为 base + lower + 1。
-
答案:DCCBA
-
解析:
- 当
m=n 时,分数共有n+1 种取值:0,\lfloor x+1 \rfloor,\cdots,\lfloor n(x+1) \rfloor ,故选 D。 - 考虑到分数可以取
0 ,ans的初始值应为1 ,故选 C。 - 我不会,求会的神仙教教我 。
- 我不会,求会的神仙教教我 。
- 我不会,求会的神仙教教我 。
- 当
-
闲话:这紧凑码风和前面代码的码风格格不入……(虽然我也是紧凑码风)话说为什么
main()的大括号换行,但后面的大括号不换行啊……