You are on page 1of 10

Universidad Nacional Mayor de San Marcos

Facultad de Ingeniería de Sistemas e Informática

P
Escuela Académico Profesional de Ingeniería de Sistemas

Práctica Nº4

Guía 4:
Algoritmos voraces, Conjuntos y dispersión

Curso: Algorítmica III, Grupo 3

Alumno: PACHECO GARAY, Juan Grisel

Código: 09200030

Profesor: Augusto Cortez Vásquez

Ciudad Universitaria, 10 de Mayo del 2011


1. Una aplicación de los algoritmos voraces lo constituye el algoritmo de Kruskal y algoritmo de Prim. Investigue de
qué trata cada uno de los algoritmos. De una especificación e implementación de cada uno. Proporcione un ejemplo de
su funcionalidad.

El algoritmo de Kruskal es un algoritmo de la teoría de grafos para encontrar un árbol recubridor mínimo en un grafo
conexo y ponderado. Es decir, busca un subconjunto de aristas que, formando un árbol, incluyen todos los vértices y donde el
valor total de todas las aristas del árbol es el mínimo. Si el grafo no es conexo, entonces busca un bosque expandido mínimo
(un árbol expandido mínimo para cada componente conexa). El algoritmo de Kruskal es un ejemplo de algoritmo voraz.

Funciona de la siguiente manera:


 se crea un bosque B (un conjunto de árboles), donde cada vértice del grafo es un árbol separado
 se crea un conjunto C que contenga a todas las aristas del grafo
 mientras C es no vacío
 eliminar una arista de peso mínimo de C
 si esa arista conecta dos árboles diferentes se añade al bosque, combinando los dos árboles en un solo árbol
 en caso contrario, se desecha la arista
Al acabar el algoritmo, el bosque tiene un solo componente, el cual forma un árbol de expansión mínimo del grafo.

// Devuelve la matriz de adyacencia del arbol minimo.


Funcion Kruska(Nodo raiz)
Inicio
Para i de 0 hasta cn con increment de 1
arbol[i]  vector(cn, INF)
pertenece[i]  i
Fin_Para
Entero nodoA
Entero nodoB
Entero arcos  1
Mientras(arcos < cn)
// Encontrar el arco minimo que no forma ciclo y guardar los nodos y la distancia.
Entero min  INF
Para i de 0 hasta cn con incremento de 1
Para (int j = 0; j < cn; j++)
Si(min > adyacencia[i][j] Y pertenece[i] ≠ pertenece[j]){
min = adyacencia[i][j]
nodoA = i
nodoB = j
Fin_Si
Fin_Para
Fin_Para
// Si los nodos no pertenecen al mismo arbol agrego el arco al arbol minimo.
Si(pertenece[nodoA] ≠ pertenece[nodoB])
arbol[nodoA][nodoB]  min
arbol[nodoB][nodoA]  min
// Todos los nodos del arbol del nodoB ahora pertenecen al arbol del nodoA.
Entero temp  pertenece[nodoB]
pertenece[nodoB]  pertenece[nodoA]
Para k de 0 hasta cn con incremento de 1
Si(pertenece[k]  temp)
pertenece[k]  pertenece[nodoA]
Fin_Si
Fin_Para
arcosarcos+1
Fin_Si
Fin_Mientras
Devolver arbol
Fin

El algoritmo de Prim es un algoritmo perteneciente a la teoría de los grafos para encontrar un árbol recubridor mínimo en
un grafo conexo, no dirigido y cuyas aristas están etiquetadas.
En otras palabras, el algoritmo encuentra un subconjunto de aristas que forman un árbol con todos los vértices, donde el peso
total de todas las aristas en el árbol es el mínimo posible. Si el grafo no es conexo, entonces el algoritmo encontrará el árbol
recubridor mínimo para uno de los componentes conexos que forman dicho grafo no conexo.
El algoritmo fue diseñado en 1930 por el matemático Vojtech Jarnik y luego de manera independiente por el científico
computacional Robert C. Prim en 1957 y redescubierto por Dijkstra en 1959. Por esta razón, el algoritmo es también
conocido como algoritmo DJP o algoritmo de Jarnik.
El algoritmo incrementa continuamente el tamaño de un árbol, comenzando por un vértice inicial al que se le van agregando
sucesivamente vértices cuya distancia a los anteriores es mínima. Esto significa que en cada paso, las aristas a considerar son
aquellas que inciden en vértices que ya pertenecen al árbol.
El árbol recubridor mínimo está completamente construido cuando no quedan más vértices por agregar.

// Inicializamos todos los nodos del grafo. La distancia la ponemos a infinito y el padre de cada nodo a NULL
// Encolamos, en una cola de prioridad donde la prioridad es la distancia, todas las parejas <nodo,distancia> del grafo
por cada u en V[G] hacer
distancia[u] = INFINITO
padre[u] = NULL
Añadir(cola,<u,distancia[u]>)
distancia[s]=0
mientras cola != 0 hacer
// OJO: Se entiende por mayor prioridad aquel nodo cuya distancia[u] es menor.
u = extraer_minimo(cola) //devuelve el minimo y lo elimina de la cola.
por cada v adyacente a 'u' hacer
si ((v ∈ cola) && (distancia[v] > peso(u, v)) entonces
padre[v] = u
distancia[v] = peso(u, v)
Actualizar(cola,<v,distancia[v]>)

2. Considere una competición mundial en la cual hay dos equipos A y B, que juegan un máximo de 2n -1 partidas, y en donde
el ganador es el primer equipo que consiga n victorias. Suponemos que no hay posibilidad de empate y que los resultados de
todos los partidos son independientes y que para cualquier partida dada hay una probabilidad constante P de que A gane, y
por tanto una probabilidad constante Q = 1-P de que gane B. Sea P(i,j) la probabilidad de que el equipo A gane el campeonato,
cuando todavía le falta i victorias más para conseguirlo, mientras que el equipo B necesita j victorias para ganar. Por ejemplo,
antes del primer partido del campeonato, la probabilidad de que gane el equipo A es P( n , n): ambos equipos necesitan
todavía n victorias para ganar el campeonato. Si A necesita cero victorias más, entonces lo cierto es que ya ha ganado el
campeonato y por tanto P(0, i ) = 1 con 1 <= i <=n. De manera similar, si el equipo B necesita cero victorias más, entonces ya
ha ganado el campeonato y por tanto P(i,0) = 0, con 1 <= i <=n .
Como no se puede dar una situación en la que A y B ganen todas las partidas que necesitan P(0,0) = 0. Carece de significado
dado que A gana cualquier partida con una probabilidad P y pierde con una probabilidad Q

P(i,j) = P P(i-1,j) + Q P(i,j-1) para i >=1 y j >=1

Construya un algoritmo voraz para hallar P(n,n)

4. Construya los algoritmos para manejar TADs conjuntos, con representación de listas :
a) Miembro
Funcion miembro(Entero b)
Inicio
booleano mfalso
Nodo g
g = CAB
Mientras (g NO NULO)
Si(b = g.dato)
Mtrue
Fin_Si
g  g.sgte;
Fin_Mientras
Devolver m
Fin

b) Union
Funcion union(Conjunto A,Conjunto B)
Inicio
Cadena msj ""
Nodo x
Nodo y
xCAB
yA.CAB
Si(x=NULO ˄ y=NULO)
msj" Conjuntos vacios"
Sino
msj" Union de conjuntos A y B"
Fin_Si
Mientras (x NO NULO)
B.adiciona(x.dato)
x  x.sgte
Fin_Mientras
Mientras (y NO NULO)
Si(!B.miembro(y.dato))
B.adiciona(y.dato)
Fin_Si
y = y.sgte
Fin_Mientras
Devolver msj
Fin

c) Interseccion
Funcion interseccion(Conjunto A, Conjunto B)
Inicio
String msj ""
Nodo x
Nodo y
x=CAB
y=A.CAB
Si(x=null ˄ y=null)
msj=" Conjuntos vacios"
Sino
msj=" Interseccion de conjuntos A y B"
Si
Mientras (y != null)
x=this.CAB;
while (x !=null)
Si(A.miembro(x.dato))
B.adiciona(x.dato)
Fin_Si
Fin_Mientras
x  x.sgte
Fin_Mientras
Devolver msj
Fin
d) Diferencia
Funcion diferencia(Conjunto A, Conjunto B)
Inicio
String msj ""
Conjunto C
Nodo x
Nodo y
Nodo z
YA.CAB
ZC.CAB
XCAB
Si(x=null ˄ y=null)
msj" Conjuntos vacios"
Sino
Msj" Diferencia de conjuntos A y B"
Interseccion(A,C)
xCAB
Mientras(x NO NULO)
SI( NO C.miembro(x.dato))
B.adiciona(x.dato)
Fin_Si
x = x.sgte
Fin_Mientras
Devolver msj
Fin

e) Complemento
Funcion complemento(Conjunto A, Conjunto B)
Inicio
String msj ""
Msjthis.diferencia(A,B)
return msj
Fin_Si

5. Una aplicación de algoritmos voraces lo constituye el codigo de Huffman. Investigue de que se trata.
La codificación de Huffman es una técnica para la compresión de datos, ampliamente usada y muy efectiva
Ejemplo : Fichero con 100.000 caracteres. Se sabe que aparecen 6 caracteres diferentes y la frecuencia de aparición de cada
uno de ellos es :

a b c d e f
Frecuencia 45 13 12 16 9 5
( en miles )

¿ Cómo codificar los caracteres para comprimir el espacio ocupado utilizando un código binario ?
Solución 1 : Código de longitud fija
Para 6 caracteres se necesitan 3 bits (300000 bits)

Fija 000 001 010 011 100 101

Solución 2 : Código de longitud variable en el que los más frecuentes tienen el código más corto. Restricción : ningún código
es prefijo de otro.
( 224000 bits )

Variable 0 101 100 111 1101 1100

Esta técnica de codificación se denomina código prefijo.


Codificación : Basta con concatenar el código de cada uno de los caracteres.
Ejemplo :
aabacd ≡ 0⋅0⋅101⋅0⋅100⋅111≡ 001010100111

Descodificación : Fácil porque ningún código es prefijo de otro código ⇒ NO hay ambigüedad.
Ejemplo :
101011101111011100 ≡ badadcf
¡ Es la única posibilidad !

Un árbol binario es una forma de representar el código prefijo que simplifica el proceso de descodificación :

 las hojas son los caracteres,

 el camino de la raíz a la hojas con la interpretación 0 a la izquierda y 1 a la derecha nos da el código de cada hoja.

Este sería el árbol binario de la codificación de longitud fija:

0 100 1
86 14
0 1 0

58 28 14
0 1 0 1 0 1

a:45 b:13 c:12 d:16 e:9 f:5

Y éste el de la codificación de longitud variable :


100
0 1

55
a:45 0 1

25 30

0 1 0 1

c:12 b:13 14 d:16

0 1

f:5 e:9

Dado T el árbol binario que corresponde a una codificación prefijo, es fácil averiguar el número de bits necesarios para
codificar el fichero :
Para cada carácter c diferente del alfabeto C que aparece en el fichero,
sea f(c) la frecuencia de c en la entrada,
sea dT(c)la profundidad de la hoja c en el árbol T, entonces el número de bits requeridos es :

B(T) = ∑ f(c)⋅ dT(c)


c∈ C
B(T) nos da el coste de T.

Algoritmo Greedy
Huffman inventó un algoritmo voraz que construye una codificación prefijo óptima.
Construye un árbol binario de códigos de longitud variable de manera ascendente de modo que [MIN] B(T).

Ejemplo de funcionamiento
Fase 1. : Caracteres colocados en orden creciente de frecuencia.

f:5 e:9 c:12 b:13 d:16 a:45

Fase 2. y posteriores : Fusionar hasta obtener un sólo árbol manteniendo la ordenación creciente.

c:12 b:13 d:16 a:45


14

0 1

f:5 e:9
14 d:16 25 a:45

0 1 0 1

f:5 e:9 c:12 b:13


25 30 a:45

0 1 0 1

c:12 b:13 14 d:16

0 1

f:5 e:9

a:45 55
0 1

25 30

0 1 0 1

c:12 b:13 14 d:16

0 1

f:5 e:9

Implementación del algoritmo


Se usa una cola de prioridad, Q, con clave la frecuencia lo que permite seleccionar los dos objetos de la cola con la frecuencia
más baja.
El resultado de fusionar dos objetos es un nuevo objeto cuya frecuencia es la suma de frecuencias de los dos objetos
fusionados.

función COD_HUF ( C es conj_<car,frec> )


{ Pre : C está bien construido y no es vacio }
n := C;
CO:= ordenar_crec_por_frec(C) ;
/* se ordena crecientemente por frecuencia el conjunto de caracteres de la entrada */
Q := Insertar_todos (CO);
/* la cola contiene todos los elementos */
Para i=1 hasta n-1 hacer
z:= crear_objeto();
/* elección de los candidatos */
x := izq(z) := primero(Q); Q:= avanzar(Q);
y:= der(z) := primero(Q); Q:= avanzar(Q);
frec[z] := frec[x] + frec[y];
/* actualizar solución */
Q:= insertar_ordenado ( Q, z);
fpara
{ Post : Q contiene un único elemento que es un árbol de codificación de prefijo óptimo }
dev ( primero(Q))
ffunción
Demostración de optimalidad del criterio
• Sea T un árbol binario de codificación óptimo.
• Sean b y c dos hojas hermanas en T que se encuentran a profundidad máxima.
• Sean x e y dos hojas de T tales que son los 2 caracteres del alfabeto C con la frecuencia más baja.
Vamos a ver que T, que es un árbol óptimo, se puede transformar en otro árbol T’’, también óptimo, en el que los 2 caracteres,
x e y, con la frecuencia más baja serán hojas hermanas que estarán a la máxima profundidad ⇒ El árbol que genera el
algoritmo voraz cumple exactamente esa condición.

Podemos suponer que f[b] ≤ f[c] y que f[x] ≤ f[y]. Además, se puede deducir que f[x] ≤ f[b] y f[y] ≤ f[c].
Se puede construir un nuevo árbol, T’, en el que se intercambia la posición que ocupan en T las hojas b y x.

B(T) – B(T’) =∑ f[c].dT(c) –∑ f[c].dT’(c) =


c∈C c∈C

= f[x].dT(x) + f[b].dT(b) –
f[x].dT’(x) – f[b].dT’(b) =

= f[x].dT(x) + f[b].dT(b) –
f[x].dT (b) –f[b].dT (x) =

= ( f[b] – f[x] ) . ( dT(b) – dT (x) ) ≥ 0

De forma similar, se construye el árbol T’’ intercambiando c e y.

Con este intercambio tampoco se incrementa el coste y B(T’) − B(T’’) ≥ 0.


Por tanto, B(T’’) ≤ B(T) y como T es óptimo, entonces T’’ también lo es y B(T’’) = B(T).

Y ya para acabar la demostración :


Sea T un árbol binario que representa un código prefijo óptimo para un alfabeto C. Consideremos 2 hojas hermanas, x e y, de
T y sea z el padre de ambas. Consideremos que la frecuencia de z es f[z] = f[x] + f[y].
Entonces, el árbol T’ = T–{x,y} representa un código prefijo óptimo para el alfabeto C’ = C – {x, y}∪ {z}.
Precisamente eso es lo que hace el algoritmo voraz : una vez que ha fusionado los dos caracteres de frecuencia más baja,
inserta un nuevo elemento en el alfabeto con su frecuencia y repite el proceso de seleccionar los dos elementos con frecuencia
más baja ahora para un alfabeto con un elemento menos.

6. Cuales son los elementos básicos para construir una estructura HASH

- Un vector direccionable mediante número de posición ( un array ) capaz de almacenar N elementos.

- Una función de dispersión que nos permita a partir de la clave obtener el índice donde estará el dato asociado a esa clave. Es
frecuente que existan dos claves distintas para las que la función de dispersión produzca el mismo indice.Esto se denomina
colisión, y las dos claves distintas que dieron lugar al mismo índice, se dicen sinónimas respecto a la función de dispersión
utilizada.

- Una función de resolución de colisiones.

7. Cuales son las técnicas para resolver colisiones

Encadenamiento. En lugar de buscar un agujero libre, lo que se hace es disponer de una serie de bloques como área de
desborde. Cuando se produce una colisión, el registro se sitúa en el área de desborde y mediante un puntero en el bloque
colisionado, se apunta a la dirección del bloque de desborde y la posición relativa del registro dentro del bloque. Además,
todos los registros que han colisionado en un mismo bloque se van encadenando mediante punteros.
Dispersión múltiple. Esta técnica de resolución de colisiones consiste en utilizar una segunda función de dispersión cuando
la primera ha producido una colisión. El objetivo es producir una nueva dirección que no provoque colisión. Normalmente, la
segunda función da una dirección de bloque situada en un área de desborde.
Direccionamiento abierto. Cuando se produce una colisión, el sistema hace una búsqueda lineal a partir del bloque al que
iba destinado el registro para encontrar un agujero donde insertarlo. Si se llega al final del archivo sin encontrar un agujero, se
continúa la búsqueda desde el principio.

8. Cuales son las desventajas de utilizar dispersión

Aunque la dispersión es el método de acceso directo más rápido a través del campo de dispersión, no es muy útil cuando
también se quiere acceder al archivo a través de otro campo. Ya que la mayoría de las funciones de dispersión que se utilizan
no mantienen el orden entre los registros, tampoco es útil cuando se quiere leer los registros ordenadamente.
Cuando se quiere hacer una búsqueda por un campo que no es de dispersión, resulta tan costoso como realizar la búsqueda en
un archivo desordenado. En este caso se prefiere realizar una búsqueda secuencial. Asimismo cuando se quiere eliminar un
registro, se presentan dos casos: El registro esta en el bloque, o se encuentra en una lista de desborde. Si esta en el bloque, se
elimina el registro del bloque y se remplaza por un registro de la lista de desborde. Si el registro a borrar está en la lista de
desborde, sólo hay que eliminarlo de ella. En este caso, será necesario mantener una lista enlazada de posiciones de desborde
no utilizadas. Si al actualizar un registro se modifica el campo de dispersión, es muy probable que el registro tenga que
cambiar de posición, por lo que habrá que borrarlo e insertarlo de nuevo.

9. Defina primera forma normal, segunda forma normal y tercera forma normal

10. Proponga un ejemplo en el que ilustre la primera forma normal, la segunda tercera forma normal

11. Cuales son las ventajas y desventajas de utilizar archivos


Ventajas de utilizar archivos
- Independencia de los datos respecto de los programas
- Facilidad de acceso por distintos programas en diferentes momentos
- La información almacenada es permanente
- Posee una gran capacidad de almacenamiento

12. Que funciones de dispersión conoce. Proporciones un ejemplo de cada una de ellas

Método de claves alfanuméricas


Cuando la clave es alfanumérica, es preciso primero convertirla a numérica, para ello existen varios procedimientos. Por
ejemplo asignar a cada símbolo un digito que lo identifique
Ejemplo
Dada la clave ABANO en un valor numérico mediante el empleo del código ASCII, código EBCDIC o posición en el
alfabeto
A = 065
B = 066
A = 065
N = 078
O = 079
La clave numérica equivalente es 065066065078079
Si utilizamos el código EBCDIC, en hexadecimal tendremos C1C2C1D5D6
Este código puede transformarse a su equivalente en base 10, o también se pueden considerar solo los dígitos 12156

Método de residuo
Se toma la clave y se divide por el tamaño de la tabla o del archivo, y el residuo determina la posición relativa en la tabla o el
archivo. Normalmente para disminuir el número de colisiones es conveniente usar un numero primo igual o ligeramente
menor que el tamaño de la tabla.

M Tamaño de la tabla
I= RESIDUO(CLAVE/M) + 1
Genera valores entre 0 y M-1
I Permite determinar la posición en
la tabla

Método de cuadrados
Se eleva la clave al cuadrado, se toman los números centrales y se multiplican por el factor de conversión, con el objeto de
ajustarla el tamaño de la tabla
Ejemplo
Dada la clave de un producto 8254
El factor de conversión es 0.4
8254 * 8254 =68128516
128*0.4=51.2
Luego Aleatorio(8254) es 51

Método de desfasamiento
Se Suman los números de ambos extremos de la clave sobre los números centrales. Se suman y se multiplican por un factor
de conversión. Esta técnica se puede emplear cuando la clave es bastante grande con respecto a tamaño de la tabla.
Ejemplo
Dada la clave de un producto 483259782
El factor de conversión es 0.4
259 + 483 + 782 = 524
La suma se multiplica por el factor de conversión
524* 0.4 = 209.6
Luego Aleatorio(483259782) es 209

Método del doblaje (folding)


Cuando la clave es muy grande, se divide en tres partes; los extremos se giran sobre la tercera parte central, se suman y se
multiplican por el factor de conversión
Ejemplo
Dada la clave de un producto 170863519
El factor de conversión es 0.7
863 + 915 + 071 = 849
La suma se multiplica por el factor de conversión
849* 0.9 = 764.1
Luego Aleatorio (17086335519) es 764

Método de cambio de base de los números


Es posible utilizar el cambio de base de los números para generar un numero pseudo aleatorio. Uno de los cambios de base
más empleados debido a la facilidad de conversión es expresar la clave en base 11.
Ejemplo
Dada la clave de un producto 59582
5 × 114 + 9 × 113 + 5 × 112 + 8 × 11 + 2
Este número se reduce mediante cualquiera de los métodos anteriores

Método de división de polinomios


A partir de la clave numérica se genera un polinomio P(x), este polinomio se divide por otro polinomio Q(x) previamente
establecido, el cual genera un polinomio R(x) ; los coeficientes de R(x) se multiplican por el factor de conversión para
generar la posición
Ejemplo
Dada la clave de un producto 359849 se quiere generar una posición en un archivo de 700 registros
P(x) = 3x 5 + 5 x 4+ 9 x3+ 8x2 + 4x + 9
Se divide entre un polinomio Q(x) que siempre va a ser el mismo.
Si consideramos Q(x)= 3x3+ 2x2+ x + 2, entonces R(x) = x2 + 5x
Con base al factor de conversión 0.7 la posición de almacenamiento es 73