【算法详解】位运算


位运算

1. 位运算简介

1.1 位运算与二进制简介

位运算(Bit Operation):在计算机内部,数是以「二进制(Binary)」的形式来进行存储。位运算就是直接对数的二进制进行计算操作,在程序中使用位运算进行操作,会大大提高程序的性能。

在学习二进制数的位运算之前,我们先来了解一下什么叫做「二进制数」。

二进制数(Binary):由 0 0 0 1 1 1 两个数码来表示的数。二进制数中每一个 0 0 0 或每一个 1 1 1 都称为一个「位(Bit)」。

我们通常使用的十进制数有 0 ∼ 9 0 \sim 9 09 10 10 10 个数字,进位规则是「满十进一」。例如:

  1. 7 ( 10 ) + 2 ( 10 ) = 9 ( 10 ) 7_{(10)} + 2_{(10)} = 9_{(10)} 7(10)+2(10)=9(10) 7 ( 10 ) 7_{(10)} 7(10) 加上 2 ( 10 ) 2_{(10)} 2(10) 等于 9 ( 10 ) 9_{(10)} 9(10)
  2. 9 ( 10 ) + 2 ( 10 ) = 1 1 ( 10 ) 9_{(10)} + 2_{(10)} = 11_{(10)} 9(10)+2(10)=11(10) 9 ( 10 ) 9_{(10)} 9(10) 加上 2 ( 10 ) 2_{(10)} 2(10) 之后个位大于等于 10 10 10,符合「满十进一」,结果等于 1 1 ( 10 ) 11_{(10)} 11(10)

而在二进制数中,我们只有 0 0 0 1 1 1 两个数码,它的进位规则是「逢二进一」。例如:

  1. 1 ( 2 ) + 0 ( 2 ) = 1 ( 2 ) 1_{(2)} + 0_{(2)} = 1_{(2)} 1(2)+0(2)=1(2) 1 ( 2 ) 1_{(2)} 1(2) 加上 0 ( 2 ) 0_{(2)} 0(2) 等于 1 ( 2 ) 1_{(2)} 1(2)
  2. 1 ( 2 ) + 1 ( 2 ) = 1 0 ( 2 ) 1_{(2)} + 1_{(2)} = 10_{(2)} 1(2)+1(2)=10(2) 1 ( 2 ) 1_{(2)} 1(2) 加上 1 ( 2 ) 1_{(2)} 1(2),大于等于 2 2 2,符合「逢二进一」,结果等于 1 0 ( 2 ) 10_{(2)} 10(2)
  3. 1 0 ( 2 ) + 1 ( 2 ) = 1 1 ( 2 ) 10_{(2)} + 1_{(2)} = 11_{(2)} 10(2)+1(2)=11(2)

1.2 二进制数的转换

1.2.1 二进制转十进制数

在十进制数中,数字 274 9 ( 10 ) 2749_{(10)} 2749(10) 可以理解为 2 × 1000 + 7 × 100 + 4 × 10 + 9 ∗ 1 2 \times 1000 + 7 \times 100 + 4 \times 10 + 9 * 1 2×1000+7×100+4×10+91,相当于 2 × 1 0 3 + 7 × 1 0 2 + 4 × 1 0 1 + 9 × 1 0 0 2 \times 10^3 + 7 \times 10^2 + 4 \times 10^1 + 9 \times 10^0 2×103+7×102+4×101+9×100,即 2000 + 700 + 40 + 9 = 274 9 ( 10 ) 2000 + 700 + 40 + 9 = 2749_{(10)} 2000+700+40+9=2749(10)

同理,在二进制数中, 0110101 0 ( 2 ) 01101010_{(2)} 01101010(2) 可以看作为 ( 0 × 2 7 ) + ( 1 × 2 6 ) + ( 1 × 2 5 ) + ( 0 × 2 4 ) + ( 1 × 2 3 ) + ( 0 × 2 2 ) + ( 1 × 2 1 ) + ( 0 × 2 0 ) (0 \times 2^7) + (1 \times 2^6) + (1 \times 2^5) + (0 \times 2^4) + (1 \times 2^3) + (0 \times 2^2) + (1 \times 2^1) + (0 \times 2^0) (0×27)+(1×26)+(1×25)+(0×24)+(1×23)+(0×22)+(1×21)+(0×20),即 0 + 64 + 32 + 0 + 8 + 0 + 2 + 0 = 10 6 ( 10 ) 0 + 64 + 32 + 0 + 8 + 0 + 2 + 0 = 106_{(10)} 0+64+32+0+8+0+2+0=106(10)

我们可以通过这样的方式,将一个二进制数转为十进制数。

1.2.2 十进制转二进制数

十进制数转二进制数的方法是:除二取余,逆序排列法

我们以十进制数中的 10 6 ( 10 ) 106_{(10)} 106(10) 为例。

106 ÷ 2 = 53 (余 0) 53 ÷ 2 = 26 (余 1) 26 ÷ 2 = 13 (余 0) 13 ÷ 2 = 6 (余 1) 6 ÷ 2 = 3 (余 0) 3 ÷ 2 = 1 (余 1) 1 ÷ 2 = 0 (余 1) 0 ÷ 2 = 0 (余 0) \begin{aligned} 106 \div 2 = 53 & \text{(余 0)} \cr 53 \div 2 = 26 & \text{(余 1)} \cr 26 \div 2 = 13 & \text{(余 0)} \cr 13 \div 2 = 6 & \text{(余 1)} \cr 6 \div 2 = 3 & \text{(余 0)} \cr 3 \div 2 = 1 & \text{(余 1)} \cr 1 \div 2 = 0 & \text{(余 1)} \cr 0 \div 2 = 0 & \text{(余 0)} \end{aligned} 106÷2=5353÷2=2626÷2=1313÷2=66÷2=33÷2=11÷2=00÷2=0(余 0(余 1(余 0(余 1(余 0(余 1(余 1(余 0

我们反向遍历每次计算的余数,依次是 0 0 0 1 1 1 1 1 1 0 0 0 1 1 1 0 0 0 1 1 1 0 0 0,即 0110101 0 ( 2 ) 01101010_{(2)} 01101010(2)

2. 位运算基础操作

在二进制的基础上,我们可以对二进制数进行相应的位运算。基本的位运算共有 6 6 6 种,分别是:「按位与运算」、「按位或运算」、「按位异或运算」、「取反运算」、「左移运算」、「右移运算」。

这里的「按位与运算」、「按位或运算」、「按位异或运算」、「左移运算」、「右移运算」是双目运算。

  • 「按位与运算」、「按位或运算」、「按位异或运算」是将两个整数作为二进制数,对二进制数表示中的每一位(即二进位)逐一进行相应运算,即双目运算。
  • 「左移运算」、「右移运算」是将左侧整数作为二进制数,将右侧整数作为移动位数,然后对左侧二进制数的全部位进行移位运算,每次移动一位,总共移动右侧整数次位,也是双目运算。

而「取反运算」是单目运算,是对一个整数的二进制数进行的位运算。

我们先来看下这 6 6 6 种位运算的规则,再来进行详细讲解。

运算符描述规则
&按位与运算符只有对应的两个二进位都为 1 1 1 时,结果位才为 1 1 1
``按位或运算符
^按位异或运算符对应的两个二进位相异时,结果位为 1 1 1,二进位相同时则结果位为 0 0 0
~取反运算符对二进制数的每个二进位取反,使数字 1 1 1 变为 0 0 0 0 0 0 变为 1 1 1
<<左移运算符将二进制数的各个二进位全部左移若干位。<< 右侧数字指定了移动位数,高位丢弃,低位补 0 0 0
>>右移运算符对二进制数的各个二进位全部右移若干位。>> 右侧数字指定了移动位数,低位丢弃,高位补 0 0 0

2.1 按位与运算

按位与运算(AND):按位与运算符为 &。其功能是对两个二进制数的每一个二进位进行与运算。

  • 按位与运算规则:只有对应的两个二进位都为 1 1 1 时,结果位才为 1 1 1

    • 1 & 1 = 1

    • 1 & 0 = 0

    • 0 & 1 = 0

    • 0 & 0 = 0

举个例子,对二进制数 0111110 0 ( 2 ) 01111100_{(2)} 01111100(2) 0011111 0 ( 2 ) 00111110_{(2)} 00111110(2) 进行按位与运算,结果为 0011110 0 ( 2 ) 00111100_{(2)} 00111100(2),如图所示:

2.2 按位或运算

按位或运算(OR):按位或运算符为 |。其功能对两个二进制数的每一个二进位进行或运算。

  • 按位或运算规则:只要对应的两个二进位有一个为 1 1 1 时,结果位就为 1 1 1
    • 1 | 1 = 1
    • 1 | 0 = 1
    • 0 | 1 = 1
    • 0 | 0 = 0

举个例子,对二进制数 0100101 0 ( 2 ) 01001010_{(2)} 01001010(2) 0101101 1 ( 2 ) 01011011_{(2)} 01011011(2) 进行按位或运算,结果为 0101101 1 ( 2 ) 01011011_{(2)} 01011011(2),如图所示:

2.3 按位异或运算

按位异或运算(XOR):按位异或运算符为 ^。其功能是对两个二进制数的每一个二进位进行异或运算。

  • 按位异或运算规则:对应的两个二进位相异时,结果位为 1 1 1,二进位相同时则结果位为 0 0 0

  • 0 ^ 0 = 0

  • 1 ^ 0 = 1

  • 0 ^ 1 = 1

  • 1 ^ 1 = 0

举个例子,对二进制数 0100101 0 ( 2 ) 01001010_{(2)} 01001010(2) 0100010 1 ( 2 ) 01000101_{(2)} 01000101(2) 进行按位异或运算,结果为 0000111 1 ( 2 ) 00001111_{(2)} 00001111(2),如图所示:

2.4 取反运算

取反运算(NOT):取反运算符为 ~。其功能是对一个二进制数的每一个二进位进行取反运算。

  • 取反运算规则:使数字 1 1 1 变为 0 0 0 0 0 0 变为 1 1 1
    • ~0 = 1
    • ~1 = 0

举个例子,对二进制数 0110101 0 ( 2 ) 01101010_{(2)} 01101010(2) 进行取反运算,结果如图所示:

2.5 左移运算和右移运算

左移运算(SHL): 左移运算符为 <<。其功能是对一个二进制数的各个二进位全部左移若干位(高位丢弃,低位补 0 0 0)。

举个例子,对二进制数 0110101 0 ( 2 ) 01101010_{(2)} 01101010(2) 进行左移 1 1 1 位运算,结果为 1101010 0 ( 2 ) 11010100_{(2)} 11010100(2),如图所示:

右移运算(SHR): 右移运算符为 >>。其功能是对一个二进制数的各个二进位全部右移若干位(低位丢弃,高位补 0 0 0)。

举个例子,对二进制数 0110101 0 ( 2 ) 01101010_{(2)} 01101010(2) 进行右移 1 1 1 位运算,结果为 0011010 1 ( 2 ) 00110101_{(2)} 00110101(2),如图所示:

3. 位运算的应用

3.1 位运算的常用操作

3.1.1 判断整数奇偶

一个整数,只要是偶数,其对应二进制数的末尾一定为 0 0 0;只要是奇数,其对应二进制数的末尾一定为 1 1 1。所以,我们通过与 1 1 1 进行按位与运算,即可判断某个数是奇数还是偶数。

  1. (x & 1) == 0 为偶数。
  2. (x & 1) == 1 为奇数。
3.1.2 二进制数选取指定位

如果我们想要从一个二进制数 X X X 中取出某几位,使取出位置上的二进位保留原值,其余位置为 0 0 0,则可以使用另一个二进制数 Y Y Y,使该二进制数上对应取出位置为 1 1 1,其余位置为 0 0 0。然后令两个数进行按位与运算(X & Y),即可得到想要的数。

举个例子,比如我们要取二进制数 X = 0110101 0 ( 2 ) X = 01101010_{(2)} X=01101010(2) 的末尾 4 4 4 位,则只需将 X = 0110101 0 ( 2 ) X = 01101010_{(2)} X=01101010(2) Y = 0000111 1 ( 2 ) Y = 00001111_{(2)} Y=00001111(2) (末尾 4 4 4 位为 1 1 1,其余位为 0 0 0) 进行按位与运算,即 01101010 & 00001111 == 00001010。其结果 00001010 00001010 00001010 就是我们想要的数(即二进制数 0110101 0 ( 2 ) 01101010_{(2)} 01101010(2) 的末尾 4 4 4 位)。

3.1.3 将指定位设置为 1 1 1

如果我们想要把一个二进制数 X X X 中的某几位设置为 1 1 1,其余位置保留原值,则可以使用另一个二进制数 Y Y Y,使得该二进制上对应选取位置为 1 1 1,其余位置为 0 0 0。然后令两个数进行按位或运算(X | Y),即可得到想要的数。

举个例子,比如我们想要将二进制数 X = 0110101 0 ( 2 ) X = 01101010_{(2)} X=01101010(2) 的末尾 4 4 4 位设置为 1 1 1,其余位置保留原值,则只需将 X = 0110101 0 ( 2 ) X = 01101010_{(2)} X=01101010(2) Y = 0000111 1 ( 2 ) Y = 00001111_{(2)} Y=00001111(2)(末尾 4 4 4 位为 1 1 1,其余位为 0 0 0)进行按位或运算,即 01101010 | 00001111 = 01101111。其结果 01101111 01101111 01101111 就是我们想要的数(即将二进制数 0110101 0 ( 2 ) 01101010_{(2)} 01101010(2) 的末尾 4 4 4 位设置为 1 1 1,其余位置保留原值)。

3.1.4 反转指定位

如果我们想要把一个二进制数 X X X 的某几位进行反转,则可以使用另一个二进制数 Y Y Y,使得该二进制上对应选取位置为 1 1 1,其余位置为 0 0 0。然后令两个数进行按位异或运算(X ^ Y),即可得到想要的数。

举个例子,比如想要将二进制数 X = 0110101 0 ( 2 ) X = 01101010_{(2)} X=01101010(2) 的末尾 4 4 4 位进行反转,则只需将 X = 0110101 0 ( 2 ) X = 01101010_{(2)} X=01101010(2) Y = 0000111 1 ( 2 ) Y = 00001111_{(2)} Y=00001111(2)(末尾 4 4 4 位为 1 1 1,其余位为 0 0 0)进行按位异或运算,即 01101010 ^ 00001111 = 01100101。其结果 01100101 01100101 01100101 就是我们想要的数(即将二进制数 X = 0110101 0 ( 2 ) X = 01101010_{(2)} X=01101010(2) 的末尾 4 4 4 位进行反转)。

3.1.5 交换两个数

通过按位异或运算可以实现交换两个数的目的(只能用于交换两个整数)。

a, b = 10, 20
a ^= b
b ^= a
a ^= b
print(a, b)
3.1.6 将二进制最右侧为 1 1 1 的二进位改为 0 0 0

如果我们想要将一个二进制数 X X X 最右侧为 1 1 1 的二进制位改为 0 0 0,则只需通过 X & (X - 1) 的操作即可完成。

比如 X = 0110110 0 ( 2 ) X = 01101100_{(2)} X=01101100(2) X − 1 = 0110101 1 ( 2 ) X - 1 = 01101011_{(2)} X1=01101011(2),则 X & (X - 1) == 01101100 & 01101011 == 01101000,结果为 0110100 0 ( 2 ) 01101000_{(2)} 01101000(2)(即将 X X X 最右侧为 1 1 1 的二进制为改为 0 0 0)。

3.1.7 计算二进制中二进位为 1 1 1 的个数

从 3.1.6 中得知,通过 X & (X - 1) 我们可以将二进制 X X X 最右侧为 1 1 1 的二进制位改为 0 0 0,那么如果我们不断通过 X & (X - 1) 操作,最终将二进制 X X X 变为 0 0 0,并统计执行次数,则可以得到二进制中二进位为 1 1 1 的个数。

具体代码如下:

int main()
	int nums;
	cin>>nums;
	int cnt=0;
	while(nums){
		nums=nums&(nums-1);
		cnt++;
	}
	cout<<cnt<<endl;
	return 0;
}
3.1.8 将一个树n的二进制表示的第 x x x位修改成 1 1 1或者 0 0 0

如果你想将一个数 n n n 的二进制表示的第 x x x位修改为 1 1 1 x x x,你可以使用以下方法:

将第 x x x位设置为 1 1 1:

n |= (1 << x)

举个例子,比如想要将二进制数 X = 1110100 1 ( 2 ) X = 11101001_{(2)} X=11101001(2) 的末尾 3 3 3 位修改成 1 1 1,则将 X = 1110100 1 ( 2 ) X = 11101001_{(2)} X=11101001(2) 0000010 0 ( 2 ) 00000100_{(2)} 00000100(2)进行按位或运算,即 11101001 | 00000100 = 11101101。其结果 1110110 1110110 1110110 就是我们想要的数(即将二进制数 X = 1 ( 2 ) X =1_{(2)} X=1(2) ,将数往右移动 x x x位得到的)。

将第 x x x 位设置为 x x x:

n &= ~(1 << x)

举个例子,比如想要将二进制数 X = 1110100 1 ( 2 ) X = 11101001_{(2)} X=11101001(2) 的末尾 3 3 3 位修改成 0 0 0,则将 X = X 1110110 1 ( 2 ) X = X11101101_{(2)} X=X11101101(2) 与 ~ ( 00000100 ) ( 2 ) (00000100)_{(2)} (00000100)(2)进行按位与运算,即 11101101 &111110111= 11101001。其结果 1110110 1110110 1110110 就是我们想要的数(即将二进制数 X = 1 ( 2 ) X =1_{(2)} X=1(2) ,将数往右移动 x x x位再取反得到的)。

那么确定一个数的二进制中第 x x x位是 1 1 1还是 0 0 0怎么操作呢
同理可得,不难推出

n = (n >> x) & 1;
3.1.9 判断某数是否为 2 2 2 的幂次方

通过判断 X & (X - 1) == 0 是否成立,即可判断 X X X 是否为 2 2 2 的幂次方。

这是因为:

  1. 凡是 2 2 2 的幂次方,其二进制数的某一高位为 1 1 1,并且仅此高位为 1 1 1,其余位都为 0 0 0。比如: 4 ( 10 ) = 0000010 0 ( 2 ) 4_{(10)} = 00000100_{(2)} 4(10)=00000100(2) 8 ( 10 ) = 0000100 0 ( 2 ) 8_{(10)} = 00001000_{(2)} 8(10)=00001000(2)
  2. 不是 2 2 2 的幂次方,其二进制数存在多个值为 1 1 1 的位。比如: 5 10 = 0000010 1 ( 2 ) 5_{10} = 00000101_{(2)} 510=00000101(2) 6 10 = 0000011 0 ( 2 ) 6_{10} = 00000110_{(2)} 610=00000110(2)

接下来我们使用 X & (X - 1) 操作,将原数对应二进制数最右侧为 1 1 1 的二进位改为 0 0 0 之后,得到新值:

  1. 如果原数是 2 2 2 的幂次方,则通过 X & (X - 1) 操作之后,新值所有位都为 0 0 0,值为 0 0 0
  2. 如果该数不是 2 2 2 的幂次方,则通过 X & (X - 1) 操作之后,新值仍存在不为 0 0 0 的位,值肯定不为 0 0 0

所以我们可以通过是否为 0 0 0 即可判断该数是否为 2 2 2 的幂次方。

3.1.10 去掉右边起第一个 1 的左边

这个表达式会产生一个二进制数,其中从右边起第一个 1 1 1 及其左边的所有位都是 1 1 1,而右边的所有位都是 0 0 0。这是因为从 x x x减去 1 1 1会导致从右边起第一个 1 1 1 变成 0 0 0,并且如果这个 1 1 1的左边有 0 0 0,那些 0 0 0会变成 1 1 1

x & (x ^ (x - 1))  

这个表达式直接提取出 x x x 的最低位的 1 1 1。在二进制补码表示中,- x x x 等于 x x x 的所有位取反加 1 1 1,因此 x x x 和- x x x 只有最低位的 1 1 1 是共同的,其他位都不同。

x & (-x)

举个例子,对于 x = 10010100 0 ( 2 ) x = 100101000_{(2)} x=100101000(2) x − 1 x - 1 x1会是 10010011 1 ( 2 ) 100100111_{(2)} 100100111(2),而 x x x x − 1 x - 1 x1 的异或按位与会得到 00000011 1 ( 2 ) 000000111_{(2)} 000000111(2),然后再与即 x x x按位与,即 100101000 | 000001000 = 000001000。如果 x x x与 - x = 01101100 0 ( 2 ) x=011011000_{(2)} x=011011000(2) 按位与操作的话,即 100101000 |011011000 = 000001000。因此,无论是使用 x & (x ^ (x - 1)) 还是 x & (-x),结果都会是 ( 000001000_{(2)} )。

3.1.11 从右边开始,把最后一个 1 改写成 0

这个表达式通过与操作清除了最低位的 1 1 1,因为 x − 1 x - 1 x1 在最低位的 1 1 1 处产生了 0 0 0,而与操作会将任何与 0 0 0 相与的位都设置为 0 0 0

 x & (x - 1)

举个例子,对于 x = 10010100 0 ( 2 ) x = 100101000_{(2)} x=100101000(2) x − 1 x - 1 x1会是 10010011 1 ( 2 ) 100100111_{(2)} 100100111(2),而 x x x x − 1 x - 1 x1的按位与会是 10010000 0 ( 2 ) 100100000_{(2)} 100100000(2),即 100101000 | 100100111= 100100000

3.2 位运算的常用操作总结

功 能位运算示例
去掉最后一位x >> 1101101 -> 10110
在最后加一个 0x << 1101101 -> 1011010
在最后加一个 1(x << 1) + 1101101 -> 1011011
把最后一位变成 1x | 1101100 -> 101101
把最后一位变成 0x | 1 - 1101101 -> 101100
最后一位取反x ^ 1101101 -> 101100
把右数第 k 位变成 1x | (1 << (k - 1))101001 -> 101101, k = 3
把右数第 k 位变成 0x & ~(1 << (k - 1))101101 -> 101001, k = 3
右数第 k 位取反x ^ (1 << (k - 1))101001 -> 101101, k = 3
取末尾 3x & 71101101 -> 101
取末尾 kx & 151101101 -> 1101, k = 4
取右数第 kx >> (k - 1) & 11101101 -> 1, k = 4
把末尾 k 位变成 1x | (1 << k - 1)101001 -> 101111, k = 4
末尾 k 位取反x ^ (1 << k - 1)101001 -> 100110, k = 4
把右边连续的 1 变成 0x & (x + 1)100101111 -> 100100000
把右边起第一个 0 变成 1x | (x + 1)100101111 -> 100111111
把右边连续的 0 变成 1x | (x - 1)11011000 -> 11011111
只保留右边连续的 1(x ^ (x + 1)) >> 1100101111 -> 1111
去掉右边起第一个 1 的左边x & (x ^ (x - 1))x & (-x)100101000 -> 1000
从右边开始,把最后一个 1 改写成 0x & (x - 1)100101000 -> 100100000

3.3 二进制枚举子集

除了上面的这些常见操作,我们经常常使用二进制数第 1 ∼ n 1 \sim n 1n 位上 0 0 0 1 1 1 的状态来表示一个由 1 ∼ n 1 \sim n 1n 组成的集合。也就是说通过二进制来枚举子集。

3.3.1 二进制枚举子集简介

先来介绍一下「子集」的概念。

  • 子集:如果集合 A A A 的任意一个元素都是集合 S S S 的元素,则称集合 A A A 是集合 S S S 的子集。可以记为 A ∈ S A \in S AS

有时候我们会遇到这样的问题:给定一个集合 S S S,枚举其所有可能的子集。

枚举子集的方法有很多,这里介绍一种简单有效的枚举方法:「二进制枚举子集算法」。

对于一个元素个数为 n n n 的集合 S S S 来说,每一个位置上的元素都有选取和未选取两种状态。我们可以用数字 1 1 1 来表示选取该元素,用数字 0 0 0 来表示不选取该元素。

那么我们就可以用一个长度为 n n n 的二进制数来表示集合 S S S 或者表示 S S S 的子集。其中二进制的每一个二进位都对应了集合中某一个元素的选取状态。对于集合中第 i i i 个元素来说,二进制对应位置上的 1 1 1 代表该元素被选取, 0 0 0 代表该元素未被选取。

举个例子,比如长度为 5 5 5 的集合 S = { 5 , 4 , 3 , 2 , 1 } S = \left \{ 5, 4, 3, 2, 1 \right \} S={5,4,3,2,1},我们可以用一个长度为 5 5 5 的二进制数来表示该集合。

比如二进制数 1111 1 ( 2 ) 11111_{(2)} 11111(2) 就表示选取集合的第 1 1 1 位、第 2 2 2 位、第 3 3 3 位、第 4 4 4 位、第 5 5 5 位元素,也就是集合 { 5 , 4 , 3 , 2 , 1 } \left \{ 5, 4, 3, 2, 1 \right \} {5,4,3,2,1},即集合 S S S 本身。如下表所示:

集合 S 中元素位置54321
二进位对应值11111
对应选取状态选取选取选取选取选取

再比如二进制数 1010 1 ( 2 ) 10101_{(2)} 10101(2) 就表示选取集合的第 1 1 1 位、第 3 3 3 位、第 5 5 5 位元素,也就是集合 { 5 , 3 , 1 } \left \{5, 3, 1 \right \} {5,3,1}。如下表所示:

集合 S 中元素位置54321
二进位对应值10101
对应选取状态选取未选取选取未选取选取

再比如二进制数 0100 1 ( 2 ) 01001_{(2)} 01001(2) 就表示选取集合的第 1 1 1 位、第 4 4 4 位元素,也就是集合 { 4 , 1 } \left \{4, 1 \right \} {4,1}。如下标所示:

集合 S 中元素位置54321
二进位对应值01001
对应选取状态未选取选取未选取未选取选取

通过上面的例子我们可以得到启发:对于长度为 5 5 5 的集合 S S S 来说,我们只需要从 00000 ∼ 11111 00000 \sim 11111 0000011111 枚举一次(对应十进制为 0 ∼ 2 5 − 1 0 \sim 2^5 - 1 0251)即可得到长度为 5 5 5 的集合 S S S 的所有子集。

我们将上面的例子拓展到长度为 n n n 的集合 S S S。可以总结为:

  • 对于长度为 n n n 的集合 S S S 来说,只需要枚举 0 ∼ 2 n − 1 0 \sim 2^n - 1 02n1(共 2 n 2^n 2n 种情况),即可得到集合 S S S 的所有子集。
3.3.2 二进制枚举子集代码
  vector<vector<int>> subsets(vector<int>& S) { 
        int n = S.size();                         // n 为集合 S 的元素个数
        vector<vector<int>> sub_sets;             // sub_sets 用于保存所有子集
        for (int i = 0; i < (1 << n); ++i) {      // 枚举 0 ~ 2^n - 1
            vector<int> sub_set;                  // sub_set 用于保存当前子集
            for (int j = 0; j < n; ++j) {         // 枚举第 i 位元素
                if (i >> j & 1) {                  // 如果第 i 为元素对应二进位删改为 1,则表示选取该元素
                    sub_set.push_back(S[j]);       // 将选取的元素加入到子集 sub_set 中
                }
            }
            sub_sets.push_back(sub_set);           // 将子集 sub_set 加入到所有子集数组 sub_sets 中
        }
        return sub_sets;                          // 返回所有子集
    }

分享一张记忆图片

请添加图片描述

  • 27
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

SuhyOvO

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

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

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

打赏作者

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

抵扣说明:

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

余额充值