CSP真题系列(3)-NOIP2018初赛普及组 提高组真题及答案参考!
摘要
CSP非专业级别的能力认证正在报名中。今天整理出NOIP2018初赛试题供大家参考。
NOIP2018初赛提高组真题及答案
说明:难题解析比较详细,简单题没有解析
一、单项选择题(共 10 题,每题 2 分,共计 20 分;每题有且仅有一个正确选项)
1. 下列四个不同进制的数中,与其它三项数值上不相等的是( )。
A. (269) 16
B. (617) 10
C. (1151) 8
D. (1001101011) 2
答案:D
解析:考察进制转换,我们可以先将A,B转换为二进制,就可以发现A,B相等,但是与D不同,所以选D
2. 下列属于解释执行的程序设计语言是( )。
A. C
B. C++
C. Pascal
D. Python
答案:D
解析:因为C,C++,pascal都是需要编译的语言,非解释性语言
而python是交互式的,也是解释性语言
3. 中国计算机学会于( )年创办全国青少年计算机程序设计竞赛。
A. 1983
B. 1984
C. 1985
D. 1986
答案:B
4. 设根节点深度为 0,一棵深度为 h 的满 k(k>1)叉树,即除最后一层无任何
子节点外,每一层上的所有结点都有 k 个子结点的树,共有( )个结点。
答案:A
解析:等比数列求和,
5. 设某算法的时间复杂度函数的递推方程是 T(n) = T(n - 1) + n(n 为正整数)
及 T(0) = 1,则该算法的时间复杂度为( )。
A. O(log n)
B. O(n log n)
C. O(n)
D. O(n2)
答案:D
解析:
,求和:
6. 表达式 a * d - b *c 的前缀形式是( )。
A. a d * b c * -
B. - * a d * b c
C. a * d - b * c
D. - * * a d b c
答案:B
解析:先建一棵表达式树,其先序遍历就是前缀表达式
7. 在一条长度为 1 的线段上随机取两个点,则以这两个点为端点的线段的期望
长度是( )。
A. 1 / 2
B. 1 / 3
C. 2 / 3
D. 3 / 5
答案:B
解析:我们先考虑固定一个端点的情况,如果左端点固定在了最左边那么答案为1/2,既然左端点更大,那么肯定答案会小于1/2,因此只能是B
熟悉ODT(即珂朵莉树)理论的同学可以发现ODT复杂度证明里面有这个东西,即随机意义下期望区间长度。
8. 关于 Catalan 数 Cn = (2n)! / (n + 1)! / n!,下列说法中错误的是( )。
A. Cn 表示有 n + 1 个结点的不同形态的二叉树的个数。
B. Cn 表示含 n 对括号的合法括号序列的个数。
C. Cn 表示长度为 n 的入栈序列对应的合法出栈序列个数。
D. Cn 表示通过连接顶点而将 n + 2 边的凸多边形分成三角形的方法个数。
答案:A
解析:对于A,令n=1,2个节点的二叉树形态有2种,但是C1=1,显然错误
9. 假设一台抽奖机中有红、蓝两色的球,任意时刻按下抽奖按钮,都会等概率
获得红球或蓝球之一。有足够多的人每人都用这台抽奖机抽奖,假如他们的
策略均为:抽中蓝球则继续抽球,抽中红球则停止。最后每个人都把自己获
得的所有球放到一个大箱子里,最终大箱子里的红球与蓝球的比例接近于
( )。
A. 1 : 2
B. 2 : 1
C. 1 : 3
D. 1 : 1
答案:D
解析:一个人在第 i 轮可以得到的红球期望数量为:
,而
所以每个人得到红球期望数量为1,而得到蓝球数量必定为1,所以为1:1
10. 为了统计一个非负整数的二进制形式中 1 的个数,代码如下:
int CountBit(int x)
{
int ret = 0;
while (x)
{
ret++;
________;
}
return ret;
}
则空格内要填入的语句是( )。
A. x >>= 1
B. x &= x - 1
C. x |= x >> 1
D. x <<= 1
答案:B
解析:排除法+模拟
二 、 不定 项选择题(共 5 题,每题 2 分,共计 10 分 ;每题有一个或多个正确选
项,多选或少选均不得分 )
1. NOIP 初赛中,选手可以带入考场的有( )。
A. 笔
B. 橡皮
C. 手机(关机)
D. 草稿纸
答案:AB
解析:草稿纸是不允许带的
2. 2-3 树是一种特殊的树,它满足两个条件:
(1)每个内部结点有两个或三个子结点;
(2)所有的叶结点到根的路径长度相同。
如果一棵 2-3 树有 10 个叶结点,那么它可能有( )个非叶结点。
A. 5
B. 6
C. 7
D. 8
答案:CD
解析:把最底层的节点都画出来向上连,先在符合条件的前提下把每2个点连到一起,这样发现有8个点,可以发现这是上界。 同理把尽量多的3个点连在一起,答案是7,可以发现这是下届,故选CD
3. 下列关于最短路算法的说法正确的有( )。
A. 当图中不存在负权回路但是存在负权边时,Dijkstra 算法不一定能求出源
点到所有点的最短路。
B. 当图中不存在负权边时,调用多次 Dijkstra 算法能求出每对顶点间最短路
径。
C. 图中存在负权回路时,调用一次 Dijkstra 算法也一定能求出源点到所有点
的最短路。
D. 当图中不存在负权边时,调用一次 Dijkstra 算法不能用于每对顶点间最短
路计算。
答案:ABD
解析:dijstra算法不适用于负权图,而且它用于求单点到其他点的最短路。熟悉图论的同学这是一个常识题。Dijkstra是单源最短路算法,因此多次调用必然能够得到所有点对的最短路。如果图中出现负环,那么dijkstra算法就会在这个环里不断地转,因此无法求出最短路。同理负权。
4. 下列说法中,是树的性质的有( )。
A. 无环
B. 任意两个结点之间有且只有一条简单路径
C. 有且只有一个简单环
D. 边的数目恰是顶点数目减 1
答案:ABD
解析:树的基本知识
5. 下列关于图灵奖的说法中,正确的有( )。
A. 图灵奖是由电气和电子工程师协会(IEEE)设立的。
B. 目前获得该奖项的华人学者只有姚期智教授一人。
C. 其名称取自计算机科学的先驱、英国科学家艾伦·麦席森·图灵。
D. 它是计算机界最负盛名、最崇高的一个奖项,有“计算机界的诺贝尔奖”
之称。
答案:BCD
解析:图灵奖是ACM设立的
由于点数很小,手动模拟下。从条件3开始找,即可找到答案。
首先如果b是a的子集,那么条件必然成立。然后手动简单玩一下,发现只有1位和2位情况存在特例。手动找到这些的答案即可。
科学的解释是:设a and b=x,a xor x=y,b xor x=z,则(x+y)(x+z)=x(x+y+z),即yz=0,即a and b=a或a and b=b
四、阅读程序写结果(共 4 题,每题8 分,共计 32 分)
1.
#include<cstdio>
int main() {
int x;
scanf('%d', &x);
int res = 0;
for (int i = 0; i < x; ++i) {
if (i * i % x == 1) {
++res;
}
}
printf('%d', res);
return 0;
}
输入:15
输出:4
解析:简单模拟即可。这种题非常需要细心、耐心。
2.
#include<cstdio>
int n, d[100];
bool v[100];
int main() {
scanf('%d', &n);
for (int i = 0; i < n; ++i) {
scanf('%d', d + i);
v[i] = false;
}
int cnt = 0;
for (int i = 0; i < n; ++i) {
if (!v[i]) {
for (int j = i; !v[j]; j = d[j]) {
v[j] = true;
}
++cnt;
}
}
printf('%d\n', cnt);
return 0;
}
输入:10 7 1 4 3 2 5 9 8 0 6
输出:6
解析:可以看到是读入一个序列,每个点都有一个出度。可以发现这是在找环的个数,手动模拟或者画个图数一下都可以。
3.
#include<iostream>
using namespace std;
string s;
long longmagic(int l, int r) {
long long ans = 0;
for (int i = l; i <= r; ++i) {
ans = ans * 4 + s[i] - 'a' + 1;
}
return ans;
}
int main() {
cin >> s;
int len = s.length();
int ans = 0;
for (int l1 = 0; l1 < len; ++l1) {
for (int r1 = l1; r1 < len; ++r1) {
bool bo = true;
for (int l2 = 0; l2 < len; ++l2) {
for (int r2 = l2; r2 < len; ++r2) {
if (magic(l1, r1) == magic(l2, r2)&& (l1 != l2 || r1 != r2)) {
bo = false;
}
}
}
if (bo) {
ans += 1;
}
}
}
cout << ans << endl;
return 0;
}
输入:abacaba
输出:16
解析:magic(l,r)是对于s[l,r]的字符串哈希,底下枚举了两个子串,那么答案其实就是不重复出现的子串个数,手动数一下就好了。
4.
#include<cstdio>
using namespace std;
const int N =110;
bool isUse[N];
int n, t;
int a[N], b[N];
bool isSmall() {
for (int i = 1; i <= n; ++i)
if (a[i] != b[i]) return a[i] < b[i];
return false;
}
boolgetPermutation(int pos) {
if (pos > n) {
return isSmall();
}
for (int i = 1; i <= n; ++i) {
if (!isUse[i]) {
b[pos] = i; isUse[i] = true;
if (getPermutation(pos + 1)) {
return true;
}
isUse[i] = false;
}
}
return false;
}
void getNext() {
for (int i = 1; i <= n; ++i) {
isUse[i] = false;
}
getPermutation(1);
for (int i = 1; i <= n; ++i) {
a[i] = b[i];
}
}
int main() {
scanf('%d%d', &n, &t);
for (int i = 1; i <= n; ++i) {
scanf('%d', &a[i]);
}
for (int i = 1; i <= t; ++i) {
getNext();
}
for (int i = 1; i <= n; ++i) {
printf('%d', a[i]);
if (i == n) putchar('\n'); else putchar('');
}
return 0;
}
输入1:6 10 1 6 4 5 32
输出 1:2 1 3 5 6 4 (3 分)
输入2:6 200 1 5 3 4 26
输出 2:3 2 5 6 1 4 (5 分)
解析:可以发现这一大堆函数唯一的用处就是找到字典序大于他的下一个排列(其实看到输入都是全排列的元素、以及一个next大概就可以猜到了。),对于第一个询问可以手动找到下10个排列。对于第二个询问,可以把后几位带在一起算,每次看把某一位更新成下一个值需要加上多少。当然也可以直接进行康托展开对全排列进行计数。
五、完善程序(共 2 题,每题 14 分,共计 28 分)
1. 对于一个1到n的排列p(即1到n中每一个数在p中出现了恰好一次),令qi为第i个位置之后第一个比pi值更大的位置,如果不存在这样的位置,则qi =n+1。
举例来说,如果n=5且p为1 5 4 2 3,则q为2 6 6 5 6。
下列程序读入了排列p,使用双向链表求解了答案。试补全程序。(第二空2分,其余3分)
数据范围 1 ≤ n ≤ 105。
#include<iostream>
using namespace std;
const int N =100010;
int n;
int L[N], R[N],a[N];
int main() {
cin >> n;
for (int i = 1; i <= n; ++i) {
int x;
cin >> x;
a[x] = i ;
}
for (int i = 1; i <= n; ++i) {
R[i]= i + 1;
L[i] = i - 1;
}
for (int i = 1; i <= n; ++i) {
L[ R[a[i]]] = L[a[i]];
R[L[a[i]]] = R[a[i] ];
}
for (int i = 1; i <= n; ++i) {
cout << R[i]<< ' ';
}
cout << endl;
return 0;
}
解析:
1. 一个有点鬼畜的双向链表。对于第一个空我们会发现如果a[i]=x的话直接cin>>a[i]其实就好了,所以必然是a[x]=i,意思是x是在第几个位置。
2,3,4空 可以考虑仿写。完善程序出现“复读机”套路是很常见的。当然不能全部复读,要讲究对称性。
5空 我们发现题目既然要求第i个数后面最近的一个比他大的,那么必然是i的后继,即R[i]。
当然已经做出前几空了把题面的那组数据带进去就会发现的确是R[i]。
如果原理不懂得可以画图进行模拟。不过反正写题本身很简单。
2. 一只小猪要买 N 件物品(N 不超过 1000)。
它要买的所有物品在两家商店里都有卖。第 i 件物品在第一家商店的价格是 a[i],在第二家商店的价格是 b[i],两个价格都不小于 0 且不超过 10000。如果在第一家商店买的物品的总额不少于 50000,那么在第一家店买的物品都可以打 95 折(价格变为原来的 0.95 倍)。
求小猪买齐所有物品所需最少的总额。
输入:第一行一个数 N。接下来 N 行,每行两个数。第 i 行的两个数分别代表 a[i],b[i]。
输出:输出一行一个数,表示最少需要的总额,保留两位小数。
试补全程序。(第一空 2 分,其余 3 分)
#include<cstdio>
#include<algorithm>
using namespace std;
const int Inf =1000000000;
const int threshold = 50000;
const int maxn =1000;
int n, a[maxn],b[maxn];
bool put_a[maxn];
int total_a,total_b;
double ans;
int f[threshold];
int main() {
scanf('%d', &n);
total_a = total_b = 0;
for (int i = 0; i < n; ++i) {
scanf('%d%d', a + i, b + i);
if (a[i] <= b[i]) total_a += a[i];
else total_b += b[i];
}
ans = total_a + total_b;
total_a = total_b = 0;
for (int i = 0; i < n; ++i) {
if( a[i] * 0.95<= b[i] ){
put_a[i] = true;
total_a += a[i];
} else {
put_a[i] = false;
total_b += b[i];
}
}
if(total_a >= threshold){
printf('%.2f', total_a * 0.95 +total_b);
return 0;
}
f[0] = 0;
for (int i = 1; i < threshold; ++i)
f[i] = Inf;
int total_b_prefix = 0;
for (int i = 0; i < n; ++i)
if (!put_a[i]) {
total_b_prefix += b[i];
for (int j = threshold - 1; j >= 0;--j) {
if (total_a +j + a[i] >= threshold && f[j] != Inf)
ans = min(ans, (total_a + j + a[i]) * 0.95+f[j] + total_b – total_b_prefix);
f[j] = min(f[j] + b[i], j >= a[i] ? f[j – a[i]] : Inf);
}
}
printf('%.2f', ans);
return 0;
}
解析:
首先先考虑算法:如果第一家便宜肯定选第一家。
(以下内容感谢知乎@林泽辉 指正)
我们考虑程序后半段的思路,大致就是假设能够在第一家买够50000,最少花多少钱。
那么如果打过九五折第一家更优,那么必然会选择第一家。反正剩下的物品都是中间物品。
那剩下的一些呢?我们称为“中间物品”。尽可能选择其中一些“中间物品”在A买,凑足50000元,使得比B便宜,但是尽可能的少。这是一个背包问题。
实现上,f[i,j]表示前i个物品,在额外在A店花了j元的情况下,购买B店“中间物品”的最小值。i呢?滚动数组空间降维。
考虑为什么要先进行这个贪心,如果直接进行正常的背包的话,背包的大小将会是 物品个数 * 物品大小 ,如果先进行贪心的话,背包的大小会减小到50000.这样就可以接受了。
第一空:如果直接a[i]<=b[i]的话上文就算过了,没必要单独循环一次。考虑贪心那么必然是看加了优惠之后a[i]是否优于b[i];
第二空:考虑printf里面的部分,那么如果a的总和已经满足优惠,直接优惠掉即可;
第三空:考虑仿写min里面的部分,那么肯定是当前枚举的优惠幅度超过了50000。这是考虑如果我们要买第i件,还额外花了j元在“中间物品”上的情况;
第四空:这是计算总价,第一店所有东西打折后,加上所有第二点需要购买的东西;
第五空:考虑它为啥要判j>=a[i],这个是做个背包问题都知道,这是背包问题的转移。
---------------------
选择题作者:Hi_KER