蓝桥杯2018年省赛[第九届]-JavaB组赛题解析(下)

蓝桥杯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. 1 <= i, j, k <= N
  2. 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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ATFWUS

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值