第十四届蓝桥杯省赛C++ A组

文章讲述了在蓝桥杯竞赛中遇到的编程题目,涉及数学原理如平方差数的性质、动态规划解决回文串问题以及并查集或DSU算法在颜色平衡树问题的应用。
摘要由CSDN通过智能技术生成

不算前言的前言:去年年底斥巨资报考了蓝桥杯,其实本人考完csp后完全疲惫变懒了…但想到花出的钱!…觉得这两周还是得练一下之前的真题,咱就是说也不能完全打水漂对吧…
OK让咱们先从最近的一次考试开始!!
贴出一个真题网址:蓝桥杯真题

平方差

给定 L,R,问 L ≤ x ≤ R中有多少个数 x满足存在整数 y,z使得 x = y2−z2
输入格式
输入一行包含两个整数 L,R,用一个空格分隔。
输出格式
输出一行包含一个整数满足题目给定条件的 x 的数量。
数据范围
对于 40%的评测用例,1 ≤ L,R ≤ 5000;
对于所有评测用例,1 ≤ L ≤ R ≤ 109。
输入样例:
1 5
输出样例:
4
样例解释
1=12−02
3=22−12
4=22−02
5=32−22

这一题很明显遍历是会超时的;而且很明显这种题目的捷径好像和代码逻辑上的优化没太大关系…主要从数学的原理上下手;
代码撰写参考了这份博客:AcWing 4996. 🌟详解+O(1)简洁写法(附证明)
主要涉及数学原理上,我应该怎么来找到这样的平方差数(这样的平方差数有什么数学特性,可以让我在遍历的时候直接根据这个简化的特性大大减少搜索难度,减少时间的开销)

根据平方差公式,若有:x = y2 - z2,则 x = (y + z) * (y - z)
令A = (y + z) ,B = (y - z) ,则有 A - B = 2 * z; 也就是说 A, B两数之差为偶数,则A, B 的组合只能是同为奇数或者同为偶数
转化为 x 的性质:x 可以看作两个奇数的乘积或者是两个偶数的乘积;继续变抽象为具象,这样的转化可以具体化为 x 的怎样的性质。接下来我们分情况讨论,在这两种情况下,可以得到的最大共性是什么

如果一个数字是两个奇数的乘积:因为1本身为奇数,所以所有的奇数都可以写作 奇数 * 1;也就是所有的奇数都可以表示成为两个奇数的乘积。此时找到最大的共性特质。
如果一个数字是两个偶数的乘积:则这个数字必然是4的倍数。此时找到最大的共性特质

整理来说,x 如果是奇数,或者是4的倍数,那么这个数字必然可以写作平方差的形式

下面写给出一种超时写法:洛谷上过了,但是C语言网和acwing都超时了

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
    ll l, r; cin >> l >> r;
    int cnt = 0;
    
    for(int i = l; i <= r; i ++){
        if(i & 1 || i % 4 == 0) cnt ++;
    }
    cout << cnt;
    return 0;
}

此时这个写法还是在遍历,只是在判断数字是否符合条件的时候将时间复杂度下降到了O(1)但是搜索遍历的数量级还是到了1e9级别;所以此时涉及优化。其实也很简单,就是求一个区间内奇数的个数和4的倍数的个数

AC代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
 //返回0 - x 之内的奇数个数加上4的倍数的个数
int fun(ll x)
{
	//下面两种写法都可以
    //return x / 4 + (x + 1) / 2;
    return (x / 4 + ceil(x / double(2)));  
}
int main()
{
    ll l, r; cin >> l >> r;
    cout << fun(r) - fun(l - 1);
    return 0;
}

更小的数

小蓝有一个长度均为 n 且仅由数字字符 0∼9组成的字符串,下标从 0 到 n−1,你可以将其视作是一个具有 n位的十进制数字 num,小蓝可以从 num中选出一段连续的子串并将子串进行反转,最多反转一次。
小蓝想要将选出的子串进行反转后再放入原位置处得到的新的数字 numnew
满足条件 numnew<num,请你帮他计算下一共有多少种不同的子串选择方案,只要两个子串在 num
中的位置不完全相同我们就视作是不同的方案。
注意,我们允许前导零的存在,即数字的最高位可以是 0,这是合法的。
输入格式
输入一行包含一个长度为 n 的字符串表示 num(仅包含数字字符 0∼9),从左至右下标依次为 0∼n−1。
输出格式
输出一行包含一个整数表示答案。
数据范围
对于 20% 的评测用例,1≤n≤100;
对于 40% 的评测用例,1≤n≤1000;
对于所有评测用例,1≤n≤5000。
输入样例:
210102
输出样例:
8
样例解释
一共有 8种不同的方案:
所选择的子串下标为 0∼1,反转后的 numnew=120102<210102;
所选择的子串下标为 0∼2,反转后的 numnew=012102<210102;
所选择的子串下标为 0∼3,反转后的 numnew=101202<210102;
所选择的子串下标为 0∼4,反转后的 numnew=010122<210102;
所选择的子串下标为 0∼5,反转后的 numnew=201012<210102;
所选择的子串下标为 1∼2,反转后的 numnew~=201102<210102;
所选择的子串下标为 1∼4,反转后的 numnew=201012<210102;
所选择的子串下标为 3∼4,反转后的 numnew=210012<210102;

该题涉及的是字符串问题,就是找回文字符串;很明显死办法肯定会超时,想到了回文串是动态规划里面的一个经典问题。
果不其然要用dp,however我真的很久没有写过dp了orz

本题考察的是区间dp,也就是在区间上用动态规划。其中区间的范围要从小到大
比方说我的第一想法是:跑两个循环找区间,提取出这个区间的字符子串,然后从两端开始向内判断,其实也就是做比较,比方说我的初始代码如下:

void handle(string str)
{
    int i = 0, j = str.size();
    bool flag = false;
    while(i < j){
        if(str[i] == str[j]) {i ++; j --;}
        else if(str[j] > str[i]) {break;}
        else {flag = true; break;}
    }
    if(flag) cnt ++;
}

为什么会TLE,如何简化?其实跟引入动态规划的初衷一样,在上面的做法中我重复比较了;
比如说比较区间[0, 5],[1, 4], [2, 3],按照上面的做法,[2, 3]这个区间因为作为子区间,其实会被同样的方法比较多次!如果区间很大的话,会出现做重复工作的情况!!

这就说明,我应该是先比较小的区间,再看大的区间;大的区间在小的区间上逐步扩大,此时只需要比较引进来的端点的大小即可,其他的直接取用已经计算过的小区间结果
下面贴上AC代码:

#include<bits/stdc++.h>
using namespace std;
const int N = 5e3 + 10;
int dp[N][N];   //dp[i][j]表示扭转i - j 位置的字串时符合题意
int main()
{
    string number; cin >> number;
    int n = number.size();

    int cnt  = 0;

    for(int len = 2; len <= n;len ++){    //区间长度从小到大, 从长度为2的字串到长度为n的字串
        for(int l = 0; l + len - 1 < n; l ++){   //遍历左端点
            int r = l + len - 1;
            if(number[r] < number[l]) {   //右端点小于左端点,直接符合答案
                cnt ++;
                dp[l][r] = 1;  //符合答案
            }
            else if(number[l] == number[r]){    //需要比较子串, 相当于原来代码的l ++, r --;比较一个更小的区间
                cnt += dp[l + 1][r - 1];
                dp[l][r] = dp[l + 1][r - 1];   //如果子串符合的话, 此时l - r也符合
            }
        }
    }
    cout << cnt;
    return 0;
}


颜色平衡树

给定一棵树,结点由 1 至 n 编号,其中结点 1 是树根。树的每个点有一个颜色 Ci
如果一棵树中存在的每种颜色的结点个数都相同,则我们称它是一棵颜色平衡树。
求出这棵树中有多少个子树是颜色平衡树。
输入格式
输入的第一行包含一个整数 n,表示树的结点数。
接下来 n 行,每行包含两个整数 Ci,Fi,用一个空格分隔,表示第 i 个结点的颜色和父亲结点编号。
特别地,输入数据保证 F1 为 0,也即 1 号点没有父亲结点。
保证输入数据是一棵树。
输出格式
输出一行包含一个整数表示答案。
数据范围
对于 30% 的评测用例,n≤200,Ci≤200;
对于 60% 的评测用例,n≤5000,Ci≤5000;
对于所有评测用例,1≤n≤200000,1≤Ci≤200000,0≤Fi<i。
输入样例:
6
2 0
2 1
1 2
3 3
3 4
1 4
输出样例:
4
样例解释
编号为 1,3,5,6 的 4 个结点对应的子树为颜色平衡树。

做蓝桥杯的题目的感觉就是…都能做但是…都会超时
however在csp的训练之下我的第一份代码如下,果不其然是超时代码,其中acwing只通过了 4 / 20 个

#include<bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;   //节点编号 + 颜色定位一个数量
const int N = 2e5 + 10;
int father[N];
unordered_map<int, unordered_set<int> > colorset;   //编号为i的节点具有哪些颜色
map<PII, int> cnt;
int main()
{
    int n; cin >> n;
    for(int i = 1; i <= n;i ++){
        int c, fa; cin >> c >> fa;
        father[i] = fa;

        int j = i;
        while(j != 0){
            colorset[j].insert(c);
            cnt[{j, c}] ++;
            j = father[j];
        }
    }
    int res = 0;
    for(int i = 1;i <= n;i ++){
        unordered_set<int> color = colorset[i];
        int stand = cnt[{i, *color.begin()}];
        bool flag = true;
        for(auto x : color){
            if(cnt[{i, x}] != stand) { flag = false; break; }
        }
        if(flag) res ++;
    }
    cout << res;
    return 0;
}

看了一下大佬们的解析,这题用到的算法是dsu on tree
知道这个算法以后,这道题就像是一道模板题…当然说起来简单学起来难啊,看代码看了蛮久但其实如果没有系统的学习并查集什么的话…其实我也没有看太懂

我简单了参考了这篇博客:树上启发式合并(dsu on tree)

自己理解的解释就是,在合并两个集合(A, B)形成一个新的集合过程中,一个减少时间复杂度的方法就是:遍历规模更小的那个集合(A),将它的所有元素添加到集合B中。完成该过程后,集合B就是我们所求的合并后的集合。
在题目中我们会遇到这样一类问题,题目的架构会形成一颗树。我们的问题通常是对节点信息的查询,而这个查询涉及到该节点的子树。例如在本题中,对于每个节点所构成的子树,我们都需要搜索一遍,找到这个节点形成的子树中包含哪些颜色,以及这些颜色的节点各有多少个。也就是该题其实就是对节点信息的整理,而该信息需要合并所有子节点的信息才能得到

模拟最根本的集合合并原理。我们找到每个节点的,包含节点最多的 [重儿子],将[轻儿子]的信息合并到重儿子当中去,那么最后重儿子节点就保留了所需要的全部孩子节点信息

看了蛮多博客,讲原理的不多,给模板代码的更多,总的来说流程就是

  • 找重孩子
  • 遍历所有的轻孩子,处理轻孩子;清空轻孩子的操作
  • 遍历重孩子
  • 用全局数组来计算父节点的相应值

说的一套一套的其实我自己也没有完全消化…对了!搜索结构都是基于深搜完成的(保证树形结构嘛)
下面是一份带注释的满分代码:(代码参考了 AcWing 4998. 颜色平衡树

#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10, M = 2 * N;  //N为顶点数量, M为边的数量
int n;
int h[N], ne[M], e[M], idx;    //常用的几件套了,树图都很常见的一个结构了
int color[N];      //记录每个节点的颜色
int sz[N];        //记录每个节点的规模
int son[N];      //记录每个节点的重孩子编号
int cnt[N];     //记录每个颜色出现的次数
int res, sum, mx;
//录入一条从a --> b的边
void add(int a, int b)
{
    e[idx] = b; 
    ne[idx] = h[a]; 
    h[a] = idx ++;
}
// 计算以节点 u 为根的子树大小
int dfs_son(int u, int father)
{
    sz[u] = 1;    //初始只有当前节点, size为1
    for(int i = h[u]; ~i; i = ne[i]){
        int j = e[i]; 
        if(j == father) continue;   //遍历孩子
        sz[u] += dfs_son(j, u);
        if(sz[j] > sz[son[u]]) son[u] = j;   //找到重孩子
    }
    return sz[u];
}

void update(int u, int father, int val, int pson)
{
    int c = color[u];
    cnt[c] += val;
    if(cnt[c] > mx) { mx = cnt[c]; sum = 1; }
    else if(cnt[c] == mx) sum ++;
    
    for(int i = h[u]; ~i; i = ne[i]){
        int j = e[i];
        if(j == pson || j == father) continue;
        update(j, u, val, pson);
    }
}
// 遍历以节点 u 为根的子树
void dfs(int u, int father, int op)   //我觉得在这里Op像一个指示, 当前节点是不是重孩子, 然后操作有所不同
{
    for(int i = h[u]; ~i; i = ne[i]){
        int j = e[i];
        if(j == son[u] || j == father) continue;  //跳过重孩子和父亲
        dfs(j, u, 0); //遍历所有的轻孩子
    }
    if(son[u]) dfs(son[u], u, 1);
    update(u, father, 1, son[u]);
    
    if(sum * mx == sz[u]) res ++;
    if(!op) { update(u, father, -1, 0); mx = 0; } //撤销操作 // 回溯时清除计数数组并重置最大计数
}

int main() 
{
    cin >> n;
    memset(h, -1, sizeof h);    //设置初始值
    for(int i = 1;i <= n;i ++){
        int fa;
        cin >> color[i] >> fa;
        if(i != 1) { add(fa, i); add(i, fa); }
    }
    
    dfs_son(1, -1);
    dfs(1, -1, 1);
    cout << res;
    return 0;
}

持续更新ing…

  • 37
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值