Download as pdf or txt
Download as pdf or txt
You are on page 1of 14

Estrategias de Programación y Estructuras de Datos.

Junio 2022, 2ª Semana

P1. Pregunta sobre la práctica (2,5 puntos)


Un polinomio sobre una variable real 𝑥𝑥 es una expresión algebraica de la forma:
𝑛𝑛

𝑃𝑃(𝑥𝑥) = � 𝑐𝑐𝑖𝑖 𝑥𝑥 𝑖𝑖 = 𝑐𝑐𝑛𝑛 𝑥𝑥 𝑛𝑛 + 𝑐𝑐𝑛𝑛−1 𝑥𝑥 𝑛𝑛−1 + ⋯ + 𝑐𝑐2 𝑥𝑥 2 + 𝑐𝑐1 𝑥𝑥 + 𝑐𝑐0


𝑖𝑖=0

donde 𝑛𝑛 ∈ ℕ es el grado del polinomio y ∀𝑖𝑖 ∈ {0,1, . . . , 𝑛𝑛}: 𝑐𝑐𝑖𝑖 ∈ ℝ son sus coeficientes.
Nótese que 𝑐𝑐0 𝑥𝑥 0 = 𝑐𝑐0 ∗ 1 = 𝑐𝑐0 .
Supongamos que queremos implementar una clase que represente estos polinomios
utilizando un array disperso como estructura de soporte.
public class Polynomial{
public SparseArrayIF<Double> polynomial;
...
}

De este modo, el par indexado (𝑖𝑖, 𝑒𝑒) representa el monomio 𝑒𝑒𝑒𝑒 𝑖𝑖 siendo 𝑖𝑖 el índice y 𝑒𝑒 el
elemento del par indexado. Se pide:

a) (1,5 puntos) Describa cómo implementar paso a paso en la clase Polynomial una
función que calcule, lo más eficientemente posible, el valor del polinomio en un
punto x dado por parámetro con el siguiente perfil:
public double evaluate(double x);

NOTA: La función Math.pow(double a, double n) calcula la n-ésima potencia del


número a, es decir 𝐚𝐚𝐧𝐧 .
b) (1 punto) Calcule el coste asintótico temporal en el caso peor del método
anterior. ¿Depende este coste de la implementación que se use de
SparseArrayIF? Justifique su respuesta.

Solución 1
a) La solución consiste en recorrer el array disperso que representa al polinomio e
ir sustituyendo en cada monomio el valor de la variable 𝑥𝑥 por el valor dado por
parámetro mientras se va acumulando el resultado. Es decir: plantear
correctamente cómo iterar el array disperso que representa al polinomio y, por
otro lado, cómo calcular el resultado.

1
Se adjunta código con fines didácticos aunque no se requiera en la solución.

1
Una manera directa de realizar el recorrido consiste en emplear el iterador de
índices e ir recuperando los coeficientes asociados a cada uno de ellos, de
manera que se sustituye el valor dado por parámetro y se va acumulando el
resultado:
public double evaluate(double x) {
/*inicializar el resultado de evaluar el polinomio en el punto
dado por parámetro */
double ev = 0.0;
/*iterador de los índices del polinomio (índices del array
disperso)*/
IteratorIF<Integer> itIndex = polynomial.indexIterator();
while(itIndex.hasNext()) {
//tomar el índice i
int index = itIndex.getNext();
//tomar el coeficiente ci
double elem = polynomial.get(index);
//calcular ci*x^i y acumularlo a ev
ev = ev+elem*Math.pow(x, index);
}
//devolver el valor de ev
return ev;
}

Sin embargo, la llamada a la función get(index) del array disperso dentro del bucle
hace que esta opción no sea la más eficiente, como veremos en el apartado b).
Otra manera consiste en utilizar los dos iteradores del array disperso (el de elementos e
índices) para evitar realizar la llamada a get dentro del bucle. Esto puede llevarse a cabo
puesto que ambos iteradores tendrán el mismo número de elementos y se recorren en
el mismo orden, de modo que coinciden índice y elemento cuando se recorren
simultáneamente.
public double evaluate2(double x) {
/*inicializar el resultado de evaluar el polinomio en el punto
dado por parámetro */
double ev = 0.0;
//iterador de los coeficientes (elementos del array dispersos)
IteratorIF<Double> itCoefs = polynomial.iterator();
/*iterador de los índices del polinomio (índices del array
disperso)*/
IteratorIF<Integer> itIndex = polynomial.indexIterator();
/*recorrer ambos iteradores ya que ambos contienen el mismo
número de elementos y se recorren en el mismo orden*/
while(itIndex.hasNext()) { //valdría también itCoefs.hasNext()
//tomar el índice i
int index = itIndex.getNext();
//tomar el coeficiente ci
double elem = itCoefs.getNext();
//calcular ci*x^i y acumularlo a ev
ev = ev+elem*Math.pow(x, index);
}
//devolver el valor de ev
return ev;
}

2
Errores comunes:
- La solución evaluate es menos eficiente que evaluate2 (ver apartado b)), de
modo que se evalúa con menos puntuación.
- Asumir que SparseArrayIF cuenta con un iterador que devuelve pares
indexados. No es correcto: SparseArrayIF cuenta con un iterador de
elementos (coeficientes en este caso) y de índices denominados iterator e
indexIterator respectivamente.
- En soluciones similares a evaluate: iterar sobre los coeficientes (iterator) en
lugar de iterar sobre los índices (indexIterator).
- Iterar mediante bucles for o while bajo la asunción de que si 𝑛𝑛 es el grado del
polinomio, entonces existen monomios de menor grado, 𝑛𝑛 − 1, 𝑛𝑛 − 2, … , 1, 0.
- Usar estructuras de datos auxiliares: tanto las vistas en la asignatura como
otras no proporcionadas por el Equipo Docente (por ej. arrays, ArrayList,
etc.).
- Calcular el resultado erróneamente.

b) En ambas soluciones, las asignaciones, la operación pow y la sentencia return


las podemos considerar operaciones básicas de coste constante 𝑂𝑂(1). Además,
la operación getNext de cualquiera de los iteradores también debería tener un
coste 𝑂𝑂(1) implementada adecuadamente.
En cuanto a la construcción de los iteradores, ya sea el de índices o el de
elementos: una implementación adecuada en SparseArraySequence puede
llevarse a cabo con coste 𝑂𝑂(1). Sin embargo, para SparseArrayBTree una
implementación adecuada tiene un coste lineal con respecto al número de
elementos del array disperso: 𝑂𝑂(𝑛𝑛) siendo 𝒏𝒏 = 𝒑𝒑𝒑𝒑𝒑𝒑𝒑𝒑𝒑𝒑𝒑𝒑𝒑𝒑𝒑𝒑𝒑𝒑𝒑𝒑. 𝒔𝒔𝒔𝒔𝒔𝒔𝒔𝒔(), puesto
que la construcción puede realizarse recorriendo todos los elementos (coste
lineal), almacenarlos en una estructura auxiliar adecuada (por ej. una cola) y
empleando operaciones de coste constante de dicha estructura auxiliar.
De acuerdo con lo dicho anteriormente, el coste de evaluate2 es 𝑂𝑂(𝑛𝑛) con
independencia de la implementación de los arrays dispersos que se utilice
(secuencia o árbol binario): creación de iteradores y bucle sobre el array
disperso. Por tanto, si la respuesta al apartado anterior se corresponde con
evaluate2, entonces la respuesta a este apartado es que el coste no depende
de la implementación que se use de SparseArrayIF y el coste es 𝑶𝑶(𝒏𝒏) por las
explicaciones detalladas anteriormente.
Sin embargo, en el caso de evaluate, el coste también depende de la operación
get dentro del bucle, que será diferente en las dos implementaciones del array
disperso pedidas en la práctica de la asignatura. En el caso de
SparseArraySequence, la operación get puede implementarse con un coste
lineal 𝑂𝑂(𝑛𝑛) dado que en el caso peor el par indexado estará ubicado en la última
posición de la estructura de secuencia empleada. Por tanto, en ese caso el coste

3
de evaluate será 𝑂𝑂(𝑛𝑛2 ). Por otro lado, si la implementación usada es
SparseArrayBTree, entonces una implementación adecuada de get no
recorre todos los elementos del árbol, sino que sigue un recorrido de acuerdo
con la codificación en binario del índice dado por parámetro. En el caso peor, el
mayor número de dígitos en binario de un índice se corresponde con la altura del
árbol, de modo que el coste de get es 𝑂𝑂(ℎ) siendo 𝒉𝒉 = 𝒃𝒃𝒃𝒃𝒃𝒃𝒃𝒃𝒃𝒃. 𝒈𝒈𝒈𝒈𝒈𝒈𝒈𝒈𝒈𝒈𝒈𝒈𝒈𝒈𝒈𝒈𝒈𝒈(),
donde 𝒃𝒃𝒕𝒕𝒕𝒕𝒕𝒕𝒕𝒕 es la estructura de árbol binario de soporte para implementar el
array disperso en SparseArrayBTree. Por tanto, en ese caso el coste de
evaluate es de 𝑂𝑂(𝑛𝑛ℎ).
En resumen: si la respuesta al apartado anterior se corresponde con evaluate,
entonces la respuesta a este apartado es que el coste sí depende de la
implementación que se use de SparseArrayIF. Por las explicaciones
anteriores, si se usa SparseArraySequence, el coste es 𝑂𝑂(𝑛𝑛2 ) y si se usa
SparseArrayBTree, el coste es 𝑂𝑂(𝑛𝑛ℎ).

Errores comunes:
- No establecer claramente el tamaño del problema.
- Establecer erróneamente el tamaño del problema, por ej. establecer como
tamaño del problema el valor x dado por parámetro.
- Si la respuesta del apartado anterior coincide con evaluate, no distinguir el
coste según la implementación de SparseArrayIF empleada.

4
Problema 1 (2 puntos). Las funciones de coste de dos algoritmos 𝐴𝐴 y 𝐵𝐵 vienen dadas
respectivamente por las siguientes recurrencias:

10 𝑠𝑠𝑠𝑠 𝑛𝑛 ≤ 2
𝑇𝑇𝐴𝐴 (𝑛𝑛) = �
4𝑇𝑇𝐴𝐴 (𝑛𝑛 − 2) + 1 𝑠𝑠𝑠𝑠 𝑛𝑛 > 2

5 𝑠𝑠𝑠𝑠 𝑛𝑛 ≤ 2
𝑇𝑇𝐵𝐵 (𝑛𝑛) = �
2𝑇𝑇𝐵𝐵 (𝑛𝑛/2) + 4𝑛𝑛 𝑠𝑠𝑠𝑠 𝑛𝑛 > 2
Se pide:
a) (0,5 puntos) Identifique los componentes de cada una de las recurrencias.
b) (1,5 puntos) Calcule el coste asintótico temporal en el caso peor de ambos
algoritmos aplicando las reglas prácticas vistas en la asignatura.
Solución
a) Los dos tipos de recurrencias vistas en la asignaturas son: reducción mediante
substracción y reducción mediante división (ver vídeo y/o el documento
resumen del Tema 3). Los esquemas de ambos tipos de recurrencias son,
respectivamente, los siguientes:

𝑐𝑐𝑏𝑏 (𝑛𝑛) 𝑠𝑠𝑠𝑠 0 ≤ 𝑛𝑛 < 𝑏𝑏


𝑇𝑇(𝑛𝑛) = �
𝑎𝑎𝑎𝑎(𝑛𝑛 − 𝑏𝑏) + 𝑐𝑐𝑛𝑛𝑛𝑛 (𝑛𝑛) 𝑠𝑠𝑠𝑠 𝑛𝑛 ≥ 𝑏𝑏
𝑐𝑐𝑏𝑏 (𝑛𝑛) 𝑠𝑠𝑠𝑠 1 ≤ 𝑛𝑛 < 𝑏𝑏
𝑇𝑇(𝑛𝑛) = �
𝑎𝑎𝑎𝑎(𝑛𝑛 𝑑𝑑𝑑𝑑𝑑𝑑 𝑏𝑏) + 𝑐𝑐𝑛𝑛𝑛𝑛 (𝑛𝑛) 𝑠𝑠𝑠𝑠 𝑛𝑛 ≥ 𝑏𝑏
Los componentes de las ecuaciones de recurrencia vistas en la asignatura son:
𝒄𝒄𝒃𝒃 (𝒏𝒏): coste del caso base
𝒄𝒄𝒏𝒏𝒏𝒏 (𝒏𝒏): coste de las operaciones no recursivas en cada llamada
𝒂𝒂: número de llamadas recursivas
𝒃𝒃: determina el tamaño de los subproblemas (𝑛𝑛 − 𝑏𝑏 para el caso de reducción
mediante substracción y 𝑛𝑛 𝑑𝑑𝑑𝑑𝑑𝑑 𝑏𝑏 para el caso de reducción mediante división)
En el caso del algoritmo 𝐴𝐴 tenemos que
𝑐𝑐𝑏𝑏 (𝑛𝑛) = 10 ∈ 𝑂𝑂(1)
𝑐𝑐𝑛𝑛𝑛𝑛 (𝑛𝑛) = 1 ∈ 𝑂𝑂(1):
𝑎𝑎 = 4
𝑏𝑏 = 2

5
En el caso del algoritmo 𝐵𝐵 tenemos que
𝑐𝑐𝑏𝑏 (𝑛𝑛) = 5 ∈ 𝑂𝑂(1)
𝑐𝑐𝑛𝑛𝑛𝑛 (𝑛𝑛) = 4𝑛𝑛 ∈ 𝑂𝑂(𝑛𝑛):
𝑎𝑎 = 2
𝑏𝑏 = 2
Errores comunes:
- Identificar el número de llamadas recursivas como 𝑎𝑎 = 1 en ambas recurrencias.
- Identificar 𝑐𝑐𝑛𝑛𝑛𝑛 (𝑛𝑛) ∈ 𝑂𝑂(1) en la recurrencia 𝑇𝑇𝐵𝐵 .

b) Una vez identificados los componentes de las recurrencias, el cálculo del coste
asintótico temporal puede realizarse aplicando los teoremas maestros:
En el caso del algoritmo 𝑨𝑨, la recurrencia encaja con el esquema de reducción mediante
substracción. El teorema maestro en este caso es el siguiente:
𝑂𝑂(𝑛𝑛 ∗ 𝑐𝑐𝑛𝑛𝑛𝑛 (𝑛𝑛) + 𝑐𝑐𝑏𝑏 (𝑛𝑛)) 𝑠𝑠𝑠𝑠 𝑎𝑎 = 1
𝑇𝑇(𝑛𝑛) ∈ � 𝑛𝑛 𝑑𝑑𝑑𝑑𝑑𝑑 𝑏𝑏
𝑂𝑂(𝑎𝑎 ∗ (𝑐𝑐𝑛𝑛𝑛𝑛 (𝑛𝑛) + 𝑐𝑐𝑏𝑏 (𝑛𝑛))) 𝑠𝑠𝑠𝑠 𝑎𝑎 > 1
Como 𝑎𝑎 = 4 > 1, sustituyendo los valores nos queda:

𝑇𝑇𝐴𝐴 (𝑛𝑛) ∈ 𝑂𝑂(4𝑛𝑛 𝑑𝑑𝑑𝑑𝑑𝑑 2 ∗ (𝑂𝑂(1) + 𝑂𝑂(1))) = 𝑂𝑂(4𝑛𝑛/2 ) = 𝑂𝑂(√4𝑛𝑛 ) = 𝑂𝑂(2𝑛𝑛 )

En el caso del algoritmo 𝑩𝑩, la recurrencia encaja con el esquema de reducción mediante
división. El teorema maestro en este caso es el siguiente:
𝑂𝑂(𝑙𝑙𝑙𝑙𝑙𝑙𝑏𝑏 (𝑛𝑛) ∗ 𝑐𝑐𝑛𝑛𝑛𝑛 (𝑛𝑛) + 𝑐𝑐𝑏𝑏 (𝑛𝑛)) 𝑠𝑠𝑠𝑠 𝑎𝑎 = 1
𝑇𝑇(𝑛𝑛) ∈ �
𝑂𝑂(𝑛𝑛𝑙𝑙𝑙𝑙𝑙𝑙𝑏𝑏(𝑎𝑎) ∗ (𝑐𝑐𝑛𝑛𝑛𝑛 (𝑛𝑛) + 𝑐𝑐𝑏𝑏 (𝑛𝑛))) 𝑠𝑠𝑠𝑠 𝑎𝑎 > 1
Como 𝑎𝑎 = 2 > 1, sustituyendo los valores nos queda:

𝑇𝑇𝐵𝐵 (𝑛𝑛) ∈ 𝑂𝑂(𝑛𝑛𝑙𝑙𝑙𝑙𝑙𝑙2(2) ∗ (𝑂𝑂(𝑛𝑛) + 𝑂𝑂(1))) = 𝑂𝑂(𝑛𝑛 ∗ 𝑂𝑂(𝑛𝑛)) = 𝑂𝑂(𝑛𝑛2 )

Errores comunes:
- No aplicar las reglas prácticas (teoremas maestros) vistas en la asignatura para
calcular los costes tal y como pide el enunciado del problema.
- Aplicación incorrecta de las reglas prácticas.

6
Problema 2 (3 puntos). Una cola circular es una estructura de datos idéntica a la cola
(QueueIF) salvo que se considera que el nodo siguiente al último elemento
(denominado trasero o rear en inglés) es el primer elemento de la cola. De este modo,
se disponen los elementos de manera circular.

Queremos implementar esta estructura a partir de la estructura de datos Sequence


empleada en la asignatura:
public class CircularQueue<E> extends Sequence<E>{
private NodeSequence rearNode;

a) (0.75 puntos) Describa cómo implementaría paso a paso la operación enqueue.


b) (0.75 puntos) Describa cómo implementaría paso a paso la operación dequeue.
c) (1,5 puntos) Justifique razonadamente si es posible utilizar directamente la
implementación del iterador de la clase Sequence para las colas circulares. En
caso negativo, describa de qué manera se podría implementar un iterador para
esta implementación de las colas circulares.
Solución 2
En primer lugar, CircularQueue hereda de Sequence, de manera que ya cuenta con
los atributos firstNode (de tipo NodeSequence) que representa el primer nodo de la
secuencia y size (puesto que Sequence hereda a su vez de Collection), de tipo
entero, que representa el tamaño de la secuencia. Además, la implementación
propuesta de CircularQueue incluye un atributo propio denominado rearNode que
representa el último nodo de la cola circular.
Nótese que los anteriores elementos son idénticos a los de la implementación de las
colas (clase Queue) salvo que el último nodo se denomina lastNode en lugar de
rearNode. En el caso de las colas circulares, se debe asegurar que la estructura cumpla
que el nodo siguiente al último nodo sea el primer nodo.

2
Se adjunta código con fines didácticos aunque no se requiera en la solución.

7
a) La operación enqueue recibe un nuevo elemento que pasará a ser el último
elemento en la cola circular.
A continuación, se muestra una posible secuencia de pasos para implementar este
método. Nótese que son similares a los de la cola, pero asegurándonos que se respeta
la disposición circular de los elementos:
1. Crear un nuevo nodo cuyo valor sea el pasado por parámetro.
2. Se comprueba si cola circular es vacía
a. Si es vacía, se asigna al primer y último nodo de la cola circular al nodo
creado en el paso anterior.
b. Si no es vacía, hay que ubicar el nuevo nodo al final de la cola respetando
la estructura. Para ello, y por este orden, primero se asigna como nodo
siguiente de rearNode al nuevo nodo para enlazarlo con el viejo último
nodo, y posteriormente se asigna como último nodo al nuevo nodo.
3. Se asigna como nodo siguiente de rearNode el primer nodo firstNode para
asegurar que la disposición circular de los elementos.
4. Se aumenta en una unidad el tamaño de la cola (atributo size).
El código correspondiente a los anteriores pasos es el siguiente:
public void enqueue(E elem) {
NodeSequence newNode = new NodeSequence(elem);

if(isEmpty()){
this.firstNode = newNode;
this.rearNode = newNode;
}
else {
this.rearNode.setNext(newNode);
this.rearNode = newNode;
}
this.rearNode.setNext(this.firstNode);
this.size++;
}

Errores comunes:
- Mantener incorrectamente la disposición circular de los elementos.
- No añadir operaciones adicionales (por ej. incrementar en una unidad el
tamaño).
- Uso innecesario de bucles.
- No respetar la estructura de datos NodeSequence. Por ej. asumir que esta
estructura tiene atributos adicionales significa tratar con una estructura
diferente, de modo que no se corresponde con las condiciones del enunciado.

8
b) La operación dequeue elimina el primer elemento de la cola circular. La
precondición de esta operación es !isEmpty() al igual que sucede con la cola.
A continuación, se muestra una posible secuencia de pasos para implementar este
método. Nótese que son pasos similares a los de la cola, pero asegurándonos que se
respeta la disposición circular de los elementos:
1. Asignar firstNode a su nodo siguiente para eliminar el primer nodo.
2. Asignar el siguiente nodo de rearNode al nuevo firstNode para asegurar la
disposición circular de los elementos.
NOTA: estos dos primeros pasos se deben realizar en este orden.
3. Decrementar en una unidad el tamaño de la cola (atributo size).
4. Si la cola resultante es vacía, asignar primer y último nodo a null.

El código correspondiente a los anteriores pasos es el siguiente:


public void dequeue() {
this.firstNode = this.firstNode.getNext();
this.rearNode.setNext(this.firstNode);
this.size--;
if(isEmpty()){
this.rearNode = null;
this.firstNode = this.rearNode;
}
}

Errores comunes:
- Mantener incorrectamente la disposición circular de los elementos.
- No añadir operaciones adicionales (por ej. decrementar en una unidad el
tamaño).
- Uso innecesario de bucles.
- No respetar la estructura de datos NodeSequence. Por ej. asumir que esta
estructura tiene atributos adicionales significa tratar con una estructura
diferente, de modo que no se corresponde con las condiciones del enunciado.

c) La operación hasNext() del iterador de las secuencias comprueba si el siguiente


nodo a recorrer es null para saber si se ha llegado al final. Esta comprobación
solamente vale para la cola circular vacía. Sin embargo, esta comprobación no
valdría en general para las colas circulares debido a que el siguiente nodo del
último nodo es el primer nodo. Por tanto, no es posible utilizar directamente la
implementación del iterador de la clase Sequence para las colas circulares, de
modo que se tendría que implementar un nuevo iterador para este tipo de
estructura de datos.

9
La manera de implementar un iterador para colas circulares pasa por mantener un
atributo NodeSequence currentNode que indique el nodo recorrido (al igual que en
el iterador de Sequence) y, además, establecer algún mecanismo que indique cuándo
se han recorrido todos los elementos de la cola circular. Esto puede realizarse de varias
maneras. Por ejemplo: añadir un atributo adicional entero n que indica cuántos
elementos de la cola circular se han recorrido. El constructor inicia currentNode al
primer nodo de la cola circular y n a 0. La operación hasNext comprueba que n sea
menor que el tamaño de la cola circular. La operación getNext es análoga a la de
Sequence (devuelve el valor del nodo actual y lo actualiza a su siguiente) pero además
aumenta el valor de n en una unidad al recorrerse un nuevo elemento. Por último, la
operación reset realiza las mismas operaciones que el constructor. Todas estas
operaciones son básicas, de modo que tienen coste 𝑂𝑂(1). El código correspondiente a
esta descripción es el siguiente:
public class CircularQueue<E> extends Sequence<E>{

public class CircularQueueIterator implements IteratorIF<E> {

protected NodeSequence currentNode;


protected int n;

protected CircularQueueIterator(){
this.currentNode = firstNode;
n = 0;
}

public E getNext() {
E elem = this.currentNode.getValue();
this.currentNode = this.currentNode.getNext();
n++;
return elem;
}

public boolean hasNext() {


return n<size;
}

public void reset() {


this.currentNode = firstNode;
n = 0;
}

public IteratorIF<E> iterator() {


return new CircularQueueIterator();
}

10
Errores comunes:
- Responder que sí es posible usar el iterador de Sequence para esta
implementación de las colas circulares.
- No justificar adecuadamente la imposibilidad de usar el iterador de Sequence
para esta implementación de las colas circulares.
- Uso innecesario de bucles.
- Si la condición de hasNext es que el nodo actual sea rearNode o
currentNode.equals(firstNode) o similar, el iterador recorre desde el
primer elemento al penúltimo y no llega a recorrer el último elemento. Por tanto,
no es correcto. Pese a ello, se ha puntuado sin alcanzar el máximo.
- Si la solución es volcar el contenido de la cola circular en otra estructura
adecuada proporcionada por el Equipo Docente (por ej. lista, cola, no vale pila)
y devolver el iterador de dicha estructura auxiliar: la solución es esencialmente
correcta, pero se ha penalizado levemente que sea menos eficiente que otras
posibles soluciones.

11
Problema 3 (2,5 puntos). Describa cómo implementar un método que recibe un árbol
general y devuelve la longitud de su rama más corta (es decir, el número de aristas
entre la raíz y la hoja más cercana a ella) con el siguiente perfil:
public int lengthOfShortestBranch(GTreeIF<E> gtree)

NOTA: no es necesario que programe el método, basta con que indique de qué
manera hacerlo paso a paso.

Solución 3
Este ejercicio se puede resolver de manera similar al cálculo de la altura del árbol (ver
función getHeight en GTree) porque en realidad hay que devolver la altura de la hoja
más cercana a la raíz. Una posible secuencia de pasos para implementar este método es
la siguiente:

1. Caso base: árbol vacío u hoja. En ambos casos no hay ramas y se devuelve 0.
2. Caso recursivo:
a. Obtener el iterador de la lista de hijos (getChildren). Se debe calcular
el mínimo de las longitudes de la rama más corta de cada hijo. Crear
variable entera que guarde ese mínimo (por ej. llamada min) inicializada
a un valor grande (por ej. Integer.MAX_VALUE) o, por ejemplo, a la altura
del árbol dado por parámetro, puesto que ese valor acota superiormente
la altura de la rama más corta. La primera opción es computacionalmente
menos costosa.
b. Para cada hijo: llamada recursiva a lengthOfShortestBranch para
determinar la longitud de su rama más corta.
i. Si el resultado de esa llamada es 0 para algún hijo, se puede
devolver directamente el valor 1 porque uno de los hijos es una
hoja.
ii. Si el resultado de esa llamada (por ej. llamado aux) no es 0, se
calcula el mínimo min = min(min, aux) para mantener la
altura de la rama más corta .
3. Si no se ha devuelto ningún resultado, entonces ningún hijo del árbol es una hoja
y se devuelve min+1 (el valor mínimo entre todas las longitudes de la rama más
corta de todos los hijos más una unidad de la raíz del árbol original)

3
Se adjunta código con fines didácticos aunque no se requiera en la solución.

12
El código correspondiente a la anterior secuencia de pasos es el siguiente:

public int longShortestBranch(GTreeIF<E> gtree) {


if (gtree.isEmpty()||gtree.isLeaf()) { return 0;}
IteratorIF<GTreeIF<E>> childIt = gtree.getChildren().iterator();
int min = Integer.MAX_VALUE;
while(childIt.hasNext()){
int aux = longShortestBranch(childIt.getNext());
if ( aux == 0 ) {return 1;}
else {
min = Math.min(min, aux);
}
}
return min+1;
}

En el caso peor, la rama más corta se sitúa lo más a la derecha posible del árbol y se
visita una vez cada nodo. Por tanto, el coste es 𝑂𝑂(𝑛𝑛), siendo 𝑛𝑛 =
𝑔𝑔𝑔𝑔𝑔𝑔𝑔𝑔𝑔𝑔. 𝑔𝑔𝑔𝑔𝑔𝑔𝑔𝑔𝑔𝑔𝑔𝑔𝑔𝑔ℎ𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖𝑖().

Errores comunes:
- Proponer solución iterativa sin describir adecuadamente de qué manera se
llevaría a cabo el recorrido.
- Proponer solución recursiva sin plantear un caso base, o caso base erróneo.
- Proponer solución recursiva con caso recursivo erróneo.
- Usar los iteradores/recorridos del árbol general, ya sea en anchura, preorden o
postorden. GTreeIF hereda los iteradores de TreeIF y dichos iteradores son
IteratorIF<E> tal y como se indica en las interfaces anexas al enunciado del
examen: es decir, recorren elementos, no árboles ni ‘nodos’. Al recorrer
elementos, no se les puede aplicar las operaciones de los árboles generales como
isLeaf u otras. Dicho de otro modo: se pierde la estructura de árbol y no es
posible realizar el cálculo pedido.
- Plantear una solución basada en calcular el mínimo de las alturas de los hijos del
primer nivel más una unidad. La razón es que la altura devuelve la longitud de la
rama más larga de los hijos, de modo que esa solución devolvería el valor mínimo
del máximo entre las longitudes de los hijos, pero la función pedida debe
devolver el valor mínimo del mínimo entre las longitudes de los hijos. Por
ejemplo, en el árbol de la Figura 1 el nodo raíz tiene tres hijos: el árbol con raíz
b, el árbol con raíz c y el árbol con raíz d. La altura de los árboles con raíces b, c
y d es 3, de modo que una solución de este tipo devolvería 3 o 4 (sumando la
arista de la raíz del árbol original). Sin embargo, el resultado para el árbol de la
Figura 1 sería 2 puesto que esa es la altura a la que se encuentra la hoja más
cercana (nodo g) a su raíz (nodo a).
- Confundir árboles generales con árboles binarios.
- Cálculo incorrecto de la rama más corta.

13
Figura 1. Ejemplo de árbol general.

14

You might also like