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;
}

代码思路:

  1. 输入处理
    • 先读取输入,存储每个宝石的"闪亮度"到数组 h 中。
  2. 统计每个公约数的出现次数
    • 代码核心在于使用 cnt[] 数组来统计每个数作为公约数出现的次数。
    • 对每个宝石 h[i],你通过枚举它的每个约数 j 来统计该约数出现的次数。对于约数 j 和对应的商 h[i]/j,都进行计数,这样可以有效统计每个数在所有宝石中的公约数出现的次数。
  3. 找到最多次作为公约数出现的数
    • 在统计完成后,代码从大到小查找第一个至少出现 3 次的公约数(即 cnt[i] >= 3),并将这个数记录为 x
  4. 输出满足条件的 3 个宝石
    • 最后,你对宝石数组 h 进行排序,并输出前 3 个能被 x 整除的宝石。

优点:

  1. 高效的公约数统计

    • 你使用了枚举约数的技巧,对于每个 h[i],你仅需要枚举到 sqrt(h[i]),所以这部分的时间复杂度是较为高效的。总体上,该部分的时间复杂度为 O(n×h[i])O(n \times \sqrt{h[i]})O(n×h[i]),对于 h[i] 较大的情况可以有效应对。
  2. 贪心的寻找公约数

    • 你从大到小查找第一个至少出现 3 次的公约数,保证了找到的公约数是最大的,从而能够保证美丽度的最大化。
  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;
}

思路分析:

  1. 基础概念

    • 使用深度优先搜索(DFS)来遍历所有可能的路径。
    • 迷宫是一个 N x N 的棋盘,每个格子里有一个数字。
    • 每一步可以按照八个方向(水平、垂直和对角线)移动到相邻的格子。
    • 在移动时,当前格子中的数字要满足从0到K-1的循环条件,即走到下一个格子的数字必须是 (当前数字 + 1) % K
  2. DFS的递归逻辑

    • 从左上角 (0, 0) 开始,每次移动到符合规则的下一个格子,并且检查这条路径是否可以到达右下角 (n-1, n-1)
    • 记录已经走过的格子,避免重复访问。
    • 对于斜线移动,题目要求两条斜线不能交叉。因此,代码中对斜线的情况进行了特别的处理,用 xie 数组记录是否有交叉的斜线。
  3. 边界条件处理

    • 当遍历到右下角 (n-1, n-1) 并且走过所有格子(步数为 n*n - 1)时,输出路径。
    • 如果尝试了所有可能的路径仍无法找到合法解,则输出 -1

代码详细分析:

  1. 坐标移动方向定义

    • dxdy 数组定义了八个方向,顺序与题目中的顺序保持一致,便于之后直接使用这些方向来移动。
  2. DFS核心部分

    • dfs 函数中,首先判断是否达到了 (n-1, n-1) 并且是否走满了所有格子(总步数为 n*n - 1)。
    • 如果条件满足,输出结果并退出程序。
    • 然后,对八个方向进行尝试移动,确保移动符合条件:格子没有访问过,数字满足条件,且斜线不能与已有斜线交叉。
    • 如果移动成功,继续递归进行深度搜索。搜索完成后进行回溯,恢复状态,以便探索其他可能的路径。
  3. 边界检查

    • 移动过程中对坐标的合法性进行检查,确保不会超出棋盘的边界。
  4. 优化措施

    • 通过 pp 宏定义加快输入输出的速度,避免DFS过程中超时。
    • 一旦找到解,直接退出程序,避免继续多余的计算。

优点:

  1. 思路清晰:代码利用DFS遍历所有可能的路径,并通过条件判断确保每条路径的合法性,思路比较清晰。
  2. 边界处理完备:考虑了各种边界条件,包括边界越界、斜线交叉、数字循环等问题。
  3. 剪枝回溯:利用回溯法在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;
}

代码分析:

  1. 数据输入与初始化
    • 首先读取输入的三个参数 npq,然后读取每座山的高度,并将它们依次存入最大堆 mount 中。这个堆会在每次操作时帮助我们迅速找到当前高度最大的山峰。
  2. 主循环:选择和处理最大山峰
    • 你通过 while (p || q) 循环不断处理山峰,直到 p(第一种魔法的次数)或 q(第二种魔法的次数)用完为止。

    • 贪心选择 :每次都选出当前最高的山峰,并将其从堆中弹出。然后根据当前的 pq 值,来决定使用哪种魔法。

    • 比较 sqrt_resdiv_res

      • 如果 pq 都有剩余,比较使用两种魔法后的高度,选择效果更好的那个:
        • 第一种魔法将山峰高度开平方,结果为 sqrt_res
        • 第二种魔法将山峰高度除以2,结果为 div_res
      • 如果 sqrt_res <= div_res,则使用第一种魔法,否则使用第二种。
    • 特殊情况

      • 如果 p == 0 && q != 0,则只使用第二种魔法。
      • 如果 q == 0 && p != 0,则只使用第一种魔法。
  3. 结果计算与输出
    • pq 都用完后,程序将剩下的山峰高度全部相加,作为最后的输出结果。

注意点:

  1. 优先队列的使用
    • 使用最大堆是一个非常好的贪心选择。因为每次都需要选择当前高度最高的山峰来操作,这个堆的性质可以确保我们在每次循环中都能快速找到最大值。
  2. 特殊情况处理
    • 代码中处理了 p == 0 && q != 0q == 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;
}

代码分析:

  1. 前缀和的使用

    • 你通过计算前缀和数组 d[] 来快速获得任何区间的和。具体来说,对于区间 [i, k] 的和,可以用 d[k] - d[i] 来得到。这是经典的前缀和技巧,大大减少了重复计算区间和的复杂度。
  2. 区间值的存储

    • 使用结构体 h 来存储每个区间的信息,包括该区间的和 a,区间的起点 b,和终点 c。你将所有的可能区间都存入 vector 容器 s 中。
  3. 排序规则

    • 区间先按区间和 a 进行排序,如果和相同,则按区间起点 b 排序,起点相同再按终点 c 排序。这确保了在相同区间和的情况下,会按照从左到右的区间顺序来排序。
  4. 寻找最小差值

    • 经过排序后,你需要比较相邻的区间和是否有交集:
      • 只有当两个区间的终点和起点没有重叠时,才是有效的两个队伍(两个不相交的区间)。
      • 当发现相邻的两个区间不相交时,计算它们的区间和之差并更新最小差值 mi
  5. 边界处理和优化

    • 代码中包含了一层优化,即在比较两个区间时,如果当前的差值已经大于等于最小差值 mi,则可以提前终止循环,避免不必要的比较。

优点:

  1. 使用前缀和优化区间和的计算:前缀和的使用有效地减少了多次重复的区间和计算,保证了时间复杂度的优化。

  2. 巧妙的排序规则:通过对区间和的排序,并按起点、终点进行二次排序,简化了之后寻找最小差值的过程。

  3. 贪心思想的应用:你通过贪心思想,即优先处理差值较小的区间和,快速找到两个不相交区间的最小差值。

相关推荐
马剑威(威哥爱编程)11 分钟前
除了递归算法,要如何优化实现文件搜索功能
java·开发语言·算法·递归算法·威哥爱编程·memoization
算法萌新——132 分钟前
洛谷P2240——贪心算法
算法·贪心算法
湖北二师的咸鱼33 分钟前
专题:二叉树递归遍历
算法·深度优先
宇宙李1 小时前
2024java面试-软实力篇
面试·职场和发展
重生之我要进大厂1 小时前
LeetCode 876
java·开发语言·数据结构·算法·leetcode
测试界柠檬1 小时前
接口测试到底测试什么?
自动化测试·软件测试·功能测试·程序人生·职场和发展·单元测试·压力测试
KBDYD10102 小时前
C语言--结构体变量和数组的定义、初始化、赋值
c语言·开发语言·数据结构·算法
Crossoads2 小时前
【数据结构】排序算法---桶排序
c语言·开发语言·数据结构·算法·排序算法
孙小二写代码2 小时前
[leetcode刷题]面试经典150题之1合并两个有序数组(简单)
算法·leetcode·面试