Fundamentospensamiento-lógicofundamentosresolución-de-problemas

Pensamiento Lateral y Lógico

Desarrolla habilidades de pensamiento lateral y lógico para resolver problemas de programación competitiva

OOI Oaxaca9 de febrero de 202611 min read

¿Qué tienen en común los detectives y los programadores?

Tanto un detective como un programador resuelven problemas usando la misma herramienta: el pensamiento lógico. Un detective observa las pistas, descarta lo que no sirve y llega a una conclusión. Un programador analiza un problema, descarta las soluciones ineficientes y encuentra la mejor forma de resolverlo.

En la programación competitiva, no basta con saber escribir código. Necesitas saber pensar. Y eso es exactamente lo que aprenderás en esta sección: a pensar de formas que te permitan ver soluciones donde otros solo ven problemas.

¿Qué es el pensamiento lateral?

El pensamiento lateral es una forma de resolver problemas saliendo de lo obvio. En lugar de atacar el problema de frente (que a veces es imposible o muy lento), buscas un camino alternativo, creativo, que nadie más había visto.

Analogía: la puerta y la ventana

Imagina que llegas a una habitación y la puerta está cerrada con llave. El pensamiento "normal" (llamado pensamiento vertical) te haría buscar la llave o intentar forzar la cerradura. El pensamiento lateral te haría notar que hay una ventana abierta por la que puedes entrar.

Ambos enfoques te llevan adentro, pero el lateral puede ser mucho más rápido y elegante.

Ejemplo clásico de pensamiento lateral

Problema: Tienes 8 monedas idénticas, pero una pesa un poco menos que las otras. Tienes una balanza de dos platos. ¿Cuál es el mínimo número de pesadas para encontrar la moneda falsa?

Pensamiento "directo": Comparar monedas una por una. Necesitarías hasta 7 pesadas.

Pensamiento lateral: Divide las monedas en 3 grupos (3, 3 y 2). Pesa los dos grupos de 3. Si uno pesa menos, la moneda falsa está ahí; si pesan igual, está en el grupo de 2. Repite. Solo necesitas 2 pesadas.

Este tipo de razonamiento es exactamente lo que necesitas en la Olimpiada.

Razonamiento Inductivo vs Deductivo

Hay dos formas principales de razonar, y las usarás constantemente en programación:

Razonamiento Inductivo: de lo particular a lo general

Observas casos específicos y descubres un patrón general. Es como ser un científico que hace experimentos y descubre una ley.

Ejemplo: Si observamos la suma de los primeros n números naturales:

  • n=1: 1 = 1
  • n=2: 1+2 = 3
  • n=3: 1+2+3 = 6
  • n=4: 1+2+3+4 = 10
  • n=5: 1+2+3+4+5 = 15

¿Ves el patrón? Los resultados son 1, 3, 6, 10, 15... Cada resultado es n(n+1)2\frac{n(n+1)}{2}. Verifiquemos: para n=5, 5×62=15\frac{5 \times 6}{2} = 15

¡Acabamos de inducir una fórmula general a partir de ejemplos específicos! Esta fórmula nos permite calcular la suma sin hacer el ciclo completo, lo cual es mucho más rápido.

💡

En competencias, una estrategia muy útil es probar tu solución con casos pequeños (n=1, 2, 3, 4...) para descubrir patrones. Muchas veces, un problema que parece complicado tiene una fórmula simple escondida.

Razonamiento Deductivo: de lo general a lo particular

Partes de reglas generales que ya conoces y las aplicas a un caso específico. Es como usar teoremas matemáticos para resolver un ejercicio.

Ejemplo:

  • Regla general: Todo número divisible entre 2 es par.
  • Caso específico: 846 es divisible entre 2 (porque termina en 6, que es par).
  • Conclusión: 846 es par.

En programación, usamos razonamiento deductivo constantemente. Si sabemos que la búsqueda binaria solo funciona en arreglos ordenados (regla general) y nuestro arreglo no está ordenado (caso específico), concluimos que debemos ordenarlo primero.

Técnicas de Pensamiento Lateral para Competencias

1. Reformular el problema: "¿Puedo verlo de otra forma?"

A veces, la forma en que se presenta un problema te lleva por un camino difícil. Pero si lo reformulas, aparece una solución más simple.

Problema original: Encontrar si existe un par de números en un arreglo que sumen exactamente K.

Enfoque directo (lento): Probar todas las parejas posibles. Si hay n números, hay n×(n-1)/2 parejas. Esto es O(n²).

// Enfoque directo: O(n²) — lento para n grande
bool existeParLento(vector<int>& arr, int k) {
    int n = arr.size();
    for (int i = 0; i < n; i++) {           // Para cada número...
        for (int j = i + 1; j < n; j++) {   // ...lo comparamos con todos los demás
            if (arr[i] + arr[j] == k) {
                return true;                 // ¡Encontramos un par!
            }
        }
    }
    return false;  // No existe tal par
}

Reformulación: Si necesito que arr[i] + arr[j] = K, entonces para cada número arr[i], necesito que exista el número K - arr[i] en alguna parte del arreglo. ¡Eso es simplemente buscar un número!

// Enfoque reformulado: O(n) — ¡mucho más rápido!
bool existeParRapido(vector<int>& arr, int k) {
    set<int> vistos;  // Un conjunto que recuerda los números que ya vimos

    for (int x : arr) {
        // ¿Ya vimos el número que necesitamos para completar la suma?
        if (vistos.count(k - x)) {
            return true;  // ¡Sí! x + (k-x) = k
        }
        vistos.insert(x);  // Recordar que vimos este número
    }
    return false;
}

¿Qué pasó aquí? Cambiamos la pregunta de "¿qué pares suman K?" a "para cada número, ¿su complemento ya apareció?". Misma respuesta, pero la segunda pregunta se puede responder mucho más rápido.

2. Trabajar hacia atrás: "¿Y si empiezo por el final?"

A veces es más fácil comenzar desde la respuesta deseada y retroceder hasta el inicio.

Problema: Partiendo del número 1, llegar a N usando solo dos operaciones: multiplicar por 2 o sumar 1. ¿Cuál es el mínimo número de pasos?

Enfoque "hacia adelante": Desde 1, probar todas las combinaciones posibles. Esto puede ser muy lento.

Enfoque "hacia atrás": Desde N, pensar al revés. Si N es par, el paso anterior fue dividir entre 2. Si N es impar, el paso anterior fue restar 1.

int pasos(int n) {
    int contador = 0;

    while (n > 1) {
        if (n % 2 == 0) {
            n /= 2;    // Si es par, el paso anterior fue multiplicar por 2
        } else {
            n -= 1;     // Si es impar, el paso anterior fue sumar 1
        }
        contador++;
    }

    return contador;
}
// Para n = 10: 10 → 5 → 4 → 2 → 1 = 4 pasos

3. Simplificar: "¿Puedo resolver una versión más fácil primero?"

Cuando un problema parece abrumador, intenta resolverlo para un caso más simple y luego generaliza.

Problema complejo: Encontrar el camino más corto en un laberinto con paredes, puertas y llaves.

Versión simplificada 1: Encontrar el camino más corto en una cuadrícula sin obstáculos. Versión simplificada 2: Agregar paredes (pero sin puertas ni llaves). Versión completa: Agregar puertas y llaves.

Cada versión te da una base sobre la cual construir. Es como aprender a caminar antes de correr.

4. Contar en lugar de generar

A veces no necesitas encontrar todas las respuestas, solo contarlas. Y contar puede ser mucho más rápido que generar.

Problema: ¿Cuántos números del 1 al N son divisibles por 3 o por 5?

Enfoque lento: Recorrer todos los números del 1 al N y verificar cada uno.

// Enfoque lento: O(n)
int contarLento(int n) {
    int cuenta = 0;
    for (int i = 1; i <= n; i++) {
        if (i % 3 == 0 || i % 5 == 0) {
            cuenta++;
        }
    }
    return cuenta;
}

Enfoque con pensamiento lateral: Usar matemáticas (principio de inclusión-exclusión). Los divisibles por 3 son N/3, los divisibles por 5 son N/5. Pero los divisibles por ambos (15) los contamos dos veces, así que los restamos.

// Enfoque rápido: O(1) — ¡tiempo constante!
int contarRapido(int n) {
    int div3 = n / 3;      // Divisibles por 3
    int div5 = n / 5;      // Divisibles por 5
    int div15 = n / 15;    // Divisibles por 3 Y por 5 (por 15)
    return div3 + div5 - div15;  // Inclusión-exclusión
}

¡Pasamos de O(n) a O(1)! Para n = 1,000,000,000, el enfoque lento haría mil millones de operaciones, y el rápido solo hace 3 divisiones.

Patrones comunes que debes reconocer

Invariantes: "¿Qué no cambia?"

Un invariante es una propiedad que se mantiene verdadera durante todo el algoritmo. Pensar en invariantes te ayuda a demostrar que tu solución es correcta.

// Invariante: en cada momento, arr[0..i-1] está ordenado
void insertionSort(vector<int>& arr) {
    for (int i = 1; i < arr.size(); i++) {
        int key = arr[i];   // Tomamos el elemento a insertar
        int j = i - 1;

        // Movemos los elementos mayores una posición a la derecha
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }

        arr[j + 1] = key;  // Insertamos en la posición correcta
        // Después de esto, arr[0..i] está ordenado (invariante se mantiene)
    }
}

Simetría: "¿Puedo aprovechar lo que se repite?"

Si un problema tiene simetría, puedes resolver solo la mitad y deducir la otra.

// Para verificar si un texto es palíndromo, solo necesitamos
// comparar la primera mitad con la segunda (en espejo)
bool esPalindromo(string s) {
    int i = 0, j = s.length() - 1;
    while (i < j) {
        if (s[i] != s[j]) return false;  // No coinciden
        i++;  // Avanzar desde el inicio
        j--;  // Retroceder desde el final
    }
    return true;
}
// "reconocer" → compara r-r, e-e, c-c, o-o → ¡es palíndromo!

Ejercicios para entrenar tu pensamiento

Problema 1: Las monedas

Tienes 100 monedas en una fila, todas mostrando "cara". Haces 100 pasadas: en la pasada k, volteas cada k-ésima moneda (la k, la 2k, la 3k, etc.). ¿Cuáles monedas terminan mostrando "cara"?

Ver pista

Una moneda termina en "cara" si se volteó un número par de veces. ¿Cuántas veces se voltea la moneda en posición n? Se voltea una vez por cada divisor de n. ¿Qué números tienen un número impar de divisores? Piénsalo...

Ver solución

Solo los cuadrados perfectos (1, 4, 9, 16, 25, 36, 49, 64, 81, 100) terminan en "cruz". Todos los demás terminan en "cara".

¿Por qué? Porque los divisores vienen en parejas (ej: para 12: 1×12, 2×6, 3×4), excepto los cuadrados perfectos que tienen un divisor que se empareja consigo mismo (ej: para 9: 1×9, 3×3). Los cuadrados perfectos tienen un número impar de divisores, así que se voltean un número impar de veces.

Problema 2: Intercambiar sin variable auxiliar

¿Puedes intercambiar el valor de dos variables sin usar una tercera?

int a = 5, b = 3;
// Después del intercambio: a = 3, b = 5
// Pero NO puedes crear una variable "temp"
Ver solución
// Método 1: Con XOR (operación a nivel de bits)
a = a ^ b;   // a = 5 ^ 3 = 6
b = a ^ b;   // b = 6 ^ 3 = 5 (¡el valor original de a!)
a = a ^ b;   // a = 6 ^ 5 = 3 (¡el valor original de b!)

// Método 2: Con aritmética
a = a + b;   // a = 8
b = a - b;   // b = 8 - 3 = 5
a = a - b;   // a = 8 - 5 = 3

Ambos métodos usan pensamiento lateral: en lugar de necesitar espacio extra, usamos operaciones matemáticas reversibles.

Problema 3: El acertijo del dado

Si lanzo dos dados, ¿cuál es la probabilidad de que la suma sea 7?

Ver solución

Hay 6 × 6 = 36 combinaciones posibles. Las que suman 7 son: (1,6), (2,5), (3,4), (4,3), (5,2), (6,1) = 6 combinaciones. Probabilidad = 6/36 = 1/6.

Este tipo de conteo se usa constantemente en programación para problemas de probabilidad y combinatoria.

El pensamiento lateral es un músculo que se entrena con práctica. Cada problema que resuelvas te hará un poco mejor. No te frustres si no ves la solución de inmediato; incluso los expertos necesitan tiempo para pensar.

Siguiente paso

Continúa aprendiendo sobre Concepto de Algoritmos para formalizar estas ideas y aprender a diseñar soluciones paso a paso.