假设您有一个数组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]
。
答案 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卷,按位技巧和技巧):
无论如何,我们可以通过以下方式使用此结构:
对于查询,首先将索引映射到叶索引。例如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
因此,与完全覆盖的范围相对应的任何节点都将作为整体使用。否则,如果左孩子或右孩子全部被覆盖,则必须递归查询他们的贡献,如果其中一个未被覆盖,则取零。顺便说一下,我假设查询在两端都是包含在内的,因此范围包括begin
和end
。
事实证明,rightmostLeaf(left(i))
和leftmostLeaf(right(i))
可以简化很多,即分别简化为i - (~i & 1)
(或者:(i + 1 & -2) - 1
)和i | 1
。但是,这似乎非常不对称。在i
不是叶子的假设下(由于叶子被完全覆盖或根本不被查询,因此不会出现在该算法中),它们分别成为i - 1
和i + 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)];
}
}
}