C++ 中的 lowbit

lowbit · 浏览次数 : 51

小编点评

lowbit 函数的定义是求一个整数 n 的二进制表示中最低位(最右边一位)的 1 及其后的所有 0 所表示的数。对于一个正整数 n,其二进制表示为 00...0xx...xx(其中 x 代表 0 或 1),则 lowbit(n) 就是这个表示中最右边的 1 及其后的所有 0 所表示的数。对于一个负整数 n,其二进制表示为 10...0xx...xx(其中 x 代表 0 或 1),则 lowbit(n) 就是这个表示中最右边的 1 及其后的所有 0 所表示的数。 lowbit 函数可以通过递归或公式两种方式进行计算。在递归方式中,我们首先假定 n 为正整数,将其转换为二进制,然后根据二进制表示中最低位的 1 及其后的所有 0 所表示的数来递归计算 lowbit(n)。在公式方式中,我们观察到 n 和 -n 的二进制表示之间的关系,从而推导出了一个公式来直接计算 lowbit(n),将时间复杂度降低到 O(1)。 lowbit 函数的应用非常广泛,例如在树状数组等数据结构中就可以使用 lowbit 函数来求解某些操作的时间复杂度。

正文

lowbit 的定义

首先了解 lowbit 的定义

\(lowbit(n)\) ,为 \(n\) 的二进制原码中最低的一位 \(1\) 以及其后面的 \(0\) 所表示的数

举个简单的例子:

\(10\) 使用二进制表示为 \(1010\)

其中最低位的 \(1\) 为第2位(\(_{10}1_0\),从右往左数)

此时 \(lowbit(10)\) 使用二进制表示为 \(10\) ,即 \(2\) . (有关进制转换详见进制与进制转换


lowbit 的计算

如何计算 \(lowbit(n)\) 呢?

lowbit 的特殊情况

由于 lowbit 会将除最低位 \(1\) 以外所有的位 \(1\) 改为 \(0\)lowbit 将只会对位 \(1\) 的位数高于1的二进制数产生影响,所以位 \(1\) 只有1位的二进制数和 \(0\) 处理后将得到原数据,即:

\[lowbit(2^n) = 2^n ~~~ ( n >= 0) \]

\[lowbit(0) = 0 \]

方法一:递归

先暂时假定 \(n\) 为正整数

\(n\) 转换为二进制,可得: \(00...00x...xxx\) ( \(x\)\(0\)\(1\))

此时 \(n · 2\) 转换为二进制可得: \(00...00x...xxx · 10 ~=~ 00...0xx...xx0\)

假设 \(n\) 转为二进制后,末尾有 \(m\) 个连续位 \(0\) (显然,此时 \(lowbit(n) ~ = ~ 2^m\)

因此, \(n · 2\) 转为二进制后,末尾有 \(m + 1\) 个连续位 \(0\) (此时 \(lowbit(n · 2) ~ = ~ 2^{m + 1} ~ = ~ 2^m · 2\)

于是我们得到了:

\[lowbit(n · 2) = lowbit(n) · 2 \]

此时 \(n · 2\) 表示为二进制是 \(00...0xx...xx0\) ,怎么样,有没有什么想法?

\(n · 2\) 加上 \(1\) ,得: \(00...0xx...xx0 + 1 ~ = ~ 00...0xx...xx1\)

显然:

\[lowbit(2n + 1) ~ = ~ 1 \]

观察 \(n\) 的二进制形式: \(00...00x...xxx\)

对比 \(-n\) 的二进制形式:\(10...00x...xxx\) (在原码中,我们一般使用第一位存储符号, \(0\) 为正, \(1\) 为负)

很明显, \(lowbit(n) ~ = ~ lowbit(-n)\)

无论 \(n\) 的符号为负还是为正,奇偶性都一致,因此,我们在上面推导出来的公式对负整数仍然成立

综上所述,任意奇数的 lowbit 值都为 \(1\) ,任意偶数的 lowbit 值都为其乘 \(0.5\) 得到的值的 lowbit 值乘 \(2\)

通过这种思路,我们可以编写一个递归函数计算 \(n\)lowbit 值,遇到奇数直接返回 \(1\) ,遇到偶数辄除以 \(2\) 后继续计算

写成伪代码是这样的:

int lowbit(int n) {
    if (n % 2 == 1) return 1;  // Odd
    else return lowbit(n / 2) * 2;  // Even
}

方法二:公式

在方法一中,我们使用了深度优先搜索,时间复杂度可能有点高,我们当然可以使用记忆化数组降低复杂度,但,我们是否可以推导出一个公式直接计算,将复杂度降低为 \(O(1)\) 呢?

当然是可行的。还是观察 \(n\) 的二进制形式: \(00...00x...xxx\) (假定 \(n\) 为非负整数)

还是对比 \(-n\) 的二进制形式:\(10...00x...xxx\)

如果对 \(10...00x...xxx\) 每一位取反(符号位除外),我们就得到了 \(-n\) 的反码: \(11...11(-x)...(-x)(-x)(-x)\)

此时 \(-n\) 末尾的 \(0\) 全部变为 \(1\) ,而最低位的 \(1\) 也难逃变为 \(0\) 的命运

如果我们现在将其加上 \(1\) ,我们将得到 \(-n\) 的补码: \(11...11(-x)...(-x)(-x)(-x) + 1\) ,反码末尾的 \(1\) 将重新变为 \(0\) ,而最低位 \(0\) 将重新变为 \(1\) ,其他位不变,仍然是取反的状态,此时如果将 \(-n\) 的补码与 \(n\) 原码进行按位与的运算( \(n\)\(-n\) 的原码只有符号位的不同),由于除符号位、最低位 \(1\) 及其后面的位 \(0\) ,其他位都进行了取反,这些位将被赋值为 0 & 11 & 0,即 \(0\) ,而符号位也会赋值为 0 & 1 ,只剩最低位 \(1\) 及其后面的位 \(0\) 分别被赋值为 1 & 10 & 0 ,即 \(1\)\(0\) ,最后结果为 \(lowbit(n)\) (或者说 \(lowbit(-n)\)

那么 \(n\) 的反码、补码呢?上文所述的只是负数的反码与补码的计算方式,实际上,正数的原码、反码、补码都是一样的(对于原码、反码、补码,本博文已经进行了必要的解释,但如果你对其感兴趣,想知道其详细解释,可参考这篇博文:二进制|原码、反码、补码

众所周知, C++ 中,数字是使用补码表示的,因此,我们可以将 \(n\) 的补码视为 \(n\) 的原码,在 C++ 中进行运算。于是,我们得到了\(n\) 的原码和 \(-n\) 的补码

上文中,我们提到了将 \(n\) 的原码和 \(-n\) 的补码进行按位与运算可以得到 \(lowbit(n)\)\(lowbit(-n)\) ,现在我们使用 \(n\) 的补码作为其原码(毕竟是一样的),可以得到:

\[lowbit(n) = -n & n \]

显然 \(n\) 是负数也不会造成影响

于是我们成功地得到了 lowbit 的计算公式,将算法的时间复杂度降低为 \(O(1)\) ,并简化了代码:

#define lowbit(n) (-n & n)

由于使用宏定义,一定要记得打括号,位运算的优先级是最低的


lowbit 的应用

lowbit 的应用也有很多,例如树状数组等,如果你对这方面感兴趣,不妨订阅一下我的博客,我以后会发布更多有趣且有用的算法知识

与C++ 中的 lowbit相似的内容:

C++ 中的 lowbit

lowbit 的定义 首先了解 lowbit 的定义 \(lowbit(n)\) ,为 \(n\) 的二进制原码中最低的一位 \(1\) 以及其后面的 \(0\) 所表示的数 举个简单的例子: 将 \(10\) 使用二进制表示为 \(1010\) 其中最低位的 \(1\) 为第2位(\(_{10}1

C#中的对象深拷贝和浅拷贝

目录C#中的对象深拷贝和浅拷贝概述1. 浅拷贝2. 深拷贝总结引用 C#中的对象深拷贝和浅拷贝 概述 在C#中,对象拷贝是指将一个对象的副本创建到另一个对象中。对象拷贝通常用于数据传输或创建对象的新实例。 C#中有两种主要的拷贝方式:浅拷贝和深拷贝 1. 浅拷贝 浅拷贝是指只拷贝对象的值类型成员,而

C#中的浅拷贝与深拷贝

## 前言 众所周知,C#中有两种类型变量:那就是**值类型**和**引用类型**。对于值类型而言,copy就相当于是全盘复制了,真正的实现了复制,属于**深拷贝**;而对于引用类型而言,一般的copy只是**浅拷贝**,只是copy到了引用对象的地址,相当于值传递了一个引用指针,==新的对象通过地

toLua中Lua调用C#中的类

toLua中Lua调用C#: [7]Lua脚本调用C#中的class 准备工作:打算在Lua脚本中使用Debug,使用lua调用C#脚本,需要绑定LuaState和自定义添加Debug Generated by EmmyLua(https://github.com/EmmyLua) Created

深入理解 C++ 中的多态与文件操作

C++ 多态 多态(Polymorphism)是面向对象编程(OOP)的核心概念之一,它允许对象在相同操作下表现出不同的行为。在 C++ 中,多态通常通过继承和虚函数来实现。 理解多态 想象一个场景,你有一个动物园,里面有各种动物,如猫、狗、鸟等。每个动物都有自己的叫声。使用面向对象编程,我们可以创

C++的extern关键字在HotSpot VM中的重要应用

extern关键字有两个用处: (1)extern在C/C++语言中表示函数和全局变量作用范围(可见性)的关键字,这个关键字会告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。 (2)在C++中引用C语言中的函数和变量,在包含C语言头文件时,需要使用extern "C"来处理。 1、ext

热更学习笔记10~11----lua调用C#中的List和Dictionary、拓展类中的方法

[10]Lua脚本调用C#中的List和Dictionary 调用还是在上文中使用的C#脚本中Student类: lua脚本: print(" 访问使用C#脚本中的List和Dictionary ") student.list:Add(2024) student.list:Add(5) studen

热更学习笔记--toLau中lua脚本对C#中枚举和数组的访问

[8]Lua脚本调用C#中的枚举学习 --调用枚举类型 print(" toLua中调用C#中枚举类型 ") PrimitiveType = UnityEngine.PrimitiveType local cubeObj = GameObject.CreatePrimitive(PrimitiveT

C++ 重载运算符在HotSpot VM中的应用

C++支持运算符重载,对于Java开发者来说,这个可能比较陌生一些,因为Java不支持运算符重载。运算符重载本质上来说就是函数重载。下面介绍一下HotSpot VM中的运算符重载。 1、内存分配与释放 在C++中可以通过new运算符创建一个C++的类实例,这个操作实际上上包含了如下3个步骤: 调用o

QML 怎么调用 C++ 中的内容?

关于Qt Quick里的开发笔记,这里主要是总结一下,怎么在 QML 文件中引用 C ++ 文件里定义的内容。