Ejercicio 1 Ideas clave del ejercicio: (1) La posición hay que guardarla indirectamente según el puntaje y no directamente según el equipo, porque no da el tiempo de actualizar la posición cuando hay muchos equipos empatados en el mismo puntaje. (2) Cuando alguién gana un punto solo modifica la posición de sı́ mismo y de quienes estuvieran empatados con él (esto se ve claramente en la especificación basada en #masPuntos). De este modo, hay sólo dos puntajes, a lo sumo, a los que hay que actualizarles la posición correspondiente (ver algoritmo de regPartido para mas detalles). Luego, hay muchas implementaciones distintas posibles combinando diccionarios sobre AVL y arreglos ordenados. Incluso, un poco mas difı́cil, se puede usar una lista enlazada de puntajes, posiciones y cantidad de equipos apuntada con iteradores desde desde un diccionario de equipos. Aquı́ se presenta la opción mas simple de entender. Estructura: equipo, puntaje y posición son nat torneo se representa con estr donde estr es h ptos: diccAVL(equipo,puntaje), pos: diccAVL(puntaje,h p: posición, cant: nat i) i ptos dice para cada equipo su punaje. pos dice para cada puntaje que tenga algún equipo la posición en la que están los equipos con ese puntaje y cuántos son. Los diccionarios devuelven y reciben todos los parámetros por copia. Algoritmos: Puntos(in t:estr, in e:equipo) res ← Obtener(t.ptos, e) Complejidad: O(log n) Pos(in t:estr, in e:equipo) res ← Obtener(t.pos, Obtener(t.ptos, e)).p Complejidad: O(log n) regPartido(in t:estr, in g:equipo, in p:equipo) nat n ← Obtener(t.ptos, g) + 1 Definir(t.ptos, g, n) h p: posición, cant: nat i p ← Obtener(t.pos, n − 1) p.cant ← p.cant − 1 if p.cant = 0 then Borrar(t.pos, n − 1) else Definir(t.pos, n − 1, p) end if if Definido(t.pos, n) then p ← Obtener(t.pos, n) p.cant ← p.cant + 1 Definir(t.pos, n, p) else p.p ← p.p + p.cant Definir(t.pos, n, p) end if Complejidad: O(log n) Ejercicio 2 La idea es la siguiente: Considerar n arreglos de longitud k, que están ordenados por la forma de generarlos. Luego tenemos que mergearlos. Si los mergeamos de forma acumulativa (los dos primeros, el resultado con el tercero, el resultado con el cuarto, y ası́ siguiendo) la complejidad no dá. Hay 2 opciones: 1. Mergear todos a la vez. 2. Mergearlos de a parejas tipo “playoff”. Escribo completa la versión 1 porque es mas simple de entender y de justificar. Aca la hice con Heap, pero se puede hacer lo mismo con AVL. ordenarMúltiplos(in A:arreglo(nat), in k:nat) → res : arreglo(nat) arreglo(arreglo(nat)) B ← CrearArreglo(tam(A)) res ← CrearArreglo(k × tam(A)) ColaDePrioridadHeapMin(tupla(nat,nat,nat)) c ← CrearCola for i = 1 hasta tam(A) do 1 B[i] ← CrearArreglo(k) for j = 1 hasta k do B[j] ← j × A[i] end for Encolar(c, hB[i][1], i, 1i) end for for i = 1 hasta k × tam(A) do x ← Primero(c) Desencolar(c) if π3 (x) < k then Encolar(c, langleB[π2 (x)][π3 (x)], π1 (x), π2 (x)i) end if res[i] ← π1 (x) end for Complejidad O(nk log n) Justificación de complejidad (escueta): Las declaraciones y creaciones iniciales son O(nk) porque es el tamaño de los arreglos que se están creando. La primer iteración también es O(nk) porque son dos anidadas de n y k elementos repsectivamente, haciendo todas cosas constantes dentro. Es importante notar que se encolan n cosas en la cola c. Lo primero es notar que c nunca tiene mas de n cosas a lo largo de la última iteración. Esto es porque empieza con n y en cada paso se saca 1 y a lo suma se encola 1, con lo cual el total de elementos que contiene se mantiene o disminuye en todos los pasos. De este modo, cada paso de la iteración cuesta O(log n) por operaciones de costo constante o logarı́tmico en la cola (solo se encola, se pregunta el mı́nimo y se desencola el mı́nimo). Como se itera nk veces, el costo total de la iteración resulta O(nk log n), que además domina los costos calculados de la parte anterior, con lo cual la complejidad total es O(nk log n). Ejercicio 3 La idea es, supongamos que el resultado esta en A: dado un ı́ndice m de A, podemos, en O(log n) ver cual serı́a su ı́ndice en el hipotético merge. Si este es i, encontramos la respuesta, si es mayor, la respuesta está a la izquierda de m (entre los menores a m) y si es menor, la respuesta está a la derecha (entre los mayores a m). Esto nos permite hacer una busqueda binaria considerando estos tres casos. Como consideramos el caso de la igualdad, el while solo concluye sin haber retornado cuando no se encontró un resultado. Si el resultado no estaba en A, debe estar en B, ası́ que hacemos lo mismo invirtiendo los roles. En esta segunda pasada el resultado se encontrará, asi que nunca llegará una tercera pasada. iésimoMerge(in A:arreglo(nat), in B:arreglo(nat), in i:nat) → res : nat nat a ← 1, b ← tam(A) + 1 while a < b do nat m ← (a + b)/2 if m + ÚltimoMenorA(B, A[m]) = i then return A[m] else if m + ÚltimoMenorA(B, A[m]) ¡i then a←m+1 else b←m−1 end if end while return iésimoMerge(B,A,i) Complejidad: O(log2 n) Justificación de complejidad (escueta): Por el razonamiento explicado antes hay 2 pasadas del algoritmo (una sola llamada recursiva). Cada una de ellas tiene una iteración que itera a lo sumo log n veces porque el intervalo [a, b] comienza con tamaño n y su tamaño se divide por dos a cada paso. Cada iteración cuesta log n por una cantidad constante de llamados a ÚltimoMenorA y una cantidad constante de instrucciones O(1), con lo cual el costo de toda la iteración es O(log2 n). Fuera de la iteración y del llamado recursivo, todo cuesta O(1), con lo cual termina la justificación. 2