取球博弈问题 (第七届蓝桥杯个人省赛JavaB组真题)
题目描述:
取球博弈
两个人玩取球的游戏。
一共有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
思路:
我们先来理解一下题意:
表示每局先取球的人能否获胜。
能获胜则输出+,
次之,如有办法逼平对手,输出0,
无论如何都会输,则输出-
看到这里,我们基本的思路是:
采用深度遍历搜索(dfs)遍历所有不同的走法,得出每种走法会出现的结果。
只要在遍历过程中发现有先取球的人获胜的结果出现,那就不用再继续遍历下去了,直接return +
若程序遍历完所有的走法,说明所有走法都没有出现先取球的人获胜的结果,那么,在我们遍历完的所有走法中,只要有出现平局的情况,则返回0,没有平局的情况则返回 -
但是
若按照以上思路去解我们就漏掉了题意当中很关键的一个点:
假设双方都采用最聪明的取法
举一个具体例子来理解:
一局游戏,开局总球数为N = 5,每次可取的数目可以是n1 = 1;
n2 = 2; n3 = 3; 玩家A B;设A为先取球的人。
双方都采用最聪明的走法:
走法 | 第一次取球 | 第二次取球 | 第三次取球 | 结果 |
---|---|---|---|---|
走法一 | A取1颗球 | B取3颗球 | A只能取1颗球 | B胜 |
走法二 | A取2颗球 | B取3颗球 | B胜 | |
走法三 | A取3颗球 | B取1颗球 | A只能取1颗球 | B胜 |
以上,为双方均采用最聪明的走法时所出现的所有可能情况。
结果表明A无法胜利,该局游戏应该return -
如果按照我们一开始的基本思路来设计程序,程序会遍历到一种情况
第一次取球 | 第二次取球 | 第三次取球 | 第四次取球 | 第五次取球 | 结果 |
---|---|---|---|---|---|
A取1颗球 | B取1颗球 | A取1颗球 | B取1颗球 | A取1颗球 | A胜 |
此时程序就会输出 + 显然不符合题意,得到了错误的答案。
因此, 我们采用深度遍历搜索(dfs)遍历所有不同的走法时,还要避免 “不聪明” 的走法带来的错误结果。
除此之外,
直接深度优先搜索肯定不行,因为如果有1000个球那么每次有三种取法,一共差不多就有3的几百次方。这个数据简直是天文数字。
因此需要进行记忆化存储,将原先算过的数据进行保存。但是要保存所有数据也是不现实的,因为我们需要保存的结果是当剩下n个球,自己有m个球,别人有k个球时候的输赢。那么需要一个三维数组a[n][m][k],而n,m,k最大为1000,就需要1000M左右内存也不现实。
怎么办呢? 还是看题目要求,是要我们判断最后两个人球的奇偶,也就是说我们只要管奇偶性就可,也就是说当剩下300个球,自己3个球,别人8个球的情况和当剩下300个球,自己5个球,别人2个球这种情况最后输赢是一样的。因为自己球再多也是奇数,别人球再少也是偶数,而可选是球总数一样所以最后奇偶性也是一样的,输赢也一样。
代码实现:
无记忆深度优先搜索代码实现
首先我们把基础的深度优先搜索代码写出来,不要管记忆的问题。至少在小数据上是妥妥的过。下面是不加记忆的深度优先搜索。
public static void main(String[] args){
twentyFour(); }
/**
* 取球博弈问题
*/
public static void twentyFour(){
Scanner scanner = new Scanner(System.in);
String nStr = scanner.nextLine();
String xStr = scanner.nextLine();
//n1 n2 n3储存在该数组中(每次可取球数的集合)
int[] nArray = new int[3];
//x1 x2 x3 x4 x5储存在该数组中(5局的初始球数)
int[] xArray = new int[5];
//每次可取的最小球数
int min = 99;
//将输入的值初始化以上两个数组
String[] split;
split = nStr.split(" ");
for (int i = 0;i < 3;i++){
nArray[i] = Integer.valueOf(split[i]);
}
split = xStr.split(" ");
for (int i = 0;i < 5;i++){
xArray[i] = Integer.valueOf(split[i]);
}
//找到每次可取的最小球数
for (int i : nArray){
if (i < min){
min = i;
}
}
StringBuffer stringBuffer = new StringBuffer();
//五局比赛 五次循环
for (int i = 0;i < 5;i++){
char result = dfs(0,0,0,xArray[i],nArray,min);
stringBuffer.append(result + " ");
}
System.out.println(stringBuffer.toString());
}
/**
*
* @param player 本轮次取球的玩家 0表示本局游戏第一个取球的玩家A 1表示另一名玩家B
* @param ABallNum A玩家手上拥有的球数
* @param BBallNum B玩家手上拥有的球数
* @param totalBallNum 目前剩余的可取球数
* @param eachGetBallNum 本轮该玩家的取球数目
* @param min 每次可取的最小球数,用于判断游戏结束
* @return
*/
public static char dfs(int player, int ABallNum, int BBallNum, int totalBallNum,
int[] eachGetBallNum, int min){
//判定胜负
if (min > totalBallNum){
if (ABallNum%2 == 0 && BBallNum%2 == 1){
return '-';
} else if (ABallNum%2 == 1 && BBallNum%2 == 0){
return '+';
} else {
return '0';
}
}
char result = 'a';
char temp = 'a';
if (player == 0){
for (int i = 0;i < 3;i++){