用于检查二进制数组是不是可以旋转到元素总和不超过 1 的快速算法

Posted

技术标签:

【中文标题】用于检查二进制数组是不是可以旋转到元素总和不超过 1 的快速算法【英文标题】:Fast algorithm for checking if binary arrays can be rotated to not have an elementwise sum over 1用于检查二进制数组是否可以旋转到元素总和不超过 1 的快速算法 【发布时间】:2018-08-04 03:35:17 【问题描述】:

假设我有一组仅包含 0 和 1 的恒定长度数组。我的目标是找出在任何数组旋转后,数组的元素总和是否不会超过 1。

例如,假设我有以下三个数组:[1, 0, 0, 0], [1, 0, 1, 0][1, 0, 0, 0]。我可以将第二个数组旋转一个元素,将第三个数组旋转两个元素以获得数组[1, 0, 0, 0], [0, 1, 0, 1], [0, 0, 1, 0],其元素总和为[1, 1, 1, 1]。但是,如果我不应用旋转,我会得到 [3, 0, 1, 0] 的总和,这不符合我的要求,因为其中一个元素(3)大于 1。

现在,我的问题是,有什么方法可以快速确定这是否适用于任意数量的数组?例如,无法旋转 [1, 0, 0, 0], [1, 0, 1, 0], [1, 0, 1, 0] 以使总和的元素不超过 1。

当前的启发式方法

显然,如果数组的总和,例如长度n,超过n,那么这是微不足道的。 到目前为止,我能想到的最佳方法是采用两个数组,找到一种方法将它们合并在一起,然后反转结果。然后,我们取这个结果和下一个数组,并重复这个过程。但是,这种方法并不能保证找到解决方案(如果存在)。

我的问题是,如果没有尝试所有可能的旋转,那么解决这个问题的好算法是什么?

【问题讨论】:

N 对你的任务来说有多大? 非常小——数组的长度小于 100。 【参考方案1】:

您可以将此问题简化为exact cover 问题并使用已知算法之一进行精确覆盖(Knuth 的算法 X、整数线性规划、SAT 求解,如 sascha 提醒我的那样,可能还有其他算法)。减少涉及创建每个输入数组的所有旋转并使用指示器扩展它们以确保选择恰好一个旋转。以实例[1, 0, 0, 0], [1, 0, 1, 0], [1, 0, 0, 0]为例,准确的覆盖实例是

[1, 0, 0, 0; 1, 0, 0]  # from [1, 0, 0, 0]
[0, 1, 0, 0; 1, 0, 0]
[0, 0, 1, 0; 1, 0, 0]
[0, 0, 0, 1; 1, 0, 0]
[1, 0, 1, 0; 0, 1, 0]  # from [1, 0, 1, 0]
[0, 1, 0, 1; 0, 1, 0]
[1, 0, 0, 0; 0, 0, 1]  # from [1, 0, 0, 0]
[0, 1, 0, 0; 0, 0, 1]
[0, 0, 1, 0; 0, 0, 1]
[0, 0, 0, 1; 0, 0, 1]
[1, 0, 0, 0; 0, 0, 0]  # extra columns to solve the impedance mismatch
[0, 1, 0, 0; 0, 0, 0]  # between zeros being allowed and exact cover
[0, 0, 1, 0; 0, 0, 0]
[0, 0, 0, 1; 0, 0, 0]

我感觉你的问题是 NP 难的,这意味着反向也有减少,因此对于可证明的最坏情况运行时间是次指数的算法没有希望。

编辑:是的,这个问题是 NP 难题。我将通过示例演示 3-partition 的简单化简。

假设 3-partition 实例是[20, 23, 25, 45, 27, 40]。然后我们做一个二进制数组

[1, ..(20 ones in total).., 1, 0, ..., 0]
[1, ..(23 ones in total).., 1, 0, ..., 0]
[1, ..(25 ones in total).., 1, 0, ..., 0]
[1, ..(45 ones in total).., 1, 0, ..., 0]
[1, ..(27 ones in total).., 1, 0, ..., 0]
[1, ..(40 ones in total).., 1, 0, ..., 0].

我们正在寻找一个分区,其中两个部分的总和为90,因此最终数组是一个“模板”

[1, 0, ..(90 zeros in total).., 0, 1, 0, ..(90 zeros in total).., 0]

强制执行 3 分区约束。

【讨论】:

我担心你的减少,因为看起来二进制数组的长度可能是原始输入大小的指数,因为原始输入中的数字变成了数组长度。 @mcdowella 幸运的是 3-partition 是强 NP-hard,所以一元是可以的【参考方案2】:

我还没有决定这个问题是 P 还是 NP 难。肯定有很多数学结构可以利用。。见David's answer。

但在其他人提出一个好的解决方案之前,这里的东西在理论上可行,在实践中也可能可行。

基本思想是:将其表述为SAT-problem,并使用very efficient solvers 来解决这种组合问题。

我们在这里使用的 SAT 求解器类型是 CDCL-based solvers,它们是完整且可靠的(他们会找到可行的解决方案或证明没有解决方案!)。

分析(天真的方法)

虽然 SAT 求解通常是 NP-hard,但通常可以求解具有数百万个变量和约束的实例。但这并不能保证这将在这里工作。没有测试很难说,遗憾的是,没有提供测试数据。

公式很简单:

N * M 二进制变量: N marks the data-row; M the roation/shift value A:预处理所有可能的成对冲突 B:添加每行至少使用一个配置的约束 C: 添加约束禁止冲突

对于M=100 列中的N=100 行,将有4950 个有序对,每个都乘以M*M (pairwise rotation-combinations)。所以有<= 4950 * 100 * 100 = 49.500.000 冲突检查(这在慢速语言中甚至是可行的)。这也是冲突约束数量的上限。

当然,如果您有非常稀疏的数据,这可能会改变,这允许 N 在固定 M 的情况下增长(在可行实例世界中)。

可能有很多潜在的减少。

这里的要点

预处理工作量很大(渐近!),该方法基于注释数组的长度小于 100 SAT 求解在传播方面非常快,如果是 P 或 NP-hard,我们提供的约束类型在传播效率方面非常强大 建议根据经验(根据您的数据)对此进行测试!

备注:

没有<= per row 约束,在某些情况下可能选择了两种配置。解决方案重构代码不检查这种情况(但没有理论上的问题 -> 只需选择一个 => 将兼容)。

代码

from pyCadical import PyCadical  # own wrapper; not much tested; @github
import itertools
import numpy as np

""" DATA """
data = np.array([[1, 0, 0, 0],
                 [1, 0, 1, 0],
                 [1, 0, 0, 0]])

""" Preprocessing """
N = len(data)
M = len(data[0])

conflicts = []
for i, j in itertools.combinations(range(N), 2):
    for rotA in range(M):
        for rotB in range(M):
            if np.amax(np.roll(data[i], rotA) + np.roll(data[j], rotB)) > 1:
                conflicts.append((i, j, rotA, rotB))
conflicts = np.array(conflicts)

""" SAT """
cad = PyCadical()
vars = np.arange(N*M, dtype=int).reshape(N,M) + 1

# at least one rotation chosen per element
for i in range(N):
    cad.add_clause(vars[i, :])  # row0rot0 OR row0rot1 OR ...

# forbid conflicts
for i, j, rotA, rotB in conflicts:
    cad.add_clause([-vars[i, rotA], -vars[j, rotB]])  # (not rowIrotA) or (not rowJrotB)

""" Solve """
cad.solve()

""" Read out solution """
sol = cad.get_sol_np().reshape(N, M)
chosen = np.where(sol > 0)

solution = []  # could be implemented vectorized
for i in range(N):
    solution.append(np.roll(data[i], chosen[1][i]))

print(np.array(solution))

输出

[[0 1 0 0]
 [1 0 1 0]
 [0 0 0 1]]

【讨论】:

【参考方案3】:

我会将每个位集合视为(足够大的)整数。

假设我在n 位上有这样一个整数集合。下面是一些 Squeak Smalltalk 代码,展示了如何减少一点组合:

SequenceableCollection>>canPreventsOverlapingBitByRotatingOver: n
    "Answer whether we can rotate my elements on n bits, such as to obtain non overlaping bits"
    | largestFirst nonNul nonSingletons smallest |

    "Exclude trivial case when there are more than n bits to dispatch in n holes"
    (self detectSum: #bitCount) > n ifTrue: [^false].

    "Exclude non interesting case of zero bits"
    nonNul := self reject: [:each | each = 0].

    "Among all possible rotations, keep the smallest"
    smallest := nonNul collect: [:each | each smallestAmongBitRotation: n].

    "Note that they all have least significant bit set to 1"
    [smallest allSatisfy: [:each | (each bitAnd: 1) = 1]] assert.

    "Bit singletons can occupy any hole, skip them"
    nonSingletons := smallest reject: [:each | each = 1].

    "Sort those with largest bitCount first, so as to accelerate detection of overlaping"
    largestFirst := nonSingletons sorted: #bitCount descending.

    "Now try rotations: all the shift must differ, otherwise the shifted LSB would overlap"
    ^largestFirst checkOverlapingBitRotated: n

我们将以下实用程序定义为:

SequenceableCollection>>checkOverlapingBitRotated: n
    "Answer true if the bits of my elements can be rotated on n bits so as to not overlap"
    ^self checkOverlapingBitRotatedBy: (1 << n - 1) among: n startingAt: 2 accum: self first

SequenceableCollection>>checkOverlapingBitRotatedBy: shiftMask among: n startingAt: index accum: accum
    index > self size ifTrue: [^true].
    (shiftMask bitClear: accum) bitsDo: [:bit |
        | shifted |
        shifted := (self at: index) bitRotate: bit lowBit - 1 among: n.
        ((accum bitAnd: shifted) = 0
            and: [self
                    checkOverlapingBitRotatedBy: shiftMask
                    among: n
                    startingAt: index + 1
                    accum: (accum bitOr: shifted)])
            ifTrue: [^true]].
    ^ false

这需要额外的解释:shiftMask 中的每一位都表示可能的移位(等级)。由于累加器已经占用了一些位,并且每个元素的 LSB 为 1,我们不能将剩余的元素移动到累加器已经占用的位。因此,我们必须从掩码中清除累加器占用的位。这大大减少了组合,这就是为什么首先排序最大的 bitCount 是有益的。

其次,守卫(accum bitAnd: shifted) = 0 会尽快切断递归,而不是生成无用的组合并在后验中测试不可行性。

然后我们就有了那些小的实用程序:

Integer>>bitRotate: shift among: n
    "Rotate the n lowest bits of self, by shift places"
    "Rotate left if shift is positive."
    "Bits of rank higher than n are erased."
    | highMask lowMask r |
    (r := shift \\ n) = 0 ifTrue: [^self].
    lowMask := 1 << (n - r) - 1.
    highMask := 1 << n - 1 - lowMask.
    ^((self bitAnd: lowMask) << r)
        bitOr: ((self bitAnd: highMask) >> (n - r))

Integer>>smallestAmongBitRotation: n
    "Answer the smallest rotation of self on n bits"
    ^self
        bitRotate: ((1 to: n) detectMin: [:k | self bitRotate: k among: n])
        among: n

Integer>>bitsDo: aBlock
    "Evaluate aBlock will each individual non nul bit of self"
    | bits lowBit |
    bits := self.
    [bits = 0] whileFalse: [
        lowBit := (bits bitAnd: 0 - bits).
        aBlock value: lowBit.
        bits := bits - lowBit].

它立即适用于像这样的小型收藏:

| collec bitCount |
collec := #( 2r11 2r1001  2r1101 2r11011 2r1110111 2r11001101
       2r11010010111010 2r1011101110101011100011).
bitCount := collec detectSum: #bitCount.
(bitCount to: bitCount*2) detect:
    [:n | collec canPreventsOverlapingBitByRotatingOver: n].

会回答 52,这意味着我们需要至少 52 位才能获得不重叠的组合,尽管 bitCount 只有 44。

所有操作都是通过简单的位操作执行的,并且应该可以很好地扩展(一旦翻译成静态语言)。

这不是我的 32 位解释器的情况,它会创建装箱的大整数,给垃圾收集器施加压力,并且需要一些时间来处理 10 个集合,总共大约 100 位:

| collec bitCount |
collec := (1 to: 10) collect: [:i | (1 << 18 - 1) atRandom].
bitCount := collec detectSum: #bitCount.
bitCount ->
    [ collec canPreventsOverlapingBitByRotatingOver: bitCount + 10] timeToRun.

bitCount=88 的第一次尝试耗时 75 秒

更公平(稀疏)的位分布导致更快的平均时间(并且仍然是可怕的最坏时间):

| collec bitCount |
collec := (1 to: 15) collect: [:i |
    ((1 to: 4) collect: [:j | (1 to: 1<<100-1) atRandom])
        reduce: #bitAnd:].
bitCount := collec detectSum: #bitCount.
bitCount ->
    [ collec canPreventsOverlapingBitByRotatingOver: (bitCount + 10 max: 100)] timeToRun.

104->1083ms
88->24ms    
88->170ms
91->3294ms
103->31083ms

【讨论】:

以上是关于用于检查二进制数组是不是可以旋转到元素总和不超过 1 的快速算法的主要内容,如果未能解决你的问题,请参考以下文章

问题列表

c_cpp 检查数组中是否存在两个元素,其总和等于数组其余部分的总和

找到所需的最小元素数,以使它们的总和等于或超过 S

检查总和的算法问题

检查元素索引是不是在二维数组内(用于在任何方向移动一)

如果使用Arrays Stream的整数数组的总和超过int max value(2147483647),如何处理?