一、前言
最近刚参加完今年的蓝桥杯省赛,发现今年的题目在算法思维和代码实现上都有不少值得深挖的点。今天简单回温一下一卷的题目,并试着写一些我的解题思路。因为我在河北,比赛用的是二卷,所以事先声明,这里一卷的解法代码不一定是正解,只是给大家提供一些能够想到的思路吧。
二、题目
试题A:逃离高塔
首先在填空题中,只要题目中有大数,那这道题很大概率是考规律。看这道题,因为立方数的个位数只与原数的个位数有关,而只有7的立方个数为3。所以我们只需要去找1-2025中有几个数个位是7即可(202个)。
public class Main {
public static void main(String[] args) {
int count = 0;
// 遍历 1 到 2025 之间的每一个数
for (int i = 1; i <= 2025; i++) {
// 判断个位是否为 7
if (i % 10 == 7) {
count++;
}
}
System.out.println(count);
}
}
试题B:消失的蓝宝
本题是一个典型的考察“中国剩余定理”的题目,当然也有别的解法,为了不与其它作者产生雷同,这里就讲一下我用定理写的思路吧。大家不懂这个定理可自行查阅一下,我在这里就不做详解了。
根据条件 “N+20250412能被20240413整除”,可转化为N+20250412≡0(mod20240413),进一步得到N≡−20250412(mod20240413) ,因此可以设N=k⋅20240413−20250412N,然后寻找最小的 k 使得 N+20240413 能被 20250412 整除。代码如下:
public class Main {
public static void main(String[] args) {
// 定义两个模数 m1 和 m2,用于后续的同余计算
long m1 = 20240413L;
long m2 = 20250412L;
// 定义两个余数 a1 和 a2,用于表示同余方程中的余数部分
long a1 = 20250412L;
long a2 = 20240413L;
// 根据同余方程 N ≡ -a1 (mod m1),可以推导出 N = k * m1 - a1,这里初始化 k 为 1
long k = 1;
// 使用无限循环来尝试不同的 k 值,直到找到满足条件的 N
while (true) {
// 根据公式 N = k * m1 - a1 计算当前 k 值对应的 N
long N = k * m1 - a1;
// 检查当前的 N 是否满足另一个同余条件 (N + a2) % m2 == 0,并且 N 要大于 0
if ((N + a2) % m2 == 0 && N > 0) {
// 如果满足条件,输出找到的 N
System.out.println(N);
// 找到满足条件的 N 后,跳出循环,结束程序
break;
}
// 如果当前 k 值对应的 N 不满足条件,将 k 加 1,继续尝试下一个 k 值
k++;
}
}
}
试题C:电池分组
这道题题意上可能会有迷惑,但其实是很简单的一道题。
关键观察:如果所有数的异或和为0,那么可以任意分成两组,异或和必然相等(因为A⊕B=0 ⇒ A=B)。
如果异或和不为0,则不可能分成两组使异或和相等(因为A⊕B=X≠0 ⇒ A≠B)。
因此,只需计算所有数的异或和,如果为0输出"YES",否则"NO"。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int T = sc.nextInt();
while (T-- > 0) {
int N = sc.nextInt();
int xor = 0;
for (int i = 0; i < N; i++) {
xor ^= sc.nextInt();
}
System.out.println(xor == 0 ? "YES" : "NO");
}
}
}
试题D:魔法科考试
这道题的考点就在于如何找出素数,然后再通过条件得到满足条件的素数个数。这里要注意,不能使用普通的找素数的方法,这样时间复杂度太高将导致超时,所以这里我用的是线性筛(欧拉筛),这样判断素数的复杂度会降到O(1)。
其次,这里会重复计算条件成立的同一个数,所以应该用Set来过滤重复的数。代码如下:
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
public class Main {
// 用于存储筛选出的素数
static int[] prime = new int[20005];
// 标记数组,用于标记某个数是否为合数,true 表示是合数,false 表示可能是素数
static boolean st[] = new boolean[20005];
// 记录筛选出的素数的个数
static int cnt = 0;
// 用于存储筛选出的素数的集合,方便后续查找判断
static Set<Integer> set = new HashSet<>();
// 记录可能出现的最大和值,即数组 a 和数组 b 的长度之和
static int num;
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int m = scan.nextInt();
int[] a = new int[n];
int[] b = new int[m];
for (int i = 0; i < n; i++) {
a[i] = scan.nextInt();
}
for (int i = 0; i < m; i++) {
b[i] = scan.nextInt();
}
// 计算可能出现的最大和值,即数组 a 和数组 b 的长度之和
num = n + m;
// 调用 seive 方法进行素数筛选
seive();
// 用于记录满足条件的组合数量,即 a 数组中的元素与 b 数组中的元素相加为素数的组合数量
int ans = 0;
// 双重循环遍历数组 a 和数组 b
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
// 计算数组 a 中第 i 个元素和数组 b 中第 j 个元素的和
int s = a[i] + b[j];
// 判断和是否小于等于可能出现的最大和值
if (s <= num) {
// 判断该和是否为素数,即是否在素数集合 set 中
if (set.contains(s)) {
// 若为素数,满足条件的组合数量加 1
ans++;
// 移除该素数,避免重复计算
set.remove(s);
}
}
}
}
// 输出满足条件的组合数量
System.out.println(ans);
}
// 该方法用于筛选出不超过 num 的所有素数
public static void seive() {
// 因为只需要筛选出 num 以内的素数,所以从 2 开始遍历到 num
for (int i = 2; i <= num; i++) {
// 如果该数未被标记为合数
if (!st[i]) {
// 将该素数存入 prime 数组
prime[cnt++] = i;
// 将该素数添加到素数集合 set 中
set.add(i);
}
// 遍历已经筛选出的素数
for (int j = 0; j < cnt; j++) {
// 如果当前数 i 乘以素数 prime[j] 超过了 num,跳出内层循环
if (i * prime[j] > num) break;
// 将 i * prime[j] 标记为合数
st[i * prime[j]] = true;
// 如果 i 能被 prime[j] 整除,跳出内层循环,避免重复标记
if (i % prime[j] == 0) break;
}
}
}
}
试题E:爆破
这题是一道图论题,考点在于用Kruskal或Prim算法求图的最小生成树,求出的总权值即为答案。下面我来详细讲一下思路。
首先要知道,两个圆之间的最短距离为:两个圆心之间的距离减去两个圆各自的半径,如果小于等于0就说明是相交的。所以我们把魔法阵看作图中的节点:
如果两圆相交,则节点间边的权值为0(无需连接);
如果两圆不相交,则节点间边的权值为两圆之间的最短距离。
所以详细步骤为:
1.使用二维数组将圆的圆心和半径存储起来。
2.通过双重循环遍历所有圆对,计算它们之间的最短距离,如果大于0,就将这条边存在一个列表中。
3.将表按距离从小到大进行排序。
4.使用并查集来处理连通性问题,使用Kruskal来构建最小生成树。遍历列表,如果起点和终点不在同一个连通分量中,则将它们合并,并累加边的距离。
只看文字很难想明白,下面来看代码:
import java.util.*;
public class Main {
// 定义一个内部类 Edge 表示边,实现 Comparable 接口以便对边按距离排序
static class Edge implements Comparable<Edge> {
// 边的起点
int u;
// 边的终点
int v;
// 边的距离
double dist;
// 构造函数,用于初始化边的起点、终点和距离
Edge(int u, int v, double dist) {
this.u = u;
this.v = v;
this.dist = dist;
}
// 实现 compareTo 方法,用于比较两条边的距离,以便对边进行排序
public int compareTo(Edge other) {
return Double.compare(this.dist, other.dist);
}
}
public static void main(String[] args) {
// 创建 Scanner 对象,用于从标准输入读取数据
Scanner sc = new Scanner(System.in);
// 读取圆的数量
int n = sc.nextInt();
// 二维数组 circles 用于存储每个圆的信息,每个圆用 [x, y, r] 表示,x 和 y 是圆心坐标,r 是半径
int[][] circles = new int[n][3];
// 循环读取每个圆的信息
for (int i = 0; i < n; i++) {
// 读取圆心的 x 坐标
circles[i][0] = sc.nextInt();
// 读取圆心的 y 坐标
circles[i][1] = sc.nextInt();
// 读取圆的半径
circles[i][2] = sc.nextInt();
}
// 用于存储所有边的列表
List<Edge> edges = new ArrayList<>();
// 双重循环遍历所有圆对,计算它们之间的边
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
// 计算两个圆心在 x 轴上的距离
double dx = circles[i][0] - circles[j][0];
// 计算两个圆心在 y 轴上的距离
double dy = circles[i][1] - circles[j][1];
// 计算两个圆心之间的距离
double centerDist = Math.sqrt(dx * dx + dy * dy);
// 计算两个圆之间的边的距离,即圆心距离减去两个圆的半径
double edgeDist = centerDist - circles[i][2] - circles[j][2];
// 如果边的距离大于 0,说明两个圆不相交,将这条边添加到边列表中
if (edgeDist > 0) {
edges.add(new Edge(i, j, edgeDist));
}
}
}
//todo 这里是Kruskal算法
// 对边列表按距离从小到大进行排序
Collections.sort(edges);
// 用于存储最小生成树的总距离
double total = 0;
// 创建并查集对象,用于处理连通性问题
UnionFind uf = new UnionFind(n);
// 遍历排序后的边列表
for (Edge e : edges) {
// 如果边的起点和终点不在同一个连通分量中
if (uf.find(e.u) != uf.find(e.v)) {
// 合并这两个连通分量
uf.union(e.u, e.v);
// 将这条边的距离累加到总距离中
total += e.dist;
// 如果并查集中只剩下一个连通分量,说明最小生成树已经构建完成,退出循环
if (uf.size == 1) break;
}
}
// 格式化输出最小生成树的总距离,保留两位小数
System.out.printf("%.2f\n", total);
}
// 定义并查集类,用于处理连通性问题
static class UnionFind {
int[] parent;
int size;
UnionFind(int n) {
parent = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
size = n;
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
void union(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx != fy) {
parent[fy] = fx;
size--;
}
}
}
}
这道题在代码量和算法知识方面都比较难,尤其是图论方面一直是我在努力攻克的题目,如果比赛中遇到这类相似的题目,我认为实力中等及偏下的人可以试着放掉,没有必要把大量时间放在难题上,以免干扰心态。
试题F:数组翻转
首先拿到这道题,我们发现可以直接暴力求解,枚举所有可能最后比较,但时间复杂度为O(n^3),无法全部通过。
优化策略:先计算出不进行翻转的最大分数。然后使用两层嵌套循环直接模拟翻转,枚举出所有可能的翻转区间[l,r]。重新计算翻转后的新数组的最大分数。
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int[] a = new int[n];
for (int i = 0; i < n; i++){ a[i] = scanner.nextInt();}
// 计算不进行翻转操作时数组的最大分数
int maxScore = calculateMaxScore(a);
// 枚举所有可能的翻转区间
// 外层循环确定翻转区间的左端点 l
for (int l = 0; l < n; l++) {
// 内层循环确定翻转区间的右端点 r,r 从 l 开始,确保区间合法
for (int r = l; r < n; r++) {
// 调用 flip 方法,对数组 a 的 [l, r] 区间进行翻转,得到新数组 flipped
int[] flipped = flip(a, l, r);
// 调用 calculateMaxScore 方法计算翻转后数组的最大分数
// 并将其与当前的最大分数 maxScore 比较,取较大值更新 maxScore
maxScore = Math.max(maxScore, calculateMaxScore(flipped));
}
}
// 输出最终得到的最大分数
System.out.println(maxScore);
}
/**
* 翻转数组的指定区间 [l, r]
* @param a 原始数组
* @param l 区间左端点
* @param r 区间右端点
* @return 翻转指定区间后的新数组
*/
private static int[] flip(int[] a, int l, int r) {
// 复制原始数组 a 到新数组 copy,避免修改原始数组
int[] copy = Arrays.copyOf(a, a.length);
// 使用双指针进行区间元素的交换,实现翻转操作
while (l < r) {
// 交换 copy[l] 和 copy[r] 的值
int temp = copy[l];
copy[l] = copy[r];
copy[r] = temp;
l++;
r--;
}
// 返回翻转后的新数组
return copy;
}
/**
* 计算当前数组的最大分数
* 分数的计算规则是:连续相同元素的长度乘以该元素的值,取所有可能情况中的最大值
* @param a 输入数组
* @return 数组的最大分数
*/
private static int calculateMaxScore(int[] a) {
// 初始化最大分数为 0
int max = 0;
// 初始化当前连续相同元素的长度为 1
int currentLen = 1;
// 从数组的第二个元素开始遍历
for (int i = 1; i < a.length; i++) {
// 如果当前元素和前一个元素相同
if (a[i] == a[i - 1]) {
// 当前连续相同元素的长度加 1
currentLen++;
} else {
// 若当前元素和前一个元素不同,计算当前连续相同元素组成的分数
// 并与当前最大分数比较,取较大值更新最大分数
max = Math.max(max, currentLen * a[i - 1]);
// 重置当前连续相同元素的长度为 1
currentLen = 1;
}
}
// 处理数组末尾连续相同元素的情况,再次更新最大分数
return Math.max(max, currentLen * a[a.length - 1]);
}
}
试题G:2的幂
这是我的解题思路,但我实现之后发现代码并不能输出正确答案,所以欢迎大佬来指点和讨论。
1. 读取输入:
首先从控制台读取第一行的两个正整数 `n` 和 `k`,分别表示数组的长度和目标幂次。
然后读取第二行的 `n` 个正整数,存储到数组 `nums` 中。
2. 计算每个数中 2 的幂次:
遍历数组 `nums`,对于每个数 `num`,通过不断除以 2 并统计次数的方式,计算出该数中包含 2 的幂次 `count`。
累加所有数中 2 的幂次,得到当前数组乘积中 2 的总幂次 `totalPower`。
3. 判断是否可能满足条件:
如果 `totalPower >= k`,说明不需要进行任何操作,直接输出 0。
4. 尝试增加数来满足条件:
当 `totalPower < k` 时,需要对数组中的数进行增加操作。
遍历数组 `nums`,对于每个数 `num`,计算将其增加到下一个 2 的幂次所需增加的数值 `addValue`,同时要保证增加后的数不超过 `100000`。
记录每次增加的数值,累加起来得到 `totalAddition`。
每次增加后,重新计算数组乘积中 2 的总幂次 `totalPower`,直到 `totalPower >= k`。
5. 输出结果:
如果最终能够使得 `totalPower >= k`,则输出 `totalAddition`。
如果无法满足条件(例如在增加过程中发现无论如何都无法达到 `k` 次幂),则输出 `-1`。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int k = scanner.nextInt();
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = scanner.nextInt();
}
// 统计数组中所有元素包含 2 的幂次的总数
int totalPower = 0;
for (int num : nums) {
totalPower += countPowerOfTwo(num);
}
// 如果当前 2 的幂次总数已经达到或超过目标 k,不需要增加任何数,输出 0 并结束程序
if (totalPower >= k) {
System.out.println(0);
return;
}
// 记录总共需要增加的数值
int totalAddition = 0;
// 当 2 的幂次总数小于目标 k 时,继续循环增加数值
while (totalPower < k) {
// 记录当前找到的最小增加量,初始化为整数最大值
int minAddition = Integer.MAX_VALUE;
// 记录最小增加量对应的数组元素索引,初始化为 -1
int index = -1;
// 遍历数组,寻找最小增加量及其对应的索引
for (int i = 0; i < n; i++) {
// 计算将当前元素变为下一个 2 的幂次需要增加的值
int addValue = getNextPowerOfTwo(nums[i]) - nums[i];
// 检查增加后的值是否不超过 100000
if (nums[i] + addValue <= 100000) {
// 如果当前增加量小于已记录的最小增加量
if (addValue < minAddition) {
// 更新最小增加量
minAddition = addValue;
// 更新对应的索引
index = i;
}
}
}
// 如果没有找到合适的增加量(说明无法满足条件),输出 -1 并结束程序
if (index == -1) {
System.out.println(-1);
return;
}
// 给对应的数组元素加上最小增加量
nums[index] += minAddition;
// 将最小增加量累加到总共需要增加的数值中
totalAddition += minAddition;
// 更新数组中所有元素包含 2 的幂次的总数
totalPower += countPowerOfTwo(nums[index]);
}
// 输出总共需要增加的数值
System.out.println(totalAddition);
}
// 计算一个数中包含 2 的幂次的个数
private static int countPowerOfTwo(int num) {
int count = 0;
// 当该数能被 2 整除时,不断除以 2 并增加计数
while (num % 2 == 0) {
num /= 2;
count++;
}
return count;
}
// 获取大于等于给定数的最小的 2 的幂次
private static int getNextPowerOfTwo(int num) {
int power = 1;
// 不断将 power 左移(相当于乘以 2),直到大于等于给定数
while (power < num) {
power <<= 1;
}
return power;
}
}
试题H:研发资源分配
一般题目最后提问有“最”字的时候,都会有贪心有点关系。以下是我的思路:
-
问题转化:将每天视为一个“回合”,A需要选择比B高的等级来赢得资源。
-
贪心策略:在资源量大的天数(后期),尽量用最小的优势等级战胜B;在资源量小的天数(前期),可以放弃。
分步解析
步骤 1:将天数按资源值降序排序
优先处理资源多的天数(因为对总差值影响更大)。
步骤 2:为每个天数选择A的等级
目标:用最小的A等级战胜B的当前等级,保留大等级用于后续高资源天数。
实现:维护一个可用等级的有序集合(如TreeSet),每次用 higher(P_i)
选择最小可 用的更高等级。
步骤 3:处理无法战胜的情况
如果A没有比B当前等级高的等级,则使用最小的等级(故意输掉,保留高等级)。
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt();
int[] P = new int[N];
for (int i = 0; i < N; i++) P[i] = sc.nextInt();
// 按资源值(天数)降序排序处理
// 创建一个长度为 N 的 Integer 数组 days,用于存储从 1 到 N 的天数
Integer[] days = new Integer[N];
// 初始化 days 数组,元素值为 1 到 N
for (int i = 0; i < N; i++) days[i] = i + 1;
// 对 days 数组进行降序排序,使用 Lambda 表达式指定比较规则
Arrays.sort(days, (a, b) -> b - a);
// 创建一个 TreeSet 集合 aLevels,用于存储 A 可使用的等级,范围是从 1 到 N
TreeSet<Integer> aLevels = new TreeSet<>();
// 向 aLevels 集合中添加 1 到 N 的等级
for (int i = 1; i <= N; i++) aLevels.add(i);
// 初始化差值变量 diff,用于记录 A 和 B 获得资源的差值
long diff = 0;
// 遍历降序排列后的 days 数组
for (int day : days) {
// 获取 B 在当天的等级,由于数组索引从 0 开始,所以使用 day - 1
int bLevel = P[day - 1];
// 从 aLevels 集合中查找比 bLevel 大的最小等级
Integer aLevel = aLevels.higher(bLevel);
// 如果找到了比 bLevel 大的等级
if (aLevel != null) {
// A 使用最小的更高等级赢得资源
// 从 aLevels 集合中移除该等级,表示该等级已被使用
aLevels.remove(aLevel);
// 增加差值,因为 A 赢得了当天的资源
diff += day;
} else {
// A 故意输掉,使用最小等级
// 获取 aLevels 集合中的最小等级
aLevel = aLevels.first();
// 从 aLevels 集合中移除该等级
aLevels.remove(aLevel);
// 减少差值,因为 A 输掉了当天的资源
diff -= day;
}
}
// 输出 A 和 B 获得资源的差值
System.out.println(diff);
}
}
三、最后总结
本次省赛题目难度分布较为合理,覆盖了基础数学、数据结构、贪心算法、图论等核心考点。整体难度中等,部分题目需要巧妙优化,但无过于复杂的算法(如动态规划、高级图论)。
其中有一些常见陷阱与注意事项,如B题直接枚举会超时,需要数学推导;F题暴力翻转区间也会超时,但如果比赛时时间不足或者没有优化思路,先暴力拿分也是很好的选择。
一句话建议:简单题稳扎稳打,难题尽力而为,优先暴力拿分,后期灵活取舍!