2024第十五届蓝桥杯c/c++B组国赛题解

目录

前记:

合法密码

思路:

代码:

选数概率

题目描述

思路:

蚂蚁开会

题目:

思路:

代码:

数位翻转

题目描述

[一、为什么这题要用 DP?](#一、为什么这题要用 DP?)

[1.1 先看暴力做法的问题](#1.1 先看暴力做法的问题)

[1.2 问题具有最优子结构](#1.2 问题具有最优子结构)

[1.3 问题具有重叠子问题](#1.3 问题具有重叠子问题)

[二、如何想到 DP 状态设计?](#二、如何想到 DP 状态设计?)

[2.1 第一步:确定要记录什么信息](#2.1 第一步:确定要记录什么信息)

[2.2 第二步:尝试一维 DP(失败)](#2.2 第二步:尝试一维 DP(失败))

[2.3 第三步:加一维记录区间数](#2.3 第三步:加一维记录区间数)

[2.4 第四步:再加一维记录当前状态](#2.4 第四步:再加一维记录当前状态)

[三、DP 数组的构建过程](#三、DP 数组的构建过程)

[3.1 初始化](#3.1 初始化)

[3.2 填表顺序](#3.2 填表顺序)

[3.3 推导转移方程](#3.3 推导转移方程)

四、完整填表模拟

初始化

[i=1, b1=2](#i=1, b[1]=2)

[i=2, b2=-9](#i=2, b[2]=-9)

[i=3, b3=-2](#i=3, b[3]=-2)

五、最终答案

[六、DP 设计的核心思想](#六、DP 设计的核心思想)

代码:

最小字符串

题目:

思路:

代码:

立定跳远

题目:

思路:

代码:


前记:

这几道题是我认为在赛时有希望能做出来的题,全做出来是60分,再加上另外几题的暴力大概最终能拿到75左右

合法密码

题目:

小蓝正在开发自己的 OJ 网站。他要求网站用户的密码必须符合以下条件:

  1. 长度大于等于 8 个字符,小于等于 16 个字符。
  2. 必须包含至少 1 个数字字符和至少 1 个符号字符。

例如 lanqiao2024!+-*/06018((>w<))8 都是合法的密码。

12345678##**##**abc0!#lanqiao20240601!? 都不是合法的密码。

请你计算以下的字符串中,有多少个子串可以当作合法密码?只要两个子串的开头字符和末尾字符在原串中的位置不同,就算作不同的子串。

字符串为:

cpp 复制代码
kfdhtshmrw4nxg#f44ehlbn33ccto#mwfn2waebry#3qd1ubwyhcyuavuajb#vyecsycuzsmwp31ipzah#catatja3kaqbcss2th

思路:

暴力遍历全部可能的区间再判断即可,题目给的字符串中只有'#'这一个字符,所以要求二所说的至少包含一个字符只能是'#'

代码:

最终答案是400

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'

bool check(string ss) {
    int fg1 = 0, fg2 = 0;
    for(int i = 0; i < ss.length(); i++) {
        if(ss[i] == '#') fg1++;
        if(ss[i] >= '0' && ss[i] <= '9') fg2++;  
    }
    return (fg1 && fg2);
}

void solve() {
    string s;
    cin >> s;
    int ans = 0;
    int n = s.size();
    
    for(int i = 0; i < n; i++) {
        for(int j = 8; j <= 16; j++) {
            if(i + j <= n) {  
                if(check(s.substr(i, j))) ans++;
            }
        }
    }
    cout << ans << endl;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);
    int t = 1;
    while(t--)
        solve();
    return 0;
}

选数概率

题目描述

一个数组中有 a 个 1,b 个 2,c 个 3。设 Pi,j​ 表示在数组中随机选取两个数,其中一个数为 i,另一个数为 j 的概率。比如 P1,2​=ab/C(a+b+c,2)​,其中 C(N,M) 为组合数,表示从 N 个不同元素中任取 M 个的方案数。

当 a=?,b=?,c=? 时,满足 P1,2​=517/2091​,P2,3​=2632​/10455,P1,3​=308/2091​,且 a+b+c 最小。保证 a+b+c 最小的解是唯一的。

你需要提交一个格式为 a,b,c 的字符串。例如假设你计算的结果是 a=12,b=34,c=56,那么你需要提交的字符串是 12,34,56

思路:

数学化简:

P1,2,P2,3,P1,3的分母都是相同的(都是C(a+b+c,2)),故而可以直接相除约掉分母,得到:

可以得到a:b:c=55:94:56

答案要求a+b+c最小,故而最终答案就是55,94,56

蚂蚁开会

题目:

题目给定n个线段,让你找出这n个线段的交点为整数的个数

思路:

把这些线段上的全部整数点都用map(因为map键值可以是pair,故而用map记录很方便)记录出现次数,大于等于2的就是符合要求的

关键:如何通过两端点找到该线段上的全部点呢?

首先我们知道,线段上的点的间隔都是均匀的,故而如果我们知道线段上共有几个点,那就能挨个求出线段上的全部点

结论:线段上的整点个数 = gcd(x方向距离, y方向距离) + 1

x方向距离:两点间横向距离

y方向距离:两点间纵向距离

代码:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int mod=1e9+7;
const int N=2e5+10;
int xx[510],yy[510],x[510],y[510];
void solve() {
	int n;
    cin >> n;
    map<pair<int,int>,int> mp;
    for (int i=0;i<n;i++) {
        cin >> xx[i] >> yy[i];
        cin >> x[i] >> y[i];
        int dx=x[i]-xx[i],dy=y[i]-yy[i];
        int d=__gcd(abs(dx),abs(dy));
计算点数
        dx/=d,dy/=d;
从起点到终点,共有d+1个点,但是只需要走d步,故而是各自除以d得到每一步所走的长度,而非d+1,
        for (int j=0;;j++) {
            int xt=xx[i]+j*dx,yt=yy[i]+j*dy;
            mp[{xt,yt}]++;
            if (xt==x[i] && yt==y[i]) {
                break;
            }
        }
    }
    int ans=0;
    for (auto it=mp.begin();it!=mp.end();it++) {
        if (it->second>=2) {
            ans++;
        }
    }
使用迭代器遍历整个map
    cout << ans << endl;
}
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);
    int t=1;
    // cin>>t;
    while(t--)
        solve();
}

数位翻转

题目描述

小明创造了一个函数 f(x) 用来翻转 x 的二进制的数位(无前导 0)。比如 f(11)=13,因为 11=(1011)2​,将其左右翻转后,变为 13=(1101)2​;再比如 f(3)=3,f(0)=0,f(2)=f(4)=f(8)=1 等等。

小明随机出了一个长度为 n 的整数数组 {a1​,a2​,⋯,an​},他想知道,在这个数组中选择最多 m 个不相交的区间,将这些区间内的数进行二进制数位翻转(将 ai​ 变为 f(ai​))后,整个数组的和最大是多少?

思路:

本题题目范围是n,m都小于1000,这是个提示点,意味着我们可以通过n方时间复杂度的思路来解决

我们仔细观察一下,发现如果拿翻转后的数和翻转前的数对应相减的话,可以得到一个贡献数组。例如:

cpp 复制代码
翻转前:
11 12 13 14 15
翻转后:
13 3 11 7 15
贡献序列:
2 -9 -2 -7 0

此时思路就明朗了,只需要找x段最大的贡献子序列即可,注意不一定要找够m段,比如贡献序列如果全是负数的话就一段都不需要。

如何找呢?

暴力肯定不行,我们此时就能想到动态规划。我感觉我讲的不是很清楚,所以我把我思路发给AI让AI帮我润色了一下,下面是AI润色后的内容:

一、为什么这题要用 DP?

1.1 先看暴力做法的问题

如果我们暴力枚举所有方案:

  • 每个数有选/不选两种可能

  • n 个数有 2^n 种组合

  • n=1000 时,2^1000 完全不可计算

1.2 问题具有最优子结构

观察这个问题:

cpp 复制代码
考虑前 i 个数,选 j 段的最优解
可以由:前 i-1 个数的最优解 + 第 i 个数的决策 得到

这就是动态规划的核心:大问题的最优解包含小问题的最优解。

1.3 问题具有重叠子问题

计算时,会反复用到"前 i 个数,选 j 段"的结果,DP 可以避免重复计算。

二、如何想到 DP 状态设计?

2.1 第一步:确定要记录什么信息

我们要做决策:每个数选还是不选?

但仅仅这样不够,因为还需要知道:

  • 当前选了几个区间了?(不能超过 m)

  • 上一个数选了没有?(决定是否要新开区间)

所以需要记录:

  1. 位置 i:考虑到第几个数

  2. 区间数 j:已经选了几个区间

  3. 上个数状态:上一个数选了没有

2.2 第二步:尝试一维 DP(失败)

最初想法:dp[i] = 前 i 个数的最大贡献

但这样无法知道选了几个区间,可能选多了。

2.3 第三步:加一维记录区间数
cpp 复制代码
dp[i][j]  // 前 i 个数,选 j 个区间的最大贡献

但还是有问题:不知道第 i 个数选了没有,无法决定第 i+1 个怎么选。

2.4 第四步:再加一维记录当前状态
cpp 复制代码
dp[i][j][0]  // 前 i 个数,选 j 个区间,第 i 个数【不选】
dp[i][j][1]  // 前 i 个数,选 j 个区间,第 i 个数【选】

这样就完美了!

三、DP 数组的构建过程

3.1 初始化
cpp 复制代码
// 0 个数,选 0 段,无论第 0 个状态如何,贡献都是 0
for (int j = 0; j <= m; j++) {
    dp[0][j][0] = 0;
    dp[0][j][1] = 0;  // 没有数,不可能选,设为 0 不影响后续
}
3.2 填表顺序
cpp 复制代码
for (int i = 1; i <= n; i++) {        // 外层:逐个考虑每个数
    for (int j = 1; j <= m; j++) {    // 内层:枚举区间数
        // 计算 dp[i][j][0] 和 dp[i][j][1]
    }
}

为什么这样填?

  • dp[i] 只依赖 dp[i-1],所以 i 从小到大

  • j 的依赖关系稍微复杂,但从小到大填没问题

3.3 推导转移方程

对于 dp[i][j][0](第 i 个不选)

cpp 复制代码
前 i 个数,选 j 段,第 i 个不选
         ↓
等于前 i-1 个数,选 j 段的最优情况
         ↓
max(dp[i-1][j][0], dp[i-1][j][1])

为什么段数不变?因为第 i 个不选,不产生新段。

图示理解

cpp 复制代码
情况1:[ ... ✓ ] ✗    ← 第 i-1 选,第 i 不选
情况2:[ ... ✗ ] ✗    ← 第 i-1 也不选
      ↑前i-1个,j段   ↑第i个

对于 dp[i][j][1](第 i 个选)

需要分两种情况:

情况A:第 i 个新开一段

cpp 复制代码
前 i-1 个数,选 j-1 段,第 i-1 不选
         +
第 i 个数作为第 j 段

= dp[i-1][j-1][0] + b[i]

为什么第 i-1 必须不选?

  • 如果第 i-1 选了,第 i 个又选,它们就连在一起了

  • 连在一起就是同一段,不会增加段数

图示

cpp 复制代码
[ ... ] ✗ | ✓
↑前i-1个  ↑第i个(新段)
段数 j-1    段数变成 j

情况B:第 i 个接在后面

cpp 复制代码
前 i-1 个数,选 j 段,第 i-1 选
         +
第 i 个数接上

= dp[i-1][j][1] + b[i]

为什么段数不变?

  • 第 i-1 已经选了,第 i 接在后面

  • 它们属于同一段

图示

cpp 复制代码
[ ... ✓ ✓
↑前i-1个  ↑第i个
段数 j    段数还是 j

取两种情况的最大值:

cpp 复制代码
dp[i][j][1] = max(dp[i-1][j-1][0], dp[i-1][j][1]) + b[i];

四、完整填表模拟

假设 b = [2, -9, -2], m = 2

初始化
cpp 复制代码
dp[0][*][*] = 0
i=1, b1=2
cpp 复制代码
dp[1][1][0] = max(dp[0][1][0], dp[0][1][1]) = 0
dp[1][1][1] = max(dp[0][0][0], dp[0][1][1]) + 2 = 0+2 = 2
dp[1][2][0] = 0
dp[1][2][1] = max(dp[0][1][0], dp[0][2][1]) + 2 = 0+2 = 2

理解

  • dp[1][1][1]=2:选第1个数,占1段,贡献2 ✓

  • dp[1][2][1]=2:选第1个数,占2段?(实际不可能,但数值上不影响)

i=2, b2=-9
cpp 复制代码
dp[2][1][0] = max(dp[1][1][0], dp[1][1][1]) 
            = max(0, 2) = 2

→ 不选第2个,保留前面选第1个的贡献

cpp 复制代码
dp[2][1][1] = max(dp[1][0][0], dp[1][1][1]) + (-9)
            = max(0, 2) + (-9) = -7

→ 选第2个,且延续第1段:2-9=-7

cpp 复制代码
dp[2][2][1] = max(dp[1][1][0], dp[1][2][1]) + (-9)
            = max(0, 2) + (-9) = -7

→ 不选第2个,保持前面(虽然前面选了2段是不可能的,但值不影响)

cpp 复制代码
dp[2][2][1] = max(dp[1][1][0], dp[1][2][1]) + (-9)
            = max(0, 2) + (-9) = -7

→ 新开第2段:前1个选1段且第1个不选(0) + (-9) = -9

→ 延续:前1个选2段且第1个选(2) + (-9) = -7

→ 取 max = -7

i=3, b3=-2
cpp 复制代码
dp[3][1][0] = max(dp[2][1][0], dp[2][1][1]) 
            = max(2, -7) = 2

→ 不选第3个,保持最优(只选第1个)

cpp 复制代码
dp[3][1][1] = max(dp[2][0][0], dp[2][1][1]) + (-2)
            = max(0, -7) + (-2) = -2

→ 延续第1段:-7-2=-9;新开:0-2=-2;取-2

cpp 复制代码
dp[3][2][0] = max(dp[2][2][0], dp[2][2][1]) = max(2, -7) = 2

→ 不选第3个,保持前面

cpp 复制代码
dp[3][2][1] = max(dp[2][1][0], dp[2][2][1]) + (-2)
            = max(2, -7) + (-2) = 0

→ 新开第2段:dp210=2(前2个选1段最优:只选第1个)

→ 2 + (-2) = 0

这个状态的含义

  • 前3个数,选2段,第3个选

  • 方案:选第1个(段1)→ 跳过第2个 → 选第3个(段2)

  • 贡献:2 + 0 + (-2) = 0

五、最终答案

这就是我上面说到的不一定要找够m段,比如贡献序列如果全是负数的话就一段也不需要。

cpp 复制代码
int mx = 0;
for (int j = 1; j <= m; j++) {
    mx = max({mx, dp[n][j][0], dp[n][j][1]});
}
cout << sum + mx << endl;

遍历所有可能的段数 j,取最大值。

六、DP 设计的核心思想

步骤 思考
1 确定决策:每个数选/不选
2 发现需要记录:位置、段数、上个数状态
3 设计状态:dp[i][j][0/1]
4 推导转移:不选→继承;选→新开或延续
5 确定顺序:i 从小到大,j 从小到大
6 初始化:dp0**=0
7 最终答案:遍历所有可能状态取最大

代码:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int mod=1e9+7;
const int N=2e5+10;
int n,m;
int a[1010];
int dp[1010][1010][2];
int fz(int x) {
    int res=0;
    while(x) {
        res=(res<<1)+(x&1);
        x>>=1;
    }
    return res;
}
void solve() {
    cin >> n >> m;
    int sum=0;
    for(int i=1;i<=n;i++) cin >> a[i];
    for(int i=1;i<=n;i++) {
        sum+=a[i];
        a[i]=fz(a[i])-a[i];
    }
    for(int i=1;i<=n;i++) {
        for(int j=1;j<=m;j++) {
            dp[i][j][0]=max(dp[i-1][j][0],dp[i-1][j][1]);
            dp[i][j][1]=max(dp[i-1][j-1][0],dp[i-1][j][1])+a[i];
        }
    }
    int mx = 0;
    for (int j = 1; j <= m; j++) {
        mx = max({mx, dp[n][j][0], dp[n][j][1]});
    }
    cout << sum + mx << endl;
}
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);
    int t=1;
    // cin>>t;
    while(t--)
        solve();
}

最小字符串

题目:

给你俩字符串ab,a是顺序不可变等着被插入的,b是顺序可变的,现在让你把b里的全部字符都插到a里面,问你怎么插字典序最小,输出这个字典序最小的字符串。

思路:

排序+双指针

对b进行排序,然后拿两个指针分别指向a和b的头,然后进行比较,若a<=b则输出a指针对应的字符,否则输出b指针对应的字符

代码:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
void solve() {
    int n,m;
    cin >> n >> m;
    string s1,s2;
    cin >> s1 >> s2;
    sort(s2.begin(),s2.end());
    for(int i=0,j=0;i<s1.size()||j<s2.size();) {
        if (i>=s1.size()||j>=s2.size()) {
            if (i>=s1.size()&&j>=s2.size()) {
                break;
            }
            if (i>=s1.size()) {
                cout << s2[j];
                j++;
            }else {
                cout << s1[i];
                i++;
            }
        }
        else {
            if (s1[i]<=s2[j]) {
                cout << s1[i] ;
                i++;
            }else {
                cout << s2[j] ;
                j++;
            }
        }
    }
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);
    int t = 1;
    while(t--)
        solve();
    return 0;
}

立定跳远

题目:

给定n个非递减的点,小明从0开始往这些点上跳,小明自己也可以在随意增加放置m个点,现在让你确定小明每一跳的最远距离L的最小值,保证此时小明能完整的跳过这些点,注意小明有个大招,释放大招后他的最长跳跃距离变成2L,但是大招只能使用一次

思路:

思路还是很明显的:二分答案,关键就是check函数怎么写

可以看我之前写的一篇介绍二分的文章,这个题目的二分类型是最大值最小

最大值最小:答案越大越符合条件,题目要求输出符合条件的最小值,看下图标蓝的就是最大值中的最小值。

最大值最小:我们要使得当前二分得到的mid是最大值,所以就要根据题目条件来让mid成为最大值,看看根据已知条件能否使得它成为最大值,如果能就return true,可以粗略想为值越大越符合条件,我们要找的是符合条件当中的最小值;

cpp 复制代码
bool check(int x) {
    int cnt=0;
    if (a[0]>x)cnt+=(a[0]+x-1)/x-1;
    for (int i=1;i<n;i++) {
        if (a[i]-a[i-1]>x)cnt+=(a[i]-a[i-1]+x-1)/x-1;
    }
    return cnt>m?0:1;
}

计算当前二分到的x时所需要增加的检查点个数,大于m就不符合要求返回0,否则返回1

每两个点之间需要的增加的检查点个数是**⌈dis​/L⌉−1**

上取整:a/b上取整等于(a+b-1)/b

注意:使用技能就相当于多一次随意增加跳跃点的机会

可以粗略的理解一下:只有两点间隔<=2L的情况下使用技能才是有效的,而此时增加一个检查点也可以达到同样效果;对于>=2L的情况下,增加一个检查点和使用大招都跳不过去,效果也是一样的。故而使用技能就相当于多一次随意增加跳跃点的机会

代码:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int mod=1e9+7;
const int N=2e5+10;
int a[N];
int n,m;
bool check(int x) {
    int cnt=0;
    if (a[0]>x)cnt+=(a[0]+x-1)/x-1;
    for (int i=1;i<n;i++) {
        if (a[i]-a[i-1]>x)cnt+=(a[i]-a[i-1]+x-1)/x-1;
    }
    return cnt>m?0:1;
}
void solve() {
    cin >> n >> m;
    m++;
    for(int i=0;i<n;i++) {
        cin >> a[i];
    }
    int l=0,r=a[n-1]-a[0];
    while (l+1!=r) {
        int mid=(l+r)>>1;
        if (check(mid))r=mid;
        else l=mid;
    }
    cout << r << endl;
}
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);
    int t=1;
    // cin>>t;
    while(t--)
        solve();
}
相关推荐
梓䈑1 小时前
C++ AI模型统一接入引擎(第一篇):项目介绍与环境搭建
c++·人工智能·chatgpt
我不是懒洋洋1 小时前
【C++】内存管理与模板(C++内存管理方式、new和delete的实现原理、malloc/free和new/delete的区别、函数模板、类模板)
c语言·开发语言·c++·青少年编程·visual studio
zlinear数据采集卡1 小时前
模拟输入限流保护电路深度解析:从理论原理到ZLinear采集卡的实战设计
c语言·单片机·嵌入式硬件·fpga开发·自动化
rsuhbsrjms1 小时前
可视采耳仪器多少钱一台?可视耳勺哪个牌子好?口碑好的可视耳勺
网络·人工智能·算法
j7~1 小时前
MySQL C语言连接库和MYSQL连接池原理与简易数据网站数据流动是如何进行的
c语言·数据库·mysql·连接池·mysqlc语言连接库
finhaz1 小时前
神经网络等机器学习模型的看法
算法
z200509301 小时前
【linux学习】深入理解 Linux 下的静态库与动态库
开发语言·c++·算法
妄想出头的工业炼药师1 小时前
腿式里程计
人工智能·算法·开源
SoftLipaRZC1 小时前
C语言自定义类型:联合和枚举完全指南
c语言·算法