Ejercicio resuelto en Python con diccionarios, conjuntos y tuplas: inscripciones a talleres, duplicados, intersección y casos límite explicados.
Actualizado el
Código de Examen Python Estructuras de datos Nivel básico Código probado

La delegación de estudiantes organiza talleres de fin de semana y ha guardado las inscripciones en un fichero de texto, una por línea. Cada línea válida contiene exactamente dos palabras separadas por espacios: el nombre del taller y el identificador del alumno, por ejemplo python ana. Los nombres distinguen mayúsculas de minúsculas, de modo que Ana y ana son alumnos distintos.
El fichero no está limpio. Hay líneas vacías o formadas solo por espacios, que se descartan sin contarlas. Hay líneas con una, tres o más palabras, que se consideran ignoradas y sí se cuentan. Y hay inscripciones repetidas: si el par (taller, alumno) ya había aparecido en una línea válida anterior, la nueva línea es un duplicado, se cuenta y no aporta nada más.
Escribe un programa en Python que lea todas las líneas de la entrada estándar hasta el final y escriba en la salida estándar, por este orden: una línea taller: n por cada taller, en el orden en que aparece por primera vez, donde n es el número de alumnos distintos inscritos; después una línea TODOS: seguida de los alumnos inscritos en todos los talleres, en orden alfabético y separados por coma y espacio, o seguida de un guion - si no hay ninguno; después DUPLICADOS: d; y por último IGNORADAS: k.
Si la entrada está vacía o no contiene ninguna línea válida, no se escribe ninguna línea de taller y la línea de alumnos comunes muestra el guion. Debes usar un diccionario para agrupar por taller, conjuntos para los alumnos y tuplas para representar cada inscripción. No se permiten bibliotecas externas.
Entrada de ejemplo
python ana
python luis
sql ana
python ana
redes ana
sql luis
redes marta
linea mal formada aqui
sql anaSalida esperada
python: 2
sql: 2
redes: 2
TODOS: ana
DUPLICADOS: 2
IGNORADAS: 1- Lectura de la entrada: descarta líneas vacías, cuenta como ignoradas las que no tienen exactamente dos palabras y detecta inscripciones repetidas con un conjunto de tuplas
- 0,75
- Diccionario de conjuntos que asocia cada taller con sus alumnos distintos, respetando el orden de primera aparición
- 0,75
- Cálculo correcto de los alumnos comunes a todos los talleres, sin modificar los datos originales y con los casos vacíos resueltos
- 0,75
- Formato de salida exacto: orden de las líneas, separadores y guion cuando no hay alumnos comunes
- 0,25
Pistas
Pista 1 · ¿Cómo sé si una inscripción ya la había visto?
Representa cada inscripción como una tupla (taller, alumno) y guárdalas en un conjunto. Las tuplas de cadenas son inmutables y, por tanto, hashables: pueden ser elementos de un set. Comprobar si una tupla ya está en el conjunto cuesta, en promedio, tiempo constante.
Pista 2 · ¿Qué estructura agrupa a los alumnos por taller?
Un diccionario cuya clave es el taller y cuyo valor es un conjunto de alumnos. Desde Python 3.7 los diccionarios conservan el orden de inserción, así que recorrer items() ya devuelve los talleres en el orden de primera aparición; no necesitas una lista auxiliar.
Pista 3 · ¿Cómo obtengo los alumnos que están en todos los talleres?
Es la intersección de todos los conjuntos del diccionario. Parte de una copia del primero y ve intersecando con los demás mediante &=. Piensa antes qué devolver si el diccionario está vacío y qué ocurre si no haces la copia.
Solución
Solución explicada
La solución separa el trabajo en dos funciones puras y un main que solo lee y escribe. leer_registros recorre las líneas, filtra, detecta duplicados y construye el diccionario de conjuntos. comunes_a_todos calcula la intersección. Así cada parte se puede probar por separado y la corrección de cada apartado de la rúbrica se localiza en una función concreta.
import sys
def leer_registros(lineas):
"""Devuelve (asistentes, duplicados, ignoradas).
asistentes: dict taller -> set de alumnos, en orden de primera aparicion.
"""
registros = set() # tuplas (taller, alumno) ya vistas
asistentes = {}
duplicados = 0
ignoradas = 0
for linea in lineas:
partes = linea.split()
if not partes:
continue # linea vacia: no cuenta
if len(partes) != 2:
ignoradas += 1
continue
par = (partes[0], partes[1])
if par in registros:
duplicados += 1
continue
registros.add(par)
taller, alumno = par
if taller not in asistentes:
asistentes[taller] = set()
asistentes[taller].add(alumno)
return asistentes, duplicados, ignoradas
def comunes_a_todos(asistentes):
"""Alumnos presentes en todos los talleres (set vacio si no hay talleres)."""
if not asistentes:
return set()
conjuntos = list(asistentes.values())
comunes = set(conjuntos[0]) # copia: no tocar el original
for alumnos in conjuntos[1:]:
comunes &= alumnos
return comunes
def main():
lineas = sys.stdin.read().splitlines()
asistentes, duplicados, ignoradas = leer_registros(lineas)
for taller, alumnos in asistentes.items():
print(f"{taller}: {len(alumnos)}")
comunes = comunes_a_todos(asistentes)
if comunes:
print("TODOS: " + ", ".join(sorted(comunes)))
else:
print("TODOS: -")
print(f"DUPLICADOS: {duplicados}")
print(f"IGNORADAS: {ignoradas}")
if __name__ == "__main__":
main()main lee toda la entrada con sys.stdin.read().splitlines(). Así obtiene una lista de líneas sin el salto final y funciona igual si la última línea termina o no en \n. Con la entrada vacía la lista también queda vacía y el resto del programa no necesita ningún tratamiento especial.
En leer_registros, linea.split() sin argumentos parte por cualquier secuencia de espacios y descarta los de los extremos. Por eso una línea formada solo por espacios produce una lista vacía y se salta con continue sin contarla. Si la lista no tiene exactamente dos elementos, la línea suma uno a ignoradas. En el caso «intersección vacía y líneas inválidas», soloUnaPalabra y tres palabras aqui dan IGNORADAS: 2, mientras que la línea en blanco no se cuenta.
Cada línea válida se convierte en la tupla par. El conjunto registros recuerda todas las tuplas ya vistas. Si par ya está, se incrementa duplicados y no se hace nada más. En el ejemplo, python ana y sql ana aparecen dos veces cada una, de ahí DUPLICADOS: 2. Usar una tupla y no una lista es obligatorio: una lista es mutable, no es hashable y no puede entrar en un set.
Las inscripciones nuevas se agregan a asistentes. Si el taller no existe todavía, se crea su conjunto vacío y, como el diccionario conserva el orden de inserción, ese taller queda colocado en el lugar de su primera aparición. En el ejemplo salen python, sql y redes en ese orden, y cada uno con 2 alumnos distintos. En el caso de tres talleres con los mismos alumnos, el orden c, a, b se respeta aunque no sea alfabético.
Un matiz: el control de duplicados con registros es necesario para contarlos. Para el recuento de alumnos no haría falta, porque añadir dos veces el mismo alumno a un conjunto no cambia su tamaño. Son dos responsabilidades distintas y conviene no mezclarlas.
comunes_a_todos devuelve un conjunto vacío si no hay talleres. Si los hay, copia el primer conjunto con set(conjuntos[0]) e interseca en el sitio con los demás. En el ejemplo, {ana, luis} con {ana, luis} da {ana, luis} y, con {ana, marta}, queda {ana}. En el caso de intersección vacía, {ana, luis} con {marta} da el conjunto vacío y se imprime TODOS: -. Con un solo taller, el bucle no itera y los comunes son sus propios alumnos, como en el caso bash pedro.
Por último, sorted(comunes) convierte el conjunto, que no tiene orden, en una lista ordenada, y ", ".join la une con el separador pedido. Por eso el caso de los tres talleres imprime TODOS: x, y, z aunque los alumnos se inscribieran en otro orden.
Casos de prueba
| Caso | Entrada | Salida esperada | Salida real | Resultado |
|---|---|---|---|---|
| ejemplo del enunciado | python ana
python luis
sql ana
python ana
redes ana
sql luis
redes marta
linea mal formada aqui
sql ana | python: 2
sql: 2
redes: 2
TODOS: ana
DUPLICADOS: 2
IGNORADAS: 1 | python: 2
sql: 2
redes: 2
TODOS: ana
DUPLICADOS: 2
IGNORADAS: 1 | OK |
| entrada vacia | (vacía) | TODOS: -
DUPLICADOS: 0
IGNORADAS: 0 | TODOS: -
DUPLICADOS: 0
IGNORADAS: 0 | OK |
| un solo taller con un alumno | bash pedro | bash: 1
TODOS: pedro
DUPLICADOS: 0
IGNORADAS: 0 | bash: 1
TODOS: pedro
DUPLICADOS: 0
IGNORADAS: 0 | OK |
| interseccion vacia y lineas invalidas | uno ana
uno luis
dos marta
soloUnaPalabra
tres palabras aqui
dos marta | uno: 2
dos: 1
TODOS: -
DUPLICADOS: 1
IGNORADAS: 2 | uno: 2
dos: 1
TODOS: -
DUPLICADOS: 1
IGNORADAS: 2 | OK |
| tres talleres con los mismos alumnos | c x
a z
c y
a y
c z
a x
b z
b x
b y | c: 3
a: 3
b: 3
TODOS: x, y, z
DUPLICADOS: 0
IGNORADAS: 0 | c: 3
a: 3
b: 3
TODOS: x, y, z
DUPLICADOS: 0
IGNORADAS: 0 | OK |
Salidas reales: código compilado con Python 3.13.16 y ejecutado en un contenedor aislado el 9 de noviembre de 2026.
Complejidad
Sea L el número de líneas y T el número de talleres. La lectura hace, por cada línea, un split proporcional a su longitud y un número fijo de operaciones de conjunto y diccionario, cada una de coste constante en promedio. Por tanto leer_registros es O(L) en promedio, si consideramos acotada la longitud de las líneas.
La intersección cuesta, en el peor caso, la suma de los tamaños de los conjuntos implicados, que está acotada por el número de inscripciones distintas: O(L). Ordenar los comunes cuesta O(c log c), siendo c su número, y c nunca supera el tamaño del menor conjunto. En memoria guardamos cada inscripción distinta dos veces, una en registros y otra dentro de asistentes, además de la lista de líneas: O(L) en total.
Errores típicos
- Escribir
comunes = conjuntos[0]sin copiar:&=modifica el conjunto en el sitio y se destruyen los alumnos del primer taller dentro del diccionario. Cualquier consulta posterior a ese taller dará un resultado erróneo. - Intentar meter listas en el conjunto de inscripciones (
registros.add([taller, alumno])): Python lanzaTypeError: unhashable type: 'list'. Hay que usar tuplas. - Contar como ignoradas las líneas vacías, o usar
linea.split(' '), que con espacios dobles genera cadenas vacías y hace que una línea correcta parezca tener más de dos palabras. - Inicializar los comunes con un conjunto vacío e intersecar con todos: el resultado siempre es vacío. La intersección debe partir del primer conjunto, no del conjunto vacío.
- Imprimir el conjunto directamente (
print(comunes)) o constr: salen llaves y comillas, y el orden no está garantizado. Hay que ordenar y unir con el separador pedido. - Ordenar los talleres alfabéticamente al imprimirlos, cuando el enunciado exige el orden de primera aparición.
Variantes
Talleres por alumno: invertir el diccionario
Si se pide, para cada alumno en orden alfabético, cuántos talleres distintos ha hecho, basta construir el diccionario inverso a partir de asistentes: por_alumno = {} y, para cada taller, alumnos y cada a en alumnos, hacer por_alumno.setdefault(a, set()).add(taller). Después se recorre sorted(por_alumno) y se imprime len(por_alumno[a]). Como hay varios talleres por alumno, el valor debe ser un conjunto o un contador, nunca una única clave sobrescrita.
Alumnos que solo han hecho un taller
Se pide el conjunto de alumnos que aparecen en exactamente un taller. Una forma limpia es contar apariciones con collections.Counter sobre la concatenación de todos los conjuntos (cada alumno cuenta una vez por taller) y filtrar los que tienen recuento 1. Con conjuntos puros también se puede: se acumulan los alumnos vistos en vistos y los vistos al menos dos veces en repetidos; el resultado es vistos - repetidos.
Intersección en una sola expresión
El profesor puede pedir reescribir comunes_a_todos sin bucle explícito: return set.intersection(*asistentes.values()) if asistentes else set(). El método set.intersection llamado sobre la clase recibe el primer conjunto como self y devuelve uno nuevo, así que no hay problema de aliasing. La guarda es imprescindible, porque sin argumentos la llamada falla.
En los temarios
Agrupar datos por clave con búsquedas en tiempo constante es, en el fondo, usar una tabla hash, y ese contenido figura en el temario de Algoritmos y Estructuras de Datos del Instituto Politécnico Nacional, junto a pilas, colas, listas y árboles. El programa de Estructuras de Datos de la Facultad de Ciencias de la UNAM recorre arreglos, listas y árboles de búsqueda, las estructuras con las que se implementan diccionarios y conjuntos cuando no se dispone de los del lenguaje.
En Colombia, el curso ISIS1206 Estructuras de datos de la Universidad de los Andes incluye listas, pilas, colas, ordenamiento y colas de prioridad. En Argentina, Algoritmos y Programación I de la Universidad de Buenos Aires trabaja arrays y punteros, la base sobre la que luego se construyen estas estructuras. Si quieres seguir practicando, en evaluar expresiones con pila y recursión en Python tienes otro ejercicio resuelto de estructuras de datos en Python.
Historial de cambios
- Reescrito con la plantilla de Código de Examen: enunciado de examen, pistas, solución explicada, casos de prueba y código compilado y ejecutado; versión en inglés enlazada.
- Publicación original.
Artículo generado mediante AI.larebelion · Revisado por Kernel


