2024蓝桥杯省B好题分析
题解来自洛谷,作为学习
目录
宝石组合
数字接龙
爬山
拔河
宝石组合
# [蓝桥杯 2024 省 B] 宝石组合
## 题目描述
在一个神秘的森林里,住着一个小精灵名叫小蓝。有一天,他偶然发现了一个隐藏在树洞里的宝藏,里面装满了闪烁着美丽光芒的宝石。这些宝石都有着不同的颜色和形状,但最引人注目的是它们各自独特的 “闪亮度” 属性。每颗宝石都有一个与生俱来的特殊能力,可以发出不同强度的闪光。小蓝共找到了 $n$ 枚宝石,第 $i$ 枚宝石的 “闪亮度” 属性值为 $H_i$,小蓝将会从这 $n$ 枚宝石中选出三枚进行组合,组合之后的精美程度 $S$ 可以用以下公式来衡量:
$$
S = H_a H_b H_c \cdot \frac{\operatorname{LCM}(H_a, H_b, H_c)}{\operatorname{LCM}(H_a, H_b) \cdot\operatorname{LCM}(H_a, H_c) \operatorname{LCM}(H_b, H_c)}
$$
其中 $\operatorname{LCM}$ 表示的是最小公倍数函数。
小蓝想要使得三枚宝石组合后的精美程度 $S$ 尽可能的高,请你帮他找出精美程度最高的方案。如果存在多个方案 $S$ 值相同,优先选择按照 $H$ 值升序排列后字典序最小的方案。
## 输入格式
第一行一个整数 $n$ 表示宝石个数。
第二行有 $n$ 个整数 $H_1, H_2, \dots H_n$ 表示每个宝石的闪亮度。
## 输出格式
输出一行包含三个整数表示满足条件的三枚宝石的 “闪亮度”。
## 样例 #1
### 样例输入 #1
```
5
1 2 3 4 9
```
### 样例输出 #1
```
1 2 3
```
## 提示
### 数据规模与约定
- 对 $30\%$ 的数据,$n \leq 100$,$H_i \leq 10^3$。
- 对 $60\%$ 的数据,$n \leq 2 \times 10^3$。
- 对全部的测试数据,保证 $3 \leq n \leq 10^5$,$1 \leq H_i \leq 10^5$。
做题思路
1.找一个 cnt[i]cnt[i] 用于记录从 H1,H2H1,H2 一直到 HnHn 中能被 ii 整除的个数。
2.找到最大的 ii 使得 cnt[i]≥3cnt[i]≥3 记为 xx
3.从小到大排序 HH 数组。
4.从 11 到 nn 遍历,判断 Hi∣xHi∣x ,输出前 3 个满足要求的数。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int h[N],cnt[N];
int main()
{
int n,x,c=0;
cin>>n;
for(int i=1;i<=n;i++){
cin>>h[i];
for(int j=1;j*j<=h[i];j++) {
if(h[i]%j==0){
cnt[j]++;
if(j*j!=h[i]) cnt[h[i]/j]++;
}
}
}
for(int i=N-5;i>=1;i--){
if(cnt[i]>=3){
x=i;
break;
}
}
sort(h+1,h+n+1);
for(int i=1;i<=n;i++){
if(h[i]%x==0){
cout<<h[i]<<" ";
c++;
}
if(c==3) return 0;
}
return 0;
}
代码思路:
- 输入处理:
- 先读取输入,存储每个宝石的“闪亮度”到数组
h
中。
- 先读取输入,存储每个宝石的“闪亮度”到数组
- 统计每个公约数的出现次数:
- 代码核心在于使用
cnt[]
数组来统计每个数作为公约数出现的次数。 - 对每个宝石
h[i]
,你通过枚举它的每个约数j
来统计该约数出现的次数。对于约数j
和对应的商h[i]/j
,都进行计数,这样可以有效统计每个数在所有宝石中的公约数出现的次数。
- 代码核心在于使用
- 找到最多次作为公约数出现的数:
- 在统计完成后,代码从大到小查找第一个至少出现 3 次的公约数(即
cnt[i] >= 3
),并将这个数记录为x
。
- 在统计完成后,代码从大到小查找第一个至少出现 3 次的公约数(即
- 输出满足条件的 3 个宝石:
- 最后,你对宝石数组
h
进行排序,并输出前 3 个能被x
整除的宝石。
- 最后,你对宝石数组
优点:
-
高效的公约数统计:
- 你使用了枚举约数的技巧,对于每个
h[i]
,你仅需要枚举到sqrt(h[i])
,所以这部分的时间复杂度是较为高效的。总体上,该部分的时间复杂度为 O(n×h[i])O(n \times \sqrt{h[i]})O(n×h[i]),对于h[i]
较大的情况可以有效应对。
- 你使用了枚举约数的技巧,对于每个
-
贪心的寻找公约数:
- 你从大到小查找第一个至少出现 3 次的公约数,保证了找到的公约数是最大的,从而能够保证美丽度的最大化。
-
简洁的实现:
- 代码简洁且逻辑清晰,从输入处理、到统计公约数再到排序输出,结构明了,易于理解。
数字接龙
#include <bits/stdc++.h>
#define pp ios_base::sync_with_stdio(0),cin.tie(0),cout.tie(0)
#define ll long long
using namespace std;
int n,k;
const int N = 30;
int f[N][N];
string ans;
bool xie[N][N][N][N],ji[N][N];
int dx[8]={-1,-1,0,1,1,1,0,-1},dy[8]={0,1,1,1,0,-1,-1,-1}; //特别注意要按照题目给的要求来,方便后面直接把i当作八个方向的编号
void dfs(int a,int b,int c,int zong){ //坐标x,y,数,总步数
if (a==n-1 && b==n-1 && zong==pow(n,2)-1){ //当走到(n-1,n-1),步数为总格子数减一时(左上角一开始的(0,0)不用走)
if (ans.empty()) cout << -1;
for (int i=0;i<zong;i++) cout << ans[i];
exit(0); //输出完答案直接关闭程序
}
for (int i=0;i<8;i++){
int x=dx[i]+a,y=dy[i]+b;
if(f[x][y]==(c+1)%k){
if (x>=0 && y>=0 && x<n && y<n && !ji[x][y]){
int lu=0; //用来记录用不用走斜线
if (i%2==1){ //如果走的是斜线需要判断
if(!xie[a][y][x][b]){ //另一条斜线没走过就能走
lu=1;
xie[a][b][x][y]=true;
xie[x][y][a][b]=true;
}
else continue; //与其它交叉就跳过这条路
}
ans.push_back(i+'0');
ji[x][y] = true;
dfs(x, y,(c+1)%k,zong+1);
ji[x][y] = false; //回溯
ans.pop_back();
if(lu & 1){ //用到斜线的话需要单独回溯
xie[a][b][x][y]=false;
xie[x][y][a][b]=false;
}
}
}
}
}
int main()
{
pp; //用到dfs怕超时,先关一手
cin >> n >> k;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n;j++) cin >> f[i][j];
}
ji[0][0] = 1; //不要忘记左上角是默认走过的
dfs(0,0,0,0); //开始dfs
cout << -1; //还有种情况是当走不到(n-1,n-1)时,输出-1
return 0;
}
思路分析:
-
基础概念:
- 使用深度优先搜索(DFS)来遍历所有可能的路径。
- 迷宫是一个
N x N
的棋盘,每个格子里有一个数字。 - 每一步可以按照八个方向(水平、垂直和对角线)移动到相邻的格子。
- 在移动时,当前格子中的数字要满足从0到K-1的循环条件,即走到下一个格子的数字必须是
(当前数字 + 1) % K
。
-
DFS的递归逻辑:
- 从左上角
(0, 0)
开始,每次移动到符合规则的下一个格子,并且检查这条路径是否可以到达右下角(n-1, n-1)
。 - 记录已经走过的格子,避免重复访问。
- 对于斜线移动,题目要求两条斜线不能交叉。因此,代码中对斜线的情况进行了特别的处理,用
xie
数组记录是否有交叉的斜线。
- 从左上角
-
边界条件处理:
- 当遍历到右下角
(n-1, n-1)
并且走过所有格子(步数为n*n - 1
)时,输出路径。 - 如果尝试了所有可能的路径仍无法找到合法解,则输出
-1
。
- 当遍历到右下角
代码详细分析:
-
坐标移动方向定义:
dx
和dy
数组定义了八个方向,顺序与题目中的顺序保持一致,便于之后直接使用这些方向来移动。
-
DFS核心部分:
- 在
dfs
函数中,首先判断是否达到了(n-1, n-1)
并且是否走满了所有格子(总步数为n*n - 1
)。 - 如果条件满足,输出结果并退出程序。
- 然后,对八个方向进行尝试移动,确保移动符合条件:格子没有访问过,数字满足条件,且斜线不能与已有斜线交叉。
- 如果移动成功,继续递归进行深度搜索。搜索完成后进行回溯,恢复状态,以便探索其他可能的路径。
- 在
-
边界检查:
- 移动过程中对坐标的合法性进行检查,确保不会超出棋盘的边界。
-
优化措施:
- 通过
pp
宏定义加快输入输出的速度,避免DFS过程中超时。 - 一旦找到解,直接退出程序,避免继续多余的计算。
- 通过
优点:
- 思路清晰:代码利用DFS遍历所有可能的路径,并通过条件判断确保每条路径的合法性,思路比较清晰。
- 边界处理完备:考虑了各种边界条件,包括边界越界、斜线交叉、数字循环等问题。
- 剪枝回溯:利用回溯法在DFS的基础上进行了路径选择和状态恢复,避免了重复计算和死循环。
爬山
贪心思路,分别求出 第一种魔法的结果sqrt_res,第二种魔法的结果div_res,比较谁更小,再决定使用第几种魔法!!!
请特别注意 p == 0 && q != 0、q == 0 && p != 0这两种情况,不需要比较,直接使用有次数的魔法即可。
次数都为0时,跳出while循环
痛哭,比赛的时候紧张,忘记优先队列头文件了,想着for循环遍历一下找最大数很快,没有使用大根堆。
#include<iostream>
#include <queue>
#include <math.h>
using namespace std;
int n,p,q;
priority_queue<int> mount;
int main()
{
scanf("%d %d %d",&n,&p,&q);
int t;
for(int i = 0;i < n;i++)
{
scanf("%d",&t);
mount.emplace(t);
}
while(p || q)
{
int tmp = mount.top();
mount.pop();
int sqrt_res = sqrt(tmp),div_res = tmp / 2;
if(p)//如果p有次数
{
if(q)//如果q也有次数,需要进行比较
{
if(sqrt_res <= div_res)
{
mount.emplace(sqrt_res);
p--;
}
}
else//q没有次数,直接emplace,不需要比较
{
mount.emplace(sqrt_res);
p--;
}
}
else//p没次数
{
if(q)
{
mount.emplace(div_res);
q--;
}
else//都没有次数,跳出循环
break;
}
}
long long res = 0;
while (!mount.empty())
{
res += mount.top();
mount.pop();
}
cout << res << endl;
return 0;
}
代码分析:
- 数据输入与初始化:
- 首先读取输入的三个参数
n
、p
、q
,然后读取每座山的高度,并将它们依次存入最大堆mount
中。这个堆会在每次操作时帮助我们迅速找到当前高度最大的山峰。
- 首先读取输入的三个参数
- 主循环:选择和处理最大山峰:
-
你通过
while (p || q)
循环不断处理山峰,直到p
(第一种魔法的次数)或q
(第二种魔法的次数)用完为止。 -
贪心选择:每次都选出当前最高的山峰,并将其从堆中弹出。然后根据当前的
p
和q
值,来决定使用哪种魔法。 -
比较
sqrt_res
和div_res
:- 如果
p
和q
都有剩余,比较使用两种魔法后的高度,选择效果更好的那个:- 第一种魔法将山峰高度开平方,结果为
sqrt_res
。 - 第二种魔法将山峰高度除以2,结果为
div_res
。
- 第一种魔法将山峰高度开平方,结果为
- 如果
sqrt_res <= div_res
,则使用第一种魔法,否则使用第二种。
- 如果
-
特殊情况:
- 如果
p == 0 && q != 0
,则只使用第二种魔法。 - 如果
q == 0 && p != 0
,则只使用第一种魔法。
- 如果
-
- 结果计算与输出:
- 当
p
和q
都用完后,程序将剩下的山峰高度全部相加,作为最后的输出结果。
- 当
注意点:
- 优先队列的使用:
- 使用最大堆是一个非常好的贪心选择。因为每次都需要选择当前高度最高的山峰来操作,这个堆的性质可以确保我们在每次循环中都能快速找到最大值。
- 特殊情况处理:
- 代码中处理了
p == 0 && q != 0
或q == 0 && p != 0
的特殊情况,保证了在一方次数用尽后,不再进行不必要的比较,节省了时间。
- 代码中处理了
拔河
前缀和思想,然后顺便记录每个队伍的区间,以及每个队伍的值,排序后求相邻区间的差值,如果区间没交集则有效,最后输出最小的
#include <bits/stdc++.h>
#define ll long long
#define PLL pair<ll,ll>
using namespace std;
int n;
const int N = 1e3+10;
ll d[N];
ll mi=LLONG_MAX;
struct h{
ll a;
int b;
int c;
};
bool cmp(h x,h y){
if(x.a==y.a) return x.b<y.b;
if(x.a==y.a && x.b==y.b) return x.c<y.c;
return x.a<y.a;
}
int main(){
vector<h> s;
scanf("%d",&n);
for(int i=1;i<=n;i++){
ll x;
scanf("%lld",&x);
d[i]=d[i-1]+x;
}
for(int i=0;i<n;i++){
for(int k=i+1;k<=n;k++){
s.push_back({d[k]-d[i],i,k});
}
}
sort(s.begin(),s.end(),cmp);
for(int i=1;i<s.size();i++){
if((s[i].b>=s[i-1].c && s[i].c>=s[i-1].c) || (s[i-1].b>=s[i].c && s[i-1].c>=s[i].c)) mi=min(s[i].a-s[i-1].a,mi);
else{
for(int j=i+1;j<s.size();j++){
if((s[i].b>=s[i-1].c && s[i].c>=s[i-1].c) || (s[i-1].b>=s[i].c && s[i-1].c>=s[i].c)){
mi=min(s[j].a-s[i].a,mi);
break;
}
if((s[j].a-s[i].a)>=mi) break;
}
}
}
printf("%lld",mi);
return 0;
}
代码分析:
-
前缀和的使用:
- 你通过计算前缀和数组
d[]
来快速获得任何区间的和。具体来说,对于区间[i, k]
的和,可以用d[k] - d[i]
来得到。这是经典的前缀和技巧,大大减少了重复计算区间和的复杂度。
- 你通过计算前缀和数组
-
区间值的存储:
- 使用结构体
h
来存储每个区间的信息,包括该区间的和a
,区间的起点b
,和终点c
。你将所有的可能区间都存入vector
容器s
中。
- 使用结构体
-
排序规则:
- 区间先按区间和
a
进行排序,如果和相同,则按区间起点b
排序,起点相同再按终点c
排序。这确保了在相同区间和的情况下,会按照从左到右的区间顺序来排序。
- 区间先按区间和
-
寻找最小差值:
- 经过排序后,你需要比较相邻的区间和是否有交集:
- 只有当两个区间的终点和起点没有重叠时,才是有效的两个队伍(两个不相交的区间)。
- 当发现相邻的两个区间不相交时,计算它们的区间和之差并更新最小差值
mi
。
- 经过排序后,你需要比较相邻的区间和是否有交集:
-
边界处理和优化:
- 代码中包含了一层优化,即在比较两个区间时,如果当前的差值已经大于等于最小差值
mi
,则可以提前终止循环,避免不必要的比较。
- 代码中包含了一层优化,即在比较两个区间时,如果当前的差值已经大于等于最小差值
优点:
-
使用前缀和优化区间和的计算:前缀和的使用有效地减少了多次重复的区间和计算,保证了时间复杂度的优化。
-
巧妙的排序规则:通过对区间和的排序,并按起点、终点进行二次排序,简化了之后寻找最小差值的过程。
-
贪心思想的应用:你通过贪心思想,即优先处理差值较小的区间和,快速找到两个不相交区间的最小差值。