第十六届蓝桥杯JavaB组题目题解解析(一卷)

 一、前言 

       最近刚参加完今年的蓝桥杯省赛,发现今年的题目在算法思维和代码实现上都有不少值得深挖的点。今天简单回温一下一卷的题目,并试着写一些我的解题思路。因为我在河北,比赛用的是二卷,所以事先声明,这里一卷的解法代码不一定是正解,只是给大家提供一些能够想到的思路吧。

二、题目

      试题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:研发资源分配

     一般题目最后提问有“最”字的时候,都会有贪心有点关系。以下是我的思路:

  1. 问题转化:将每天视为一个“回合”,A需要选择比B高的等级来赢得资源。

  2. 贪心策略:在资源量大的天数(后期),尽量用最小的优势等级战胜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题暴力翻转区间也会超时,但如果比赛时时间不足或者没有优化思路,先暴力拿分也是很好的选择。

       一句话建议:简单题稳扎稳打,难题尽力而为,优先暴力拿分,后期灵活取舍!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值