有没有一种已知的算法可以通过数字比较来简化布尔表达式?

Is there a known algorithm for simplifying a boolean expression with number comparisons?

提问人:Spu7Nix 提问时间:7/6/2021 最后编辑:John KugelmanSpu7Nix 更新时间:7/19/2021 访问量:812

问:

例如,如果我有表达式 , 该表达式可以简化为 ,并且对 A ∈ Z 仍然具有相同的行为。(A > 5) && (A == 6)(A == 6)

我还需要它来处理多个变量,因此例如应该简化为 .((B > 2) && (C == 2)) || ((B > 2) && (C < 2))(B > 2) && (C < 3)

我不需要比较两个未知数,只需要比较未知数和数字,我只需要它与运算符 、 和 数字以及 and 表达式(当然是 AND 和 OR)一起使用。所有未知数都是整数。<>==&&||&&||

是否有任何算法可以接受这样的表达式并返回具有相同行为和最少运算符数量的表达式?

(在我的具体情况下,运算符优先于||&&)

算法 器优化 代数

评论

1赞 David Eisenstat 7/6/2021
这在计算上至少与最小化布尔函数一样困难,布尔函数不仅是 NP-hard,而且是 NP-with-oracle-access-to-NP--hard。不过,我认为许多(指数时间)算法是可以推广的。
0赞 Matt Timmermans 7/6/2021
编译器中执行此类操作的部分称为“窥视孔优化”。你可以用谷歌搜索一下。这是一个很大的话题。这不是一个通用的算法,因为这是不切实际的。它的工作原理是匹配特定的模式,然后用更有效的模式替换。一个好的编译器可能有 1000 种模式,它将匹配和优化这些模式。
0赞 Gene 7/6/2021
GCC做到了。gcc.godbolt.org/z/9PPaTKMPz .一种方法是抽象解释。您会在谷歌搜索中找到许多参考资料。正如@DavidEisenstat所说,你不能指望一个真正的最小值,但你要求最小值,这对于给定的抽象领域选择是可以实现的。
0赞 Spu7Nix 7/6/2021
我认为有一种通用算法的原因是运算符数量很少。当只有一个未知变量时,每个比较都可以被认为是在数字线上定义一个部分,每个布尔表达式可以被认为是这些部分的重叠或联合。结果很容易找到最小表达式,因为没有 NOT 运算符等。我很确定,在多个维度上,用相同的几何思维可以解决更多变量的问题,但我很难解决这个问题。
0赞 Gene 7/6/2021
您已经描述了一个用于抽象解释的抽象域。这就是“通用算法”。

答:

2赞 David Eisenstat 7/6/2021 #1

这是一个慢速动态规划算法,与你所想到的一样。

from collections import defaultdict, namedtuple
from heapq import heappop, heappush
from itertools import product
from math import inf

# Constructors for Boolean expressions. False and True are also accepted.
Lt = namedtuple("Lt", ["lhs", "rhs"])
Eq = namedtuple("Eq", ["lhs", "rhs"])
Gt = namedtuple("Gt", ["lhs", "rhs"])
And = namedtuple("And", ["lhs", "rhs"])
Or = namedtuple("Or", ["lhs", "rhs"])

# Variable names. Arbitrary strings are accepted.
A = "A"
B = "B"
C = "C"

# Example formulas.
first_example = And(Gt(A, 5), Eq(A, 6))
second_example = Or(And(Gt(B, 2), Eq(C, 2)), And(Gt(B, 2), Lt(C, 2)))
third_example = Or(And(Gt(A, 1), Gt(B, 1)), And(Gt(A, 0), Gt(B, 2)))
fourth_example = Or(Lt(A, 6), Gt(A, 5))
fifth_example = Or(And(Eq(A, 2), Gt(C, 2)), And(Eq(B, 2), Lt(C, 2)))

# Returns a map from each variable to the set of values such that the formula
# might evaluate differently for variable = value-1 versus variable = value.
def get_critical_value_sets(formula, result=None):
    if result is None:
        result = defaultdict(set)
    if isinstance(formula, bool):
        pass
    elif isinstance(formula, Lt):
        result[formula.lhs].add(formula.rhs)
    elif isinstance(formula, Eq):
        result[formula.lhs].add(formula.rhs)
        result[formula.lhs].add(formula.rhs + 1)
    elif isinstance(formula, Gt):
        result[formula.lhs].add(formula.rhs + 1)
    elif isinstance(formula, (And, Or)):
        get_critical_value_sets(formula.lhs, result)
        get_critical_value_sets(formula.rhs, result)
    else:
        assert False, str(formula)
    return result


# Returns a list of inputs sufficient to compare Boolean combinations of the
# primitives returned by enumerate_useful_primitives.
def enumerate_truth_table_inputs(critical_value_sets):
    variables, value_sets = zip(*critical_value_sets.items())
    return [
        dict(zip(variables, values))
        for values in product(*({-inf} | value_set for value_set in value_sets))
    ]


# Returns both constants and all single comparisons whose critical value set is
# a subset of the given ones.
def enumerate_useful_primitives(critical_value_sets):
    yield False
    yield True
    for variable, value_set in critical_value_sets.items():
        for value in value_set:
            yield Lt(variable, value)
            if value + 1 in value_set:
                yield Eq(variable, value)
            yield Gt(variable, value - 1)


# Evaluates the formula recursively on the given input.
def evaluate(formula, input):
    if isinstance(formula, bool):
        return formula
    elif isinstance(formula, Lt):
        return input[formula.lhs] < formula.rhs
    elif isinstance(formula, Eq):
        return input[formula.lhs] == formula.rhs
    elif isinstance(formula, Gt):
        return input[formula.lhs] > formula.rhs
    elif isinstance(formula, And):
        return evaluate(formula.lhs, input) and evaluate(formula.rhs, input)
    elif isinstance(formula, Or):
        return evaluate(formula.lhs, input) or evaluate(formula.rhs, input)
    else:
        assert False, str(formula)


# Evaluates the formula on the many inputs, packing the values into an integer.
def get_truth_table(formula, inputs):
    truth_table = 0
    for input in inputs:
        truth_table = (truth_table << 1) + evaluate(formula, input)
    return truth_table


# Returns (the number of operations in the formula, the number of Ands).
def get_complexity(formula):
    if isinstance(formula, bool):
        return (0, 0)
    elif isinstance(formula, (Lt, Eq, Gt)):
        return (1, 0)
    elif isinstance(formula, And):
        ops_lhs, ands_lhs = get_complexity(formula.lhs)
        ops_rhs, ands_rhs = get_complexity(formula.rhs)
        return (ops_lhs + 1 + ops_rhs, ands_lhs + 1 + ands_rhs)
    elif isinstance(formula, Or):
        ops_lhs, ands_lhs = get_complexity(formula.lhs)
        ops_rhs, ands_rhs = get_complexity(formula.rhs)
        return (ops_lhs + 1 + ops_rhs, ands_lhs + ands_rhs)
    else:
        assert False, str(formula)


# Formula compared by complexity.
class HeapItem:
    __slots__ = ["_complexity", "formula"]

    def __init__(self, formula):
        self._complexity = get_complexity(formula)
        self.formula = formula

    def __lt__(self, other):
        return self._complexity < other._complexity

    def __le__(self, other):
        return self._complexity <= other._complexity

    def __eq__(self, other):
        return self._complexity == other._complexity

    def __ne__(self, other):
        return self._complexity != other._complexity

    def __ge__(self, other):
        return self._complexity >= other._complexity

    def __gt__(self, other):
        return self._complexity > other._complexity


# Like heapq.merge except we can add iterables dynamically.
class Merge:
    __slots__ = ["_heap", "_iterable_count"]

    def __init__(self):
        self._heap = []
        self._iterable_count = 0

    def update(self, iterable):
        iterable = iter(iterable)
        try:
            value = next(iterable)
        except StopIteration:
            return
        heappush(self._heap, (value, self._iterable_count, iterable))
        self._iterable_count += 1

    def __iter__(self):
        return self

    def __next__(self):
        if not self._heap:
            raise StopIteration
        value, index, iterable = heappop(self._heap)
        try:
            next_value = next(iterable)
        except StopIteration:
            return value
        heappush(self._heap, (next_value, index, iterable))
        return value


class Combinations:
    __slots__ = ["_op", "_formula", "_best_formulas", "_i", "_n"]

    def __init__(self, op, formula, best_formulas):
        self._op = op
        self._formula = formula
        self._best_formulas = best_formulas
        self._i = 0
        self._n = len(best_formulas)

    def __iter__(self):
        return self

    def __next__(self):
        if self._i >= self._n:
            raise StopIteration
        formula = self._op(self._formula, self._best_formulas[self._i])
        self._i += 1
        return HeapItem(formula)


# Returns the simplest equivalent formula, breaking ties in favor of fewer Ands.
def simplify(target_formula):
    critical_value_sets = get_critical_value_sets(target_formula)
    inputs = enumerate_truth_table_inputs(critical_value_sets)
    target_truth_table = get_truth_table(target_formula, inputs)
    best = {}
    merge = Merge()
    for formula in enumerate_useful_primitives(critical_value_sets):
        merge.update([HeapItem(formula)])
    best_formulas = []
    for item in merge:
        if target_truth_table in best:
            return best[target_truth_table]
        formula = item.formula
        truth_table = get_truth_table(formula, inputs)
        if truth_table in best:
            continue
        n = len(best_formulas)
        for op in [And, Or]:
            merge.update(Combinations(op, formula, best_formulas))
        best[truth_table] = formula
        best_formulas.append(formula)


print(simplify(first_example))
print(simplify(second_example))
print(simplify(third_example))
print(simplify(fourth_example))
print(simplify(fifth_example))

输出:

Eq(lhs='A', rhs=6)
And(lhs=Lt(lhs='C', rhs=3), rhs=Gt(lhs='B', rhs=2))
And(lhs=And(lhs=Gt(lhs='B', rhs=1), rhs=Gt(lhs='A', rhs=0)), rhs=Or(lhs=Gt(lhs='B', rhs=2), rhs=Gt(lhs='A', rhs=1)))
True
Or(lhs=And(lhs=Eq(lhs='B', rhs=2), rhs=Lt(lhs='C', rhs=2)), rhs=And(lhs=Gt(lhs='C', rhs=2), rhs=Eq(lhs='A', rhs=2)))

评论

1赞 David Eisenstat 7/7/2021
@Spu7Nix你可以通过直接组合真值表和复杂性来优化 - 比每次从头开始重新计算更有效,但不太清楚。
0赞 Spu7Nix 7/19/2021
它似乎遇到了一个问题 公式 ,它只是用数百万个项目填充堆,直到计算机内存耗尽。这是一个错误,还是效率低下?Or(And(Eq(A, 2), Gt(C, 2)), And(Eq(B, 2), Lt(C, 2)))
0赞 David Eisenstat 7/19/2021
@Spu7Nix就是效率低下。我将搜索升级为效率低下,这样它就可以解决您的新示例,但它仍然很糟糕。
0赞 Spu7Nix 7/19/2021
非常感谢,但似乎它仍然不够高效,因为如果我再添加一个变量,它会再次耗尽内存。你对我如何制作一个更具可扩展性的算法有什么想法吗?也许一个不使用 dp 的?这个线程中还有另一个答案,建议我尝试使用范围,但我很难看到它在所有情况下是如何工作的。如果变量少于 3 个并且所有其他公式的“不完美”算法,也许我可以使用您的算法?
0赞 David Eisenstat 7/19/2021
@Spu7Nix实现仍然非常浪费,但缩放是双倍指数的(每个附加变量的复杂性平方为不等式,每个相等的变量为立方体),因此高效(例如)C++端口可能会处理另一个变量。范围答案绝对行不通。我会研究生产编译器在做什么。
2赞 Daniel 7/6/2021 #2

也许您可以考虑变量的间隔,例如:

(A > 5) && (A == 6)

假设你有一个变量,为它设置一个初始间隔:。AA: [-∞, ∞]

您阅读的每个条件,您都可以缩短间隔:

(A > 5)  sets the interval for A: [6, ∞]
(A == 6) sets the interval for A: [6, 6]

对于间隔的每次更新,请检查新条件是否可行,例如:

(A > 5)  sets the interval for A: [6, ∞]
(A == 5) out of the interval, impossible condition.

再举一个例子:

((B > 2) && (C == 2)) || ((B > 2) && (C < 2))

最初: 和 .B: [-∞, ∞]C: [-∞, ∞]

((B > 2) && (C == 2))

(B > 2)  sets the interval for B: [3, ∞]
(C == 2) sets the interval for C: [2, 2]

下一个条件附加了 ,因此您可以添加间隔:||

((B > 2) && (C < 2)) 

(B > 2) sets the interval for B: [3, ∞]
(C < 2) sets the interval for C: [2, 2] U [-∞, 1] = [-∞, 2]

评论

0赞 Spu7Nix 7/7/2021
这没有考虑到的是,一个变量的区间可能取决于另一个值的值,例如对于表达式,一个变量的区间无法定义,因为它依赖于另一个变量(这个例子不能真正简化,所以它可能是多余的,但仍然)((A > 1) && (B > 1)) || ((A > 0) && (B > 2))
0赞 Abhinav Mathur 7/7/2021
@Spu7Nix,您可以递归地简化由运算符分隔的每个条件||
0赞 Spu7Nix 7/19/2021
你的方法将如何解决这样的情况:?(简化为((A == 2) || (B == 2)) && ((A < 3) || (B > 2))((B == 2) || (A == 2)) || (A < 3))