24点游戏7节课--第2节-24点计算处理

本文深入探讨了使用C++编程语言解决24点游戏的算法实现,包括构建公共定义文件、实现核心计算函数以及设计复杂的循环结构以遍历所有可能的数与操作符组合。通过详细讲解每个步骤,旨在为读者提供一个全面理解并实践游戏算法的指南。

     这是前面的课:  第一节-游戏介绍与基本算法 

    尽管写程序时,由上而下分解逻辑是常见的,但到了讲课,通常要先扫清一些边边角角的函数或定义。这一节课的作用就是这样的。

    不管用的什么IDE,先建立一个控制台工程(Console project)。

    然后加入一个 main.cpp,用于写C++的入口函数(main函数)——通常,你的IDE向导,会帮你生成这个文件及main函数。Code::Blocks就是这样,不过我们先不管它们。

   再新建 “common_def.h” 和"common_def.cpp" 文件,把它加入工程。

  一、 common_def.h 文件:

Code:
  1. #ifndef COMMON_DEF_H_INCLUDED  
  2. #define COMMON_DEF_H_INCLUDED  
  3.   
  4. /* 
  5.     为了方便运算,在内部并不直接使用'+','-' 
  6.     等字符表示加减乘除的操作符.  
  7.     而是使用 0,1,2,3分别表示加减乘除 
  8. */  
  9. typedef unsigned char operator_index;  
  10.   
  11. extern char operator_char_def[];  //'+', '-', '*' , '/'  
  12.   
  13. #endif // COMMON_DEF_H_INCLUDED  

   把 "unsigned char"  类型取个别名,存是为了直观。我们用(0,1,2,3)四个数来表达“加、减、乘、除” 。定义一个enum或许也很常见,但在本例中enum会带来不方便(以后再说,差不多类似于当年java抛弃enum的某些原因,当然,当然,我知道java后面又捡回来了)。

   既然用整数表达操作符,那 typedef  int  operator_index 似乎更直观?不过我还是故意避开int了,原因也以后再说。

   可以肯定,后面我们会需要显示这些操作符,这时还是直接显示“+”, "-", "*", "/" 直观可读。所以,我们又定义了一个char数组。具体内容见 cpp 文件:

     二、common_def.cpp 文件:

Code:
  1. #include "common_def.h"  
  2.   
  3. char operator_char_def[] = { '+''-''*''/' };  

   公共定义的源文件就这么简单,列出四个操作符而已。

   小测一下,从一个operator_index,要得到它的操作符字符,方法太简单了:

   operator_index oi = 2; //乘法

    cout << operator_char_def[oi]; //输出 '*' 字符 

   ==================================

   接下来,再新建一个 calc_24.h 和calc_24.cpp文件,将它们都加入到工程。

   三、calc_24.h 文件:

Code:
  1. #ifndef CALC_24_H_INCLUDED  
  2. #define CALC_24_H_INCLUDED  
  3.   
  4. #include "common_def.h"  
  5.   
  6. bool calc_24(int card[4]);  
  7.   
  8. #endif // CALC_24_H_INCLUDED  

   再简单不过了,声明了一个 calc_24 的函数。这个函数:

  入参是一个整数数组……当然,由于在c/c++语言中, 数组作为函数入参时,将退化为指针,所以该函数也相当于:

          bool calc_24(int* card);

    所以, 那个[4]其实一点用处没有,写1,写2,甚至什么都一个样,但这里我们还是一个4,用来提示该函数需要4张牌的数字。本例实在简单,对函数入参的检查略掉。

    假设我们在玩扑克时,拿到 红桃K,黑桃5, 黑桃2 ,方块8 , 那些花色对算24点没有什么用,将值组成int数组,传给calc_24数组即可。

  返回值: 为真表示用这4张牌算出24点了,为假表示算不出来,估计是无解。

       四、 calc_24.cpp 文件:

      这个文件显然最复杂。为了实现 calc_24函数,我们还需要一些边边角角的函数。下面代码是一段一段讲的,除非文中有特别提到插入在某处,否则代码就是相当于一点点往下写去。

     先是简单的头文件包含及名字空间引入等。

Code:
  1. #include <iostream>  
  2.   
  3. #include "calc_24.h"  
  4.   
  5. using namespace std;  

    然后是一个只需要计算两个数操作的函数,比如,给出 n1=1 和 n2= 2, 再给出  operator_index  为 0 ,则进行 1 + 2 的计算,结果为3,很简单不是? (如果o为2,则计算 1 * 2 = 2)。

    4.1 实现两数一步计算的函数:calc

Code:
  1. int calc(int n1, int n2, operator_index o)  
  2. {      
  3.     if (o < 0 || o >= 4)  
  4.         return -1;  
  5.       
  6.     switch(operator_char_def[o])  
  7.     {  
  8.         case '+' :  
  9.         {  
  10.             return (n1 + n2);  
  11.         }  
  12.         case '-' :  
  13.         {  
  14.             if (n1 < n2) //不允许出现负数,所以被减数不能小于减数  
  15.                 return -1;  
  16.                   
  17.             return (n1 - n2);  
  18.         }  
  19.         case '*' :  
  20.         {  
  21.             return (n1 * n2);  
  22.         }  
  23.         case '/' :  
  24.         {  
  25.             if ((n2 == 0) || (n1 % n2) !=0) //1)除数不为0; 2)要确保整除  
  26.                 return -1;  
  27.               
  28.             return (n1 / n2);              
  29.         }  
  30.     }  
  31.   
  32.     return -1;  
  33. }  

      第一节说过,这是小学生玩的游戏,不允许出现负数计算,所以当o为减法时,n1 < n2 ,则返回 -1 表示无效计算 ……对于通用的计算,因为计算结果也可能是-1……所以此时出错通常是抛出异常(c++),但我们的计算过程已经约定不能是负数了,所以用-1表示出错就可以了,调用者可以判断得出。

      除法需要判断的事情多了点:除0错肯定是要防的,然后就是不允许有余数,因为我们已经规定不允许在计算过程出现小数,同时也确实没有用double来表达一个张牌的值。

   4.2   连续计算

  第一节说过,排好一个算式时,我们有两种计算方法。第一种称为“连贯式”计算方法,比如: 1 + 2 * 3 - 4 , 用连贯计算,结果是5,而不是3!,因为我们只是从左到右连续拿数计算(先左后右),而不用考虑什么“先乘除后加减”优先级。

Code:
  1. int calc_consecutive(int n[4], operator_index o[3])  
  2. {  
  3.     int r1 = calc(n[0], n[1], o[0]);  
  4.       
  5.     if (r1 < 0)  
  6.         return 0;  
  7.           
  8.     int r2 = calc(r1, n[2], o[1]);  
  9.       
  10.     if (r2 < 0)  
  11.         return 0;  
  12.           
  13.     int r = calc(r2, n[3], o[2]);  
  14.       
  15.     return r;  
  16. }  

         代码太好读了:) 先拿前面两个数: n[0] 和n[1],用第一个操作符:o[0] 进行运算,得到临时结果 r。再拿 r 和第三个数n[2] 及第二个操作符o[1] 进行运算……

      calc 返回-1表示当前计算出错,此时可以放弃尝试,比如出现: 1 - 2 + 3 * 4 ,一开始计算 1-2 就可放弃(不用继续计算)了,因为我们不允许负数(原因,及它为什么并无影响结果正确性见第一节)。

    4.3 分隔式计算

   分隔式计算先算前两个数,再算后两个数,然后再用中间的操作符最后计算。比如: 同样是 1 + 2 * 3 - 4 ,则相当于是:(1+2) * (3-4) 结果是……噢,真不凑巧,出现负数,无效,换个例子: 5 + 3/ 3  - 1 ,结果不是 5, 而是 (5+3) / (3-1)得到4。

Code:
  1. int calc_separate(int n[4], operator_index o[3])  
  2. {  
  3.     int r1 = calc(n[0], n[1], o[0]);  
  4.       
  5.     if (r1 < 0)  
  6.         return 0;  
  7.   
  8.     int r2 = calc(n[2], n[3], o[2]);  
  9.   
  10.     if (r2 < 0)  
  11.         return 0;  
  12.       
  13.     int r = calc(r1, r2, o[1]);  
  14.       
  15.     return r;  
  16. }  

    4.4 calc_24 函数!

  calc_24的逻辑,在第一节已经讲过一些,总之它的特点就是**,现在来看一个完整代码:

Code:
  1. bool calc_24(int card[4])  
  2. {  
  3.     int n[4];    //四个操作数  
  4.     operator_index o[3];  //三个操作符  
  5.       
  6.     for (int i1=0; i1<4; ++i1)  
  7.     {  
  8.         n[0] = card[i1];  
  9.           
  10.         for (int i2=0; i2<4; ++i2)  
  11.         {  
  12.             if (i2==i1)  
  13.                 continue;  
  14.           
  15.             n[1] = card[i2];  
  16.           
  17.             for (int i3=0; i3<4; ++i3)  
  18.             {  
  19.                 if (i3 == i1 || i3 == i2)  
  20.                     continue;  
  21.                       
  22.                 n[2] = card[i3];  
  23.                   
  24.                 int i4 = 6-i1-i2-i3; //6 <- 0+1+2+3;  
  25.                   
  26.                 n[3] = card[i4];  
  27.                   
  28.                 for (operator_index j1=0; j1<4; ++j1)  
  29.                 {  
  30.                     o[0] = j1;  
  31.                       
  32.                     for (operator_index j2=0; j2<4; ++j2)  
  33.                     {  
  34.                         o[1] = j2;  
  35.                           
  36.                         for (operator_index j3=0; j3<4; ++j3)  
  37.                         {  
  38.                             o[2] = j3;  
  39.   
  40.                             if (calc_consecutive(n, o) == 24)  
  41.                             {  
  42.                                 tmp_output(1, n, o);  
  43.                                 return true;  
  44.                             }  
  45.                               
  46.                             if (calc_separate(n, o) == 24)  
  47.                             {  
  48.                                 tmp_output(2, n, o);  
  49.                                 return true;  
  50.                             }  
  51.                         }  
  52.                     }  
  53.                 }  
  54.             }  
  55.         }  
  56.     }  
  57.       
  58.     return false;  
  59. }  

  循环循环套循环……当然不好看。如果用来练习C++,倒是可以写成一个具备iterator接口的类来处理,,不过对于本例,那样做只会让事情更不直观(还只会慢不会更快,代码也短不了)。再看那些著名的C数学计算库,这样的循环算是干净的了……(这里,最可能的改进是使用标准库的next_permutation)

    这里重点是理解前面三层for,是用来不断以各种排列,将四个数放到n数组里去(为什么四个数只需要3层循环?见第一节)。相比前一节课,这里又嵌入了三层循环,用来将操作符(+,-,*/)塞入到o数组里去。和牌出了就不能再出不一样,同一个操作符可以出多次,所以这三层循环里没有看到if ... continue。

    排完好四个数,又排好三个操作符,我们就先调用 “ calc_consecutive(连贯式) ”函数计算一下,看是否结果为24。如果结果是0表示无效,当然这里也不需如何特殊处理无效计算; 如果 calc_consecutive 算出24,我们就当是找到了(我们暂时只需要找出一个答案就可以)。如果不24,对于的算式,我们再调“ calc_separate (分割式)” 函数计算一下。

    不管哪种算法算出了24,我们都调用一个用于临时的函数(tmp_output),将结果输出到屏幕。之所以是临时的,是因它的输出内容很不很不直观,我们肯定要重写它的。

    tmp_output函数定义如下,请将它插入到 bool calc_24(int card[4]) 函数之前。

Code:
  1. void tmp_output(int method, int n[4], operator_index o[3])  
  2. {  
  3.     cout << ((method == 1)? "consecutive" : "separate") << endl;  
  4.       
  5.     cout << n[0];  //先输出第一个数  
  6.       
  7.     //输出后面的操作符和操作数  
  8.     for (int i=1; i<4; ++i)   
  9.         cout << operator_char_def[o[i-1]] << n[i];  
  10.           
  11.     cout << endl;  
  12. }  

     tmp_output 函数需要3个入参。第一个入参表示当前用的是哪一种方法计算出24点的,1是连贯法,2是分割法。 然后就是输出那个算式。比如对于 4, 7, 8,8 这四张牌,它会输出:

     consecutive

     4+7-8*8

    你能看懂吧?  4+7-8*8 按照我们说的连贯法计算之后,确实得到24。。

 五、  第一版 main 函数

      编译一下,如果没错,就可以开始写本版的main函数,其实主要用来测试。

Code:
  1. #include <iostream>  
  2.   
  3. #include "calc_24.h"  
  4.   
  5.   
  6. using namespace std;  
  7.   
  8. int main()  
  9. {         
  10.     int num[4];  
  11.     cout << "(按Ctrl + C结束!)" << endl;   
  12.       
  13.     do  
  14.     {     
  15.         cout << "请输入4个数字 : ";  
  16.           
  17.         for (int i=0; i<4; ++i)  
  18.             cin >> num[i];  
  19.           
  20.         if (!calc_24(num))  
  21.         {  
  22.             cout << "查无答案" << endl;  
  23.         }  
  24.     }  
  25.     while(1);  
  26.       
  27.     return 0;  
  28. }  

    为了简单,我们甚至让这个程序只能通过按 Ctrl + C 键来强行中断运行。

    实际运行示例如下:

------------------------------------------------------------

请输入4个数字 : 4 5 6 10

consecutive

4*5-6+10

------------------------------------------------------------

请输入4个数字 : 5 6 10 2

consecutive

5+10*2-6

------------------------------------------------------------

再特意搞个分隔式的:

请输入4个数字 : 5 3 6 6

separate

5-3*6+6

------------------------------------------------------------

     再三地,输出内容实在有背人类的良知 啊, 下一节改进。

    另外 其这连贯式和分隔式计算会存在重复计算时候,最简单的,比如:   1 + 2 + 3 + 4 。用连贯式计算,和用分隔式计算 (1+2) + (3+4) ,显然是一样的,我们可以对此做一点点优化。这个我们也放在以后讲。

    布置个作业: 自己动手改一下tmp_output函数,让它可以分步输出计算过程,比如:

5, 10, 2 6 ==>

5+10 = 15

15 * 2 = 30

30 - 6 = 24;

-------------------------------------------------------------------------------

如果您想与我交流,请点击如下链接成为我的好友:

http://student.youkuaiyun.com/invite.php?u=112600&c=f635b3cf130f350c

 

 

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南郁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值