2020年蓝桥杯模拟赛解题报告(Java版本)——2021.3.25

第八题 长草

题目
【问题描述】
小明有一块空地,他将这块空地划分为 n 行 m 列的小块,每行和每列的长度都为 1。
小明选了其中的一些小块空地,种上了草,其他小块仍然保持是空地。
这些草长得很快,每个月,草都会向外长出一些,如果一个小块种了草,则它将向自己的上、下、左、右四小块空地扩展,这四小块空地都将变为有草的小块。
请告诉小明,k 个月后空地上哪些地方有草。
【输入格式】
输入的第一行包含两个整数 n, m。
接下来 n 行,每行包含 m 个字母,表示初始的空地状态,字母之间没有空格。如果为小数点,表示为空地,如果字母为 g,表示种了草。
接下来包含一个整数 k。
【输出格式】
输出 n 行,每行包含 m 个字母,表示 k 个月后空地的状态。如果为小数点,表示为空地,如果字母为 g,表示长了草。
【样例输入】
4 5
.g…

…g…

2
【样例输出】
gggg.
gggg.
ggggg
.ggg.
【评测用例规模与约定】
对于 30% 的评测用例,2 <= n, m <= 20。
对于 70% 的评测用例,2 <= n, m <= 100。
对于所有评测用例,2 <= n, m <= 1000,1 <= k <= 1000。

解析

典型的bfs,基本是个模板题。时间复杂度最多为O(N*M)。
代码如下:

package com.lanqiao.mike;

import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;

public class ZhangCao {

	public static void main(String[] args) {
		Scanner scanner=new Scanner(System.in);
		int n=scanner.nextInt();
		int m=scanner.nextInt();
		char[][] data=new char[n][m];//.表示无草,g表示有草
		for (int i = 0; i < n; i++) {
			//这个地方要理解next()与nextLine()的区别
			//next():一定要读取到有效字符后才可以结束输入,对输入有效字符之前遇到的空格键、Tab键或Enter键等结束符,next()方法会自动将其去掉,只有在输入有效字符之后,next()方法才将其后输入的空格键、Tab键或Enter键等视为分隔符或结束符。
			//nextLine():next方法不能得到带空格的字符串而nextLine()方法的结束符只是Enter键,即nextLine()方法返回的是Enter键之前的所有字符,它是可以得到带空格的字符串的。
			//s1=sc.next(); s2=sc.nextLine();nextLine()自动读取了被next()去掉的Enter作为他的结束符,所以没办法给s2从键盘输入值。经过验证,其他的next的方法,如double nextDouble()  , float nextFloat() , int nextInt() 等与nextLine()连用时都存在这个问题,解决的办法是:在每一个 next()、nextDouble()  、 www.gzlij.com()、nextFloat()、nextInt() 等语句之后加一个nextLine()语句,将被next()去掉的Enter结束符过滤掉
			String str=scanner.next();
			for (int j = 0; j < m; j++) {
				
				data[i][j]=str.charAt(j);
			}
		}
		int k=scanner.nextInt();
		
		Queue<Point> queue=new LinkedList<Point>();
		
		//队列初始化
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < m; j++) {
				if (data[i][j]=='g') {
					queue.add(new Point(i, j,0));
				}
			}
		}
		
		while(!queue.isEmpty()){//典型的bfs遍历,一层层往外围扩散
			//add()入队,remove()出队,poll()返回队首元素,但不出队
			Point point=queue.remove();
			int x=point.x;
			int y=point.y;
			int month=point.month;
			int t=month+1;
			if (month<k) {
				//考虑上下左右位置是否长草,若没有草则赋值草且入队,有草不考虑
				if (x-1>=0) {
					if (data[x-1][y]!='g') {
						data[x-1][y]='g';
						queue.add(new Point(x-1, y, t));
					}
				}
				if (x+1<n) {
					if (data[x+1][y]!='g') {
						data[x+1][y]='g';
						queue.add(new Point(x+1, y, t));
					}
					
				}
				if (y-1>=0) {
					if (data[x][y-1]!='g') {
						data[x][y-1]='g';
						queue.add(new Point(x, y-1, t));
					}
					
				}
				if (y+1<m) {
					if (data[x][y+1]!='g') {
						data[x][y+1]='g';
						queue.add(new Point(x, y+1, t));
					}
					
				}
			}
		}
		
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < m; j++) {
				System.out.print(data[i][j]);
			}
			System.out.println();
		}
	}
}

class Point{
	int x;
	int y;
	int month;
	public Point(int x,int y,int month){
		this.x=x;
		this.y=y;
		this.month=month;
	}
}

测试用例:

4 5
.g...
.....
..g..
.....
2
gggg.
gggg.
ggggg
.ggg.

第九题 序列计数

题目
【问题描述】
小明想知道,满足以下条件的正整数序列的数量:

  1. 第一项为 n;
  2. 第二项不超过 n;
  3. 从第三项开始,每一项小于前两项的差的绝对值。
    请计算,对于给定的 n,有多少种满足条件的序列。
    【输入格式】
    输入一行包含一个整数 n。
    【输出格式】
    输出一个整数,表示答案。答案可能很大,请输出答案除以10000的余数。
    【样例输入】
    4
    【样例输出】
    7
    【样例说明】
    以下是满足条件的序列:
    4 1
    4 1 1
    4 1 2
    4 2
    4 2 1
    4 3
    4 4
    【评测用例规模与约定】
    对于 20% 的评测用例,1 <= n <= 5;
    对于 50% 的评测用例,1 <= n <= 10;
    对于 80% 的评测用例,1 <= n <= 100;
    对于所有评测用例,1 <= n <= 1000。

解析

解法一(我的思路):
新建一个数组arr[],令dfs(x,m)表示在m位上填入数x的所有序列的和,当前m位arr[m]=x。当m=0时,arr[0]=n;当m=1时,arr[m]可取1-n的数;当m>1时,有递归关系:
d f s ( x , m ) = Σ d f s ( y , m + 1 ) dfs(x,m)=\Sigma dfs(y,m+1) dfs(x,m)=Σdfs(y,m+1)
其中, y 的 取 值 范 围 是 [ 1 , a b s ( a r r [ m ] − a r r [ m − 1 ] ) − 1 ] y的取值范围是[1,abs(arr[m]-arr[m-1])-1] y[1,abs(arr[m]arr[m1])1]
代码如下:

package com.lanqiao.mike;

import java.util.Scanner;

public class Class_9序列计数2 {

	static int n;
	static int ans;
	static int[] arr;
	static int MOD=10000;
	
	static int dfs(int x,int m){
		arr[m]=x;
		
		int ans=1;//长度为m+1序列也是一个,及在m位置填入x为止。后面再加数就在m+1位置
		for (int i = 1; i <Math.abs(arr[m]-arr[m-1]); i++) {
			ans=(ans+dfs(i, m+1))%MOD;
		}
		//这里我曾经想用mem[x][m]记录这个状态来优化递归,发现不行,因为arr[m-1]的不同
		//导致在不同arr[m-1]的前提下,i取值范围不同,dfs(i,m+1)累加的值不一样,即mem[x][m]不一样
		//即这种记录的状态存在多义性。mem[x][m]究其原因它只涉及到m位的数,但没有考虑到m-1位上的数的限制。
		return ans;
	}
	
	public static void main(String[] args) {
		Scanner scanner=new Scanner(System.in);
		n=scanner.nextInt();
		arr=new int[n];
		arr[0]=n;
		ans=0;
		for (int i = 1; i <=n; i++) {
			ans=(ans+dfs(i,1))%MOD;
		}
		System.out.println(ans);
	}

}

测试用例:

4
7
10
452
//这个结果秒出
30
3556
//这个结果经过10秒左右才出,显然本程序只能过50%的测试用例。为何这个递归程序的时间复杂度如此高,我一时不知道这个递归的时间复杂度如何衡量。

解法二:老师思路
题干的第三个条件是一个递归式,由此可得
f ( p r e , c u r ) = f ( c u r , 1 ) + f ( c u r , 2 ) + . . . + f ( c u r , a b s ( p r e − c u r ) − 1 ) + 1 f(pre,cur) = f(cur,1) + f(cur,2) + ... +f(cur,abs(pre-cur)-1) + 1 f(pre,cur)=f(cur,1)+f(cur,2)+...+f(cur,abs(precur)1)+1
pre表示前一个数,cur代表当前的数,选定之后,序列种数等于以cur为前序,以1到abs-1为当前的序列数的总和再加1.如f(5,2) = f(2,1)+f(2,2).
暴力递归的复杂度是指数级的。基本的优化方案加状态记忆:输入1000时,实测运行时间为1000~2000ms;
代码如下:

package com.lanqiao.mike;

import java.util.Scanner;

public class Class_9序列计数 {

	static final int MOD=10000;
	static int N;
	static long ans;
	static long[][] mem=new long[1001][1001];
	static Scanner scanner;
	
	static long dfs(int pre,int cur){
		//询问状态
		if (mem[pre][cur]!=0) {
			return mem[pre][cur];
		}
		long ans=1;
		for (int j = 1; j < Math.abs(cur-pre); j++) {
			ans=(ans+dfs(cur, j))%MOD;
		}
		mem[pre][cur]=ans;//这个状态的记忆保证了唯一性,不存在多义性
		return ans;
	}
	
	public static void main(String[] args) {
		ans=0;
		scanner=new Scanner(System.in);
		N=scanner.nextInt();
		long ago=System.currentTimeMillis();
		for (int i = 1; i <=N; i++) {
			ans=(ans+dfs(N, i))%MOD;
		}
		System.out.println(ans);
		long end=System.currentTimeMillis();
		System.out.println(end-ago);
	}
}

测试用例:

4
7
0   //0毫秒结果就出来啦
10
452
0
100
6961
7         //7毫秒出结果
1000
9449
2034       //2000毫秒出结果,效率就不够啦,还要进行优化。

解法三:对解法二进行优化。
至此可过80%的用例(1000毫秒限制下)。解空间是N2 (这里怎么理解,得到一个mem[pre][cur]),每次循环加总,所以为N3 ,在同样解空间下,避免循环加总,即可优化到N2 .
重新考虑状态的转移, f ( i , j ) f(i,j) f(i,j)表示前一个数是i,当前数是1至j的合法序列的个数。 f ( i , j ) f(i,j) f(i,j)可拆成两个部分, f ( i , j ) = f ( i , j − 1 ) + f ( j , a b s ( i − j ) − 1 ) f(i,j)=f(i,j-1)+f(j,abs(i-j)-1) f(i,j)=f(i,j1)+f(j,abs(ij)1)
1)i作为前一个数,从1到j-1为当前数的合法序列个数
2) F ( i , j ) = F ( j , k ) , k 取 [ 1 , a b s ( i − j ) − 1 ] F(i,j)=F(j,k),k取[1,abs(i-j)-1] F(i,j)=F(j,k),k[1,abs(ij)1],所以 F ( i , j ) = f ( j , a b s ( i − j ) − 1 ) F(i,j)=f(j,abs(i-j)-1) F(i,j)=f(j,abs(ij)1),这里F(i,j)的含义和解法二的含义一致,注意。
如 f(10,5)=f(10,4)+f(5,4);而不是枚举1到5;这样每次解答树只展开两个节点,相当于减少一层循环,虽然解答树的层次还是很深,但是由于记忆的存在,解空间仍然是N的平方。可在100ms内解决。(有关时间复杂度的理解有待加深)
代码如下:

package com.lanqiao.mike;

import java.util.Scanner;

public class Class_9序列计数优化 {

	static final int MOD=10000;
	static int N;
	static long ans;
	static long[][] mem=new long[1001][1001];
	static Scanner scanner;
	
	static long dfs(int pre,int cur){//dfs(i,j)含义发生变化,前一个数位i,当前数1至j的合法序列的个数
		if (cur<=0) {
			return 0;
		}
		//询问状态,因为存在记忆,所以设得出一个mem[pre][cur]的时间为1,而mem[pre][cur]的解的规模是n*n
		//这个地方有点类似递推,从边界得到结果,往前推。(我的理解)
		if (mem[pre][cur]!=0) {
			return mem[pre][cur];
		}
		//集合拆分的概念
		mem[pre][cur]=((1+dfs(pre, cur-1)+dfs(cur, Math.abs(pre-cur)-1)))%MOD;
		return mem[pre][cur];
	}
	
	public static void main(String[] args) {
		ans=0;
		scanner=new Scanner(System.in);
		N=scanner.nextInt();
		long ago=System.currentTimeMillis();
		System.out.println(dfs(N, N));//消除了原来的最外层循环。
		long end=System.currentTimeMillis();
		System.out.println(end-ago);
	}
}

测试用例:

100
6961
1       //1毫秒
1000
9449
25        //25毫秒,效率够高。

总结:递归用法。首先难点在于定义一个恰当的递归集合概念。其次就是集合的拆分技巧。一般递归的优化是记忆递归的状态。特别注意的是递归状态要唯一。这就是定义的合法性问题。总而言之,定义一个集合的概念并写出递归表达式才是关键所在。

第十题 晚会节目单

题目
【问题描述】
小明要组织一台晚会,总共准备了 n 个节目。然后晚会的时间有限,他只能最终选择其中的 m 个节目。
这 n 个节目是按照小明设想的顺序给定的,顺序不能改变。
小明发现,观众对于晚会的喜欢程度与前几个节目的好看程度有非常大的关系,他希望选出的第一个节目尽可能好看,在此前提下希望第二个节目尽可能好看,依次类推。
小明给每个节目定义了一个好看值,请你帮助小明选择出 m 个节目,满足他的要求。
【输入格式】
输入的第一行包含两个整数 n, m ,表示节目的数量和要选择的数量。
第二行包含 n 个整数,依次为每个节目的好看值。
【输出格式】
输出一行包含 m 个整数,为选出的节目的好看值。
【样例输入】
5 3
3 1 2 5 4
【样例输出】
3 5 4
【样例说明】
选择了第1, 4, 5个节目。
【评测用例规模与约定】
对于 30% 的评测用例,1 <= n <= 20;
对于 60% 的评测用例,1 <= n <= 100;
对于所有评测用例,1 <= n <= 100000,0 <= 节目的好看值 <= 100000。

解析

解法一:他希望选出的第一个节目尽可能好看,在此前提下希望第二个节目尽可能好看,依次类推。要对题干这句话理解正确,并非求所选节目的好看值总和的最大值,而是从前往后尽量好看,注意n个节目的顺序是固定不变的。
第一个节目尽量好看,并希望第二个节目尽量好看,那么我们选择的第一个节目是max(g[0]~g[n-m]), 选择的第二个节目是max(g[lastMax+1]~g[n-m-1]),直至剩下的节目必须全部选择。算法用尺取法,双指针移动,理论上复杂度O(M*(N-M)),极端情况是M=N/2,整体达到N2 /2.如果输入数据

100000 50000
100000 99999 ...

实测10秒左右的时间。测试用例数据太多的话,感觉很烦,想到一个方法,随机数,这样产生的用例就好一些。
代码如下:

package com.lanqiao.mike;


import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Scanner;

public class Class_10晚会 {

	
	public static BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(System.out));
	static int N,M;
	
	public static void main(String[] args) throws IOException {
		
//		System.setIn(new FileInputStream(new File("E:\\1.txt")));
		Scanner scanner=new Scanner(System.in);
		N=scanner.nextInt();
		M=scanner.nextInt();
		int[] games=new int[N];
		for (int i = 0; i < N; i++) {
//			games[i]=(int)(Math.random()*100000);
//			System.out.println(games[i]);
			games[i]=scanner.nextInt();
		}
		
		long start=System.currentTimeMillis();//这一句千万不要放到输入的前面,要不然会把输入的时间也算进去,我说时间怎么这么长,一会儿结果就出来啦,疑惑不解。
		
		int pos_max=0;
		int pos_1=0;
		int pos_2=N-M;
		while(pos_1<pos_2&&pos_2<N){//时间复杂度O(M*(N-M)),自己画索引区间图理解
			while(pos_1<pos_2){//在[pos_1,pos_2-1]区间寻找最受欢迎的节目
				if (games[++pos_1]>games[pos_max]) {
					pos_max=pos_1;
				}
			}
			System.out.print(games[pos_max]+" ");
//			bw.write(games[pos_max]+" ");
			pos_1=pos_max+1;
			pos_2++;
			pos_max=pos_1;//在新的[pos_1,pos_2-1]区间继续寻找,重置pos_max
		}
		//上述循环退出后,后面所有的节目必须全部选上,否则节目数量不够。
		while(pos_2!=N){
			System.out.print(games[pos_2++]+" ");
//			bw.write(games[pos_2++]+" ");
		}
		System.out.println();
//		bw.write("\n");
//		bw.flush();
		long end=System.currentTimeMillis();
		System.out.println("耗时="+(end-start));
	}
}

测试用例:

5 3 
3 1 2 5 4 
3 5 4 
耗时=1   //单位是毫秒

第二个用例如下(使用的随机数)
在这里插入图片描述
可见只有205毫秒,老师说运行有10秒左右,不知道怎么出来的。依我的理解是输出50000个数据在控制台上用了这么多时间。上面的空白需要全选复制才行,即100000 50000与下面的耗时及中间空白一起复制。
测试用例3如下:
在这里插入图片描述

解法二:区间最值查询O(nlogn),对解法一优化

			while(pos_1<pos_2){//在[pos_1,pos_2-1]区间寻找最受欢迎的节目
				if (games[++pos_1]>games[pos_max]) {
					pos_max=pos_1;
				}
			}

这一段代码是区间内查询最大值,反复多次,且数据是静态的,所以选择ST做RMQ。
这里对ST和RMQ做个介绍:
RMQ问题
RMQ(Range Minimum Query)范围最小值(最大值)问题。具体表现为一下一类问题:

给出一个 n 个元素的数组 A1,A2,…,An,求解 min(l,r) : 计算 min{Al,Al+1,…,Ar}

RMQ问题有很多解法,其中较为快捷和简便的是 Tarjan 的 Sparse−Table 算法,简称 ST 表。

Sparse−Table 算法基于倍增思想,整个算法由预处理和查询两部分组成,分别描述一下:

  • 预处理

我们令d(i,j) 为从 i 开始的, 长度为 2j 的一段元素中的最小值。根据倍增思想,d(i,j)可以通过 d(i,j−1) 和 d(i+2j−1,j−1) 转移得到,具体操作就是对两个数取 min 。

没有接触过倍增思想的同学可能对这步表示有点难以理解,具体解释一下:

d(i,j)表示的是从 i 开始的长度为2j 的一段元素中的最小值,区间右端点是 i+2j−1 。

d(i,j−1) 表示的是从 i 开始的长度为 2j-1 的一段元素中的最小值,区间右端点是 i+2j-1−1。

d(i+2j-1,j−1)表示的是从 i+2j-1 开始的长度为 2j-1 的一段元素中的最小值,区间右端点是 i+2j-1+2j-1−1=i+2j-1

现在就明显了,[i,i+2j-1] 这段区间被划分成了了 [i,i+2j-1−1]和 [i+2j-1,i+2j-1−1] 两段区间,不重不漏,所以这样操作是可行的。

预处理的时间复杂度是 O(nlog2n)

  • 查询

有了刚才对预处理的讲述,查询部分应该不难想到,我们令 k 为满足 2k≤R−L+1 的最大整数。则可知 k=log2(R−L+1)。则以 L 开头, 以 R 结尾的两个长度为 2k 的区间合并起来就覆盖了 [L,R]。由于是求范围最小值,有元素被重复计算也没问题。

则 Query(L,R)=min(d(L,k),d(R−2k+1,k)) 。

查询的时间复杂度是 O(1)

由此可见,Sparse−Table算法思想简单,好写,是求解 RMQ问题的首选算法。

具体实现的时候还要注意一点,每次用 pow(2,x)计算 2x 是非常浪费时间的。由于计算机内部使用的是二进制,我们可以用 (1<<x) 表示 2x
————————————————
版权声明:本文为CSDN博主「Nekroz_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Diogenes_/article/details/80794838

ST和RMQ介绍完后
f[i][j]表示以 i 为起点,连续 2j 个数中的最大值(的下标);

转移方程就是:f[i][j] = data[f[i][j-1]] >= data[f[i+pow_2(j-1)][j-1]]?f[i][j-1]:f[i+pow_2(j-1)][j-1]; 注:比较原始数据,记录下标

由于预处理是O(nlogn),M次查询是O(M),每次查询是O(1),所以整体复杂度为O(nlogn)。
代码如下:

package com.lanqiao.mike;

import java.util.Scanner;

public class Class_10晚会优化 {

	public static Scanner scanner;
	static int N,M;
	private static int[] data;
	
	/*===st rmq begin===*/
	private static int[][] st;
	private static int[] log;
	
	private static int pow_2(int x){
		return 1<<x;
//		return (int) Math.pow(2, x);//比较费时间
	}
	
	private static void initLog(){//2^k<=log(R-L+1)=logN ==> k<=log2(N)
		log=new int[N+1];
		log[1]=0;
		for (int i = 2; i <=N; i++) {
			log[i]=log[i/2]+1;
		}
	}
	
	private static void initSt(){
		st=new int[N][log[N]+1];
		for (int i = 0; i < N; i++) {
			st[i][0]=i;//注意此处记录索引
		}

		for (int j = 1; pow_2(j)<N; j++) {
			for (int i = 0; i+pow_2(j-1)<N; i++) {//区间窗口滑动,时间复杂度nlogn
				int index1=st[i][j-1];
				int index2=st[i+pow_2(j-1)][j-1];
				st[i][j]=data[index1] >= data[index2]?index1:index2;
			}
		}
	}
	
	private static int query(int l,int r){//区间左端点是l,右端点是r
		int len=r-l+1;
		int k=log[len];
		
		int index1=st[l][k];
		int index2=st[r-pow_2(k)+1][k];
		
		return data[index1] >= data[index2] ? index1 : index2;
	}
	
	public static void main(String[] args) {
		scanner=new Scanner(System.in);
		N=scanner.nextInt();
		M=scanner.nextInt();
		data=new int[N];
		for (int i = 0; i < N; i++) {
			data[i]=scanner.nextInt();
//			data[i]=(int)(Math.random()*100000+1);//1-100000
		}
		long start=System.currentTimeMillis();
		//初始化st数据
		initLog();
		initSt();
		int pos_max=0,pos_1=0,pos_2=N-M;
		while(pos_1<pos_2&&pos_2<N){
			pos_max=query(pos_1, pos_2);//查找区间最值
			System.out.print(data[pos_max]+" ");
			pos_1=pos_max+1;
			pos_2++;
		}
		while(pos_2!=N){
			System.out.print(data[pos_2++]+" ");
		}
		System.out.println();
		long end=System.currentTimeMillis();
		System.out.println("耗时="+(end-start));
	}
}

测试用例如下:

5 3 
2 1 3 5 4
3 5 4 
耗时=0

随机数的测试用例如下:
在这里插入图片描述
在这里插入图片描述
搞不懂老师的想法。

这是高职高专组的题目,还是有些麻烦的。

本文有不当之处,恳请读者批评指正

————————————————
版权声明:本文为CSDN博主「小9」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zhengwei223/article/details/105065566

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值