累积按位运算

时间:2018-09-03 03:35:36

标签: java arrays algorithm bit-manipulation bit

假设您有一个数组A = [x, y, z, ...]

然后您计算一个前缀/累积的BITWISE-OR数组P = [x, x | y, x | y | z, ... ]

如果我想找到索引1和索引6之间的元素的BITWISE-OR,如何使用此预先计算的P数组来做到这一点?有可能吗?

我知道它可以累积总和来获得一定范围内的总和,但是我不确定使用位运算。

编辑A中允许重复,因此有可能A = [1, 1, 2, 2, 2, 2, 3]

3 个答案:

答案 0 :(得分:3)

不可能使用前缀/累积的BITWISE-OR数组来计算某个随机范围的按位或,您可以尝试使用2个元素的简单情况来验证自己。

但是,有另一种方法,即使用前缀和。

假设我们正在处理32位整数,我们知道,对于范围x到y的按位或和,如果范围内存在一个数字,结果的ith位将为1(具有ith位的x,y)为1。因此,通过反复回答以下查询:

  • 是否存在ith位设置为1的范围(x,y)中的任何数字?

我们可以形成问题的答案。

那么如何检查范围(x,y)中是否至少有一个设置了ith位的数字?我们可以预处理并填充数组pre[n][32],该数组包含数组中所有32位的前缀和。

for(int i = 0; i < n; i++){
   for(int j = 0; j < 32; j++){
       //check if bit i is set for arr[i]
       if((arr[i] && (1 << j)) != 0){
           pre[i][j] = 1;
       }
       if( i > 0) {
           pre[i][j] += pre[i - 1][j];
       }
   }
}

并且,检查范围i是否等于位(x, y)是否设置为:

pre[y][i] - pre[x - 1][i] > 0

重复此检查32次以计算最终结果:

int result = 0;
for (int i = 0; i < 32; i++){
   if((pre[y][i] - (i > 0 ? pre[x - 1][i] : 0)) > 0){
       result |= (1 << i);
   }
}
return result;

答案 1 :(得分:0)

如果您有足够的存储空间,Pham Trungs的答案显然是解决之道,因为它可以持续运行。如果该数组很大,并且您无法创建多个其他相同大小的数组,则建议使用以下简单方案:

预处理数组A并计算块中的元素的按位或,例如一千个元素,并将它们存储在数组B中。此数组的大小当然小一千倍。然后,执行查找时,可以使用数组B进行大部分范围的查找,查找速度应快近一千倍。

您也可以分几个步骤进行操作,例如每个A中的一百个元素组成一个数组B,每个B中的一百个元素组成一个数组C,依此类推。

为了选择最佳的块大小,最好对要查找的范围进行统计细分,或者至少对平均情况有所了解。

答案 2 :(得分:0)

纯前缀数组不起作用,因为为了支持任意范围查询,它要求元素相对于运算符具有反函数,因此例如,反函数为负数,对于XOR,反函数为元素本身,对于按位或,则没有逆。

基于同样的原因,二叉索引树也不起作用。

但是一个横向堆确实可以工作,但代价是要存储大约2 * n到4 * n个元素(取决于四舍五入后加多少),​​而扩展要比32 * n小得多。这不会充分利用侧向堆,但是它避免了显式链接树的问题:块状节点对象(每个节点约32个字节)和指针追逐。可以使用常规的隐式二叉树,但是这使得将其索引与原始数组中的索引关联起来更加困难。侧向堆就像一个完整的二叉树,但是,从概念上讲,没有根-实际上,我们确实有一个根,即存储的最高级别上的单个节点。像常规的隐式二叉树一样,侧向堆也被隐式链接,但是规则不同:

  • left(x) = x - ((x & -x) >> 1)
  • right(x) = x + ((x & -x) >> 1)
  • parent(x) = (x & (x - 1)) | ((x & -x) << 1)

此外,我们还可以计算其他一些东西,例如:

  • leftmostLeaf(x) = x - (x & -x) + 1
  • rightmostLeaf(x) = x + (x & -x) - 1
  • 两个节点的最低共同祖先,但是公式有点大。

x & -x可以写为Integer.lowestOneBit(x)的地方。

该算术看起来晦涩难懂,但是结果却是这样的结构,您可以逐步进行算术确认(来源:计算机编程艺术,第4A卷,按位技巧和技巧):

sideways heap

无论如何,我们可以通过以下方式使用此结构:

  • 将原始元素存储在叶子中(奇数索引)
  • 对于每个偶数索引,存储其子项的按位OR
  • 对于范围查询,请计算表示不超出查询范围的范围的元素的OR。

对于查询,首先将索引映射到叶索引。例如1-> 3和3-> 7。然后,找到端点的最低共同祖先(或仅从最高节点开始)并递归定义:

rangeOR(i, begin, end):
    if leftmostLeaf(i) >= begin and rightmostLeaf(i) <= end
        return data[i]
    L = 0
    R = 0
    if rightmostLeaf(left(i)) >= begin
        L = rangeOR(left(i), begin, end)
    if leftmostLeaf(right(i)) <= end
        R = rangeOR(right(i), begin, end)
    return L | R

因此,与完全覆盖的范围相对应的任何节点都将作为整体使用。否则,如果左孩子或右孩子全部被覆盖,则必须递归查询他们的贡献,如果其中一个未被覆盖,则取零。顺便说一下,我假设查询在两端都是包含在内的,因此范围包括beginend

事实证明,rightmostLeaf(left(i))leftmostLeaf(right(i))可以简化很多,即分别简化为i - (~i & 1)(或者:(i + 1 & -2) - 1)和i | 1。但是,这似乎非常不对称。在i不是叶子的假设下(由于叶子被完全覆盖或根本不被查询,因此不会出现在该算法中),它们分别成为i - 1i + 1分别好得多。无论如何,我们可以使用节点的所有左后代具有比其更低的索引,而所有右后代具有更高的索引。

可以用Java编写(未经测试):

int[] data;

public int rangeOR(int begin, int end) {
    return rangeOR(data.length >> 1, 2 * begin + 1, 2 * end + 1);
}

private int rangeOR(int i, int begin, int end) {
    // if this node is fully covered by [begin .. end], return its value
    int leftmostLeaf = i - (i & -i) + 1;
    int rightmostLeaf = i + (i & -i) - 1;
    if (leftmostLeaf >= begin && rightmostLeaf <= end)
        return data[i];

    int L = 0, R = 0;
    // if the left subtree contains the begin, query it
    if (begin < i)
        L = rangeOR(i - (Integer.lowestOneBit(i) >> 1), begin, end);
    // if the right subtree contains the end, query it
    if (end > i)
        R = rangeOR(i + (Integer.lowestOneBit(i) >> 1), begin, end);
    return L | R;
}

另一种策略是从底部开始,一直向上直到双方碰面,同时又在向上收集数据。从begin开始且其父级位于其右侧时,父级的右子级比begin的索引高,因此它属于查询范围的一部分-除非父级是公共祖先两个向上的“链”。例如(未经测试):

public int rangeOR(int begin, int end) {
    int i = begin * 2 + 1;
    int j = end * 2 + 1;
    int total = data[i];
    // this condition is only to handle the case that begin == end,
    // otherwise the loop exit is the `break`
    while (i != j) {
        int x = (i & (i - 1)) | (Integer.lowestOneBit(i) << 1);
        int y = (j & (j - 1)) | (Integer.lowestOneBit(j) << 1);
        // found the common ancestor, so done
        if (x == y) break;
        // if the low chain took a right turn, the right child is part of the range
        if (i < x)
            total |= data[x + (Integer.lowestOneBit(x) >> 1)];
        // if the high chain took a left turn, the left child is part of the range
        if (j > y)
            total |= data[y - (Integer.lowestOneBit(y) >> 1)];
        i = x;
        j = y;
    }
    return total;
}

首先构建树并不是一件容易的事,以索引的升序构建树是行不通的。可以从底部开始逐级构建。较高的节点被尽早触摸(例如,对于第一层,模式为2, 4, 6,而4在第二层中),但是无论如何它们都将被覆盖,暂时保留非最终值是可以的有价值。

public BitwiseORRangeTree(int[] input) {
    // round length up to a power of two, then double it
    int len = input.length - 1;
    len |= len >> 1;
    len |= len >> 2;
    len |= len >> 4;
    len |= len >> 8;
    len |= len >> 16;
    len = (len + 1) * 2;

    this.data = new int[len];

    // copy input data to leafs, odd indexes
    for (int i = 0; i < input.length; i++)
        this.data[i * 2 + 1] = input[i];

    // build higher levels of the tree, level by level
    for (int step = 2; step < len; step *= 2) {
        for (int i = step; i < this.data.length; i += step) {
            this.data[i] = this.data[i - (step >> 1)] | this.data[i + (step >> 1)];
        }
    }
}