前言
蓝桥杯作为一项在大学生群体中极具影响力的竞赛,其参与人数之多堪称众多竞赛中的佼佼者。对于广大大学生而言,蓝桥杯无疑是一个展示自己编程能力、锻炼逻辑思维的重要平台。
我们先来了解一下蓝桥杯的获奖分布情况。蓝桥杯的奖项设置有着明确的比例划分:一等奖占总参赛人数的前 10%,二等奖则是在 10% - 30% 这个区间,三等奖的范围是 30% - 60%。从这个比例分布来看,相较于其他一些竞赛,在蓝桥杯中获得奖项的概率相对较高。这意味着,只要我们做好充分的准备,获奖并非遥不可及的梦想。
然而,在准备和参与蓝桥杯的过程中,我们常常会看到一些不太理想的现象。很多同学在报名的时候,其实并没有真正做好系统学习和深入准备的打算,更像是抱着一种 “赌徒” 心态。他们天真地以为自己在后续有足够的时间去学习相关知识,结果往往是直到考前一个星期才匆匆忙忙地开始复习。这种临时抱佛脚的做法,显然很难在竞赛中取得优异的成绩。
当真正进入考场,面对众多的竞赛题目时,不少同学会不由自主地感到心慌意乱。其实,这种紧张和焦虑是完全没有必要的。要知道,在蓝桥杯这样的竞赛中,90% 的参赛者都很难在规定的时间内将所有题目全部完成。所以,我们要做的就是调整好自己的心态,保持冷静和理智。在考试过程中,我们要根据自己的实际情况,有针对性地选择题目进行解答,确保把自己能够拿到的分数稳稳地收入囊中。
一、报数游戏
题目描述
小蓝和朋友们在玩一个报数游戏。由于今年是 2024 年,他们决定要从小到大轮流报出是 20 或 24 倍数的正整数。前 10 个被报出的数是:20, 24, 40, 48, 60, 72, 80, 96, 100, 120。请问第 202420242024202420242024 个被报出的数是多少?
思路分析
蓝桥杯前面两个的填空题是不需要过程的,也就是不需要你去编码,只需要直接用 System.out.println()
输出就行了,所以我们可以从数学角度去思考这个问题。
-
方法一:找规律
-
首先,我们列出更多的数来观察规律:
-
前 10 个数:20, 24, 40, 48, 60, 72, 80, 96, 100, 120
-
接着往后推 10 个:140, 144, 160, 168, 180, 192, 200, 216, 220, 240
-
-
规律一:
-
可以发现,如果位次为单数的话,就一定为 20 的整倍数,如果位次为双数的话就一定为 24 的整数倍数。202420242024 为双数位次,即第 202420242024 个数是 24 的倍数。第 202420242024 个数是 24 的 202420242024÷2=101210121012 倍,即 24×101210121012。但这个规律仅从倍数奇偶性考虑有局限性,因为 20 和 24 并不是一个共同的公倍数,越到后面,数据的差距会越来越大。
-
-
规律二:
-
这些数据可以按 120 的倍数分组,如 20 - 120 之间有 20, 24, 40, 48, 60, 72, 80, 96, 100, 120;140 - 240 之间有 140, 144, 160, 168, 180, 192, 200, 216, 220, 240。
-
每一组的第一个数是 20+120×(n−1)(n 表示组数)。
-
要求第 202420242024 个数,先求第 202420242024 个数所在组的第一个数,即第 20242024202 组的第一个数:20+120×(20242024203−1)=2429042904260。
-
第 202420242024 个数是该组的第 4 个数(因为每组 10 个数,202420242024mod10=4),该组第一个数往后推 4 个,根据规律组内数的差值(第二个数比第一个数加 4,第三个数比第一个数加 20,第四个数比第一个数加 28,第五个数比第一个数加 40……),可得第 202420242024 个数为 2429042904260+28=2429042904288。
-
-
-
方法二:算法求解
-
可以使用二分查找和集合的容斥原理来解决这个题目。
-
二分查找与容斥原理思路
-
二分查找:通过不断缩小查找范围,找到满足条件的最小数。
-
容斥原理:对于求 20 或 24 的倍数的个数,利用集合的容斥原理,即 ∣A∪B∣=∣A∣+∣B∣−∣A∩B∣,这里 A 表示 20 的倍数集合,B 表示 24 的倍数集合,A∩B 表示 20 和 24 的公倍数(即 120 的倍数)集合。
代码实现
public class Main {
public static void main(String[] args) {
long n = 202420242024L;
long left = 0;
// 给定一个足够大的上界
long right = 2000000000000000000L;
// 二分查找
while (left < right) {
long mid = left + (right - left) / 2;
long count = countMultiples(mid);
if (count < n) {
left = mid + 1;
} else {
right = mid;
}
}
System.out.println(left);
}
// 匹配正确的数
private static long countMultiples(long x) {
// 集合的容斥原理,并集元素总数 = |A| + |B| - |A∩B|
return x / 20 + x / 24 - x / 120;
}
}
二、类斐波拉契循环数
题目描述
对于一个有 n 位的十进制数 N=d1d2d3...dn可以生成一个类斐波那契数列 S,数列 S 的前 n 个数为:{S1=d1, S2=d2, S3=d3, …, Sn=dn}数列 S 的第 k(k>n) 个数为:
对于数列S中的第k个元素(k > n),可以通过以下公式计算: $$ S_k = \sum_{i=k-n}^{k-1} S_i $$。
如果这个数 N 会出现在对应的类斐波那契数列 S 中,那么 N 就是一个类斐波那契循环数。例如对于 197,对应的数列 S 为:{1,9,7,17,33,57,107,197,…},197 出现在 S 中,所以 197 是一个类斐波那契循环数。请问在 0 至 107 中,最大的类斐波那契循环数是多少?
思路分析
首先这个题目要求我们求出类斐波拉契数列,注意这个 “类” 字。我们的思路是先用二分查找法查找范围内的数,对于每个可能的 n 位数,构造初始的 S 数组(各位数字),然后按照类斐波那契的方式生成后续的数,直到生成的数等于初始的 N 或者进入循环而不包含 N 为止。如果在生成过程中出现了 N,则记录下来。
具体步骤
-
遍历从 9999999 到 0 的所有数 N,逐个检查是否为循环数。
-
对于每个数N:
-
分解其各位数字,得到数组
digits
,长度为 n。 -
如果 n==1,则直接返回 N,因为所有一位数都是循环数。
-
初始化队列为
digits
数组,计算初始窗口的和sum
。 -
创建哈希集合
seen
,并将digits
中的元素加入。 -
进入生成循环:
-
生成
current = sum
。 -
检查
current
是否等于 N?若是,返回 N 作为答案。 -
检查
current
的位数是否超过 n?若是,跳过当前数,继续下一个。 -
检查
current
是否已存在于seen
中?若是,跳过。 -
将
current
加入seen
。 -
弹出队首元素,压入
current
,更新sum
。 -
重复。
-
-
代码实现
import java.util.*;
public class Main {
public static void main(String[] args) {
System.out.println(findLargestFibLoopNumber());
}
private static long findLargestFibLoopNumber() {
for (long n = 10_000_000; n >= 0; n--) {
if (isFibLoopNumber(n)) {
return n;
}
}
return -1;
}
private static boolean isFibLoopNumber(long num) {
if (num < 0) return false;
String s = Long.toString(num);
int n = s.length();
char[] digits = s.toCharArray();
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = digits[i] - '0';
}
if (n == 1) {
return true;
}
Queue<Integer> queue = new LinkedList<>();
for (int d : arr) {
queue.add(d);
}
long sum = Arrays.stream(arr).sum();
Set<Long> seen = new HashSet<>();
for (int d : arr) {
seen.add((long) d);
}
while (true) {
long current = sum;
if (current == num) {
return true;
}
int currentDigits = Long.toString(current).length();
if (currentDigits > n) {
return false;
}
if (seen.contains(current)) {
return false;
}
seen.add(current);
int popped = queue.poll();
queue.add((int) current);
sum += (current - popped);
}
}
}
三、分布式队列
题目描述
小蓝最近学习了一种神奇的队列:分布式队列。简单来说,分布式队列包含 N 个节点(编号为 0 至 (N - 1),其中 0 号为主节点),其中只有一个主节点,其余为副节点。主 / 副节点中都各自维护着一个队列,当往分布式队列中添加元素时,都是由主节点完成的(每次都会添加元素到主节点对应的队列的尾部);副节点只负责同步主节点中的队列。可以认为主 / 副节点中的队列是一个长度无限的一维数组,下标为 (0,1,2,3...) ,同时副节点中的元素的同步顺序和主节点中的元素添加顺序保持一致。
由于副本的同步速度各异,因此为了保障数据的一致性,元素添加到主节点后,需要同步到所有的副节点后,才具有可见性。
给出一个分布式队列的运行状态,所有的操作都按输入顺序执行。你需要回答在某个时刻,队列中有多少个元素具有可见性。
思路解析
在这个题目中,我们主要需要处理三种不同的指令:add
、sync
和 query
。
-
add
指令:这是最基本的操作,我们可以使用一个List
来存储主节点的元素。当接收到add
指令时,将元素添加到主节点对应的队列(即List
)的尾部。 -
sync
指令:对于sync
指令,我们不需要创建多个副节点对象,而是可以使用一个数组来模拟副节点的同步状态。数组的每个索引对应一个副节点的 ID,数组的值表示该副节点已经同步到的元素位置。当接收到sync
指令时,更新对应副节点的同步位置。 -
query
指令:对于query
指令,我们需要遍历所有副节点的同步位置,找到最小的同步位置,这个最小的同步位置就是当前队列中具有可见性的元素数量。
代码实现
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt();
// 读取换行符
scanner.nextLine();
List<Integer> mainQueue = new ArrayList<>();
// 每一个索引对应对应的副节点
int[] syncPointers = new int[N];
while (scanner.hasNextLine()) {
// 读取一行
String line = scanner.nextLine().trim();
if (line.isEmpty()) {
continue;
}
// 分割
String[] parts = line.split(" ");
// 取出对应的指令
String command = parts[0];
switch (command) {
case "add":
// 取出添加值
int element = Integer.parseInt(parts[1]);
// 添加到主队列
mainQueue.add(element);
break;
case "sync":
// 取出副节点 ID
int followerId = Integer.parseInt(parts[1]);
if (followerId <= 0 || followerId >= N) {
// 无效的副节点 ID,忽略
break;
}
// 同步副节点的指针
if (syncPointers[followerId] < mainQueue.size()) {
syncPointers[followerId]++;
}
break;
case "query":
// 处理特殊的情况
if (N == 1) {
System.out.println(mainQueue.size());
} else {
// 遍历副节点的指针,找到最小的
int minSync = Integer.MAX_VALUE;
for (int i = 1; i < N; i++) {
if (syncPointers[i] < minSync) {
minSync = syncPointers[i];
}
}
System.out.println(minSync);
}
break;
}
}
scanner.close();
}
}
三、食堂
题目描述
SS 学校里一共有 (a_2) 个两人寝、(a_3) 个三人寝、(a_4) 个四人寝,而食堂里有 (b_4) 个四人桌和 (b_6) 个六人桌。学校想要安排学生们在食堂用餐,并且满足每个寝室里的同学都在同一桌就坐,请问这个食堂最多同时满足多少同学用餐?
思路解析
要解决这个问题,我们需要将不同类型的宿舍(两人、三人、四人)分配到四人桌和六人桌,使得每个宿舍的学生都能坐在同一张桌子,并且最大化同时用餐的学生人数。正确的策略是在有限的桌子里尽可能多地安排学生,采用贪心算法优先处理最优组合。
具体的组合策略如下:
- 输入处理:读取数据组数
q
,并循环处理每组数据。 - 变量初始化:分别读取寝室和桌子的数量,并初始化总人数
total
。 - 处理四人桌的满座情况:
- 优先将四人寝安排到四人桌。
- 剩余四人桌安排两个两人寝。
- 处理六人桌的满座情况:
- 优先安排四人寝加两人寝。
- 然后安排两个三人寝。
- 最后安排三个两人寝。
- 处理六人桌的部分填充:
- 两人加三人寝(5人)。
- 处理四人桌的剩余:
- 安排三人寝到四人桌。
- 处理六人桌的剩余:
- 两个两人寝(4人)、四人寝(4人)、单个三人寝(3人)、单个两人寝(2人)。
- 处理四人桌的剩余:
- 单个两人寝到四人桌。
- 输出结果:每组数据处理完毕后,输出最大同时用餐人数。
代码实现
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int q = scanner.nextInt();
while (q-- > 0) {
int a2 = scanner.nextInt();
int a3 = scanner.nextInt();
int a4 = scanner.nextInt();
int b4 = scanner.nextInt();
int b6 = scanner.nextInt();
int total = 0;
// 处理四人桌的满座情况:优先四人寝,然后两个两人寝
while (b4 > 0 && a4 > 0) {
total += 4;
b4--;
a4--;
}
while (b4 > 0 && a2 >= 2) {
total += 4;
b4--;
a2 -= 2;
}
// 处理六人桌的满座情况:优先四人+两人,然后两个三人,最后三个两人
while (b6 > 0 && a4 > 0 && a2 > 0) {
total += 6;
b6--;
a4--;
a2--;
}
while (b6 > 0 && a3 >= 2) {
total += 6;
b6--;
a3 -= 2;
}
while (b6 > 0 && a2 >= 3) {
total += 6;
b6--;
a2 -= 3;
}
// 处理六人桌的部分填充:两人+三人(5人)
while (b6 > 0 && a2 >= 1 && a3 >= 1) {
total += 5;
b6--;
a2--;
a3--;
}
// 处理四人桌的剩余:三人寝(3人)
while (b4 > 0 && a3 > 0) {
total += 3;
b4--;
a3--;
}
// 处理六人桌的部分填充:两个两人(4人)
while (b6 > 0 && a2 >= 2) {
total += 4;
b6--;
a2 -= 2;
}
// 处理六人桌的剩余:四人寝(4人)
while (b6 > 0 && a4 > 0) {
total += 4;
b6--;
a4--;
}
// 处理四人桌的剩余:单个两人(2人)
while (b4 > 0 && a2 > 0) {
total += 2;
b4--;
a2--;
}
// 处理六人桌的剩余:单个三人(3人)
while (b6 > 0 && a3 > 0) {
total += 3;
b6--;
a3--;
}
// 处理六人桌的剩余:单个两人(2人)
while (b6 > 0 && a2 > 0) {
total += 2;
b6--;
a2--;
}
System.out.println(total);
}
scanner.close();
}
}
四、结语
就拿前面提到的四个题目来说,如果能够认真、细致地完成,那么保底也能获得一个二等奖。而对于三等奖,运气好的话,只需要成功做出前面两个填空题就有机会。值得一提的是,这两个填空题很多时候都可以运用数理逻辑进行推理得出答案。这就要求我们在日常的学习过程中,不仅要注重编程能力的提升,还要加强对数理逻辑知识的学习和运用。
当然,我们也要明白,竞赛的结果并不是衡量我们能力和努力的唯一标准。即使最终没有获得奖项,也不要过于沮丧和失落。毕竟,参与蓝桥杯的过程本身就是一次宝贵的学习和成长经历。在这个过程中,我们不仅可以提升自己的编程技能和逻辑思维能力,还能结识到一群志同道合的朋友,拓宽自己的视野。所以,无论最终的结果如何,我们都应该以积极的心态去面对,珍惜这个难得的机会。
总之,蓝桥杯是一场充满挑战与机遇的竞赛。只要我们能够以正确的心态去对待,做好充分的准备,并且在考试过程中发挥出自己的最佳水平,就一定能够有所收获。最重要的是,享受这个过程,让自己在不断的挑战中变得更加优秀。