¡Esta es una revisión vieja del documento!
Todos los números en la computadora son almacenados internamente como un número en binario, usualmente de 32 o 64 bits. Por ejemplo, en C++ el tipo int
guarda números de 32 bits, y si tipeamos: int x = 43
, la computadora representará internamente a x
por:
x=00000000000000000000000000101011⏟32
Donde la representación debe entenderse con los bits menos significativos a la derecha. Es decir:
43=20⋅1+21⋅1+22⋅0+23⋅1+24⋅0+25⋅1+26⋅0+⋯+230⋅0+231⋅0
Sin embargo, si tipeamos long long x = 43
, la computadora representará internamente a x
por:
x=0000000000000000000000000000000000000000000000000000000000101011⏟64
Ya que
43=20⋅1+21⋅1+22⋅0+23⋅1+24⋅0+25⋅1+26⋅0+⋯+262⋅0+263⋅0
Para más información sobre distintos tipos de enteros, pueden ingresar a Tipos de Datos en la wiki.
Dados dos números x e y, x AND y nos da otro número que tiene un 1 en el desarrollo binario exactamente en las posiciones donde tanto x como y tienen un 1 en sus respectivos desarrollos binarios.
Ejemplo:
10110(22)&11010(26)=10010(18)
En C++, si tenemos dos enteros x
e y
, podemos escribir x AND y como x & y
.
Dados dos números x e y, x OR y nos da otro número que tiene un 1 en el desarrollo binario exactamente en las posiciones donde alguno de x e y tiene un 1 en sus respectivos desarrollos binarios (posiblemente ambos).
Ejemplo:
10110(22)|11010(26)=11110(30)
En C++, si tenemos dos enteros x
e y
, podemos escribir x OR y como x | y
.
Dados dos números x e y, x XOR y nos da otro número que tiene un 1 en el desarrollo binario exactamente en las posiciones donde solamente uno de x e y tiene un 1 en su desarrollo binario.
Ejemplo:
10110(22)∧11010(26)=01100(12)
En C++, si tenemos dos enteros x
e y
, podemos escribir x XOR y como x ^ y
.
Dado un número x, NOT x nos da otro número que invierte cada bit del desarrollo binario de x. Es decir, nos devuelve un número con un 1 donde el desarrollo de x tiene un 0 y viceversa (un 0 donde hay un 1).
Cuidado, si usamos un tipo de enteros con signo, también da vuelta el bit de signo.
Ejemplo (trabajando con enteros de 32 bits con signo, o sea int
en C++):
∼00000000000000000000000000011101(29)=11111111111111111111111111100010(−30)
En C++, si tenemos un entero x
y queremos negarlo, es decir NOT x, podemos escribirlo como ~x
.
Hay dos tipos de desplazamientos que podemos hacer. Un shift a izquierda de k posiciones a un número x, agrega k ceros en la representación binaria al final del número. En C++ se nota con x << k
.
De manera análoga, un shift a derecha de k posiciones a un número x, remueve los últimos k bits del desarrollo binario del número. En C++ se nota con x >> k
.
Ejemplo:
14=0000000000001110(14)14<<2=0000000000111000(56)14>>2=0000000000000011(3)
Una aplicación muy común de los shift de bits es obtener el k-ésimo bit de un número. Usando que 1 << k
nos da el número 2k, cuya representación tiene solamente el k-ésimo bit igual a 1, podemos acceder al k-ésimo bit de un número x en C++ haciendo x & (1 << k)
.
Si estamos trabajando con 64 bits hay que tener cuidado y escribir 1LL << k
si queremos tener al número 2k con k≥31 (el motivo de esto se explica aquí).
Dado un número x, combinando operaciones de bits podemos generar otros números que guardan una relación con x en su desarrollo binario que pueden ser de gran utilidad.
x | (1 << k)
x & ~(1 << k)
x ^ (1 << k)
x & (x-1)
x & (-x)
x | (x-1)
[definir LSB (least significant bit) y reescribir lo anterior más prolijo en términos del LSB]
Notar que si quisiéramos ver si x es una potencia de 2, podemos hacerlo viendo si (x & (x-1)) == 0
(Si asumimos x>0)
En C++ tenemos las siguientes funciones que pueden resultar muy útiles al trabajar con enteros de 32 bits.
Si estamos trabajando con enteros de 64 bits y queremos usar estas funciones, podemos hacerlo agregando ll
(por long long
) al final de la función. Por ejemplo __builtin_clzll(29) nos devuelve 59 como respuesta, porque (29)2=11101, así que tiene 59 bits en 0 antes de esos 5 bits finales. En cambio, __builtin_clz(29) devuelve 27, ya que solamente hay 32 bits en total.
Si tenemos un conjunto de n elementos dispuesto en un arreglo A, el conjunto {0,1,2,…,n−1} representa los índices de los elementos de nuestro arreglo. De esta forma, cada subconjunto de {0,1,2,…,n−1}, representa un subconjunto de nuestro arreglo original. Si n≤31, usando un entero de 32 bits, podemos representar a cada subconjunto de A⊆{0,1,2,…,n−1} con un entero: asociaremos cada subconjunto A con el entero no negativo cuyo desarrollo binario tenga un 1 exactamente en las posiciones correspondientes a los elementos de A.
Ejemplo: Si A={1,3,4,8}, entonces, A se corresponde con :
a=00000000000000000000⋯0090817060504131201100=282
Entonces, utilizando las operaciones de bits que vimos más arriba podemos describir las operaciones entre conjuntos de manera práctica y compacta. La siguiente tabla muestra un resumen de estas operaciones. a es la máscara de bits1) que representa al conjunto A, y b es la máscara que representa al conjunto B. conjuntosbitsintersecciónA∩Ba&buniónA∪Ba|bcomplementoAc∼adiferenciaA∖Ba&(∼b)
Notar que si quisiéramos generar el subconjunto A={1,3,4,8} en C++ y representarlo en un entero, podemos hacerlo con el siguiente código utilizando las operaciones que vimos anteriormente.
int x = 0; x |= (1 << 1); // pongo en 1 el bit correspondiente a la posición 1 x |= (1 << 3); // pongo en 1 el bit correspondiente a la posición 3 x |= (1 << 4); // pongo en 1 el bit correspondiente a la posición 4 x |= (1 << 8); // pongo en 1 el bit correspondiente a la posición 8
Si quisiéramos iterar en los elementos de una máscara, podríamos hacerlo de la siguiente forma.
for (int i = 0; i < 32; i++) { if (x & (1 << i) ) // Si el i-ésimo bit tiene un 1... { // Hago lo que quiero con i (o A[i] si queremos usar los valores) ... } }
Una forma de iterar todos los subconjuntos de n elementos es
for (int b = 0; b < (1 << n); b++) { // Hago lo que quiero con el subconjunto b }
Si tenemos una máscara x que ya representa a un subconjunto, podemos iterar en todos los subconjuntos de los elementos de esa máscara x de la siguiente forma
int b = 0; do { // Hago lo que quiero con el subconjunto b, incluido en el subconjunto que representa x } while (b = (b-x)&x);
Todas las ideas explayadas en este artículo, se encuentran en el capítulo 10 del siguiente libro Competitive Programmer's Handbook de Antii Laaksonen (está en inglés).