目录
前言
官方真题链接
2024年蓝桥杯Java B组省赛真题相比于2023年,总体难度有所下降,题量也从10道下降到了8道。
前两道题都是填空题,可以输出答案即可。
每道题目是独立,建议那题不会看那题。
如果题解看完仍然有所困惑欢迎评论区讨论交流或者@博主🥰🥰
好的废话不多说,一起:
第一题『 报数游戏』
一道简单的找规律题目。
问题描述:
解题思路
简单得找规律题目。题目给得数列,第奇数项是20的倍数,第偶数项时24的倍数。题目要求第 n = 202420242024 n=202420242024 n=202420242024项是多少。这一项是偶数,所以答案一定是24的倍数,并且偶数项的个数和奇数项的个数各占一半,所以最终的答案ans= ( n / 2 ) × 24 (n/2)\times24 (n/2)×24
AC代码:
import java.util.*;
public class Main1 {
public static void main(String[] args) {
long n = 202420242024L;
n /= 2;
long a = 24;
long ans = 0;//记录答案
ans = n * 24;
System.out.println(ans);
}
}
第二题『 类斐波那契循环数』
问题描述
一道中等偏下的模拟题
对于一个有
n
n
n 位的十进制数
N
=
d
1
d
2
d
3
…
d
n
N = d_1d_2d_3\ldots d_n
N=d1d2d3…dn,可以生成一个类斐波那契数列
S
S
S,数列
S
S
S 的前
n
n
n 个数为:
{
S
1
=
d
1
,
S
2
=
d
2
,
S
3
=
d
3
,
…
,
S
n
=
d
n
}
\{S_1 = d_1, S_2 = d_2, S_3 = d_3, \ldots, S_n = d_n\}
{S1=d1,S2=d2,S3=d3,…,Sn=dn}
数列
S
S
S 的第
k
k
k(
k
>
n
k > n
k>n)个数为:
S
k
=
∑
i
=
k
−
n
k
−
1
S
i
S_k = \sum_{i=k-n}^{k-1} S_i
Sk=i=k−n∑k−1Si
如果这个数 N N N 会出现在对应的类斐波那契数列 S S S 中,那么 N N N 就是一个类斐波那契循环数。
例如对于
197
197
197,对应的数列
S
S
S 为:
{
1
,
9
,
7
,
17
,
33
,
57
,
107
,
197
,
…
}
\{1, 9, 7, 17, 33, 57, 107, 197, \ldots\}
{1,9,7,17,33,57,107,197,…}
197 197 197 出现在 S S S 中,所以 197 197 197 是一个类斐波那契循环数。
问题:请问在 0 0 0 至 1 0 7 10^7 107 中,最大的类斐波那契循环数是多少?
答案提交:
这是一道结果填空的题,你只需要算出结果后提交即可。本题的结果为一个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。
解题思路
1、 从最大值
1
e
7
1e7
1e7开始判断,如果判断是类斐波那契数,直接打印然后返回即可。
2、判断一个数是否是类斐波那契数(重点):
-
将给定数字转为分解成单个数字:
toList
方法- 将数字 ( a ) 的每一位提取出来,存储到一个列表中,从高位到低位排列。
- 示例:
197 -> [1, 9, 7]
。 - 用于初始化类斐波那契数列的起始部分。
static List<Integer> toList(int a) { List<Integer> list = new ArrayList<>(); while (a > 0) { int t = a % 10; list.add(t); a /= 10; } Collections.reverse(list); return list; }
-
判断是否为循环数:
isFab
方法- 初始化类斐波那契数列的起始部分为
a
的各位数。 - 持续递推生成新数(类斐波那契数列的下一个数字为前 ( len ) 个数的和),并检查:
- 若生成的数字等于 ( a ),则 ( a ) 是循环数,返回
true
。 - 若生成的数字大于 ( a ),则 ( a ) 不是循环数,返回
false
。
- 若生成的数字等于 ( a ),则 ( a ) 是循环数,返回
static boolean isFab(int a) { ArrayList<Integer> list = new ArrayList<>(toList(a)); int len = list.size(); while (true) { int sum = 0; //注意下标,不要越界了!!! for (int i = list.size() - 1; i > list.size() - 1 - len; i--) { // 计算新数 sum += list.get(i); } if (sum == a) return true; // 若等于 a,返回 true if (sum > a) return false; // 若大于 a,返回 false list.add(sum); // 将新数加入列表 } }
- 初始化类斐波那契数列的起始部分为
-
寻找最大循环数
- 从 ( 10^7 ) 开始向下遍历,逐个判断是否为循环数。
- 遇到第一个循环数时,直接输出并终止程序。
public static void main(String[] args) { int end = (int) 1e7; while (end > 0) { if (isFab(end)) { // 判断是否为类斐波那契循环数 System.out.println(end); // 输出最大循环数 return; } end--; } }
AC代码
import java.util.*;
public class Main1 {
//获取起始序列
static List<Integer> toList(int a){
List<Integer> list=new ArrayList<>();
while(a>0){
int t=a%10;//得到个位数
list.add(t);
a/=10;
}
Collections.reverse(list);//逆置
return list;
}
//判断是否是循环数
static boolean isFab(int a){
ArrayList<Integer> list=new ArrayList<>(toList(a));
int len=list.size();//获得a的位数
while(true){
int sum=0;
//注意下标不要越界!!!
for(int i=list.size()-1;i>list.size()-1-len;i--){//递推类斐波那契数
sum+=list.get(i);
}
if(sum==a)return true;
if(sum>a)return false;
list.add(sum);
}
}
public static void main(String[] args) {
int end=(int)1e7;
while(end>0){
if(isFab(end)){
System.out.println(end);
return;
}
end--;
}
}
}
第三题『 分布式队列』
就是一道简单的模拟题,但是要注意输入格式
问题描述
S S S 学校里一共有 N N N 个节点(编号为 0 0 0 至 N − 1 N-1 N−1,其中 0 0 0 号为主节点),其中只有一个主节点,其余为副节点。
主节点和副节点各自维护着一个队列。当往分布式队列中添加元素时,都是由主节点完成的(每次都会将元素添加到主节点对应的队列的尾部);副节点只负责同步主节点中的队列。可以认为主节点和副节点中的队列是一个长度无限的一维数组,下标为 0 , 1 , 2 , 3 , … 0, 1, 2, 3, \ldots 0,1,2,3,…,同时副节点中的元素的同步顺序和主节点中的元素添加顺序保持一致。
由于副本的同步速度各异,为了保障数据的一致性,元素添加到主节点后,需要同步到所有的副节点后,才具有可见性。
给出一个分布式队列的运行状态,所有的操作都按输入顺序执行。你需要回答在某个时刻,队列中有多少个元素具有可见性。
输入格式
第一行包含一个整数 N N N,表示节点个数。
接下来包含多行输入,每一行包含一个操作,操作类型共有以下三种:
- add element:表示这是一个添加操作,将元素 e l e m e n t element element 添加到队列中;
- sync follower_id:表示这是一个同步操作,编号为 f o l l o w e r _ i d follower\_id follower_id 的副节点会从主节点中同步下一个自己缺失的元素;
- query:查询操作,询问当前分布式队列中有多少个元素具有可见性。
输出格式
对于每一个 query 操作,输出一行,包含一个整数表示答案。
样例输入
3
add 1
add 2
query
add 1
sync 1
sync 1
sync 2
query
sync 1
query
sync 2
sync 2
sync 1
query
样例输出
0
1
1
3
样例说明
-
第一组操作:
- 添加元素 1 1 1 和 2 2 2 到主节点队列。
- 执行 query 时,所有副节点尚未同步任何元素,因此答案为 0 0 0。
-
第二组操作:
- 添加元素 1 1 1 到主节点队列。
- 副节点 1 1 1 同步两次,副节点 2 2 2 同步一次。
- 执行 query 时,只有第一个元素 1 1 1 被所有副节点同步,因此答案为 1 1 1。
-
第三组操作:
- 副节点 1 1 1 再次同步一次。
- 执行 query 时,仍然只有第一个元素 1 1 1 被所有副节点同步,答案为 1 1 1。
-
第四组操作:
- 副节点 2 2 2 同步两次,副节点 1 1 1 同步一次。
- 执行 query 时,前三个元素 1 , 2 , 1 1, 2, 1 1,2,1 都被所有副节点同步,答案为 3 3 3。
评测用例规模与约定
- 对于 30% 的评测用例,保证输入的操作数 ≤ 100 \leq 100 ≤100。
- 对于 100% 的评测用例,保证:
- 1 ≤ N ≤ 10 1 \leq N \leq 10 1≤N≤10,
- 1 ≤ f o l l o w e r _ i d < N 1 \leq follower\_id < N 1≤follower_id<N,
- 0 ≤ e l e m e n t ≤ 1 0 5 0 \leq element \leq 10^5 0≤element≤105。
解题思路
本题考察对分布式队列中元素同步状态的模拟与维护。为了高效地回答每次 query 操作,我们需要跟踪每个副节点已经同步到主节点队列的元素数量,并找出所有副节点中同步到的最少元素数量,这个数量即为所有副节点都已经同步的元素数量。
具体步骤如下:
-
初始化:
- 维护主节点队列的长度
mainQueueSize
,初始为 0 0 0。 - 对于每个副节点,维护一个数组
followerSync
,记录每个副节点已经同步到主节点队列的元素数量,初始均为 0 0 0。
- 维护主节点队列的长度
-
处理操作:
- add element:
- 主节点队列长度
mainQueueSize
增加 1 1 1。
- 主节点队列长度
- sync follower_id:
- 对应副节点的同步数量
followerSync[follower_id]
增加 1 1 1,但不得超过mainQueueSize
!!
- 对应副节点的同步数量
- query:
- 找出所有副节点中已经同步的最少元素数量
minSync
。 - 输出
minSync
,即当前队列中具有可见性的元素数量。
- 找出所有副节点中已经同步的最少元素数量
- add element:
AC代码
以下是 Java 的实现代码:
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
sc.nextLine();
// index[i] 表示第 i 个节点已同步的最新元素下标,初始为 -1
int[] index = new int[n];
Arrays.fill(index, -1);
while (sc.hasNextLine()) {
String line = sc.nextLine();
String[] tokens = line.split(" ");
String s = tokens[0];
if ("query".equals(s)) {
int min = Integer.MAX_VALUE;
for (int i : index) {
if (min > i) min = i;
}
System.out.println(min + 1);
} else if ("add".equals(s)) {
// 添加元素,主节点的索引加一
int element = Integer.parseInt(tokens[1]);
index[0]++;
} else if ("sync".equals(s)) {
// 同步操作,只有当副节点的索引小于主节点时才能同步
int followerId = Integer.parseInt(tokens[1]);
if (followerId >= 1 && followerId < n) {
if (index[followerId] < index[0]) {
index[followerId]++;
}
}
}
}
}
}
第四题『 食堂』
个人认为,这道题目还是比较难的贪心题,代码好写,但很思维很难。
问题描述
S S S 学校里一共有 a 2 a_2 a2 个两人寝、 a 3 a_3 a3 个三人寝、 a 4 a_4 a4 个四人寝,而食堂里有 b 4 b_4 b4 个四人桌和 b 6 b_6 b6 个六人桌。学校想要安排学生们在食堂用餐,并且满足每个寝室里的同学都在同一桌就坐,请问这个食堂最多同时满足多少同学用餐?
输入格式
采用多组数据输入。
输入共 q + 1 q + 1 q+1 行。
- 第一行为一个正整数 q q q,表示数据组数。
- 后面 q q q 行,每行五个非负整数 a 2 , a 3 , a 4 , b 4 , b 6 a_2, a_3, a_4, b_4, b_6 a2,a3,a4,b4,b6,表示一组数据。
输出格式
输出共 q q q 行,每行一个整数表示对应输入数据的答案。
样例输入
2
3 0 1 0 1
0 2 2 1 1
样例输出
6
10
样例说明
-
第一组数据:有 3 3 3 个两人寝、 0 0 0 个三人寝、 1 1 1 个四人寝,食堂有 0 0 0 个四人桌和 1 1 1 个六人桌。
由于只有一个六人桌,最多可以安排三个两人寝的同学用餐,总人数为 3 × 2 = 6 3 \times 2 = 6 3×2=6。
-
第二组数据:有 0 0 0 个两人寝、 2 2 2 个三人寝、 2 2 2 个四人寝,食堂有 1 1 1 个四人桌和 1 1 1 个六人桌。
可以将两个三人寝安排在六人桌上,再将一个四人寝安排在四人桌上,总人数为 2 × 3 + 4 = 10 2 \times 3 + 4 = 10 2×3+4=10。
评测用例规模与约定
- 对于 20% 的评测用例,保证 1 ≤ a 2 + a 3 + a 4 ≤ 8 1 \leq a_2 + a_3 + a_4 \leq 8 1≤a2+a3+a4≤8。
- 对于 100% 的评测用例,保证:
- 1 ≤ q ≤ 100 1 \leq q \leq 100 1≤q≤100,
- b 4 + b 6 ≤ a 2 + a 3 + a 4 ≤ 100 b_4 + b_6 \leq a_2 + a_3 + a_4 \leq 100 b4+b6≤a2+a3+a4≤100。
解题思路
我们需要合理分配寝室成员到食堂的餐桌上,最大化同时用餐的学生人数。
由于每个寝室的同学必须坐在同一张桌子上,所以需要设计一种贪心策略,以尽可能高效地利用食堂的餐桌资源。
贪心的核心优先级:
1. 满桌优先
首先一定是让桌子都塞满人,这样利用率才能最大化。
2. 小桌优先
先把人放到小桌中,因为小桌(4人桌)的人数组合搭配不够灵活,大桌(6人桌)可以有更多中组合搭配。
3. 人多优先
寝室人多的先安置,因为人多不灵活,如果后面不能安置了,那么会损失很多人,利用率就下降了。
4. 空少优先
当一个桌子不得不空出位置时,尽量少空出位置。
具体贪心策略步骤
下面的贪心步骤不是固定的,只要都严格满足核心的贪心优先级即可:
- 安排四人寝到四人桌:
- 尽可能将四人寝安排到四人桌,因为这样可以完全利用四人桌的容量。
- 安排两人寝到四人桌:
- 将两个两人寝安排到一个四人桌,充分利用桌子的容量。
- 安排四人寝和两人寝到六人桌:
- 将一个四人寝和一个两人寝组合,安排到六人桌上,利用剩余的两人空间。
- 安排三个三人寝到六人桌:
- 将两个三人寝安排到一个六人桌上,尽可能填满桌子。
- 安排两人寝到六人桌:
- 将三个两人寝安排到一个六人桌上,充分利用空间。
- 安排三人寝和两人寝到六人桌:
- 将一个三人寝和一个两人寝组合,安排到六人桌上,利用剩余的两人空间。
- 安排三人寝到四人桌:
- 如果六人桌已满,将一个三人寝安排到四人桌上,利用桌子的一部分空间。
- 安排四人寝到六人桌:
- 将剩余的四人寝安排到六人桌上,利用剩余的两人空间。
- 安排两个两人寝到六人桌:
- 将两个两人寝安排到六人桌上,利用剩余的两人空间。
- 安排一个两人寝到四人桌:
- 最后,将剩余的一个两人寝安排到四人桌上。
- 安排一个三人寝到六人桌:
- 将剩余的三人寝安排到六人桌上,利用桌子的一部分空间。
- 安排一个两人寝到六人桌:
- 将剩余的两人寝安排到六人桌上,利用桌子的一部分空间。
AC代码:
以下是 Java 的实现代码:
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int q = sc.nextInt();
while (q-- > 0) {
int a2 = sc.nextInt(); // 两人寝
int a3 = sc.nextInt(); // 三人寝
int a4 = sc.nextInt(); // 四人寝
int b4 = sc.nextInt(); // 四人桌
int b6 = sc.nextInt(); // 六人桌
long ans = 0;
int t;
// 1. 一个四人寝室放到四人桌
t = Math.min(a4, b4);
ans += t * 4;
a4 -= t;
b4 -= t;
// 2. 两个两人寝放到四人桌
t = Math.min(a2 / 2, b4);
ans += t * 4;
a2 -= t * 2;
b4 -= t;
// 3. 一个四人寝 + 一个两人寝放到六人桌
t = Math.min(b6, Math.min(a4, a2));
ans += t * 6;
a4 -= t;
a2 -= t;
b6 -= t;
// 4. 两个三人寝放到六人桌
t = Math.min(b6, a3 / 2);
ans += t * 6;
a3 -= t * 2;
b6 -= t;
// 5. 三个两人寝放到六人桌
t = Math.min(b6, a2 / 3);
ans += t * 6;
a2 -= t * 3;
b6 -= t;
// 6. 一个三人寝 + 一个两人寝放到六人桌
t = Math.min(b6, Math.min(a3, a2));
ans += t * 5;
a3 -= t;
a2 -= t;
b6 -= t;
// 7. 一个三人寝放到四人桌
t = Math.min(b4, a3);
ans += t * 3;
a3 -= t;
b4 -= t;
// 8. 一个四人寝放到六人桌
t = Math.min(b6, a4);
ans += t * 4;
a4 -= t;
b6 -= t;
// 9. 两个两人寝放到六人桌
t = Math.min(b6, a2 / 2);
ans += t * 4;
a2 -= t * 2;
b6 -= t;
// 10. 一个两人寝放到四人桌
t = Math.min(b4, a2);
ans += t * 2;
a2 -= t;
b4 -= t;
// 11. 一个三人寝放到六人桌
t = Math.min(b6, a3);
ans += t * 3;
a3 -= t;
b6 -= t;
// 12. 一个两人寝放到六人桌
t = Math.min(b6, a2);
ans += t * 2;
a2 -= t;
b6 -= t;
System.out.println(ans);
}
}
}
第五题『 最优分组』
这道题目其实纯考数学,数学中的期望。
问题描述
小蓝开了一家宠物店,最近有一种 X X X 病毒在动物之间进行传染。为了以防万一,小蓝打算购买测试剂对自己的宠物进行病毒感染测试。
为了减少使用的测试剂数目,小蓝想到了一个好方法:将 N N N 个宠物平均分为若干组,使得每组恰好有 K K K 只宠物。这样对同一组的宠物进行采样并混合后用一个试剂进行检测。如果测试结果为阴性,则说明组内宠物都未感染 X X X病毒;如果是阳性的话,则需要对组内所有 K K K 只宠物单独检测,需要再消耗 K K K 支测试剂(当 K = 1 K = 1 K=1 时,就没必要再次进行单独检测了,因为组内只有一只宠物,一次检测便能确认答案)。
现在我们已知小蓝的宠物被感染的概率为 p p p,请问 K K K 应该取值为多少才能使得期望的测试剂的消耗数目最少?如果有多个答案,输出最小的 K K K。
输入格式
- 第一行包含一个整数 N N N。
- 第二行包含一个浮点数 p p p。
输出格式
输出一行,一个整数 K K K 表示答案。
样例输入
1000
0.05
样例输出
5
评测用例规模与约定
- 对于 30% 的评测用例,保证 1 ≤ N ≤ 10 1 \leq N \leq 10 1≤N≤10
- 对于 60% 的评测用例,保证 1 ≤ N ≤ 1000 1 \leq N \leq 1000 1≤N≤1000
- 对于 100% 的评测用例,保证:
- 1 ≤ N ≤ 1 0 6 1 \leq N \leq 10^6 1≤N≤106
- 0 ≤ p ≤ 1 0 \leq p \leq 1 0≤p≤1
解题思路
本题可以从期望的角度切入并解决。
期望值,也称为数学期望,是随机变量所有可能取值的概率加权平均值。
对于离散型随机变量 X X X,其期望值 E ( X ) E(X) E(X) 计算公式如下:
E ( X ) = ∑ i = 1 n x i ⋅ p i E(X) = \sum_{i=1}^{n} x_i \cdot p_i E(X)=i=1∑nxi⋅pi
其中:
- x i x_i xi 是随机变量 X X X 的第 i i i 个可能取值。
- p i p_i pi 是随机变量 X X X 取值为 x i x_i xi 的概率。
在本题中,题目要求我们将 N N N 个宠物分解为若干组,且每组的宠物个数均为 K K K。这意味着 K K K 必然是 N N N 的约数。于是,我们可以先求出 N N N 的所有因子,并将所有因子进行存储、排序。然后,我们只需计算出 K K K 在不同取值下对应的测试剂的消耗数目的期望值,并进行比较,即可完成求解(显然,计算出的期望值越小,所对应的 K K K 就越优)。
那么,当我们将宠物分为了 N K \frac{N}{K} KN 组,每组有 K K K 个宠物后,如何计算期望值呢?
简单分析如下:
当我们将宠物分为了 N K \frac{N}{K} KN 组后,每组都至少需要消耗一个测试剂进行检测。检测结果只分为两种:
- 所有 K K K 只宠物都没有感染病毒。
- 至少有 1 1 1 只宠物感染了病毒。
根据宠物感染病毒的概率 p p p,我们可以求得:
-
所有宠物都没有感染病毒的概率为 ( 1 − p ) K (1 - p)^K (1−p)K
-
至少有一只宠物感染病毒的概率为 1 − ( 1 − p ) K 1 - (1 - p)^K 1−(1−p)K
针对一组的期望进行研究,有两类检测情况:
- 如果所有宠物都没有感染病毒,则我们无需再使用测试剂,一共需要使用的测试剂的个数为 1 1 1。
- 如果至少有一只宠物感染病毒,则我们还需使用 K K K 个测试剂,即一共需要使用的测试剂的个数为 1 + K 1 + K 1+K。
于是, K K K 对应的期望值即为:
e = [ ( 1 − p ) K × 1 + ( 1 − ( 1 − p ) K ) × ( 1 + K ) ] e = \left[(1 - p)^K \times 1 + \left(1 - (1 - p)^K\right) \times (1 + K)\right] e=[(1−p)K×1+(1−(1−p)K)×(1+K)]
扩展到总所有的试剂只需要乘以总组数
N
÷
K
N \div K
N÷K:
E
=
[
(
1
−
p
)
K
×
1
+
(
1
−
(
1
−
p
)
K
)
×
(
1
+
K
)
]
×
N
K
E = \left[(1 - p)^K \times 1 + \left(1 - (1 - p)^K\right) \times (1 + K)\right] \times \frac{N}{K}
E=[(1−p)K×1+(1−(1−p)K)×(1+K)]×KN
但有一个情况特殊! 当
K
=
1
K = 1
K=1 时,就是全部宠物一个个测,一共需要使用的测试剂个数应为
N
N
N。
所以最终的期望E:
- K不等于1:
E = [ ( 1 − p ) K × 1 + ( 1 − ( 1 − p ) K ) × ( 1 + K ) ] × N K E = \left[(1 - p)^K \times 1 + \left(1 - (1 - p)^K\right) \times (1 + K)\right] \times \frac{N}{K} E=[(1−p)K×1+(1−(1−p)K)×(1+K)]×KN - K等于1:
E = N E=N E=N(总试剂数)
综上,我们只需比较 变量 K K K 在不同取值下的期望值,求出最小期望值所对应的 K K K 的取值,即可完成求解。
AC代码:
以下是 Java 的实现代码:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Scanner;
public class Main {
static int N, ans;
static double p, min = Double.MAX_VALUE / 2; // min是可能的最小期望
static ArrayList<Integer> factors = new ArrayList<>();
// 计算不同K值对应的期望
static double calculateExpected(int K) {
if (K == 1) return N; // 每一个都检测,所以直接是N的消耗
double probabilityAllNegative = Math.pow(1 - p, K);
double expectedTestsPerGroup = probabilityAllNegative * 1 + (1 - probabilityAllNegative) * (1 + K);
return expectedTestsPerGroup * ((double) N / K);
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
N = sc.nextInt();
p = sc.nextDouble();
// 计算所有的约数K
for (int i = 1; i <= Math.sqrt(N); i++) {
if (N % i == 0) {
factors.add(i);
if (N / i != i) {
factors.add(N / i);
}
}
}
// 对K进行升序
Collections.sort(factors);
// 枚举所有可能的约数K,寻找最小期望对应的K
for (int K : factors) {
double currentExpected = calculateExpected(K);
if (currentExpected < min) {
ans = K;
min = currentExpected;
}
}
System.out.println(ans);
}
}
代码说明
-
因数计算:首先,我们计算出 N N N 的所有因数 K K K,这些因数即为可能的分组大小。通过遍历从 1 1 1 到 N \sqrt{N} N 的整数,找到所有能整除 N N N 的数,并将其添加到因数列表中。
-
期望值计算:对于每一个可能的 K K K,计算其对应的期望测试剂消耗数目。具体公式为:
E = [ ( 1 − p ) K × 1 + ( 1 − ( 1 − p ) K ) × ( 1 + K ) ] × N K E = \left[(1 - p)^K \times 1 + \left(1 - (1 - p)^K\right) \times (1 + K)\right] \times \frac{N}{K} E=[(1−p)K×1+(1−(1−p)K)×(1+K)]×KN
- 当 K = 1 K = 1 K=1 时,期望值直接为 N N N,因为每只宠物都直接单独检测了呀。
- 对于 K > 1 K > 1 K>1,计算每组的期望测试剂消耗,然后乘以总组数 N K \frac{N}{K} KN。
-
寻找最优K:遍历所有可能的 K K K,计算对应的期望值,记录最小期望值及其对应的 K K K。如果有多个 K K K 对应相同的最小期望值,选择其中最小的 K K K。
-
输出结果:最后,输出最优的 K K K。
第六题_『 星际旅行』
问题描述
小明计划在国庆节期间进行星际旅行。他所在的星系共有 n n n 个星球,通过 m m m 道双向传送门相连。每道传送门连接两颗不同的星球,且任意两颗星球之间最多只有一条传送门。
小明购买了一个包含 Q Q Q 个盲盒的“旅游盲盒”。每个盲盒定义了一个旅行方案,具体包括:
起始星球
x
i
x_i
xi
最多使用传送门的次数
y
i
y_i
yi
在每个方案中,小明可以从起始星球出发,使用不超过
y
i
y_i
yi 次传送门到达其他星球。小明关心的是,在每个方案中可以旅行到的不同星球的数量,并希望计算这些数量的期望值。
输入格式
-
输入共 m + Q + 1 m + Q + 1 m+Q+1 行。
-
第一行包含三个正整数 n n n, m m m, Q Q Q。
-
接下来的 m m m 行,每行两个正整数 a i a_i ai, b i b_i bi,表示传送门连接星球 a i a_i ai 和 b i b_i bi。
-
再接下来的 Q Q Q 行,每行两个整数 x i x_i xi, y i y_i yi,表示第 i i i 个盲盒的旅行方案。
输出格式
输出一个浮点数,表示期望可达星球数量,保留两位小数。
样例输入
3 2 3
1 2
2 3
2 1
2 0
1 1
样例输出
2.00
样例说明
- 第一个盲盒:起始星球为 2,最多使用 1 次传送门,可以到达星球 1、2、3,共 3 个星球。
- 第二个盲盒:起始星球为 2,最多使用 0 次传送门,只能留在星球 2,共 1 个星球。
- 第三个盲盒:起始星球为 1,最多使用 1 次传送门,可以到达星球 1、2,共 2 个星球。
期望值为 ( 3 + 1 + 2 3 = 2.00 ) ( \frac{3 + 1 + 2}{3} = 2.00 ) (33+1+2=2.00)。
解题思路
我们可以把这道问题转化成:
1. 从某一个点开始,求解到达其他地方的最短路径。
2. 星球数量代表结点数。两个星球是否有传送门代表两个结点是否有边
3. 传送门的使用次数就是边的消耗次数
因此,我们只用计算任意两个边的最短路径即可。如果最短路径小于等于传送门最多使用次数,那么这个星球就可以到达。最后,计算所有可以到达的星球的期望即可。
计算最短路径两个经典算法:floyd和dijkstra。因为样例规模:
结点数最大只有1000,所以用floyd也可以,因为它好写,只用三层for循环即可。
AC代码:
import java.util.*;
public class Main {
static long INF = Long.MAX_VALUE / 2;
public static void main(String[] args) {
long[][] g = new long[1010][1010];
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int m = in.nextInt();
int q = in.nextInt();
for(int i = 0; i < g.length; i++)
Arrays.fill(g[i], INF);
for(int i = 0; i < g.length; i++) g[i][i] = 0;
// 建立邻接矩阵
for(int i = 0; i < m; i++) {
int u = in.nextInt();
int v = in.nextInt();
g[u][v] = 1;
g[v][u] = 1;
}
// 更新最短路径(Floyd-Warshall)
for(int k = 1; k <= n; k++) {
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= n; j++) {
if(g[i][k] != INF && g[k][j] != INF) {
if(g[i][j] > g[i][k] + g[k][j])
g[i][j] = g[i][k] + g[k][j];
}
}
}
}
long ans = 0;
// 处理 Q 次查询
for(int i = 0; i < q; i++) {
int start = in.nextInt();
int k = in.nextInt();
for(int j = 1; j <= n; j++) {
if(g[start][j] <= k) ans++;
}
}
System.out.printf("%.2f", 1.0 * ans / q);
}
}
代码解析
-
邻接矩阵初始化:使用一个二维数组
g
存储星球之间的距离,初始值设为无穷大(INF
),并将对角线元素设为 0。 -
建立图结构:根据输入的传送门信息,更新邻接矩阵中对应的距离为 1。
-
Floyd-Warshall 算法:通过三重循环迭代更新所有星球对之间的最短路径。
-
处理查询:对于每个盲盒的查询,遍历对应星球的最短路径数组,统计不超过 ( y_i ) 的星球数量,并累加到总和
ans
中。 -
计算并输出期望值:将总和
ans
除以查询次数Q
,并格式化输出保留两位小数。
第七题_『俄罗斯方块』
问题描述
俄罗斯方块是一款经典的益智游戏,其中包含多种由四个小方块组成的图案(称为“tetrominoes”)。在本题中,我们关注四种经典图案:L、I、T、S。这些图案可以任意旋转,但不允许翻转,且每种图案必须独立存在,不能共用同一个小方块。
给定一个大小为 ( N × N ) ( N \times N ) (N×N) 的格子图,每个格子上标有 0 或 1,其中 1 表示该格子有一个小方块,0 则表示空白。任务是判断是否能在该格子图中找到上述四种方块图案,使得每种图案的所有小方块都对应格子图中的 1,并且不同图案之间不重叠。
输入格式
- 第一行一个整数 ( T ),表示有 ( T ) 组数据。
- 每组数据的第一行包含一个整数 ( N ),表示格子图大小。
- 接下来 ( N ) 行,每行包含 ( N ) 个 0 或 1,表示格子布局。
输出格式
对于每组数据,输出一行 “Yes” 或 “No”,表示是否存在满足条件的图案摆放方式。
样例输入
2
5
1 1 1 1 1
1 0 1 1 0
1 0 0 0 1
1 0 1 0 1
1 1 1 1 1
5
1 0 0 1 1
1 1 1 1 1
1 1 1 1 0
1 1 1 0 1
0 1 1 1 1
样例输出
No
Yes
样例解释
第一组数据
5
1 1 1 1 1
1 0 1 1 0
1 0 0 0 1
1 0 1 0 1
1 1 1 1 1
对于这组数据,无法找到四种图形(L、I、T、S)的非重叠摆放方式,因此输出 “No”。
第二组数据
5
1 0 0 1 1
1 1 1 1 1
1 1 1 1 0
1 1 1 0 1
0 1 1 1 1
对于这组数据,一种可行的摆放方式如下:
1 0 0 1 1
L S T T T
L S S T 0
L L S 0 1
0 I I I I
因此输出 “Yes”。
解题思路
其实就是暴力枚举,因为问题规模非常小。我们只需要枚举图形的排列组合,然后判断是否在图中同时出现即可。
具体步骤如下:
-
定义图形旋转状态:对于每种方块图案(L、I、T、S),预先定义所有可能的旋转状态。旋转角度为 0°, 90°, 180°, 270°。
-
枚举图形组合:由于每种图形有多个旋转状态,总共有 ( 4 × 2 × 4 × 2 = 64 ) ( 4 \times 2 \times 4 \times 2 = 64 ) (4×2×4×2=64) 种可能的组合方式(L 有 4 种,I 有 2 种,T 有 4 种,S 有 2 种)。
-
深度优先搜索(DFS):
- 从格子图的左上角开始,尝试依次放置四种图形。
- 对于每个图形,尝试所有可能的旋转状态,并检查其在当前格子图中的合法性(即所有小方块对应的位置为 1 且未被其他图形占用)。
- 使用回溯法,当某一分支无法满足条件时,回退到上一步,尝试其他可能。
-
剪枝优化:
- 当某一图形无法放置时,立即回退,避免不必要的计算。
- 若已经找到一个可行解,立即停止搜索,输出结果。
-
结果判断:
- 若找到一种图形摆放方式满足条件,输出 “Yes”。
- 否则,输出 “No”。
代码实现
以下是基于上述思路的 Java 实现:
import java.util.*;
public class Main {
// 定义四种方块(L, I, T, S)的所有可能旋转状态
//它类似的与dx dy 一定要细心设置,不然会出错
private static int[][][][] lits = {
// L 形状的 4 种旋转状态
{
{{0, 0}, {0, -1}, {-1, -1}, {-2, -1}}, // 0 度旋转
{{0, 0}, {-1, 0}, {-1, 1}, {-1, 2}}, // 90 度旋转
{{0, 0}, {-1, 0}, {-2, 0}, {-2, -1}}, // 180 度旋转
{{0, 0}, {0, -1}, {0, -2}, {-1, 0}} // 270 度旋转
},
// I 形状的 2 种旋转状态
{
{{0, 0}, {-1, 0}, {-2, 0}, {-3, 0}}, // 0 度旋转(竖直)
{{0, 0}, {0, -1}, {0, -2}, {0, -3}} // 90 度旋转(水平)
},
// T 形状的 4 种旋转状态
{
{{0, 0}, {-1, 0}, {-1, -1}, {-1, 1}}, // 0 度旋转
{{0, 0}, {-1, 0}, {-2, 0}, {-1, -1}}, // 90 度旋转
{{0, 0}, {0, -1}, {0, -2}, {-1, -1}}, // 180 度旋转
{{0, 0}, {-1, 0}, {-2, 0}, {-1, 1}} // 270 度旋转
},
// S 形状的 2 种旋转状态
{
{{0, 0}, {0, -1}, {-1, 0}, {-1, 1}}, // 0 度旋转
{{0, 0}, {-1, 0}, {-1, -1}, {-2, -1}} // 90 度旋转
}
};
static int[] pos = new int[4]; // 记录每种图形的旋转状态
static boolean flag = false; // 标记是否找到满足条件的摆放方式
static boolean[] vis = new boolean[4]; // 标记每种图形是否已被放置
// 判断是否所有图形都已被放置
static boolean success(){
for(boolean t:vis) if(!t) return false;
return true;
}
// 填充或撤销图形
static void fill(int x, int y, int k, int[][] map, int val){
for(int i = 0; i < 4; i++){
int xx = x + lits[k][pos[k]][i][0];
int yy = y + lits[k][pos[k]][i][1];
map[xx][yy] = val; // 标记为 val(2 表示已占用,1 表示恢复)
}
}
// 检查图形是否可以放置
static boolean check(int x, int y, int k, int[][] map){
int N = map.length;
for(int i = 0; i < 4; i++){
int xx = x + lits[k][pos[k]][i][0];
int yy = y + lits[k][pos[k]][i][1];
if(xx >= N || xx < 0 || yy >= N || yy < 0 || map[xx][yy] != 1) return false;
}
return true;
}
// 深度优先搜索尝试放置图形
static void dfs(int x, int y, int[][] map){
int N = map.length;
if(success()){
flag = true;
return;
}
if(flag || x == N) return;
if(y == N){
dfs(x + 1, 0, map);
return;
}
if(map[x][y] != 1){
dfs(x, y + 1, map);
return;
}
for(int i = 0; i < 4; i++){
if(!vis[i] && check(x, y, i, map)){
fill(x, y, i, map, 2); // 放置图形
vis[i] = true;
dfs(x, y + 1, map);
if(flag) return;
fill(x, y, i, map, 1); // 撤销放置
vis[i] = false;
}
}
dfs(x, y + 1, map);
}
// 枚举所有图形的旋转组合
static boolean solve(int[][] map){
for(int l = 0; l < 4; l++){
for(int i = 0; i < 2; i++){
for(int t = 0; t < 4; t++){
for(int s = 0; s < 2; s++){
Arrays.fill(vis, false);
flag = false;
pos[0] = l;
pos[1] = i;
pos[2] = t;
pos[3] = s;
dfs(0, 0, map);
if(flag) return true;
}
}
}
}
return false;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int t = sc.nextInt();
while(t-- > 0){
int n = sc.nextInt();
int[][] g = new int[n][n];
for(int i = 0; i < n; i++) {
for(int j = 0; j < n; j++) {
g[i][j] = sc.nextInt();
}
}
boolean ans = solve(g);
System.out.println(ans ? "Yes" : "No");
}
}
}
总结
思路并不难想到,枚举所有可能的组合,拿着可能组合去判断是否在图中同时出现即可,困难点主要在于代码的编写上。
第八题_『拼十字』
问题描述
在神秘的古老森林中,有一个被称为 「拼十字」 的遗迹。现在给出 N N N 个矩形,第 i i i 个矩形的长度和宽度分别为 l i l_i li 和 w i w_i wi,颜色 c i c_i ci 为红( 0 0 0)、黄( 1 1 1)、蓝( 2 2 2)中的一种。
小蓝想知道在这 N N N 个矩形中,有多少对矩形可以「拼十字」。两个矩形可以「拼十字」的充要条件是:
- 两个矩形的颜色不同;
- 矩形 1 1 1 的长度严格大于矩形 2 2 2 的长度,且矩形 1 1 1 的宽度严格小于矩形 2 2 2 的宽度。
注意:矩形的长度和宽度属性是固定的,不可以通过旋转矩形而改变。
输入格式
第一行一个整数 N N N,表示有 N N N 个矩形。
接下来 N N N 行,每行输入三个整数 l l l、 w w w、 c c c,表示一个矩形的长、宽和颜色。
输出格式
输出一个整数表示答案。由于答案可能很大,所以需要将答案对 1 0 9 + 7 10^9+7 109+7 取模后输出。
样例输入
5
1 10 0
6 6 0
8 6 1
6 10 0
1 2 1
样例输出
2
样例说明
第 3 3 3 个矩形可以和第 1 1 1 个矩形拼十字,第 3 3 3 个矩形也可以和第 4 4 4 个矩形拼十字。所以一共有两对矩形可以拼十字,答案为 2 2 2。
评测用例规模与约定
对于
30
%
30\%
30% 的评测用例:
1
≤
N
≤
5000
1 \leq N \leq 5000
1≤N≤5000
对于
100
%
100\%
100% 的评测用例:
1
≤
N
≤
1
0
5
1
≤
l
,
w
≤
1
0
5
0
≤
c
≤
2
1 \leq N \leq 10^5 \\ 1 \leq l, w \leq 10^5 \\ 0 \leq c \leq 2
1≤N≤1051≤l,w≤1050≤c≤2
解题思路
问题分析
我们需要统计满足以下条件的矩形对 ( i , j ) (i, j) (i,j) 的数量:
- 颜色不同: c i ≠ c j c_i \ne c_j ci=cj
- 长度关系: l i > l j l_i > l_j li>lj
- 宽度关系: w i < w j w_i < w_j wi<wj
直接遍历所有可能的矩形对会导致 O ( N 2 ) O(N^2) O(N2) 的时间复杂度,无法通过数据量较大的测试用例。因此,我们需要设计一个高效的算法,在 O ( N log N ) O(N \log N) O(NlogN) 的时间内完成统计。
算法设计
1. 排序预处理
定义一个三维数组来存储一个矩形的信息,用List把所有矩形信息存储起来:
存储所有矩形信息
List<int[]> arr = new ArrayList<>();
//读入所有的矩形
for (int i = 0; i < n; i++) {
int l = sc.nextInt();//长
int w = sc.nextInt();//宽
int c = sc.nextInt();//颜色
arr.add(new int[]{l, w, c});
}
将所有矩形按照 优先长度 l l l 升序,其次宽度 w w w 升序进行排序:
//对矩形进行排序,l升序,然后w升序(下面使用的lambda表达式)
arr.sort((o1, o2) -> {
if (o1[0] != o2[0]) return Integer.compare(o1[0], o2[0]);
else return Integer.compare(o1[1], o2[1]);
});
为什么长度和宽度都要升序排序?? 约束条件不是,长度要严格大于,宽度要严格小于吗?
对于这个问题,现在还不好回答,等一下会做出解释。
2. 树状数组维护宽度信息
为了在遍历的过程中快速统计满足宽度条件的矩形数量,我们使用 树状数组(Fenwick Tree) 来维护宽度的信息:
//FenwickTree类的构建(起始就是写一个树状数组)
static class FenwickTree {
int n;//树状数组的长度(n+1)
int[] tree;//树状数组 tree[i]的含义是宽度为i的矩形的数量
//构造方法
public FenwickTree(int n) {
this.n = n;
tree = new int[n + 1];
}
//计算管理区间,i是下标
int lowbit(int i) {
return (-i) & i;
}
//在树状数组的下标i位置增加元素val
//含义:更新宽度为i的矩形的数量
void add(int i, int val) {
//i位置更新,祖宗也要更新
for (; i <= n; i += lowbit(i)) {
tree[i] += val;
}
}
//计算前缀和
//含义:计算宽度小于等于i的矩形的数量
int preSum(int i) {
int ret = 0;
//统计所有子节点
for (; i > 0; i -= lowbit(i)) {
ret += tree[i];
}
return ret;
}
//计算区间合[l,r]
//含义:rangeSum(l,r)宽度大于等于l的矩形的数量
int rangeSum(int l, int r) {
return preSum(r) - preSum(l - 1);
}
}
3. 按颜色分组
由于矩形的颜色只有
3
3
3 种,我们可以为每种颜色建立一个独立的树状数组。
这样可以在统计时,直接排除与当前矩形颜色相同的矩形:
//建立三种颜色对应的树状数组
FenwickTree[] tree = new FenwickTree[3];
//每个颜色的树状数组的长度设置成一个较大值,因为其下标的含义是矩形宽度,
//所以maxn我们设置成题目给定的最大矩形宽度+10(冗余)即可
for (int i = 0; i < 3; i++) tree[i] = new FenwickTree(maxn);
4. 遍历并统计
用一个变量ans统计最终的答案,遍历排序后的矩形列表arr
,对于每个矩形
i
i
i:
- 查询:对于所有与当前矩形颜色不同的颜色 c c c,在对应的树状数组中查询宽度严格大于 w i w_i wi 的矩形数量cnt.
- 累加:将cnt累加到ans中,然后取模(因为答案可能很大,题目要求取模)。
- 更新树状数组: 把当前枚举到的矩形,添加到对应颜色的树状数组中。
//然后枚举排序好的矩形列表arr
for (int[] a : arr) {
int l = a[0];//矩形的长度
int w = a[1];//矩形的宽度
int c = a[2];//矩形的颜色
for (int i = 0; i < 3; i++) {
if (c == i) continue;//相同颜色被排除掉
//要求宽度严格大于w!
int cnt=tree[i].rangeSum(w+1,maxn);
ans += tree[i].rangeSum(w + 1, maxn);//累加答案
ans %= mod;//取模
}
//管理c颜色的树状数组中,宽度为w的矩形的数量+1
tree[c].add(w, 1);//更新树状数组
}
理解思路的重点和难点:
为什么每次枚举一个矩形,都要把他添加到对应颜色的树状数组中?
//管理c颜色的树状数组中,宽度为w的矩形的数量+1,添加到对应树状数组
tree[c].add(w, 1);//更新树状数组
上面这个问题是我们理解为什么代码可以保证满足题目要求的后两个约束条件的关键:
- 矩形1的长度严格大于矩形2的长度
- 矩形1的宽度严格小于矩形2的宽度
解释:
从代码中我们了解到,
rangeSum(w+1,maxn)
会被累加到答案中,因为题目的一个约束条件是当前矩形(以矩形2为基准)的宽度要严格大于另一个匹配的矩形的宽度。
除此之外还有两个约束条件:1. 颜色不得相同、2. 当前矩形长度要严格小于另一个匹配的矩形的长度。第一点我们已经通过数组维度的方式区分了,我们主要来讲讲第二点是上述代码如何实现的:首先我们知道,矩形列表arr已经是根据长度、宽度按顺序排序好了的。其次,树状数组最开始是没有任何值,也就是全为0;我们在每次枚举矩形的时候,才把长度逐渐增大(升序)的矩形存放到树状数组中;因此,当我们枚举到一个矩形:长度是L,宽度是W,颜色是C;那么当前管理颜色C的树状数组中,只存储了长度小于等于L的矩形数量。
这貌似好像不符合"长度要严格小于"的约束条件。但实际上这种设计很巧妙的满足了“长度要严格小于”这个约束条件。我们刚才讲过,ans的更新是通过累加rangeSum(W+1,maxn)完成的,也就是说,即使树状数组中有多个长度为L的矩形,但是这些矩形中宽度最大值是W(换句话说,长度为L,宽度大于W的矩形当前还不存在!),但ans要累加的是宽度大于W的矩形,所以不会统计长度相同的矩形的情况!
这也是长度和宽度都要升序排序的原因。
算法步骤总结
-
读取并存储所有矩形信息。
-
排序:按照长度 l l l 升序,若长度相同则按宽度 w w w 升序。
-
初始化:为每种颜色建立一个树状数组。
-
遍历:对于每个矩形:
- 对于所有不同颜色的树状数组,查询宽度严格大于 w i w_i wi 的矩形数量,累加到答案中。
- 将当前矩形的宽度加入到对应颜色的树状数组中。
-
输出:输出最终答案,对 1 0 9 + 7 10^9+7 109+7 取模。
AC代码
import java.io.*;
import java.util.*;
public class Main {
static class FenwickTree {
int n;
int[] tree;
public FenwickTree(int n) {
this.n = n;
tree = new int[n + 1];
}
int lowbit(int i) {
return i & -i;
}
void add(int i, int val) {
for (; i <= n; i += lowbit(i)) {
tree[i] += val;
}
}
int preSum(int i) {
int ret = 0;
for (; i > 0; i -= lowbit(i)) {
ret += tree[i];
}
return ret;
}
int rangeSum(int l, int r) {
return preSum(r) - preSum(l - 1);
}
}
static int maxn = 100010;
static int ans = 0;
static int mod = (int) 1e9 + 7;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
List<int[]> arr = new ArrayList<>();
// 读入所有的矩形
for (int i = 0; i < n; i++) {
int l = sc.nextInt(); // 长
int w = sc.nextInt(); // 宽
int c = sc.nextInt(); // 颜色
arr.add(new int[]{l, w, c});
}
// 对矩形进行排序,l升序,然后w升序
arr.sort((o1, o2) -> {
if (o1[0] != o2[0]) return Integer.compare(o1[0], o2[0]);
else return Integer.compare(o1[1], o2[1]);
});
// 建立三种颜色对应的树状数组
FenwickTree[] tree = new FenwickTree[3];
for (int i = 0; i < 3; i++) tree[i] = new FenwickTree(maxn);
// 然后枚举排序好的arr
for (int[] a : arr) {
int l = a[0];
int w = a[1];
int c = a[2];
for (int i = 0; i < 3; i++) {
if (c == i) continue; // 相同颜色被排除掉
ans = (ans + tree[i].rangeSum(w + 1, maxn)) % mod; // 累加答案
}
tree[c].add(w, 1); // 更新树状数组
}
System.out.println(ans);
}
}
复杂度分析
-
时间复杂度:
-
排序: O ( N log N ) O(N \log N) O(NlogN)。
-
遍历:每次查询和更新树状数组的时间复杂度为 O ( log W ) O(\log W) O(logW),总共 N N N 次操作,因此为 O ( N log W ) O(N \log W) O(NlogW)。
-
总时间复杂度: O ( N log N ) O(N \log N) O(NlogN)(因为 W W W 最大为 1 0 5 10^5 105, log W \log W logW 可以视为常数项)。
-
若题解过程不够严谨,或者错误,期待佬们在评论区及时指正!!