第八届蓝桥杯国赛 Java B组 第五题 标题:填字母游戏

小明经常玩 LOL 游戏上瘾,一次他想挑战K大师,不料K大师说:
“我们先来玩个空格填字母的游戏,要是你不能赢我,就再别玩LOL了”。

K大师在纸上画了一行n个格子,要小明和他交替往其中填入字母。

并且:

1. 轮到某人填的时候,只能在某个空格中填入L或O
2. 谁先让字母组成了“LOL”的字样,谁获胜。
3. 如果所有格子都填满了,仍无法组成LOL,则平局。

小明试验了几次都输了,他很惭愧,希望你能用计算机帮他解开这个谜。

本题的输入格式为:
第一行,数字n(n<10),表示下面有n个初始局面。
接下来,n行,每行一个串,表示开始的局面。
  比如:“******”, 表示有6个空格。
  “L****”,   表示左边是一个字母L,它的右边是4个空格。


要求输出n个数字,表示对每个局面,如果小明先填,当K大师总是用最强着法的时候,小明的最好结果。
1 表示能赢
-1 表示必输
0 表示可以逼平

例如,
输入:
4
***
L**L
L**L***L
L*****L

则程序应该输出:
0
-1
1
1

资源约定:
峰值内存消耗 < 256M
CPU消耗  < 1000ms

请严格按要求输出,不要画蛇添足地打印类似:“请您输入...” 的多余内容。

所有代码放在同一个源文件中,调试通过后,拷贝提交该源码。
注意:不要使用package语句。不要使用jdk1.7及以上版本的特性。
注意:主类的名字必须是:Main,否则按无效代码处理。

这道题在其它博友那里也有源码,我看过好几个,多数是用c++写的。作为Java爱好者,当然想找到Java版本的,于是翻了好久,终于翻到一篇。这篇来自于https://blog.csdn.net/nibaby9/article/details/79748889,我试过将源码放到蓝桥杯练习系统上评分,结果测试数据全都超时了。还把一篇用c++写的源码https://blog.csdn.net/krypton12138/article/details/79631866也放了上去,官网5个测试数据也就一个通过,也就是得了20分,其它的测试数据也都是超时。想想看,c++都超时更别说是Java了,Java不仅超时还超内存。我们都知道Java是运行与虚拟机上的,消耗的内存肯定比c++大得多。

所以,这种题完完全全暴力破解肯定是不行的,多少做点优化啊!毕竟是国赛的题,没想象中那么简单的,时间复杂度和空间复杂度上还有很多要考虑的,不是有思路会做就行的。


我就拿别人的代码做了一些优化,主要优化点有以下几个:

1.由题意知,k大师给小明留下了LO*,L*L,*OL这三种局面时,小明这是肯定能赢的,不用过多解释了吧。

2.在以下代码的f()方法中,要判断一个字符串中是否包含某个字符串时,估计很多人想到的是调用contains(String str)或indexOf(String str)方法吧,从API中可以看出形参是String类型,应该都知道Java处理String速度很慢吧?这样调用肯定是不可行的!所以,我就耍了点小聪明(其实也是挺笨的方法),进入到indexOf(String str)方法追踪底层源码,发现了底层调用的是这个方法static int indexOf(char[] source, int sourceOffset, int sourceCount,char[] target, int targetOffset, int targetCount, int fromIndex),由于是包访问权限的,所以没法直接String.indexOf这样调用,我就把它源码原封不动拷"偷"(拷贝)出来。优化了这点,我又把源码放上系统,发现空间由200+M将到了20+M,这么小小的一个动作,竟然能省下那么多内存(不知道真正比赛有没给出底层源码)。不过,还不行,时间上还是超时的。

3.我们仔细再分析,加入初始局面是***,我们先填第一个位置L**,然后填第二个位置LL*。反过来,我们先填第二个位置*L*,再填第一个位置LL*,很显然会有重复局面出现吧?这时就需要记录下出现过的局面的情况了,然后遇到相同局面时直接判定是赢是输还是平就好了。但是这要怎么记录呢?

    我们知道每个位置都可能有三种情况,也就是L,O,*,那么假如给出的是***,那么局面就有3^3=27种了。我第一反应就是想到用数组去存,但是下标怎么定义呢?开始我也想了很久。后来开窍了,可以把L当作1,O当作2,*当作0啊,这样表示下来不就是三进制数了吗?比如LOL等同于121=16。下标就这样确定了。

4.做完第3个优化,计算3^k时,还是调用Math.pow(double a,double b)这个方法算的,我又查看这个方法的底层源码了,发现虽然是native的,但是形参和返回值都是double,而我们要算的是整数的整数次幂。将一个小数强转为整数是会失去精度的啊,所以直接调用这个方法不行啊。所以,还得自己写个方法专门用来算3^k了。在算3^k方时,估计也很多人想到是用for循环然后i从0到k,乘k次然后用一个变量保存起来。其实计算机做移位运算比乘除法快多了,我这里就用了点小技巧,将乘法转成移位运算和加法。例如 a*3 = (a << 1) + a。真的不要小看这个优化,对程序影响还是蛮大的。


做完4点以上优化暂且能拿60分了,起码“及格”了(有组测试数据在自己电脑上能算出来,但是系统直接说运行错误,可能是系统的配置没有自己电脑配置好吧)。目前知道还存在个问题,就是数组长度不能大于Integer的最大值,如果给出的串长度较长时(我们知道指数型增长非常快,长度又是3^k,在k大于20时就已经超出Integer范围了),记录的数组就没法用了。毕竟能力有限,看来看去不知道那里还能优化了,望大神能指点下。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

	static char[] lox = { 'L', 'O', '*' };
	static char[] lxl = { 'L', '*', 'L' };
	static char[] xol = { '*', 'O', 'L' };

	static char[] x = { '*' };

	/**
	 * 来源于java.lang.String
	 * 
	 * 在source字符数组中查找target字符数组所在位置
	 * 
	 * @param source
	 *            源字符数组
	 * @param sourceOffset
	 *            源数组起始位置
	 * @param sourceCount
	 *            源数组参与查找的字符个数
	 * @param target
	 *            待查找数组
	 * @param targetOffset
	 *            待查找数组起始位置
	 * @param targetCount
	 *            待查找参与的字符个数
	 * @param fromIndex
	 *            查找起始位置
	 * @return 返回第一次出现的第一个字符所在位置,不存在返回-1
	 */
	static int indexOf(char[] source, int sourceOffset, int sourceCount,
			char[] target, int targetOffset, int targetCount, int fromIndex) {
		if (fromIndex >= sourceCount) {
			return (targetCount == 0 ? sourceCount : -1);
		}
		if (fromIndex < 0) {
			fromIndex = 0;
		}
		if (targetCount == 0) {
			return fromIndex;
		}

		char first = target[targetOffset];
		int max = sourceOffset + (sourceCount - targetCount);

		for (int i = sourceOffset + fromIndex; i <= max; i++) {
			if (source[i] != first) {
				while (++i <= max && source[i] != first)
					;
			}

			if (i <= max) {
				int j = i + 1;
				int end = j + targetCount - 1;
				for (int k = targetOffset + 1; j < end
						&& source[j] == target[k]; j++, k++)
					;

				if (j == end) {
					return i - sourceOffset;
				}
			}
		}
		return -1;
	}

	/**
	 * 回溯试探博弈,暴力破解
	 * 
	 * 非常耗时,c++都超时别说是Java,时间复杂度2^n啊
	 * 
	 * 能不能做点优化或者用动态规划实现?
	 * 
	 * @param c
	 * @return
	 */
	static int f(char[] c, short[] jl) {
		// 处处是套路,直接调用肯定不行
		// String s = new String(c);

		// 像这种情况应该做个备忘录吧?
		// 查找到数组对应的索引值
		int idx = getJLindex(c);
		if (jl[idx] != 0) {
			if (jl[idx] == 2) {
				return 0;
			}
			return jl[idx];
		}

		// 偷API果然省下不少的空间,在运行上也变快了

		// 仔细想想,其实对方留下XOL,LXL,LOX这种局面时,必然是赢的
		if (indexOf(c, 0, c.length, lox, 0, 3, 0) != -1) {
			jl[idx] = 1;
			return 1;
		}

		if (indexOf(c, 0, c.length, lxl, 0, 3, 0) != -1) {
			jl[idx] = 1;
			return 1;
		}

		if (indexOf(c, 0, c.length, xol, 0, 3, 0) != -1) {
			jl[idx] = 1;
			return 1;
		}

		// 在序列中找不到可以填的位置了,然后有没形成LOL(能形成的都被上面return了),那肯定平了。
		if (indexOf(c, 0, c.length, x, 0, 1, 0) == -1) {
			jl[idx] = 2;
			return 0;
		}

		// if (s.contains("LOL"))
		// return -1;
		// if (s.contains("*") == false)
		// return 0;// 出口勿漏

		boolean ping = false;// 假设无法逼平

		for (int i = 0; i < c.length; i++) {
			// 每个位置有三种情况L,O,*
			// 这种填法会出现相同的局面,重点是要重复判断,如果能记录下局面,遇到相同局面就立马能判定胜负
			if (c[i] == '*') {
				try {
					c[i] = 'L';// 试探填L
					if (f(c, jl) == -1) {
						jl[idx] = 1;
						return 1;
					} else if (f(c, jl) == 0)
						ping = true; // 不能直接返回0,否则不能进行进一步试探

					c[i] = 'O';// 试探填O
					if (f(c, jl) == -1) {
						jl[idx] = 1;
						return 1;
					} else if (f(c, jl) == 0)
						ping = true;

				} finally {
					c[i] = '*';// 回溯
				}
			}
		}

		if (ping) {
			jl[idx] = 2;
			return 0;
		}

		jl[idx] = -1;
		return -1;
	}

	/**
	 * 根据数组获得索引值
	 * 
	 * @param c
	 * @return
	 */
	private static int getJLindex(char[] c) {
		int l = c.length;
		int sum = 0;

		// 我这里用了类比三进制数的表示法,从00...0到22...2,下标值转成十进制
		// 其中L表示1,O表示2,*则表示0,例如LOL三进制表示121,索引16
		for (int i = 0; i < l; i++) {
			if (c[i] == '*') {
				continue;
			} else if (c[i] == 'L') {
				sum += pow3(i);
			} else {
				//这里是2*3^i的意思
				sum += (pow3(i) << 1);
			}
		}
		return sum;
	}
	
	/**
	 * 求3的k次方
	 * @param num
	 * @param k
	 * @return
	 */
	private static int pow3(int k) {
		int pow = 1;
		for(int i = 1;i <= k;i++) {
			pow = (pow << 1) + pow;
		}
		return pow;
	}

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(
				new InputStreamReader(System.in));
		int n = Integer.parseInt(br.readLine());
		// 存储输入的字符串
		String[] str = new String[n];
		for (int i = 0; i < n; i++) {
			str[i] = br.readLine();
		}

		for (int i = 0; i < n; i++) {
			int l = str[i].length();
			int len = pow3(l);
			// 用于记录局面所得到的结果,当长度较长时出现OOM(java.lang.OutOfMemoryError)
			// 因为每个位置有三种值,所以没法用boolean表示了,所以开辟了short数组
			// 默认值0用于标识这种局面还没遇过,其它值表示遇到过相同的局面了,1表示胜,2表示平,-1表示负
			short[] record = new short[len];
			System.out.println(f(str[i].toCharArray(), record));
		}
	}

}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值