第十二届蓝桥杯大赛软件赛决赛题解 C/C++B组

第十二届蓝桥杯大赛软件赛决赛C/C++ 大学 B 组

结果填空题

填空题答案一览

题目答案分值
A: 带宽255
B: 纯质数19035
C: 完全日期97710
D: 最小权值265363137210

A: 带宽

分析:
代码:

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

int main(){
    cout<< 200 / 8 <<endl;
    return 0;
}

B: 纯质数

分析: 质数筛,遇到质数的同时判断是否是否符合纯质数
代码:

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

const int n = 20210605;
bool f[n+10];

bool check(int n){
    while(n){
        int t = n % 10;
        if(t != 2 && t != 3 && t != 5 && t != 7) return false;
        n/=10;
    }
    return true;
}
int main(){
    f[2] = 0;
    int ans = 0;
    //质数筛
    for(int i = 2;i<=n;i++){
        if(!f[i]){
            if(check(i)) ans ++;
            for(int j = 2; i*j <= n;j++) f[i*j] = 1;
        }
    }
    cout<< ans <<endl;
    return 0;
}

C: 完全日期

分析: 枚举所有的日期一个个进行判断,具体细节实现可以看代码,有不少小技巧
代码:

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

int M[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

bool checkYear(int y){
    return (y%400 == 0 || (y % 100 != 0 && y % 4 == 0));
}

bool check(int y, int m, int d){
    int n = 0;
    while(y || m || d){
        n += y % 10 + m % 10 + d % 10;
        y/=10, m/=10, d/=10;
    }
    int t = sqrt(n);
    return t * t == n;
}
int main(){
    int y = 2001, m = 1, d = 1;
    int ans = 0;
    while(y != 2021 || m != 12 || d != 31){
        M[2] = checkYear(y)? 29 : 28;
        if(check(y,m,d)) ans ++;
        ++d;
        if(d > M[m]){
            ++m; d = 1;
        }
        if(m > 12){
            ++y; m = 1;
        }
    }
    cout<< ans <<endl;
    return 0;
}

D: 最小权值

分析: 题目概括:假设某颗二叉树的节点数为v,怎么分配左右子树的节点数,使得这颗的权重 W ( v ) W(v) W(v)最小。二叉树的权值计算公式为: W ( v ) = 1 + 2 W ( L ) + 3 W ( R ) + ( C ( L ) ) 2 C ( R ) W(v) = 1 + 2W(L) + 3W(R) + (C(L))2 C(R) W(v)=1+2W(L)+3W(R)+(C(L))2C(R)。(解释: W ( L ) W(L) W(L):左子树的权重、 W ( R ) W(R) W(R):右子树的权重、 C ( L ) C(L) C(L):左子树节点数、 C ( R ) C(R) C(R):右子树节点数。)

很显然可以这样思考:枚举所有的左子树的节点数,取计算出的所有 W ( v ) W(v) W(v)中最小权重就是所求。

举例子:求节点总数为5的二叉树最小权重,那么左子树的节点数可能是0,1,2,3,4(不能为5,因为根节点占用了一个节点,可以为0,因为子树可以为空),对应的右子树的节点数可能是4,3,2,1,0,将每一对的可能带进公式,求最小的w,就是节点总数为5的二叉树最小权重。

观察公式: W ( v ) W(v) W(v)的结果又由 W ( L ) W(L) W(L) W ( R ) W(R) W(R)的状态得来,这就涉及状态转移。并且, L L L R R R总是恒小于 v v v,因为 v = L + R + 1 ( 0 < = L , R < v ) v = L + R + 1 (0 <=L, R < v) v=L+R+1(0<=L,R<v)

状态转移方程:
W ( v ) = { 0 , v = 0 m i n ( c o u n t ( L , v − L − 1 ) , 0 < = L < v ) , v > 0 W(v)=\begin{cases} 0,v=0\\ min(count(L,v-L-1),0 <= L < v), v>0\end{cases} W(v)={0v=0min(count(L,vL1)0<=L<v)v>0

代码:

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

typedef long long ll;
const int n = 2021;
ll f[n+10];

int main(){
    f[0] = 0;
    for(int i = 1; i<=n; i++)
    {
        ll w = LLONG_MAX;
        for(int j = 0; j<i; j++){
            int r = i - j - 1;
            ll t = 1 + 2ll*f[j] + 3ll*f[r] + 1ll*j*j*r;
            w = min (w, t);
        }
        f[i] = w;
    }
    cout<< f[n] <<endl;
    return 0;
}

程序设计题

E: 大写

分析:
代码:

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

int main(){
    string s;
    cin >> s;
    for(char& c : s) c = (toupper(c));
    cout << s;
    return 0;
}

F: 123

分析: 利用前缀和O(1)时间复杂度内求出区间和这一特性:比如f是前缀和数组求[l,r]区间和则:[l,r] = f[0,r] - f[0, l - 1]。用了前缀和数组预处理,还要思考一点数据规模的大小:

对于所有评测用例, 1 ≤ T ≤ 100000 , 1 ≤ l i ≤ r i ≤ 1 0 12 。 1 ≤ T ≤ 100000, 1 ≤ li ≤ ri ≤ 10^{12}。 1T100000,1liri1012

显然,开一个 1 0 12 10^{12} 1012长度的数组是不现实的,并且初始化这个前缀和数组的时间复杂度也已经超出限制。所以,怎么优化这个前缀和数组就是这题的关键。
思考下面的三个式子(0是作为前缀和的辅助项,方面写代码):

0	1  	1 2  	1 2 3  	1 2 3 4  	1 2 3 4 5	//原始数列
0	1  	3  		6 		10 			15		//t[i]: 前i段数列一共有多少项-用来求索引
0	1  	4 		10 		20 			35		//f[i]:前i段数列的所有项的和-前缀和数组、用来求区间和

有没有看出一丝端倪,将原始数列压缩为两个如上所示的前缀和数组后,前缀和的数组长度不到2w(代码跑出来的),就可以达到空间要求。
怎么使用这两个前缀和数组?
问:当l = 5, r = 8时,分别先从索引数组找到最后一位小于等于 l or r(l二分查找,复杂度 O ( l o g n ) O(log_n) O(logn)对应的索引,这里是2(对应索引数组中t[2] = 3,因为65大,不符合要求)。同理找到8的索引下标3(t[3]=6),通过两个下标,访问对应的f[i]为: f[2] = 4f[3] = 10。重点来了,前4项和前6项作差,是我们要求的l = 5, r = 8的结果吗?显然不是!
由于每一段的数列都是由1开始递增为1的等差数列,并且剩下的项不可能跨越到下一个索引(因为下一个索引的项数一定比目标值大,不理解回头看二分查找的目标值),所以我们就可以通过等差数列求和公式求出剩余项的和,再相加求出区间和:

等差数列求和公式: n(1 + n)/2;
f8 = [0,8] = f[3] + b-f[3](1 + b-f[3]) / 2;
f5 = [0,5-1] = f[2] + b-f[2](1 + b-f[2]) / 2; //[0,5-1],因为求[5, 8]是闭合区间,包括第5项
[5, 8] = f8 - f5; //查询的答案

具体看代码!
代码:

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

typedef long long ll;
const int  maxn = 200000;
int n = 1;
ll p[maxn], t[maxn]; //两个前缀和数组

//二分:查找最后一个小于等于num的索引
int find(ll num){
    int l = 0, r = n;
    while(l < r){
        ll mid = l + (r-l)/2;
        if(t[mid] >= num) r = mid - 1;
        else l = mid+1;
    }
    if(t[l] > num) --l;
    return l;
}

void pv(int l ,int r){
    --l;
    int i = find(l), j = find(r); 		//求索引
    int sl = l - t[i], sr = r - t[j];	//未覆盖的项
    ll ans = p[j] + sr*(1 + sr)/2 - p[i] - sl*(1 + sl)/2; //区间作差
    cout<< ans <<endl;
}

int main(){
    p[0] = t[0] = 0;
    //前缀和预处理
    for(;;n++){
        t[n] += t[n-1] + n;
        p[n] = n*(1+n)/2 + p[n-1];
        if(p[n] >= 1e12) break;
    }
     int n;
     cin >> n;
     while(n --){
         int l , r;
         cin >> l >> r;
         pv(l, r);
     }
    return 0;
}

G: 异或变换

分析: 数据规模1 ≤ t ≤ 1018,循环t次会超时。找规律。

输入10110时,模拟一下

11101
10011
11010
10111
11100
10010
11011
10110

11101
10011
11010
10111
11100

换一个数据试试0110

0101
0111
0100
0110

0101
0111
0100

再试一个0010

0011
0010

0011
0010

再试一个1110

1001
1101
1011
1110

1001

再来一个000010

000011
000010

000011
000010

手动举例和大佬的讲的:

G异或变换的周期是比n大的二进制幂,动手写一下就知道了,5的话周期就是8,13的话周期就是16

总结:周期是比n的有效长度(去除前缀0后的长度)大的二进制幂。比赛的时候推出来有周期,但是推不出来周期怎么算。。
代码:

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

const int maxn = 10010;
char s[maxn], a[maxn];

int main(){
    int n, t;
    cin >> n >> t >> s;
    int len = n;
    for(int i = 0;i<n && s[i] == '0';i++) len--; //有效长度
    int c = 1; //周期
    while(c < len) c <<= 1;
    t %= c;
    
    a[0] = s[0];
    //a 和 s 交替滚动
    for(int j = 1; j <= t; j++){
        for(int i = 1;i<n;i++) 
            if(j%2) a[i] = (s[i]^s[i-1]) ? '1' : '0';
            else s[i] = (a[i]^a[i-1]) ? '1' : '0';
    }
    
    for(int i = 0;i<n;i++) printf("%c", t%2? a[i] : s[i]);
    
    return 0;
}

H: 二进制问题

分析: 朴素算法:遍历1n,判断是否二进制表示1的个数是k。只能过30%的数据
代码:

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

int n, k;
//i的二进制表示中1的个数是否为k个
bool check(int i){
    int res = 0;
    while(i){
        ++res;
        i &= (i-1);
    }
    return res == k;
}
int main(){
    cin >> n >> k; 
    int ans = 0;
    for(int i = 1;i <= n;i++){
        if(check(i)) ans++;
    }
    cout<< ans;
    return 0;
}

I: 翻转括号序列 (待更)

分析:
代码:


J: 异或三角(待更)

分析:
代码:


  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值