[离散] 编程求命题公式真值表
概述
真值表是离散数学中的一个重要概念,由真值表我们能求得任意命题公式的主析取范式和主合取范式。下面我们先来回顾一下真值表的概念:
将命题公式A在所有赋值下取值情况列成表,称作A的真值表
由真值表的定义我们不难得出我们将要做些什么,
- 分析命题公式A
- 对命题公式进行赋值并求值
- 输出真值表
本文将利用C语言编写一个程序,实现上述过程。
分析问题
要求真值表,就必须分析命题公式,将输入的命题公式,如 p&!q|(q>p)
,转化为计算机能理解的对象,方便后续求解。
注:这里用 & 表示合取, 用 | 表示析取,用 > 表示蕴含,用 = 表示等价,!表示命题的否定
人求解的思路
一个解析命题公式最直接的也是最朴素的想法就是模拟,也就是模拟你是怎么想的,让计算机按照你分析公式的思路进行分析。
这样,我们就需要仔细想想,我们在分析命题公式,求真值表的时候都在做些什么呢 ?
例:给定如下命题公式((p>q)&(q>r))>(p>r)
,求它的真值表
读者不妨自己在纸上写下这个公式,然后自己想一想,理清思路
一个常见的思路
- 给定一个赋值,从左到右按顺序求出每个子公式的值,最终得到公式的值
- 如,假设
p =1,q = 0, r = 1
- 计算
p>q
得到 0,计算q>r
得到1,计算p>r
得到1 - 最后计算
(0 & 1 ) > 1
得到1
我们分析这个思路,会发现,这里3步骤里计算哪个表达式选择往往因人而异,同时现实中很可能根本就是随意选取,这样是不利于计算机实现的。
如果将第三步限定为从左到右选取,那么这个思路,我们要按顺序解析出每个形如 A[符号]B
的命题,还要记下子命题之间的运算符,并且还需要用别的方法,搞清楚我们最后算总命题的顺序。
不是不能做,也能做,有兴趣的也可以尝试编写对应代码试一试。不过在这里,我要介绍另一种利用递归的方法。
给出参考思路:
- 我们在看到这个公式的时候,首先寻找了优先级最低的符号(即>),然后将这个表达式分成了两份。
- 观察分好的表达式,如果是形如
A[符号]B
的形式 ,则直接计算,如果不是进入下一步。 - 对新的子式重复做 步骤1,直到分成的每个子式都最简
- 然后按顺序反回去计算整个表达式的值
熟悉递归的朋友,可能一下就会发现这就是个递归,将大问题拆分成子问题,用子问题的返回值,对父问题进行解答。
抽象人的思路
如果我们用一个树来表达我们的拆解过程,那么可以得到下面一颗非完全二叉树,我们的拆解过程,就是一个构建树的过程。
还是之前的例子:((p>q)&(q>r))>(p>r)
,通过递归,我们得到了这样一颗树。
我们接下了的所有操作都将以这个树为基础,所以构建这个颗树极为关键,是整个算法的核心
下面给出完整思路(忽略实现细节)
- 建树(建树思路前面给出)。
- 从树的叶子节点开始,自下而上,同层优先,进行计算。
- 得到根节点的值,然后重新赋值
- 回到步骤2,直到真值表构建完毕
有了这个思路框架,我们就可以开始编写具体的代码,并补充一些其他需要注意的东西。
实现细节
建树
节点的存储
定义一个结构体 node 存储节点信息,除叶子节点外,每个节点都有至少有一个左儿子或右儿子(当然大部分是都有),这些节点要存储的信息如下:
- 一个字符串存储命题公式
- 一个字符存储,两个儿子之间的运算符
- 一个int型整数存储该节点存储的命题公式的真值
递归建树
考虑递归结束的条件:
- 当当前命题公式不含符号时
- 当当前命题公式为命题的否定时
按照运算的优先级,从低到高枚举第一个出现的符号,按符号将公式分成两个子式①
优先级:=
<>
< |
< &
< !
< ()
考虑一些特殊情况
- 第一个满足①的符号,包括在括号里②
- 命题公式以
!
开头③
对于第一种,我们需要先检测公式第一个字符是不是右括号
- 如果是,我们找到对应的左括号,并以左括号旁边的括号为分界点分出子式。
- 如果不是,按原样处理。可以证明,如果第一个符号不是右括号或取反符号
!
,则一定存在满足①的符号将子式正确分成两份。
特殊中的特殊:出现多重括号。需要找到和最外层匹配的左括号。
对于第二种,暂时忽略掉感叹号,从第二个字符开始处理,则可以按①或②进行处理,最后生成子串的时候记得感叹号任然保留。
计算公式的真值
根据我们之前的设计,我们要从最底层的叶子节点开始,并且同层优先,自下而上进行计算。
而我们求真值的时候真的需要这么做吗?
答案是:不需要
同样的,我们利用递归,从根节点开始计算,递归表达式如下: