解法1: 递推法
分析
官方题解 给出的答案如下。
首先假设:
在上图的例子中,我们可以发现,对所有数字执行按位与运算的结果是所有对应二进制字符串的公共前缀再用零补上后面的剩余位。
[left, left+1, … , right]这些数字的与结果,一定是由右侧的连续0,和左侧的相同数字位组成的。
所有这些二进制字符串的公共前缀也即指定范围的起始和结束数字 m 和 n 的公共前缀(即在上面的示例中分别为 9 和 12)。
比如9,10,11,12的公共前缀时00001,而右侧有3位数与结果为0,所以9一直按位与到12结果是0001000。而题解认为,只要比较9和12的二进制位,它们的共同前缀0001可以直接放置到结果中,而它们第一个不同的位与其右侧的位都置为0。
证明
如何证明这个做法是正确的呢?
首先,我们任取数字n,并比较n和n+1
- 如果n为偶数,则n的二进制的最右侧为0(比如0001010),而n+1的二进制最右侧为1(比如0001011)。
- 如果n为奇数,则n的二进制最右侧为1,且可能有若干连续的1(比如0001011),右侧有两个连续的1。则n+1的二进制会是将那些连续的1变成0,再将其左侧的0变成1(比如0001100)。
所以,无论n是偶数还是奇数,当n变成n+1时,会做以下行为:
- 令右侧的那个0变成1,这样做,最右侧的与结果一定是0
- 令右侧连续的1变为0,并令那个左侧0变成1,如这么做,会令那个连续1和左侧0所在位置的与结果变成0.
总结而言,无论n是奇数/偶数,都会令右侧一个或若干个位的结果变为0。已经变为0的区域则不会再变为1,所以“零区”只会一直扩大,不会缩小。所以依次类推,当从left不断+1到right的过程中,一定会令越来越多右侧位的与结果会因为1&0的操作而变成0。
那么反过来说,如果比较left和right上的某个位,发现该位不同,则该位结果是0。又由于该位的0是由零区域一直扩大而来的,所以该位右侧的所有位上结果一定都是0。举个例子,当left和right分别是00001001和00001100的时候,左侧第5位不同,则它和它右侧的结果一定都是0.
那么它们的共同前缀00001上的位置结果是否为0?不会。因为假设取一个数字,它在这些前缀位置上不同,比如00010xxx,那么这个数显然大于上界right。如果取00000,则这个数显然小于下界left。也就是说,任何前缀不一样的数字,它都不在[left, right]范围中。
所以,只要找到左侧第一个不是共同前缀的位,它和它右侧的结果一定都是0,而它左侧都是共同前缀,结果会保留为那些前缀。
解法2:逐位法
分析
另一种方法则是诸位计算法,时间复杂度也是对数级,但代码较为复杂,不容易理解。
我们从右向左分析每一位,看它距离下一个“翻面”有多少,然后看left到right的跨度有多少,两者相比较。比如,假设最低位是第0位:
- 对于数字00110的第2位,其下一个“翻面”为01000,距离为2
- 对于数字00010的第2位,其下一个“翻面”为00100,距离为2
- 易知对于数字00100,其距离下一个翻面为4。
所以,其距离为 2 i − s t a r t 2^i-start 2i−start,start为第i位右侧的数字。举例子,当i=2时,数字00110的右侧数字为 ( 10 ) 2 (10)_2 (10)2。我们比较right-left的跨度delta是否大于等于距离d:
- 如果是,则说明在跨度内该位会发生“翻转”,其与结果为0。
- 否则,该位不会“翻转”,保持原样。
答案
class Solution:
def rangeBitwiseAnd(self, left: int, right: int) -> int:
delta = right - left
res = 0
max_i = 30
while max_i >= 0:
if ((1 << max_i) & right) > 0:
break
max_i -= 1
for i in range(max_i+1):
d_range = 1 << i
start = ((1 << i) - 1) & left
d = d_range - start
if d > delta:
res |= (1 << i) & left
return res