第十四届蓝桥杯省赛Java大学A组题解 第十四届蓝桥杯省赛Java大学A组题解

目录

A特殊日期

题目:

题解:

B与或异或

题目:

题解:

part1 - 全排列:

part2 - 深搜:

C平均

题目:

题解: 

D棋盘

题目:

题解: 

解1 - 暴力:

解2 - 前缀和差分(TODO):

E互质数个数

题目:

题解:

part1 - 欧拉函数:

part2 - E(a) :

part3 - 快速幂:

总体代码:

F阶乘的和

题目:

题解:

G小蓝的旅行计划

题目:

题解:

贪心:

线段树: 

H太阳

题目:

题解:

极角排序:

J反异或01串

题目:

题解:


A特殊日期

题目:

题解:

 使用LocalDate对日期进行枚举,统计满足条件的日期数量即可

public static void main(String[] args) {
 LocalDate start = LocalDate.of(1900, 1, 1);
 LocalDate end = LocalDate.of(9999, 12, 31);
 int ans = 0;
 while (start.isBefore(end)) {//枚举日期
 int y = start.getYear(), m = start.getMonthValue(), d = start.getDayOfMonth();
 if (isEqual(y, m, d)) {//判断是否满足条件
 ans++;
 }
 start = start.plusDays(1);
 }
 System.out.println(ans);//70910
}
private static boolean isEqual(int y, int m, int d) {
 int count = 0;
 while (y > 0) {
 count += y % 10;
 y /= 10;
 }
 while (m > 0) {
 count -= m % 10;
 m /= 10;
 }
 while (d > 0) {
 count -= d % 10;
 d /= 10;
 }
 return count == 0;
}

 最终答案为

70910

B与或异或

题目:

题解:

深搜枚举所有情况,统计最后结果为1的方案数 

part1 - 全排列:

要从该层的值推导出下一层的值, 需要枚举门的全部选择方案

/**
 对门进行全情况枚举,0表示与门,1表示或门,2表示异或门
 @param oper 结果存储
 @param start 当前要枚举的门编号
 @param end 要枚举的门数量
 @param stack 存储当前枚举的情况
 */
static void All(List oper, int start, int end, Stack stack) {
 if (start == end) {
 oper.add(new ArrayList(stack));
 return;
 }
 for (int i = 0; i < 3; i++) {
 stack.push(i);
 All(oper, start + 1, end, stack);
 stack.pop();
 }
}

part2 - 深搜:

定义int dfs(int[]arr)表示初始输入为arr时,最终结果为1的方案数,令n为arr的长度

  • 当n为1时判断是否为1即可
  • 当n大于1时,需要生成一个门的全排列(长度为n-1) , 对于每一个全排列创建一个新的数组newArr(长度为n-1, 其值由arr与门的排列生成).  那么dfs(arr)可以表示为 Sum(dfs(newArr))
static int dfs(int[] arr) {
 int n = arr.length;
 if (n == 1) return arr[0] == 1 ? 1 : 0;
 //生成长度为n-1的门的情况枚举
 List oper = new ArrayList();
 All(oper, 0, n - 1, new Stack());
 int res = 0;
 for (List op : oper) {
 //每个排列生成一个长度为n-1的新数组
 int[] newArr = new int[n - 1];
 for (int i = 0; i < n - 1; i++) {
 int o = op.get(i);
 if (o == 0) {
 newArr[i] = arr[i] & arr[i + 1];
 } else if (o == 1) {
 newArr[i] = arr[i] | arr[i + 1];
 } else {
 newArr[i] = arr[i] ^ arr[i + 1];
 }
 }
 res += dfs(newArr);//dfs(arr)=Sum(dfs(newArr))
 }
 return res;
}

最后调用dfs(new int[]{1,0,1,0,1})输出即可

答案为

30528

C平均

题目:

题解: 

n是10的倍数,每个数的出现次数都相等, 一个数修改成任意数都是等代价的, 我们只需要关心哪些数大于n/10即可,什么数少了多少个不需要管,因为多的数必须修改,而修改成哪个数并不重要

贪心: 假如数字1有是多余, 更改数字1的代价有 {a,b,c,..}, 那么肯定是选择最小的一个代价去修改才是最优解. 所以对于多余的数字,需要找到修改它的代价集,选择最小的代价

基于以上,可以使用优先级队列存储一个数代价集,方便获取它的最小代价,然后还需要用HashMap构建数字到它的代价集的映射 

public static void main(String[] args) {
 Scanner sc = new Scanner(System.in);
 HashMap map = new HashMap();//数字->优先级队列(代价集),代价升序排列
 int[] count = new int[10];//计数
 int n = sc.nextInt();
 for (int i = 0; i < n; i++) {
 int num = sc.nextInt(), b = sc.nextInt();
 map.computeIfAbsent(num, k -> new PriorityQueue()).offer(b);
 count[num]++;
 }
 int ans = 0;
 for (int i = 0; i < 10; i++) {
 int more = count[i] - n / 10;//数字i的多余个数
 Queue queue = map.get(i);
 for (int j = 0; j < more; j++) {//修改p个数字i
 ans += queue.poll();//挑最小代价进行修改
 }
 }
 System.out.println(ans);
 sc.close();
}

D棋盘

题目:

题解: 

解1 - 暴力:

创建n*n的二维数组,每次操作对(x1,y1)至(x2,y2)的区域的数进行取反,操作完后打印即可

public static void main(String[] args) {
 Scanner sc = new Scanner(System.in);
 int n = sc.nextInt();
 int[][] arr = new int[n][n];
 int m = sc.nextInt();
 for (int i = 0; i < m; i++) {
 //对(x1,y1)至(x2,y2)的区域的数进行取反
 int x1 = sc.nextInt() - 1, y1 = sc.nextInt() - 1, x2 = sc.nextInt() - 1, y2 = sc.nextInt() - 1;
 for (int x = x1; x 

解2 - 前缀和差分(TODO):

正解应该是使用前缀和差分达到每次操作O(1)的时间复杂度, 但是没想到暴力直接过了, 那我也懒得写了, 需要这个解法的私信戳我, 我更一下

E互质数个数

题目:

题解:

part1 - 欧拉函数:

令E(x)表示[1,x]上与x互质数的个数

欧拉函数有以下两个常用公式:

  1. E(a^b) = E(a) * a^(b-1)
  2. E(k) = k * ∏(1 - 1/pi) 其中pi是k的质因子

所以套用公式1得 E(a^b) % MOD = E(a) * pow( a, b-1 ) % MOD

其中E(a)的求解可以使用公式2, pow(a,b-1)可以使用快速幂算法

part2 - E(a) :

暴力: 要求出a的质因数, 最简单的做法就是从i=2开始枚举,如果i是质数且a%i==0, 则找到一个质因数.

素数筛法: 从第一个质数i=2开始枚举, 如果i是a的因数, 那么将a不断除以2, 直到a%2!=0, 筛掉了所有a中2的倍数的因子, i=3时也是如此, i=4时因为已经被2筛掉了, 所以i不是a的因数了,可以直接跳过

举一些例子: 

实际上不需要全部枚举,只需要枚举到sqrt(a)即可

因为sqrt(a)*sqrt(a)<=a, 不可能存在两个大于sqrt(a)的质因数, 所以如果a存在一个大于sqrt(a)的质因数k, 那么经过操作后, a的值就等于k, 如果不存在, 那么最后a的值会等于1, 所以只需要做一个a是否等于1的判断即可

static long E(long a) { // E(a) = a * ∏(1 - 1/pi)
 long p = a;
 long res = a;
 int sq = (int) Math.sqrt(a);
 for (int i = 2; i 

part3 - 快速幂:

快速幂算法求x^n % MOD , 这里介绍二进制快速幂算法

先看一个例子(我还没想好怎么讲):  

也就是说对指数n进行二进制拆分, 从最低位开始取, 如果是1, 那么该项需要乘, 其中要乘的数为底数, 底数初始为base=x, 每次取n的二进制时需要将base平方

static long pow(long x, long n) { //x^n 
 long ans = 1;
 while (n != 0) {
 if ((n & 1) == 1) ans = (ans * x) % MOD;//最低位为1,则需要乘
 x = (x * x) % MOD;//底数平方
 n >>= 1;
 }
 return ans;
}

总体代码:

public static void main(String[] args) {
 Scanner sc = new Scanner(System.in);
 long a = sc.nextLong(), b = sc.nextLong();
 System.out.println(E(a) * pow(a, b - 1) % MOD); //E(a^b)=E(a)*a^(b-1)
}
static long E(long a) { // E(a) = a * ∏(1 - 1/pi)
 long p = a;
 long res = a;
 int sq = (int) Math.sqrt(a);
 for (int i = 2; i >= 1;
 }
 return ans;
}

F阶乘的和

题目:

题解:

将A从低到高排列, m=A1时一定满足要求, 因为 A1! % m! = 0, 而A2!、A3!...都含有因子A1!, 则Sum(Ai!) % m! = 0

当 m=A2 (A1!=A2) 时一定不满足要求, 因为 A1! 中不含 A2项, 所以一定无法整除m!

比如 2!、3!、4!, 当m=2时, (2!+3!+4!)/m!=1+3+4*3可以整除, 当m=3时, (2!+3!+4!)/m!=1/3+1+4不能整除

如果A1==A2呢, 每3个2! 可以提升为1个3! (3个二星融合1个3星[doge]), 每4个3!可以提升为1个, 即 n个k! 可以融合为 n/(k+1)个(k+1)! , 其中n是k+1的倍数, 只需要经过层层融合后, 最小一个A就是答案

public static void main(String[] args) {
 Scanner sc = new Scanner(System.in);
 //将数据存储到优先级队列中,按A的值进行排序
 HashMap map = new HashMap();//A_Value->A_Count
 int n = sc.nextInt();
 for (int i = 0; i < n; i++) {
 int num = sc.nextInt();
 map.put(num, map.getOrDefault(num, 0) + 1);
 }
 Queue queue = new PriorityQueue((Comparator.comparingInt(o -> o[0])));//[A,A的个数]
 for (Map.Entry entry : map.entrySet()) {
 queue.offer(new int[]{entry.getKey(), entry.getValue()});
 }
 //计算答案
 int ans = queue.peek()[0];//第一个最小值一定能整除
 while (!queue.isEmpty()) {
 int[] p = queue.poll();
 int A_Value = p[0], A_Count = p[1];//A_Value有A_Count个
 if (A_Count % (A_Value + 1) != 0) {//不能完全融合,最后剩余的最小的A一定就是当前A,后面不需要融合了
 break;
 } 
 //该项A可以完全融合为A+1
 int[] next = queue.peek();
 if (next == null || next[0] != A_Value + 1) {//不是连续的A项,需要新建这个A项
 next = new int[]{A_Value + 1, A_Count / (A_Value + 1)};
 queue.offer(next);
 } else {//连续A项,直接在它原数量上增加
 next[1] += A_Count / (A_Value + 1);
 }
 ans++;//融合是从A_Value->V_Value+1,所以ans的增加一定也是连续的
 }
 System.out.println(ans);
 sc.close();
}

G小蓝的旅行计划

题目:

题解:

贪心:

假如说现在不考虑油箱的容量(油箱容量无限), 那么最优的策略是 先闷着头往前开, 在发现开不到下一个站点时, 回头看哪个加油站还有油加(如果没有油可以加了, 返回-1), 有多个选择的情况下肯定是选一个最便宜的加油站加油, 加到能走到下一个加油站为止, 并统计花的钱.

public static void main(String[] args){// 9AC 1WR
 Scanner sc = new Scanner(System.in);
 int n = sc.nextInt(), m = sc.nextInt();//n个站点,最大容量m
 int cost = 0;//当前最小消耗
 int curr = m;//当前油量
 Queue queue = new PriorityQueue(Comparator.comparingInt(a -> a[0]));//[油价,剩余可加的油量]按油价排序
 for (int i = 0; i < n; i++) {
 int distance = sc.nextInt();// 到下一个站点的消耗
 if (curr >= distance) {// 可以直接走到下一个站点
 curr -= distance;
 } else {// 走不到下一站
 int need = distance - curr;// 需要加need升油, 只加need升,不多加,下次出发时再加
 while (need > 0) {
 if (queue.isEmpty()) { // 还需要加油,但是没油可加了
 System.out.println(-1);
 return;
 }
 int[] data = queue.poll();// 取已到达过的,还能加油的,最便宜的站点加油
 if (data[1] > need) { // 有超出的油,加need升,剩余的放回队列
 data[1] -= need;
 cost += need * data[0];
 need = 0;
 queue.offer(data);
 } else {// 全部加
 cost += data[1] * data[0];
 need -= data[1];
 }
 }
 curr = 0;
 }
 // 到达下一个站点,加油机会存入队列
 int co = sc.nextInt();// 油价
 int maxAdd = sc.nextInt();// 最多加的油量
 queue.offer(new int[]{co, maxAdd});
 }
 System.out.println(cost);
}

这个代码已经能拿到90分了

线段树: 

现在考虑油箱容量有上限m, 令 limit[i] 表示加油站i的最大加油量, rest[i] 表示到达加油站i的剩余容量

那么在加油站i的最大加油量就等于 min( m-rest[i], limit[i] )

对于之前的贪心算法, 假设现在准备从加油站i-1出发前往下一个加油站i, 但是油量不足, 在i前面的加油站j是最便宜的加油站. 如果直接在加油站j加k升油, 这会导致 rest [ j ~ i-1 ] 都会增加k, 这个操作可能会超出油箱上限, 所以如果想在加油站j加油, 需要求出 rest [ j ~ i-1 ] 的最大值maxOil, 能加的油量就是 min(m - maxOil, limit[j])

对于频繁的区间查询、区间更新的问题, 一般使用线段树/树状数组, 所以可以使用线段树来维护rest数组, 目的是快速地查询 rest [ j ~ i-1 ] 的最大值

static int M = 200001;
static int[] distance = new int[M];// 开往下一个站点的距离
static int[] limit = new int[M];// 加油站的加油上限
static int[] cost = new int[M];// 加油站的加油花费
public static void main(String[] args) {
 Scanner sc = new Scanner(System.in);
 int n = sc.nextInt(), m = sc.nextInt();
 for (int i = 1; i o.cost)));
 SegmentTree tree = new SegmentTree();
 int curr = m;//当前油量
 for (int i = 1; i 

H太阳

题目:

题解:

极角排序:

首先为了方便计算, 把太阳移到原点处, 这样所有的线段都会位于三四象限, 我们把线段的左端点称为起点,右端点称为终点

如果从x的负半轴开始逆时针转180°, 对线段的端点进行扫描, 在遇到线段起点时, 我们可以把线段的y轴加入有序集合set中, 当遇到线段终点时, 将其从集合中移除, 这样就表示出了一条线段的覆盖范围.

有序集合set提供了一个重要信息, 正在被扫描的线段中,最上面的线段是哪一条,令其为set.first.

令ans[i]表示线段i能被照射到, 当遇到某线段端点时, 取被正在扫描的最上面的线段i, 则ans[i]=true

下面举个例子演示执行过程:

(第一个扫描到的线段肯定能被照到,特殊处理ans)首先扫描到线段1的左端点,set为null, 将线段1添加到set中

然后扫描到线段2的左端点,取set.first=线段1, 将线段1标记为true, 将线段2加入set中,它的y轴低于线段1,所以排在线段1后面

扫描线段1的右端点, set.first依然是线段1,重复刚才的操作, 因为是右端点,那么线段1的覆盖范围就结束了,将线段1从set中移除

扫描线段4的左端点, 取set.first=线段2,将线段2标记为true, 线段4加入set

扫描线段4的右端点,  取set.first=线段2,将线段2标记为true, 线段4从set中移除

扫描线段3的左端点, 取set.first=线段2, 将线段2标记为true, 线段3加入set

扫描线段2的右端点,  取set.first=线段3,将线段3标记为true, 线段2从set中移除

public static void main(String[] args) {
 Scanner sc = new Scanner(System.in);
 int n = sc.nextInt();
 long X = sc.nextInt(), Y = sc.nextInt();
 Node[] a = new Node[2 * n + 1];//0位置不存储数据
 for (int i = 1; i {//按斜率排序,(从左到右)
 long l = aa.x * bb.y - aa.y * bb.x;
 if (l > 0) return -1;
 if (l == 0) return 0;
 return 1;
 });
 TreeSet s = new TreeSet(((o1, o2) -> Math.toIntExact(o1.y - o2.y)));//按y轴坐标排序
 boolean[] ans = new boolean[n + 1];//ans[i]表示编号为i的线段可以被照到
 a[0] = new Node(0, 1, 0, 0);
 for (int i = 1; i 

I高塔

题目

题解

不会,dfs过了5个点,15个超时,数据有点大, 不好做, 数组我都开不出来, 写出来了我再回来更

static int MOD = 998244353;
static StreamTokenizer st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static int Int() {
 try {
 st.nextToken();
 } catch (Exception ignored) {
 }
 return (int) st.nval;
}
static long Long() {
 try {
 st.nextToken();
 } catch (Exception ignored) {
 }
 return (long) st.nval;
}
public static void main(String[] args) {
 n = Int();
 long m = Long();
 A = new int[n];
 for (int i = 0; i < n; i++) A[i] = Int();
 System.out.println(f(0, m));
}
static int[] A;
static int n;
/**
 当前进行到了第i回合,剩余remain点能量的方案数
 */
static long f(int i, long remain) {
 if (i == n || remain == 0) return 1;
 long ans = 0;
 for (long Ci = 1; Ci 

J反异或01串

题目:

题解:

0是可以随便添加的, 而1不能随便添加, 所以我们的目的是使用反异或凑1出来

  • 反异或操作最多能使用一次
  • 仅有1^0可以得到1
  • 一个字符串s反异或后一定是一个回文串

假如T中有一串字符是101101, 这是一个偶数长度的回文串, 那么我们只需要对101000做反异或, 101000 ^ 000101 = 101101, 就可以将后半部分变为101, 本来需要4个1, 现在只需要一半

所以, 如果我们找到T中1最多的一个回文串, 假设有x个1, 那么这个回文串我们只需要添加x/2个1, 然后添加适当的0, 进行反异或就可以得到这个回文串

另外中心为1的一个奇数长度回文串, 比如1101011, 它不能使用反异或操作凑1, 因为1^1=0, 取110100进行反异或的话, 会将中心值变为0, 110100 ^ 001011 = 1100011与最终的结果就不符了

所以, 寻找T中最多1的且中心值不为1的回文子串, 假设有x个1, 这x个1只需要添加一半, 答案为count(T,1) - x / 2

对于求回文串可以使用中心拓展算法, 枚举回文串的中心点(可以是一个字符,也可以是两个字符),然后尝试向外拓展, 直到不能拓展,即可找到以中心点为中心的最长的回文串, 我们需要的是最多1的回文串, 最长和最多1并不是很冲突, 只需要在拓展时记录一下1的个数即可,如果1更多则最后更新答案

public static void main(String[] args) {
 //最多1的回文01串,如果是中心是1的奇数长度串,则无效
 Scanner sc = new Scanner(System.in);
 char[] T = sc.next().toCharArray();
 int len = T.length;
 longestPalindrome(T);//求1最多的回文01串,[left,right]
 int count = 0;
 for (int i = 0; i < left; i++) {
 if (T[i] == '1') count++;
 }
 for (int i = right + 1; i < len; i++) {
 if (T[i] == '1') count++;
 }
 System.out.println(count + count1);
}
static int left, right, count1;
static void longestPalindrome(char[] chars) {
 left = -1;
 right = -1;
 count1 = 0;
 for (int i = 0; i < chars.length - 1; i++) {
 //中心拓展算法
 if (chars[i] != '1') extend(chars, i, i);//一个字符作为中心点
 extend(chars, i, i + 1);//两个字符作为中心点
 }
}
static void extend(char[] chars, int i, int j) {
 // 选择一个中间点,向两边查找
 int count = 0;
 while (i >= 0 & j < chars.length && chars[i] == chars[j]) {
 if (chars[i] == '1') count++;
 i--;
 j++;
 }
 //直到不是回文,退出循环
 i++;
 j--;
 if (count > count1) {//记录最多1的区间
 left = i;
 right = j;
 count1 = count;
 }
}

作者:绝迹的星原文地址:https://blog.csdn.net/2203_75477002/article/details/136430870

%s 个评论

要回复文章请先登录注册