使用一种有向无环图(DAG)BDD的数据结构表示布尔函数,同时在该数据结构基础之上,提供一组相应的操作算法。
1. 布尔函数
- 应用:数字逻辑设计与验证,人工智能模型校验,组合数学。
- 难点:布尔函数应用在许多问题上需要解决NP-compelte 或者 co NP-complete的问题
- 方案:采用一种更加聪明的表示方式,同时基于该表示方式的操作算法,避免指数级的计算
一些传统的表示方式:Truth table(真值表); Karnaugh maps(卡诺图); Canonical sum of products(规范的析取范式)
缺点:
- 一些常见的布尔函数表示需要指数级别
- 较合理大小的表示但是经过简单的布尔操作后产生的布尔函数需要指数级别表示
- 没有canonical form(规范的表示,唯一的表示)
采用BDD的优点:
- 大多数的布尔函数有合理的表示
- 基于BDD的布尔函数操作随着计算性能降低较慢
- 每一个布尔函数都有唯一的BDD表示,从而方便布尔函数的比较
Notation(符号表示)
我们规定所有的布尔函数参数都是 x 1 , . . . , x n x_1,...,x_n x1,...,xn,但不会引起BDD的占用空间爆炸。
-
限制:采用常数 b b b 替换参数 x i x_i xi
f ∣ x i = b = ( x i , . . . , x n ) = f ( x 1 , . . . , x i − 1 , b , x i + 1 , . . . , x n ) f|_{x_i=b}=(x_i,...,x_n)=f(x_1,...,x_{i-1},b,x_{i+1},...,x_n) f∣xi=b=(xi,...,xn)=f(x1,...,xi−1,b,xi+1,...,xn)
对布尔函数的参数 x i x_i xi使用Shannon expansion(香农展开)
f = x i ⋅ f ∣ x i = 1 + x − ⋅ f ∣ x i = 0 f=x_i \cdot f|_{x_i=1} + \overset{-}{x} \cdot f|_{x_i=0} f=xi⋅f∣xi=1+x−⋅f∣xi=0
这个公式在后面会多次用到,将一个布尔函数拆分为2个布尔函数的析取。
-
复合函数:使用一个布尔函数 g g g替换部分参数 x i x_i xi
f ∣ x i = g = ( x i , . . . , x n ) = f ( x 1 , . . . , x i − 1 , g ( x i , . . . , x n ) , x i + 1 , . . . , x n ) f|_{x_i=g}=(x_i,...,x_n)=f(x_1,...,x_{i-1},g(x_i,...,x_n),x_{i+1},...,x_n) f∣xi=g=(xi,...,xn)=f(x1,...,xi−1,g(xi,...,xn),xi+1,...,xn)
-
函数的依赖集,固定其他参数不变,其中一个参数 x i x_i xi分别取0和1,函数的值不同,则函数依赖于参数 x i x_i xi
I f = { i ∣ f x i = 0 ≠ f ∣ x i = 1 } I_f=\{i|f_{x_i=0} \neq f|_{x_i=1}\} If={i∣fxi=0=f∣xi=1}
当然,如果布尔函数 f = 1 或 f = 0 f=1 或 f=0 f=1或f=0对于参数恒成立,则 I f I_f If为空集。
-
布尔函数 f f f的satisfying set,使用 S f S_f Sf表示如下:
S f = { ( x i , . . . x n ) ∣ f ( x i , . . . , x n ) = 1 } S_f=\{(x_i,...x_n)|f(x_i,...,x_n)=1\} Sf={(xi,...xn)∣f(xi,...,xn)=1}
2. BDD表示布尔函数
定义1:BDD:有向无环图,包含两种类型的结点的数据。
- 非终端结点 v v v:1个参数的索引,2个孩子指针 l o w ( v ) , h i g h ( v ) ∈ V low(v), high(v) \in V low(v),high(v)∈V
- 终端结点 v v v: v a l u e ( v ) value(v) value(v)为0或者1。
各个结点的参数索引递增排序,即 i n d e x ( v ) < i n d e x ( l o w ( v ) ) , i n d e x ( v ) < i n d e x ( h i g h ( v ) ) index(v)<index(low(v)), index(v)<index(high(v)) index(v)<index(low(v)),index(v)<index(high(v))。
定义2:BDD表示布尔函数 f v f_v fv,其中 v v v是BDD的根结点。
-
终端结点: 如果 value(v) = 1 , 则 f v = 1 f_v=1 fv=1;如果 v a l u e ( v ) = 0 value(v)=0 value(v)=0,则 f v = 0 f_v=0 fv=0
-
非终端节点,参数索引 i n d e x ( v ) = i index(v)=i index(v)=i, 则 f v f_v fv定义如下:
f v ( x 1 , . . . , x n ) = x − ⋅ f l o w ( v ) ( x i , . . . , x n ) + x i ⋅ f h i g h ( v ) ( x 1 , . . . , x n ) f_v(x_1,...,x_n)= \overset{-}{x} \cdot f_{low(v)}(x_i,...,x_n) + x_i \cdot f_{high(v)}(x_1,...,x_n) fv(x1,...,xn)=x−⋅flow(v)(xi,...,xn)+xi⋅fhigh(v)(x1,...,xn)
3. 属性
3.1 Example Functions
圆形代表非终端结点,方形代表终点结点。
两条连线代表该非终端对应参数取值为0或者1。
x i : x_i: xi:可以使用1个非终端结点和2个终端节点表示输入情况
Odd parity(奇校验):可以使用2n+1个非终端节点和2个终端节点表示,偶校验也可以有类似的实现。
x 1 ⋅ x 2 + x 4 : x_1 \cdot x_2 + x_4: x1⋅x2+x4:仅使用3个非终端结点和2个终端结点表示。
可以看到每个图结点都是按参数索引递增的顺序,同时可以发现采用该结构可以共享相同的子图(节省空间),对子图对应的布尔函数结果进行存储(减少计算量)。
同时也可以看到,虽然前面我们定义对于 ∀ f \forall f ∀f,都统一有相同的参数 x 1 , . . . , x n x_1,...,x_n x1,...,xn,但是,对于第三个布尔函数只涉及 x 1 , x 2 , x 4 x_1,x_2,x_4 x1,x2,x4,对于的BDD不会引入无关的变量。
3.2 Ordering Dependence
BDD的大小依赖于布尔函数参数的排列顺序。对于同一个布尔函数,不同的参数排列顺序,影响BDD的大小。具体如下:
Why?
我们将计算机看作一个位处理器,每次读入一个参数。
对于第一种顺序顺序,当我们读入 x 1 , x 2 x_1,x_2 x1,x2后,由于进行And操作,只要保存该结果是0还是1即可。
对于第二种输入顺序,当我们读入 x 1 , x 4 x_1,x_4 x1,x4后,由于我们的BDD是要求按照递增的顺序排列,我们不知道此时 x 2 , x 3 x_2,x_3 x2,x3的输入情况,只能分别取0或1组合表示所有可能状态。
3.3 Inherently Complex Functions
采用BDD就足够了?
不,采用BDD来表示布尔函数,还是存在一些需要指数级别表示的函数。
如:整数乘法器,该布尔函数的BDD表示,任意参数顺序,都需要包含至少 2 n / 8 2^{n/8} 2n/8个结点。
具体证明见附录。
4. Operations
接下来介绍一系列关于该BDD的操作算法,对应的算法时间复杂度如下:
4.1 Data structures
后续采用类pascal的伪代码。
接下来使用下面的数据结构来定义一个结点:
type vertex = record
low, high: vertex; {两个孩子结点}
index: 1..n+1 {该节点对应参数的索引}
val:(0,1,X); {该BDD子图表示的值,终端结点0或1,否则X表示}
id:integer {每个结点唯一的表示号}
mark:boolean {是否访问}
end
对于终端结点和非终端结点,它们的结点数据如下:
Field | Terminal | Nonterminal |
---|---|---|
low | null | low(v) |
high | null | high(v) |
index | n+1 | index(v) |
val | value(v) | X |
4.2 Algorithms
A. Reduction
如果一个BDD满足以下条件,则是Reduced Graph。
- 对于任意一个非终端结点,它的两个孩子不同
- BDD的图中不存在两个异构的子图
Reduced Graph是Canonical(规范、唯一表示)。同时它的结点数量最少,如果存在其他的图来表示该布尔函数,需要更多的结点。
因此,对BDD进行Reduce,可以节省空间,减少节点数量,后续算法的时间复杂度也和BDD图的大小有关。因此,如果对于一个布尔函数,如果BDD的大小可以在一个合理的范围内,对其进行操作也将高效。
function Reduce(v:vertex): vertex;
var subgraph: array[1..|G|] of vertex; {存储BDD中每一个唯一结点的子图数组}
var vlist: array[1..n+1] of list; {邻接表}
begin
Put each vertex u on vlist[u.index] {结点存放到邻接表中,按索引顺序存储}
nextid := 0;
for i := n+1 downto 1 do
begin
Q := empty set;
for each u in vlist[i] do
if u.index = n+1 {终端结点}
then add <key,u> to Q where key = (u.value)
else if u.low.id = u.high.id
then u.id := u.low.id
else add <key,u> to Q where key = (u.low.id, u.high.id);
Sort elements of Q by keys;
oldkey := (-1,-1); {unmatchable key}
for each <key,u> in Q removed in order do
if key = oldkey
then u.id = nextid; {当前结点已存在}
else begin
nextid := nextid+1; u.id := nextid; subgraph[nextid] := u
u.low := subgraph[u.low.id]; u.high := subgraph[u.high.id];
oldkey := key
end;
end
return(subgraph[v.id])
end;
对于一个BDD的图,首先line 5将所有节点遍历然后收集进邻接表vlist中,vlist[i]存放索引为i的结点。
line 7自低向上遍历每一个层次的结点,即从索引号n+1(终端结点)开始处理,直到根。
line 10对于该层的每一个结点:
如果index(u)=n+1即终端结点,添加映射 <u.value, u>到Q中
非终端结点:line 11 如果两个孩子id相同即重复,设置当前id为low的id,相当于删除当前结点。不重复,则添加 ((u.low.id, u.high.id), u)到Q中。
line 17,根据id号对Q排序。
nextid为每一个唯一结点分配id,分配后增加1。
oldkey,为上一次访问的key。
line 19,依次取出每一个key-value对,line 21如果当前key和oldkey相同,则分配上一次的id给当前结点。代表两者是同一个结点。否则,nextid+1后分配给他,同时保存到subgraph之中,更新两个孩子为当前subgraph保存的唯一子图。oldkey设置为当前key值。
line 28,返回规约后的BDD图。
算法的时间复杂度主要在Sort上,时间复杂度为 O ( ∣ G ∣ l o g ( ∣ G ∣ ) ) O(|G|log(|G|)) O(∣G∣log(∣G∣))
B. Apply
两个布尔函数之间的布尔操作即Apply。
[ f 1 ⟨ o p ⟩ f 2 ] ( x 1 , . . . , x n ) = f 1 ( x 1 , . . . , x n ) ⟨ o p ⟩ f 2 ( x 1 , . . . , x n ) [f_1 \langle op \rangle f_2](x_1,...,x_n)=f_1(x_1,...,x_n) \langle op \rangle f_2(x_1,...,x_n) [f1⟨op⟩f2](x1,...,xn)=f1(x1,...,xn)⟨op⟩f2(x1,...,xn)
类似的,我们可以采用香农展开将Apply操作转化为两个布尔函数的析取,我们的算法也是该思想,分解为两个子问题递归求解。
[ f 1 ⟨ o p ⟩ f 2 ] = x − ⋅ ( f 1 ∣ x i = 0 ⟨ o p ⟩ f 2 ∣ x i = 0 ) + x ⋅ ( f 1 ∣ x i = 1 ⟨ o p ⟩ f 2 ∣ x i = 1 ) [f_1 \langle op \rangle f_2] = \overset{-}{x} \cdot (f_1|_{x_i=0} \langle op \rangle f_2|_{x_i=0}) + x \cdot (f_1|_{x_i=1} \langle op \rangle f_2|_{x_i=1}) [f1⟨op⟩f2]=x−⋅(f1∣xi=0⟨op⟩f2∣xi=0)+x⋅(f1∣xi=1⟨op⟩f2∣xi=1)
A p p l y ( f U , f V ) Apply(f_U,f_V) Apply(fU,fV),根据 f U f_U fU和 f V f_V fV是否为终端结点和参数索引关系,分为以下4中情况。
Case 1:至少1个非终端结点,且 U.index = V.index。
Case 2:至少1个非终端结点,且 U.index > V.index
Case 3:至少1个非终端结点,且U.index < V.index
Case 4:两个终端结点
优化:
采用前面的递归算法,最坏情况下时间复杂度为 O ( 2 n ) O(2^n) O(2n),n为参数个数。
T ( n ) = T ( n − 1 ) + f = 2 n , f = 1 T(n)=T(n-1)+f=2^n,f=1 T(n)=T(n−1)+f=2n,f=1
优化1:打表,cache的思想
优化2:controlling value, i.e.,类似于编程语言中"&&“和”||"的截断行为,
1 OR X = 1, 0 AND X = 0, 这种情况下,1和0就称为 controlling value,这意味着如果我们计算出的结果不是X,我们就没必要继续递归下去,提前结束。
优化后时间复杂度: O ( ∣ G 1 ∣ ∣ G 2 ∣ ) O(|G_1||G_2|) O(∣G1∣∣G2∣)
function Apply(v1, v2: vertex; <op> operator): vertex
var T:array[1..|G1|,1..|G2|] of vertex; {dp表}
{Recursive routine to implement Apply}
function Apply-setp(v1,v2: vertex): vertex;
begin
u := T[v1.id, v2.id]; {查表是否记录}
if u != null then return(u)
T[v1.id,v2.id] := u {不存在,新建记录}
u.value := v1.value <op> v2.value;
if u.value != X
then begin {当前有具体结果0或1,创建终端节点}
u.index := n+1; u.low = null; u.high := null;
end
else begin {创建非终端节点,同时递归两个孩子}
u.index := Min(v1.index, v2.index);
if v1.index = u.index {相对或者较小的索引向下递归,否则保持}
then begin vlow1 := v1.low; vhigh1 := v1.high end
else begin vlow1 := v1; vhigh1 := v1; end
if v1.index = u.index {相对或者较小的索引向下递归,否则保持}
then begin vlow2 := v2.low; vhigh2 := v2.high end
else begin vlow2 := v2; vhigh2 := v2; end
u.low := Apply-step(vlow1, vlow2);
u.high := Apply-setp(vhigh1, vhigh2);
end;
return(u);
end;
begin {Main routine}
Initial all elements of T to null;
u := Apply-setp(v1,v2);
return(Reduce(u)); {对结果进行规约标准化}
end
时间复杂度,对于每组参数,由于只计算1次。时间复杂度: O ( ∣ G 1 ∣ ⋅ ∣ G 2 ∣ ) O(|G_1| \cdot |G_2|) O(∣G1∣⋅∣G2∣)
C. Restriction
第1章介绍过该定义,即将一个布尔函数的某个参数变量替换为一个常数。
该算法比较简单,算法步骤如下:假设将 x i x_i xi替换为常数b。
Step 1:使用先序遍历查找索引 index=i的结点 v v v。
Step 2:如果b为1,将v的两个指针指向high,如果为0,指向low。
Step 3:此时存在冗余,调用reduce函数归约。
时间复杂度:O(|G|log(|G|)),时间花费在reduce上。
D. Composition
实现复合函数的BDD操作,使用香农展开。
C o m p o s i t i o n S h a n n o n e x p a n s i o n → R e s t r i c t i o n + B o o l e a n o p e r a t i o n Composition \underrightarrow{Shannon \; expansion} Restriction+Boolean \;operation CompositionShannonexpansionRestriction+Booleanoperation
f 1 ∣ x i = f 2 = f 2 ⋅ f 1 ∣ x i = 1 + ( ¬ f 2 ) ⋅ f 1 ∣ x i = 0 f_1|_{x_i=f_2}=f_2 \cdot f_1|_{x_i=1} + (\neg f_2) \cdot f_1|_{x_i=0} f1∣xi=f2=f2⋅f1∣xi=1+(¬f2)⋅f1∣xi=0
如果采用前面的方法,进行2次合取后,在进行析取,时间复杂度 O ( ∣ G 1 ∣ 2 ⋅ ∣ G 2 ∣ 2 ) O(|G_1|^2 \cdot |G_2|^2) O(∣G1∣2⋅∣G2∣2)
优化: ITE(If-then-else): I T E ( a , b , c ) = a ⋅ b + ( ¬ a ) ⋅ c ITE(a,b,c)=a \cdot b + (\neg a) \cdot c ITE(a,b,c)=a⋅b+(¬a)⋅c
时间复杂度 O ( ∣ G 1 ∣ 2 ⋅ ∣ G 2 ∣ ) O(|G_1|^2\cdot|G_2|) O(∣G1∣2⋅∣G2∣)
算法类似前面的Apply。
function Compose(v1, v2:vertex; i:integer): vertex
var T:array[1..|G1|,1..|G1|,1..|G2|] of vertex;
{Recursive routine to implement Compose}
function Compose-step(vlow1, vhigh1, v2:vertex): vertex;
begin
{Perform restrictions}
if vlow1.index = i then vlow1 := vlow1.low
if vhigh1.index = i then vhigh1 := vhigh1.high
{Apply operation ITE}
u := T[vlow1.id, vhigh1.id, v2.id]
if u != null then return(u);{已经记录,返回}
u := new vertex record; u.mark := false;
T[vlow1.id, vhigh1.id, v2.id] := u;{不存在,根据id添加信息记录}
u.value := (!v2.value and vlow1.value) or (v2.value and vhigh1.value);{香农展开公式}
if u.value != X
u.index := n+1; u.low := null; u.high := null;
end
else begin{结果非终端结点,继续递归,该部分类似Apply,较小或相等的index向下递归}
u.index := Min(vlow1.index, vhigh1.index, v2.index);
if vlow1.index = u.index
then begin vll1 := vlow1.low; vlh1 := vlow1.high end
else begin vll1 := vlow1; vlh1 := vlow1 end;
if vhigh.index = u.index
then begin vhl1 := vhigh.low; vhh1 := vhigh.high end
else begin vhl1 := vhigh; vhh1 := vhigh end;
if v2.index = u.index
then begin vlow2 := v2.low; vhigh2 := v2.high end
else begin vlow2 := v2; vhigh2 := v2 end;
u.low := Compose-step(vll1,vhl1,vlow2);
u.high := Compose-step(vhl1,vhh1, vhigh2);
end;
return(u);
end;
begin{Main routine}
Initialize all elements of T to null;
u := Compose-step(v1,v1,v2);
return(Reduce(u));{规约化结果}
end;
E. Satisfy
- Satisfy-one
如果采用传统的真值表方法,需要穷举 2 n 2^n 2n的组合,判断每一种情况是否满足。
采用BDD,对该BDD进行搜索即可,找到1个终端结点为1,搜索过程中记录每一层的参数取值情况。
function Satisfy-one(v: vertex; var x:array[1..n] of integer): boolean
begin
if v.value = 0 then return(false);{不满足}
if v.value = 1 then return;{成功找到}
{非终端节点,递归查找}
x[i] := 0;//取0,查找low
if Satisfy-one(v.low, x) then return(true);
x[i] := 1;
retrun (Satisfy-one(v.high,x))
end
该算法的时间复杂度基于图的大小,因此经过Reduce操作后,如果图在合理的范围内,相当高效。
- Satisfy-all
实现与Satisfy-one类似。
function Satisfy-all(i:integer; v:vertex; x:array[1..n] of integer):
begin
{终端结点}
if v.value = 0 then return;
if i = n+1 and v.value = 1
then begin
Print elements x[1],...,x[n];
end
{非终端结点}
{当前索引号大于i,枚举i为0和1,同时i+1继续}
if v.index > i
then begin
x[i]:=0; Satisfy-all(i+1,v,x);
x[i]:=1; Satisfy-all(i+1,v,x);
end
else begin
x[i]:=0;Satisfy-all(i+1,v.low,x);
x[i]:=1;Satisfy-all(i+1,v.high,x);
end;
end;
- Satisfy-count
对于BDD的每一个结点,添加1个 a v a_v av值。该值的定义如下:
- v是终端节点, a v = v a l u e ( v ) a_v=value(v) av=value(v)
- v是非终端节点
a v = a l o w ( v ) ⋅ 2 i n d e x ( l o w ( v ) ) − i n d e x ( v ) + a h i g h ( v ) ⋅ 2 i n d e x ( h i g h ( v ) ) − i n d e x ( v ) a_v=a_{low(v)} \cdot 2^{index(low(v))-index(v)}+a_{high(v)} \cdot 2^{index(high(v))-index(v)} av=alow(v)⋅2index(low(v))−index(v)+ahigh(v)⋅2index(high(v))−index(v),其中终端结点index(v)=n+1。
可满足集的大小: ∣ S f ∣ = a v ⋅ 2 i n d e x ( v ) − 1 |S_f|=a_v \cdot 2^{index(v)-1} ∣Sf∣=av⋅2index(v)−1
对于 a v a_v av我们可以对图进行1次遍历即可,对于每个结点,判断自身index和孩子的index差值,该差值意味着中间缺少的参数个数,我们需要穷举所有缺少的参数组合,因此需要 2 k 2^k 2k的大小。
参考阅读
Bryant, R. E . Graph-Based Algorithms for Boolean Function Manipulation[J]. Computers, IEEE Transactions on, 1986.