位掩码 位运算 集合

引言

利用CPU使用二进制表示和处理数据这种特性,将整数的二进制表示法用作数据结构的方法就是位掩码(bitmask),严格来说,位掩码算不上数据结构,但可以作为一个很有用的工具。

使用位掩码的代码的优点:

(1)更快的执行速度。
(2)更简洁的代码。
(3)更少的内存占用量。
(4)用数组替代关联数组。

术语定义:计算机将所有的整数变量表示为二进制的形式,此时,一个二进制位就称为位bit,位只能取0或1,是计算机表示数据的基础。

位运算符

(1)位单位进行的AND运算按照位比较输入的两个整数,两整数的当前位都是1的时候结果才是1。代码表示为:a&b。
(2)位单位OR运算按照位比较输入的两个整数,两整数有一位是1,结果就是1。代码表示为:a|b。
(3)位单位XOR运算按照位比较输入的两个整数,两整数有一位是1一位是0,结果才是1。代码表示为:a^b。
(4)位单位NOT运算会对输入的整数各位状态进行取反。代码表示为:~a。
(5)位移运算符(shift)能都将整数a按照位向左或向右移动。代码表示为:将整数a左移b位a<<b,将a右移b位a>>b。

注意事项

(1)&、|、^位运算符的优先级低于==或!=等比较运算符,注意使用括号。
(2)使用左移右移时,注意位数是否打开,否则会出现溢出的情形。
(3)有符号和无符号数的左移右移操作会动即符号位,出现错误。
(4)把N位二进制数向左移动N位以上,C++中没有定义这种操作,会出现错误。

利用位掩码实现集合

这种应用中,N位整数变量表示可以拥有0到N-1整数元素的集合,要判断某个元素i是否包含在集合里,只要查看表示2i是否已打开即可。

这里有个示例,披萨店有0到19这20种配料,当然可以使用大小20的布尔类型数组,也可以使用位掩码表示集合进行运算。

如果不加任何配料,即空集

int emptyPizza = 0;

如果添加所有配料,即紧致集

int fullPizza = (1 << 20) - 1;

如果添加第p种配料:

toppings |= (1 << p);

确认是否已包含元素

if(toppings & (1 << p)) cout << “pepperoni is in” << endl;

这里需要注意一下,&运算的结果值是0或1<<p。如果将结果返回值想成1就是错误的,例如错误示例:

if(toppings & (1 << p) == 1) cout << “pepperoni is in” << endl;

删除元素
当确定有这一位时可以这样删除:

toppings -= (1 << p);

当然这个删除方式,第p位不存在时就不可以使用了。
可以使用这样的删除方法:

toppings &= ~(1 << p);

切换元素
如果已经添加第p个配料就删去,如果没有添加第p个配料就加上,可以使用XOR运算

toppings ^= (1 << p);

对两个集合的运算

int added = (a | b);		\\a和b的和集
int intersection(a & b);	\\a和b的交集
int removed = (a & ~b);	\\从a减去b的差集
int toggled = (a ^ b);	\\只被a或b一个集合包含的元素集合

求集合大小
利用递归函数遍历所有位,从而算出打开位的个数:

int bitcount(int x)
{
	if(x\==0) return 0;
	return x % 2 + bitcount(x / 2);
}

也可以使用位运算优化一下:

int bitcount(int x)
{
	if(x\==0) return 0;
	return x & 1 + bitcount(x >> 1);
}

g++给出了一个内部命令,可以使用__builtin_popcount(toppings) 代替这个函数,而且这些命令经过优化,时间效率很高。

找出最小元素

这个问题也可以这样表述,求低位有几个0。这里g++也有一个内部命令__builtin_ctz(toppings),求得最小元素。

也可以直接把第i位的数拿出来,也就是树状数组的一个操作。

int firstToppings = (toppings & -toppings);

删除最小元素

toppings &= (toppings - 1);

这个方法可以判断这个数是否是2的多次方。

遍历所有子集

for(int subset = pizza; subset; subset = ((subset - 1) & pizza))
{
//subset是pizza的子集
}

位掩码的应用示例

指数时间动态规划法:简化制表方法。

埃拉托色尼的筛:使用位掩码优化的埃拉托色尼的筛可以把空间优化到原来的1/8。

实现:

int n;
unsigned char sieve[(MAX_N + 7) / 8];
//确定k是否为素数
inline bool isPrime(int k){
	return sieve[k >> 3] & (1 << (k & 7));
}
//标记为k不是素数
inline void setComposite(int k){
	sieve[k >> 3] &= ~(1 << (k & 7));
}
//实现使用位掩码的埃拉托色尼筛选法
//执行此函数后,利用isPrime()就能判断各数值是否为素数
void eratosthenes(){
	memset(sieve, 255, sizeof(sieve));
	setComposite(0);
	setComposite(1);
	int sqrtn = int(sqrt(n));
	for(int i=2;i<=sqrtn;++i)
	//如果该数值尚未删除
	if(isPrime(i))
		//对i的倍数j赋予isPrime[j]=false值
		//小于i*i的倍数已被删除,不必考虑
		for(int j=i*i;j<=n;j+=i)
		setComposite(j); 
}

“十五拼图”的状态表示法

位掩码不仅能表示布尔类型的数组,还可以把多个位并在一块,作为一个单元用作数组。

实现:

typedef unsigned long long uint64;
//返回mask中index位置的值
int get(uint64 mask, int index){
	return (mask >> (index << 2)) & 15;
}
//返回将mask中的index位置的值设置为value后的结果值
uint64 set(uint64 mask, int index, uint64 value){
	return mask & ~(15LL << (index << 2)) | (value << (index << 2));
}

O(1)优先级队列:优先级队列是从元素中快速找到最高优先级元素的数据结构。向优先级队列添加数据、删除数据,如果有N个元素,那么将耗费O(lgN)的时间。

当优先级队列限定在特殊范围内时,可以使用位掩码生成能够在O(1)时间内完成运算的优先级队列,这种概念能实际应用于Linux内核的进程管理。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三元湖有大锦鲤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值