蓝桥杯|2018年蓝桥杯省赛 C++ B组

1. 螺旋折线 1.1 原题概要

如图所示的螺旋折线经过平面上所有整点恰好一次。
对于整点(X, Y),我们定义它到原点的距离dis(X, Y)是从原点到(X, Y)的螺旋折线段的长度。
例如dis(0, 1)=3, dis(-2, -1)=9
给出整点坐标(X, Y),你能计算出dis(X, Y)吗?
蓝桥杯|2018年蓝桥杯省赛 C++ B组
文章图片

这一题为了AK,我花了较长时间,关键是要细节,不能出错。比赛的时候建议把代码敲得好看点,这样逻辑更加清晰。
这一题是个分类讨论的题,建议分完一种类就检查一下错误。确保之前没有错误,尤其是该题,后面的答案与前面是息息相关的。(我找的规律是的)
1.2 解题过程 本题想出题目解法没什么难度,主要难度就在于敢去想,以及细心。这里就不过渡,直接讲题解。
解这一题,我想着应该要分块,最可能的就是分成四个部分,即四个象限,于是代码很自然就有:
if(x >= 0 && y >= 0){} else if(x >= 0 && y < 0){} else if(x <= 0 && y <= 0){} else if(x <= 0 && y > 0){}


  • y > x && x >= 0 && y >= 0: 然后我脑袋里就随便选了一个点,想着从这个(2,3)点开始 :
蓝桥杯|2018年蓝桥杯省赛 C++ B组
文章图片

我们发现,这个点的距离由三个部分组成坐标:
左边紫色的线:1 + 2*1 + 3 + 2*2 + 5 + 2*3。
【蓝桥杯|2018年蓝桥杯省赛 C++ B组】右边蓝色的线:2*1 + 2 + 2*2 + 4。
多出黑色的一小截:2。
细心点就可以发现(如果是自己写出这个过程,立即就会发现):
1. 紫色部分每一层都有两部分:1+(h-1)*2 和2*h, (h是紫色线的层数)
2. 同样的,蓝色线每一层有两个部分:h*2 + 2*h,(h是蓝色线的层数)
再看紫色线的层数一定是比蓝色线多1层的( (2,3)这个点)因此,(2,3)这个点的值可以这样算:
// h = 3 for(int i = 1; i <= h; i++) ans+=(1 + (i-1)*2 + 2*i); // 紫色 for(int i = 1; i <= h-1; i++) ans+=4*i; // 蓝色

而黑色的部分呢?就是x的长度,于是最后ans加上x就能得出这一部分的答案。
然后我就想把这个点推广到更广泛的区域,其实在找到紫色线由两部分组成的时候,我就已经开始假设,是不是(2,3) 是不是属于 y > x 区域,我想这要用y = x和 y = -x 把整个区域分为8各部分。事实上是不是这样暂时还不知道,至少我发现,在y > x && x >= 0 && y >= 0的时候,我们只需要把y作为层数h,带入至上述公式中即可。
int h = y for(int i = 1; i <= h; i++) ans+=(1 + (i-1)*2 + 2*i); // 紫色 for(int i = 1; i <= h-1; i++) ans+=4*i; // 蓝色 ans += x

接着就从该区域推广至整个图:
蓝桥杯|2018年蓝桥杯省赛 C++ B组
文章图片


  • y < x && x >= 0 && y >= 0:
1就是我们之前选的点,而2呢?比1多了那部分呢?
他把原来黑色的那一截用补满了,即添加了紫色线,而紫色线加上原来的黑色线,就是层数h,于是这一部分可不可以就是把上述总结公式的ans += x给换成:ans+=h。
然后还有多的蓝色部分就是h - y。
不过此时的h已经变成了x,我们发现,层数就是x和y中较大的那个。
int h = x for(int i = 1; i <= h; i++) ans+=(1 + (i-1)*2 + 2*i); // 紫色 for(int i = 1; i <= h-1; i++) ans+=4*i; // 蓝色 ans += h; ans += (h-y);

之后就是在这个层上继续往下延申,其实都是把上一环节中多出的一截补全,然后又多出一小截
当然,当x < 0 && y < 0时,不光x <= y时层数h = 2,y + 1 == x的时候,层数也是2,(因为这时候是上一层的最后一步,新一层的开始)这样就和之前一样,先将上一环节的部分补全,然后再算多出来的小截。
我这里没有细推每个过程,但是最好自己把这个过程推出来,有了开头,后面的其实都差不多,直接看题解效果不大。
1.2 代码
#include #define int long long #define rep(i, a, b) for(int i = a; i <= b; i ++) #define per(i, a, b) for(int i = a; i >= b; i --) using namespace std; const int N = 2e5 + 10; signed main(){ int x,y; cin >> x >> y; if(!x && !y){ cout << 0; return 0; } if(x >= 0 && y >= 0){ if(x <= y){ int h = y,ans = x; rep(i,1,h) ans += (i*2 + 1 + 2*(i-1)); rep(i,1,h-1) ans += (i*2 + 2*i); cout << ans << endl; }else{ int h = x,ans = x - y; rep(i,1,h) ans += (i*2 + 1 + 2*(i-1)); rep(i,1,h-1) ans += (i*2 + 2*i); ans += h; cout << ans << endl; } }else if(x >= 0 && y < 0){ if(x > abs(y)){ int h = x,ans = x + abs(y); rep(i,1,h) ans += (i*2 + 1 + 2*(i-1)); rep(i,1,h-1) ans += (i*2 + 2*i); ans += h; cout << ans << endl; }else{ int h = abs(y),ans =abs(y) - x; rep(i,1,h) ans += (i*2 + 1 + 2*(i-1)); rep(i,1,h-1) ans += (i*2 + 2*i); ans += 3*h; cout << ans << endl; } }else if(x <= 0 && y <= 0){ x = abs(x),y = abs(y); if(x <= y || y+1==x){ int h = y,ans =x; rep(i,1,h) ans += (i*2 + 1 + 2*(i-1)); rep(i,1,h-1) ans += (i*2 + 2*i); ans += 4*h; cout << ans << endl; }else{ int h = x - 1,ans = h - y; rep(i,1,h) ans += (i*2 + 1 + 2*(i-1)); rep(i,1,h-1) ans += (i*2 + 2*i); ans += 5*h+1; cout << ans << endl; } }else if(x <= 0 && y > 0){ x = abs(x),y = abs(y); if(x > y){ int h = x - 1,ans = y; rep(i,1,h) ans += (i*2 + 1 + 2*(i-1)); rep(i,1,h-1) ans += (i*2 + 2*i); ans += 6*h+1; cout << ans << endl; } }return 0; }

2. 乘积最大
给定N个整数A1, A2, ... AN。请你从中选出K个数,使其乘积最大。
请你求出最大的乘积,由于乘积可能超出整型范围,你只需输出乘积除以1000000009的余数。
注意,如果X<0, 我们定义X除以1000000009的余数是负(-X)除以1000000009的余数。
即:0-((0-x) % 1000000009)
这一题也是分类讨论的思路,不过和上面一题的不一样,上面一题简单,规律就一个。虽然是分类,但是是对过程的分类,主要看重细节。这一题的分类注重对类别的区分严格覆盖所以问题集。
比赛时,我们可以列出树状图,当前是因为什么分出去的?分出去的每种情况又可以分成哪几种情况。要是想得高分,比赛的时候,就千万注意不要边敲边分类。
2.1 思路导图 先介绍下我大概的思路,这个问题一看懂,我就有一个猜想,先将所有的数按绝对值大小降序排序,(因为若是两个很大的负数相乘也能得到一个很大的乘积)然后取前面的数来做乘积运算。然后,挺幸运的,这么想是对的。
先将数按照绝对值进行降序排序。如6 -8 -7 0 0 0 0可以排序成 -8 -7 6 0 0 0 0。
蓝桥杯|2018年蓝桥杯省赛 C++ B组
文章图片


2.2 直接上代码:
#include #define int long long #define rep(i, a, b) for(int i = a; i <= b; i ++) #define per(i, a, b) for(int i = a; i >= b; i --) using namespace std; const int mod = 1000000009; int a[100010],b[100010]; bool cmp(int a,int b){ return abs(a) > abs(b); }bool cmp1(int a,int b){ return abs(a) < abs(b); } signed main(){ int n,k; cin >> n >> k; rep(i,1,n) cin >> a[i],b[i] = a[i]; sort(a+1,a+n+1,cmp); bool f = false; int cnt = 0,last_f = 0,last_z = 0; rep(i,1,k){ if(a[i] < 0) cnt++,last_f = i; else if(a[i] == 0) f = true; else last_z = i; }if(f){ cout << 0 << endl; return 0; } if(cnt&1){ int first_z = 0,first_f = 0,ans = 1LL; rep(i,k+1,n){ if(a[i]>= 0){ first_z = i; break; } } rep(i,k+1,n){ if(a[i] <= 0){ first_f = i; break; } } rep(i,1,k){ if(i != last_f && i != last_z){ a[i] = abs(a[i]); int x = a[i] % mod; ans*=x; ans %= mod; } }if(last_z){ if(first_z || first_f){ int x = max(a[first_f] * a[last_f] , a[first_z] * a[last_z]); x %= mod; cout << (x*ans)%mod; }else{ int an = abs(( ((a[last_z]%mod)*(a[last_f]%mod))%mod * ans ) % mod)*-1; cout << an; } }else{ if(first_z){ cout << ( (a[first_z]%mod) * ans ) % mod; }else{ sort(b+1,b+1+n,cmp1); int sum = 1LL; rep(i,1,k){ b[i]%=mod; sum *= b[i]; sum%=mod; } cout << sum ; } } }else{ int ans = 1LL; rep(i,1,k){ a[i] = abs(a[i]); a[i] %= mod; ans *= a[i]; ans %= mod; } cout << ans; } return 0; }

3 全球变暖
你有一张某海域NxN像素的照片,"."表示海洋、"#"表示陆地,如下所示:
....... .##.... .##.... ....##. ..####. ...###. .......

其中"上下左右"四个方向上连在一起的一片陆地组成一座岛屿。例如上图就有2座岛屿。
由于全球变暖导致了海面上升,科学家预测未来几十年,岛屿边缘一个像素的范围会被海水淹没。
具体来说如果一块陆地像素与海洋相邻(上下左右四个相邻像素中有海洋),它就会被淹没。
例如上图中的海域未来会变成如下样子:
....... ....... ....... ....... ....#.. ....... .......

请你计算:依照科学家的预测,照片中有多少岛屿会被完全淹没。
这一题是一个很简单的求连通图个数的题,DFS,BFS都可以,算是一个基础的题型。不过要注意题意,多少岛屿能被淹没,众所周知,人的生命只有一次,岛屿也是,要是淹没了,就再也不能在此淹没了 。这么说的意义在哪呢?这题占半成以上的样例都在这上面挖坑了。举个样例
....... ....... ....... .##.##. .#####. .##.##. .......


....... ....... ....... ....... ..#.#.. ....... .......

这样,原来一个岛屿就变成了两个了,那淹没了几个岛屿呢?要是简单的求开始的连通图个数 - 之后连通图个数,答案就是-1,这显然是不行的。
为了即表示他被淹没了,又能突出他原来是有陆地的,我们可以用第三种符号代替,如‘&’
....... ....... ....... .&&.&&. .&#&#&. .&&.&&. .......

之后算连通图的时候,若遇到了#,连通图++,意味着这篇岛屿没有被淹没,但是在标记岛屿的时候,我们也要吧&考虑进去。这样能确保该片岛屿所有区域被标记,而不会下次遇见在此让岛屿数++。
至于其他的,就是很基本的找联通图个数的算法了。属于是板子题了。要是不懂建议去刷一下这种体型,要是细讲就是另一个话题了。可以拓展很多。
这里推荐天梯赛的一题,是有节点的图(或者是树,不记得了)求连通图个数。
PTA | 程序设计类实验辅助教学平台 L2-013红色警告
3.2 完整代码
#include #define int long long #define rep(i, a, b) for(int i = a; i <= b; i ++) #define per(i, a, b) for(int i = a; i >= b; i --) using namespace std; const int N = 2e5 + 10; char a[1010][1010]; bool st[1010][1010]; void dfs(int i,int j){ st[i][j] = true; if(a[i][j] == '#' || a[i][j] == '&'){ if(!st[i+1][j]) dfs(i+1,j); if(!st[i-1][j]) dfs(i-1,j); if(!st[i][j+1]) dfs(i,j+1); if(!st[i][j-1]) dfs(i,j-1); } } signed main(){ int n; cin >> n; rep(i,1,n){ rep(j,1,n){ cin >> a[i][j]; } } int cnt1 = 0; rep(i,2,n-1){ rep(j,2,n-1){ if(!st[i][j] && a[i][j] == '#'){ cnt1++; dfs(i,j); } } } rep(i,2,n-1){ rep(j,2,n-1){ if(a[i][j] == '#' && (a[i][j-1] == '.' || a[i][j+1] == '.' ||a[i-1][j] == '.' ||a[i+1][j] == '.' )){ a[i][j] = '&'; } } } int cnt2 = 0; memset(st,0,sizeof st); rep(i,2,n-1){ rep(j,2,n-1){ if(!st[i][j] && a[i][j] == '#'){ cnt2++; dfs(i,j); } } } cout << cnt1 - cnt2 << endl; return 0; }

4 日志统计
小明维护着一个程序员论坛。现在他收集了一份"点赞"日志,日志共有N行。
其中每一行的格式是:ts id。表示在ts时刻编号id的帖子收到一个"赞"。
现在小明想统计有哪些帖子曾经是"热帖"。
如果一个帖子曾在任意一个长度为D的时间段内收到不少于K个赞,小明就认为这个帖子曾是"热帖"。
具体来说,如果存在某个时刻T满足该帖在[T, T+D)这段时间内(注意是左闭右开区间)收到不少于K个赞,该帖就曾是"热帖"。
给定日志,请你帮助小明统计出所有曾是"热帖"的帖子编号。
这一题既然要按ID从小到大输出(满足要求的)。
那我们把每个ID从小到大存就可以了,然后与ID匹配的东西可以是一个数组,里面存着按时序序列排序的时间。我用的方式是:
map > mp;

注意> >嵌套的数据结构,后面的两个大于号不能连接如>>,在蓝桥杯的c++11编译器中,这个会被识别成位运算。我们要把它用空格分开。
再回到这题,mp的key是id,value是这个id收到点赞的时间。
我们得到每个ID对应的点赞时间,只要用一个滑动窗口在时间序列中滑动就行,举个例子:
  • 若要求的是时间在4之内(不等于4),点赞数要达到2。那1 2 6 13这个点赞序列是可行的
因为我们要求的点赞数是2,那我们的区间长度设为2就行,只要这个区间的时间小于4就行。很显然1,2这个序列时间是满足的(2-1+1 = 2 < 4)
  • 若要求的是时间在4之内(不等于4),点赞数要达到3。2 6 13 19 26是不行的。
我们的窗口开始时是(2,6 13),13 - 2 + 1 > 4不满足,先后移动(6,13,19),19 - 6 + 1 > 4不满足 (13,19,26)也同理,不满足。
这样的思路是,固定点赞数,看时间满不满足,还有一种思路是,固定时间,看点赞数满不满足,但是时间是不均匀的,不好比较。而点赞数呢,把区间扩大一位就行。
4.2 完整代码 代码需要提一句的就是,for(auto x: mp)这种语法是cpp14才有的,蓝桥杯要求的cpp11里是没有的,大家在备赛的时候要尽快调整用迭代器的方式遍历,或访问这个数据结构。包括上面提到的 map> 是错误的写法,(事实上是没错的)
#include #define int long long #define rep(i, a, b) for(int i = a; i <= b; i ++) #define per(i, a, b) for(int i = a; i >= b; i --) using namespace std; const int N = 2e5 + 10; map > mp; multiset > st; set v; signed main(){ int n,d,k; cin >> n >> d >> k; rep(i,1,n){ int ts,id; cin >> ts >> id; st.insert({ts,id}); } for(multiset >::iterator it = st.begin(); it != st.end(); it++){ pair xx = *it; mp[xx.second].push_back(xx.first); } for(map >::iterator it = mp.begin(); it != mp.end(); it++){ int ans = it->first; vector x = it->second; int l = x.size(),L = 0; rep(i,k-1,l-1){ if(x[i] - x[L] < d){ v.insert(ans); break; } L++; } } for(set::iterator it = v.begin(); it!=v.end(); it++){ cout << *it << endl; } return 0; }

5 递增三元组
给定三个整数数组
A = [A1, A2, ... AN],
B = [B1, B2, ... BN],
C = [C1, C2, ... CN],
请你统计有多少个三元组(i, j, k) 满足:
1. 1 <= i, j, k <= N
2. Ai < Bj < Ck
这一题开始写不知道咋写,没想到睡一觉起来立刻就知道咋写了,也许是但是太晚了,没啥思路。总体不难,当刷题量要足够,要能看出来这题是前缀和(我是用前缀和)
他说了每个数都小于1e5那我就用数组来记录每个元组的前缀和。
5.1 思路 suma[1]表示a数组中小于等于1的个数,sumb[10]表示b数组中小于等于10的个数。sumc[100]表示c数组小于等于100的个数。
然后b作为桥梁,因为b是既联系了a也联系了c,也就是只要b选中了a和c的范围就限定了。
一开始我想的就是用a作为桥梁,但是a确定b后,对应的b不同c也会不同,如a=1,b=2 ,那c可以大于2,但是a=1,b=3,c此时范围就变成了大于3。
以c作为桥梁也是如此。
再看以b作为桥梁:b = 3,则a< 3,c > 3即可,而且只有这一种情况。
那我们从1遍历到100000,然后找到a的方案数,suma[i-1],b的方案数sumc[100000] - sumc[i]。
如果这个不懂,建议去看看前缀和的相关知识。
5.2 完整代码
#include #define int long long #define rep(i, a, b) for(int i = a; i <= b; i ++) #define per(i, a, b) for(int i = a; i >= b; i --) using namespace std; const int N = 1e5 + 10; int a[N],b[N],c[N]; int ca[N],cb[N],cc[N]; map mp1,mp2,mp3; signed main(){ int n; cin >> n; rep(i,1,n) cin >> a[i] ,mp1[a[i]]++; rep(i,1,n) cin >> b[i],mp2[b[i]]++; rep(i,1,n) cin >> c[i],mp3[c[i]]++; sort(a+1,a+1+n); sort(b+1,b+1+n); sort(c+1,c+1+n); int maxn = c[n]; rep(i,0,maxn) ca[i] = ca[i-1] + mp1[i]; rep(i,0,maxn) cb[i] = cb[i-1] + mp2[i]; rep(i,0,maxn) cc[i] = cc[i-1] + mp3[i]; int ans = 0; rep(i,1,maxn){ ans += (mp2[i] * ca[i-1] * (cc[maxn] - cc[i])); } cout << ans << endl; return 0; }

6 明码
汉字的字形存在于字库中,即便在今天,16点阵的字库也仍然使用广泛。
16点阵的字库把每个汉字看成是16x16个像素信息。并把这些信息记录在字节中。
一个字节可以存储8位信息,用32个字节就可以存一个汉字的字形了。
把每个字节转为2进制表示,1表示墨迹,0表示底色。每行2个字节,
一共16行,布局是:
第1字节,第2字节
第3字节,第4字节
....
第31字节, 第32字节
这道题目是给你一段多个汉字组成的信息,每个汉字用32个字节表示,这里给出了字节作为有符号整数的值。
题目的要求隐藏在这些信息中。你的任务是复原这些汉字的字形,从中看出题目的要求,并根据要求填写答案。
4 0 4 0 4 0 4 32 -1 -16 4 32 4 32 4 32 4 32 4 32 8 32 8 32 16 34 16 34 32 30 -64 0 16 64 16 64 34 68 127 126 66 -124 67 4 66 4 66 -124 126 100 66 36 66 4 66 4 66 4 126 4 66 40 0 16 4 0 4 0 4 0 4 32 -1 -16 4 32 4 32 4 32 4 32 4 32 8 32 8 32 16 34 16 34 32 30 -64 0 0 -128 64 -128 48 -128 17 8 1 -4 2 8 8 80 16 64 32 64 -32 64 32 -96 32 -96 33 16 34 8 36 14 40 4 4 0 3 0 1 0 0 4 -1 -2 4 0 4 16 7 -8 4 16 4 16 4 16 8 16 8 16 16 16 32 -96 64 64 16 64 20 72 62 -4 73 32 5 16 1 0 63 -8 1 0 -1 -2 0 64 0 80 63 -8 8 64 4 64 1 64 0 -128 0 16 63 -8 1 0 1 0 1 0 1 4 -1 -2 1 0 1 0 1 0 1 0 1 0 1 0 1 0 5 0 2 0 2 0 2 0 7 -16 8 32 24 64 37 -128 2 -128 12 -128 113 -4 2 8 12 16 18 32 33 -64 1 0 14 0 112 0 1 0 1 0 1 0 9 32 9 16 17 12 17 4 33 16 65 16 1 32 1 64 0 -128 1 0 2 0 12 0 112 0 0 0 0 0 7 -16 24 24 48 12 56 12 0 56 0 -32 0 -64 0 -128 0 0 0 0 1 -128 3 -64 1 -128 0 0

这题算是挺有意思的题目了,不难,但是要知道怎么方便的遍历一个数的二进制。
这里就介绍两个方式,(常用的)
有的考搜索的题,喜欢说二进制的第一位,第二位,第三位,第四位,若为1,往对应上,下,左,右移动。我们可以直接让x&1,x&2,x&4,x&8,对应为1,那就是对应的1,2,3,4位置二进制是1。
我们看:
(1)十 :(0001)二
(2)十:(0010)二
(4)十:(0100)二
(8)十:(1000)二
还有一种通用遍历二进制数的方式,x >> i,>> 表示右移i位,假如x右移0位,那x二进制右边数第一位。x右移1位,表示x的二进制从右数第二位。
在此题,我们要取每个数的从右数1-8位,那我们只需要:若x 的第i+1位是1,那就打印这个像素点,两个数字一行,共16行,最后可以打印出对应的汉字是:“九的九次方等于多少?”
for(int i = 7; i >= 0; i--){ if(x >> i & 1){} }

7 乘积尾0
如下的10行数据,每行有10个整数,请你求出它们的乘积的末尾有多少个零?
5650 4542 3554 473 946 4114 3871 9073 90 4329 2758 7949 6113 5659 5245 7432 3051 4434 6704 3594 9937 1173 6866 3397 4759 7557 3070 2287 1453 9899 1486 5722 3135 1170 4014 5510 5120 729 2880 9019 2049 698 4582 4346 4427 646 9742 7340 1230 7683 5693 7015 6887 7381 4172 4341 2909 2027 7355 5649 6701 6645 1671 5978 2704 9926 295 3125 3878 6785 2066 4247 4800 1578 6652 4616 1113 6205 3264 2915 3966 5291 2904 1285 2193 1428 2265 8730 9436 7074 689 5510 8243 6114 337 4096 8199 7313 3685 211

7.1 方法1-python 因为是填空题,填空题不限制做法,只求答案,我们可以利用python大数无限制的特性,直接暴力求出乘积,最后计算有多少0就行。
7.2 方法2-分解质因子 先提一个问题4 和 5的乘积最后有多少个0呢?答案是1个。
0是怎么来的呢?是4*5得来的吗?本质上是2*5得来的,我们看10的约数:1 2 5 10。
由此可以看出,所有10都是由10或者2*5构成的。并且分解出质因子2,是不会影响5质因子个数的,因为2和5的最大公约数是1。如果不理解的话,可以看16,如果我们要分解2和4的话,分解了4,那2的个数就会减少,因此他们俩会互相影响,但是2和5没有这样的问题。
即一旦分数2和5,就可以得到真正这个数的2因子个数和5因子个数。
再看4*5 = 2*2*5。所有的数的乘积一定等于他们因子之间的乘积,这是一定的。
所以问题就转变为,2*2*5了,再看他们能够造出10的个数又取决与谁呢?取决于2和5中较小的那个。当然如果又2*2*5*10。那10也要被算进个数里。
完整代码:

8 第几天
题目描述
2000年的1月1日,是那一年的第1天。
那么,2000年的5月4日,是那一年的第几天?
蓝桥杯常考年份,月份,天数之间的转换问题。这是有套路可寻的。

    推荐阅读