摘要:
除3道笔试题外,还有10道单选、5道不定项、2道Java单选、1道Java不定项选择题,笔试时长100分组,整体难度很大。三道算法题本人全部没有AC(惭愧),事后总结至此。
第一道算法题,常规的质数判别的算法(2~sqrt(num))在我其它步骤都没有错误的情况下导致了时间复杂度过超!经过查询资料,找到了6的倍数两侧判别法作为替代,兴许能够通过(?
第二道算法题,像是一道数论问题,原谅笔者答题时着急忙慌外加没有竞赛经历,对于这类数论题目束手无策。经过事后查询资料和分析,得到了一种可能的数学归纳解法。
第三道算法题,到此题时时间已经不多,然而此题思路其实简单,主要考察的是对字符串操作的掌握,通过定长数组记录字符频次的方式,可将构造内部排序的前缀串的时间复杂度降到O(n*26)(实际上就是O(n)),然后再经过O(nlogn)的排序算法得到最终结果。大厂笔试环境下的Java是JDK1.8的,Arrays.sort()和Collections.sort()方法都是默认支持的,放心用!
1.质数组个数
题目分析:
题目描述:
给定两个长度为n的正整数数组a和b,现在将两者合并成数组c,其中c_i = a_i + b_i,问数组c中有多少个质数组?
质数组定义:一个连续子数组中的每个数都是质数。
输入描述:
第一行是n
第二行是a数组的用空格隔开的n个数
第三行是b数组的用空格隔开的n个数
输出描述:
输出一行,包含一个整数,表示符合条件的质数组个数。
解题思路:
用双指针法标定质数子数组的边界。初始化左指针l为0,不断移动左指针直至找到质数,然后令右指针r=l,不断移动右指针r直至指向的数不为质数,此时得到一段质数区间的长度为r - l,那么可以得到2^(r-l) - 1个质数组,并更新左指针l到右指针r所指向处然后重复上述步骤,直至遍历完全。
关于素数判定的算法,对于比1大的正整数num,遍历从2~Math.sqrt(num)的整数,检验num是否能被整除,如果能被整除则不是质数,否则是质数。
时间复杂度分析:
双指针法遍历数组c的时间复杂度为O(n),判断质数的操作的时间复杂度为O(√m),m是数组c中的最大值。得到总的时间复杂度为O(n√m)。
然而,这样的时间复杂度还是过不了本题的运行时间要求,经过调试,可以确定是判断质数的操作时间复杂度过大导致的超时!!!
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 读取数组长度 n
int n = scanner.nextInt();
int[] a = new int[n];
// 读取数组 a 的元素
for (int i = 0; i < n; i++) {
a[i] = scanner.nextInt();
}
// 合并数组 a 和 b 得到数组 c
int[] c = new int[n];
for (int i = 0; i < n; i++) {
c[i] = a[i] + scanner.nextInt();
}
int count = 0;// 记录质数组个数
int l = 0;// 记录返回值
while (l < n) {
// 找到第一个质数
while (l < n &&!isPrime(c[l])) {
l++;
}
int r = l;
// 找到质数区间的结束位置
while (r < n && isPrime(c[r])) {
r++;
}
// count += (int)Math.pow(2, r-l) - 1;
count += (1 << (r-l)) - 1 // 更高效
l = r;
}
System.out.println(count);
scanner.close();
}
// 判断一个数是否为质数
public static boolean isPrime(int num) {
if (num < 2) {
return false;
}
for (int i = 2; i <= Math.sqrt(num); i++) {
if (num % i == 0) {
return false;
}
}
return true;
}
}
时间复杂度优化:6的倍数两侧判断法
如果一个数是质数(除了2和3),那么一定在6的倍数的两侧。
public static boolean isPrime(int num) {
if (num == 1) {
return false;
}
if (num == 2 || num == 3) {
return true;
}
if (num % 2 == 0 || num % 3 == 0) {
return false;
}
/*if (num % 6 != 1 && num % 6 != 5) {
return false;
// 与上一个if等价
}*/
for (int i = 5; i * i <= num; i += 6) {
if (num % i == 0 || num % (i + 2) == 0) {
return false;
}
}
return true;
}
2.正方形问题
题目分析:
题目描述:
给定a个1×1的正方形和b个2×2的正方形,尝试将他们拼成一个大正方形,在大正方形中,每个部位仅有一个小正方形,且没有小正方形重叠。要求判别给定的a+b个小正方形能否组成一个大正方形。
输入描述:
第一行是一个整数T,表示有T组测试数据。
每组测试数据有一行,是两个用空格隔开的正整数,分别表示a和b。
输出描述:
每组测试数据有一个输出,占一行,如果判别结果为能,输出"YES",否则输出"NO"。
解题思路:
a+b个小正方形能够组成大正方形,则对于总面积S=a+b*4,必然存在一个整数d,使得d^2 = S,如果不存在,那么直接返回"NO"。
现在,我们得到了大正方形的边长d。
根据边长d的奇偶性,分别处理:
- 偶数边长,最大能放置的2×2正方形个数为 maxB = S / 4 - 1(由于边长是偶数,即2的倍数,那么总面积一定能被4整除),之所以减去1,是因为a不为0,要留出至少1个2*2的网格给1×1的小正方形,此时a是4的倍数。
- 奇数边长,可以把大正方形看作是一个边长为偶数的子正方形和一些额外的行或列组成。我们可以想象,子偶数边长正方形的边长为m = d - 1,那么最大能放置的maxB的个数为maxB = m * m / 4,这种情况下额外的行和列有2m + 1个格子,即对应奇数个1×1的小正方形。
思路简化:由于总面积符合大正方形,则只需2×2的小正方形不越界即可。
从而得到代码如下:
import java.util.Scanner;
public class Main {
public static boolean canFormSquare(int a, int b) {
int S = a + 4 * b;
int d = (int) Math.sqrt(S);
if (d * d != S) {
return false;
}
if (d % 2 == 0) {
int maxB = S / 4 - 1;
return b <= maxB && a % 4 == 0;
} else {
int m = d - 1;
int maxB = m * m / 4;
return b <= maxB && a % 2 == 1;
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int T = scanner.nextInt();
for (int i = 0; i < T; i++) {
int a = scanner.nextInt();
int b = scanner.nextInt();
if (canFormSquare(a, b)) {
System.out.println("YES");
} else {
System.out.println("NO");
}
}
scanner.close();
}
}
3.第k小的前缀串
题目分析:
题目描述:
给定一个长度为n的仅由小写字母组成的字符串s,对于任意的0 <= i <= n-1,区间为[0, i]的子串都是字符串s的前缀串。
现在将这n个前缀串按如下规则排序:
- 先在每个前缀串内部,将其字符按照字典序升序排列
- 然后在各个前缀串之间,将他们按照字典序升序排列
对于任意的两个字符串,从头开始逐个比较其字符,如果一个字符串已结束而另一个字符串还有余,仍未分出大小,则长度较短的这个字符串按字典序排在前面。
给定一个整数k(1 <= k <= n),求这样排序后第k小的前缀串。
输入描述:
第一行是一个正整数T,表示有T组测试数据。
每组测试数据的第一行是两个用空格隔开的整数,分别是n和k;
每组测试数据的第二行是一个长度为n的仅有小写字符组成的字符串s。
输出描述:
每组测试数据的输出占一行,即第k小的前缀串。
解题思路:
这道题主要考察的是字符串的操作。由于字符串仅由小写字符组成,我们可以用一个长度为26的整型数组记录前缀串中不同字符的频次,从而在O(n*26)的时间复杂度内,构造n个前缀串。
至于不同前缀串之间的排序,由于直到遍历完所有前缀串之前都无法确定最终的顺序,所以必将有一个时间复杂度为O(nlogn)的排序算法对n个前缀串进行按字典排序。
时间复杂度分析:
构造前缀串:O(n*26);按字典排序前缀串:O(nlogn)。
最终得到时间复杂度为O(nlogn)的算法。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int T = scanner.nextInt();
for (int t = 0; t < T; t++) {
int n = scanner.nextInt();
int k = scanner.nextInt();
scanner.nextLine();
String s = scanner.nextLine();
String result = findKthSmallestPrefix(s, k);
System.out.println(result);
}
scanner.close();
}
public static String findKthSmallestPrefix(String s, int k) {
int n = s.length();
List<String> prefixes = new ArrayList<>();
int[] count = new int[26];
for (int i = 0; i < n; i++) {
char c = s.charAt(i);
count[c - 'a']++;
StringBuilder sortedPrefix = new StringBuilder();
for (int j = 0; j < 26; j++) {
for (int l = 0; l < count[j]; l++) {
sortedPrefix.append((char) ('a' + j));
}
}
prefixes.add(sortedPrefix.toString());
}
Collections.sort(prefixes);
return prefixes.get(k - 1);
}
}