Professional Documents
Culture Documents
2022 Jun 2 S
2022 Jun 2 S
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);
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.
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:
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:
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:
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.
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.
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.
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 CircularQueueIterator(){
this.currentNode = firstNode;
n = 0;
}
public E getNext() {
E elem = this.currentNode.getValue();
this.currentNode = this.currentNode.getNext();
n++;
return elem;
}
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:
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