`
ldb19890624
  • 浏览: 243674 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

算法系列之十六:使用穷举法解猜结果游戏

 
阅读更多

一、 引言

穷举是解决问题的一种常用思路,当对一个问题无从下手的时候,可以考虑在问题域允许的范围内将所有可能的结果穷举出来,然后根据正确结果的判断规则对这些结果逐个验证,从而找出正确的结果。采用穷举的方法求解问题的答案比较适合计算机做,对这种体力活它们没有怨言,本文就以常见的两个猜结果的题目为例,介绍一下如何通过计算机程序解决此类问题,顺便介绍一下穷举法常见的算法结构和实现方式。

二、 猜结果游戏的分析过程

先来看一个问题,有五个运动员(甲、乙、丙、丁、戊)参加运动会,分别获得了一百米、二百米、跳高、跳远和铅球冠军,现在有另外四个人(ABCD)对比赛的结果进行了描述,分别是:

A说:“乙获得铅球冠军,丁获得跳高冠军”

B说:“甲获得一百米冠军,戊获得跳远冠军”

C说:“丙获得跳远冠军,丁获得二百米冠军”

D说:“乙获得跳高冠军,戊获得铅球冠军”

ABCD四个人每个人的描述都对一句,错一句,现在根据这四个人的描述猜一下五名运动员各获得了什么项目的冠军?

现在来分析这个问题,五个运动员获得5个冠军,正确的结果需要5个描述即可,现在题目给出了四个人的8个描述,其中一些是相互矛盾的错误描述,因此需要对这些描述的正确性进行假设,假设的过程其实就是穷举的过程。每个人有两个描述,分别对其进行正确性假设可以得到两个假设结果,四个人就有2416种假设结果的组合,下表就是这16种假设结果组合的全部描述:

描述

结果

A

B

C

D

1

对,错

对,错

对,错

对,错

2

对,错

对,错

对,错

错,对

3

对,错

对,错

错,对

对,错

4

对,错

对,错

错,对

错,对

5

对,错

错,对

对,错

对,错

6

对,错

错,对

对,错

错,对

7

对,错

错,对

错,对

对,错

8

对,错

错,对

错,对

错,对

9

错,对

对,错

对,错

对,错

10

错,对

对,错

对,错

错,对

11

错,对

对,错

错,对

对,错

12

错,对

对,错

错,对

错,对

13

错,对

错,对

对,错

对,错

14

错,对

错,对

对,错

错,对

15

错,对

错,对

错,对

对,错

16

错,对

错,对

错,对

错,对

寻找正确答案的过程就是对这16组假设结果逐个判断的过程。先来看看第1个结果,A说“乙获得铅球冠军”被假定是正确的,但是D说“乙获得跳高冠军”也被假定是正确的,因为一个运动员只能获得一个冠军,因此第1个结果就互相矛盾,不能得到正确的答案。再来看看第2个结果,A说“乙获得铅球冠军”被假定是正确的,但是D说“戊获得铅球冠军”也被假定是正确结果,这又是另一种矛盾,因为铅球冠军不能被两个人同时获得。如果某个结果的所有假定没有互相矛盾的地方,就可以得到一个正确的结果,比如第10个结果,其正确的描述分别是A的第二句描述、BC的第一句描述以及D的第二句描述,这几个描述没有矛盾,可以得到一个正确答案,就是:

丁获得跳高冠军 A描述的第二句)

甲获得一百米冠军(B描述的第一句)

丙获得跳远冠军 C描述的第一句)

戊获得铅球冠军 D描述的第二句)

乙获得二百米冠军 (根据前面的结果做不矛盾推解)

三、 用程序求解猜结果游戏

3.1 建立数学模型

《算法系列》的文章中多次提到,为计算机程序建立的数学模型必须能够解决三大问题,其一是要能够方便地把自然语言描述的问题转化成计算机能够理解的数据结构,其二是转换后的数据结构要适用与问题的求解模式并能够推演出正确的结果,其三是能够方便地把求解的结果转化成自然语言方式的输出。针对不同的问题建立的数学模型没有统一的模式,也没有标准答案,能够解决问题就是正确的数学模型,但是只有简洁且能够配合算法高效地求解问题的数学模型才是好的数学模型。

首先需要数学模型化的是四个人的描述,考察这些自然语言的描述,发现每句描述其实只包含了两个信息:一个是运动员是谁?另一个是他得了什么冠军?如果我们给每个运动员编一个序号,同时给每种运动的冠军也编一个序号,则每个自然语言的描述就可以量化为两个数字。为运动员或冠军编制序号有多种方法,本文的算法采用的是C/C++语言,因此用枚举类型来定义它们:

13typedef enum tagAthleteNumber

14{

15 athleteJia = 0,

16 athleteYi,

17 athleteBing,

18 athleteDing,

19 athleteWu

20}AthleteNumber;

21

22typedef enum tagChampionResult

23{

24 ShotChampion = 0,

25 HighJumpChampion,

26 LongJumpChampion,

27 OneHMChampion,

28 TwoHMChampion

29}ChampionResult;

描述的数学模型就可以这样定义:

32typedef struct tagDescription

33{

34 AthleteNumber athlete;

35 ChampionResult result;

36}Description;

最终自然语言描述的问题就可以量化为如下二维数组:

44Description peopleDesc[peopleCount][descPerPeople] =

45{

46 { {athleteYi, ShotChampion}, {athleteDing, HighJumpChampion} },

47 { {athleteJia, OneHMChampion}, {athleteWu, LongJumpChampion} },

48 { {athleteBing, LongJumpChampion}, {athleteDing, TwoHMChampion} },

49 { {athleteYi, HighJumpChampion}, {athleteWu, ShotChampion} }

50};

问题的求解过程中,还有一个重要信息需要记录,那就是每一个描述者提供的两个描述中总是一真一假。穷举的过程就是真和假的不断假设过程,根据第二节的分析,在这个过程中可以得到16组真和假的配对结果,这也需要一个数学模型承载这些配对结果。本来描述是四个人给出的,每个人给出两个描述,但是结果判断不需要知道某个描述是哪个人给出的,只需要知道描述的内容以及结果认定是真还是假就可以了,因此定义每组配对结果为一维数组,从而简化算法:

std::vector<DescDecision> decisions;

DescDecision定义包含两部分,一部分是描述Description,另一部分是描述真假标志,具体定义如下:

38typedef struct tagDescDecision

39{

40 Description desc;

41 bool decision;

42}DescDecision;

最后是结果输出,通过对问题的分析可以得知,结果其实就是5个描述,只不过这5个描述都是正确的,因此结果也可以定义为Description的数组:

std::vector<Description> result;

3.2 正确结果判断方法

穷举可以得到问题域内的所有可能的结果,但是如何判断哪个是正确的结果?根据本题的题意,正确的结果就是所有描述都没有互相矛盾的结果。再根据第二节的分析,这个题目只有两种可能的矛盾:一种是两个运动员得到同一个冠军,另一种是同一个运动员得到两个(或两个以上)冠军。

正确结果的判断和结果的生成是同时进行的,对每个穷举得到的结果开始判断之前,result是空的。每个穷举结果有8个描述,逐个进行判断,当一个描述被标识为正确描述,且和result中的现有描述不矛盾,则将这个描述加入到result中,如果一个描述被判定与result中已经的描述有矛盾,则说明这个结果中的8个描述有互相矛盾的地方,无法得到正确结果,直接返回错误。当某个穷举结果的8个描述全部判断完成,没有返回错误,则说明得到了一个正确的结果。ParseResultFromDecisions()函数就是完成上述过程:

119bool ParseResultFromDecisions(const std::vector<DescDecision>& decisions,

120 std::vector<Description>& result)

121{

122 std::vector<DescDecision>::const_iterator cit;

123 for(cit = decisions.begin(); cit != decisions.end(); ++cit)

124 {

125 if(CheckDecision(result, *cit))

126 {

127 if(cit->decision)//如果是不矛盾的真描述,就记录结果

128 {

129 result.push_back(cit->desc);

130 }

131 }

132 else

133 {

134 return false;

135 }

136 }

137

138 //只有四个描述,需要补上第五个描述,且不能矛盾

139 PatchTheLastOne(result);

140 return true;

141}

CheckDecision()函数就是判断当前的描述是否与result中已有的描述(被认定是正确的描述)冲突,主要方法就是逐项检查result中的描述,判断其是否与当前描述存在两类矛盾的情况,实现方法如下:

70bool CheckDecision(const std::vector<Description>& result, const DescDecision& decision)

71{

72 std::vector<Description>::const_iterator cit;

73 for(cit = result.begin(); cit != result.end(); ++cit)

74 {

75 if(cit->athlete == decision.desc.athlete)

76 {

77 if(decision.decision && (decision.desc.result != cit->result))

78 {

79 return false;

80 }

81 if(!decision.decision && (decision.desc.result == cit->result))

82 {

83 return false;

84 }

85 }

86 if(cit->result == decision.desc.result)

87 {

88 if(decision.decision && (decision.desc.athlete != cit->athlete))

89 {

90 return false;

91 }

92 if(!decision.decision && (decision.desc.athlete == cit->athlete))

93 {

94 return false;

95 }

96 }

97 }

98

99 return true;

100}

最后说明一下PatchTheLastOne()函数的作用,对于一个完整的正确答案,应该是5个描述,但是四个人的描述只有4个正确描述,在互相不矛盾的基础上,需要补上第5个描述,PatchTheLastOne()函数就是做这个事情。补的方法很简单,就是前4个描述中没有提到的那个运动员得到的是前4个描述中没有提到的那个项目的冠军。因为简单,这里就不列出代码了。

3.3 穷举算法

猜结果游戏的穷举算法与排列组合算法类似,或者说就是使用排列组合的穷举方式。《算法系列》的《排列组合算法》一文对常见的排列组合算法都有介绍,这里不再赘述。只是在选择多重循环还是递归方法上,我倾向于使用递归方法,原因就是代码简单易懂。本算法穷举的主要思想就是对四个描述者的真假状态逐个进行遍历,使用递归结构,每次遍历一个人的描述。用一个索引来标识当前要遍历真假状态的描述者,当索引达到最大值时,表示一组描述结果完成,可以尝试判断是否此结果就是正确结果。以下就是递归方式的穷举算法主体部分代码:

159void EnumPeopleDescriptions(int peopleIdx,

160 std::vector<DescDecision>& decisions,

161 void (*callback)(const std::vector<DescDecision>& decisions))

162{

163 if(peopleIdx == peopleCount)

164 {

165 callback(decisions);

166 return;

167 }

168

169 EnumPeopleDescriptions(peopleIdx + 1, decisions, callback);

170 //翻转描述者两个描述的状态,总是保持一对一错

171 DescDecision& dd1 = decisions[peopleIdx * descPerPeople];

172 dd1.decision = !dd1.decision;

173 DescDecision& dd2 = decisions[peopleIdx * descPerPeople + 1];

174 dd2.decision = !dd2.decision;

175 EnumPeopleDescriptions(peopleIdx + 1, decisions, callback);

176}

peopleIdx参数就是描述者索引,decisions参数就是当前对所有描述的真假判断结果,通过成对地翻转一个描述者的两个描述的真假状态,达到枚举所有结果的目的。callback回调函数负责判断一组结果是否正确并打印正确的结果,DescriptionsCallback()函数就是本算法使用的callback回调函数:

148void DescriptionsCallback(const std::vector<DescDecision>& decisions)

149{

150 std::vector<Description> result;

151

152 if(ParseResultFromDecisions(decisions, result))

153 {

154 PrintResult(result);

155 }

156}

ParseResultFromDecisions()函数在3.2节已经介绍过了,PrintResult()函数只是用来输出正确结果,无需多做说明。

3.4 结果输出

至此,完整的算法都已经介绍完毕,剩下的就是输出结果,结果不出意外,只有一个正确答案:

丁获得跳高冠军

甲获得一百米冠军

丙获得跳远冠军

戊获得铅球冠军

乙获得二百米冠军

这个输出是按照描述者的正确描述顺序输出的,不太符合生活习惯,调整其实也很简单,只要在结果输出前进行一次排序即可:

sort(result.begin(), result.end(), AthleteComparator);

AthleteComparator()函数比较运动员编号大小,排序后的输出就是这个样子了:

甲获得一百米冠军

乙获得二百米冠军

丙获得跳远冠军

丁获得跳高冠军

戊获得铅球冠军

3.5 另一个猜结果游戏

此类型的猜结果游戏很多,参照上面的代码,稍加修改就可以解决类似的问题,比如这个猜结果游戏:

有五个游泳运动员参加完比赛回来时有人询问他们的比赛结果,他们说“我们每个人都告诉你两个结果,其中一个正确,一个错误,你自己猜猜名次究竟如何?”

甲说:“乙第二,我第三”

乙说:“我第二,戊第四”

丙说:“我第一,丁第二”

丁说:“丙最后,我第三”

戊说:“我第四,甲第一”

名次究竟如何,你猜出来了吗?

这个题目的描述者就是运动员本身,与前面讨论的题目有些差异,但是本文介绍的算法并不关心描述者信息,只关心其描述的内容,因此前面的算法也适用于这个题目,只需把描述中标识自己的第一人称“我”修改成相应的描述者就可以了,修改后的描述就变成和描述者无关的信息了:

甲说:“乙第二,第三”

乙说:“第二,戊第四”

丙说:“第一,丁第二”

丁说:“丙最后,第三”(丙最后,其实就是丙第五)

戊说:“第四,甲第一”

根据描述内容修改描述的定义如下:

32typedef struct tagDescription

33{

34 AthleteNumber athlete;

35 MatchResult result;

36}Description;

其中有改变的是MatchResult,定义如下:

22typedef enum tagMatchResult

23{

24 matchNo1 = 0,

25 matchNo2,

26 matchNo3,

27 matchNo4,

28 matchNo5

29}MatchResult;

最后把输出结果的函数稍作调整,整体代码无需做修改就可以得到结果了,有两组与描述都不冲突的结果,第一组结果是:

甲获得第五名

乙获得第二名

丙获得第一名

丁获得第三名

戊获得第四名

第二组结果是:

甲获得第三名

乙获得第一名

丙获得第五名

丁获得第二名

戊获得第四名

四、 总结

穷举类方法的重点就是两个,一个是穷举所有可能解的方法,另一个是判断正确解的规则。前一个重点需要一些技巧来构造合适的穷举算法,对于不同的问题来说,穷举算法的差异很大,没有统一的模板可以借用。后一个判断正确解的规则可以根据题目自然语言的描述构造,也没有定势可用。当然,这两者都是需要建立在适当的数学模型上的,明智地构造数学模型可以简化算法的复杂度,提高效率,对于一个成功的算法来说,以上都是缺一不可的。

分享到:
评论

相关推荐

    算法与程序设计:第2章 穷举法与迭代法.ppt

    穷举法是一种简单、基础的算法,它的特点是穷举所有可能的候选区间,找出符合要求的解集。虽然穷举法的效率不高,但它具有超级无敌准确性和天下第一全面性。 穷举法的概念可以分为两部分:找出穷举范围和找出约束...

    算法分析与设计课件:穷举法.ppt

    穷举法,也称为全搜索法,是一种基础的算法设计方法,主要用于寻找问题的所有解或者最优解。在解决货郎担问题的例子中,穷举法通过列举所有可能的城市排列组合来找出最短的路线。当城市数量为n时,路线的总数为(n-1)...

    算法设计与分析:穷举法、贪心法、分枝限界法讲稿.doc

    本文将深入探讨三种常见的算法策略:穷举法、贪心法和分枝限界法。 一、穷举法(枚举法) 穷举法,顾名思义,是通过列举所有可能的解决方案来找到正确答案的方法。其主要思路是从可能解的集合中逐个尝试,通过题目...

    matlab程序(穷举法).rar_matlab_枚举法_穷举法 matlab_穷举法MATLAB_穷举算法 tsp

    MATLAB优化算法案例分析与应用(进阶篇)1-10章程序下载

    穷举法破解路由器密码的软件

    穷举法破解路由器密码的软件 常见的TPlink dlink之类的路由器密码 需挂字典穷举破解

    用穷举法设计程序教学设计.doc

    穷举法,又称枚举法,是一种在计算机科学中广泛使用的算法设计思想。它通过遍历所有可能的解决方案来寻找正确答案,适用于问题的解空间有限且可枚举的情况。 2. **穷举法的适用范围**: - 当问题可以通过明确的...

    C语言中使用穷举法的一些算法

    根据给定的文件信息,我们可以总结出C语言中使用穷举法解决的几个关键知识点,主要涉及全排列的实现以及通过穷举法求解最大公约数(GCD)与最小公倍数(LCM),还有数字分解算法。下面将详细阐述这些知识点。 ### ...

    C语言24点游戏穷举法

    本文将深入探讨如何使用C语言实现24点游戏的穷举法。24点游戏是一种数学智力游戏,玩家需要从四张1到13之间的扑克牌中,通过加、减、乘、除和括号的操作,使得结果等于24。 首先,我们需要理解游戏规则。四张牌可以...

    穷举法代码解析带注释(学习穷举法代码好资料)

    穷举法,作为一种基础而有效的算法策略,被广泛应用于解决特定类型的问题,尤其是在面对有限解空间且解的数量相对较少的情况下,穷举法能够通过遍历所有可能的情况来寻找问题的解。 ### 穷举法概述 穷举法,又称...

    经典 算法思想 穷举法 高精度 动态规划 回溯 贪心 排列组合 排序

    本资源包聚焦于几种常见的算法策略,包括穷举法、高精度计算、动态规划、回溯、贪心算法、排列组合以及排序。下面将逐一详细阐述这些算法思想及其应用。 1. **穷举法**:穷举法,也称为全搜索法,是一种通过尝试...

    旅行者问题+商品选取问题(算法分析与设计:穷举法(C++,含可执行源码+完整算法分析))

    题目 2:某大型商场举办了一个游戏,为了吸引大家参与该游戏,商场对游戏的获胜者进行了奖励:游戏的获胜者可以使用商场提供的小汽车到商场中任意选取其中的商品,但要求每种商品最多只能取一个,不能多取。...

    穷举法C/C++程序

    在编程领域,穷举法是一种常见的解决问题的策略,尤其在C和C++这两种语言中,开发者经常使用这种方法来实现特定的算法。穷举法,也称为全搜索或枚举,是指在解决一个问题时,尝试所有可能的情况,直到找到正确答案。...

    sjwtu算法分析与设计 穷举法 03

    【穷举法】,也称为全搜索法或枚举法,是一种基础的算法设计思想,主要通过对所有可能的解进行尝试来找到问题的正确答案。在本次实验中,我们运用穷举法解决了一个分配问题,目标是寻找一种分配方案,使得每个人分配...

    模拟密码破解 6为以内 穷举法 破解

    密码破解 穷举破解 6为以内字母、数字等 破解

    算法设计与分析-穷举法、贪心法、分枝限界法讲稿

    通过本篇内容,我们将重点介绍几种常用的算法设计方法:穷举法、贪心法以及分枝限界法,并探讨它们的应用场景、特点及局限性。 ### 穷举法详解 #### 定义 穷举法,又称暴力搜索法,是一种最基本的算法设计策略。它...

    穷举算法 回溯算法 介绍

    穷举算法是最简单、最基础的算法之一,也是通常被认为非常没效率的算法。但是,穷举拥有很多优点,它在算法中占有一席之地。首先,穷举具有准确性,只要时间足够,正确的穷举得出的结论是绝对正确的;其次,穷举拥有...

    穷举法解迷宫问题(C语言)

    ### 知识点一:穷举法的基本概念 穷举法是一种通过遍历所有可能的情况来寻找解决问题的方法。它适用于解决那些解决方案空间有限且能够明确界定的问题。在本例中,穷举法被用来解决迷宫问题,即寻找从起点到终点的...

Global site tag (gtag.js) - Google Analytics