蓝桥杯2016年省赛[第七届]-JavaB组赛题解析
蓝桥杯官方讲解视频:https://www.lanqiao.cn/courses/2737
真题文档:https://www.lanqiao.cn/courses/2786/learning/?id=67641
与由于篇幅原因,此届真题解析分为两篇,本篇是下篇,主要是8-10题和总结。1-7题解析见上篇文章
蓝桥杯2016年省赛[第七届]-JavaB组赛题解析(上)
本篇3个编程题,每个题都给出了两种方法(有的方法会超时)。
题8.四平方和[21分]
1.题目描述
四平方和定理,又称为拉格朗日定理:
每个正整数都可以表示为至多4个正整数的平方和。
如果把0包括进去,就正好可以表示为4个数的平方和。
比如:
5 = 0^2 + 0^2 + 1^2 + 2^2
7 = 1^2 + 1^2 + 1^2 + 2^2
(^符号表示乘方的意思)
对于一个给定的正整数,可能存在多种平方和的表示法。
要求你对4个数排序:
0 <= a <= b <= c <= d
并对所有的可能表示法按 a,b,c,d 为联合主键升序排列,最后输出第一个表示法。
程序输入为一个正整数N (N<5000000)
要求输出4个非负整数,按从小到大排序,中间用空格分开
例如,输入:
5
则程序应该输出:
0 0 1 2
再例如,输入:
12
则程序应该输出:
0 2 2 2
再例如,输入:
773535
则程序应该输出:
1 1 267 838
资源约定:
峰值内存消耗(含虚拟机) < 256M
CPU消耗 < 3000ms
请严格按要求输出,不要画蛇添足地打印类似:“请您输入…” 的多余内容。
所有代码放在同一个源文件中,调试通过后,拷贝提交该源码。
注意:不要使用package语句。不要使用jdk1.7及以上版本的特性。
注意:主类的名字必须是:Main,否则按无效代码处理。
2.简要分析
-
题目的意思很简单,就是要找四个非零整数,让他们的平方和等于n。
-
但是,最重要的一点是,题目只要求输出联合主键升序排列。
-
这样我们就可以使用暴力的方法去枚举这些数字。
- 我们可以使用a,b,c,d四个变量去枚举。并且假设按照升序的顺序去找这些数字,反正程序只要求输出第一组。
- 仔细一想,发现a,b,c确定之后,只需要判断
n-a*a-b*b-c*c
是不是一个平方数就行了,这样就优化掉一层循环。 - 然后a,b,c三个变量的遍历终点肯定可以进行优化,对于a,它一定小于根号n,对于b,它一定小于根号n-a方,对于c,它一定小于根号n-a方-b方。
- 经过上面的优化,最终这种枚举的方法,在百万数据时,可以稳定100ms以内,应该是能通过题目的所有点的。
-
除了最普通的枚举优化,其实我们还可以用哈希表来进行优化:
- 我们可以先将前两个数的平方和存入哈希表,再枚举后面两个数的可能性。
- 如果n-后面两位数的平方和在哈希表中,那么可确定这就是满足条件的第一个数。(与顺序有关的实现见代码)
-
方法二哈希表会占用一部分空间,并且哈希表的操作也有时间消耗。
-
通过n取1000000-5000000中随机一万个数进行测试,测试结果表示,方法一的平均用时为
2ms
,方法二的平均用时为95ms
。
3.实现代码1(普通枚举优化)
public class _2016_b8 {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
long start=System.currentTimeMillis();
double m=Math.sqrt(n);
for(int a=0;a<=m;a++){
double endb=Math.sqrt(n-a*a);
for(int b=0;b<=endb;b++){
double endc=Math.sqrt(n-a*a-b*b);
for(int c=0;c<=endc;c++){
int d=n-a*a-b*b-c*c;
if(isSquare(d)){
System.out.println(a+" "+b+" "+c+" "+(int)Math.sqrt(d));
long end=System.currentTimeMillis();
System.out.println("耗时:"+(end-start)+" ms");
return;
}
}
}
}
}
public static boolean isSquare(int num) {
double a = 0;
try {
a = Math.sqrt(num);
} catch (Exception e) {
return false;
}
int b = (int) a;
return a - b == 0;
}
}
4.实现代码2(哈希枚举优化)
public class _08四平方和 {
static int N;
static Map<Integer,Integer> cache=new HashMap<Integer,Integer>();
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
N = sc.nextInt();
for (int c = 0; c*c <=N/2 ; ++c) {
for (int d = c; c*c+d*d <= N; ++d) {
if(cache.get(c*c+d*d)==null)
cache.put(c*c+d*d,c);
}
}
for (int a = 0; a*a <=N/4 ; ++a) {
for (int b = a; a*a+b*b<=N/2 ; ++b) {
if(cache.get(N-a*a-b*b)!=null){
int c = cache.get(N-a*a-b*b);
int d=(int)sqrt(N-a*a-b*b-c*c);
System.out.printf("%d %d %d %d\n",a,b,c,d);
return;
}
}
}
}
}
题9.取球博弈[23分]
1 .题目描述
两个人玩取球的游戏。
一共有N个球,每人轮流取球,每次可取集合{n1,n2,n3}中的任何一个数目。
如果无法继续取球,则游戏结束。
此时,持有奇数个球的一方获胜。
如果两人都是奇数,则为平局。
假设双方都采用最聪明的取法,
第一个取球的人一定能赢吗?
试编程解决这个问题。
输入格式:
第一行3个正整数n1 n2 n3,空格分开,表示每次可取的数目 (0<n1,n2,n3<100)
第二行5个正整数x1 x2 … x5,空格分开,表示5局的初始球数(0<xi<1000)输出格式: 一行5个字符,空格分开。分别表示每局先取球的人能否获胜。 能获胜则输出+, 次之,如有办法逼平对手,输出0,
无论如何都会输,则输出-例如,输入:
1 2 3
1 2 3 4 5程序应该输出:
+ 0 + 0 -
再例如,输入:
1 4 5
10 11 12 13 15程序应该输出:
0 - 0 + +
再例如,输入:
2 3 5
7 8 9 10 11程序应该输出:
+ 0 0 0 0
资源约定:
峰值内存消耗(含虚拟机) < 256M
CPU消耗 < 3000ms
请严格按要求输出,不要画蛇添足地打印类似:“请您输入…” 的多余内容。
所有代码放在同一个源文件中,调试通过后,拷贝提交该源码。
注意:不要使用package语句。不要使用jdk1.7及以上版本的特性。
注意:主类的名字必须是:Main,否则按无效代码处理。
2.简要分析
- 看题,发现是一个经典的博弈问题。
- 关键点在于如何理解都采用最聪明的解法。
- 在这离,最聪明的解法的含义应该是枚举所有可能的情况,选其中最好的情况。
- 对题中的我而言,最好的情况应该这样理解:如果对手有一次负的情况,那么我的最好情况就是赢了。如果没有负的情况,但是至少有一次平的情况,那么我的最好结果就是平了。如果对手没有负的情况,也没有平的情况,那么我无论如何都会输。
- 在每一个局面,都存在三种状态:剩余的球数,我手中的球数,对手手中的球数。
- 我们可以使用递归的方式去搜索所有可能的方法。
- 但是,还有一个重要的点是,在博弈类题中,一般要求双方都是最佳方案,所以在递归的时候需要交换自己和对方的状态就表示对方的决策。
- 有了上面的思路,可以写出的基本的递归代码了。
- 但是,数据规模还是有些大,普通的递归并不能通过所有的点(使用如下数据就会超时)。递归层数过深。
1 7 8
900 901 903 905 907
0 + - - +
-
因此,我们需要对这段递归代码进行优化。
-
优化的思路:
- 优化的思路主要是做记忆型递归。
- 在所有可能返回的地方,将当前的状态存储起来,记忆所有的状态,在执行递归前先查询这种状态是否存在,存在就直接返回。
- 这里面的状态,其实没必要是双方的球数目,只要奇偶性就已经可以确定唯一的状态了。
- 这样可以优化大量不必要的递归。
-
经过缓存优化的递归可以通过题目的所有点。
3.实现代码1(普通递归)(会超时)
public class _2016_b9 {
static int[] n=new int[3];
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
for(int i=0;i<3;i++){
n[i]=sc.nextInt();
}
//排序,便于取最小值判断是否还可以取
Arrays.sort(n);
for(int i=0;i<5;i++){
int num=sc.nextInt();
char res=f(num,0,0);
System.out.print(res+" ");
}
System.out.println();
}
private static char f(int num, int me, int you) {
//剩下的球不够取了,递归出口
if(num<n[0]){
if ((me&1)==1&&(you&1)==0)return '+';//我手中奇数个球,对手手中偶数个球,我赢
else if ((me&1)==0&&(you&1)==1)return '-';//我手中偶数个球,对手手中奇数个球,我输
else return '0';//其余情况是平局
}
boolean isPing=false;
//枚举取每一个球的可能情况
for(int i=0;i<3;i++){
//这个球已经不可取了,没有继续枚举的意义
if(num<n[i]){
break;
}
//看对手的情况
char res=f(num-n[i],you,me+n[i]);
//对手有负的一种情况,那么最好的结果是正的
if(res=='-'){
return '+';
}
//平局
if(res=='0'){
isPing=true;
}
}
//已经不存对手输的情况
//考虑是否有可能两人是平局
if(isPing){
return '0';
}else{
return '-';
}
}
}
4.实现代码2(缓存优化)
public class _09取球博弈 {
private static int[] n;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = new int[3];
for (int i = 0; i < 3; i++) {
n[i] = sc.nextInt();
}
Arrays.sort(n);//排序
int[] nums=new int[5];
for (int i = 0; i < 5; i++) {
nums[i] = sc.nextInt();
}
for (int i =0; i < 5; i++) {
char res = f(nums[i], 0, 0);
System.out.print(res + " ");
}
System.out.println();
}
static char[][][]cache = new char[1000][2][2];
/**
* 参数代表着当前取球人面临的局面
* @param num 球的总数
* @param me 我方持有的数目-->我方数目的奇偶性
* @param you 对手持有的数目-对方数目的奇偶性
* @return
*/
private static char f(int num, int me, int you) {
if (num<n[0])//不够取
{
if ((me&1)==1&&(you&1)==0)return '+';//我手中奇数个球,对手手中偶数个球,我赢
else if ((me&1)==0&&(you&1)==1)return '-';//我手中偶数个球,对手手中奇数个球,我输
else return '0';//其余情况是平局
}
//当前状态存在
if (cache[num][me][you]!='\0')return cache[num][me][you];
boolean ping = false;
//枚举取每一个球的可能情况
for (int i = 0; i < 3; i++) {
if (num >= n[i]) {
char res = f(num - n[i], you, (n[i]&1)==0?me:(1-me));//注意此处,传递me和you的奇偶性
if (res == '-')
{
cache[num][me][you]='+';
return '+';
}
if (res == '0')
ping = true;
}
}
//如果能走到第这行,说明不存在对手输的情况,那么是否存在平的情况
if (ping)
{
cache[num][me][you]='0';
return '0';
}
else
{
cache[num][me][you]='-';
return '-';
}
}
}
10.压缩变换[31分]
1.题目描述
小明最近在研究压缩算法。
他知道,压缩的时候如果能够使得数值很小,就能通过熵编码得到较高的压缩比。
然而,要使数值很小是一个挑战。
最近,小明需要压缩一些正整数的序列,这些序列的特点是,后面出现的数字很大可能是刚出现过不久的数字。对于这种特殊的序列,小明准备对序列做一个变换来减小数字的值。
变换的过程如下:
从左到右枚举序列,每枚举到一个数字,如果这个数字没有出现过,则将数字变换成它的相反数,如果数字出现过,则看它在原序列中最后的一次出现后面(且在当前数前面)出现了几种数字,用这个种类数替换原来的数字。
比如,序列(a1, a2, a3, a4, a5)=(1, 2, 2, 1, 2)在变换过程为:
a1: 1未出现过,所以a1变为-1;
a2: 2未出现过,所以a2变为-2;
a3: 2出现过,最后一次为原序列的a2,在a2后、a3前有0种数字,所以a3变为0;
a4: 1出现过,最后一次为原序列的a1,在a1后、a4前有1种数字,所以a4变为1;
a5: 2出现过,最后一次为原序列的a3,在a3后、a5前有1种数字,所以a5变为1。
现在,给出原序列,请问,按这种变换规则变换后的序列是什么。
输入格式:
输入第一行包含一个整数n,表示序列的长度。
第二行包含n个正整数,表示输入序列。
输出格式:
输出一行,包含n个数,表示变换后的序列。
例如,输入:
5
1 2 2 1 2
程序应该输出:
-1 -2 0 1 1
再例如,输入:
12
1 1 2 3 2 3 1 2 2 2 3 1
程序应该输出:
-1 0 -2 -3 1 1 2 2 0 0 2 2
数据规模与约定
对于30%的数据,n<=1000;
对于50%的数据,n<=30000;
对于100%的数据,1 <=n<=100000,1<=ai<=10^9
资源约定:
峰值内存消耗(含虚拟机) < 256M
CPU消耗 < 3000ms
请严格按要求输出,不要画蛇添足地打印类似:“请您输入…” 的多余内容。
所有代码放在同一个源文件中,调试通过后,拷贝提交该源码。
注意:不要使用package语句。不要使用jdk1.7及以上版本的特性。
注意:主类的名字必须是:Main,否则按无效代码处理。
2.简要分析
-
题目很好理解,基本的实现方式,模拟它的压缩过程,还是可以很快的做出来的。
- 我们可以用一个哈希表将数字与在数组中最后一次出现的下标存起来。(是一个不更新的过程)。
- 统计前面的数字种数的话,可以用set实现,从该数字上一次出现的位置到这一次出现的位置中的数全部存入set集合,最后set集合的数目就是数字的种数。
- 这样的模拟是可以完成题目的需求的,但是每一次的set操作,map操作都是耗时非常大的,整体的时间复杂度差不多是
O(n*n)
,n非常大时,无法在短时间内计算出来。大概只能得到30%
左右的分数。
-
上面简单模拟思路的高耗时主要在内层的区间线性扫描中,我们需要想办法将这一层进行优化。
-
我们可以将这个区间变为一个01序列,其中0代表a中对应的数目前不是最后一个,1代表a中对应的数目目前已经是最后一个了。
-
那么统计这个区间数字的种数,实际上是求区间的和,而求区间的和我们可以使用线段树来求,这样就可以将总的时间复杂度压缩为
O(n*logn)
。可以通过题目的所有测试用例。
3.实现代码1(暴力模拟)
- 哈希表加Set集合。
public class _10_压缩变换 {
static Map<Integer, Integer> lastIndex = new HashMap<Integer, Integer>();//数字与下标的映射
static int[] data;//记录原始数据
static int[] ans;//记录答案
private static int n;
public static void main(String[] args) {
// 处理输入
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
data = new int[n];
ans = new int[n];
for (int i = 0; i < n; i++) {
int num = sc.nextInt();
data[i] = num;
if (lastIndex.get(num) == null)//没出现过
{
ans[i] = -num;
} else {
//统计p_num和i之间有多少不同的数字
Set<Integer> set = new HashSet<Integer>();
for (int j = lastIndex.get(num) + 1; j < i; j++) {
set.add(data[j]);
}
ans[i] = set.size();
}
lastIndex.put(num, i);//更新
}
for (int i = 0; i < n; i++) {
System.out.print(ans[i] + " ");
}
}
}
4.实现代码2(线段树)
public class _10_压缩变换_2 {
static Map<Integer, Integer> lastIndex = new HashMap<Integer, Integer>();//数字与下标的映射
static int[] a;//记录原始数据
static int[] ans;//记录答案
static int[] b;//这是一个01序列,某一个位置p上的数字为1,代表着a[p]这个数字最后出现的位置是p,而a[p]曾经出现过的位置上都是0
private static int n;
private static SegTree root;
public static void main(String[] args) throws FileNotFoundException {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
a = new int[n];
ans = new int[n];
b=new int[n];
root = buildSegTree(0, n - 1);
for (int i = 0; i < n; i++) {
int num = sc.nextInt();
a[i] = num;
//之前最后一次出现的下标
Integer preIndex = lastIndex.get(num);
if (preIndex == null)//没出现过
{
ans[i] = -num;
b[i] = 1;
//更新线段树
update(root, i, 1);
} else {
//统计p_num和i之间有多少不同的数字
ans[i] = query(root, preIndex + 1, i - 1);//统计两个位置之间的1的个数==>求区间和
b[preIndex] = 0;
b[i] = 1;
//更新线段树
update(root, preIndex, -1);
update(root, i, 1);
}
lastIndex.put(num, i);//更新
}
for (int i = 0; i < n; i++) {
System.out.print(ans[i] + " ");
}
}
private static int query(SegTree tree, int x, int y) {
int l = tree.l;
int r = tree.r;
if (x <= l && y >= r) return tree.sum;
int mid = (l + r) / 2;
int ans = 0;
if (x <= mid) ans += query(tree.lson, x, y);
if (y > mid) ans += query(tree.rson, x, y);
return ans;
}
/*构建线段树*/
private static SegTree buildSegTree(int l, int r) {
SegTree segTree = new SegTree(l, r);
if (l == r) {
segTree.sum = b[l];
return segTree;
}
int mid = (l + r) / 2;
SegTree lson = buildSegTree(l, mid);
SegTree rson = buildSegTree(mid + 1, r);
segTree.lson = lson;
segTree.rson = rson;
segTree.sum = lson.sum + rson.sum;
return segTree;
}
static void update(SegTree tree, int p, int i) {
if (tree == null) return;
//更新根节点的sum
tree.sum += i;
int l = tree.l;
int r = tree.r;
int mid = (l + r) >> 1;
if (p <= mid) {
update(tree.lson, p, i);
} else {
update(tree.rson, p, i);
}
}
static class SegTree {
int l, r;//所有区间
int sum;//区间和
SegTree lson;//左子树
SegTree rson;//右子树
public SegTree(int l, int r) {
this.l = l;
this.r = r;
}
}
}
总结
-
总的来说,题目经过一定的思考还是能做出来,但是三个编程题都涉及到了算法的优化,如果优化不到位,还是无法拿到全部的分数。
-
列举一下这套试卷的考点:
- 1.找规律。
- 2.枚举。
- 3.全排列。
- 4.代码分析。
- 5.递归。
- 6.DFS连通性判断。
- 7.枚举缓存优化。
- 8.博弈框架。
- 9.记忆型递归。
- 10.线段树求区间和。
-
这套题中的常见的算法框架要能随手就写出来:
全排列+check
,DFS
,博弈
,记忆型递归
,线段树
。 -
难度不大,但要求非常熟练。
ATFWUS Writing 2021-1-24