防爆0
这篇文档是CSP第二轮考试的注意事项及防爆零指南,主要从程序题目提交要求、程序中可能存在爆0的非能力问题、未认真读题造成的粗心错误、程序性能问题、其他易犯非能力错误以及考试结束前要做的事情这几个方面进行介绍,以下是详细内容:
程序题目提交要求
- 阅读试卷注意事项
- 拿到试卷后需认真阅读注意事项。
- 提交要求
- 文件名规范
- 程序名、输入输出文件名必须用英文小写。
- C/C++中
main()
函数返回值类型必须是int
,正常结束返回值为0。 - 提交的程序代码文件放置位置按各省要求。违反上述三点申诉不予受理。
- 编译选项
- 对于C++语言是
-02 -std=c++14
。
- 对于C++语言是
- 结果比较方式
- 若无特殊说明,结果比较为全文比较(过滤行空格及文末回车)。
- 程序文件大小及内存限制
- 提交的程序源文件不大于100KB,程序可使用的栈空间内存限制与题目内存限制一致。
- 评测环境
- 全国统一评测机器配置为Inter® Core(T)i - 8700K CPU 3.70GHz,内存32GB。时限以此配置为准。只提供Linux格式附加样例文件,评测在NOI Linux下进行,各语言编译器版本以此为准。
- 题目相关信息示例(以2022入门级为例)
- 包括题目名称、类型、目录、可执行文件名、输入文件名、输出文件名、每个测试点时限、内存限制、测试点数目、测试点是否等分等信息。
- 提交文件夹结构
- 一般以考号命名文件夹(如
SX - 00001
),里面为每个题目的文件夹(以题目英文名称命名,如pow
等),每个题目文件夹内有以题目英文名称命名的.cpp
文件(如pow.cpp
)。要注意听从监考老师要求存放文件,检查文件夹目录结构、名字以及源代码名称是否正确,注意Windows系统中扩展名显示问题,文件夹内不能有其余文件,考试结束后确认文件夹是否提交成功。
- 一般以考号命名文件夹(如
- 文件名规范
程序中可能存在爆0的非能力问题
- 头文件问题
- 推荐使用万能头文件
<bits/stdc++.h>
,在Windows环境中<bits istdc++.>
也可编译,但Linux环境中需注意斜杠方向,否则编译失败导致0分。
- 推荐使用万能头文件
- 文件输入输出
- 使用要求
- 要求解题程序使用文件输入输出,通过
freopen
函数实现,写在main
函数内第一行,freopen
需要头文件<bits/stdc++.h>
,格式为freopen("xxx.in","r",stdin); freopen("xxx.out","w",stdout);
,其中xxx
为题目英文名,函数名称和参数不能写错。
- 要求解题程序使用文件输入输出,通过
- 测试样例操作
- 测试样例时,将样例输入文件放在和
.cpp
同文件夹下并修改freopen
参数,提交代码时要改回正确文件名。
- 测试样例时,将样例输入文件放在和
- 不使用文件输入输出时
- 若不使用文件输入输出方式测试,可注释掉
freopen
代码,但提交前要撤销注释并编译运行确保正确。
- 若不使用文件输入输出方式测试,可注释掉
- 使用要求
- 系统变量名冲突
- 使用标准命名空间
using namespace std;
时,不能在全局变量上定义y0
,y1
,yn
,j0
,j
,jin
,next
,time
等已在标准库中定义过的变量,否则编译错误导致0分,可定义在局部变量但不推荐。
- 使用标准命名空间
- 忘记删除过程调试代码
- 提交程序时要删除调试代码,否则会因存在不必要输出语句导致0分。
未认真读题造成的粗心错误
- 未按照题目指定规则输出
- 要注意输出的分隔方式(空格或换行)、无解情况输出以及大小写。
- 未按照题目指定规则输入
- 多组测试数据题目
- 要注意不是一组数据,处理每组数据前初始化相关变量、清空数组。
- 未看清输入格式描述
- 要仔细看清输入格式中对数据含义的描述,避免输入错误。
- 多组测试数据题目
- 数据范围看错
- 要正确判断数据范围,数组开太小会越界,开太大空间超限,一般比题目要求多开5个空间。
- 数据类型使用错误
- 要预估数据范围,避免数据溢出,根据情况选择
int
、long long
类型或高精度算法。
- 要预估数据范围,避免数据溢出,根据情况选择
程序性能问题
- 题目存在大量的输入/输出数据
- 当数据超过105个时,
cin/cout
读写速度慢可能导致超时,可选择scanf/printf
或手动实现快速读入和快速输出函数。
- 当数据超过105个时,
- 程序的复杂度过高
- 时间复杂度
- 要确保程序简单运算次数不超过1亿次(10的8次方),根据数据范围选择合适算法,避免超时。
- 空间复杂度
- 要估计内存使用,避免内存超限,注意不同变量类型所占内存大小和常用内存单位换算,不要乱开大容量数组。
- 时间复杂度
其他易犯非能力错误
- 声明局部变量要初始化,建议用全局变量或给局部变量赋初始值。
- 数组建议用全局数组,不要放在
main
函数里面。 - 浮点类型一律使用
double
,不要使用float
。 - 输出
long long
类型变量用printf
时要注意格式。 - 注意定义数据的类型,避免错误转换。
- 注意运算符优先级,不确定时用小括号控制。
- 涉及取模问题要在运算过程中取模。
- 读入带空格字符串用合适方法。
- 严格禁止使用
gets
函数。 - 看清字符数组下标起始情况。
考试结束前一定要做的
- 检查文件存放目录是否正确,包括文件夹和文件名称。
- 检查头文件是否正确完整。
- 检查文件输入输出语句及文件名是否正确。
- 检查是否删掉中间调试用的代码。
- 对每个题目的程序再编译、运行一下,确保无编译错误。
- 检查提交的文件夹内是否还有不需要的文件。
15159.选择排序
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每趟找出第 𝑖 小的元素(也就是 𝐴[𝑖 ∼ 𝑛] 中最小的元素),然后将这个元素与数组第 𝑖 个位置上的元素 𝐴[𝑖] 交换;在 𝑛 − 1 趟之后序列 𝐴 变为升序。
例如 𝐴 = [3,4,1,5,2]:
• 第 1 趟交换 𝐴[1], 𝐴[3],序列变为 [1,4,3,5,2]
• 第 2 趟交换 𝐴[2], 𝐴[5],序列变为 [1,2,3,5,4]
• 第 3 趟交换 𝐴[3], 𝐴[3],序列不变
• 第 4 趟交换 𝐴[4], 𝐴[5],序列变为 [1,2,3,4,5]
现在给定初始序列 𝐴[1 ∼ 𝑛] (保证 𝐴 是排列,即 1 ∼ 𝑛 每个数恰好出现一次)和 𝑚 个询问 𝑞[1,2, . . . , 𝑚](保证 𝑞[𝑖] < 𝑞[𝑖 + 1]),请你依次输出第 𝑞[𝑖] 趟之后的序列 𝐴。
输入描述
第一行 2 个整数 𝑛, 𝑚。
第二行 𝑛 个整数 𝐴[1 ∼ 𝑛],保证 𝐴 是排列。
第三行 𝑚 个整数 𝑞[1 ∼ 𝑚],保证 𝑞[𝑖] < 𝑞[𝑖 + 1]。
输出描述
输出 𝑚 行,第 𝑖 行包含 𝑛 个整数代表第 𝑞[𝑖] 趟之后的序列 A。
样例输入 1
5 4
3 4 1 5 2
1 2 3 4
样例输出 1
1 4 3 5 2
1 2 3 5 4
1 2 3 5 4
1 2 3 4 5
样例输入 2
6 3
6 4 2 3 1 5
1 3 5
样例输出 2
1 4 2 3 6 5
1 2 3 4 6 5
1 2 3 4 5 6
提示
数据范围与提示
对于所有数据,满足 1≤n≤105 ,1≤m≤10,1≤A[i]≤n,1≤q[i]<q[i+1]<n,保证 A 是排列。
对于测试点 1~8:n≤10
对于测试点 9~13:n≤2000
对于测试点 14~20:n≤10^5
思路:
首先理解选择排序的原理,即每趟找出第𝑖小的元素(也就是𝐴[𝑖 ∼ 𝑛] 中最小的元素),然后将这个元素与数组第𝑖个位置上的元素𝐴[𝑖] 交换,经过𝑛 − 1 趟后序列变为升序
实现方式:可以使用两个数组,一个存储原始序列𝐴,另一个存储元素值到下标的映射,以便快速找到元素位置进行交换操作
注意–>由于𝑛的最大值为 10^5,在实现排序算法时,需要注意时间复杂度不能过高。
样例解答:
比如第一个样例数组 [3,4,1,5,2],我们一起来看看它是怎么排序的。
第 1 趟,我们要找最小的元素,也就是 1,它在第 3 个位置,所以我们把它和第 1 个位置的 3 交换,数组就变成了 [1,4,3,5,2]。
第 2 趟,在剩下的 [4,3,5,2] 中找第 2 小的元素,也就是 2,它在第 5 个位置,我们把它和第 2 个位置的 4 交换,数组就变成了 [1,2,3,5,4]。
第 3 趟,在剩下的 [3,5,4] 中找第 3 小的元素,其实还是 3,它就在第 3 个位置,所以数组不变,还是 [1,2,3,5,4]。
第 4 趟,在剩下的 [5,4] 中找第 4 小的元素,也就是 4,它在第 5 个位置,我们把它和第 4 个位置的 5 交换,数组就变成了 [1,2,3,4,5]。
发现什么了,就是我们交换元素的时候,是要将对应映射的元素交换了就ok了
#include<bits/stdc++.h>
using namespace std;
// 规范点的N
const int N = 1e5+5;
int a[N],b[N];
int q,i=1,t;
int main(){
//freopen("*.in","r",stdin);
//freopen("*.out","w",stdout); 复赛啦自己加上吧
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0); // 优化cin,cout就不说啦
int n,m;
cin>>n>>m;
// 存储输入的数组元素
for(int i=1;i<=n;i++){
// 存储输入的数组元素
cin>>a[i];
//存储元素值到下标的映射
b[a[i]]=i;
}
while(m--){
cin >> q;
// 注--i是全局变量哈,所以会让我们的循环次数大大减少的
for(;i<=q;i++){
//第𝑖小的元素与数组第𝑖个位置上的元素交换
b[a[i]]=b[i];
a[b[i]]=a[i];
//更新数组𝑎和𝑏中第𝑖个位置的元素值和映射关系
a[i]=i;
b[i]=i;
}
for(int i=1;i<=n;i++)
cout << a[i] <<" ";
cout<<"\n";
}
return 0;
}
1138 .插入排序
我们先分析一个涉及数列排序和元素位置查询的问题。
一、题意描述
给定一个数列(a_1,a_2,……,a_n),要求求出经过修改和特定排序(这里题目虽说是插入排序,但实际类似冒泡排序)之后每个元素所在的位置。
具体来说,假设 H 老师按照特定规则对数列进行操作,需要告诉 H 老师原来数列的第(x)个元素,即(a[x]),在排序后的新数组所处的位置,并且保证(1≤x≤n)。题目还明确规定,排序后的数组不会被保留,也不会影响后续的操作。
二、思路分析
- 初步思考与暴力解法
- 题目要求的是原来的(a_x)在现在的位置,所以我们可以开一个数组来维护原位置与排序后的位置的对应关系,就像这样的代码:
void get_order(){ for(int i=1;i<=n;i++){ order[a[i].num]=i; } }
- 首先看到题目中询问的要求,由于排序后的数组不会被保留且不影响后续操作,看起来我们需要每次将数组复制一遍,然后进行排序,得到答案后再将数组复原,于是有了这样的代码:
然而,这样的解法只有 52 分,想要完全正确解答(AC),还需要进一步优化。void query(int x){ for(int i=1;i<=n;i++){ b[i]=a[i]; } sort(b+1,b+n+1); get_order(); printf("%d\n",order[x]); } void updata(int x,int v){ a[x].val=v; }
- 正解思路
- H 老师的每一次询问都需要排序,这太浪费时间了。我们可以思考不用每次排序的方法。我们可以在每一次修改后,把修改后的元素放在正确的位置,就像一个元素的冒泡操作。
- 可能有同学会疑惑,这样的写法是否与题意冲突呢?题目中说排序后的数组不会被保留且不影响后续操作。但实际上,这种写法是正确的。因为每次查询的时候都需要排序,所以查询的时候元素一定是有序的。而我们一直保证元素的有序性,这样最终得到的序列和前面暴力得出的序列是一样的,那么答案自然也就正确。
- 所以修改的函数可以这样写:
而最后的查询只需要输出我们之前维护的对应关系的第(x)个就可以了。void updata(int x,int v){ a[order[x]].val=v; for(int i=order[x];i<n;i++){ if(a[i]>a[i+1]){ swap(a[i],a[i+1]); } } for(int i=order[x];i>1;i--){ if(a[i]<a[i-1]){ swap(a[i],a[i-1]); } } get_order(); }
三、细节注意
和第一种暴力解法不同的是,由于我们更改了元素的顺序,所以我们修改的时候并不是直接修改(a_x),而是需要修改(a_{order[x]}),其中 order 维护的是前面所提到的原位置与当前位置的关系,而冒泡也需要从(a_{order[x]})开始,而非(a_x)。
希望同学们通过这个问题,更好地理解数列排序和元素位置查询的方法,以及在编程中如何优化算法以提高效率。
#include<bits/stdc++.h>
using namespace std;
const int N=8000+5;
int a[N],b[N];
struct stu{
int c,d;
}e[N];
bool cmp(stu A,stu B){
return A.c==B.c?A.d<B.d:A.c<B.c;
}
int main(){
cin.tie(0);
int n,q;
cin>>n>>q;
for(int i=1;i<=n;i++){
cin>>a[i];
e[i].c=a[i],e[i].d=i;
}
sort(e+1,e+1+n,cmp);
for(int i=1;i<=n;i++){
b[e[i].d]=i;
}
while(q--){
int op,x,v;
cin>>op;
if(op==1){
cin>>x>>v;
a[x]=v;
e[b[x]].c=v;
for(int i=b[x]-1;i>=1;i--){
if(cmp(e[i+1],e[i])){
swap(b[e[i+1].d],b[e[i].d]);
swap(e[i+1].d,e[i].d);
swap(e[i+1].c,e[i].c);
}
else break;
}
for(int i=b[x]+1;i<=n;i++){
if(cmp(e[i],e[i-1])){
swap(b[e[i-1].d],b[e[i].d]);
swap(e[i-1].d,e[i].d);
swap(e[i-1].c,e[i].c);
}
else break;
}
}
else{
cin>>x;
cout<<b[x]<<'\n';
}
}
return 0;
}
3.黑白棋子
解题思路
读取数据:首先读取棋子的总数N,然后读取每个棋子的颜色,存储在数组a中,其中0表示白色,1表示黑色。
构建连续块长度数组:遍历棋子序列,统计每一段连续相同颜色棋子的长度,并将这些长度存储在ans数组中。这一步是为了后续计算最长“黑白列”提供基础数据,即通过观察连续块的变化来判断使用超能力的最佳时机。
计算最长“黑白列”:遍历ans数组,对于每个元素(即每段连续块),尝试将其与前后连续块合并(模拟使用超能力的效果),计算可能达到的最长“黑白列”长度。这里的关键在于理解连续块的合并实际上模拟了使用超能力改变一段连续棋子颜色的效果,从而实现“黑白列”的最大化。
输出结果:遍历完成后,maxn变量中存储的就是通过一次超能力操作后可能得到的最长“黑白列”的长度。最后输出这个长度。
#include<bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
vector<int> ans;
int cnt = 1, maxn = 0;
for (int i = 2; i <= n; i++) {
if (a[i] == a[i - 1]) {
ans.push_back(cnt);
cnt = 1;
} else {
cnt++;
}
}
ans.push_back(cnt); // 处理最后一个计数
for (int i = 0; i < ans.size(); i++) {
int cur = ans[i]; // 当前连续段长度
if (i > 0) cur += ans[i - 1]; // 加上前一个
if (i < ans.size() - 1) cur += ans[i + 1]; // 加上后一个
maxn = max(cur, maxn);
}
cout << maxn << endl;
return 0;
}