第十六届蓝桥杯京津冀赛区省赛(二卷)javaB组部分题目题解解析

一、前言

       就在前天,我抱着紧张的情绪进入了赛场。比赛前看了一遍遍的算法模板,到了比赛的时候却发现很少用到。我自认为二卷的难度是要比一卷简单不少的,考察的算法不多(比如动态规划,DFSBFS),相反,对数学推导、贪心思想、字符串处理考察的更多。但我仍然有一些该AC的题目没能拿全。下面,我来讲部分题目的我的思路,填空第一题和大题第一题都不进行解析,因为这两道题是绝对的签到题,十分简单。事不宜迟,直接开始。

二、题解解析

        因为没有找到pdf版本的试卷,这里直接从洛谷上截图了。

(提示:因为官方的题解还没发放且洛谷中的数据不够准确和足够,本文中的代码只有部分能做到AC,没能AC的代码我会讲一下我的思路,欢迎大佬来谈论指点。)

试题B:脉冲强度之和

       拿到这道题时,我首先把它看成了规律题,因为p=10*k+45,又要求数位数字相同,我直接认定要求的是从1到2025202的数位上是5的数字总和。但是这里要排除5,因为5不能满足条件1,其次是55555555(不满足条件3)。

public class Main {
    public static void main(String[] args) {
        long sum = 0;
        long p = 0;
        for (int n = 1; n <= 8; n++) { // 生成1~8位的全5数字
            p = p * 10 + 5;
            if (p > 20255202) break;    // 超出范围则终止
            if ((p - 45) % 10 == 0 && (p - 45) / 10 >= 1) {
                sum += p;
            }
        }
        System.out.println(sum); // 输出6172830
    }
}

试题D:旗帜

       这道题我在比赛时用了一个指针遍历“LANQIAO”转换后的字符数组,在遍历矩阵时通过指针做到循环输入。每行结束后,指针额外右移1(模拟题意左移)代码如下:

解法一

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int h = scanner.nextInt();
        int w = scanner.nextInt();
        char[] pattern = {'L', 'A', 'N', 'G', 'I', 'A', 'O'};
        int countA = 0;
        int ptr = 0; // 指针初始指向pattern[0]

        for (int i = 0; i < h; i++) {
            for (int j = 0; j < w; j++) {
                // 填充字符并检查是否为'A'
                if (pattern[ptr] == 'A') {
                    countA++;
                }
                // 移动指针(循环)
                ptr = (ptr + 1) % 7;
            }
            // 每行起始位置左移1
            ptr = (ptr + 1) % 7;
        }
        System.out.println(countA);
    }
}

        但赛后我又写出一种写法,先看代码:

解法二

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int h = scanner.nextInt();
        int w = scanner.nextInt();
        String pattern = "LANGIAO";
        int countA = 0;

        for (int i = 0; i < h; i++) {
            for (int j = 0; j < w; j++) {
                int index = (i + j) % 7; // 直接计算字符位置
                if (pattern.charAt(index) == 'A') {
                    countA++;
                }
            }
        }
        System.out.println(countA);
    }
}

        关键在于填充的规律:矩阵中 (i,j) 位置的字符为 pattern[(i + j) % 7]比如,i和j分别为1,4时,(i+j)%7=5,对应的是'A'。相比之下,这个写法更简单也无需考虑指针转移时可能出现的偏差。

试题E:数列差分

        这道题是我的一个重大失误,我在读到题目时,把解题重点放在了如何实现“差分”这个操作,而不是如何求出最小操作数。比赛时在这道题上浪费了很多时间,最后只让代码输出了“1”,大概估测了一下,只能拿到15分。赛后我听其他大佬的解法,排序后用双指针来进行比较,在洛谷上也是直接AC了。代码如下:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Collections;

public class Main {
	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		int n = Integer.parseInt(br.readLine());
		Integer[] A = new Integer[n];
		Integer[] B = new Integer[n];
		
		// 读取数组A
		String[] parts = br.readLine().split(" ");
		for (int i = 0; i < n; i++) {
			A[i] = Integer.parseInt(parts[i]);
		}
		
		// 读取数组B
		parts = br.readLine().split(" ");
		for (int i = 0; i < n; i++) {
			B[i] = Integer.parseInt(parts[i]);
		}
		
		// 排序:都按降序排列
		Arrays.sort(A, Collections.reverseOrder());
		Arrays.sort(B, Collections.reverseOrder());
		
		int ans = 0;
		int left = 0, right = n - 1;  // left指向A当前最大,right指向A当前最小
		
		for (int j = 0; j < n; j++) {
			if (A[left] > B[j]) {
				left++;  // 匹配成功,移动左指针
			} else {
				right--;  // 用B[j]匹配A的最小元素
				ans++;    // 需要修改
			}
		}
		
		System.out.println(ans);
	}
}

        这里简单用了BufferedReader进行了快读。其中,left指针指向A中当前最大的元素,right指针指向A中当前最小的元素。如果A[left]>B[j],则直接匹配;否则就用B[j]去匹配A[right],并移动right指针,且要增加操作次数。

        这里的贪心策略为:总是尝试用最大的B去匹配最大的A,如果不能匹配则用最小的A

试题F:基因配对

        这道题我在比赛时因为没有思路直接跳过了,虽然这道题没写出来,但下面的“栈与乘积”我做了出来。所以说,在比赛时我认为在必要时是要做取舍的,不能在一道自己毫无思路的题上浪费太多时间。赛后我也查看了其他大佬的思路,发现使用滑动窗口+哈希表这个思路非常棒,输入案例得到的输出也正确,不过我这里洛谷不知为何卡住了,没办法提交这个答案,有人想知道通过率的话可以试着提交一下。

mport java.util.Scanner;

import java.util.*;

public class Main {
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		String s = sc.next();
		int n = s.length();
		// 用于统计有效配对的数量,初始化为0
		long count = 0;
		
		// 限制最大子串长度,防止内存溢出(MLE),取字符串长度和20中的较小值
		int maxLen = Math.min(20, n);
		
		// 预处理:存储每个子串及其反转子串的出现位置
		// key为子串,value为该子串在原字符串中出现的起始位置的列表
		Map<String, List<Integer>> substrPos = new HashMap<>();
		
		// 枚举子串的长度,从1到最大长度maxLen
		for (int len = 1; len <= maxLen; len++) {
			// 枚举子串的起始位置,确保子串不越界
			for (int i = 0; i + len <= n; i++) {
				// 截取长度为len的子串
				String sub = s.substring(i, i + len);
				// 生成子串的反转子串(0变为1,1变为0)
				String reversed = reverseBits(sub);
				
				// 将子串及其出现位置存入map中,如果子串不存在则创建一个新的列表
				substrPos.computeIfAbsent(sub, k -> new ArrayList<>()).add(i);
				// 将反转子串及其出现位置存入map中,如果反转子串不存在则创建一个新的列表
				substrPos.computeIfAbsent(reversed, k -> new ArrayList<>()).add(i);
			}
		}
		
		// 滑动窗口统计有效配对
		// 再次枚举子串的长度,从1到最大长度maxLen
		for (int len = 1; len <= maxLen; len++) {
			// 再次枚举子串的起始位置,确保子串不越界
			for (int i = 0; i + len <= n; i++) {
				// 截取当前的子串
				String current = s.substring(i, i + len);
				// 获取当前子串在原字符串中出现的起始位置的列表
				List<Integer> positions = substrPos.get(current);
				
				if (positions != null) {
					// 当前窗口结束位置
					int endPos = i + len - 1;
					// 二分查找第一个起始位置 > endPos的索引
					int left = 0, right = positions.size();
					while (left < right) {
						int mid = left + (right - left) / 2;
						if (positions.get(mid) > endPos) {
							right = mid;
						} else {
							left = mid + 1;
						}
					}
					// 统计有效配对的数量,即当前子串在当前窗口之后出现的次数
					count += positions.size() - left;
				}
			}
		}
		
		// 输出有效配对的数量
		System.out.println(count);
	}
	
	// 反转01字符串的方法
	private static String reverseBits(String s) {
		StringBuilder sb = new StringBuilder();
		// 遍历字符串中的每个字符
		for (char c : s.toCharArray()) {
			// 如果字符为0则转换为1,否则转换为0
			sb.append(c == '0' ? '1' : '0');
		}
		// 返回反转后的字符串
		return sb.toString();
	}
}

试题G:栈与乘积

        这道题我当开始用的是java自带的Stack,但写到第三步的时候我发现,实现累乘操作的话比较麻烦,所以我又改成了模拟栈,前两步都没问题,第三步直接用的遍历,当时没有想太多,现在想想可能会超时。不过拿到90分应该是没有问题。赛后我又进行了简单的优化,每次累乘之后判断是否溢出,这样就可以提前结束,更有可能拿到满分。

        这段代码中也使用了快读来处理大数据输入。

import java.util.*;
import java.io.*;

public class Main {
	// 定义一个常量 MAX,其值为 2 的 32 次方,用于判断栈中元素乘积是否溢出
	static final long MAX = 1L << 32;
	
	public static void main(String[] args) throws IOException {
		// 创建一个 BufferedReader 对象,用于从标准输入读取数据
		// 使用 InputStreamReader 将字节流转换为字符流,以处理文本输入
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		// 读取第一行输入并将其转换为整数,此整数表示操作的总次数
		int Q = Integer.parseInt(br.readLine());
		// 创建一个长度为 Q 的整型数组 stack,用于模拟栈结构
		// Q 代表操作的最大可能次数,因此数组最大可能空间为 Q
		int[] stack = new int[Q];
		// 定义一个整型变量 top 作为栈顶指针,初始值为 -1 表示栈为空
		int top = -1;
		
		// 循环执行 Q 次操作
		while (Q-- > 0) {
			// 使用 StringTokenizer 对读取的一行输入进行分割,方便获取不同的操作信息
			StringTokenizer st = new StringTokenizer(br.readLine());
			// 从分割后的输入中获取第一个标记,并将其转换为整数,得到当前操作的类型
			int op = Integer.parseInt(st.nextToken());
			
			// 当操作类型为 1 时,执行入栈操作
			if (op == 1) {
				// 栈顶指针 top 加 1,指向新的栈顶位置
				// 从分割后的输入中获取第二个标记,并将其转换为整数,存入新的栈顶位置
				stack[++top] = Integer.parseInt(st.nextToken());
				// 当操作类型为 2 时,执行出栈操作
			} else if (op == 2) {
				// 检查栈是否不为空(即栈顶指针 top 大于等于 0)
				if (top >= 0) {
					// 若栈不为空,栈顶指针 top 减 1,相当于将栈顶元素弹出
					top--;
				}
				// 当操作类型为 3 时,执行计算栈中前 y 个元素乘积的操作
			} else if (op == 3) {
				// 从分割后的输入中获取第二个标记,并将其转换为整数,得到要计算乘积的元素个数 y
				int y = Integer.parseInt(st.nextToken());
				// 检查栈中元素的数量(top + 1)是否小于 y
				if (top + 1 < y) {
					// 若栈中元素数量不足 y 个,输出 "ERROR" 并跳过本次循环的后续操作
					System.out.println("ERROR");
					continue;
				}
				
				// 初始化一个长整型变量 product 用于存储乘积结果,初始值为 1
				long product = 1;
				// 定义一个布尔型变量 overflow 用于标记乘积是否溢出,初始值为 false
				boolean overflow = false;
				// 从栈顶开始,向前遍历 y 个元素
				for (int i = top; i > top - y; i--) {
					// 将当前元素乘到乘积结果 product 中
					product *= stack[i];
					// 检查乘积结果 product 是否大于等于 MAX
					if (product >= MAX) {
						// 若大于等于 MAX,说明乘积溢出,将 overflow 标记为 true 并跳出循环
						overflow = true;
						break;
					}
				}
				// 根据 overflow 的值输出结果,若为 true 则输出 "OVERFLOW",否则输出乘积结果 product
				System.out.println(overflow ? "OVERFLOW" : product);
			}
		}
	}
}   

试题H:破解信息

        最后这道题我在看的时候是有思路的,刚开始想要使用DFS暴力求解,但当时写到最后脑子已经跟不上了,没能把DFS写出来,最后也只是试着拿了部分分。

        先讲一下我刚开始的暴力思路吧。先用DFS枚举出所有的子序列组合,在写一个判断回文序列的函数,最后根据题中条件选出满足条件的字典序最大的回文子序列。代码如下:

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Main {
    // 定义一个静态的列表,用于存储所有找到的回文子序列
    // 因为后续要在所有回文子序列中找出字典序最大的,所以先将它们都存起来
    private static List<String> palindromicSubsequences = new ArrayList<>();

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String s = scanner.next();

        // 调用深度优先搜索方法 dfs 开始生成所有可能的子序列,并筛选出回文子序列
        dfs(s, 0, "");
        // 初始化一个空字符串,用于存储最终找到的字典序最大的回文子序列
        String maxPalindrome = "";
        // 遍历存储所有回文子序列的列表
        for (String palindrome : palindromicSubsequences) {
            // 使用 compareTo 方法比较当前回文子序列和已记录的最大回文子序列的字典序
            // 如果当前回文子序列的字典序更大,则更新最大回文子序列
            if (palindrome.compareTo(maxPalindrome) > 0) {
                maxPalindrome = palindrome;
            }
        }
        // 输出最终找到的字典序最大的回文子序列
        System.out.println(maxPalindrome);
    }

    // 深度优先搜索方法,用于生成字符串 s 的所有子序列,并将其中的回文子序列添加到列表中
    // s 是原始字符串,index 是当前处理到的字符索引,current 是当前已经生成的子序列
    private static void dfs(String s, int index, String current) {
        // 当索引等于字符串的长度时,说明已经处理完了所有字符
        if (index == s.length()) {
            // 检查当前生成的子序列是否为回文序列
            if (isPalindrome(current)) {
                // 如果是回文序列,则将其添加到存储回文子序列的列表中
                palindromicSubsequences.add(current);
            }
            // 结束当前递归调用
            return;
        }
        // 情况一:不选择当前索引对应的字符,继续处理下一个字符
        dfs(s, index + 1, current);
        // 情况二:选择当前索引对应的字符,添加到当前子序列中,并继续处理下一个字符
        dfs(s, index + 1, current + s.charAt(index));
    }


    // 判断一个字符串是否为回文序列的方法
    private static boolean isPalindrome(String s) {
        // 初始化左指针,指向字符串的起始位置
        int left = 0;
        // 初始化右指针,指向字符串的末尾位置
        int right = s.length() - 1;
        // 当左指针小于右指针时,继续比较字符
        while (left < right) {
            // 如果左右指针指向的字符不相等,说明该字符串不是回文序列
            if (s.charAt(left) != s.charAt(right)) {
                return false;
            }
            // 左指针右移一位
            left++;
            // 右指针左移一位
            right--;
        }
        // 如果循环结束都没有发现不相等的字符,说明该字符串是回文序列
        return true;
    }
}    

        后来我又想到一种很简单的思路,真的很简单很简单!不废话,直接看代码:

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String s = scanner.next();

        // 初始化一个字符变量 maxChar,用于存储字符串 s 中字典序最大的字符,初始值设为 'a'
        char maxChar = 'a';
        // 遍历字符串 s 中的每个字符
        for (char c : s.toCharArray()) {
            // 如果当前字符 c 的字典序大于 maxChar,则更新 maxChar 的值为 c
            if (c > maxChar) {
                maxChar = c;
            }
        }
        // 初始化一个整数变量 count,用于统计字符串 s 中字典序最大字符的出现次数,初始值为 0
        int count = 0;
        // 再次遍历字符串 s 中的每个字符
        for (char c : s.toCharArray()) {
            // 如果当前字符 c 等于字典序最大的字符 maxChar,则将计数器 count 加 1
            if (c == maxChar) {
                count++;
            }
        }
        // 输出由字典序最大的字符重复 count 次组成的字符串,该字符串即为字典序最大的回文子序列
        System.out.println(String.valueOf(maxChar).repeat(count));
    }
}

        没错,用的就是贪心思想。要使整个回文子序列的字典序最大,就应该选择最大的字符,又已知全由相同字符组成的字符串必然是回文,所以最长且全由最大字符组成的回文子序列,就是最优解。因此,只需统计字符串中字典序最大的字符,并全部选取即可

三、赛后总结

        在本次竞赛的征程中,二卷试题虽未呈现出令人望而却步的高难度,但其背后更多的是对思维深度与心态把控的考验。于赛场之上,紧张的情绪恰似绊脚之石,会扰乱清晰的思路;而在难题上的过度执着,亦如深陷迷雾的徘徊,消耗着宝贵的时间与精力。

        既然比赛结束了,就不该再有太多的顾虑,认真复盘,放松心情才是赛后该做的事。一次失利不代表努力白费,日后多加磨炼必能造就自己的成功!

 

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值