题目
【问题描述】
小明有一块空地,他将这块空地划分为 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.
题目
【问题描述】
小明想知道,满足以下条件的正整数序列的数量:
解法一(我的思路):
新建一个数组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[m−1])−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(pre−cur)−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,j−1)+f(j,abs(i−j)−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(i−j)−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(i−j)−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<
————————————————
版权声明:本文为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