小明经常玩 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));
}
}
}