题干
24点游戏是指,给你4个数,一般来说范围是1到13,然后用这个4个数算出24点。比如 11,8,3,5。可以(11-8)*(3+5)=24,那么这样就可以得到24了。当然也不是所有的数都能算出来,所以算不出来就直接返回算不出来即可。
这其实是一种扑克游戏,将一副牌分给两个人后,两个人每次随机抽出两张牌放在桌子上,然后用这4张牌的值计算出24点。
思路
穷举
我觉得我每次会犯一个错,就是觉得什么题都有很巧妙的解法。而我总是想一下想到那个巧妙的解法。这道题就是没有巧妙解法的题。而是只能优化最基础的解法。最基本的想法就是将所有能够计算算式全部列出来,也就是穷举,然后算一算是不是有结果24就好了。
很容易想到4个数,放在4个位置,就是一个组合问题,需要3个运算符。所以对数来说一共有4!=4 * 3 * 2 * 1=24个排列,而3个运算符,每个位置有4个选择,那就是4 * 4 * 4 = 64 。一共就该有 24 * 64 = 1536个组合方式。
当然到了这里之后很容易让人忽略一步,那就是还有括号的情况,我在想括号问题的时候还总是想迷糊了。所以这里使用书上的考虑方式,因为()只是改变了计算运算符的顺序。除了在没有括号的情况下,全部正序计算以外。其他情况均用括号改变先计算的运算符的顺序
一共六种情况,这也是一个组合问题
- 0、ABCD --> 没有括号的情况下是不知道哪一个先算
- 1、(A(B(CD))) -->321:先计算最后一个符号,再计算第二个符号,最后计算第一个符号
- 2、(A((BC)D)) -->231
- 3、((AB)(CD)) -->132
- 4、(((AB)C)D) -->123
- 5、((A(BC))D) -->213
动态规划
那么在穷举的基础上有什么办法优化呢?那就减少重复计算,比如对11,8,3,5而言,有一个算式是:(11-8)*(3+5)=24,但是(3+5)*(11-8)=24,但是 3+5 和 11-8 就计算了两次。其中有一次是多余的计算。对于这样的问题,就需要利用动态规划中的自底向上的方式了,那就是先计算(3+5)将结果保存起来,等到要用的时候,再取出来即可。而不需要每次都重新计算。这样就需要我们消耗额外的空间来保存一系列子集合(如果称11,8,3,5是完整的集合的话)。所以这也是一种用空间换时间的策略。最极端的情况书中也提到了,如果真的在游戏中实现的话,我们可以把所有输入和对应的算术表达式、结果都保存起来,这是因为对于4个数,最多13*13*13*13个输入,空间也不是很大。这就是用空间换时间的最极端情况。代码
我是用for循环完成了这道题,书中的解法是递归,更为高档一些。但是我还是认为我的解法好让人懂一些,这道题本来用java(c、python应该也是,都涉及将字符串与算术表达式的转换)这样的代码来实现就已经比较麻烦了。虽然确实完成了,但是我也在考虑了很久这是不是最好的写法。另外一个吸引我的就是我真的很想用最抽象的语言lisp写这个会是有方便吗?main函数的类:
public class Game24 {
static int[][] num = new int[24][4];//4个数的全组合就是24个数组
static int index=0;
/**
* 拿到这4个数的所有组合
* @param ss 这4个数字char数组形式
* @param i 前i位被忽略(或者说固定)
*/
public static void permutation(int[]ss,int i){
if(ss==null||i<0 ||i>ss.length){
return;
}
if(i==ss.length){
num[index][0] = ss[0];
num[index][1] = ss[1];
num[index][2] = ss[2];
num[index][3] = ss[3];
index++;
}else{
for(int j=i;j<ss.length;j++){
int temp=ss[j];
ss[j]=ss[i];
ss[i]=temp;
permutation(ss,i+1);
temp=ss[j];
ss[j]=ss[i];
ss[i]=temp;
}
}
}
public static void main(String args[]){
//ss数组就是输入的4个数
int []ss={1,2,22,3};
char []operator={'+','-','*','/'};
//拿到4个数的排列组合
permutation(ss,0);
//i代表数字的排序,j1、j2、j3是对应的符号,h是括号
//对于运算符而言:1代表+,2代表-,3代表*,4代表/
//对于括号而言一共有以下几种:
/**
* 使用书上的考虑方式更好一些,这是因为()只是改变了计算运算符的顺序。
* 除了在没有括号的情况下,全部正序计算以外。
* 其他情况均用括号改变先计算的运算符的顺序
* 一共六种情况,这也是一个组合问题
* 0、ABCD --> 没有括号的情况下是不知道哪一个先算
* 1、(A(B(CD))) -->321:先计算最后一个符号,再计算第二个符号,最后计算第一个符号
* 2、(A((BC)D)) -->231
* 3、((AB)(CD)) -->132
* 4、(((AB)C)D) -->123
* 5、((A(BC))D) -->213
*/
for(int i=0;i<index;i++){
for(int j1=0;j1<4;j1++){
for(int j2=0;j2<4;j2++){
for(int j3=0;j3<4;j3++){
for(int h=0;h<6;h++){
String expr;
int rslt;
switch (h) {
case 0:
expr = num[i][0]+""+operator[j1]+num[i][1]+operator[j2]+num[i][2]+operator[j3]+num[i][3];
rslt = (int)String2expr.evalbr(expr);
if(rslt==24){
System.out.println(expr+"="+rslt);
}
break;
case 1://(A(B(CD))) -->321:先计算最后一个符号,再计算第二个符号,最后计算第一个符号
expr = "("+num[i][0]+""+operator[j1]+"("+num[i][1]+operator[j2]+"("+num[i][2]+operator[j3]+num[i][3]+")))";
rslt = (int)String2expr.evalbr(expr);
if(rslt==24){
System.out.println(expr+"="+rslt);
}
break;
case 2://(A((BC)D)) -->231
expr = "("+num[i][0]+""+operator[j1]+"(("+num[i][1]+operator[j2]+num[i][2]+")"+operator[j3]+num[i][3]+"))";
rslt = (int)String2expr.evalbr(expr);
if(rslt==24){
System.out.println(expr+"="+rslt);
}
break;
case 3://((AB)(CD)) -->132
expr = "(("+num[i][0]+""+operator[j1]+num[i][1]+")"+operator[j2]+"("+num[i][2]+operator[j3]+num[i][3]+"))";
rslt = (int)String2expr.evalbr(expr);
if(rslt==24){
System.out.println(expr+"="+rslt);
}
break;
case 4://(((AB)C)D) -->123
expr = "((("+num[i][0]+""+operator[j1]+num[i][1]+")"+operator[j2]+num[i][2]+")"+operator[j3]+num[i][3]+")";
rslt = (int)String2expr.evalbr(expr);
if(rslt==24){
System.out.println(expr+"="+rslt);
}
break;
case 5://((A(BC))D) -->213
expr = "(("+num[i][0]+""+operator[j1]+"("+num[i][1]+operator[j2]+num[i][2]+"))"+operator[j3]+num[i][3]+")";
rslt = (int)String2expr.evalbr(expr);
if(rslt==24){
System.out.println(expr+"="+rslt);
}
break;
default:
break;
}
}
}
}
}
}
}
}
辅助类:计算字符串形式的算术表达式的结果
import java.util.*;
import java.math.*;
public class String2expr
{
static double ADD = Math.PI; //加
static double SUB = Math.PI + 1; //减
static double MUL = Math.PI + 2; //乘
static double DIV = Math.PI + 3; //除
static double LBR = Math.PI + 4; //左括号
static double RBR = Math.PI + 5; //右括号
//按表达式中出现的次序存储提取出的操作数和操作符
static ArrayList<Double> al = new ArrayList<Double>();
//计算含括号的表达式的值
public static double evalbr(String expr)
{
al.clear();//为了多次运算不出错先将保存结果的代码清空
//解析这个expr表达式,结果存入arraylist
parse(expr);
//左右括号位置
int lbr_pos = 0;
int rbr_pos = 0;
//如果表达式中还有括号的话,就从右向左继续计算各个括号对里表达式的值,并替换化简
while(al.contains(LBR))
{
//最右边的左括号位置
lbr_pos = al.lastIndexOf(LBR);
//其对应的右括号
rbr_pos = lbr_pos + 1;
while(al.get(rbr_pos) != RBR) rbr_pos++;
//计算该括号对内表达式的值, 并替换化简
eval(lbr_pos + 1, rbr_pos - 1);
//删除括号对
al.remove(lbr_pos + 2);
al.remove(lbr_pos);
}
//计算所有括号化简掉后的表达式的值
eval(0, al.size() - 1);
//化简到最后剩下的唯一一个操作数就是答案
return al.get(0);
}
//计算不含括号的表达式的值, 该表达式表示为al中从start到end的一段元素
public static void eval(int start, int end)
{
//元素在当前化简表达式中的位置
int i = start;
//化简的停止位置
int stop = end;
//从左向右计算乘除法,每消去一个操作符则更新al
//因为乘除法要先计算
while(i <= stop)
{
double element = al.get(i);
//临时存放结果
double rslt = 0;
//如果是乘除操作符,则计算该符结果并更新表达式为化简后的
if(element == MUL)
{
rslt = al.get(i - 1) * al.get(i + 1);
//删除操作符和右操作数,左操作数用本步计算结果代替
al.remove(i + 1);
al.remove(i);
al.set(i - 1, rslt);
stop = stop - 2;
//display();
}
else if(element == DIV)
{
rslt = al.get(i - 1) / al.get(i + 1);
al.remove(i + 1);
al.remove(i);
al.set(i - 1, rslt);
stop = stop - 2;
//display();
}
else
{
i++;
}
}
i = start;
//从左向右计算加减法,每消去一个操作符则更新al
while(i <= stop)
{
double element = al.get(i);
//临时存放结果
double rslt = 0;
//如果是乘除操作符,则计算该符结果并更新表达式为化简后的
if(element == ADD)
{ rslt = al.get(i - 1) + al.get(i + 1);
al.remove(i + 1);
al.remove(i);
al.set(i - 1, rslt);
stop = stop - 2; //因为remove掉了两个
//display();
}
else if(element == SUB)
{
rslt = al.get(i - 1) - al.get(i + 1);
al.remove(i + 1);
al.remove(i);
al.set(i - 1, rslt);
stop = stop - 2;
//display();
}
else
{
i++;
}
}
}
//提取字符串中的运算符和操作数并存储到al中
public static void parse(String expr)
{
//去掉所有空格
String str = expr.replace(" ", "");
//标记单目操作符负号位置
int minus_pos = 0;
//将表达式中的所有单目运算符负号转化成双目运算符减号,补充上左操作数0
while( (minus_pos = str.indexOf("(-")) != - 1)
{
str = str.substring(0, minus_pos + 1) + "0" + str.substring(minus_pos + 1);
}
//一个操作数、操作符的开始位置
int pos_start = 0;
//一个操作数、操作符的结束位置
int pos_end = 0;
//将字符串转换为字符数组
char[] arr = str.toCharArray();
//逐个提取表达式中的操作数和操作符
while(pos_start != arr.length )
{
pos_end = pos_start;
while( Character.isDigit(arr[pos_start]) == Character.isDigit(arr[pos_end]) || arr[pos_end] == '.')
{
pos_end++;
if(pos_end == arr.length)
{
break;
}
}
//提取到的操作符或操作数字符串
String tmp = new String(arr, pos_start, pos_end - pos_start);
if(Character.isDigit(arr[pos_start]))
{
//将提取出的操作数放入arraylist
al.add(Double.parseDouble(tmp));
}
//将提取出的操作符放入arraylist
else
{
char[] op = tmp.toCharArray();
for(char c : op)
{
if(c == '+')
al.add(ADD);
else if(c == '-')
al.add(SUB);
else if(c == '*')
al.add(MUL);
else if(c == '/')
al.add(DIV);
else if(c == '(')
al.add(LBR);
else if(c == ')')
al.add(RBR);
else
throw new RuntimeException("Operator not allowd : " + tmp);
}
}
pos_start = pos_end;
}
}
//显示化简到当前一步的表达式
public static void display()
{
//输出提取到的所有元素
for(int i = 0; i < al.size(); i++)
{
if(al.get(i) == ADD)
System.out.print("+");
else if(al.get(i) == SUB)
System.out.print("-");
else if(al.get(i) == MUL)
System.out.print("*");
else if(al.get(i) == DIV)
System.out.print("/");
else if(al.get(i) == LBR)
System.out.print("(");
else if(al.get(i) == RBR)
System.out.print(")");
else
System.out.print(al.get(i));
}
System.out.println("\n");
}
}
结果
1-2+22+3=24
((1-2)+(22+3))=24
(((1-2)+22)+3)=24
(1-(2-(22+3)))=24
((1-(2-22))+3)=24
(1-((2-22)-3))=24
1-2+3+22=24
((1-2)+(3+22))=24
(((1-2)+3)+22)=24
(1-(2-(3+22)))=24
((1-(2-3))+22)=24
(1-((2-3)-22))=24
1+22-2+3=24
....