蓝桥杯2016年省赛[第七届]-JavaB组赛题解析
蓝桥杯官方讲解视频:https://www.lanqiao.cn/courses/2737
真题文档:https://www.lanqiao.cn/courses/2786/learning/?id=67811
由于篇幅原因,本篇只有6-10题(所有编程题)的解析,1-5题解析见上篇文章
蓝桥杯2018年省赛[第九届]-JavaB组赛题解析(上)
题6.递增三元组[11分](★★★)
1.题目描述
给定三个整数数组
A = [A1, A2, … AN],
B = [B1, B2, … BN],
C = [C1, C2, … CN],
请你统计有多少个三元组(i, j, k) 满足:
- 1 <= i, j, k <= N
- Ai < Bj < Ck
【输入格式】 第一行包含一个整数N。 第二行包含N个整数A1, A2, … AN。 第三行包含N个整数B1, B2, … BN。第四行包含N个整数C1, C2, … CN。
对于30%的数据,1 <= N <= 100
对于60%的数据,1 <= N <= 1000
对于100%的数据,1 <= N <=100000 0 <= Ai, Bi, Ci <= 100000
【输出格式】 一个整数表示答案
【输入样例】 3 1 1 1 2 2 2 3 3 3
【输出样例】 27
资源约定:
峰值内存消耗(含虚拟机) < 256M
CPU消耗 < 1000ms
请严格按要求输出,不要画蛇添足地打印类似:“请您输入…” 的多余内容。 所有代码放在同一个源文件中,调试通过后,拷贝提交该源码。
不要使用package语句。不要使用jdk1.7及以上版本的特性。 主类的名字必须是:Main,否则按无效代码处理。
2.简要分析
- 一看题目还是很简单的,话不多说就写了暴力枚举,但仔细一看,需要三重循环,肯定过不了,于是开始考虑如何优化。
- 第一步优化的思路其实也好办,我们可以发现:如果我们先求出b中元素每个对应c有多少递增二元组,再将这些结果存入哈希表中,然后再遍历a中每个元素,看对应b有多少个二元组,结果相乘即可。思路是:如果b[j]对应整个c数组有x个递增二元组,如果a[i]对应b[j]是一个二元组,那么此处四赠三元组的数目就是x。
- 通过上面的方法,我们可以将三层循环优化为二层循环。但是很遗憾,还是过不了,能拿到88%的分数。
- 我们思考一下我们一定要遍历每个元素的根本原因:因为这些元素是无序的,必须遍历到每一个才能保证考虑了所有的可能。
- 但实际上,题目的这个递增三元组的要求,只要求三个元素满足递增的关系就行了,所以如果我们对三个数组进行排序的话,其实是没有影响的。
- 排序后就有个好处了,就是b每一个元素对应c的递增二元组,我们就可以根据二分法算出来了(计算c数组中有多少个元素比b里面特定元素大就行了),同样的,对于a里面的每一个元素,也能很快的计算出有多少个元素比这个元素大。
- 我们的整体思路是:对a,b,c数组进行排序,对b数组的每一项b[j],用二分法计算出c数组中有多少个元素比b[j]大,这个数记为x,同时维护b数组对应的前缀和pre,pre[j]表示b[0]到b[j]的所有x和。对a数组的每一项a[i],用二分法计算出b数组中第一个比它大的数的下标first,那么a[i]这个元素能产生的递增三元组的数目就是pre[n-1]-pre[first-1]。再考虑一些极端情况即可。
3.实现代码1(枚举哈希优化)(会超时)
O(N^2)
*/
/*
10
1 8 6 5 4 3 7 5 8 5
0 8 5 9 8 4 8 5 2 0
3 7 8 9 5 0 4 1 9 0
89
*/
public class _06递增三元组 {
static int n;
static int[] a;
static int[] b;
static int[] c;
static Map<Integer,Long> map=new HashMap<>();
static long ans;
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
n=sc.nextInt();
a=new int[n];
b=new int[n];
c=new int[n];
for(int i=0;i<n;i++){
a[i]=sc.nextInt();
}
for(int i=0;i<n;i++){
b[i]=sc.nextInt();
}
for(int i=0;i<n;i++){
c[i]=sc.nextInt();
}
solve();
}
static void solve(){
ans=0;
map=new HashMap<>();
//统计B到C的递增序列
for(int j=0;j<n;j++){
long count=0;
for(int k=0;k<n;k++){
if(c[k]>b[j]){
count++;
}
}
map.put(j,count);
}
//统计A到B的递增序列
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(b[j]>a[i]&&map.get(j)!=null){
ans+=map.get(j);
}
}
}
System.out.println(ans);
}
}
4.实现代码2(二分法+前缀和)
O(N*logN)
*/
/*
10
1 8 6 5 4 3 7 5 8 5
0 8 5 9 8 4 8 5 2 0
3 7 8 9 5 0 4 1 9 0
89
*/
public class _06递增三元组 {
static int n;
static int[] a;
static int[] b;
static int[] c;
static Map<Integer,Long> map=new HashMap<>();
static long ans;
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
n=sc.nextInt();
a=new int[n];
b=new int[n];
c=new int[n];
for(int i=0;i<n;i++){
a[i]=sc.nextInt();
}
for(int i=0;i<n;i++){
b[i]=sc.nextInt();
}
for(int i=0;i<n;i++){
c[i]=sc.nextInt();
}
solve();
}
static void solve(){
ans=0;
map=new HashMap<>();
long pre[]=new long[n];//pre[j]表示b中第0项到第j项中可以组成二元组的和
Arrays.sort(a);
Arrays.sort(b);
Arrays.sort(c);
for(int j=0;j<n;j++){
int result=searchGreaterNum(c,b[j]);
pre[j]=(j==0?result:pre[j-1]+result);//维护bc二元组关系数的前缀和数组
}
//需要先找到b中第一个比a[i]大的数的下标first,然后a->b->c的三元组的数目就是pre[n-1]-pre[first-1]
for(int i=0;i<n;i++){
int first=searchFirstGreaterIndex(b,a[i]);
if(first==0){
ans+=pre[n-1];
}else if(first==-1){
ans+=0;
}else if(first>0){
ans+=(pre[n-1]-pre[first-1]);
}
}
System.out.println(ans);
}
/**
* 搜索数组a中第一个比key大的数的下标
* @param a 升序数组
* @param key 待查找关键字
* @return 返回第一个比key大的数,不存在则返回-1
* @author ATFWUS
*/
public static int searchFirstGreaterIndex(int[] a,int key){
int result=Arrays.binarySearch(a,key);
if(result>=0){
//key在数组中
int k=result;
for(;k<a.length;k++){
if(a[k]!=a[result]){
return k;
}
}
return -1;
}else{
return -result-1;
}
}
/**
* 搜索数组中比key大的数的个数
* @param a 升序数组
* @param key 待查找关键字
* @return 返回数组中比key大的数的个数
* @author ATFWUS
*/
public static int searchGreaterNum(int[] a,int key){
int result=Arrays.binarySearch(a,key);
if(result>=0){
//key在数组中
int k=result;
for(;k<a.length;k++){
if(a[k]!=a[result]){
return a.length-k;
}
}
return 0;
}else{
return a.length+result+1;
}
}
}
题7.螺旋折线[19分](★★★)
1.题目描述
如图p1.pgn所示的螺旋折线经过平面上所有整点恰好一次。
对于整点(X, Y),我们定义它到原点的距离dis(X, Y)是从原点到(X, Y)的螺旋折线段的长度。
例如dis(0, 1)=3, dis(-2, -1)=9
给出整点坐标(X, Y),你能计算出dis(X, Y)吗?
【输入格式】
X和Y
对于40%的数据,-1000 <= X, Y <= 1000
对于70%的数据,-100000 <= X, Y <= 100000
对于100%的数据, -1000000000 <= X, Y <= 1000000000
【输出格式】
输出dis(X, Y)
【输入样例】
0 1
【输出样例】
3
资源约定:
峰值内存消耗(含虚拟机) < 256M
CPU消耗 < 1000ms
请严格按要求输出,不要画蛇添足地打印类似:“请您输入…” 的多余内容。
所有代码放在同一个源文件中,调试通过后,拷贝提交该源码。
不要使用package语句。不要使用jdk1.7及以上版本的特性。
主类的名字必须是:Main,否则按无效代码处理。
2.简要分析
- 这个题首先惊讶到我们的是这个数据的规模,十个亿,显然就算是O(N)都是不可能完成的,所以我们必须在常数级别的时间内计算出来,这显然就是让我们找规律了。
- 找规律一个最关键的点就在于:以哪个点作为基准,只要在每一个回旋中都找到一个基准点,那我们就可以计算出那个回旋中,任意一个点的dis了。
- 但是这是个螺旋状的,并不对称,所以处理起来比较困难。仔细一看,发现只要把
(-1,0)-(0,0)
这条边顺时针旋转90度,(-2,-1)-(-1,-1)
这条边顺时针旋转90度,依次类推,这些位置上的边都旋转90度,就变成了一个正方形了。如下图:
- 这样就可以算出正方形的周长是
8,16,24
,也就是8*n
。 - 此时我们只需要计算出点
(x,y)
所在的正方形,以及和点(-n,-n)
的位置关系,就可以确定dis的大小了。 - 计算是第几个正方形:
n=max(abs(x),abs(y))
。 - 计算
(x,y)
和(-n,-n)
之间的距离:d=x-(-n)+y-(-n)=x+y+2*n
。 - 如果
x>=y
,说明点(x,y)
在正方的最右或者最下的一条边上,此时旋过的长度就是8*n-d
。 - 如果
x<y
,说明点(x,y)
在正方形的最做或者最上的一条边上,此时旋过的长度就是d
。 - 注意:做边旋转后,之前的正方形的点细节可以忽略,但最后一个正方形的点细节必须考虑。
3.实现代码
public class _07螺旋折线 {
static long x;
static long y;
static long ans;
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
x=sc.nextLong();
y=sc.nextLong();
solve();
}
static void solve(){
//判断所在正方形数
long n=Math.max(Math.abs(x),Math.abs(y));
//计算前面所有正方形的和
ans=4*(n-1)*n;
//计算(x,y)和(-n,-n)之间的距离
long d=x-(-n)+y-(-n);
if(x>=y){
//8*n是一个正方形的周长 x>=y说明点(x,y)在正方的最右或者最下的一条边上,此时旋过的长度就是8*n-d
ans+=8*n-d;
}else{
//说明点(x,y)在正方形的最做或者最上的一条边上,此时旋过的长度就是d
ans+=d;
}
System.out.println(ans);
}
}
题8.日志统计[21分](★★★)
1.题目描述
小明维护着一个程序员论坛。现在他收集了一份"点赞"日志,日志共有N行。其中每一行的格式是:
ts id
表示在ts时刻编号id的帖子收到一个"赞"。
现在小明想统计有哪些帖子曾经是"热帖"。如果一个帖子曾在任意一个长度为D的时间段内收到不少于K个赞,小明就认为这个帖子曾是"热帖"。
具体来说,如果存在某个时刻T满足该帖在[T, T+D)这段时间内(注意是左闭右开区间)收到不少于K个赞,该帖就曾是"热帖"。
给定日志,请你帮助小明统计出所有曾是"热帖"的帖子编号。
【输入格式】
第一行包含三个整数N、D和K。
以下N行每行一条日志,包含两个整数ts和id。
对于50%的数据,1 <= K <= N <= 1000
对于100%的数据,1 <= K <= N <= 100000 0 <= ts <= 100000 0 <= id <= 100000
【输出格式】
按从小到大的顺序输出热帖id。每个id一行。
【输入样例】
7 10 2
0 1
0 10
10 10
10 1
9 1
100 3
100 3
【输出样例】
1
3
资源约定:
峰值内存消耗(含虚拟机) < 256M
CPU消耗 < 1000ms
请严格按要求输出,不要画蛇添足地打印类似:“请您输入…” 的多余内容。
所有代码放在同一个源文件中,调试通过后,拷贝提交该源码。
不要使用package语句。不要使用jdk1.7及以上版本的特性。
主类的名字必须是:Main,否则按无效代码处理。
2.简要分析
-
看完输入,发现所有的日志都是无序的,所以我们的第一步肯定是对日志按照id进行排序,对id相同的日志来判断是否符合条件,同时还能保证最后是按照id的顺序输出的。
-
接下来就是判断是否满足条件了,最简单的方法肯定就是枚举区间长度
d-1
,然后看漫步满足要求,但是如果这样的话,极限情况下,时间复杂度还是会达到O(N^2)
无法通过题目的数据,所以我们优化的重点就在如何判断是否满足条件了。 -
在这里我们可以使用滑动窗口的做法来判断,这样时间的消耗会小很多。
-
这里滑动窗口主要思路:
- 初始化左指针
left=i
,初始化右指针right=i
,初始化计数量count=0
。 - 在左指针不超过右指针且有指针不超过同id边界时,做循环。
- 循环开始
count++
,如果count的数目大于等于k
了,判断此时的窗口是否满足条件,满足条件直接返回判断下一个,不满足条件就左指针右移,count--
。
- 初始化左指针
3.实现代码
public class _08日志统计 {
static class Log implements Comparable<Log>{
int ts;
int id;
public Log(int ts, int id) {
this.ts = ts;
this.id = id;
}
@Override
public int compareTo(Log o) {
//按id升序,id一样则ts升序
if(id==o.id){
return ts-o.ts;
}else{
return id-o.id;
}
}
}
static int n;
static int d;
static int k;
static Log[] logs;
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
n=sc.nextInt();
d=sc.nextInt();
k=sc.nextInt();
logs=new Log[n];
for(int i=0;i<n;i++){
logs[i]=new Log(sc.nextInt(),sc.nextInt());
}
solve();
}
static void solve(){
Arrays.sort(logs);
for(int i=0;i<n;){
int j=i+1;//用j来统计id相同的有多少
for(;j<n;j++){
if(logs[i].id!=logs[j].id){
break;
}
}
//此时j是第一个不等于logs[i].id的数的下标
//用左右指针来确定满足条件的窗口
int left=i;
int right=i;
int count=0;//记录赞的个数
while(left<=right && right<j){
count++;
if(count>=k){
if(logs[right].ts-logs[left].ts<d){
//是满足条件的id,直接输出,然后遍历下一个id
System.out.println(logs[i].id);
break;
}else{
//时间不满足要求,left尝试右移
left++;
count--;
}
}
//count个数太小了,right右移
right++;
}
//i从下一个id开始遍历
i=j;
}
}
}
题9.全球变暖[23分](★★★★)
1.题目描述
你有一张某海域NxN像素的照片,".“表示海洋、”#"表示陆地,如下所示:
.......
.##....
.##....
....##.
..####.
...###.
.......
其中"上下左右"四个方向上连在一起的一片陆地组成一座岛屿。例如上图就有2座岛屿。
由于全球变暖导致了海面上升,科学家预测未来几十年,岛屿边缘一个像素的范围会被海水淹没。具体来说如果一块陆地像素与海洋相邻(上下左右四个相邻像素中有海洋),它就会被淹没。
例如上图中的海域未来会变成如下样子:
.......
.......
.......
.......
....#..
.......
.......
请你计算:依照科学家的预测,照片中有多少岛屿会被完全淹没。
【输入格式】
第一行包含一个整数N。 (1 <= N <= 1000)
以下N行N列代表一张海域照片。
照片保证第1行、第1列、第N行、第N列的像素都是海洋。
【输出格式】
一个整数表示答案。
【输入样例】
7
…
.##…
.##…
…##.
…####.
…###.
…
【输出样例】
1
资源约定:
峰值内存消耗(含虚拟机) < 256M
CPU消耗 < 1000ms
请严格按要求输出,不要画蛇添足地打印类似:“请您输入…” 的多余内容。
所有代码放在同一个源文件中,调试通过后,拷贝提交该源码。
不要使用package语句。不要使用jdk1.7及以上版本的特性。
主类的名字必须是:Main,否则按无效代码处理。
2.简要分析
-
典型的DFS连通块题目,关键是要做一些优化保证不会超过Java的堆栈层次。
-
主要的思路:
- 因为题目说明了保证边界一圈都是海洋,所以我们就省去了很多边界的判断。
- 遍历地图,如果一个地方是陆地,那么就开始DFS。
- 如果进入DFS的点不是陆地,没有搜索的必要,直接返回。
- 把该块陆地变成其它的字符,防止重复搜索。
- 如果是陆地,判断四周的情况,如果四周都不是海洋,那么这块岛屿肯定不会被沉没,同时我们应该手动的去沉没这片岛屿,防止后面重复搜索到这里导致超时。
- 如果四周有海洋,那么继续搜索相邻的其它点。
- 遍历中每调用一次DFS,实际上完成了一整个岛屿的搜索。
-
具体的DFS思路还是得看代码去领悟。
3.实现代码(DFS)
public class _09全球变暖 {
static int n;
static char[][] grid;
static int[][] dir={{-1,0},{1,0},{0,-1},{0,1}};
static int start_num;//初始时岛屿数目
static int final_num;//结束时岛屿数目
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
n=sc.nextInt();
sc.nextLine();
grid=new char[n][n];
for(int i=0;i<n;i++){
String s=sc.nextLine();
grid[i]=s.toCharArray();
}
solve();
}
static void solve(){
for(int i=1;i<n-1;i++){
for(int j=1;j<n-1;j++){
if(grid[i][j]=='#'){
//此时的#已经代表是一片岛屿,因为这块陆地在搜索的时候,会将相邻的变成其他字符*
start_num++;
dfs(i,j);
}
}
}
System.out.println(start_num-final_num);
}
static void dfs(int x,int y){
//这个点不是陆地(被标记成已经探索的岛屿内容或本身是海洋)
if(grid[x][y]!='#'){
return;
}
//已探索过的岛屿,标记为*
grid[x][y]='*';
//如果四个方向都不是海洋,那么这个岛屿肯定不会被淹
if(grid[x+1][y]!='.' && grid[x-1][y]!='.' && grid[x][y+1]!='.' && grid[x][y-1]!='.'){
final_num++;
change(x,y);
return;
}
for(int i=0;i<4;i++){
dfs(x+dir[i][0],y+dir[i][1]);
}
}
//将所有与(x,y)相邻的陆地变成海洋,防止重复搜索
static void change(int x, int y) {
Stack<Integer> stack = new Stack<>();
stack.add(x);
stack.add(y);
while (!stack.isEmpty()) {
y = stack.pop();
x = stack.pop();
grid[x][y] = '.';
if (grid[x + 1][y] != '.') {
stack.add(x + 1);
stack.add(y);
}
if (grid[x - 1][y] != '.') {
stack.add(x - 1);
stack.add(y);
}
if (grid[x][y + 1] != '.') {
stack.add(x);
stack.add(y + 1);
}
if (grid[x][y - 1] != '.') {
stack.add(x);
stack.add(y - 1);
}
}
}
}
题10.堆的计数[25分](★★★★★)
1.题目描述
我们知道包含N个元素的堆可以看成是一棵包含N个节点的完全二叉树。
每个节点有一个权值。对于小根堆来说,父节点的权值一定小于其子节点的权值。
假设N个节点的权值分别是1~N,你能求出一共有多少种不同的小根堆吗?
例如对于N=4有如下3种:
1
/ \
2 3
/
4
1
/ \
3 2
/
4
1
/ \
2 4
/
3
由于数量可能超过整型范围,你只需要输出结果除以1000000009的余数。
【输入格式】
一个整数N。
对于40%的数据,1 <= N <= 1000
对于70%的数据,1 <= N <= 10000
对于100%的数据,1 <= N <= 100000
【输出格式】
一个整数表示答案。
【输入样例】
4
【输出样例】
3
资源约定:
峰值内存消耗(含虚拟机) < 256M
CPU消耗 < 1000ms
请严格按要求输出,不要画蛇添足地打印类似:“请您输入…” 的多余内容。
所有代码放在同一个源文件中,调试通过后,拷贝提交该源码。
不要使用package语句。不要使用jdk1.7及以上版本的特性。
主类的名字必须是:Main,否则按无效代码处理。
2.简要分析
-
这道题实在是没有什么好的思路,暴力肯定是不行的。
-
目前有的思路:
- 根节点一定是最小的1。
- 问题是考虑把n-1个节点放入完全二叉树。
- 后续的就没有更好的想法了。
-
下面代码是根据某博主的C++代码翻译过来的,也不是很理解,哈哈哈,原文出处:戳我前往
-
待日后对这方面的知识有新的理解了再来看看吧!
3.实现代码
public class _10堆的计数 {
static long MOD=1000000009;
static long f[];
static long s[];
static long d[];
static long inv[];
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
f=new long[n+5];
s=new long[n+5];
d=new long[n+5];
inv=new long[n+5];
f[0]=1;
for(int i=1;i<n+5;i++){
f[i]=f[i-1]*i%MOD;
inv[i]=qpow(f[i],MOD-2);
}
for(int i=n;i>=1;i--){
s[i]=(i*2<=n?s[i*2]:0)+((i*2+1)<=n?s[i*2+1]:0)+1;
}
for(int i=0;i<n+5;i++){
d[i]=1;
}
for(int i=n;i>=1;i--){
if(i*2+1<=n){
d[i]= ((C(s[i]-1,s[i*2+1])*d[i*2])%MOD*d[i*2+1])%MOD;
}
}
System.out.println(d[1]);
}
static long C(long n,long m){
return f[(int)n]*inv[(int)m]%MOD*inv[(int)n-(int)m]%MOD;
}
//快速幂
static long qpow(long a,long n){
if(n==0||a==1){
return 1;
}
long x=qpow(a,n/2);
return n%2>0?(x*x%MOD*a%MOD):(x*x%MOD);
}
}
总结
-
2018年开始,编程题就多起来了,总共有5个,并且每个想得满分都不是件容易的事。5个填空题除了第四题稍微有点难度外,其他的都不难,并且第四题是很经典的题型,见过应该很快能做出来。
-
总的来说,做一遍下来头都大了,想能做出这些题目,还是要有扎实的代码功底,并不像传闻的水水就基本做的差不多了。
-
列一下考点吧:
- 1.日期API使用。
- 2.代数几何思维。
- 3.BigInteger类的使用,对数据量的把握。
- 4.动态规划,极限思想。
- 5.分治思想。
- 6.二分法,前缀和。
- 7.观察,找通用规律。
- 8.滑动窗口。
- 9.DFS连通块问题。
- 10 .不会做所以不知道考的啥(哈哈哈哈哈,来自弱者的悲哀)
-
除了第1个题,感觉每个题都出的很有质量,选拔题果然一般的人也不怎么会做。。。。
ATFWUS Writing 2021-1-31