Divide y vencerás
Parte el problema en trozos más pequeños del mismo tipo, resuelve cada uno (casi siempre con recursividad) y combina los resultados: mergesort, quicksort, la potencia rápida o la búsqueda binaria.
Visualízalo paso a paso
Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.
Mergesort
Escribe los números que se ordenan (de 2 a 16) y mira cómo se parten y se mezclan.
- en la zona de trabajo
Paso 1
Array inicial: [38, 27, 43, 3, 9, 82, 10].
1static void mergeSort(int[] a, int ini, int fin) {
2 if (fin - ini < 2) return; // 0 o 1 elementos: ya ordenado
3 int mitad = (ini + fin) / 2; // tramo = —, comparaciones = 0, escrituras = 0
4 mergeSort(a, ini, mitad);
5 mergeSort(a, mitad, fin);
6 mezclar(a, ini, mitad, fin);
7}
8
9static void mezclar(int[] a, int ini, int mitad, int fin) {
10 int[] aux = Arrays.copyOfRange(a, ini, fin);
11 int i = 0, j = mitad - ini, k = ini;
12 while (i < mitad - ini && j < fin - ini) {
13 if (aux[i] <= aux[j]) a[k++] = aux[i++];
14 else a[k++] = aux[j++];
15 }
16 while (i < mitad - ini) a[k++] = aux[i++];
17 while (j < fin - ini) a[k++] = aux[j++];
18}Variables
- tramo
- —
- comparaciones
- 0
- escrituras
- 0
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
Un problema grande a veces es muy difícil de resolver de golpe y muy fácil si es pequeño. Divide y vencerás se aprovecha de eso en tres pasos: dividir el problema en subproblemas del mismo tipo y más pequeños, vencer cada subproblema (con la misma técnica, hasta llegar a un caso tan pequeño que se resuelve directamente) y combinar las soluciones de los trozos en la del problema entero.
El ejemplo clásico es mergesort: ordenar un array es difícil, pero un array de un elemento ya está ordenado, y mezclar dos arrays ordenados en uno ordenado es fácil y rápido. Así que se parte el array por la mitad, se ordena cada mitad (partiéndola otra vez) y se mezclan.
Su coste se calcula contando niveles: si cada nivel parte los problemas por la mitad, hay log₂ n niveles; si combinar en cada nivel cuesta n en total, el algoritmo cuesta n · log₂ n. Para un millón de elementos son unos 20 millones de pasos frente al billón de los métodos cuadráticos.
No todos los divide y vencerás generan dos subproblemas: la búsqueda binaria y la potencia rápida generan uno solo de la mitad de tamaño, y por eso cuestan log n.
Cuándo usarlo
- El problema se puede partir en trozos independientes del mismo tipo y las soluciones se combinan de forma sencilla.
- Para ordenar (mergesort, quicksort), buscar en datos ordenados, multiplicar números o matrices enormes, calcular potencias.
- Cuando los trozos se pueden resolver en paralelo: cada hilo resuelve uno (el framework Fork/Join de Java está pensado para esto).
Cuándo no
- Si los subproblemas se repiten (como en la definición recursiva de Fibonacci): se recalcula lo mismo exponencialmente; ahí va la programación dinámica.
- Si combinar es tan caro como resolver el problema directamente, no se gana nada.
- Para problemas pequeños, la recursión tiene un coste fijo: por eso los algoritmos reales pasan a inserción por debajo de unos pocos elementos.
Paso a paso
- Caso base. Si el problema es lo bastante pequeño (un elemento, exponente 0…), se resuelve directamente.
- Dividir. Se parte el problema en subproblemas del mismo tipo, normalmente por la mitad.
- Vencer. Se resuelve cada subproblema con una llamada recursiva.
- Combinar. Se unen las soluciones de los subproblemas en la solución del problema: mezclar dos mitades ordenadas, elevar al cuadrado la potencia de la mitad.
El código
Mergesort
El tramo se indica con desde (incluido) y hasta (excluido), así no hace falta crear arrays nuevos para las mitades: solo un array temporal al mezclar.
1import java.util.Arrays;
2
3public class Main {
4 /** Ordena a[desde..hasta) partiéndolo por la mitad, ordenando cada mitad y mezclándolas. */
5 static void mergesort(int[] a, int desde, int hasta) {
6 if (hasta - desde <= 1) return; // caso base: 0 o 1 elementos ya están ordenados
7 int medio = (desde + hasta) / 2;
8 mergesort(a, desde, medio); // vencer: cada mitad, recursivamente
9 mergesort(a, medio, hasta);
10 mezclar(a, desde, medio, hasta); // combinar
11 }
12
13 /** Mezcla dos tramos ordenados consecutivos en uno ordenado. */
14 static void mezclar(int[] a, int desde, int medio, int hasta) {
15 int[] tmp = new int[hasta - desde];
16 int i = desde, j = medio, k = 0;
17 while (i < medio && j < hasta) tmp[k++] = a[i] <= a[j] ? a[i++] : a[j++]; // el menor de los dos
18 while (i < medio) tmp[k++] = a[i++]; // lo que quede de la izquierda
19 while (j < hasta) tmp[k++] = a[j++]; // o de la derecha
20 System.arraycopy(tmp, 0, a, desde, tmp.length);
21 }
22
23 public static void main(String[] args) {
24 int[] a = {38, 27, 43, 3, 9, 82, 10};
25 mergesort(a, 0, a.length);
26 System.out.println(Arrays.toString(a));
27 }
28}def mergesort(a, desde, hasta):
"""Ordena a[desde:hasta] partiéndolo por la mitad, ordenando cada mitad y mezclándolas."""
if hasta - desde <= 1: # caso base: 0 o 1 elementos ya están ordenados
return
medio = (desde + hasta) // 2
mergesort(a, desde, medio) # vencer: cada mitad, recursivamente
mergesort(a, medio, hasta)
mezclar(a, desde, medio, hasta) # combinar
def mezclar(a, desde, medio, hasta):
"""Mezcla dos tramos ordenados consecutivos en uno ordenado."""
tmp = []
i, j = desde, medio
while i < medio and j < hasta: # el menor de los dos
if a[i] <= a[j]:
tmp.append(a[i])
i += 1
else:
tmp.append(a[j])
j += 1
tmp.extend(a[i:medio]) # lo que quede de la izquierda
tmp.extend(a[j:hasta]) # o de la derecha
a[desde:hasta] = tmp
a = [38, 27, 43, 3, 9, 82, 10]
mergesort(a, 0, len(a))
print(a)/** Ordena a[desde..hasta) partiéndolo por la mitad, ordenando cada mitad y mezclándolas. */
function mergesort(a, desde, hasta) {
if (hasta - desde <= 1) return; // caso base: 0 o 1 elementos ya están ordenados
const medio = Math.floor((desde + hasta) / 2);
mergesort(a, desde, medio); // vencer: cada mitad, recursivamente
mergesort(a, medio, hasta);
mezclar(a, desde, medio, hasta); // combinar
}
/** Mezcla dos tramos ordenados consecutivos en uno ordenado. */
function mezclar(a, desde, medio, hasta) {
const tmp = [];
let i = desde, j = medio;
while (i < medio && j < hasta) tmp.push(a[i] <= a[j] ? a[i++] : a[j++]); // el menor de los dos
while (i < medio) tmp.push(a[i++]); // lo que quede de la izquierda
while (j < hasta) tmp.push(a[j++]); // o de la derecha
a.splice(desde, tmp.length, ...tmp);
}
const a = [38, 27, 43, 3, 9, 82, 10];
mergesort(a, 0, a.length);
console.log("[" + a.join(", ") + "]");using System;
class Program {
// Ordena a[desde..hasta) partiéndolo por la mitad, ordenando cada mitad y mezclándolas.
static void Mergesort(int[] a, int desde, int hasta) {
if (hasta - desde <= 1) return; // caso base: 0 o 1 elementos ya están ordenados
int medio = (desde + hasta) / 2;
Mergesort(a, desde, medio); // vencer: cada mitad, recursivamente
Mergesort(a, medio, hasta);
Mezclar(a, desde, medio, hasta); // combinar
}
// Mezcla dos tramos ordenados consecutivos en uno ordenado.
static void Mezclar(int[] a, int desde, int medio, int hasta) {
int[] tmp = new int[hasta - desde];
int i = desde, j = medio, k = 0;
while (i < medio && j < hasta) tmp[k++] = a[i] <= a[j] ? a[i++] : a[j++]; // el menor de los dos
while (i < medio) tmp[k++] = a[i++]; // lo que quede de la izquierda
while (j < hasta) tmp[k++] = a[j++]; // o de la derecha
Array.Copy(tmp, 0, a, desde, tmp.Length);
}
static void Main() {
int[] a = { 38, 27, 43, 3, 9, 82, 10 };
Mergesort(a, 0, a.Length);
Console.WriteLine("[" + string.Join(", ", a) + "]");
}
}<?php
/** Ordena $a[$desde..$hasta) partiéndolo por la mitad, ordenando cada mitad y mezclándolas. */
function mergesort(array &$a, int $desde, int $hasta): void {
if ($hasta - $desde <= 1) return; // caso base: 0 o 1 elementos ya están ordenados
$medio = intdiv($desde + $hasta, 2);
mergesort($a, $desde, $medio); // vencer: cada mitad, recursivamente
mergesort($a, $medio, $hasta);
mezclar($a, $desde, $medio, $hasta); // combinar
}
/** Mezcla dos tramos ordenados consecutivos en uno ordenado. */
function mezclar(array &$a, int $desde, int $medio, int $hasta): void {
$tmp = [];
$i = $desde;
$j = $medio;
while ($i < $medio && $j < $hasta) $tmp[] = $a[$i] <= $a[$j] ? $a[$i++] : $a[$j++]; // el menor de los dos
while ($i < $medio) $tmp[] = $a[$i++]; // lo que quede de la izquierda
while ($j < $hasta) $tmp[] = $a[$j++]; // o de la derecha
array_splice($a, $desde, count($tmp), $tmp);
}
$a = [38, 27, 43, 3, 9, 82, 10];
mergesort($a, 0, count($a));
echo "[" . implode(", ", $a) . "]\n";Salida al ejecutarlo (la misma en los 5 lenguajes)
[3, 9, 10, 27, 38, 43, 82]
Potencia rápida
Un solo subproblema de la mitad de tamaño: x¹⁰⁰ se calcula con unas 8 multiplicaciones en lugar de 99.
1/** x elevado a n con O(log n) multiplicaciones: x^n = (x^(n/2))², por x si n es impar. */
2static long potencia(long x, int n) {
3 if (n == 0) return 1; // caso base
4 long mitad = potencia(x, n / 2); // un solo subproblema, de la mitad de tamaño
5 long r = mitad * mitad; // combinar
6 return n % 2 == 0 ? r : r * x;
7}def potencia(x, n):
"""x elevado a n con O(log n) multiplicaciones: x^n = (x^(n/2))², por x si n es impar."""
if n == 0: # caso base
return 1
mitad = potencia(x, n // 2) # un solo subproblema, de la mitad de tamaño
r = mitad * mitad # combinar
return r if n % 2 == 0 else r * x
# Python ya lo hace así por dentro: pow(x, n) y x ** n; pow(x, n, m) además con módulo/** x elevado a n con O(log n) multiplicaciones: x^n = (x^(n/2))², por x si n es impar. */
function potencia(x, n) {
if (n === 0) return 1n; // caso base
const mitad = potencia(x, Math.floor(n / 2)); // un solo subproblema, de la mitad de tamaño
const r = mitad * mitad; // combinar
return n % 2 === 0 ? r : r * x;
}
// Con BigInt (1n, 2n…) los resultados grandes son exactos: potencia(2n, 100)// x elevado a n con O(log n) multiplicaciones: x^n = (x^(n/2))², por x si n es impar.
static long Potencia(long x, int n) {
if (n == 0) return 1; // caso base
long mitad = Potencia(x, n / 2); // un solo subproblema, de la mitad de tamaño
long r = mitad * mitad; // combinar
return n % 2 == 0 ? r : r * x;
}/** $x elevado a $n con O(log n) multiplicaciones: x^n = (x^(n/2))², por x si n es impar. */
function potencia(int $x, int $n): int {
if ($n === 0) return 1; // caso base
$mitad = potencia($x, intdiv($n, 2)); // un solo subproblema, de la mitad de tamaño
$r = $mitad * $mitad; // combinar
return $n % 2 === 0 ? $r : $r * $x;
}Traza: mergesort de {38, 27, 43, 3, 9, 82, 10}
| Paso | Operación | Resultado |
|---|---|---|
| 1 | dividir {38, 27, 43, 3, 9, 82, 10} | {38, 27, 43} y {3, 9, 82, 10} |
| 2 | dividir {38, 27, 43} | {38} y {27, 43} |
| 3 | dividir y mezclar {27, 43} | {27, 43} |
| 4 | mezclar {38} con {27, 43} | {27, 38, 43} |
| 5 | dividir {3, 9, 82, 10} | {3, 9} y {82, 10} |
| 6 | dividir y mezclar {3, 9} y {82, 10} | {3, 9} y {10, 82} |
| 7 | mezclar {3, 9} con {10, 82} | {3, 9, 10, 82} |
| 8 | mezclar {27, 38, 43} con {3, 9, 10, 82} | {3, 9, 10, 27, 38, 43, 82} |
Al mezclar se compara siempre el primero que queda de cada mitad y se toma el menor: cada elemento se mueve una vez por nivel.
Complejidad
| Algoritmo | Subproblemas | Coste de combinar | Coste total |
|---|---|---|---|
| Mergesort | 2 de tamaño n/2 | O(n) | O(n log n) siempre; memoria O(n) |
| Quicksort | 2 de tamaño variable | O(n) al partir | O(n log n) de media; O(n²) con pivotes malos |
| Búsqueda binaria | 1 de tamaño n/2 | O(1) | O(log n) |
| Potencia rápida | 1 de tamaño n/2 | O(1) | O(log n) multiplicaciones |
La regla práctica: cuenta los niveles (log n si se divide por la mitad) y lo que cuesta cada nivel. Mergesort hace n de trabajo en cada uno de sus log₂ n niveles.
- Mejor caso: O(n log n)
- Caso medio: O(n log n)
- Peor caso: O(n log n)
Mergesort: log₂ n niveles con n de trabajo en cada uno, siempre. Las curvas grises son las demás clases, para comparar.
En la práctica
Arrays.sortde objetos usa TimSort, una mezcla de mergesort e inserción; con tipos primitivos, quicksort con dos pivotes.- El framework Fork/Join (
RecursiveTask) y los streams paralelos de Java reparten los trozos entre hilos y combinan los resultados. - La exponenciación modular rápida está en el corazón del cifrado RSA y de
BigInteger.modPow. - MapReduce, la técnica de procesamiento de grandes volúmenes de datos, aplica la misma idea a miles de máquinas.
Errores típicos
- Olvidar el caso base o ponerlo mal: la recursión no termina y da
StackOverflowError. - Calcular dos veces el mismo subproblema (
potencia(x, n/2) * potencia(x, n/2)): la potencia rápida se vuelve lineal. - En quicksort, elegir siempre el primer elemento como pivote: con datos ya ordenados los trozos son de 0 y n−1 elementos y el coste sube a O(n²).
- Equivocarse con los límites de los tramos (incluidos o excluidos) al dividir y al mezclar: elementos repetidos o perdidos.
- Crear arrays nuevos para cada mitad en cada llamada: funciona, pero gasta mucha más memoria de la necesaria.
Ejercicios
Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.
1. Contar inversiones con mergesort
Una inversión en un array es un par de posiciones i < j con a[i] > a[j]: mide lo desordenado que está (un array ordenado tiene 0; uno al revés, el máximo). Cuéntalas mientras ordenas con mergesort: al mezclar, cada vez que se toma un elemento de la mitad derecha, es menor que todos los que quedan en la izquierda, y cada uno de ellos forma una inversión con él. El main ya está: completa ordenarYContar.
- Entrada: una línea con los números separados por espacios (puede estar vacía).
- Salida:
Inversiones: NyOrdenado: …con los números ordenados (o(vacío)). Si no son números:Entrada no válida.
Ejemplo
2 4 1 3 5
Inversiones: 3 Ordenado: 1 2 3 4 5
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 /** Ordena a[desde..hasta) y devuelve cuántas inversiones (i < j con a[i] > a[j]) tenía. */
5 static long ordenarYContar(int[] a, int desde, int hasta) {
6 if (hasta - desde <= 1) return 0;
7 int medio = (desde + hasta) / 2;
8 long inv = ordenarYContar(a, desde, medio) + ordenarYContar(a, medio, hasta);
9 int[] tmp = new int[hasta - desde];
10 int i = desde, j = medio, k = 0;
11 while (i < medio && j < hasta) {
12 if (a[i] <= a[j]) tmp[k++] = a[i++];
13 else {
14 inv += medio - i; // a[j] es menor que todos los que quedan a la izquierda
15 tmp[k++] = a[j++];
16 }
17 }
18 while (i < medio) tmp[k++] = a[i++];
19 while (j < hasta) tmp[k++] = a[j++];
20 System.arraycopy(tmp, 0, a, desde, tmp.length);
21 return inv;
22 }
23
24 public static void main(String[] args) {
25 Scanner sc = new Scanner(System.in);
26 String linea = sc.hasNextLine() ? sc.nextLine().trim() : "";
27 int[] a;
28 try {
29 a = linea.isEmpty() ? new int[0] : Arrays.stream(linea.split("\\s+")).mapToInt(Integer::parseInt).toArray();
30 } catch (NumberFormatException e) {
31 System.out.println("Entrada no válida");
32 return;
33 }
34 long inv = ordenarYContar(a, 0, a.length);
35 System.out.println("Inversiones: " + inv);
36 System.out.println("Ordenado: " + (a.length == 0 ? "(vacío)" : String.join(" ", Arrays.stream(a).mapToObj(String::valueOf).toList())));
37 }
38}Contar las inversiones con dos bucles es O(n²). Contarlas al mezclar aprovecha que las dos mitades ya están ordenadas: se cuentan muchas de golpe y el total sigue siendo O(n log n).
Es la misma técnica que se usa para comparar dos rankings (cuántos pares están en distinto orden) o para medir lo parecidas que son dos listas de preferencias.
2. Potencia rápida modular
Calcula base^exp mod m con la potencia rápida recursiva y cuenta las multiplicaciones: si exp es 0 el resultado es 1 mod m y si es 1, base mod m (sin multiplicar); si no, se calcula la potencia de exp / 2, se eleva al cuadrado (1 multiplicación) y, si exp es impar, se multiplica además por la base (otra). Haz siempre el módulo después de cada multiplicación para no desbordar. El main ya está: cambia el bucle de potencia por divide y vencerás.
- Cada línea:
base exp m(base y exponente ≥ 0, m entre 1 y 2.000.000.000). Con este método, 3¹³ necesita 5 multiplicaciones: para 3³, un cuadrado y un producto (3² · 3); para 3⁶, un cuadrado; y para 3¹³, un cuadrado y un producto (3¹² · 3). - Salida:
b^e mod m = r (k multiplicaciones; la forma directa haría d), donde d = e − 1 (o 0).1 multiplicaciónen singular. - Una línea mal escrita o fuera de rango:
Línea no válida: línea.
Ejemplo
3 13 1000 2 10 1000000 5 0 7 7 1 10
3^13 mod 1000 = 323 (5 multiplicaciones; la forma directa haría 12) 2^10 mod 1000000 = 1024 (4 multiplicaciones; la forma directa haría 9) 5^0 mod 7 = 1 (0 multiplicaciones; la forma directa haría 0) 7^1 mod 10 = 7 (0 multiplicaciones; la forma directa haría 0)
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 static int multiplicaciones;
5
6 /** base^exp mod m con potencia rápida recursiva, contando las multiplicaciones. */
7 static long potencia(long base, long exp, long m) {
8 if (exp == 0) return 1 % m;
9 if (exp == 1) return base % m;
10 long mitad = potencia(base, exp / 2, m);
11 long r = mitad * mitad % m;
12 multiplicaciones++;
13 if (exp % 2 == 1) {
14 r = r * (base % m) % m;
15 multiplicaciones++;
16 }
17 return r;
18 }
19
20 public static void main(String[] args) {
21 Scanner sc = new Scanner(System.in);
22 while (sc.hasNextLine()) {
23 String linea = sc.nextLine().trim();
24 if (linea.isEmpty()) continue;
25 String[] p = linea.split("\\s+");
26 long b, e, m;
27 try {
28 if (p.length != 3) throw new NumberFormatException();
29 b = Long.parseLong(p[0]);
30 e = Long.parseLong(p[1]);
31 m = Long.parseLong(p[2]);
32 } catch (NumberFormatException ex) {
33 System.out.println("Línea no válida: " + linea);
34 continue;
35 }
36 if (b < 0 || e < 0 || m < 1 || m > 2_000_000_000L) {
37 System.out.println("Línea no válida: " + linea);
38 continue;
39 }
40 multiplicaciones = 0;
41 long r = potencia(b, e, m);
42 long directa = Math.max(0, e - 1);
43 System.out.println(b + "^" + e + " mod " + m + " = " + r + " (" + multiplicaciones
44 + (multiplicaciones == 1 ? " multiplicación" : " multiplicaciones") + "; la forma directa haría " + directa + ")");
45 }
46 }
47}Cada nivel de la recursión divide el exponente entre dos, así que hay unos log₂ e niveles con una o dos multiplicaciones cada uno: para e = 10¹⁸, unas 90 multiplicaciones en lugar de 10¹⁸.
Hacer el módulo en cada paso mantiene los números pequeños; es la misma cuenta que hace BigInteger.modPow y que usan RSA y otros cifrados.
Test
Test: Divide y vencerás
0/5 respondidas · 0 aciertosElige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.
1.¿Cuáles son los tres pasos de divide y vencerás?
2.¿Por qué mergesort cuesta O(n log n)?
3.¿Qué pasa con la potencia rápida si escribes
potencia(x, n/2) * potencia(x, n/2)?4.¿Cuándo es mala idea aplicar divide y vencerás sin más?
5.¿En qué caso quicksort, eligiendo como pivote el primer elemento, tarda O(n²)?