Sunteți pe pagina 1din 83

Robert Espinoza Domínguez

Divide y vencerás.
Introducción

 El término Divide y Vencerás en su acepción más


amplia es una filosofía general para resolver
problemas.
 Por esta razón se utiliza en muchos otros ámbitos
como la estrategia militar o la política.
 En nuestro contexto utilizaremos ésta expresión para
nombrar una técnica de diseño de algoritmos.
 Esta técnica sirve para resolver un problema a partir
de la solución de subproblemas del mismo tipo pero de
menor tamaño.
Divide y Vencerás

 La resolución de un problema mediante esta técnica


consta de los siguientes pasos:
 Plantearse el problema de forma que pueda
descomponerse en k subproblemas del mismo tipo,
pero de menor tamaño. A ésta tarea se le conoce
como división.
 Resolver de manera sucesiva e independiente cada
uno de estos subproblemas, bien directamente si son
elementales (caso base) o bien de forma recursiva.
 Combinar las soluciones obtenidas en el paso
anterior para construir la solución del problema
original.
Justificación del Divide y Vencerás

 Para que se justifique divide y vencerás se


necesitan tres condiciones.
 Tiene que ser posible descomponer el problema
en subproblemas y recomponer las soluciones
parciales de forma bastante eficiente.
 Los subproblemas deben ser en lo posible
aproximadamente del mismo tamaño.
 La decisión de utilizar el subalgoritmo básico en
lugar de hacer llamadas recursivas debe tomarse
cuidadosamente.
Esquema General

Método divideYVenceras(x)
Si (x es suficientemente pequeño) entonces
retornar soluciónInmediata(x)
Sino
descomponer x en {x1,…, xk}
Para i desde 1 hasta k hacer
yi  divideYVenceras(xi)
Fin Para
y  recombinar (y1, …, yk)
retornar y
Fin Si
Fin Método
Esquema General

 El número k de subproblemas debe ser independiente


y pequeño de una entrada determinada, es decir no
debe haber solapamiento entre ellos. De lo contrario el
tiempo de ejecución será exponencial.
 La sucesión fibonacci es recursivo pero no es Divide y
Vencerás pues su tiempo de ejecución es exponencial.
 Cuando k = 1, no tiene sentido descomponer x en un
subproblema más sencillo x1. Lo que si tiene sentido es
reducir la solución de un caso muy grande a la de uno
más pequeño.
 El tamaño de los k subproblemas es aproximadamente
n/k para alguna constante k, en donde n es el tamaño
del caso original.
Esquema General

 Sea g(n) el tiempo requerido por DV en casos de


tamaño n, sin contar el tiempo necesario para
llamadas recursivas. El tiempo total t(n) requerido
por este algoritmo de divide y vencerás es parecido
a:

t ( n)  k * t ( n / k )  g ( n)

Siempre que n sea suficientemente grande.


Aspectos de Diseño

 Algoritmo recursivo
División del problema en subproblemas y combinación
eficiente de las soluciones parciales.
 Los subproblemas deben tener, aproximadamente, el
mismo tamaño.
 Algoritmo específico
Para resolver problemas de tamaño pequeño.
 Determinación del umbral
Para decidir cuando finalizar la descomposición
recursiva del problema y aplicar el algoritmo
específico.
Algoritmos de simplificación

 En el caso particular de los algoritmos Divide y


Vencerás que contienen sólo una llamada recursiva,
es decir k=1, hablaremos de algoritmos de
simplificación. Ejemplo:
 Factorial
 Búsqueda binaria en un vector
 Hallar el k-ésimo elemento.
 La ventaja de los algoritmos de simplificación es que
consiguen reducir el tamaño del problema en cada paso,
por lo que sus tiempos de ejecución suelen ser muy
buenos (logarítmicos o lineal)
Ventajas y desventajas

 Estos algoritmos van a heredar las ventajas e


inconvenientes que la recursión plantea:
 El diseño suele ser simple, claro, robusto y elegante.
 Mayor legibilidad y facilidad de depuración y
mantenimiento.
 Conllevan normalmente un mayor tiempo de ejecución
que los iterativos.
 Mayor complejidad espacial que puede representar el
uso de la pila de recursión.
Aplicaciones

 Algoritmo de búsqueda binaria.


 Algoritmos de ordenación (Mergesort, Quicksort).
 Problema de la selección (p.ej. mediana)
 Exponenciación rápida
 Multiplicación de matrices: Algoritmo de Strassen.
 Subsecuencia de suma máxima
 Par de puntos más cercano.
 Eliminación de superficies ocultas.
 Numero de inversiones (rankings).
 FFT: Transformada Rápida de Fourier (convoluciones).
 Interacciones entre n partículas.
 Calendario de una liga, etc.
Ejemplo. Búsqueda Binaria
Búsqueda Binaria

 Aplica la técnica Divide y vencerás


 Se usa en un arreglo ordenado
 Consiste en dividir por la mitad el arreglo original
obteniendo dos subarreglos.
 La búsqueda se limita a uno de los subarreglos
según el resultado de la comparación con el
elemento central.
Búsqueda Binaria

 Hallar elemento 22
Búsqueda binaria

Clase BusquedaBinaria
Método BusBin (a, prim, ult, val )
Si (prim > ult) entonces
retornar -1
Sino
medio  (prim + ult) div 2
Si (val = a[medio])
retornar medio
Sino
Búsqueda binaria

Si (val < a[mitad]) entonces


BusBin(a, prim, medio – 1, val)
Else
BusBin(a, medio + 1, ult, val)
Fin si
Fin si
Fin si
Fin Metodo
Fin Clase
Búsqueda binaria

 Eficiencia
O(log n), en todos los casos.
Ejemplo: Multiplicación de enteros
grandes
Multiplicación de enteros de n cifras

Algoritmo clásico
1234*5678 = 1234* (5*1000 + 6*100+7*10+8)
= 1234*5*1000 + 1234*6*100 + 1234*7*10 +
1234*8

Operaciones básicas:
 Multiplicaciones de digitos: O(1)
 Sumas de digitos: O(1)
 Desplazamientos: O(1)

Eficiencia algoritmo: O(n2)


Multiplicación de enteros de n cifras

Algoritmo “divide y vencerás” simple

1234 = 12*100 + 34
5678 = 56*100 + 78

1234*5678 = (12*100 + 34)*(56*100 + 78)


= 12*56*10000 + (12*78+34*56)*100 +
(34*78)
Idea: Se reduce una multiplicación de 4 cifras a cuatro
multiplicaciones de 2 cifras, más tres sumas y varios
desplazamientos.
Multiplicación de enteros de n cifras

Algoritmo “divide y vencerás” simple

1. Dividir
X = 12345678 = xi*104 + xd xi=1234, xd=5678
Y = 24680135 = yi*104 + yd yi=2468, yd=0135

2. Combinar
X*Y = (xi*104 + xd) * (yi*104 + yd)
= xi*yi*108 + (xi*yd+xd*yi)*104 + xd*yd
Multiplicación de enteros de n cifras

Algoritmo “divide y vencerás” simple


En general:
s = n div 2

X = xi*10s + xd
Y = yi*10s + yd

X*Y = (xi*10s + xd) * (yi*10s + yd)


= xi*yi*102s + (xi*yd+xd*yi)*10s + xd*yd
Multiplicación de enteros de n cifras
Método multiplica (x, y)
n máx(tamaño(x),tamaño(y));
Si (n es pequeño) entonces
retorna x*y
Sino
//Obtener xi, xd, yi, yd (Dividir)
s  n div 2
xi x div 10s; xd  x mod 10s
yi y div 10s; yd  y mod 10s
p1  multiplica(xi,yi)
p2  multiplica(xi,yd)
p3  multiplica(xd,yi)
p4  multiplica(xd,yd)
//Combinar
devuelve p1*102s +(p2 + p3)*10s + p4
Fin Si
Fin Método
Multiplicación de enteros de n cifras

Algoritmo “divide y vencerás” simple

T(n) = 4T(n/2) + n ∈ O(n2)

 El cuello de botella esta en el numero de multiplicaciones


de tamaño n/2.
 Para mejorar la eficiencia debemos reducir el número de
multiplicaciones necesario
Multiplicación de enteros de n cifras
eficiente

Algoritmo divide y vencerás eficiente


r = (xi+xd)*(yi+yd) = xi*yi + (xi*yd + xd*yi) + xd*yd
p = xi*yi
q = xd*yd
r-p-q = (xi*yd + xd*yi)

x*y = p*102s + (r-p-q)*10s + q

Luego podemos realizar una multiplicación de tamaño n a


partir de 3 multiplicaciones de tamaño s=n/2.
Multiplicación de enteros de n cifras
eficiente
Método multiplica2 (x, y)
n máx(tamaño(x),tamaño(y));
Si (n es pequeño) entonces
retorna x*y
Sino
//Obtener xi, xd, yi, yd (Dividir)
s  n div 2
xi x div 10s; xd  x mod 10s
yi x div 10s; yd  x mod 10s
r  multiplica2(xi+xd,yi+yd)
p  multiplica2(xi,yi)
q  multiplica2(xi,yd)
//Combinar
devuelve p*102s +(r-p-q)*10s + q
Fin Si
Fin Método
Multiplicación de enteros de n cifras
mejorado

Eficiencia
Multiplicación de enteros de n cifras

Comparación de la eficiencia
Ejemplo. Ordenación por fusión
(MergeSort)
MergeSort

 Dado un vector A de n elementos se trata de ordenar


de forma creciente esos elementos.
 Técnica de divide y vencerás:
 Dividir el vector en dos mitades
 Ordenar recursivamente las dos mitades
 Fusionar las dos mitades ordenadas
MergeSort

Dividir O(1)

Ordenar 2t*O(n/2)

Fusionar O(n)
MergeSort

 ¿Cómo fusionar eficientemente dos listas ordenadas?


Usando un arreglo auxiliar y un número lineal de
comparaciones:
 Controlar la posición del elemento mas pequeño en cada
mitad.
 Añadir el más pequeño de los dos a un vector auxiliar.
 Repetir hasta que se hayan añadido todos los elementos.
MergeSort
MergeSort
MergeSort
MergeSort
MergeSort
MergeSort

Método mergeSort (A, inicio, fin)


Si (inicio < fin) entonces
mitad  (inicio+fin) div 2
mergeSort (A, inicio, mitad)
mergeSort (A, mitad+1, fin)
fusionar (A, inicio, mitad, fin)
Sino
// No hace nada pues arreglo es de tamaño 1
FinSi
Fin Método
MergeSort

 El algoritmo utiliza el hecho de que un arreglo de


tamaño 1 ya esta ordenado, en el caso base no se
realiza ninguna operación.
 En el caso recursivo el algoritmo fusionar (…) es
utilizado para combinar las dos mitades ordenadas en
un arreglo ya ordenado.
MergeSort

Método fusionar(A, inicio, mitad, fin) ´


i inicio
j mitad + 1
k0
Mientras (i<=mitad y j <= fin) hacer
Si (A[ i ] < A[ j ] entonces
B[k]  A[ i ]
i i + 1
Sino
B[k]  A[ j ]
jj+1
Fin Si
kk+1
Fin Mientras
MergeSort
Si (i > mitad) entonces //se acabó arreglo izquierdo
Para q desde j hasta fin hacer
B[ k ]  A[ q ]
kk+1
Fin Para
Sino //se acabó arreglo derecho
Para q desde i hasta mitad hacer
B[ k ]  A[ q ]
kk+1
Fin Para
Fin Si
Para i desde inicio a fin hacer
A[ i ]  B[ i ]
Fin Para
Fin Método
MergeSort. Eficiencia
Recorrido de árboles binarios
Nodo

 Es cualquier tipo de datos cuyos elementos son


registros formados por un campo Datos y un número
dado de apuntadores o enlaces.
Árbol general

 Un árbol de grado n consta de un conjunto finito de


nodos con n apuntadores.
 Cada uno de los nodos es apuntado por un único nodo
salvo uno y apunta a uno o más árboles denominados
subárboles.
 Se dice que un árbol está vacío sin no contiene ningún
nodo.
Árbol general

F H

B M C N D

E P R
Terminología de árboles

 Un nodo y es “descendiente directo” o “hijo” de un


nodo x, si y es apuntado por x.
 El nodo y es “ascendiente directo” o “padre” de x, si
y apunta a x.
 La “raíz” del árbol se define como el nodo que no
tiene ascendiente o padre.
 Si un nodo no tiene descendientes se le denomina
“hoja” o “nodo terminal”.
 Un elemento que no es terminal es un nodo “interior”.
 El enlace entre dos nodos se conoce como “arista” o
“rama”.
Terminología de árboles

 Se denomina “grado” del nodo al número de


descendientes directos del nodo.
 El “grado” del árbol es el mayor de los grados de los
nodos.
 Según el grado, los árboles se denominan:
 Binarios (como máximo dos hijos),
 Ternarios (como máximo tres hijos), y así sucesivamente.
 Un camino de n1 a n2 es una secuencia de ramas
contiguas que van de n1 a n2
 La longitud de un camino es el número de ramas que
contiene (en otras palabras el número de nodos – 1)
Terminología de árboles

 Se denomina “nivel” de un nodo al número de


descendientes que deben recorrerse desde la raíz
hasta el nodo. El nivel de la raíz es cero.
 También se puede definir “nivel” de un nodo como la
longitud del camino que lo conecta a la raíz
 La altura de un nodo de un árbol es la longitud del
camino desde el nodo a la hoja más lejana
 La profundidad del árbol se define como la longitud
del camino más largo que conecta la raíz a una hoja
más uno.
Terminología de árboles

raíz Nivel 0
Padre
de y
Nodo x int int Nivel 1

Nodo y ter int ter Nivel 2

Hijo de x

ter Nivel 3

Grado del árbol = 2 Profundidad del árbol = 4 Altura del árbol = 3


Terminología de árboles

 Un árbol se subdivide en subárboles.


 Un subárbol es cualquier estructura conectada por
debajo de la raíz.
 Cada nodo de un árbol es la raíz de un subárbol que
se define por el nodo y todos los descendientes del
nodo.
 Los subárboles se pueden subdividir en subárboles
Terminología de árboles

A
subárbol

F H

B M C N D

P R
Árboles Binarios

 Es un árbol en el que ningún nodo puede tener más de


dos subárboles.
 En un árbol binario, cada nodo puede tener, cero, uno o
dos hijos (subárboles).
 Se conoce el nodo de la izquierda como hijo izquierdo
y el nodo de la derecha como hijo derecho.
 Un árbol degenerado es un árbol binario especial en el
que existe un solo nodo hoja y cada nodo no hoja sólo
tiene un hijo.
 Un árbol degenerado es equivalente a una lista enlazada
Árboles binarios

A A

Árbol
B degenerado
B C

D E F
D

G H E
Representación mediante punteros

izdo Datos dcho

izdo Datos dcho


izdo Datos dcho
Hijo derecho
Hijo izquierdo

struct nodo{
float datos;
nodo *hijo_izdo;
nodo *hijo_dcho;
};
typedef nodo *pnodo;
Árboles binarios mediante punteros

A
A

B C
N
B C

E F
D
N N N
D E F N

G H
G H N N N N
Declarar nodo de un árbol binario

struct nodo{
float dato;
nodo *izdo;
nodo *dcho;
};
typedef nodo *pnodo;

pnodo izdo dato dcho


Crear nodo de árbol binario

pnodo crear_nodo( float valor)


{
pnodo nuevo;
nuevo = new nodo;
if (nuevo==NULL)
cout<<"Error de memoria";
else {
nuevo->dato=valor;
nuevo->izdo=NULL;
nuevo->dcho=NULL;
}
return nuevo;
}
Crear un árbol binario

Por ejemplo si se desea crear el siguiente árbol binario:

7 11

El procedimiento sería el siguiente:


pnodo raiz
raiz = crear_nodo(9);
raiz->izdo = crear_nodo(7);
raiz->dcho = crear_nodo(11);
Operaciones en árboles binarios

 Una vez creado el árbol binario, se pueden realizar


varias operaciones sobre él, tales como:
 Determinar su altura
 Determinar su número de elementos
 Hacer una copia
 Visualizar el árbol binario en pantalla.
 Determinar si dos árboles binarios son idénticos.
 Borrar (eliminar el árbol).
 Si es un árbol de expresión, evaluar la expresión.
 Todas estas operaciones se pueden realizar
recorriendo el árbol binario de un modo sistemático.
Recorrido de un árbol binario

 El recorrido de un árbol binario requiere que cada nodo


del árbol sea procesado (visitado) una sola vez en una
secuencia predeterminada.
 Existen dos enfoques para la secuencia de recorrido:
 Recorrido en anchura. Se realiza horizontalmente
desde la raíz a todos sus hijos, a continuación a los hijos
de sus hijos y así sucesivamente.
Cada nivel se procesa totalmente antes de que comience
el siguiente nivel.
 Recorrido en profundidad. Exige un camino desde la
raíz a través de un hijo, al descendiente más lejano del
primer hijo antes de proseguir a un segundo hijo.
Todos los descendientes de un hijo se procesan antes del
siguiente hijo
Recorrido en profundidad de un árbol
binario
 Pre Orden (N I D)
 In Orden (I N D)
 Post Orden (I D N)
Recorrido pre orden - NID

 Regla: La raíz se procesa antes que los subárboles


izquierdo y derecho.
 Pasos a seguir si el árbol no está vacío.
 Procesa la raíz (N)
 Recorrer el subárbol izquierdo ( I ) en pre orden
 Recorrer el subárbol derecho (D) en pre orden
 Dada las características recursivas de los árboles, el
algoritmo de recorrido (preorden) tiene naturaleza
recursiva.
 Primero se procesa la raíz
 Para procesar subárbol izquierda se llama recursivamente al
procedimiento Preorden
 Se hace lo mismo con el subárbol derecho.
Recorrido pre orden - NID

 Pre Orden. NID (nodo – izquierdo – derecho)

I D
Recorrido pre orden - NID

B C

2 5

D E F G

3 4 6 7

A, B, D, E, C, F, G
Recorrido pre orden - NID

15 2

5 9 19

17 3 23

25

 1, 15, 5, 9, 17, 3, 2, 19, 23, 25


Recorrido pre orden - Algoritmo

Acción Preorden (T)


Si T no es vacío entonces
Ver los datos de la raíz de T
Preorden (subárbol izquierdo de la raíz de T)
Preorden (subárbol derecho de la raíz de T)
Fin Si
Fin Acción
Recorrido pre orden – Algoritmo refinado

Acción Preorden (raíz)


Si raiz < > NULL entonces
Procesar raíz
Preorden (raíz.hijoizquierdo)
Preorden (raíz.hijoderecho)
Fin Si
Fin Acción

2 3
Recorrido pre orden en C++

struct nodo{
float dato;
nodo *hijo_izdo, *hijo_dcho;
};
typedef nodo *pnodo;
void preorden (pnodo raiz)
{
if (raiz!=NULL){
cout<<raiz->datos;
preorden(raiz->hijo_izdo);
preorden(raiz->hijo_dcho);
}
}
Recorrido In Orden - IND

 Regla: procesa primero el subárbol izquierdo, después la


raíz y a continuación el subárbol derecho.
 Pasos a seguir si el árbol no está vacío.
 Recorrer el subárbol izquierdo ( I ) en inorden
 Visita el nodo raíz (N)
 Recorrer el subárbol derecho (D) en inorden
Recorrido In Orden - IND

 In Orden. IND (izquierdo – nodo – derecho)

I D
Recorrido in orden - IND

B C

2 6

D E F G

1 3 5 7

D, B, E, A, F, C, G
Recorrido In Orden - IND

15 2

5 9 19

17 3 23

25

 5, 15, 17, 9, 3, 1, 2, 19, 25, 23


Recorrido in orden - Algoritmo

Acción Inorden (T)


Si T no es vacío entonces
Inorden (subárbol izquierdo de la raíz de T)
Ver los datos de la raíz de T
Inorden (subárbol derecho de la raíz de T)
Fin Si
Fin Acción
Recorrido in orden – Algoritmo refinado

Acción Inorden (raíz)


Si raiz < > NULL entonces
Preorden (raíz.hijoizquierdo)
Procesar raíz
Preorden (raíz.hijoderecho)
Fin Si
Fin Acción

1 3
Recorrido in orden en C++

struct nodo{
float dato;
nodo *hijo_izdo, *hijo_dcho;
};
typedef nodo *pnodo;
void inorden (pnodo raiz)
{
if (raiz!=NULL){
inorden(raiz->hijo_izdo);
cout<<raiz->datos;
inorden(raiz->hijo_dcho);
}
}
Recorrido Post Orden - IDN

 Regla: procesa primero el subárbol izquierdo, a


continuación el subárbol derecho y después la raíz.
 Pasos a seguir si el árbol no está vacío.
 Recorrer el subárbol izquierdo ( I ) en postorden
 Recorrer el subárbol derecho (D) en iostorden
 Visita el nodo raíz (N)
Recorrido Post Orden - IDN

 Post Orden. IDN (izquierdo – derecho – nodo)

I D
Recorrido Post Orden - IDN

B C

3 6

D E F G

1 2 4 5

D, E, B, F, G, C, A
Recorrido Post Orden - IDN

15 2

5 9 19

17 3 23

25

 5, 17, 3, 9, 15, 25, 23, 19, 2, 1


Recorrido post orden - Algoritmo

Acción Postorden (T)


Si T no es vacío entonces
Postorden (subárbol izquierdo de la raíz de T)
Postorden (subárbol derecho de la raíz de T)
Ver los datos de la raíz de T
Fin Si
Fin Acción
Recorrido post orden – Algoritmo refinado

Acción Postorden (raíz)


Si raiz < > NULL entonces
Postorden (raíz.hijoizquierdo)
Postorden (raíz.hijoderecho)
Procesar raíz
Fin Si
Fin Acción

1 2
Recorrido post orden en C++

struct nodo{
float dato;
nodo *hijo_izdo, *hijo_dcho;
};
typedef nodo *pnodo;
void postorden (pnodo raiz)
{
if (raiz!=NULL){
postorden(raiz->hijo_izdo);
postorden(raiz->hijo_dcho);
cout<<raiz->datos;
}
}