1.题目分析
24点游戏是经典的纸牌益智游戏。
常见游戏规则:
从扑克中每次取出4张牌。使用加减乘除,第一个能得出24者为赢。
(其中,J代表11,Q代表12,K代表13,A代表1),按照要求编程解决24点游戏。
基本要求: 随机生成4个代表扑克牌牌面的数字字母,程序自动列出所有可能算出
24的表达式,用擅长的语言(C/C++/Java或其他均可)实现程序解决问题。
1.程序风格良好(使用自定义注释模板)
2.列出表达式无重复。
提高要求:用户初始生命值为一给定值(比如3),初始分数为0。随机生成
4个代表扑克牌牌面的数字或字母,由用户输入包含这4个数字或字母的运算
表达式(可包含括号),如果表达式计算结果为24则代表用户赢了此局。
1. 程序风格良好(使用自定义注释模板)
2.使用计时器要求用户在规定时间内输入表达式,如果规定时间内运算
正确则加分,超时或运算错误则进入下一题并减少生命值(不扣分)。
3.所有成绩均可记录在TopList.txt文件中。
分析与解法
最直接的想法就是采用穷举法,因为运算符号只有4种,每个数字只能使
用一次,所以通过穷举4个数所有可能的表达式,并分别计算出各表达式
的值,就可以得到答案。
先不考虑使用括号,可以做出如下分析:
因为每个数只能使用一次,那么就对4个数进行全排列,总共有4!=4 * 3 * 2 * 1=24
种排列。4个数的四则运算中总共需要3个运算符,同一运算符可以重复出现,
那么对于每一个排列,总共可有4 * 4 * 4种表达式。因此在不考虑括号的情况下,
总共可以得到4! * 4= 1536种表达式。
接下来再考虑加上括号后的情况,对于4个数而言,总共会有以下5种加括号的方式:
(A(B(CD))、 (A((BC)D)、 ((4B)(CD))、 (A(BC)D)、 (AB)C)D)。
所以需要遍历的表达式数最多有4! * 4 * 5 = 7680种。当然,这里可以采用逆波兰
表达式的方法,但其表达式数仍为4! * 4 * 5= 7680种。
通过上面的分析,得到了一种解24点的基本思路,即遍历运算符、数字和括号的
所有排列组合形式,接下来,我们将更加细致地讨论这种解法的一个具体实现。
假设给定的4个数组成的集合为A={1,2,3,4},定义函数f(A)为对集合A中的元素进
行所有可能的四则混合运算所得到的值。
首先从集合A中任意取出两个数,如取出1和2, A=A- {1,2},对取出来的数分别进
行不同的四则运算,1+2=3, 1-2=-1, 1/2=0.5, 1x2=2,将所得的结果再分别加入
集合A,可得到B={3,3,4}, C={-1,3,4}, D={0.5,3,4}, E= {, 3,4}四个新的集合,那么
f(A)=f(B)+f(C)+f(D)+J(E),通过以上的计算就达到了分而治之的目的,问题规模就
从4个数降到了3个数,成了3个数的4个子问题之和。
综上所述,可以得到递归解法为:
首先将给定的4个数放入数组Array中,将其作为参数传入函数f中,伪代码如下:
程序伪代码
if (Array. Length < 2) {
if (得到的最终结果为24)输出表达式)
else(输出无法构造符合要求的表达式)
}
foreach (从数组中任取两个数的组合) {
foreach (运算符( +,一,x, /) ) {
1.计算该组合在此运算符下的结果
2.将该组合中的两个数从原数组中移除,并将步骤1的计算结果放入数 组
3.对新数组递归调用f.如果找到一-个表达式则返回
4.将步骤1的计算结果移除,并将该组合中的两个数重新放回数组中对应的位置
}
}
2.关键算法构造
程序流程总图
3.程序实现
papackage www.homework.ChapterThree;
import java.io.;
import java.util.;
public class FourRandomGameExceptionThreadOf24 {
private static final int CardsNumber = 4;
private static final int ResultValue = 24;
private static double number[] = new double[CardsNumber];
private static String result[] = new String[CardsNumber];
private static HashMap<Integer,String> map = new HashMap<>();//存放结果Hash表,key为按递增序列排序的,方便对map的操作
private static int hashSize = 0;
private static HashMap<Integer,String> JQKA = new HashMap<>();//存放数字和扑克牌一一对应关系
private static ArrayList str = new ArrayList();
private static Set treeMap = new TreeSet<>();//用存放Person类的信息,方便按照分数由高到底输出
static {
JQKA.put(1,“A”);
JQKA.put(2,“2”);
JQKA.put(3,“3”);
JQKA.put(4,“4”);
JQKA.put(5,“5”);
JQKA.put(6,“6”);
JQKA.put(7,“7”);
JQKA.put(8,“8”);
JQKA.put(9,“9”);
JQKA.put(10,“10”);
JQKA.put(11,“J”);
JQKA.put(12,“Q”);
JQKA.put(13,“K”);
}
/**
* 对随机的四个数进行递归且24点,穷举了所有可能,将结果存储到map中
* @param n 要递归的操作数的个数
*/
private static void PointsGame(int n) {
if (n == 1) {
if (Math.abs(number[0] - ResultValue) == 0) {
if (!map.containsValue(result[0])) {
map.put(hashSize++,result[0]);
}
return ;
} else {
return ;
}
}
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
double a;
double b;
String expa;
String expb;
a = number[i];
b = number[j];
number[j] = number[n - 1];
expa = result[i];
expb = result[j];
result[j] = result[n - 1];
//避免了因交换律产生的多个重复的解
if (a <= b) {
result[i] = '(' + expa + '+' + expb + ')';
number[i] = a + b;
PointsGame(n - 1);
}
result[i] = '(' + expa + '-' + expb + ')';
number[i] = a - b;
PointsGame(n - 1);
result[i] = '(' + expb + '-' + expa + ')';
number[i] = b - a;
PointsGame(n - 1);
if (a <= b) {
result[i] = '(' + expa + '*' + expb + ')';
number[i] = a * b;
PointsGame(n - 1);
}
if (b != 0) {
result[i] = '(' + expa + '/' + expb + ')';
number[i] = a / b;
PointsGame(n - 1);
}
if (a != 0) {
result[i] = '(' + expb+ '/' + expa + ')';
number[i] = b / a;
PointsGame(n - 1);
}
number[i] = a;
number[j] = b;
result[i] = expa;
result[j] = expb;
}
}
}
/**
* 简单的开始界面函数
*/
private static void menu() {
System.out.println("*****************");
System.out.println("* 24点游戏 *");
System.out.println("* 1.开始 *");
System.out.println("* 2.排名 *");
System.out.println("* 3.结束 *");
System.out.println("*****************");
}
/**
* 对用户按照分数进行排序,通过TreeSet集合可以简单的实现,只要向里面存储就行了
* @throws FileNotFoundException
*/
public static void sort() throws FileNotFoundException {
Scanner input = new Scanner(new File("F:\\Program\\Java\\IDEA\\Arithmetic\\src\\www\\homework\\ChapterThree\\TopList.txt"));
//input.useDelimiter("\r\n");
while (input.hasNextLine()) {
String scannerRead = input.nextLine();
treeMap.add(new Person(scannerRead.split(" ")[0],Integer.valueOf(scannerRead.split(" ")[1])));
/*for (int i = 0; i < str.size(); i++) {
for (int j = i; j >0; j--) {
String[] spj = str.get(j).split(" ");
String[] spj_1 = str.get((j - 1)).split(" ");
if (Integer.valueOf(spj[1]) > Integer.valueOf(spj_1[1])) {
String temp = str.get(j);
str.set(j,str.get(j - 1));
str.set(j - 1,temp);
}
}
}*/
}
}
public static void main(String[] args) throws IOException {
Scanner input = new Scanner(System.in);
int select;
while (true) {
menu();
System.out.println("select:>");
select = input.nextInt();
if (select == 3) {
break;
} else if (select == 2) {
sort();
System.out.println(treeMap);
} else if (select == 1) {
System.out.println("请输入用户ID:");
input.nextLine();
String ID = input.nextLine();
Person person = new Person(ID);
while (person.life != 0) {
System.out.println("随机的4个扑克牌是");
for (int i = 0; i < CardsNumber; i++) {
int x = (int) (Math.random() * 13) + 1;
if (x <= 10 && x >= 2) {
System.out.print(x + " ");
} else {
System.out.print(JQKA.get(x) + " ");
}
number[i] = x;
result[i] = JQKA.get(x);
}
PointsGame(CardsNumber);
if (map.size() == 0) {
System.out.println();
System.out.println("这组组合没有解,进行下一组!");
continue;
} else {
System.out.println();
System.out.println("答案表如下!");
for (int mapKey : map.keySet()
) {
System.out.println(map.get(mapKey));
}
}
System.out.println("您有30秒的时间答题!");
System.out.println("请输入求24点的表达式:");
/**
* 倒计时操作
*/
int time = 0;
new Timer().schedule(new MyTimerTask1(), 2000);
// 下面这段代码是每隔1秒,打印下当前的时间
while (true) {
try {
if (input.hasNext()) {//在有输入的时候提前跳出,判断是否正确
break;
}
if (++time == 150) {
System.out.println("30秒时间到,进入下一题,减少1个生命值!");
System.out.println("你还剩下" + (--person.life) + "的生命值!");
System.out.println("1.继续");
System.out.println("2.不玩了");
for (int i = hashSize - 1; i >= 0; i--) {
map.remove(i);
}
break;
}
Thread.sleep(1000);//其实这是一个线程,让程序停滞1秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//用户再此输入答案
String personResult = input.nextLine();
if (personResult.equals("1")) {
continue;
} else if (personResult.equals("2")) {
break;
}
if (map.containsValue(personResult)) {
System.out.println("表达式正确,加10分!进入下一题!");
person.score += 10;
} else {
System.out.println("答题错误,进入下一题,减少1个生命值!");
System.out.println("你还剩下" + (--person.life) + "的生命值!");
}
map.clear();
}
if (person.life == 0) {
System.out.println("您的生命值已用完,游戏结束!");
} else {
System.out.println("您已结束游戏!");
}
System.out.println("用户:" + person.ID);
System.out.println("分数:" + person.score);
//存入文件的操作
File user = new File("F:\\Program\\Java\\IDEA\\Arithmetic\\src\\www\\homework\\ChapterThree\\TopList.txt");
Writer out = new FileWriter(user,true);
Scanner fileScanner = new Scanner(user);
ArrayList<String> fileReadStr = new ArrayList<>();
while (fileScanner.hasNextLine()) {
fileReadStr.add(fileScanner.nextLine());
}
/**
* 下面是判断用户是否是之前有过记录的用户
* 如果用具名称相同,只修改他现有的分数即可
* 其实这一部分用HashMap作为数据结构然后结合文件进行操作即可
* 这里是使用字符串切割然后比较的方式
*/
int contain = 0;
for (int i = 0; i < fileReadStr.size(); i++) {
if (fileReadStr.get(i).split(" ")[0].equals(person.ID)) {
fileReadStr.set(i,person.ID + " " + person.score + "\r\n");
out.write(fileReadStr.get(i));
contain = 1;
} else {
out.write(fileReadStr.get(i) + "\r\n");
}
}
if (contain == 0) {
out.write(person.ID + " " + person.score + "\r\n");
}
out.close();
}
}
System.out.println("游戏结束,祝您生活愉快!");
}
}
}
4.调试、测试及运行结果
4.1调试截图
前端用户的输入
程序内部变量
number数组是操作数数组,存放生成的随机扑克牌对应的数字
其中result数组是一个String类型的数组中吧保存扑克牌,递归最终result[0]为表达式最终解
我使用了HashMap JQKA作为存储数字和扑克牌之间的一一对应关系,方便对数字运算的时候,对字符串也进行操作
用户类信息,
包括
life:生命
ID:用户ID
score:用户的分数
开始递归的模样
过程截图,正在递归
递归的过程中找到了合适的答案
由图所示map中存放的就是所求的答案
如下图所示,找到了新的第4个答案
4.2测试截图
开始界面,选择1进行操作,输入用户名,随机产生数字
随机生成的数的所有答案
答对答案加10分
答错不扣分,减少生命值
当你的生命值用完之后可以选择继续下一个用户,可以结束游戏,也可以查看排名
第一个用户:wangchong答对了两题20分
第二个用户:gaoyangyang对了一题10分
按照分数有低到高排序,这里我是用了TreeSet保存用户自定义的类Person从而达到用户信息不会重复,并且Person类实现了Comparable接口,然后在Person类中重写了compareTo方法,使之按照分数大小排序
在文件中保存的是
5.经验归纳
本次实验,我做了好几个不同的版本,文件夹下面的FourRandomGameExceptionThreeOf24.java是最终的作业版本,里面实现了随机生成的扑克牌并存储所有可能算出二十四点的带有括号的中缀表达式,将这些所有的结果,存储到hash表,这样再用户输入答案的时候,程序可以方便的在hash表中匹配是否有这样的答案,如果有则用户回答正确,如果没有则回答错误。可以方便的为实现游戏功能。
PosExprePro.java实现了中缀表达式求解加减乘除的运算,也就是用栈实现的,只要程序调用这个类,并且传递给最终递归得到的结果就可以轻松的算处是否是24点,但是我并没有采用这个方式计算24点,因为它没有HashMap时间复杂度O(1)的快捷。并且他的两个栈的开销,计算时间也相对较大所以我选择了前者。
AllResultGameOf24.java下的程序是对于求二十四点的所有表达式,包含相同的运算方式但是只是操作数的结合方式不同而已,例如如果四个数的全加法正好等于二十四,那么所有操作数的位置不同,结合方式不同就会产生不同的答案。我认为这样的答案对程序员是不友好的,因为它明明就是一种解法,但是他对用户是非常友好的,因为你并不知道用户是如何输入答案的,作为程序员应该尽可能考虑用户的所有输入可能。
为了让我更加舒服的看程序,我想看看到底四个数可以生成的算二十四点的方式有几种,我设计了产生更纯粹答案的类AllPureResultGameOf24.java 它可以算出来更为纯粹的答案,就明确的得到答案的个数,实现方法很简单,在AllResultGameOf24.java类的基础上,将重复答案去除即可,因为产生重复答案的可能仅仅出现在加法和乘法操作中(因为加法和乘法的操作虽然操作数的位置不同但是结果必然是相同的),在一个答案表达式中,加号或乘号的个数小于3的颠倒操作数的是多种答案可能可以通过当其中其中一个操作数小于等于另外一个操作数的时候在进行加操作或者乘操作。当加号或乘号的个数为3个的时候,我只保留第一出现的这种情况,当第二次出线这种情况事,不保存他,这只需要对答案数组按照加号,或者乘号切分字符串即可,这样他们必然被切分为4个子字符串。
FourRandomGameExceptionThreadOf24.java类下面,在实现倒计时的时候,运用了多线程的功能,即在用户的输入的同时,计时的程序同时也在进行了,运用多线程是非常合适的。
还有一处亮点是在用户类Person.java下面实现了Comparable接口,这样在进行排名的时候就相当的方便了,我只需要吧Person类的相关信息保存在TreeSet集合中,并覆写了CompareTo方法保证按照score(用户分数)由大到小排列,这样,在输出排名信息的时候就直接对TreeSet进行输出即可,这样既能看见用户信息,用对用户的分数进行了排名,一举两得。