CP-UPV Bootcamp - Capítulo 7

Capítulo 7: Manipulación de Bits (Bitwise Operations)

PROBLEMAS DEL CAPÍTULO
Para acceder a los problemas de este capítulo, accede con tu usuario y contraseña a cpupv.contest.codeforces.com. Si no tienes usuario y contraseña, inscríbete aquí.

Índice de contenidos

1. Representación Binaria y Operadores a Nivel de Bit

En el nivel más elemental de un procesador, toda la información se almacena en forma de ceros y unos (bits). La manipulación directa de bits (o bitwise operations) nos permite ejecutar operaciones lógicas y aritméticas ultraeficientes en tiempo constante $O(1)$ sin consumo adicional de memoria.

Un entero sin signo de $32$ bits puede representar números desde $0$ hasta $2^{32}-1$. El bit situado más a la derecha se denomina Bit Menos Significativo (LSB - Least Significant Bit) y corresponde a $2^0$, mientras que el de la izquierda es el Bit Más Significativo (MSB - Most Significant Bit).

Los 6 Operadores de Bit Fundamentales

int a = 5;  // 0101 en binario
int b = 3;  // 0011 en binario

int res_and = a & b;   // 0001 -> 1
int res_or  = a | b;   // 0111 -> 7
int res_xor = a ^ b;   // 0110 -> 6
int res_not = ~a;      // Invasión de bits (complemento a dos)
int shift_l = a << 2;  // 010100 -> 20 (5 * 2^2)
int shift_r = a >> 1;  // 0010 -> 2   (5 / 2^1)
a = 5  # 0101 en binario
b = 3  # 0011 en binario

res_and = a & b   # 0001 -> 1
res_or  = a | b   # 0111 -> 7
res_xor = a ^ b   # 0110 -> 6
res_not = ~a      # Complemento en Python
shift_l = a << 2  # 20 (5 * 2^2)
shift_r = a >> 1  # 2  (5 // 2^1)
int a = 5;  // 0101 en binario
int b = 3;  // 0011 en binario

int resAnd = a & b;   // 0001 -> 1
int resOr  = a | b;   // 0111 -> 7
int resXor = a ^ b;   // 0110 -> 6
int resNot = ~a;      // Complemento a dos
int shiftL = a << 2;  // 20 (5 * 2^2)
int shiftR = a >> 1;  // 2  (5 / 2^1)

2. Operaciones Fundamentales sobre Bits (Bitmasking)

Trataremos un entero como si fuera una lista de interruptores o booleanos. Para consultar, encender, apagar o alternar el $k$-ésimo bit (indexado desde $0$ a la derecha), utilizamos la máscara de bit de un solo bit definida como (1 << k).

Operación Expresión de Bitwise Explicación
Comprobar si el bit $k$ está encendido (mask & (1 << k)) != 0 Aplica un AND con solo el bit $k$ activo. Si da distinto de $0$, el bit estaba en $1$.
Encender el bit $k$ (Set Bit) mask = mask | (1 << k) Fuerza el bit $k$ a $1$ sin alterar los demás bits.
Apagar el bit $k$ (Clear Bit) mask = mask & ~(1 << k) Invierte la máscara para tener ceros únicamente en la posición $k$ y apaga dicho bit.
Alternar / Invertir el bit $k$ (Toggle Bit) mask = mask ^ (1 << k) Si el bit $k$ era $1$ lo pasa a $0$, y viceversa.
Cuidado con el desbordamiento de enteros (Integer Overflow) al desplazar

En C++ y Java, escribir 1 << k utiliza por defecto enteros signed de $32$ bits. Si $k \ge 31$, provocaremos un comportamiento indefinido o un overflow a valores negativos. Para máscaras de hasta $64$ bits debemos usar la constante de $64$ bits: 1LL << k en C++ o 1L << k en Java.

3. Trucos de Bits Esenciales en CP

Existen varias identidades algebraicas de bits de uso habitual en programación competitiva:

4. Representación de Subconjuntos mediante Máscaras de Bits

Uno de los usos más habituales del bitmasking en CP es representar un subconjunto de un conjunto finito de $N$ elementos utilizando un único entero de $N$ bits.

Si tenemos $N$ elementos, existen $2^N$ subconjuntos posibles. Podemos iterar fácilmente por todos los subconjuntos haciendo un bucle de $0$ a $2^N - 1$ (es decir, de 0 a (1 << N) - 1):

int n = 3; // Elementos: {0, 1, 2}

// Iterar por los 2^N subconjuntos posibles:
for (int mask = 0; mask < (1 << n); ++mask) {
    cout << "Subconjunto " << mask << ": { ";
    for (int i = 0; i < n; ++i) {
        if (mask & (1 << i)) { // Si el i-ésimo elemento está presente
            cout << i << " ";
        }
    }
    std::cout << "}\n";
}
n = 3 # Elementos: {0, 1, 2}

# Iterar por los 2^N subconjuntos posibles:
for mask in range(1 << n):
    elementos = [i for i in range(n) if (mask & (1 << i))]
    print(f"Subconjunto {mask}: {elementos}")
int n = 3; // Elementos: {0, 1, 2}

for (int mask = 0; mask < (1 << n); mask++) {
    System.out.print("Subconjunto " + mask + ": { ");
    for (int i = 0; i < n; i++) {
        if ((mask & (1 << i)) != 0) {
            System.out.print(i + " ");
        }
    }
    System.out.println("}");
}

5. Funciones Intrínsecas del Compilador (Builtin Functions)

Los procesadores modernos disponen de instrucciones de ensamblador dedicadas a operaciones sobre bits. C++, Java y Python exponen funciones optimizadas para usarlas directamente:

Operación C++ (GCC/Clang) Java Python
Contar bits en 1 (Popcount) __builtin_popcount(x)
__builtin_popcountll(x)
Integer.bitCount(x) x.bit_count()
Ceros a la izquierda (Leading Zeros) __builtin_clz(x) Integer.numberOfLeadingZeros(x) (32 - x.bit_length())
Ceros a la derecha (Trailing Zeros) __builtin_ctz(x) Integer.numberOfTrailingZeros(x) (x & -x).bit_length() - 1

Ejemplo Práctico: El entrenamiento en Algomania

Danimania ha publicado $N$ problemas en la plataforma Algomania ($1 \le N \le 20$). Cada problema $i$ tiene una dificultad estimada de $d_i$ puntos.

Un estudiante del bootcamp dispone de una capacidad total de esfuerzo de $K$ puntos para su sesión de entrenamiento. Queremos seleccionar el subconjunto de problemas que maximice la suma total de dificultades sin superar el límite $K$.

Pista y Estrategia: Búsqueda Completa con Bitmask (Haz clic para desplegar)

Dado que $N \le 20$, el número total de subconjuntos posibles es $2^{20} = 1.048.576$, una cifra perfectamente asumible para evaluar todas las combinaciones mediante máscaras de bits.

Iteramos por cada máscara mask desde $0$ hasta $(2^N - 1)$:

  1. Para cada máscara, sumamos las dificultades cuyo bit $i$ está activo: if (mask & (1 << i)) suma_actual += d[i].
  2. Si suma_actual <= K, actualizamos la respuesta máxima con max(mejor_suma, suma_actual).
Complejidad total: $\mathcal{O}(2^N \cdot N)$, ejecutándose en cuestión de milisegundos.

ENLACE AL PROBLEMA
Ponte a prueba antes de desplegar el código: Resolver El entrenamiento en Algomania en Codeforces
Ver Solución (C++, Python, Java)
#include <bits/stdc++.h>

using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n;
    long long k;
    if (!(cin >> n >> k)) return 0;

    vector<long long> d(n);
    for (int i = 0; i < n; i++) {
        cin >> d[i];
    }

    long long mejor_suma = 0;

    // Recorremos las 2^N máscaras de bits posibles
    for (int mask = 0; mask < (1 << n); mask++) {
        long long suma_actual = 0;
        
        for (int i = 0; i < n; i++) {
            if (mask & (1 << i)) {
                suma_actual += d[i];
            }
        }

        if (suma_actual <= k) {
            mejor_suma = max(mejor_suma, suma_actual);
        }
    }

    cout << mejor_suma << "\n";

    return 0;
}
import sys
from itertools import combinations

def resolver():
    entrada = sys.stdin.read().split()
    if not entrada:
        return

    n = int(entrada[0])
    k = int(entrada[1])
    dificultades = [int(x) for x in entrada[2:n + 2]]

    mejor_suma = 0

    # Usamos itertools para iterar por todos los subconjuntos eficientemente en CPython
    for r in range(n + 1):
        for combo in combinations(dificultades, r):
            suma_actual = sum(combo)
            if mejor_suma < suma_actual <= k:
                mejor_suma = suma_actual
                if mejor_suma == k:
                    print(k)
                    return

    print(mejor_suma)

if __name__ == "__main__":
    resolver()
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.IOException;
import java.util.StringTokenizer;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader lector = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer tokens;

        String linea = lector.readLine();
        if (linea == null) return;
        tokens = new StringTokenizer(linea);

        int n = Integer.parseInt(tokens.nextToken());
        long k = Long.parseLong(tokens.nextToken());

        long[] dificultades = new long[n];
        tokens = new StringTokenizer(lector.readLine());
        for (int i = 0; i < n; i++) {
            dificultades[i] = Long.parseLong(tokens.nextToken());
        }

        long mejorSuma = 0;

        for (int mask = 0; mask < (1 << n); mask++) {
            long sumaActual = 0;
            for (int i = 0; i < n; i++) {
                if ((mask & (1 << i)) != 0) {
                    sumaActual += dificultades[i];
                }
            }

            if (sumaActual <= k) {
                mejorSuma = Math.max(mejorSuma, sumaActual);
            }
        }

        PrintWriter escritor = new PrintWriter(System.out);
        escritor.println(mejorSuma);
        escritor.flush();
    }
}

Problemas de práctica

Ponte a prueba completando los problemas asignados en la sección activa de nuestro bootcamp en Codeforces.