毕设全记录——探索 JavaBDD

这篇文章会介绍一种能高效操作 BDD(Binary Decision Diagrams,二叉决策图)和布尔函数的数据结构 —— JavaBDD。二叉决策图广泛用于模型检查,形式验证,优化电路图等方面。同时,该数据结构也可以和上一篇文章中介绍的 Graphviz 联系起来。
提示:本篇内容略多,但如果坚持读完,一定会有收获 : )

BDD与布尔函数

毕设刚开始不久,指导老师让我学习一下 JavaBDD 库。之前我也没听说过这玩意儿,在网上一查,看到 BDD 的意思是 Behavior Driven Development,行为驱动开发。仔细了解以后,感觉和毕设题目 “ 状态机自动生成与图形化仿真系统研究与实现 ” 没有半毛钱的关系。(后来才知道行为驱动开发在近年来的热度在逐渐上涨,是一种比较新的思想,感兴趣的同学可以自行了解一下)

于是重新又搜索了几次,重要发现旮旯角里的一个网站 http://javabdd.sourceforge.net/ 提到了 JavaBDD 这个库,原来它就是 JavaBDD 的官网,古老而简陋的网站,信息相当少,几句话就介绍完了。官网还给出了一个介绍 BDD 的页面,然而打开显示 404。拜托,这到底是个啥东西!不得已只好各种查找,最后在维基百科上看到了下面的定义:

BDD 是用于表示布尔函数的数据结构。布尔函数可以表示为有根,有向,非循环图,它由若干决策节点和终端节点组成。有两种类型的终端节点称为0终端和1终端。每个决策节点都用布尔变量标记,并有两个子节点,称为低子节点和高子节点。从节点到低(或高)子节点的边表示0(分别为1)的赋值。如果不同的变量在来自根的所有路径上以相同的顺序出现,则这种BDD称为“有序”。如果将以下两个规则应用于其图表,则称BDD为“约减”。

  • 合并任何同构子图。
  • 消除两个孩子节点同构的任何节点。

在流行的用法中,术语 BDD 几乎总是指减少有序二进制决策图(文献中的ROBDD,当需要强调排序和减少方面时使用)。ROBDD的优点在于它对于特定函数和变量顺序是规范的(唯一的)。该属性使其在功能等价检查和其他操作(如功能技术映射)中非常有用。

从根节点到1终端的路径表示所表示的布尔函数为真的(可能是部分的)变量赋值。当路径从节点下降到低(或高)子节点时,该节点的变量被分配为0(分别为1)。


我估计维基百科给出的定义没人看能理解的同学,我给你点个大大的赞至少我当时是懵逼的。不过还好,维基百科接下来给出了例子,这次再看,应该能理解一些了:

二叉决策图的例子,from Wikipedia

请各位仔细看左下角的布尔函数 f (x1, x2, x3),这是一个典型的布尔函数,包括三个布尔变量 x1, x2, x3。该函数的真值表在上图中间,里面很清晰的注明了布尔变量和布尔函数值的对应关系。

再看左边的类似二叉树的图,根节点对应着第一个布尔变量 x1,第二层和第三层的节点对应着第二个和第三个布尔变量。这些代表布尔变量的圆形的非叶子节点也就是上面的定义中的决策节点,而方形的叶子节点只有 0 和 1 两个取值,代表着布尔函数的值,就是上面的定义中的终端节点。最后,对于一个决策节点来说,都有两条边,一虚一实,分别表示该节点代表的布尔变量的值取假(0)或取真(1)

那么这个类似二叉树的东西(二叉决策树)和布尔函数 f (x1, x2, x3) 有什么关系呢?仔细观察可以发现,从根节点到任意一个叶子节点的路径,其实对应着三个布尔变量的取值和布尔函数的取值,而这些取值和真值表是对应的。比如我们看到真值表的第一行,x1, x2, x3 都为零,对应的就是根节点 x1 走左边的虚边(因为虚边对应的取值是 0,实边对应的取值是 1),到达第二个节点 x2 后,继续走虚的那条边边,到达第三个节点 x3 后,仍然走左边的虚边,就到达了为 1 的叶子节点,意味着布尔函数值为 1(也就是布尔函数结果为真)。而真值表第一行 f 的取值也是 1。再比如让 x1=0, x2=1, x3=0,那么从根节点开始先往左,再往右,再往左,到达了为 0 的叶子节点,意味着布尔函数为假,真值表第三行的结果确认了这一点。

所以我们可以发现,实际上左边的二叉决策树使用了另外一种方式表示了布尔函数真值表的信息!也就是说,一个布尔函数是可以由二叉决策树来表示的!(感觉真的很神奇,就像以前上学第一次听到老师讲数形结合的时候,老师在黑板上列了一个 y 关于 x 的函数,然后把它在直角坐标系中画了出来,感觉这种数与形的转换很有意思)


那么实际上,左边的图是可以进一步压缩的。为啥要压缩呢,很简单,因为二叉决策树中的节点数目随着布尔变量的增加会以指数方式增大,2 个变量对应的节点数是 1+2,3 个变量对应的节点数是 1+2+4,4 个变量对应的节点数是 1+2+4+8 ... 那么 x 个变量对应的节点数是 2^x - 1 这么多!(注:这个地方不太严谨,因为不同的布尔函数对应的二叉决策树,每层节点数并不一定是按 1, 2, 4, 8... 这样的规律增加的,这里为了简化就这么写,但不管怎样,如果不压缩的话,节点数目的增长速度肯定是越来越快的 : ) 大家理解一下)

你可能会说,那咋办嘛,我不知道怎么压缩。这个时候,维基百科的定义就发挥作用了!请往上翻到定义那里,将粗体定义再次阅读一遍,相信会有新的收获 (๑•̀ㅂ•́)و✧

定义中说 BDD 一般指的都是 ROBDD(Reduced Ordered BDD,约减且有序的BDD),也就是说 BDD 默认都是约减的和有序的。约减需要满足两个条件:合并任何同构子图,以及消除两个孩子节点同构的任何节点。这两个条件比较绕,翻译成更通俗的语言就是:把两个一模一样的部分合并到一块,以及对于任意两个节点,如果它们左孩子一样,右孩子也一样(注意,这里没说左右孩子是一样的。事实上,如果左右孩子都一样,说明这个节点不论取真还是假都不会对结果有一些,也就是说这个节点是冗余的,可以删掉),那么就删除其中一个节点(或者说把这两个节点合并到一起)。有序则需要满足,从根节点出发的所有路径上,布尔变量出现的顺序都是相同的。这一段话如果看不懂,不用急,当作了解就可以,如果之后接触 BDD 比较多,这些概念到时候顺其自然就理解了。

接下来再次翻到上面的图,看到最右边,就是布尔函数 f (x1, x2, x3) 对应的 BDD(或者说是 ROBDD)了。仔细观察它和左边的二叉决策树的区别,可以发现有不少变化:

  • 二叉决策树中的节点,虚边都在左侧,实边都在右侧,但 BDD 中不一定这样,而且在虚边都标记了 0,实边都标记了 1,显得更清晰了;
  • 终端节点只有两个了,分别为 0 和 1。这是约减过的结果,因为布尔函数最后的取值只有这两种情况,因此之前有很多冗余的 0 节点和 1 节点;
  • 对应 x1 节点取 1 的这一侧,x2 节点直接就指向终端节点了,没有了 x3 节点。这也是约减过的结果,在二叉决策树中可以看到,有两个 x3 节点不管取真还是假,到达的终端节点都一样,这意味着它们是冗余的,可以删去,而指向 x3 节点的边只需要直接指向 x3 节点的孩子即可;
  • 有多个边可以同时指向一个节点,比如终端节点就是这样。实际上对于 BDD 来说这是没问题的,这实现了节点的复用,进一步压缩了节点数。

因此,在维基百科的定义里才会说,布尔函数可以表示为有根,有向,非循环图,它由若干决策节点和终端节点组成。这也就是说 BDD(或者说ROBDD)本质是一个图,且有根,有向,非循环,可以用来表示布尔函数。之前说了这么多,也就是想阐明布尔函数和 BDD 之间的转换关系。那么知道了这个有什么用呢?当然有用!它为我们提供了一种全新的解决布尔函数相关问题的思路!就像之前用数形结合法解题,有时候你看着函数就是不知道怎么做题,但是当你把他在直角坐标系中画出来,你可能瞬间就明白了答案是什么。布尔函数也是这样。当涉及到大型、复杂的布尔函数时,我们可以将其转换成图的形式(BDD),然后使用图论相关的知识或者其他的性质来高效的解决问题。就是这么厉害~~~


JavaBDD库与一篇论文

终于要开始写 JavaBDD 这个库了。。。它是由美国斯坦福大学的 John Whaley 领导的团队设计开发的开源代码库,采用 Java 开发。这个库相当优秀,在此膜拜一下作者,但是还是得吐槽一下,为什么文档这么烂!!!是的,这个库几乎就是没有文档,官网中唯一给出的只有 API 文档 http://javabdd.sourceforge.net/apidocs/index.html,各位可以感受一下它的酸爽。网上关于 JavaBDD 的资料也极少,可能是因为用的人不多吧。。。

我查了很久也没有找到任何的资料,倒是找到了 John Whaley 在 ResearchGate 上介绍 JavaBDD 的一篇论文,但是无法查看和下载。。。还有就是 Stackoverflow 上的这个回答。可以说我是根据这个回答,然后对照着 API 文档和源码各种尝试、失败、重复才一点点知道 JavaBDD 是怎么使用的。从这一点上来说,JavaBDD 真的很坑。

在这段探索的过程中,我做了一些 JavaBDD 的使用笔记,比较简单,就先不发出来了,如果有同学需要可以在文章底部留言。同时在搜集资料期间,也发现了一些和 BDD 相关的有些用的资料,分别是一个讲座 PPT OBDD原理及其应用 以及知网的一篇论文《二元判断图BDD及其JAVA实现的应用与研究》。

这里对于这个论文,我得重点说几句。首先它对我的毕设帮助还是很大的,里面也有一点使用 JavaBDD 的代码,是我探索过程中很好的参考。但是我在之后做毕设的过程中发现,这篇论文相当有意思。首先看它的目录如下图:

《二元判断图BDD及其JAVA实现的应用与研究》的目录

不知道大家还记不记得 JavaBDD 库的官网,如果有同学打开过,应该记得在为数不多的几句介绍中有这样一句:“有关BDD数据结构的精彩概述,请参阅Henrik Reif Andersen的这套 讲义。”然而这个链接的打不开的。后来我尝试着搜索 “ Henrik Reif Andersen Binary Decision Diagrams ”,发现第一个结果正是这篇讲义的 PDF。当时我很激动,认为找到宝了,虽然是全英文的,但是也能大致理解其中的内容。但是当我看了一部分以后,感觉似乎内容有些似曾相识,再一看论文,发现第二章的内容基本就是从讲义里摘取的,图片都是一样的,文字就是从英文翻译成中文,然后再加上了少量自己的东西。

还没完,在这篇论文的第 4.3.2 节的第一段中,有一句这样的话:

《二元判断图BDD及其JAVA实现的应用与研究》第 4.3.2 节的第一段

当时我对论文中的有些地方不太懂,但又不知道在哪里找资料,突然看到了这句话,就想着找到这本书来看看,或许能得到一下启发。但是这个书已经很老了,图书馆里也没有,我又不想买‍经过多方查找,在 这个网站 发现可以在线阅读。在页面右边点击 “图书馆文献传递”,登录后就可以看了,从 103 页开始,论文中第三章的内容和书中基本是重复的。同时,论文第 4.2 节中的例子也是套用的 Henrik Reif Andersen 的讲义中的内容。

可以说,整篇论文,除了第一章和部分第四章的内容是作者写的,论文的核心第二三章绝大部分都是来自别人的成果。作者除了提供背景说明、一些分析以及一些 JavaBDD 的代码之外,其实做的很有限。作为硕士毕业论文,这样的情况其实是不应该发生的。希望看到这里的同学能够以此为鉴。


接下来开始真正的介绍 JavaBDD 的用法了!
(我怎么感觉都要写完了才开始介绍。。。)

JavaBDD 库能够方便地操作 BDD 并进行一些复合操作。在 JavaBDD 中创建 BDD 需要用到 BDDFactory 类,首先需要通过 init() 方法初始化一个BDDFactory 对象。该方法接收两个 int 类型的参数,分别表示初始节点表大小和操作缓存大小。

接下来需要对该 BDDFactory 对象调用 setVarNum() 方法,接收一个 int 类型的参数,设置需要使用的 BDD 变量个数。接下来就可以对 BDDFactory 对象调用 ithVar() 方法创建表示第 i 个变量的BDD 变量。可以将返回值看成一个节点,子节点为 true 和 false。除此之外,请求的变量必须在setvarnum 定义的范围内。

当创建了若干变量之后,就可以调用 and()or() 等方法进行操作了。如果要获得结果BDD 的详情,主要有两种方式,一是调用 printDot() 方法以 DOT 方式打印这个 BDD,二是调用 printSet() 方法,打印此 BDD 指定的真值分配集合。注意,这里的 printDot() 方法非常重要,正是由于这个方法,BDD 才能以 DOT 的格式输出,还记得上一篇文章里的 Graphviz 吗,Graphviz 接收 DOT 格式的输入,然后可以直接输出一张图片。这样的话,我们就可以看到抽象的 BDD 了!我感觉这应该不是巧合,可能是 JavaBDD 的作者也使用 Graphviz,觉得很好用,就实现了这个 printDot() 方法。JavaBDD 与 Graphviz,就可以通过这个方法结合起来了~~~

上述的步骤描述可能有些抽象,接下来举一个例子说明。首先通过以下的代码创建一个 BDDFactory 对象和两个 BDD 对象:

BDDFactory B = BDDFactory.init(16, 16);
B.setVarNum(3);
BDD a = B.ithVar(0);
BDD b = B.ithVar(1);

接下来对 a 和 b 执行 or 操作,并将结果赋给一个新的 BDD 变量 c,然后用 printDot()printSet() 两种方法获得它的信息:

BDD c = a.or(b);
c.printDot();
c.printSet();

就可以看到控制台有了以下两个输出:
(如果是在 64 位电脑上用,会有错误提示,但是不用管)

digraph G {
0 [shape=box, label="0", style=filled, shape=box, height=0.3, width=0.3];
1 [shape=box, label="1", style=filled, shape=box, height=0.3, width=0.3];
2 [label="0"];
2 -> 3 [style=dotted];
2 -> 1 [style=filled];
3 [label="1"];
3 -> 0 [style=dotted];
3 -> 1 [style=filled];
}
<0:0, 1:1><0:1>

其中从第一行到倒数第二行是 printDot() 的输出,这就是一个用 DOT 语言描述的图。可以用 Graphviz 自带的 gvedit 工具渲染查看最终效果,如下图所示。它实际上就是 BDD 变量 c,是由a 和b 相与得到的,所以实际上可以理解成布尔函数 c = a or b,当 a 或 b 为真的时候 c 为真,当 a 和 b 均为假的时候c 为假。这一点也可以从图中看出来。

用 DOT 语言描述的 BDD 的渲染结果

接下来看到第二个输出 <0:0, 1:1><0:1>,仔细观察可以发现,每个尖括号都代表着一种真值分配的情况,每个冒号前的数字代表着 BDD 变量的索引,冒号后的数字代表着取值情况。这个输出就包含着两种情况,第一种情况是 0 节点为假,1 节点为真;第二种情况是 0 节点为真。这两种情况就是使得布尔函数为真的所有情况。


JavaBDD 中重要方法的探究

在这里给出一些 JavaBDD 中很重要的方法的说明,基于我的笔记进行了一些补充:

public abstract BDD apply(BDD that, BDDFactory.BDDOp opr):返回将二元运算符 opr 应用于两个 BDD 变量的结果。通过查看源码,可以发现 BDDOp 类是一个枚举类,定义了十种二元运算符,分别为 and(逻辑与,符号是∧),xor(逻辑异或),or(逻辑或,符号是∨),nand(逻辑与非),nor(逻辑或非),imp(逻辑蕴含,符号是→),biimp(逻辑同或,符号是↔),diff(第一个操作数和第二个操作数的非相与,相当于 p∧¬q),less(第一个操作数的非和第二个操作数相与,相当于 ¬p∧q),invimp(逻辑反蕴含,符号是←)。

public abstract BDD ite(BDD thenBDD, BDD elseBDD):if-then-else 运算符。例如当对 BDD 变量 a、b 和 c 使用 a.ite(b, c) 时,如果 a 是真,那么该方法返回 b;如果a 是假,那么该方法返回 c。实际上这个方法可以转化成普通的二元操作,相当于布尔表达式 a·b+a’·c,用 JavaBDD 的方式表示就是 BDD result = a.and(b).or(a.not().and(c))

public abstract BDD replace(BDDPairing pair) : 该方法传入的参数是一个 BDDPairing 对象,该对象可以通过对 BDDFactory 变量调用 makePair() 方法得到。BDDPairing 对象本质上存放着一个或多个需要被替换的节点和新的节点,当它作为 replace() 的参数被某个 BDD 变量调用时,该方法会返回一个 BDD 对象,其中所有变量都替换为 pair 定义的变量。成对的每个条目都由一个新旧变量组成。每当在这个 BDD 中找到旧的变量时,就会插入一个具有新变量的新节点。

public abstract BDD restrict(BDD var):对于调用该方法的 BDD,将其节点中同时也包含在参数 BDD 中的变量限制为常量值。如果节点变量包含在作为参数的 BDD 变量的正形式中,则将此 BDD 中的变量限制为常量真;如果变量包含在负形式中,则将其限制为常量假。对节点的约束会使得所有指向该节点的指针重定向。如果节点为真,则使之指向其 1-分支子节点;如果为假则使之指向其 0-分支子节点。

public abstract boolean equals(BDD that):该方法用于判断两个 BDD 变量是否相等。如果参数中的 BDD 变量等于调用该方法的 BDD 变量,则返回true,否则返回 false。

BDDDomain 类:代表 BDD 变量的域,可以将其看成是若干 BDD 变量的集合,对于描述有限状态机及其各种状态之间的转化十分有用。可以通过对 BDDFactory 变量调用 extDomain() 得到一个 BDDDomain 数组,稍后可以用于有限状态机的遍历和有限域上的其他操作。该函数接收一个 int 类型的数组,数组的元素个数等于返回的 BDDDomain 数组的个数,设数组中某个元素的大小为 i,则其对应的 BDDDomain 变量包含 log2(|i|) 个 BDD 变量。一般情况下,会将整型数组的长度设为 2 以表示变化前和变化后的两个布尔函数,而且如果布尔函数有 n 个变量,那么整型数组的每个元素值都应该是 2n。最后需要注意的是,为了达到更好的效果,extdomain 方法返回的 BDDDomain 中 BDD 变量的编序是交错的。这意味着假设域 D0 需要2 个 BDD 变量 x1 和 x2,而另一个域 D1 需要 4 个 BDD 变量 y1、y2、y3 和 y4,那么它们的编序将是 x1、y1、x2、y2、y3、y4。可以对 BDDFactory 变量调用 getDomain(i) 返回通过调用 extdomain() 定义的第 i 个 BDDDomain。


使用 JavaBDD 表示状态集和迁移关系

在文章开始的引言里,提到了二叉决策图广泛用于模型检查,形式验证,优化电路图等方面。实际上,这些应用都和状态有着密不可分的关系。

在之前介绍 BDD 的时候,提到它是一种表示布尔函数的有力工具。但是状态集和迁移关系似乎与布尔函数并没有什么联系,它们如何能被 BDD 表示?实际上,对于一个单独的状态来说,它能满足部分布尔变量,当处于该状态时这些布尔变量是成立的。因此可以将这个状态看成是一个布尔函数,它等于所有满足的布尔变量相与,再和其他不满足的布尔变量的非相与,就得到了该状态的布尔函数表示。

一个模型示意图的例子

以上图为例,该系统包含的布尔变量为 a、b 和 c,状态集为 S0、S1 和 S2(表示包含这三个状态),迁移关系(也就是状态之间的转换关系)也很清晰。对于 S0,满足 a 和 b,不满足 c,也就是说在这个状态下,布尔变量 a 和 b 为真,c 为假。如果要用 BDD 表示该状态,就应该是 S0 = a∧b∧¬c。同理,S1 = ¬a∧b∧c,S2 = ¬a∧¬b∧c。接下来如果要表示状态集,就需要将所有的单个状态的布尔函数相加,对应到逻辑关系中就是或操作。在这个例子中,状态集 S = (a∧b∧¬c)∨(¬a∧b∧c)∨(¬a∧¬b∧c)。在 JavaBDD 中,需要将与操作转换成 and 方法,或操作转换成 or 方法,逻辑非转化成 not 方法。最终 S 对应的 BDD 如下图所示,其中的 0、1 和 2 节点对应着布尔变量 a、b 和 c。
状态集 S 对应的 BDD

仔细观察可以发现,在这个 BDD 中,到达终点 1 节点的路径有两条,但是实际上包含着三种情况。在 0 节点为假、2 节点为真的情况下,1 节点为真或为假都是可以的,BDD 没有将这两种情况分开表示,而是通过合并它们减少了所需的空间。这三种情况也对应着状态集 S 的布尔函数中被逻辑与∨分开的三部分。因此,布尔函数的组成部分和其BDD 中到达终点1 节点的路径是可以对应上的。

对于迁移关系的 BDD 表示,需要用到之前提到的 BDDDomain 类。因为该系统一共有 3 个布尔变量,因此给 extDomain 方法传入一个包含两个 int 变量的 int 数组,每个 int 的值都是 2^3 也就是 8。这样会生成两个 BDDDomain 变量,每个都包含 3 个BDD,且为了更高的效率,它们的编序是交替的,第一个 BDDDomain 变量的编序是 0,2,4,第二个的编序是 1,3,5。将它们分别看成迁移前和迁移后对应的状态,迁移关系的布尔函数就是将迁移前后状态满足的状态相与。以之前的模型示意图中的迁移关系 S0—>S1 为例,其布尔函数为 a∧b∧¬c∧¬a’∧b’∧c’,同理,总迁移关系的布尔函数就是所有单独迁移关系的布尔函数相加,对应到逻辑关系中就是或操作。在这个例子中,由于有 4 个迁移关系,因此总迁移关系可以表示成它们相与,也就是 (a∧b∧¬c∧¬a’∧b’∧c’) ∨ (a∧b∧¬c∧¬a’∧¬b’∧c’) ∨ (¬a∧b∧c∧¬a’∧¬b’∧c’) ∨ (¬a∧¬b∧c∧¬a’∧¬b’∧c’)。这实际上也是一个布尔函数,它对应的 BDD 如下图所示,其中的 0、2、4 节点对应着布尔变量 a、b、c,而 1、3、5 节点则对应着布尔变量 a’、b’、c’。


总迁移关系对应的 BDD

可以看到,虽然到终点 1 节点的路径只有两条,但是每条路径都包含着两种情况,因此最终有 4 种使总迁移关系的布尔函数为真的情况,这一点从上面的布尔函数也可以看出来。


OK,这篇文章内容就先写到这里了,下一篇会进一步介绍 JavaBDD 的应用——布尔表达式的运算。以布尔函数 result = (A or C)and(B or A)为例,如何利用 JavaBDD 瞬间得到它的真值表?聪明的同学应该能猜到了,不就是从根节点走完每条路径嘛!

欲知后事如何,且听下回分解~~~

感谢大家的阅读,如果觉得这篇文章很有帮助,赞赏支持一波啊~~~
金额不重要,态度是关键✨✨✨

你可能感兴趣的:(毕设全记录——探索 JavaBDD)