Apuntes DAM
Volver al inicio

Ejercicios resueltos de Programación

Los 62 ejercicios de Programación de la web, tema a tema: cada uno con su enunciado, los datos que necesitas (código de partida, ejemplos de entrada y salida o la base de datos) y, al final, la solución explicada.

Descargar el PDF

Fundamentos de la Programación

1. Del pseudocódigo a Java

Fácil · Fundamentos de la Programación · apuntesdam.com/subject/programacion/topic/introduccion-programacion

Traduce a Java este pseudocódigo: Leer n Si n = 0 entonces Escribir «cero» Si no Si n > 0 entonces signo ← «positivo» si no signo ← «negativo» Si n mod 2 = 0 entonces paridad ← «par» si no paridad ← «impar» Escribir signo, « », paridad Fin si Por ejemplo, para 7 muestra «positivo impar» y para -4, «negativo par».

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int n = sc.nextInt();        // TODO: traduce el pseudocódigo del enunciado    }}

Ejemplo: Positivo impar

Entrada

7

Salida esperada

positivo impar

Ejemplo: Negativo par

Entrada

-4

Salida esperada

negativo par

Ejemplo: Cero

Entrada

0

Salida esperada

cero

2. Traza del algoritmo de Euclides

Medio · Fundamentos de la Programación · apuntesdam.com/subject/programacion/topic/introduccion-programacion

El algoritmo de Euclides, de hace más de 2000 años, calcula el máximo común divisor de dos números: mientras b no sea 0, se divide a entre b, a pasa a valer b y b pasa a valer el resto. Lee a y b (enteros positivos) y muestra la traza: en cada paso, «Paso k: a = cociente · b + resto»; al final, «mcd = m en n pasos» («1 paso» en singular). Hacer trazas así es la forma de comprobar a mano que un algoritmo es correcto.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int a = sc.nextInt(), b = sc.nextInt();        // TODO: algoritmo de Euclides mostrando la traza    }}

Ejemplo: mcd(1071, 462)

Entrada

1071 462

Salida esperada

Paso 1: 1071 = 2 · 462 + 147
Paso 2: 462 = 3 · 147 + 21
Paso 3: 147 = 7 · 21 + 0
mcd = 21 en 3 pasos

Ejemplo: Divisible

Entrada

48 12

Salida esperada

Paso 1: 48 = 4 · 12 + 0
mcd = 12 en 1 paso

3. Conversor de bases sin atajos

Difícil · Fundamentos de la Programación · apuntesdam.com/subject/programacion/topic/introduccion-programacion

Los ordenadores trabajan en binario y los programadores leemos a menudo hexadecimal (colores #FF8800, direcciones de memoria). Implementa aBase(n, base), que convierte un entero no negativo a cualquier base de 2 a 16 sin usar Integer.toBinaryString ni similares: se divide sucesivamente entre la base y los restos, leídos de abajo arriba, son las cifras (10 → A, 11 → B… 15 → F). El programa lee varios números y muestra cada uno en binario, octal y hexadecimal.

Código de partida (java)
import java.util.Scanner; public class Main {    static String aBase(int n, int base) {        // TODO: divisiones sucesivas entre la base        return "";    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextInt()) {            int n = sc.nextInt();            System.out.println(n + " → binario " + aBase(n, 2) + " · octal " + aBase(n, 8) + " · hexadecimal " + aBase(n, 16));        }    }}

Ejemplo: Varios números

Entrada

10
255
0
4096

Salida esperada

10 → binario 1010 · octal 12 · hexadecimal A
255 → binario 11111111 · octal 377 · hexadecimal FF
0 → binario 0 · octal 0 · hexadecimal 0
4096 → binario 1000000000000 · octal 10000 · hexadecimal 1000

4. Simula una máquina con su propio ensamblador

Muy difícil · Fundamentos de la Programación · apuntesdam.com/subject/programacion/topic/introduccion-programacion

Un procesador repite sin parar el ciclo de captación y ejecución: lee la instrucción a la que apunta el contador de programa (PC), la ejecuta y pasa a la siguiente (o salta). Simula una máquina con un acumulador, 16 posiciones de memoria (a 0) y estas instrucciones, una por línea (las líneas vacías y los comentarios con ; se ignoran; el número de línea cuenta desde 0 sin ellas): LOADI v (acumulador = v), LOAD d (acumulador = memoria[d]), STORE d (memoria[d] = acumulador), ADD d y SUB d (suma o resta memoria[d] al acumulador), JMP l (salta a la instrucción l), JZ l (salta si el acumulador es 0), JNEG l (salta si es negativo), PRINT (muestra el acumulador) y HALT (muestra «HALT tras n instrucciones · acumulador = v» y termina). Errores: si el PC se sale del programa, «ERROR: el programa termina sin HALT»; si una instrucción no existe, «ERROR: instrucción desconocida X en la línea l»; y si se han ejecutado ya 1000 instrucciones sin terminar, «ERROR: demasiadas instrucciones (¿bucle infinito?)».

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        List<String[]> programa = new ArrayList<>();        while (sc.hasNextLine()) {            String l = sc.nextLine().trim();            if (!l.isEmpty() && !l.startsWith(";")) programa.add(l.split("\\s+"));        }        int[] memoria = new int[16];        int acumulador = 0, pc = 0, ejecutadas = 0;        // TODO: ciclo de captación y ejecución de instrucciones    }}

Ejemplo: Suma de 1 a 5

Entrada

; Suma de 1 a 5 con un bucle
LOADI 5
STORE 0      ; m[0] = contador
LOADI 0
STORE 1      ; m[1] = suma
LOADI 1
STORE 2      ; m[2] = 1 (constante)
LOAD 0       ; 6: bucle
JZ 15
LOAD 1
ADD 0
STORE 1
LOAD 0
SUB 2
STORE 0
JMP 6
LOAD 1       ; 15: fin
PRINT
HALT

Salida esperada

15
HALT tras 56 instrucciones · acumulador = 15

Ejemplo: Cuenta atrás

Entrada

LOADI 3
STORE 0
LOADI 1
STORE 1
LOAD 0
PRINT
SUB 1
STORE 0
JZ 10
JMP 4
HALT

Salida esperada

3
2
1
HALT tras 22 instrucciones · acumulador = 0

Introducción a Java

5. Desglose de segundos

Fácil · Introducción a Java · apuntesdam.com/subject/programacion/topic/introduccion-java

Lee un número de segundos y desglósalo en días, horas, minutos y segundos usando la división entera (/) y el resto (%). Muestra «N s = d d h h m min s s» y después «Formato reloj: hh:mm:ss (+d días)» («+1 día» en singular) con dos cifras en horas, minutos y segundos.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        long total = sc.nextLong();        // TODO: días, horas, minutos y segundos    }}

Ejemplo: Un día y pico

Entrada

90061

Salida esperada

90061 s = 1 d 1 h 1 min 1 s
Formato reloj: 01:01:01 (+1 día)

Ejemplo: Menos de un minuto

Entrada

45

Salida esperada

45 s = 0 d 0 h 0 min 45 s
Formato reloj: 00:00:45 (+0 días)

6. Las trampas de los tipos numéricos

Medio · Introducción a Java · apuntesdam.com/subject/programacion/topic/introduccion-java

Los tipos de Java tienen límites y reglas que causan errores muy habituales. Lee dos enteros a y b (b distinto de 0) y muestra, en este orden: «int: a * b = r» calculado con int (puede desbordarse); «long: a * b = r» convirtiendo antes a long; «división entera: a / b = r»; «división real: a / b = r» convirtiendo a double antes de dividir; «casting: (int) real = r», truncando esa división real; y «char: 'A' + k = L (código c)», donde k = |a| % 26 y L es la letra que está k posiciones después de la A.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int a = sc.nextInt(), b = sc.nextInt();        // TODO: muestra las seis líneas del enunciado    }}

Ejemplo: Sin desbordamiento

Entrada

7 2

Salida esperada

int: 7 * 2 = 14
long: 7 * 2 = 14
división entera: 7 / 2 = 3
división real: 7 / 2 = 3.5
casting: (int) 3.5 = 3
char: 'A' + 7 = H (código 72)

Ejemplo: Con desbordamiento

Entrada

100000 30000

Salida esperada

int: 100000 * 30000 = -1294967296
long: 100000 * 30000 = 3000000000
división entera: 100000 / 30000 = 3
división real: 100000 / 30000 = 3.3333333333333335
casting: (int) 3.3333333333333335 = 3
char: 'A' + 4 = E (código 69)

7. La cuota de un préstamo

Difícil · Introducción a Java · apuntesdam.com/subject/programacion/topic/introduccion-java

Lee el capital de un préstamo, el interés anual en % y los años. La cuota mensual (sistema francés) es C · i / (1 − (1 + i)^−n), con i el interés mensual (anual / 100 / 12) y n el número de meses; si el interés es 0, la cuota es C / n. Muestra «Cuota mensual: X EUR (n cuotas)», «Total pagado: T EUR · intereses: I EUR» y el desglose de las tres primeras cuotas (o menos si hay menos): en cada una, los intereses son el capital pendiente por i, lo amortizado es la cuota menos los intereses y el pendiente baja en lo amortizado: «Cuota k: intereses a · amortiza b · pendiente c». Todos los importes con dos decimales y punto decimal.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in).useLocale(Locale.US);        double capital = sc.nextDouble();      // euros        double interesAnual = sc.nextDouble(); // en %        int anios = sc.nextInt();        // TODO: cuota mensual, intereses totales y las tres primeras cuotas    }}

Ejemplo: Hipoteca

Entrada

150000 3.5 25

Salida esperada

Cuota mensual: 750.94 EUR (300 cuotas)
Total pagado: 225280.61 EUR · intereses: 75280.61 EUR
Cuota 1: intereses 437.50 · amortiza 313.44 · pendiente 149686.56
Cuota 2: intereses 436.59 · amortiza 314.35 · pendiente 149372.22
Cuota 3: intereses 435.67 · amortiza 315.27 · pendiente 149056.95

Ejemplo: Coche sin intereses

Entrada

12000 0 1

Salida esperada

Cuota mensual: 1000.00 EUR (12 cuotas)
Total pagado: 12000.00 EUR · intereses: 0.00 EUR
Cuota 1: intereses 0.00 · amortiza 1000.00 · pendiente 11000.00
Cuota 2: intereses 0.00 · amortiza 1000.00 · pendiente 10000.00
Cuota 3: intereses 0.00 · amortiza 1000.00 · pendiente 9000.00

8. Permisos de Unix con operadores de bits

Muy difícil · Introducción a Java · apuntesdam.com/subject/programacion/topic/introduccion-java

Los permisos de un fichero en Linux son 9 bits: lectura, escritura y ejecución (r = 4, w = 2, x = 1) para el usuario (bits 8-6), el grupo (5-3) y los demás (2-0); por eso se escriben en octal, como 754. La entrada empieza con los permisos en octal y sigue con órdenes «+bits quién», «-bits quién» o «=bits quién» (bits: combinación de r, w, x; quién: u, g, o o a para todos), como en chmod. Implementa simbolico(permisos), que devuelve la forma rwxr-xr--, y aplica cada orden con operadores de bits: + enciende los bits (|), - los apaga (& con ~) y = deja en ese grupo exactamente esos bits. Tras cada orden se muestra «orden quién → ooo rwx…» (en octal con tres cifras).

Código de partida (java)
import java.util.Scanner; public class Main {    static String simbolico(int permisos) {        // TODO: rwx por cada bit encendido, - por cada apagado        return "";    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int permisos = Integer.parseInt(sc.next(), 8);      // p. ej. 754 en octal        System.out.println("Inicial: " + String.format("%03o", permisos) + " " + simbolico(permisos));        while (sc.hasNext()) {            String orden = sc.next();                       // +x, -w, =r…            String quien = sc.next();                        // u, g, o o a            // TODO: aplicar la orden con operadores de bits            System.out.println(orden + " " + quien + " → " + String.format("%03o", permisos) + " " + simbolico(permisos));        }    }}

Ejemplo: Ficheros de una web

Entrada

644
+x u
-r o
=rw g
+w a

Salida esperada

Inicial: 644 rw-r--r--
+x u → 744 rwxr--r--
-r o → 740 rwxr-----
=rw g → 760 rwxrw----
+w a → 762 rwxrw--w-

Ejemplo: Script

Entrada

700
+rx a
-w u
=x o

Salida esperada

Inicial: 700 rwx------
+rx a → 755 rwxr-xr-x
-w u → 555 r-xr-xr-x
=x o → 551 r-xr-x--x

Estructuras Condicionales

9. Calificación a partir de la nota

Fácil · Estructuras Condicionales · apuntesdam.com/subject/programacion/topic/condicionales

Lee una nota entera por teclado y muestra su calificación: Suspenso (0-4), Aprobado (5), Bien (6), Notable (7-8) o Sobresaliente (9-10). Si la nota no está entre 0 y 10, muestra 'Nota no valida'.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int nota = sc.nextInt();         // TODO: muestra la calificación correspondiente        System.out.println("?");    }}

Ejemplo: Nota 4 → Suspenso

Entrada

4

Salida esperada

Suspenso

Ejemplo: Nota 6 → Bien

Entrada

6

Salida esperada

Bien

Ejemplo: Nota 9 → Sobresaliente

Entrada

9

Salida esperada

Sobresaliente

10. Encuentra el fallo: años bisiestos

Fácil · Estructuras Condicionales · apuntesdam.com/subject/programacion/topic/condicionales

El programa lee varios años y debe decir si cada uno es bisiesto: lo es si es divisible entre 4, salvo los divisibles entre 100 que no lo son entre 400 (1900 no es bisiesto; 2000 sí). Hay años que salen mal. Corrige la condición.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextInt()) {            int a = sc.nextInt();            boolean bisiesto = a % 4 == 0 || a % 100 != 0 || a % 400 == 0;            System.out.println(a + (bisiesto ? ": bisiesto" : ": no bisiesto"));        }    }}

Ejemplo: 2024, 2023, 1900 y 2000

Entrada

2024
2023
1900
2000

Salida esperada

2024: bisiesto
2023: no bisiesto
1900: no bisiesto
2000: bisiesto

11. Encuentra el fallo: la nota en palabras

Fácil · Estructuras Condicionales · apuntesdam.com/subject/programacion/topic/condicionales

El programa convierte una nota entera (0 a 10) en su calificación: 0-4 Insuficiente, 5 Suficiente, 6 Bien, 7-8 Notable, 9-10 Sobresaliente. Con un 6 escribe varias palabras en lugar de una. Corrígelo.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        int nota = new Scanner(System.in).nextInt();        switch (nota) {            case 5:                System.out.println("Suficiente");                break;            case 6:                System.out.println("Bien");            case 7:            case 8:                System.out.println("Notable");                break;            case 9:            case 10:                System.out.println("Sobresaliente");                break;            default:                System.out.println("Insuficiente");        }    }}

Ejemplo: Un 6

Entrada

6

Salida esperada

Bien

Ejemplo: Un 8

Entrada

8

Salida esperada

Notable

12. Año bisiesto

Medio · Estructuras Condicionales · apuntesdam.com/subject/programacion/topic/condicionales

Lee un año y di si es bisiesto. Un año es bisiesto si es divisible entre 4, excepto los divisibles entre 100, salvo que también lo sean entre 400. Formato: '2024 es bisiesto' o '2023 no es bisiesto'.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int anio = sc.nextInt();         boolean bisiesto = false; // TODO: calcula si es bisiesto         if (bisiesto) {            System.out.println(anio + " es bisiesto");        } else {            System.out.println(anio + " no es bisiesto");        }    }}

Ejemplo: 2024 es bisiesto

Entrada

2024

Salida esperada

2024 es bisiesto

Ejemplo: 1900 no es bisiesto (divisible entre 100)

Entrada

1900

Salida esperada

1900 no es bisiesto

13. Validar una fecha y calcular el día siguiente

Difícil · Estructuras Condicionales · apuntesdam.com/subject/programacion/topic/condicionales

Lee fechas «día mes año» (una por línea) y, para cada una, muestra «dd/mm/aaaa: no válida (el mes debe estar entre 1 y 12)», «dd/mm/aaaa: no válida (ese mes tiene N días)» o «dd/mm/aaaa → siguiente: dd/mm/aaaa», añadiendo « (año bisiesto)» si el año de la fecha lo es. Un año es bisiesto si es divisible entre 4 y no entre 100, o si es divisible entre 400 (2024 y 2000 lo son; 1900 no). Abril, junio, septiembre y noviembre tienen 30 días y febrero 28 o 29.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextInt()) {            int d = sc.nextInt(), m = sc.nextInt(), a = sc.nextInt();            // TODO: validar la fecha y calcular el día siguiente        }    }}

Ejemplo: Fechas variadas

Entrada

15 3 2025
28 2 2024
29 2 2023
31 12 1999
31 4 2025
10 13 2025

Salida esperada

15/03/2025 → siguiente: 16/03/2025
28/02/2024 → siguiente: 29/02/2024 (año bisiesto)
29/02/2023: no válida (ese mes tiene 28 días)
31/12/1999 → siguiente: 01/01/2000
31/04/2025: no válida (ese mes tiene 30 días)
10/13/2025: no válida (el mes debe estar entre 1 y 12)

14. IRPF por tramos (simplificado)

Muy difícil · Estructuras Condicionales · apuntesdam.com/subject/programacion/topic/condicionales

El IRPF es progresivo: cada tramo de la base se grava a un tipo distinto. Implementa escala(base) con la escala estatal y autonómica sumadas (simplificada): hasta 12 450 € al 19 %; de 12 450 a 20 200 al 24 %; hasta 35 200 al 30 %; hasta 60 000 al 37 %; hasta 300 000 al 45 %; y más al 47 %. Cada tramo se suma a lo acumulado en los anteriores (por ejemplo, escala(20 200) = 2 365,50 + 0,24 · 7 750). Lee el salario bruto anual, el número de hijos y la edad. Seguridad Social = 6,35 % del bruto; base = bruto − Seguridad Social − 2 000 (gastos), sin bajar de 0. Mínimo personal y familiar = 5 550, +1 150 si tiene más de 65 años, +2 400 por el primer hijo, +2 700 por el segundo, +4 000 por el tercero y +4 500 por cada uno a partir del cuarto. Cuota = escala(base) − escala(mínimo), o 0 si el mínimo no es menor que la base. Muestra «Base liquidable», «Mínimo personal y familiar», «Cuota de IRPF», «Tipo efectivo: x %» (cuota sobre el bruto) y «Neto anual: … · al mes (14 pagas): …», todo con dos decimales y punto decimal.

Código de partida (java)
import java.util.*; public class Main {    static double escala(double base) {        // TODO: impuesto por tramos de una base        return 0;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in).useLocale(Locale.US);        double bruto = sc.nextDouble();        int hijos = sc.nextInt();        int edad = sc.nextInt();        // TODO: base, mínimo personal y familiar, cuota, tipo efectivo y neto    }}

Ejemplo: Sueldo medio sin hijos

Entrada

28000 0 30

Salida esperada

Base liquidable: 24222.00
Mínimo personal y familiar: 5550.00
Cuota de IRPF: 4377.60
Tipo efectivo: 15.63 %
Neto anual: 21844.40 · al mes (14 pagas): 1560.31

Ejemplo: Dos hijos

Entrada

45000 2 42

Salida esperada

Base liquidable: 40142.50
Mínimo personal y familiar: 10650.00
Cuota de IRPF: 8530.73
Tipo efectivo: 18.96 %
Neto anual: 33611.78 · al mes (14 pagas): 2400.84

Ejemplo: Sueldo bajo

Entrada

14000 1 25

Salida esperada

Base liquidable: 11111.00
Mínimo personal y familiar: 7950.00
Cuota de IRPF: 600.59
Tipo efectivo: 4.29 %
Neto anual: 12510.41 · al mes (14 pagas): 893.60

Bucles

15. Tabla de multiplicar

Fácil · Bucles · apuntesdam.com/subject/programacion/topic/bucles

Lee un número n y muestra su tabla de multiplicar del 1 al 10 con el formato 'n x i = resultado', una línea por operación.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int n = sc.nextInt();         // TODO: muestra la tabla de multiplicar de n    }}

Ejemplo: Tabla del 7

Entrada

7

Salida esperada

7 x 1 = 7
7 x 2 = 14
7 x 3 = 21
7 x 4 = 28
7 x 5 = 35
7 x 6 = 42
7 x 7 = 49
7 x 8 = 56
7 x 9 = 63
7 x 10 = 70

16. Encuentra el fallo: la suma de 1 a n

Fácil · Bucles · apuntesdam.com/subject/programacion/topic/bucles

El programa lee n y debe mostrar la suma 1 + 2 + … + n. Para n = 4 debería salir 10, pero sale 6. Corrige el bucle.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        int n = new Scanner(System.in).nextInt();        int suma = 0;        for (int i = 1; i < n; i++) {            suma += i;        }        System.out.println("Suma: " + suma);    }}

Ejemplo: n = 4

Entrada

4

Salida esperada

Suma: 10

17. Suma de los dígitos

Medio · Bucles · apuntesdam.com/subject/programacion/topic/bucles

Lee un número entero positivo y calcula la suma de sus dígitos usando un bucle while. Formato: 'Suma: X'.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int n = sc.nextInt();        int suma = 0;         // TODO: suma los dígitos de n con un bucle while         System.out.println("Suma: " + suma);    }}

Ejemplo: 1234 → 10

Entrada

1234

Salida esperada

Suma: 10

Ejemplo: 9 → 9

Entrada

9

Salida esperada

Suma: 9

18. Encuentra el fallo: la media que pierde decimales

Medio · Bucles · apuntesdam.com/subject/programacion/topic/bucles

El programa lee números enteros hasta un 0 y muestra su media con dos decimales. Con 7, 8 y 8 debería salir 7,67 (en formato 7.67) pero sale 7.00. Corrígelo sin cambiar el tipo de los números leídos.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int suma = 0, contador = 0;        int n = sc.nextInt();        while (n != 0) {            suma += n;            contador++;            n = sc.nextInt();        }        double media = suma / contador;        System.out.printf(java.util.Locale.US, "Media: %.2f%n", media);    }}

Ejemplo: 7, 8 y 8

Entrada

7 8 8 0

Salida esperada

Media: 7.67

19. La conjetura de Collatz

Difícil · Bucles · apuntesdam.com/subject/programacion/topic/bucles

Toma un número: si es par, divídelo entre 2; si es impar, multiplícalo por 3 y súmale 1. Repite. La conjetura de Collatz dice que siempre se acaba llegando a 1, y nadie ha conseguido demostrarlo. Lee a y b (1 ≤ a ≤ b). Muestra la secuencia de a separada por « → » y después «Entre a y b: m necesita p pasos; el valor más alto alcanzado es t», donde m es el número de ese intervalo que más pasos necesita para llegar a 1 (el menor si hay empate) y t el mayor valor que aparece en cualquiera de esas secuencias. Usa long: los valores intermedios crecen mucho.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int a = sc.nextInt(), b = sc.nextInt();        // TODO: secuencia de a y número con más pasos entre a y b    }}

Ejemplo: Del 6 al 10

Entrada

6 10

Salida esperada

6 → 3 → 10 → 5 → 16 → 8 → 4 → 2 → 1
Entre 6 y 10: 9 necesita 19 pasos; el valor más alto alcanzado es 52

Ejemplo: Del 27 al 30

Entrada

27 30

Salida esperada

27 → 82 → 41 → 124 → 62 → 31 → 94 → 47 → 142 → 71 → 214 → 107 → 322 → 161 → 484 → 242 → 121 → 364 → 182 → 91 → 274 → 137 → 412 → 206 → 103 → 310 → 155 → 466 → 233 → 700 → 350 → 175 → 526 → 263 → 790 → 395 → 1186 → 593 → 1780 → 890 → 445 → 1336 → 668 → 334 → 167 → 502 → 251 → 754 → 377 → 1132 → 566 → 283 → 850 → 425 → 1276 → 638 → 319 → 958 → 479 → 1438 → 719 → 2158 → 1079 → 3238 → 1619 → 4858 → 2429 → 7288 → 3644 → 1822 → 911 → 2734 → 1367 → 4102 → 2051 → 6154 → 3077 → 9232 → 4616 → 2308 → 1154 → 577 → 1732 → 866 → 433 → 1300 → 650 → 325 → 976 → 488 → 244 → 122 → 61 → 184 → 92 → 46 → 23 → 70 → 35 → 106 → 53 → 160 → 80 → 40 → 20 → 10 → 5 → 16 → 8 → 4 → 2 → 1
Entre 27 y 30: 27 necesita 111 pasos; el valor más alto alcanzado es 9232

20. El calendario de un mes

Muy difícil · Bucles · apuntesdam.com/subject/programacion/topic/bucles

Lee un año y un mes y muestra su calendario como el de la orden cal de Linux, empezando la semana en lunes: el nombre del mes y el año; la cabecera «Lu Ma Mi Ju Vi Sá Do»; y las semanas, cada día con dos caracteres alineado a la derecha y separado por un espacio, dejando huecos de tres espacios antes del día 1 (sin espacios al final de las líneas). Termina con «N días · F de fin de semana · empieza en día». Para saber en qué día de la semana cae el día 1 usa la congruencia de Zeller: con m = mes e y = año, pero contando enero y febrero como los meses 13 y 14 del año anterior, k = y % 100 y j = y / 100, h = (1 + 13·(m+1)/5 + k + k/4 + j/4 + 5·j) % 7 da 0 = sábado, 1 = domingo, 2 = lunes… (divisiones enteras).

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int anio = sc.nextInt(), mes = sc.nextInt();        String[] nombres = {"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto",                            "Septiembre", "Octubre", "Noviembre", "Diciembre"};        // TODO: días del mes, día de la semana del día 1 y la cuadrícula    }}

Ejemplo: Octubre de 2025

Entrada

2025 10

Salida esperada

Octubre 2025
Lu Ma Mi Ju Vi Sá Do
       1  2  3  4  5
 6  7  8  9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
31 días · 8 de fin de semana · empieza en miércoles

Ejemplo: Febrero bisiesto

Entrada

2024 2

Salida esperada

Febrero 2024
Lu Ma Mi Ju Vi Sá Do
          1  2  3  4
 5  6  7  8  9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29
29 días · 8 de fin de semana · empieza en jueves

Ejemplo: Septiembre de 2025

Entrada

2025 9

Salida esperada

Septiembre 2025
Lu Ma Mi Ju Vi Sá Do
 1  2  3  4  5  6  7
 8  9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
30 días · 8 de fin de semana · empieza en lunes

Métodos

21. Números primos con una función

Medio · Métodos · apuntesdam.com/subject/programacion/topic/metodos

Completa la función esPrimo(int n) y úsala para mostrar, separados por espacios, todos los números primos desde 2 hasta el número leído.

Código de partida (java)
import java.util.Scanner; public class Main {     static boolean esPrimo(int n) {        // TODO: devuelve true si n es primo        return false;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int limite = sc.nextInt();        StringBuilder sb = new StringBuilder();        for (int i = 2; i <= limite; i++) {            if (esPrimo(i)) sb.append(i).append(" ");        }        System.out.println(sb.toString().trim());    }}

Ejemplo: Primos hasta 10

Entrada

10

Salida esperada

2 3 5 7

Ejemplo: Primos hasta 20

Entrada

20

Salida esperada

2 3 5 7 11 13 17 19

22. Factorial recursivo

Medio · Métodos · apuntesdam.com/subject/programacion/topic/metodos

Implementa la función recursiva factorial(int n) que devuelva n! como long. El programa muestra 'n! = resultado'. Recuerda que 0! = 1.

Código de partida (java)
import java.util.Scanner; public class Main {     static long factorial(int n) {        // TODO: implementa el factorial de forma recursiva        return 0;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int n = sc.nextInt();        System.out.println(n + "! = " + factorial(n));    }}

Ejemplo: 5! = 120

Entrada

5

Salida esperada

5! = 120

Ejemplo: 0! = 1

Entrada

0

Salida esperada

0! = 1

23. Encuentra el fallo: el método que no cambia nada

Medio · Métodos · apuntesdam.com/subject/programacion/topic/metodos

El método aplicarDescuento debería rebajar el precio un porcentaje, pero el programa sigue mostrando el precio original. Corrige el método y su llamada para que el precio cambie.

Código de partida (java)
public class Main {    static void aplicarDescuento(double precio, int porcentaje) {        precio = precio - precio * porcentaje / 100;    }     public static void main(String[] args) {        double precio = 50;        aplicarDescuento(precio, 20);        System.out.println("Precio final: " + precio);    }}

Ejemplo: 50 € con un 20 %

Salida esperada

Precio final: 40.0

24. Las torres de Hanói

Difícil · Métodos · apuntesdam.com/subject/programacion/topic/metodos

Hay tres postes (A, B y C) y n discos de distinto tamaño apilados en A, del mayor abajo al menor arriba. Hay que pasarlos todos a C moviendo un disco cada vez y sin poner nunca uno grande encima de uno pequeño. La solución recursiva es sorprendentemente corta: para mover n discos de origen a destino, se mueven los n−1 de encima al poste auxiliar, se mueve el disco n al destino y se vuelven a mover los n−1 encima de él. Completa hanoi para que cuente los movimientos y, si mostrar es cierto (n ≤ 4), escriba cada uno como «Disco k: X → Y». El main ya muestra el total y lo compara con 2^n − 1.

Código de partida (java)
import java.util.Scanner; public class Main {    static int movimientos = 0;     static void hanoi(int n, char origen, char destino, char auxiliar, boolean mostrar) {        // TODO: mover n discos de origen a destino usando auxiliar    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int n = sc.nextInt();        hanoi(n, 'A', 'C', 'B', n <= 4);        System.out.println(n + (n == 1 ? " disco: " : " discos: ") + movimientos + (movimientos == 1 ? " movimiento" : " movimientos") + " (2^" + n + " - 1 = " + ((1L << n) - 1) + ")");    }}

Ejemplo: Tres discos

Entrada

3

Salida esperada

Disco 1: A → C
Disco 2: A → B
Disco 1: C → B
Disco 3: A → C
Disco 1: B → A
Disco 2: B → C
Disco 1: A → C
3 discos: 7 movimientos (2^3 - 1 = 7)

Ejemplo: Un disco

Entrada

1

Salida esperada

Disco 1: A → C
1 disco: 1 movimiento (2^1 - 1 = 1)

25. Una calculadora con descenso recursivo

Muy difícil · Métodos · apuntesdam.com/subject/programacion/topic/metodos

Los compiladores leen las expresiones con analizadores recursivos. Implementa uno para expresiones de enteros (long) con +, −, *, / (división entera), % , paréntesis y el signo menos delante de un número o de un paréntesis, respetando la prioridad: primero paréntesis y signo, después * / %, y por último + −, de izquierda a derecha. Se usa una gramática con tres métodos que se llaman entre sí: expresion() lee términos separados por + o −; termino() lee factores separados por *, / o %; factor() lee un número, un − seguido de otro factor o una expresión entre paréntesis (llamando otra vez a expresion: ahí está la recursividad). texto es la línea sin espacios y pos la posición actual. Los errores se lanzan con throw new ErrorExpresion(mensaje): «división entre cero», «falta «)»», «la expresión termina antes de tiempo» y «se esperaba un número en la posición p». El main ya muestra «expresión = resultado» o «expresión → error: mensaje».

Código de partida (java)
import java.util.Scanner; public class Main {    static String texto;    static int pos;     static class ErrorExpresion extends RuntimeException {        ErrorExpresion(String mensaje) { super(mensaje); }    }     static long expresion() {        // TODO: términos separados por + o -        return 0;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String linea = sc.nextLine();            if (linea.isBlank()) continue;            texto = linea.replace(" ", "");            pos = 0;            try {                long valor = expresion();                if (pos < texto.length()) throw new ErrorExpresion("sobra «" + texto.charAt(pos) + "» en la posición " + pos);                System.out.println(linea.trim() + " = " + valor);            } catch (ErrorExpresion e) {                System.out.println(linea.trim() + " → error: " + e.getMessage());            }        }    }}

Ejemplo: Expresiones variadas

Entrada

2 + 3 * 4
(2 + 3) * 4
100 / 7 % 3
-(8 - 10) * -3
2 * (3 + (4 - 1) * 5) - 6
7 / (3 - 3)
(1 + 2
4 + * 5

Salida esperada

2 + 3 * 4 = 14
(2 + 3) * 4 = 20
100 / 7 % 3 = 2
-(8 - 10) * -3 = -6
2 * (3 + (4 - 1) * 5) - 6 = 30
7 / (3 - 3) → error: división entre cero
(1 + 2 → error: falta «)»
4 + * 5 → error: se esperaba un número en la posición 2

Arrays

26. Máximo y mínimo de un array

Fácil · Arrays · apuntesdam.com/subject/programacion/topic/arrays

Dado un array de enteros, encuentra el valor máximo, el valor mínimo y las posiciones donde se encuentran. Imprime cada resultado en una línea separada con el formato: 'Maximo: X (posicion Y)' y 'Minimo: X (posicion Y)'.

Código de partida (java)
public class Main {    public static void main(String[] args) {        int[] numeros = {34, 12, 78, 5, 90, 23, 67, 45};         // TODO: encuentra el máximo y su posición        int max = numeros[0];        int posMax = 0;        // Tu código aquí         // TODO: encuentra el mínimo y su posición        int min = numeros[0];        int posMin = 0;        // Tu código aquí         System.out.println("Maximo: " + max + " (posicion " + posMax + ")");        System.out.println("Minimo: " + min + " (posicion " + posMin + ")");    }}

Ejemplo: Array {34,12,78,5,90,23,67,45} → máximo 90 en pos 4, mínimo 5 en pos 3

Salida esperada

Maximo: 90 (posicion 4)
Minimo: 5 (posicion 3)

27. Encuentra el fallo: el máximo de temperaturas bajo cero

Fácil · Arrays · apuntesdam.com/subject/programacion/topic/arrays

El programa lee varias temperaturas y muestra la más alta. Funciona en verano, pero en invierno, con todas bajo cero, dice que la máxima es 0. Corrígelo.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int n = sc.nextInt();        int[] t = new int[n];        for (int i = 0; i < n; i++) t[i] = sc.nextInt();        int max = 0;        for (int x : t) {            if (x > max) max = x;        }        System.out.println("Máxima: " + max);    }}

Ejemplo: Verano

Entrada

4
31 28 35 30

Salida esperada

Máxima: 35

Ejemplo: Invierno

Entrada

3
-5 -2 -9

Salida esperada

Máxima: -2

28. Invertir un array

Medio · Arrays · apuntesdam.com/subject/programacion/topic/arrays

Invierte el contenido de un array sin usar un segundo array auxiliar. El programa debe imprimir el array original y el array invertido usando Arrays.toString().

Código de partida (java)
import java.util.Arrays; public class Main {    public static void main(String[] args) {        int[] array = {1, 2, 3, 4, 5, 6, 7, 8};         System.out.println("Original:  " + Arrays.toString(array));         // TODO: invierte el array en su lugar (sin array auxiliar)        // Tu código aquí         System.out.println("Invertido: " + Arrays.toString(array));    }}

Ejemplo: Invertir {1,2,3,4,5,6,7,8} → {8,7,6,5,4,3,2,1}

Salida esperada

Original:  [1, 2, 3, 4, 5, 6, 7, 8]
Invertido: [8, 7, 6, 5, 4, 3, 2, 1]

29. Estadísticas completas de notas

Medio · Arrays · apuntesdam.com/subject/programacion/topic/arrays

Dado un array de notas (valores entre 0 y 10), calcula y muestra: la media con dos decimales, cuántos alumnos han aprobado (nota >= 5), cuántos han suspendido, la nota más alta y la nota más baja.

Código de partida (java)
public class Main {    public static void main(String[] args) {        double[] notas = {8.5, 4.0, 6.5, 9.0, 3.5, 7.0, 5.0, 8.0, 2.5, 6.0};         // TODO: calcula todas las estadísticas en un solo recorrido        double suma = 0;        int aprobados = 0;        int suspensos = 0;        double maxima = notas[0];        double minima = notas[0];         // Tu bucle aquí         double media = suma / notas.length;         System.out.printf("Media: %.2f%n", media);        System.out.println("Aprobados: " + aprobados);        System.out.println("Suspensos: " + suspensos);        System.out.printf("Nota maxima: %.1f%n", maxima);        System.out.printf("Nota minima: %.1f%n", minima);    }}

Ejemplo: Estadísticas correctas para {8.5,4.0,6.5,9.0,3.5,7.0,5.0,8.0,2.5,6.0}

Salida esperada

Media: 6.00
Aprobados: 7
Suspensos: 3
Nota maxima: 9.0
Nota minima: 2.5

30. Suma de filas y columnas de una matriz

Difícil · Arrays · apuntesdam.com/subject/programacion/topic/arrays

Dada una matriz de enteros 3x3, calcula e imprime la suma de cada fila y la suma de cada columna. Formato: 'Fila 0: X', 'Fila 1: Y', 'Fila 2: Z', 'Col 0: A', 'Col 1: B', 'Col 2: C'.

Código de partida (java)
public class Main {    public static void main(String[] args) {        int[][] matriz = {            {1, 2, 3},            {4, 5, 6},            {7, 8, 9}        };         // TODO: imprime la suma de cada fila        // Fila 0: 6, Fila 1: 15, Fila 2: 24        for (int i = 0; i < matriz.length; i++) {            int sumaFila = 0;            // Tu código aquí            System.out.println("Fila " + i + ": " + sumaFila);        }         // TODO: imprime la suma de cada columna        // Col 0: 12, Col 1: 15, Col 2: 18        for (int j = 0; j < matriz[0].length; j++) {            int sumaCol = 0;            // Tu código aquí            System.out.println("Col " + j + ": " + sumaCol);        }    }}

Ejemplo: Sumas de {{1,2,3},{4,5,6},{7,8,9}}

Salida esperada

Fila 0: 6
Fila 1: 15
Fila 2: 24
Col 0: 12
Col 1: 15
Col 2: 18

31. El juego de la vida de Conway

Muy difícil · Arrays · apuntesdam.com/subject/programacion/topic/arrays

En una cuadrícula, cada célula está viva (#) o muerta (.). En cada generación, todas cambian a la vez según sus 8 vecinas: una viva sigue viva si tiene 2 o 3 vecinas vivas (si no, muere); una muerta nace si tiene exactamente 3. Fuera de la cuadrícula no hay células. La entrada es el número de generaciones y las filas de la cuadrícula inicial. Implementa siguiente(t), que devuelve una matriz nueva con la siguiente generación, y simula: si una generación es igual a la anterior, muestra «Estable desde la generación g» (g es la última que cambió, contando la inicial como 0) y para. Al final muestra la cuadrícula y «Células vivas: n».

Código de partida (java)
import java.util.*; public class Main {    static boolean[][] siguiente(boolean[][] t) {        // TODO: calcular la siguiente generación        return t;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int generaciones = sc.nextInt();        List<String> filas = new ArrayList<>();        while (sc.hasNext()) filas.add(sc.next());        boolean[][] t = new boolean[filas.size()][filas.get(0).length()];        for (int i = 0; i < t.length; i++)            for (int j = 0; j < t[0].length; j++) t[i][j] = filas.get(i).charAt(j) == '#';        // TODO: avanzar las generaciones, detectar si se estabiliza y mostrar el resultado    }}

Ejemplo: Planeador

Entrada

4
......
..#...
...#..
.###..
......
......

Salida esperada

......
......
...#..
....#.
..###.
......
Células vivas: 5

Ejemplo: Parpadeador

Entrada

3
.....
..#..
..#..
..#..
.....

Salida esperada

.....
.....
.###.
.....
.....
Células vivas: 3

Ejemplo: Bloque estable

Entrada

10
....
.##.
.##.
....

Salida esperada

Estable desde la generación 0
....
.##.
.##.
....
Células vivas: 4

Strings

32. Contar vocales

Fácil · Strings · apuntesdam.com/subject/programacion/topic/strings

Lee una frase y cuenta cuántas vocales (a, e, i, o, u, en mayúscula o minúscula) contiene. Formato: 'Vocales: N'.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String frase = sc.nextLine();        int vocales = 0;         // TODO: cuenta las vocales de la frase         System.out.println("Vocales: " + vocales);    }}

Ejemplo: 'Hola Mundo'

Entrada

Hola Mundo

Salida esperada

Vocales: 4

Ejemplo: 'programacion'

Entrada

programacion

Salida esperada

Vocales: 5

33. Encuentra el fallo: la contraseña que nunca es correcta

Fácil · Strings · apuntesdam.com/subject/programacion/topic/strings

El programa lee una contraseña y debe decir «Acceso concedido» si es «java2025». Aunque se escriba bien, siempre dice «Acceso denegado». Corrígelo.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        String clave = new Scanner(System.in).nextLine();        if (clave == "java2025") {            System.out.println("Acceso concedido");        } else {            System.out.println("Acceso denegado");        }    }}

Ejemplo: Contraseña correcta

Entrada

java2025

Salida esperada

Acceso concedido

Ejemplo: Contraseña incorrecta

Entrada

java2024

Salida esperada

Acceso denegado

34. Palíndromos

Medio · Strings · apuntesdam.com/subject/programacion/topic/strings

Lee una frase y di si es palíndroma ignorando espacios y mayúsculas. Muestra 'Es palindromo' o 'No es palindromo'.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String frase = sc.nextLine();         boolean palindromo = false; // TODO         System.out.println(palindromo ? "Es palindromo" : "No es palindromo");    }}

Ejemplo: 'Anita lava la tina'

Entrada

Anita lava la tina

Salida esperada

Es palindromo

Ejemplo: 'Java'

Entrada

Java

Salida esperada

No es palindromo

35. Encuentra el fallo: el palíndromo exigente

Medio · Strings · apuntesdam.com/subject/programacion/topic/strings

Una frase es palíndroma si se lee igual al revés sin contar mayúsculas ni espacios. El programa dice que «Anita lava la tina» no lo es. Corrígelo para que ignore mayúsculas y espacios.

Código de partida (java)
import java.util.Scanner; public class Main {    public static void main(String[] args) {        String frase = new Scanner(System.in).nextLine();        String limpia = frase;        String inversa = new StringBuilder(limpia).reverse().toString();        System.out.println(limpia.equals(inversa) ? "Es palíndromo" : "No es palíndromo");    }}

Ejemplo: Anita lava la tina

Entrada

Anita lava la tina

Salida esperada

Es palíndromo

Ejemplo: No palíndromo

Entrada

Hola mundo

Salida esperada

No es palíndromo

36. El cifrado de Vigenère

Difícil · Strings · apuntesdam.com/subject/programacion/topic/strings

El cifrado de Vigenère (siglo XVI) desplaza cada letra tantas posiciones como indica la letra de una clave que se repite: con la clave LIMON, la primera letra se desplaza 11 (L), la segunda 8 (I), etc. La entrada es «cifrar» o «descifrar», la clave (solo letras) y el texto. Implementa vigenere: las letras del alfabeto inglés (A-Z, a-z) se desplazan respetando mayúsculas y minúsculas y dando la vuelta al llegar a la Z; cualquier otro carácter (espacios, signos, ñ, letras con tilde) se copia sin cambios y no consume letra de la clave. Para descifrar se desplaza en sentido contrario.

Código de partida (java)
import java.util.Scanner; public class Main {    static String vigenere(String texto, String clave, boolean cifrar) {        // TODO: desplazar cada letra según la letra de la clave que le toca        return texto;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String modo = sc.nextLine().trim(), clave = sc.nextLine().trim(), texto = sc.nextLine();        System.out.println(vigenere(texto, clave, modo.equals("cifrar")));    }}

Ejemplo: Cifrar

Entrada

cifrar
LIMON
Ataque al amanecer

Salida esperada

Lbmehp ix ozlvqqrc

Ejemplo: Descifrar

Entrada

descifrar
LIMON
Lbmehp ix ozlvqqrc

Salida esperada

Ataque al amanecer

37. Justificar un texto

Muy difícil · Strings · apuntesdam.com/subject/programacion/topic/strings

Los procesadores de textos justifican los párrafos: reparten los espacios para que todas las líneas ocupen el mismo ancho. La primera línea de la entrada es el ancho y el resto, el texto (que puede venir en varias líneas). Reparte las palabras en líneas metiendo en cada una todas las que quepan con un espacio entre ellas. Justifica cada línea añadiendo espacios entre palabras hasta llegar al ancho; si los espacios no se reparten por igual, los primeros huecos reciben uno más. La última línea y las que solo tienen una palabra van alineadas a la izquierda. Muestra cada línea entre barras «|…|» (rellenando con espacios hasta el ancho) y al final «n líneas de ancho caracteres». Si alguna palabra es más larga que el ancho, muestra «Error: la palabra «p» no cabe en ancho caracteres» y nada más.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int ancho = Integer.parseInt(sc.nextLine().trim());        StringBuilder todo = new StringBuilder();        while (sc.hasNextLine()) todo.append(sc.nextLine()).append(' ');        String[] palabras = todo.toString().trim().split("\\s+");        // TODO: repartir en líneas y justificarlas    }}

Ejemplo: Ancho 30

Entrada

30
Un buen programa no es el que funciona, sino el que
cualquier compañero puede leer y cambiar sin miedo dentro de
seis meses.

Salida esperada

|Un  buen programa no es el que|
|funciona,    sino    el    que|
|cualquier compañero puede leer|
|y  cambiar sin miedo dentro de|
|seis meses.                   |
5 líneas de 30 caracteres

Ejemplo: Ancho 16

Entrada

16
El veloz murciélago hindú comía feliz cardillo y kiwi.

Salida esperada

|El         veloz|
|murciélago hindú|
|comía      feliz|
|cardillo y kiwi.|
4 líneas de 16 caracteres

Clases y Objetos

38. Encuentra el fallo: el alumno sin nombre

Fácil · Clases y Objetos · apuntesdam.com/subject/programacion/topic/clases

La clase Alumno guarda el nombre y la nota, pero al imprimir un alumno sale «null: 0.0». Corrige el constructor.

Código de partida (java)
public class Main {    static class Alumno {        private String nombre;        private double nota;         Alumno(String nombre, double nota) {            nombre = nombre;            nota = nota;        }         @Override        public String toString() {            return nombre + ": " + nota;        }    }     public static void main(String[] args) {        System.out.println(new Alumno("Lucía", 8.5));        System.out.println(new Alumno("Mario", 6.0));    }}

Ejemplo: Dos alumnos

Salida esperada

Lucía: 8.5
Mario: 6.0

39. Una clase Fraccion inmutable

Difícil · Clases y Objetos · apuntesdam.com/subject/programacion/topic/clases

Completa la clase Fraccion. El constructor debe lanzar IllegalArgumentException("denominador 0") si el denominador es 0, dejar el signo en el numerador (1/-2 pasa a -1/2) y simplificar con el máximo común divisor (4/8 pasa a 1/2). Los métodos sumar, restar, multiplicar y dividir devuelven una fracción nueva sin modificar las originales; dividir entre una fracción que vale 0 lanza ArithmeticException("división entre cero"). toString muestra «num/den», o solo el numerador si el denominador es 1. El main ya lee las operaciones (por ejemplo «1/2 + 1/3») y muestra los resultados o los errores.

Código de partida (java)
import java.util.Scanner; class Fraccion {    private final long num;    private final long den;     Fraccion(long num, long den) {        // TODO: rechazar denominador 0, simplificar y dejar el signo en el numerador        this.num = num;        this.den = den;    }     private static long mcd(long a, long b) {        return b == 0 ? Math.abs(a) : mcd(b, a % b);    }     Fraccion sumar(Fraccion o) {        // TODO        return this;    }     Fraccion restar(Fraccion o) {        // TODO        return this;    }     Fraccion multiplicar(Fraccion o) {        // TODO        return this;    }     Fraccion dividir(Fraccion o) {        // TODO: lanzar ArithmeticException("división entre cero") si o vale 0        return this;    }     @Override    public String toString() {        // TODO: si el denominador es 1, solo el numerador        return num + "/" + den;    }} public class Main {    static Fraccion leer(String s) {        String[] p = s.split("/");        return new Fraccion(Long.parseLong(p[0]), p.length > 1 ? Long.parseLong(p[1]) : 1);    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int n = Integer.parseInt(sc.nextLine().trim());        for (int i = 0; i < n; i++) {            String linea = sc.nextLine().trim();            String[] t = linea.split(" ");            try {                Fraccion a = leer(t[0]), b = leer(t[2]);                Fraccion r = switch (t[1]) {                    case "+" -> a.sumar(b);                    case "-" -> a.restar(b);                    case "*" -> a.multiplicar(b);                    default -> a.dividir(b);                };                System.out.println(linea + " = " + r);            } catch (ArithmeticException | IllegalArgumentException e) {                System.out.println(linea + " → Error: " + e.getMessage());            }        }    }}

Ejemplo: Las cuatro operaciones

Entrada

4
1/2 + 1/3
3/4 - 1/4
2/3 * 9/4
1/2 / 3/4

Salida esperada

1/2 + 1/3 = 5/6
3/4 - 1/4 = 1/2
2/3 * 9/4 = 3/2
1/2 / 3/4 = 2/3

Ejemplo: Signos y enteros

Entrada

4
1/-2 + 1/2
-3/-6 * 4
5/3 - 2/3
7 / -14

Salida esperada

1/-2 + 1/2 = 0
-3/-6 * 4 = 2
5/3 - 2/3 = 1
7 / -14 = -1/2

40. Máquina expendedora

Muy difícil · Clases y Objetos · apuntesdam.com/subject/programacion/topic/clases

Completa la clase Maquina (los precios y las monedas van en céntimos). insertar acepta monedas de 5, 10, 20, 50, 100 y 200 y responde «Crédito: 1,50 €»; cualquier otra da «Moneda no válida: 3». comprar comprueba, en este orden, que el producto existe («No existe el producto X9»), que queda stock («Zumo agotado») y que el crédito llega («Saldo insuficiente: faltan 0,40 €»). Después calcula el cambio de mayor a menor moneda, usando las monedas de la caja más las que se acaban de insertar; si así no se puede dar exacto responde «No hay cambio: inserte el importe exacto» y no vende. Si vende: «Sale Agua. Cambio: 1×0,10 €» (o «Cambio: nada»), la caja se actualiza y el crédito vuelve a 0. devolver entrega las monedas insertadas («Devuelto: 1×2,00 €, 1×0,50 €» o «Devuelto: nada»). estado lista cada producto con su stock y el total de la caja. Usa el método euros que ya tienes para dar formato.

Código de partida (java)
import java.util.*; class Producto {    final String codigo, nombre;    final int precio;            // en céntimos    int stock;     Producto(String codigo, String nombre, int precio, int stock) {        this.codigo = codigo;        this.nombre = nombre;        this.precio = precio;        this.stock = stock;    }} class Maquina {    static final int[] VALORES = {200, 100, 50, 20, 10, 5};    private final Map<String, Producto> productos = new LinkedHashMap<>();    private final Map<Integer, Integer> monedas = new TreeMap<>(Comparator.reverseOrder());    private final List<Integer> insertadas = new ArrayList<>();     Maquina() {        for (int v : VALORES) monedas.put(v, 0);    }     static String euros(int centimos) {        return centimos / 100 + "," + String.format("%02d", centimos % 100) + " €";    }     void cargarMonedas(int valor, int cantidad) {        monedas.merge(valor, cantidad, Integer::sum);    }     void añadirProducto(Producto p) {        productos.put(p.codigo, p);    }     String insertar(int valor) {        // TODO        return "";    }     String comprar(String codigo) {        // TODO        return "";    }     String devolver() {        // TODO        return "";    }     String estado() {        // TODO        return "";    }} public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Maquina m = new Maquina();        while (sc.hasNextLine()) {            String[] t = sc.nextLine().trim().split(" ");            switch (t[0]) {                case "MONEDAS" -> m.cargarMonedas(Integer.parseInt(t[1]), Integer.parseInt(t[2]));                case "PRODUCTO" -> m.añadirProducto(new Producto(t[1], t[2], Integer.parseInt(t[3]), Integer.parseInt(t[4])));                case "INSERTAR" -> System.out.println(m.insertar(Integer.parseInt(t[1])));                case "COMPRAR" -> System.out.println(m.comprar(t[1]));                case "DEVOLVER" -> System.out.println(m.devolver());                case "ESTADO" -> System.out.print(m.estado());                default -> { }            }        }    }}

Ejemplo: Compra con cambio

Entrada

MONEDAS 100 2
MONEDAS 50 1
MONEDAS 20 3
MONEDAS 10 2
PRODUCTO A1 Agua 90 2
PRODUCTO B2 Galletas 135 1
PRODUCTO C3 Zumo 160 0
INSERTAR 100
COMPRAR A1
ESTADO

Salida esperada

Crédito: 1,00 €
Sale Agua. Cambio: 1×0,10 €
A1 Agua 0,90 € (1)
B2 Galletas 1,35 € (1)
C3 Zumo 1,60 € (0)
Caja: 4,20 €

Ejemplo: Errores y devolución

Entrada

MONEDAS 100 2
MONEDAS 50 1
MONEDAS 20 3
MONEDAS 10 2
PRODUCTO A1 Agua 90 2
PRODUCTO B2 Galletas 135 1
PRODUCTO C3 Zumo 160 0
INSERTAR 3
INSERTAR 50
COMPRAR B2
COMPRAR C3
COMPRAR X9
INSERTAR 200
DEVOLVER
DEVOLVER

Salida esperada

Moneda no válida: 3
Crédito: 0,50 €
Saldo insuficiente: faltan 0,85 €
Zumo agotado
No existe el producto X9
Crédito: 2,50 €
Devuelto: 1×2,00 €, 1×0,50 €
Devuelto: nada

Herencia

41. Cuentas bancarias con herencia

Difícil · Herencia · apuntesdam.com/subject/programacion/topic/herencia

La clase abstracta Cuenta ya guarda el IBAN y el saldo y sabe ingresar. Completa sus dos subclases. CuentaCorriente permite quedarse en descubierto hasta −300 € (si una retirada lo supera lanza IllegalStateException("supera el descubierto")) y al cierre de mes cobra 2 € de comisión si el saldo es negativo. CuentaAhorro no admite descubierto (IllegalStateException("saldo insuficiente")), permite como mucho 3 retiradas al mes (IllegalStateException("máximo de retiradas del mes")) y al cierre suma un 0,5 % de interés y reinicia el contador de retiradas. En las dos, un importe menor o igual que 0 lanza IllegalArgumentException("importe no válido"). El main ya procesa las órdenes y trata las cuentas de forma polimórfica.

Código de partida (java)
import java.util.*; abstract class Cuenta {    private final String iban;    protected double saldo;     Cuenta(String iban, double saldoInicial) {        this.iban = iban;        this.saldo = saldoInicial;    }     String getIban() { return iban; }    double getSaldo() { return saldo; }     void ingresar(double importe) {        if (importe <= 0) throw new IllegalArgumentException("importe no válido");        saldo += importe;    }     // Cada tipo de cuenta decide si se puede retirar    abstract void retirar(double importe);     // Se llama al final de cada mes    abstract void cierreMes();     abstract String tipo();     @Override    public String toString() {        return String.format(Locale.US, "%s %s: %.2f", tipo(), iban, saldo);    }} class CuentaCorriente extends Cuenta {    // TODO: descubierto hasta -300; comisión de 2 € al cierre si el saldo es negativo    CuentaCorriente(String iban, double saldo) { super(iban, saldo); }    void retirar(double importe) { }    void cierreMes() { }    String tipo() { return "Corriente"; }} class CuentaAhorro extends Cuenta {    // TODO: sin descubierto, máximo 3 retiradas al mes, 0,5 % de interés al cierre    CuentaAhorro(String iban, double saldo) { super(iban, saldo); }    void retirar(double importe) { }    void cierreMes() { }    String tipo() { return "Ahorro"; }} public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Map<String, Cuenta> cuentas = new LinkedHashMap<>();        while (sc.hasNextLine()) {            String[] t = sc.nextLine().trim().split(" ");            try {                switch (t[0]) {                    case "CORRIENTE" -> cuentas.put(t[1], new CuentaCorriente(t[1], Double.parseDouble(t[2])));                    case "AHORRO" -> cuentas.put(t[1], new CuentaAhorro(t[1], Double.parseDouble(t[2])));                    case "INGRESAR" -> cuentas.get(t[1]).ingresar(Double.parseDouble(t[2]));                    case "RETIRAR" -> cuentas.get(t[1]).retirar(Double.parseDouble(t[2]));                    case "MES" -> {                        for (Cuenta c : cuentas.values()) c.cierreMes();                        System.out.println("-- cierre de mes --");                    }                    case "VER" -> cuentas.values().forEach(System.out::println);                }            } catch (IllegalArgumentException | IllegalStateException e) {                System.out.println(String.join(" ", t) + " → " + e.getMessage());            }        }    }}

Ejemplo: Descubierto y comisión

Entrada

CORRIENTE ES01 100
RETIRAR ES01 350
RETIRAR ES01 100
MES
VER

Salida esperada

RETIRAR ES01 100 → supera el descubierto
-- cierre de mes --
Corriente ES01: -252.00

Ejemplo: Límite de retiradas e interés

Entrada

AHORRO ES02 1000
RETIRAR ES02 100
RETIRAR ES02 100
RETIRAR ES02 100
RETIRAR ES02 100
MES
RETIRAR ES02 50
VER

Salida esperada

RETIRAR ES02 100 → máximo de retiradas del mes
-- cierre de mes --
Ahorro ES02: 653.50

42. Encuentra el fallo: el producto que no está en la lista

Difícil · Herencia · apuntesdam.com/subject/programacion/topic/herencia

La clase Producto define equals para que dos productos con el mismo código sean iguales, pero lista.contains(...) dice que no encuentra uno que sí está. Corrige equals (y lo que haga falta) para que contains funcione.

Código de partida (java)
import java.util.*; public class Main {    static class Producto {        final String codigo;        final String nombre;        Producto(String codigo, String nombre) { this.codigo = codigo; this.nombre = nombre; }         public boolean equals(Producto otro) {            return codigo.equals(otro.codigo);        }    }     public static void main(String[] args) {        List<Producto> lista = new ArrayList<>(List.of(new Producto("A1", "Teclado"), new Producto("B2", "Ratón")));        System.out.println("¿Está B2? " + lista.contains(new Producto("B2", "Ratón inalámbrico")));        System.out.println("¿Está C3? " + lista.contains(new Producto("C3", "Monitor")));    }}

Ejemplo: Buscar por código

Salida esperada

¿Está B2? true
¿Está C3? false

43. Expresiones matemáticas con polimorfismo

Muy difícil · Herencia · apuntesdam.com/subject/programacion/topic/herencia

Una expresión como (2 · x) + 3 se puede representar como un árbol de objetos: una Suma cuyo operando izquierdo es un Producto de Num(2) y Var(x), y cuyo derecho es Num(3). Num, Var y la clase abstracta Binaria (que ya sabe escribirse entre paréntesis) están hechas. Completa Suma, Resta y Producto con tres métodos: evaluar (con los valores de las variables), derivar respecto de una variable (la suma y la resta derivan cada operando; el producto usa la regla (a·b)' = a'·b + a·b') y simplificar, que primero simplifica los operandos y luego aplica estas reglas: dos números se operan; 0 + e y e + 0 dan e; e − 0 da e; e − e (mismo texto) da 0; 0 · e y e · 0 dan 0; 1 · e y e · 1 dan e. La entrada es la expresión en notación prefija y los valores de las variables.

Código de partida (java)
import java.util.*; abstract class Expr {    abstract double evaluar(Map<String, Double> vars);    abstract Expr derivar(String v);    Expr simplificar() { return this; }     static String num(double d) {        return d == Math.floor(d) && !Double.isInfinite(d) ? String.valueOf((long) d) : String.valueOf(d);    }} class Num extends Expr {    final double valor;    Num(double valor) { this.valor = valor; }    double evaluar(Map<String, Double> vars) { return valor; }    Expr derivar(String v) { return new Num(0); }    public String toString() { return num(valor); }} class Var extends Expr {    final String nombre;    Var(String nombre) { this.nombre = nombre; }    double evaluar(Map<String, Double> vars) { return vars.get(nombre); }    Expr derivar(String v) { return new Num(nombre.equals(v) ? 1 : 0); }    public String toString() { return nombre; }} // Clase base de las operaciones con dos operandosabstract class Binaria extends Expr {    final Expr izq, der;    Binaria(Expr izq, Expr der) { this.izq = izq; this.der = der; }    abstract String simbolo();    public String toString() { return "(" + izq + " " + simbolo() + " " + der + ")"; }} class Suma extends Binaria {    Suma(Expr a, Expr b) { super(a, b); }    String simbolo() { return "+"; }    // TODO: evaluar, derivar y simplificar    double evaluar(Map<String, Double> vars) { return 0; }    Expr derivar(String v) { return this; }} class Resta extends Binaria {    Resta(Expr a, Expr b) { super(a, b); }    String simbolo() { return "-"; }    // TODO: evaluar, derivar y simplificar    double evaluar(Map<String, Double> vars) { return 0; }    Expr derivar(String v) { return this; }} class Producto extends Binaria {    Producto(Expr a, Expr b) { super(a, b); }    String simbolo() { return "*"; }    // TODO: evaluar, derivar y simplificar    double evaluar(Map<String, Double> vars) { return 0; }    Expr derivar(String v) { return this; }} public class Main {    // Lee una expresión en notación prefija: "+ * 2 x 3" es (2 * x) + 3    static Expr leer(Iterator<String> it) {        String t = it.next();        return switch (t) {            case "+" -> new Suma(leer(it), leer(it));            case "-" -> new Resta(leer(it), leer(it));            case "*" -> new Producto(leer(it), leer(it));            default -> Character.isLetter(t.charAt(0)) ? new Var(t) : new Num(Double.parseDouble(t));        };    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Expr e = leer(Arrays.asList(sc.nextLine().trim().split(" ")).iterator());        Map<String, Double> vars = new TreeMap<>();        while (sc.hasNext()) vars.put(sc.next(), Double.parseDouble(sc.next()));        System.out.println("Expresión: " + e);        System.out.println("Simplificada: " + e.simplificar());        System.out.println("Valor: " + Expr.num(e.evaluar(vars)));        Expr d = e.derivar("x");        System.out.println("Derivada respecto de x: " + d);        System.out.println("Derivada simplificada: " + d.simplificar());    }}

Ejemplo: Recta

Entrada

+ * 2 x 3
x 4

Salida esperada

Expresión: ((2 * x) + 3)
Simplificada: ((2 * x) + 3)
Valor: 11
Derivada respecto de x: (((0 * x) + (2 * 1)) + 0)
Derivada simplificada: 2

Ejemplo: Parábola

Entrada

- * x x * 3 x
x 5

Salida esperada

Expresión: ((x * x) - (3 * x))
Simplificada: ((x * x) - (3 * x))
Valor: 10
Derivada respecto de x: (((1 * x) + (x * 1)) - ((0 * x) + (3 * 1)))
Derivada simplificada: ((x + x) - 3)

Interfaces

44. Clasificación de una liga con Comparator

Difícil · Interfaces · apuntesdam.com/subject/programacion/topic/interfaces

Cada línea de la entrada es un partido con el formato «Local 2-1 Visitante». Completa registrar, que actualiza las estadísticas de un equipo con los goles a favor y en contra de un partido (3 puntos por victoria y 1 por empate), y el Comparator CLASIFICACION, que ordena por puntos de mayor a menor; a igualdad de puntos, por diferencia de goles; después por goles a favor (ambos de mayor a menor) y, si todo coincide, por nombre en orden alfabético. El main ya lee los partidos y muestra la tabla.

Código de partida (java)
import java.util.*; class Equipo {    final String nombre;    int jugados, ganados, empatados, perdidos, golesFavor, golesContra;     Equipo(String nombre) { this.nombre = nombre; }     int puntos() { return ganados * 3 + empatados; }    int diferencia() { return golesFavor - golesContra; }     void registrar(int favor, int contra) {        // TODO: actualizar partidos, resultado y goles    }} public class Main {    // TODO: puntos (de más a menos), diferencia de goles, goles a favor y, por último, nombre (A-Z)    static final Comparator<Equipo> CLASIFICACION = (a, b) -> 0;     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Map<String, Equipo> equipos = new HashMap<>();        while (sc.hasNextLine()) {            String linea = sc.nextLine().trim();            if (linea.isEmpty()) continue;            // Formato: Local 2-1 Visitante            String[] t = linea.split(" ");            String[] goles = t[1].split("-");            int gl = Integer.parseInt(goles[0]), gv = Integer.parseInt(goles[1]);            equipos.computeIfAbsent(t[0], Equipo::new).registrar(gl, gv);            equipos.computeIfAbsent(t[2], Equipo::new).registrar(gv, gl);        }        List<Equipo> tabla = new ArrayList<>(equipos.values());        tabla.sort(CLASIFICACION);        System.out.println("Pos Equipo      PJ  G  E  P  GF  GC  DG Pts");        int pos = 1;        for (Equipo e : tabla)            System.out.printf("%3d %-10s %3d %2d %2d %2d %3d %3d %+3d %3d%n", pos++, e.nombre, e.jugados,                    e.ganados, e.empatados, e.perdidos, e.golesFavor, e.golesContra, e.diferencia(), e.puntos());    }}

Ejemplo: Jornada simple

Entrada

Leones 2-1 Tigres
Osos 0-0 Lobos
Tigres 3-0 Osos
Lobos 1-1 Leones

Salida esperada

Pos Equipo      PJ  G  E  P  GF  GC  DG Pts
  1 Leones       2  1  1  0   3   2  +1   4
  2 Tigres       2  1  0  1   4   2  +2   3
  3 Lobos        2  0  2  0   1   1  +0   2
  4 Osos         2  0  1  1   0   3  -3   1

Ejemplo: Desempates

Entrada

Alfa 3-0 Delta
Beta 2-0 Delta
Gamma 3-1 Delta
Epsilon 2-0 Delta

Salida esperada

Pos Equipo      PJ  G  E  P  GF  GC  DG Pts
  1 Alfa         1  1  0  0   3   0  +3   3
  2 Gamma        1  1  0  0   3   1  +2   3
  3 Beta         1  1  0  0   2   0  +2   3
  4 Epsilon      1  1  0  0   2   0  +2   3
  5 Delta        4  0  0  4   1  10  -9   0

45. Reglas de contraseñas componibles

Muy difícil · Interfaces · apuntesdam.com/subject/programacion/topic/interfaces

Regla es una interfaz funcional: su único método abstracto, comprobar, devuelve la lista de errores de un texto (vacía si lo cumple). Completa sus métodos default y static para poder combinar reglas como piezas. y(otra) devuelve una regla que exige las dos y junta los errores de ambas, en orden. o(otra) se cumple si se cumple cualquiera de las dos; si fallan ambas, da un único error con los dos mensajes unidos por « o ». minimo(n) falla con «mínimo n caracteres». contiene(clase, nombre) exige algún carácter de la clase de expresión regular [clase] y falla con «falta nombre». noContiene(palabra) falla, sin distinguir mayúsculas, con «no puede contener «palabra»». El main ya monta dos políticas con estas piezas y comprueba una contraseña por línea.

Código de partida (java)
import java.util.*; @FunctionalInterfaceinterface Regla {    // Lista de errores; vacía si el texto cumple la regla    List<String> comprobar(String s);     default Regla y(Regla otra) {        // TODO: se deben cumplir las dos; errores de ambas        return this;    }     default Regla o(Regla otra) {        // TODO: basta con que se cumpla una; si fallan las dos, un único error «e1 o e2»        return this;    }     static Regla minimo(int n) {        // TODO: «mínimo n caracteres»        return s -> List.of();    }     static Regla contiene(String clase, String nombre) {        // TODO: el texto debe tener algún carácter de la clase [clase]; si no, «falta nombre»        return s -> List.of();    }     static Regla noContiene(String palabra) {        // TODO: sin distinguir mayúsculas; si la contiene, «no puede contener «palabra»»        return s -> List.of();    }} public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String politica = sc.nextLine().trim();        Regla regla = switch (politica) {            case "BASICA" -> Regla.minimo(6).y(Regla.contiene("0-9", "un número"));            default -> Regla.minimo(10)                    .y(Regla.contiene("0-9", "un número"))                    .y(Regla.contiene("A-Z", "una mayúscula").o(Regla.contiene("^a-zA-Z0-9", "un símbolo")))                    .y(Regla.noContiene("password"))                    .y(Regla.noContiene("1234"));        };        while (sc.hasNextLine()) {            String clave = sc.nextLine();            List<String> errores = regla.comprobar(clave);            System.out.println(errores.isEmpty() ? "✓ " + clave : "✗ " + clave + ": " + String.join("; ", errores));        }    }}

Ejemplo: Política básica

Entrada

BASICA
abc
abcdef
abc123

Salida esperada

✗ abc: mínimo 6 caracteres; falta un número
✗ abcdef: falta un número
✓ abc123

Ejemplo: Política estricta

Entrada

ESTRICTA
Corto1
MiClaveSegura7
miclavesegura7
miclave-segura7
MyPassword2025

Salida esperada

✗ Corto1: mínimo 10 caracteres
✓ MiClaveSegura7
✗ miclavesegura7: falta una mayúscula o falta un símbolo
✓ miclave-segura7
✗ MyPassword2025: no puede contener «password»

Ficheros y Streams

46. Validar y resumir un CSV de ventas

Difícil · Ficheros y Streams · apuntesdam.com/subject/programacion/topic/ficheros

La entrada es un fichero CSV (separado por «;», con decimales con coma) con la cabecera fecha;producto;unidades;precio y una venta por línea. Valida cada línea y muestra sus errores con su número de línea en el fichero (la cabecera es la 1; las líneas en blanco se saltan pero cuentan): «se esperaban 4 campos y hay N», «producto vacío», «unidades no válidas («dos»)», «precio no válido («2,5O»)» o «valores negativos o cero» (unidades ≤ 0 o precio < 0). Solo el primer error de cada línea. Con las líneas correctas, acumula el importe (unidades × precio) por producto y muestra «--- N líneas correctas, M con errores ---», los productos de mayor a menor importe (a igualdad, por nombre) con formato «%-10s %9.2f» y una línea TOTAL.

Código de partida (java)
import java.io.*;import java.util.*; public class Main {    public static void main(String[] args) throws IOException {        // La entrada estándar hace de fichero: se lee igual que con un FileReader        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));        Map<String, Double> ventas = new HashMap<>();        int correctas = 0, erroneas = 0;        String cabecera = br.readLine();        String linea;        int numLinea = 1;        while ((linea = br.readLine()) != null) {            numLinea++;            if (linea.isBlank()) continue;            // TODO: validar la línea; si es correcta, acumular unidades × precio en el producto        }        // TODO: mostrar el resumen ordenado por importe (de mayor a menor)    }}

Ejemplo: Fichero correcto

Entrada

fecha;producto;unidades;precio
2025-03-01;Café;3;1,20
2025-03-01;Tostada;2;2,50
2025-03-02;Café;5;1,20
2025-03-02;Zumo;1;3

Salida esperada

--- 4 líneas correctas, 0 con errores ---
Café            9.60
Tostada         5.00
Zumo            3.00
TOTAL          17.60

Ejemplo: Con errores

Entrada

fecha;producto;unidades;precio
2025-03-01;Café;3;1,20
2025-03-01;Tostada;dos;2,50
2025-03-01;Zumo;1

2025-03-02;Café;-1;1,20
2025-03-02;;1;2
2025-03-03;Tostada;4;2,5O
2025-03-03;Tostada;4;2,50

Salida esperada

Línea 3: unidades no válidas («dos»)
Línea 4: se esperaban 4 campos y hay 3
Línea 6: valores negativos o cero
Línea 7: producto vacío
Línea 8: precio no válido («2,5O»)
--- 2 líneas correctas, 5 con errores ---
Tostada        10.00
Café            3.60
TOTAL          13.60

47. Comparar dos ficheros (diff)

Muy difícil · Ficheros y Streams · apuntesdam.com/subject/programacion/topic/ficheros

Herramientas como git diff muestran qué ha cambiado entre dos versiones de un fichero. La entrada contiene las líneas de la versión antigua, una línea «=====» y las de la nueva. Implementa diff: debe conservar el mayor número posible de líneas comunes en orden (la subsecuencia común más larga, LCS) y marcar el resto. Cada línea del resultado empieza por un espacio si está en las dos versiones, por «-» si solo está en la antigua y por «+» si solo está en la nueva. Recorre las dos versiones desde el principio: si las líneas actuales son iguales, se conservan; si no, se quita la de la antigua cuando eso no empeora la LCS (lcs[i+1][j] ≥ lcs[i][j+1]) y, si no, se añade la de la nueva. El main ya cuenta los cambios.

Código de partida (java)
import java.io.*;import java.util.*; public class Main {    // Devuelve las líneas del diff: " línea" (igual), "-línea" (solo en a), "+línea" (solo en b)    static List<String> diff(List<String> a, List<String> b) {        // TODO        return new ArrayList<>();    }     public static void main(String[] args) throws IOException {        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));        List<String> a = new ArrayList<>(), b = new ArrayList<>();        List<String> actual = a;        String linea;        while ((linea = br.readLine()) != null) {            if (linea.equals("=====")) { actual = b; continue; }            actual.add(linea);        }        List<String> d = diff(a, b);        int quitadas = 0, añadidas = 0;        for (String l : d) {            System.out.println(l);            if (l.startsWith("-")) quitadas++;            if (l.startsWith("+")) añadidas++;        }        System.out.println("@@ " + quitadas + (quitadas == 1 ? " línea quitada, " : " líneas quitadas, ")                + añadidas + (añadidas == 1 ? " añadida @@" : " añadidas @@"));    }}

Ejemplo: Un cambio

Entrada

uno
dos
tres
cuatro
=====
uno
DOS
tres
cuatro

Salida esperada

uno
-dos
+DOS
 tres
 cuatro
@@ 1 línea quitada, 1 añadida @@

Ejemplo: Código

Entrada

public class A {
    int x;
    void f() {
        x++;
    }
}
=====
public class A {
    private int x;
    void f() {
        x++;
        log();
    }
}

Salida esperada

public class A {
-    int x;
+    private int x;
     void f() {
         x++;
+        log();
     }
 }
@@ 1 línea quitada, 2 añadidas @@

Colecciones

48. Encuentra el fallo: borrar mientras se recorre

Medio · Colecciones · apuntesdam.com/subject/programacion/topic/colecciones

El programa debe quitar de la lista las tareas terminadas (las que empiezan por «x ») y mostrar las pendientes. En lugar de eso, termina con ConcurrentModificationException. Corrígelo.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        List<String> tareas = new ArrayList<>(List.of("x comprar pan", "estudiar Java", "x llamar a Ana", "x pagar luz", "hacer la práctica"));        for (String t : tareas) {            if (t.startsWith("x ")) {                tareas.remove(t);            }        }        System.out.println("Pendientes: " + tareas);    }}

Ejemplo: Tres tareas terminadas

Salida esperada

Pendientes: [estudiar Java, hacer la práctica]

49. Encuentra el fallo: el contador de palabras olvidadizo

Medio · Colecciones · apuntesdam.com/subject/programacion/topic/colecciones

El programa cuenta cuántas veces aparece cada palabra de una frase y las muestra en orden alfabético. Todas salen con 1 aunque se repitan. Corrígelo.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        String frase = new Scanner(System.in).nextLine().toLowerCase();        Map<String, Integer> contador = new TreeMap<>();        for (String p : frase.split(" ")) {            contador.put(p, 1);        }        contador.forEach((p, n) -> System.out.println(p + " " + n));    }}

Ejemplo: Frase con repeticiones

Entrada

el perro y el gato y el loro

Salida esperada

el 3
gato 1
loro 1
perro 1
y 2

50. Agrupar anagramas

Difícil · Colecciones · apuntesdam.com/subject/programacion/topic/colecciones

Dos palabras son anagramas si tienen las mismas letras en distinto orden (amor, mora, Roma, ramo). La entrada es una lista de palabras. Implementa agrupar: trabaja en minúsculas (las letras con tilde cuentan como distintas: lámina no es anagrama de animal), ignora las palabras repetidas y devuelve solo los grupos con 2 o más palabras distintas. Cada grupo va ordenado alfabéticamente y los grupos se ordenan de más a menos palabras y, a igualdad, por su primera palabra. Si no hay ninguno, el main muestra «No hay anagramas».

Código de partida (java)
import java.util.*; public class Main {    // Devuelve los grupos de anagramas (2 o más palabras), cada uno ordenado alfabéticamente    // y los grupos de más a menos palabras (a igualdad, por su primera palabra)    static List<List<String>> agrupar(List<String> palabras) {        // TODO        return new ArrayList<>();    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        List<String> palabras = new ArrayList<>();        while (sc.hasNext()) palabras.add(sc.next());        List<List<String>> grupos = agrupar(palabras);        if (grupos.isEmpty()) System.out.println("No hay anagramas");        for (List<String> g : grupos) System.out.println(g.size() + ": " + String.join(", ", g));    }}

Ejemplo: Ejemplo

Entrada

amor saco Roma cosa mora ramo casa ocas

Salida esperada

4: amor, mora, ramo, roma
3: cosa, ocas, saco

Ejemplo: Repetidas y mayúsculas

Entrada

Lámina animal Manila listen silent enlist Listen tinsel perro

Salida esperada

4: enlist, listen, silent, tinsel
2: animal, manila

51. Ruta más corta en el metro

Muy difícil · Colecciones · apuntesdam.com/subject/programacion/topic/colecciones

Cada línea «LINEA nombre e1 e2 e3…» conecta estaciones consecutivas en los dos sentidos. Cada «RUTA origen destino» pide el camino con menos paradas. Implementa ruta con una búsqueda en anchura (BFS): una cola con las estaciones por explorar y un mapa que guarda, para cada estación descubierta, desde cuál se llegó a ella. Al sacar el destino de la cola, reconstruye el camino hacia atrás siguiendo ese mapa. Las vecinas se exploran en orden alfabético (ya vienen así en red), lo que decide el camino cuando hay empate. Si no se puede llegar, devuelve null. El main muestra «Sol → Opera → Callao (2 paradas)».

Código de partida (java)
import java.util.*; public class Main {    // Estación → estaciones vecinas (en orden alfabético)    static Map<String, Set<String>> red = new TreeMap<>();     static void conectar(String a, String b) {        red.computeIfAbsent(a, k -> new TreeSet<>()).add(b);        red.computeIfAbsent(b, k -> new TreeSet<>()).add(a);    }     // Camino con menos paradas de origen a destino (incluidos los dos), o null si no hay    static List<String> ruta(String origen, String destino) {        // TODO: búsqueda en anchura        return null;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String linea = sc.nextLine().trim();            if (linea.isEmpty()) continue;            String[] t = linea.split(" ");            if (t[0].equals("LINEA")) {                for (int i = 2; i < t.length - 1; i++) conectar(t[i], t[i + 1]);            } else if (t[0].equals("RUTA")) {                if (!red.containsKey(t[1]) || !red.containsKey(t[2])) {                    System.out.println("Estación desconocida");                    continue;                }                List<String> r = ruta(t[1], t[2]);                if (r == null) System.out.println(t[1] + " → " + t[2] + ": sin conexión");                else System.out.println(String.join(" → ", r) + " (" + (r.size() - 1) + " paradas)");            }        }    }}

Ejemplo: Misma línea y transbordo

Entrada

LINEA L1 Sol Opera Callao Plaza Norte
LINEA L2 Sol Retiro Goya Este
LINEA L3 Callao Centro Goya
LINEA L4 Isla Faro
RUTA Sol Callao
RUTA Opera Este

Salida esperada

Sol → Opera → Callao (2 paradas)
Opera → Callao → Centro → Goya → Este (4 paradas)

Ejemplo: Varios caminos

Entrada

LINEA L1 Sol Opera Callao Plaza Norte
LINEA L2 Sol Retiro Goya Este
LINEA L3 Callao Centro Goya
LINEA L4 Isla Faro
RUTA Plaza Retiro
RUTA Norte Sol
RUTA Goya Goya

Salida esperada

Plaza → Callao → Centro → Goya → Retiro (4 paradas)
Norte → Plaza → Callao → Opera → Sol (4 paradas)
Goya (0 paradas)

Ejercicios largos

52. Biblioteca: préstamos, devoluciones y multas

Difícil · Java · 90 minutos · apuntesdam.com/ejercicios/java/biblioteca-prestamos-y-multas

La biblioteca de un instituto lleva los préstamos en una libreta y quiere un pequeño programa de consola. Cada línea que recibe el programa es una orden: dar de alta un libro o un socio, prestar un libro, devolverlo o pedir un informe. El programa contesta a cada orden con una línea (o con varias, en el informe).

Los días se cuentan como números enteros desde el comienzo del curso (día 0, día 1…), así que no hace falta trabajar con fechas. Un libro puede tener varios ejemplares: se puede prestar mientras quede alguno libre.

Hay tres normas: cada préstamo dura 14 días; un socio no puede tener más de 3 libros a la vez; y si devuelve un libro tarde, paga 0,50 € por cada día de retraso y no puede llevarse libros durante tantos días como se retrasó, contados desde el día en que lo devuelve.

Requisitos

  • LIBRO isbn ejemplares título da de alta un libro (el título puede tener espacios) y responde Libro añadido: título (n ejemplares), con «ejemplar» en singular si es 1. Si el ISBN ya existe: Error: ya existe un libro con el ISBN isbn.

  • SOCIO número nombre da de alta un socio y responde Socio añadido: nombre (número). Si el número ya existe: Error: ya existe un socio con el número número.

  • PRESTAR día socio isbn comprueba, en este orden: que exista el socio (Error: no existe el socio número), que exista el libro (Error: no existe el libro isbn), que el socio no esté bloqueado (Error: nombre no puede llevarse libros hasta el día X), que no tenga ya 3 préstamos (Error: nombre ya tiene 3 libros prestados), que no tenga ya ese libro (Error: nombre ya tiene «título») y que quede algún ejemplar (Error: no quedan ejemplares de «título»). Si todo va bien: Préstamo: nombre se lleva «título» hasta el día X, donde X es el día del préstamo más 14.

  • DEVOLVER día socio isbn: si ese socio no tiene prestado ese libro, Error: el socio número no tiene prestado el libro isbn. Si lo devuelve el día del vencimiento o antes, Devolución: «título» a tiempo. Si no, Devolución: «título» con N días de retraso. Multa: X,XX € («día» en singular si es 1), se suma la multa al socio y queda bloqueado hasta el día de la devolución más los días de retraso (si ya estaba bloqueado más tiempo, se queda el bloqueo más largo).

  • INFORME muestra == Informe ==, Préstamos activos: N, una línea por cada socio con préstamos, ordenados por número de socio (- nombre (número): «título 1», «título 2», con los libros en el orden en que se los llevó) y por último Multas: X,XX € con la suma de todas las multas.

  • Cualquier otra orden responde Error: orden desconocida ORDEN. Las líneas vacías se ignoran. Los importes se escriben con coma decimal y dos decimales.

Formato de la entrada

  • Una orden por línea, con las palabras separadas por un espacio. Los números de día y de socio son enteros; el ISBN es una palabra sin espacios.

  • El programa termina cuando se acaba la entrada.

Ejemplo: Préstamos, un ejemplar agotado y una devolución a tiempo

Entrada

LIBRO 978-84-376-0494-7 2 Cien años de soledad
LIBRO 978-84-204-8291-3 1 El Hobbit
SOCIO 1 Ana Ruiz
SOCIO 2 Luis Gil
PRESTAR 1 1 978-84-376-0494-7
PRESTAR 1 2 978-84-204-8291-3
PRESTAR 3 1 978-84-204-8291-3
DEVOLVER 10 2 978-84-204-8291-3
PRESTAR 11 1 978-84-204-8291-3
INFORME

Salida esperada

Libro añadido: Cien años de soledad (2 ejemplares)
Libro añadido: El Hobbit (1 ejemplar)
Socio añadido: Ana Ruiz (1)
Socio añadido: Luis Gil (2)
Préstamo: Ana Ruiz se lleva «Cien años de soledad» hasta el día 15
Préstamo: Luis Gil se lleva «El Hobbit» hasta el día 15
Error: no quedan ejemplares de «El Hobbit»
Devolución: «El Hobbit» a tiempo
Préstamo: Ana Ruiz se lleva «El Hobbit» hasta el día 25
== Informe ==
Préstamos activos: 2
- Ana Ruiz (1): «Cien años de soledad», «El Hobbit»
Multas: 0,00 €

Ejemplo: Retrasos, multas y bloqueo

Entrada

LIBRO A1 1 Dune
LIBRO B2 1 Fundación
SOCIO 7 Marta Sanz
PRESTAR 0 7 A1
DEVOLVER 20 7 A1
PRESTAR 22 7 B2
PRESTAR 26 7 B2
DEVOLVER 27 7 A1
DEVOLVER 41 7 B2
INFORME

Salida esperada

Libro añadido: Dune (1 ejemplar)
Libro añadido: Fundación (1 ejemplar)
Socio añadido: Marta Sanz (7)
Préstamo: Marta Sanz se lleva «Dune» hasta el día 14
Devolución: «Dune» con 6 días de retraso. Multa: 3,00 €
Error: Marta Sanz no puede llevarse libros hasta el día 26
Préstamo: Marta Sanz se lleva «Fundación» hasta el día 40
Error: el socio 7 no tiene prestado el libro A1
Devolución: «Fundación» con 1 día de retraso. Multa: 0,50 €
== Informe ==
Préstamos activos: 0
Multas: 3,50 €
Código de partida (java)
import java.util.*; public class Main {    static class Libro {        final String isbn;        final String titulo;        int disponibles;         Libro(String isbn, String titulo, int ejemplares) {            this.isbn = isbn;            this.titulo = titulo;            this.disponibles = ejemplares;        }    }     static class Prestamo {        final Libro libro;        final int vence;         Prestamo(Libro libro, int vence) {            this.libro = libro;            this.vence = vence;        }    }     static class Socio {        final int numero;        final String nombre;        final List<Prestamo> prestamos = new ArrayList<>();        int bloqueadoHasta = 0;        double multas = 0;         Socio(int numero, String nombre) {            this.numero = numero;            this.nombre = nombre;        }    }     static final Locale ES = Locale.forLanguageTag("es-ES");    static final Map<String, Libro> libros = new HashMap<>();    static final Map<Integer, Socio> socios = new TreeMap<>();     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String linea = sc.nextLine().trim();            if (!linea.isEmpty()) procesar(linea);        }    }     static void procesar(String linea) {        String[] p = linea.split(" ");        switch (p[0]) {            case "LIBRO" -> altaLibro(p[1], Integer.parseInt(p[2]), resto(p, 3));            case "SOCIO" -> altaSocio(Integer.parseInt(p[1]), resto(p, 2));            case "PRESTAR" -> prestar(Integer.parseInt(p[1]), Integer.parseInt(p[2]), p[3]);            case "DEVOLVER" -> devolver(Integer.parseInt(p[1]), Integer.parseInt(p[2]), p[3]);            case "INFORME" -> informe();            default -> System.out.println("Error: orden desconocida " + p[0]);        }    }     static String resto(String[] p, int desde) {        return String.join(" ", Arrays.copyOfRange(p, desde, p.length));    }     static void altaLibro(String isbn, int ejemplares, String titulo) {        // TODO    }     static void altaSocio(int numero, String nombre) {        // TODO    }     static void prestar(int dia, int numero, String isbn) {        // TODO: comprueba las condiciones en el orden del enunciado    }     static void devolver(int dia, int numero, String isbn) {        // TODO: retraso, multa y bloqueo    }     static void informe() {        // TODO    }     static String euros(double importe) {        return String.format(ES, "%.2f €", importe);    }}

53. Clasificación de una liga de fútbol

Difícil · Java · 75 minutos · apuntesdam.com/ejercicios/java/clasificacion-de-una-liga

Una liga de fútbol de barrio apunta los resultados de cada jornada en un fichero de texto y quiere un programa que saque la clasificación sin tener que echar cuentas a mano. El fichero lo rellenan varias personas, así que a veces hay líneas vacías o mal escritas: el programa debe avisar de ellas y seguir con las demás.

Cada línea válida es un partido: equipo local;goles-goles;equipo visitante, por ejemplo Atlético Norte;2-1;Real Sur. Una victoria da 3 puntos, un empate 1 y una derrota 0. Los nombres de los equipos tienen como mucho 18 caracteres y pueden llevar espacios.

La clasificación se ordena por puntos. Si dos equipos empatan a puntos, va delante el de mayor diferencia de goles (goles a favor menos goles en contra); si siguen empatados, el que más goles ha marcado; y si aún así empatan, por orden alfabético del nombre.

Requisitos

  • Lee la entrada línea a línea. Las líneas vacías (o solo con espacios) se ignoran sin avisar.

  • Una línea es válida si al separarla por ; salen exactamente tres partes, los dos nombres no están vacíos, son distintos (un equipo no juega contra sí mismo) y el marcador son dos números enteros separados por un guion. Se ignoran los espacios al principio y al final de cada parte.

  • Por cada línea no válida escribe en el momento Aviso: la línea N no es un resultado válido y se ignora, donde N es su número de línea en la entrada (contando también las vacías, empezando en 1).

  • Para cada equipo cuenta: partidos jugados (PJ), ganados (G), empatados (E), perdidos (P), goles a favor (GF), goles en contra (GC), diferencia de goles (DG) y puntos (Pts).

  • Escribe la clasificación con una cabecera y una fila por equipo, con este formato exacto de columnas: String.format("%-4d%-20s%3d%3d%3d%3d%4d%4d%5s%5d", …) para las filas (la DG va como texto, con signo + si es positiva) y la cabecera que ya trae el código inicial.

  • Después, una línea en blanco y dos líneas más: Partidos: N · Goles: M · Media: X,XX goles por partido (media con dos decimales y coma) y Equipo menos goleado: nombre (N goles en contra), con «gol» en singular si es 1. Si varios equipos empatan con menos goles en contra, se escribe el que va más arriba en la clasificación.

  • Si no hay ningún resultado válido, después de los avisos escribe solo No hay resultados válidos.

Formato de la entrada

  • Una línea por partido: local;goles-goles;visitante. Puede haber líneas vacías y líneas mal escritas.

Ejemplo: Una liga de cuatro equipos

Entrada

Atlético Norte;2-1;Real Sur
Deportivo Este;0-0;Unión Oeste
Real Sur;3-3;Deportivo Este
Unión Oeste;1-2;Atlético Norte
Atlético Norte;2-0;Deportivo Este
Real Sur;1-0;Unión Oeste

Salida esperada

Pos Equipo               PJ  G  E  P  GF  GC   DG  Pts
1   Atlético Norte        3  3  0  0   6   2   +4    9
2   Real Sur              3  1  1  1   5   5    0    4
3   Deportivo Este        3  0  2  1   3   5   -2    2
4   Unión Oeste           3  0  1  2   1   3   -2    1

Partidos: 6 · Goles: 15 · Media: 2,50 goles por partido
Equipo menos goleado: Atlético Norte (2 goles en contra)

Ejemplo: Con avisos y empates a puntos

Entrada

Ciudad Alta;1-0;Puerto Bajo
Puerto Bajo;3-1;Valle Verde
Valle Verde;2-0;Ciudad Alta

esto no es un resultado
Montaña Azul;2;Valle Verde
Puerto Bajo;0-0;Puerto Bajo
Montaña Azul;1-1;Ciudad Alta

Salida esperada

Aviso: la línea 5 no es un resultado válido y se ignora
Aviso: la línea 6 no es un resultado válido y se ignora
Aviso: la línea 7 no es un resultado válido y se ignora
Pos Equipo               PJ  G  E  P  GF  GC   DG  Pts
1   Ciudad Alta           3  1  1  1   2   3   -1    4
2   Puerto Bajo           2  1  0  1   3   2   +1    3
3   Valle Verde           2  1  0  1   3   3    0    3
4   Montaña Azul          1  0  1  0   1   1    0    1

Partidos: 4 · Goles: 9 · Media: 2,25 goles por partido
Equipo menos goleado: Montaña Azul (1 gol en contra)
Código de partida (java)
import java.util.*; public class Main {    static class Equipo {        final String nombre;        int pj, g, e, p, gf, gc;         Equipo(String nombre) {            this.nombre = nombre;        }         int dg() {            return 0; // TODO: diferencia de goles        }         int puntos() {            return 0; // TODO: 3 por victoria y 1 por empate        }    }     static final Locale ES = Locale.forLanguageTag("es-ES");    static final Map<String, Equipo> equipos = new LinkedHashMap<>();     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int numLinea = 0;        while (sc.hasNextLine()) {            String linea = sc.nextLine();            numLinea++;            // TODO: ignora las líneas vacías, valida el resto y registra el resultado en los dos equipos        }        // TODO: ordena los equipos, imprime la tabla y las dos líneas finales    }     static boolean esValida(String[] partes) {        return false; // TODO    }     static Equipo equipo(String nombre) {        return equipos.computeIfAbsent(nombre, Equipo::new);    }     static void registrar(Equipo t, int aFavor, int enContra) {        // TODO    }     static void imprimirTabla(List<Equipo> tabla) {        System.out.println(String.format("%-4s%-20s%3s%3s%3s%3s%4s%4s%5s%5s", "Pos", "Equipo", "PJ", "G", "E", "P", "GF", "GC", "DG", "Pts"));        // TODO: una fila por equipo con el formato del enunciado    }}

54. Cajero automático con PIN, límite diario y billetes

Medio · Java · 50 minutos · apuntesdam.com/ejercicios/java/cajero-automatico

Un banco quiere probar la lógica de sus cajeros antes de instalarla en las máquinas. En lugar de pulsar botones, el programa recibe por teclado lo que hace el cliente, una orden por línea, y responde con lo que aparecería en la pantalla del cajero.

La primera línea no la escribe el cliente: es lo que el cajero lee de la tarjeta, el saldo de la cuenta y el PIN correcto. Mientras el cliente no acierte el PIN no puede hacer nada más, y si falla tres veces el cajero se queda la tarjeta.

El cajero solo tiene billetes de 50, 20 y 10 €, así que solo se puede sacar dinero en múltiplos de 10, y nunca más de 600 € al día entre todas las retiradas. Como cualquier programa que maneja dinero, trabaja en céntimos enteros: sumar 49,95 en coma flotante acaba dando 49,94999… y el saldo dejaría de cuadrar.

Requisitos

  • La primera línea es saldo pin, por ejemplo 850.50 1234. Después, cada línea es una orden (en mayúsculas o minúsculas) con su argumento separado por un espacio. Las líneas vacías se ignoran.

  • Hasta acertar el PIN, cualquier orden que no sea PIN responde Introduzca el PIN. PIN n responde PIN correcto si acierta; si falla, PIN incorrecto. Quedan 2 intentos o PIN incorrecto. Queda 1 intento, y al tercer fallo PIN incorrecto. Tarjeta retenida y el programa termina sin leer nada más.

  • Con el PIN correcto: SALDO escribe Saldo: X; PIN escribe Ya ha introducido el PIN; cualquier orden desconocida, Operación desconocida.

  • INGRESAR cantidad: la cantidad es un número positivo con punto decimal y como mucho dos decimales. Escribe Ingresados X. Saldo: Y, o Cantidad no válida.

  • RETIRAR cantidad, comprobando en este orden: que sea un múltiplo de 10 € mayor que 0 (si no, Importe no válido: debe ser múltiplo de 10 €), que haya saldo (Saldo insuficiente) y que con lo ya retirado hoy no se pasen los 600 € (Supera el límite diario: hoy puede retirar hasta X). Si todo va bien: Retirados X: desglose. Saldo: Y, con el desglose en el menor número de billetes, de mayor a menor y sin los que no se usan (por ejemplo 3x50 1x20 1x10).

  • SALIR escribe Operaciones: N · Retirado hoy: X (N cuenta los ingresos y retiradas hechos) y Retire su tarjeta, y termina: lo que venga después no se procesa. Si la entrada se acaba sin SALIR y el cliente se había identificado, el cajero escribe esas dos mismas líneas.

  • Los importes se escriben con punto de millar, coma decimal y dos decimales, seguidos de un espacio y €: 1.012.345,60 €.

Formato de la entrada

  • Línea 1: saldo pin. Resto: una orden por línea (PIN, SALDO, INGRESAR, RETIRAR, SALIR).

Ejemplo: Una sesión completa

Entrada

850.50 1234
SALDO
PIN 1111
PIN 1234
SALDO
RETIRAR 180
INGRESAR 49.95
RETIRAR 75
RETIRAR 500
RETIRAR 420
CONSULTAR
SALIR
SALDO

Salida esperada

Introduzca el PIN
PIN incorrecto. Quedan 2 intentos
PIN correcto
Saldo: 850,50 €
Retirados 180,00 €: 3x50 1x20 1x10. Saldo: 670,50 €
Ingresados 49,95 €. Saldo: 720,45 €
Importe no válido: debe ser múltiplo de 10 €
Supera el límite diario: hoy puede retirar hasta 420,00 €
Retirados 420,00 €: 8x50 1x20. Saldo: 300,45 €
Operación desconocida
Operaciones: 3 · Retirado hoy: 600,00 €
Retire su tarjeta

Ejemplo: Tres fallos de PIN

Entrada

2000 9876
PIN 1
PIN 2
PIN 3
SALDO

Salida esperada

PIN incorrecto. Quedan 2 intentos
PIN incorrecto. Queda 1 intento
PIN incorrecto. Tarjeta retenida
Código de partida (java)
import java.util.Locale;import java.util.Scanner; public class Main {    static final long LIMITE_DIARIO = 60000;      // 600 € en céntimos    static final int[] BILLETES = {50, 20, 10};     /** Céntimos → «1.234,50 €». */    static String euros(long centimos) {        String entero = String.format(Locale.ROOT, "%,d", centimos / 100).replace(',', '.');        return entero + "," + String.format("%02d", centimos % 100) + " €";    }     /** «49.95» → 4995 céntimos; -1 si no es una cantidad válida (hasta 2 decimales). */    static long centimos(String texto) {        // TODO        return -1;    }     /** Desglose en billetes de 50, 20 y 10 con el menor número de billetes: «3x50 1x20 1x10». */    static String desglose(long euros) {        // TODO        return "";    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String[] inicio = sc.nextLine().trim().split("\\s+");        long saldo = centimos(inicio[0]);        String pin = inicio[1];         while (sc.hasNextLine()) {            String linea = sc.nextLine().trim();            // TODO: PIN (3 intentos), SALDO, INGRESAR, RETIRAR y SALIR        }    }}

55. Buscaminas: números, casilla más peligrosa y zonas despejadas

Difícil · Java · 70 minutos · apuntesdam.com/ejercicios/java/buscaminas-numeros-y-zonas

En el Buscaminas, cada casilla sin mina muestra cuántas minas hay en las ocho casillas que la rodean (también en diagonal). Con esos números el jugador deduce dónde están las minas.

Cuando se pulsa una casilla con un 0, el juego no la abre sola: abre de golpe todos los ceros conectados con ella (también en diagonal), como una mancha. Cada una de esas manchas es una «zona despejada», y contar cuántas hay da una idea de lo fácil que es un tablero.

Vas a escribir la parte del juego que prepara un tablero nuevo: recibe dónde están las minas y calcula todo lo demás. Los tableros vienen de un fichero de niveles que a veces está mal escrito, así que hay que validarlo.

Requisitos

  • La primera línea tiene el número de filas y de columnas separados por un espacio (de 1 a 30 cada uno). Después vienen tantas líneas como filas, con exactamente tantos caracteres como columnas: * es una mina y . una casilla libre.

  • Si la cabecera no es válida, escribe Tablero no válido: línea 1. Si una fila no tiene la longitud correcta o tiene otro carácter, Tablero no válido: línea N (N es su número de línea en la entrada, contando la cabecera). Si faltan filas, Tablero no válido: faltan filas. En todos los casos no se escribe nada más.

  • Escribe Tablero: y el tablero con las minas como * y cada casilla libre sustituida por el número de minas vecinas (de 0 a 8).

  • Minas: M de T casillas (P %), con el porcentaje con un decimal y coma decimal.

  • Casilla más peligrosa: fila F, columna C (K minas alrededor): la casilla libre con más minas vecinas (filas y columnas se numeran desde 1; si hay empate, la primera leyendo por filas; «mina» en singular si K es 1). Si ninguna casilla libre tiene minas alrededor (o no hay casillas libres), Casilla más peligrosa: ninguna.

  • Zonas despejadas: Z: cuántos grupos de casillas con 0 hay, considerando que dos ceros están conectados si son vecinos, también en diagonal.

Formato de la entrada

  • Línea 1: filas columnas. Después, una línea por fila con * (mina) y . (libre).

Ejemplo: Un tablero de 5 × 6

Entrada

5 6
*.....
..*...
......
...**.
......

Salida esperada

Tablero:
*21100
12*100
012321
001**1
001221
Minas: 4 de 30 casillas (13,3 %)
Casilla más peligrosa: fila 3, columna 4 (3 minas alrededor)
Zonas despejadas: 2

Ejemplo: Un tablero mal escrito

Entrada

3 4
..*.
.x..
....

Salida esperada

Tablero no válido: línea 3
Código de partida (java)
import java.util.Locale;import java.util.Scanner; public class Main {    static int filas, columnas;    static char[][] tablero;    static int[][] cuenta;     /** Minas en las 8 casillas vecinas de (f, c). */    static int minasAlrededor(int f, int c) {        // TODO        return 0;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        // TODO: lee y valida la cabecera y las filas del tablero        // TODO: escribe el tablero con los números, las minas, la casilla más peligrosa y las zonas despejadas    }}

56. Cifrado de Vigenère con el alfabeto español

Difícil · Java · 60 minutos · apuntesdam.com/ejercicios/java/cifrado-vigenere-con-enie

El cifrado de Vigenère se usó durante siglos para mensajes militares y diplomáticos. Funciona como el de César (desplazar cada letra unas posiciones en el alfabeto), pero el desplazamiento cambia en cada letra según una palabra clave: con la clave LIMON, la primera letra se desplaza 11 posiciones (la L), la segunda 8 (la I), y así sucesivamente, volviendo a empezar la clave cuando se acaba.

La mayoría de las implementaciones usan el alfabeto inglés de 26 letras. Aquí se usa el español, con la Ñ entre la N y la O: 27 letras en total, de la A (posición 0) a la Z (posición 26). Al desplazar más allá de la Z se vuelve a empezar por la A.

Los espacios, los números, los signos y las vocales con tilde no se cifran: se copian tal cual y no gastan letra de la clave. Las minúsculas se cifran como su mayúscula y siguen siendo minúsculas.

Requisitos

  • Cada línea es una orden seguida de su argumento (todo lo que hay tras el primer espacio). La orden puede ir en mayúsculas o minúsculas; las líneas vacías se ignoran. Una orden desconocida escribe Orden desconocida: ORDEN (en mayúsculas).

  • CLAVE palabra guarda la clave si todas sus letras son del alfabeto (en mayúsculas o minúsculas, la Ñ incluida) y escribe Clave guardada (N letras) («letra» si es una). Si no, Clave no válida: solo letras de la A a la Z y la Ñ y se mantiene la clave anterior.

  • CIFRAR texto escribe Cifrado: …: cada letra del alfabeto se desplaza hacia delante tantas posiciones como la posición de la letra de la clave que le toca. La clave vuelve a empezar en cada orden y solo avanza con las letras que se cifran.

  • DESCIFRAR texto escribe Descifrado: … deshaciendo el desplazamiento.

  • Si no hay clave guardada, CIFRAR y DESCIFRAR escriben Falta la clave; si hay clave pero no texto, Falta el texto.

  • ANALIZAR texto escribe Letras: N · Más frecuentes: A 5, B 3, C 2: el total de letras del alfabeto del texto (sin distinguir mayúsculas) y las tres más repetidas con su número de apariciones, de más a menos (si hay empate, en el orden del alfabeto; si hay menos de tres letras distintas, las que haya). Si no hay ninguna letra, solo Letras: 0.

Formato de la entrada

  • Una orden por línea: CLAVE, CIFRAR, DESCIFRAR o ANALIZAR, seguida de su argumento.

Posición de cada letra
ABCDEFGHIJKLMNÑOPQRSTUVWXYZ
01234567891011121314151617181920212223242526

Ejemplo: Cifrar, descifrar y analizar

Entrada

CIFRAR hola
CLAVE Limón
CLAVE Limon
CIFRAR Ataque al amanecer, ¡ya!
DESCIFRAR Lbmfho iw oylupqqc, ¡gm!
ANALIZAR El pingüino Ñoño come ñoquis

Salida esperada

Falta la clave
Clave no válida: solo letras de la A a la Z y la Ñ
Clave guardada (5 letras)
Cifrado: Lbmfho iw oylupqqc, ¡gm!
Descifrado: Ataque al amanecer, ¡ya!
Letras: 23 · Más frecuentes: O 5, I 3, Ñ 3

Ejemplo: Órdenes en minúsculas

Entrada

clave sol
cifrar
descifrar Zzz zzz
analizar ab ba
analizar 1234 ¿?
borrar todo

Salida esperada

Clave guardada (3 letras)
Falta el texto
Descifrado: Hlo hlo
Letras: 4 · Más frecuentes: A 2, B 2
Letras: 0
Orden desconocida: BORRAR
Código de partida (java)
import java.util.Scanner; public class Main {    static final String ALFABETO = "ABCDEFGHIJKLMNÑOPQRSTUVWXYZ";    static final int N = ALFABETO.length();   // 27 letras     /** La clave en mayúsculas, o null si tiene algo que no sea una letra del alfabeto. */    static String validarClave(String texto) {        // TODO        return null;    }     /** Cifra (sentido = 1) o descifra (sentido = -1). Solo cambian las letras del alfabeto, que conservan su caja. */    static String vigenere(String texto, String clave, int sentido) {        // TODO        return texto;    }     /** «Letras: 23 · Más frecuentes: O 5, Ñ 3, E 2». */    static String analizar(String texto) {        // TODO        return "Letras: 0";    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String clave = null;        while (sc.hasNextLine()) {            String linea = sc.nextLine().trim();            // TODO: separa la orden del argumento y atiende CLAVE, CIFRAR, DESCIFRAR y ANALIZAR        }    }}

57. Parking con tarifas por tipo de vehículo

Difícil · Java · 75 minutos · apuntesdam.com/ejercicios/java/parking-con-tarifas

El aparcamiento de un centro comercial tiene 4 plazas para coches, 2 para motos y 1 para furgonetas (es una versión de prueba: el real tiene cientos). La barrera de entrada y la de salida envían al programa una línea por cada vehículo que pasa.

Cada tipo de vehículo paga distinto, pero todos con la misma regla: los 10 primeros minutos son gratis (para quien entra y se va) y, a partir de ahí, se cobra cada cuarto de hora empezado, con un máximo por día. Esa regla común es justo lo que debe ir en una clase padre, y lo que cambia (el precio y el máximo), en cada clase hija.

Las cámaras leen las matrículas en el formato actual español: cuatro cifras y tres consonantes, sin vocales, ni Ñ ni Q, para evitar palabras y confusiones.

Requisitos

  • Cada línea es una orden: ENTRA HH:MM tipo matrícula, SALE HH:MM matrícula u OCUPACION. La orden, el tipo y la matrícula pueden venir en mayúsculas o minúsculas (la matrícula se guarda y se muestra en mayúsculas). Una línea con un número de palabras distinto del de su orden escribe Orden incompleta; una orden desconocida, Orden desconocida. Las líneas vacías se ignoran.

  • ENTRA comprueba, en este orden: la hora (Hora no válida: X, de 00:00 a 23:59), el tipo (Tipo no válido: X; son coche, moto y furgoneta), la matrícula (Matrícula no válida: X: 4 cifras y 3 letras de BCDFGHJKLMNPRSTVWXYZ), que no esté ya dentro (X ya está dentro) y que queden plazas de su tipo (No quedan plazas de tipo). Si todo va bien: Entra X (tipo) a las HH:MM.

  • SALE comprueba la hora, que el vehículo esté dentro (X no está en el parking) y que la salida no sea anterior a la entrada (La hora de salida es anterior a la entrada). Si todo va bien, el vehículo deja libre su plaza y se escribe Sale X (tipo) tras D: I, con la duración como 2 h 35 min, 3 h o 8 min y el importe en euros (6,60 €).

  • Tarifas: hasta 10 minutos, 0 €. Después, por cada 15 minutos empezados: coche 0,60 €, moto 0,30 € y furgoneta 0,90 €, con un máximo de 18 €, 9 € y 25 € respectivamente. Todas las estancias son del mismo día.

  • OCUPACION escribe Ocupación: coche 2/4 · moto 0/2 · furgoneta 1/1.

  • Al terminar la entrada: Recaudación: X (N salidas) («salida» si es 1) y Siguen dentro: … con las matrículas que no han salido en orden alfabético, separadas por coma y espacio, o ninguno.

Formato de la entrada

  • Una orden por línea: ENTRA HH:MM tipo matrícula, SALE HH:MM matrícula u OCUPACION.

Plazas y tarifas
TipoPlazasCada 15 min empezadosMáximo diario
coche40,60 €18,00 €
moto20,30 €9,00 €
furgoneta10,90 €25,00 €

Ejemplo: Un día de parking

Entrada

ENTRA 08:15 coche 1234BCD
ENTRA 08:20 moto 5678FGH
ENTRA 08:25 furgoneta 9012JKL
ENTRA 08:30 furgoneta 3456MNP
OCUPACION
SALE 08:28 5678FGH
SALE 10:50 1234BCD
ENTRA 11:00 coche 1234bcd
SALE 19:45 9012JKL
SALE 12:00 0000BBB

Salida esperada

Entra 1234BCD (coche) a las 08:15
Entra 5678FGH (moto) a las 08:20
Entra 9012JKL (furgoneta) a las 08:25
No quedan plazas de furgoneta
Ocupación: coche 1/4 · moto 1/2 · furgoneta 1/1
Sale 5678FGH (moto) tras 8 min: 0,00 €
Sale 1234BCD (coche) tras 2 h 35 min: 6,60 €
Entra 1234BCD (coche) a las 11:00
Sale 9012JKL (furgoneta) tras 11 h 20 min: 25,00 €
0000BBB no está en el parking
Recaudación: 31,60 € (3 salidas)
Siguen dentro: 1234BCD

Ejemplo: Datos no válidos

Entrada

ENTRA 25:00 coche 1234BCD
ENTRA 09:00 bici 1234BCD
ENTRA 09:00 coche 1234ABC
ENTRA 09:00 coche 1234BC
ENTRA 09:05 moto 1111BBB
ENTRA 09:06 moto 1111BBB
SALE 09:00 1111BBB
ENTRA
ESTADO

Salida esperada

Hora no válida: 25:00
Tipo no válido: bici
Matrícula no válida: 1234ABC
Matrícula no válida: 1234BC
Entra 1111BBB (moto) a las 09:05
1111BBB ya está dentro
La hora de salida es anterior a la entrada
Orden incompleta
Orden desconocida
Recaudación: 0,00 € (0 salidas)
Siguen dentro: 1111BBB
Código de partida (java)
import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Locale;import java.util.Map;import java.util.Scanner; abstract class Vehiculo {    private final String matricula;    private final int entrada;   // minutos desde las 00:00     Vehiculo(String matricula, int entrada) {        this.matricula = matricula;        this.entrada = entrada;    }     String getMatricula() { return matricula; }    int getEntrada() { return entrada; }     abstract String tipo();    /** Céntimos por cada 15 minutos empezados. */    abstract long precioFraccion();    /** Lo máximo que se cobra en un día, en céntimos. */    abstract long maximoDiario();     /** Importe en céntimos de una estancia: 10 minutos gratis y, después, cada cuarto de hora empezado, con un máximo. */    long importe(int minutos) {        // TODO        return 0;    }} // TODO: las clases Coche, Moto y Furgoneta public class Main {    static final List<String> TIPOS = List.of("coche", "moto", "furgoneta");    static final Map<String, Integer> PLAZAS = Map.of("coche", 4, "moto", 2, "furgoneta", 1);     static final Map<String, Vehiculo> dentro = new HashMap<>();     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String linea = sc.nextLine().trim();            // TODO: ENTRA, SALE y OCUPACION        }        // TODO: recaudación y vehículos que siguen dentro    }}

58. Evaluador de expresiones con pilas y variables

Muy difícil · Java · 100 minutos · apuntesdam.com/ejercicios/java/evaluador-de-expresiones-con-variables

Las calculadoras, las hojas de cálculo y los compiladores tienen que entender expresiones como 3 + 4 * (2 - 1), donde la multiplicación va antes que la suma y los paréntesis cambian el orden. Hacerlo leyendo de izquierda a derecha no funciona: hay que reorganizar la expresión.

La solución clásica, del informático Edsger Dijkstra, es el algoritmo de la estación de maniobras (shunting-yard): pasa la expresión a notación postfija, en la que los operadores van detrás de sus operandos (3 4 2 1 - * +) y ya no hacen falta paréntesis ni prioridades. Evaluar la postfija es muy sencillo con una pila: cada número se apila y cada operador desapila dos, opera y apila el resultado.

Tu calculadora trabajará con números enteros (long), recordará variables y explicará cada error en lugar de fallar: es la parte de un intérprete que se ocupa de las expresiones.

Requisitos

  • Cada línea no vacía es una expresión, una asignación nombre = expresión o la orden VARIABLES. Los nombres de variable empiezan por letra o _ y siguen con letras, cifras o _; distinguen mayúsculas.

  • Las expresiones usan enteros sin signo, variables, paréntesis, +, -, *, / (división entera, como en Java) y % (resto, como en Java), con los espacios que se quiera. *, / y % tienen más prioridad que + y -, y operadores de la misma prioridad se aplican de izquierda a derecha. Un - al principio, tras ( o tras otro operador es un menos unario, que tiene la mayor prioridad.

  • Para una expresión correcta escribe línea → postfija → resultado: la línea tal como se ha leído (sin espacios al principio ni al final), la postfija con sus elementos separados por un espacio (el menos unario se escribe ~) y el valor. En una asignación el resultado es nombre = valor, y la variable queda guardada.

  • Si hay un error al separar la expresión o al pasarla a postfija, escribe línea → Error: mensaje; si el error aparece al evaluarla, línea → postfija → Error: mensaje (sin la postfija si está vacía). Los mensajes son: carácter no válido 'c', paréntesis sin abrir, paréntesis sin cerrar, expresión incompleta (falta un operando o un operador), división entre cero, variable desconocida nombre, número demasiado grande (no cabe en un long), número no válido texto (empieza por cifra pero tiene letras) y desbordamiento (el resultado de una operación no cabe en un long). Una asignación con error no cambia la variable.

  • VARIABLES escribe Variables: a = 1, b = 2 con todas las variables por orden alfabético, o Variables: ninguna.

Formato de la entrada

  • Una expresión, asignación u orden VARIABLES por línea.

Ejemplo: Prioridades, menos unario y variables

Entrada

3 + 4 * (2 - 1)
(3 + 4) * 2 - 10 / 3
x = 2 * -5
y = x * x % 7
-(x + 3) * y
VARIABLES

Salida esperada

3 + 4 * (2 - 1) → 3 4 2 1 - * + → 7
(3 + 4) * 2 - 10 / 3 → 3 4 + 2 * 10 3 / - → 11
x = 2 * -5 → 2 5 ~ * → x = -10
y = x * x % 7 → x x * 7 % → y = 2
-(x + 3) * y → x 3 + ~ y * → 14
Variables: x = -10, y = 2

Ejemplo: Errores

Entrada

8 / (4 - 2 * 2)
(1 + 2
1 + 2)
3 +
5 $ 2
z + 1
4 5

Salida esperada

8 / (4 - 2 * 2) → 8 4 2 2 * - / → Error: división entre cero
(1 + 2 → Error: paréntesis sin cerrar
1 + 2) → Error: paréntesis sin abrir
3 + → 3 + → Error: expresión incompleta
5 $ 2 → Error: carácter no válido '
#x27; z + 1 → z 1 + → Error: variable desconocida z 4 5 → 4 5 → Error: expresión incompleta
Código de partida (java)
import java.util.ArrayDeque;import java.util.ArrayList;import java.util.Deque;import java.util.List;import java.util.Map;import java.util.Scanner;import java.util.TreeMap; public class Main {    /** Error de una expresión, con el mensaje que se muestra. */    static class ErrorExpresion extends Exception {        ErrorExpresion(String mensaje) { super(mensaje); }    }     static final Map<String, Long> variables = new TreeMap<>();     /** «x * (2 - 15)» → [x, *, (, 2, -, 15, )]. */    static List<String> separar(String texto) throws ErrorExpresion {        // TODO        return new ArrayList<>();    }     /** Notación infija → postfija (algoritmo de la estación de maniobras). */    static List<String> aPostfija(List<String> tokens) throws ErrorExpresion {        // TODO        return new ArrayList<>();    }     static long evaluar(List<String> postfija) throws ErrorExpresion {        // TODO        return 0;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String linea = sc.nextLine().trim();            // TODO: VARIABLES, asignaciones y expresiones        }    }}

59. Conversor de CSV a JSON con comillas y tipos

Difícil · Java · 75 minutos · apuntesdam.com/ejercicios/java/conversor-csv-a-json

Una academia exporta sus alumnos de una hoja de cálculo en CSV y quiere cargarlos en una aplicación web que solo entiende JSON. Parece trivial (separar por comas), pero los datos reales lo complican: hay apellidos como «López, Juan» que llevan una coma dentro y por eso van entre comillas, y textos que contienen las propias comillas, que en CSV se escriben dobles ("").

Además, en JSON los tipos importan: 8.5 es un número, true un booleano, un campo vacío es null y el resto son textos entre comillas. Hay una trampa clásica: un código postal como 08001 parece un número, pero si se convierte se pierde el cero. En JSON un número no puede empezar por cero, así que esos valores se quedan como texto.

Algunas líneas vienen mal (les faltan campos o tienen comillas sin cerrar). No se convierten, pero el programa dice cuáles eran para poder corregirlas.

Requisitos

  • La primera línea no vacía es la cabecera, con los nombres de los campos. Cada línea siguiente es un registro. Las líneas en blanco se ignoran (pero cuentan para numerar las líneas).

  • Los campos se separan por comas. Un campo puede ir entre comillas dobles, y entonces puede contener comas; dentro de las comillas, "" representa una comilla. A cada campo (y a cada nombre de la cabecera) se le quitan los espacios del principio y del final.

  • Un registro con un número de campos distinto del de la cabecera, o con comillas sin cerrar, no se convierte: se anota su número de línea.

  • Tipos: un campo vacío es null; un campo que cumple el formato de número JSON (signo menos opcional, 0 o una cifra del 1 al 9 seguida de más cifras, y opcionalmente un punto y decimales) se escribe tal cual; true y false son booleanos; todo lo demás es texto entre comillas, con " escrito \", \ escrito \\ y el tabulador escrito \t.

  • Salida: un array JSON con un objeto por registro, en el orden del fichero, con dos espacios de sangría para cada objeto y cuatro para cada propiedad ("campo": valor), comas entre elementos y nada al final de la última línea. Sin registros, [].

  • Después del JSON, Registros: N · Líneas ignoradas: M (l1, l2…), sin el paréntesis si M es 0. Si la entrada no tiene ni la cabecera, solo El fichero está vacío.

Formato de la entrada

  • Un fichero CSV: cabecera y una línea por registro, campos separados por comas y entre comillas cuando hace falta.

Ejemplo: Alumnos de una academia

Entrada

id,nombre,ciudad,cp,nota,beca
1,Ana Gil,Madrid,28001,8.5,true
2,"López, Juan",Barcelona,08001,6,false
3,"Eva ""la rápida"" Ruiz",Sevilla,41001,,true
4,Luis,Valencia,46001,7.25
5,Marta Díaz,Bilbao,48001,-0.5,false

Salida esperada

[
  {
    "id": 1,
    "nombre": "Ana Gil",
    "ciudad": "Madrid",
    "cp": 28001,
    "nota": 8.5,
    "beca": true
  },
  {
    "id": 2,
    "nombre": "López, Juan",
    "ciudad": "Barcelona",
    "cp": "08001",
    "nota": 6,
    "beca": false
  },
  {
    "id": 3,
    "nombre": "Eva \"la rápida\" Ruiz",
    "ciudad": "Sevilla",
    "cp": 41001,
    "nota": null,
    "beca": true
  },
  {
    "id": 5,
    "nombre": "Marta Díaz",
    "ciudad": "Bilbao",
    "cp": 48001,
    "nota": -0.5,
    "beca": false
  }
]
Registros: 4 · Líneas ignoradas: 1 (5)

Ejemplo: Espacios, barras y comillas sin cerrar

Entrada

codigo , descripcion , precio
A1 , "Cable HDMI 2m" , 7.95
A2,"Ruta C:\temp",0
A3,"Sin cerrar,1

A4,,

Salida esperada

[
  {
    "codigo": "A1",
    "descripcion": "Cable HDMI 2m",
    "precio": 7.95
  },
  {
    "codigo": "A2",
    "descripcion": "Ruta C:\\temp",
    "precio": 0
  },
  {
    "codigo": "A4",
    "descripcion": null,
    "precio": null
  }
]
Registros: 3 · Líneas ignoradas: 1 (4)
Código de partida (java)
import java.util.ArrayList;import java.util.List;import java.util.Scanner; public class Main {    /** Campos de una línea CSV separados por comas, con comillas dobles opcionales ("" es una comilla). null si hay comillas sin cerrar. */    static List<String> campos(String linea) {        // TODO        return new ArrayList<>();    }     /** Texto JSON entre comillas, con las comillas, barras y tabuladores escapados. */    static String cadena(String s) {        // TODO        return "\"" + s + "\"";    }     /** El valor JSON de un campo: null, número, booleano o texto. */    static String valor(String campo) {        // TODO        return cadena(campo);    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        // TODO: lee la cabecera y cada línea, y escribe el JSON y el resumen    }}

60. Nóminas de una plantilla con distintos contratos

Difícil · Java · 80 minutos · apuntesdam.com/ejercicios/java/nominas-de-una-plantilla

Una pequeña empresa calcula las nóminas con una hoja de cálculo que nadie se atreve a tocar. Quiere sustituirla por un programa que reciba la plantilla del mes y saque la tabla de nóminas.

Hay cuatro tipos de contrato y cada uno calcula el bruto a su manera, pero las deducciones son iguales para todos: una retención de IRPF según el tramo del bruto y una cotización a la Seguridad Social. Excepto los becarios, a los que en este ejercicio no se les retiene IRPF. Es el caso de libro para una interfaz con métodos por defecto que una clase concreta puede sobrescribir.

Las cifras están simplificadas (una tabla de retenciones de la empresa en lugar del cálculo real del IRPF, y un único porcentaje de cotización): lo que importa es la estructura del programa y que cada céntimo cuadre.

Requisitos

  • Cada línea es TIPO datos, con los datos separados por ;: ASALARIADO nombre;salario anual;pagas (12 o 14 pagas), HORAS nombre;horas;precio por hora (horas enteras, como mucho 300), COMERCIAL nombre;base;ventas;comisión % (comisión hasta el 20 %) y BECARIO nombre;ayuda. Las cantidades usan punto decimal y hasta dos decimales. El tipo puede ir en minúsculas.

  • Una línea con un tipo desconocido escribe Línea N ignorada: tipo desconocido TIPO; con más o menos datos de los que le tocan (o sin nombre), Línea N ignorada: faltan o sobran datos; con un número mal escrito o fuera de rango, Línea N ignorada: dato no válido. N es el número de línea en la entrada (las vacías también cuentan). Estos avisos salen antes que la tabla.

  • Bruto mensual: asalariado, salario anual ÷ pagas; por horas, las primeras 160 horas al precio normal y las siguientes al 125 %; comercial, base + comisión sobre las ventas, con la comisión limitada a 2.000 €; becario, la ayuda.

  • Retención de IRPF, un único tipo sobre todo el bruto: hasta 1.000 €, el 2 %; hasta 2.000 €, el 12 %; hasta 3.000 €, el 18 %; por encima, el 24 %. Los becarios no tienen retención. Seguridad Social: el 6,5 % del bruto para todos. Neto = bruto − IRPF − Seguridad Social.

  • Todos los cálculos se hacen en céntimos y cada porcentaje se redondea al céntimo más cercano (las mitades suben).

  • Tabla: la cabecera Empleado, Tipo, Bruto, IRPF, SS, Neto y una fila por empleado en el orden de la entrada, con el nombre en 20 caracteres y el tipo (asalariado, por horas, comercial, becario) en 12, alineados a la izquierda, y los cuatro importes en 11 caracteres a la derecha, con punto de millar y coma decimal y sin símbolo. Después, una fila TOTAL con las sumas (el tipo en blanco).

  • Para terminar, Coste para la empresa: X € (el bruto total más un 30 % de cotización de la empresa) y Mayor neto: nombre (X €) (el primero si hay empate). Si no hay ningún empleado válido, después de los avisos solo No hay nóminas que calcular.

Formato de la entrada

  • Una línea por empleado: TIPO nombre;dato;dato….

Retención de IRPF (tabla de la empresa)
Bruto mensualTipo
hasta 1.000,00 €2 %
de 1.000,01 € a 2.000,00 €12 %
de 2.000,01 € a 3.000,00 €18 %
más de 3.000,00 €24 %

Ejemplo: Una plantilla con los cuatro contratos

Entrada

ASALARIADO Ana Gil;30000;14
ASALARIADO Luis Mora;48000;12
HORAS Eva Ruiz;172;12.50
COMERCIAL Pablo Sanz;1100;42000;3.5
BECARIO Irene Vidal;600

Salida esperada

Empleado            Tipo              Bruto       IRPF         SS       Neto
Ana Gil             asalariado     2.142,86     385,71     139,29   1.617,86
Luis Mora           asalariado     4.000,00     960,00     260,00   2.780,00
Eva Ruiz            por horas      2.187,50     393,75     142,19   1.651,56
Pablo Sanz          comercial      2.570,00     462,60     167,05   1.940,35
Irene Vidal         becario          600,00       0,00      39,00     561,00
TOTAL                             11.500,36   2.202,06     747,53   8.550,77
Coste para la empresa: 14.950,47 €
Mayor neto: Luis Mora (2.780,00 €)

Ejemplo: Líneas que no se pueden calcular

Entrada

ASALARIADO Mar Gómez;25000;13
JEFE Pedro;5000
HORAS Ana;abc;10

COMERCIAL Leo;1000;5000
becario Sol;450.5

Salida esperada

Línea 1 ignorada: dato no válido
Línea 2 ignorada: tipo desconocido JEFE
Línea 3 ignorada: dato no válido
Línea 5 ignorada: faltan o sobran datos
Empleado            Tipo              Bruto       IRPF         SS       Neto
Sol                 becario          450,50       0,00      29,28     421,22
TOTAL                                450,50       0,00      29,28     421,22
Coste para la empresa: 585,65 €
Mayor neto: Sol (421,22 €)
Código de partida (java)
import java.util.ArrayList;import java.util.List;import java.util.Locale;import java.util.Scanner; interface Retribuible {    /** Bruto del mes, en céntimos. */    long bruto();     // TODO: métodos por defecto retencion(), seguridadSocial() y neto()} abstract class Empleado implements Retribuible {    private final String nombre;     Empleado(String nombre) { this.nombre = nombre; }     String getNombre() { return nombre; }     abstract String tipo();} // TODO: Asalariado, PorHoras, Comercial y Becario public class Main {    /** cantidad × centésimas / 10000, redondeado al céntimo (las mitades suben). */    static long porcentaje(long cantidad, long centesimas) {        return (cantidad * centesimas + 5000) / 10000;    }     static String euros(long c) {        return String.format(Locale.ROOT, "%,d", c / 100).replace(',', '.') + "," + String.format("%02d", c % 100);    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        List<Empleado> plantilla = new ArrayList<>();        // TODO: lee y valida cada línea, y escribe la tabla de nóminas y el resumen    }}

61. Validador de DNI, NIE e IBAN diseñado con clases de equivalencia

Medio · Java · 50 minutos · apuntesdam.com/ejercicios/java/validador-dni-nie-iban

Casi todas las aplicaciones de gestión piden un DNI, un NIE o un IBAN, y casi todas los validan mal: aceptan letras que no corresponden, rechazan minúsculas o se atascan con los espacios del IBAN. Los tres llevan un control matemático que permite detectar la mayoría de los errores al teclear.

La letra del DNI es el resto de dividir el número entre 23, buscado en la tabla TRWAGMYFPDXBNJZSQVHLCKE. El NIE de los extranjeros funciona igual cambiando la X, la Y y la Z iniciales por 0, 1 y 2. El IBAN usa otro método (ISO 13616): se pasan los cuatro primeros caracteres al final, cada letra se sustituye por un número (A = 10, B = 11… Z = 35) y el número resultante, de unas 26 cifras, tiene que dar resto 1 al dividirlo entre 97.

Este ejercicio es también de Entornos de Desarrollo: las pruebas no se han elegido al azar, sino con la técnica de clases de equivalencia (un caso representativo de cada grupo de entradas que el programa trata igual) y valores límite (los bordes de cada grupo). Tienes la tabla abajo: tu programa tiene que pasar todos esos casos.

Requisitos

  • Cada línea es TIPO valor, con TIPO DNI, NIE o IBAN en mayúsculas o minúsculas. Las líneas vacías se ignoran. Para cada línea escribe TIPO valor → válido o TIPO valor → no válido: motivo, con el tipo en mayúsculas y el valor tal como venía (sin espacios al principio ni al final).

  • DNI: 8 cifras y una letra (mayúscula o minúscula). Si no tiene ese formato, formato incorrecto (8 cifras y una letra); si la letra no es la que toca, la letra debería ser L.

  • NIE: X, Y o Z, 7 cifras y una letra. Si no tiene ese formato, formato incorrecto (X, Y o Z, 7 cifras y una letra); si la letra no cuadra, el mismo mensaje que el DNI.

  • IBAN (se ignoran los espacios y las minúsculas): si no empieza por ES, solo se admiten IBAN españoles; si no tiene 24 caracteres, un IBAN español tiene 24 caracteres; si tras ES hay algo que no sea una cifra, después de ES solo puede haber cifras; si el control módulo 97 no da 1, los dígitos de control no cuadran. Se comprueba en ese orden.

  • Un tipo desconocido escribe TIPO valor → no válido: tipo de documento desconocido.

  • Al final, Válidos: V de T.

Formato de la entrada

  • Una línea por documento: DNI valor, NIE valor o IBAN valor.

Clases de equivalencia y valores límite del DNI
ClaseCaso de pruebaResultado esperado
Válida: 8 cifras y su letra12345678Zválido
Válida: letra en minúscula12345678zválido
Límite inferior del número00000000Tválido
Límite superior del número99999999Rválido
No válida: letra que no toca12345678Ala letra debería ser Z
No válida: 7 cifras1234567Zformato incorrecto
No válida: 9 cifras123456789Zformato incorrecto
No válida: sin letra12345678formato incorrecto
Clases de equivalencia del IBAN
ClaseCaso de pruebaResultado esperado
Válida, con espaciosES91 2100 0418 4502 0005 1332válido
Válida, en minúsculas y sin espacioses9121000418450200051332válido
Otro paísDE89370400440532013000solo se admiten IBAN españoles
Longitud incorrectaES91210004184502000513324 caracteres
Letras tras ESES91 2100 0418 4502 0005 13ABsolo cifras
Control incorrecto (una cifra cambiada)ES91 2100 0418 4502 0005 1333no cuadran

Ejemplo: DNI y NIE

Entrada

DNI 12345678Z
DNI 12345678z
DNI 12345678A
DNI 1234567Z
NIE X1234567L
NIE W1234567L
PASAPORTE AB123456

Salida esperada

DNI 12345678Z → válido
DNI 12345678z → válido
DNI 12345678A → no válido: la letra debería ser Z
DNI 1234567Z → no válido: formato incorrecto (8 cifras y una letra)
NIE X1234567L → válido
NIE W1234567L → no válido: formato incorrecto (X, Y o Z, 7 cifras y una letra)
PASAPORTE AB123456 → no válido: tipo de documento desconocido
Válidos: 3 de 7

Ejemplo: IBAN

Entrada

IBAN ES91 2100 0418 4502 0005 1332
iban es9121000418450200051332
IBAN DE89370400440532013000
IBAN ES912100041845020005133
IBAN ES91 2100 0418 4502 0005 13AB
IBAN ES91 2100 0418 4502 0005 1333

Salida esperada

IBAN ES91 2100 0418 4502 0005 1332 → válido
IBAN es9121000418450200051332 → válido
IBAN DE89370400440532013000 → no válido: solo se admiten IBAN españoles
IBAN ES912100041845020005133 → no válido: un IBAN español tiene 24 caracteres
IBAN ES91 2100 0418 4502 0005 13AB → no válido: después de ES solo puede haber cifras
IBAN ES91 2100 0418 4502 0005 1333 → no válido: los dígitos de control no cuadran
Válidos: 2 de 6
Código de partida (java)
import java.util.Scanner; public class Main {    static final String LETRAS = "TRWAGMYFPDXBNJZSQVHLCKE";     /** null si el DNI es correcto, o el motivo por el que no lo es. */    static String validarDni(String dni) {        // TODO        return null;    }     /** El NIE se valida como un DNI cambiando X, Y y Z por 0, 1 y 2. */    static String validarNie(String nie) {        // TODO        return null;    }     /** IBAN español: 24 caracteres y control módulo 97 (ISO 13616). */    static String validarIban(String iban) {        // TODO        return null;    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        // TODO: valida cada línea y escribe el resultado y el resumen    }}

62. Fichero de productos con registros de tamaño fijo y acceso directo

Muy difícil · Java · 90 minutos · apuntesdam.com/ejercicios/java/fichero-de-registros-con-acceso-directo

Antes de las bases de datos, los programas de gestión guardaban sus datos en ficheros binarios con registros de tamaño fijo. Todavía hoy es la forma de entender cómo trabaja un gestor de bases de datos por dentro: si cada registro ocupa siempre lo mismo, la posición del registro número N se calcula con una multiplicación y se puede ir directamente a él, sin leer los anteriores.

En este fichero cada producto ocupa 77 bytes: el código (int, 4 bytes), el nombre (30 caracteres char de 2 bytes, 60 en total, rellenando con el carácter \0), el precio (double, 8), el stock (int, 4) y una marca de borrado (boolean, 1). El producto con código N vive en el byte (N − 1) × 77: es acceso directo, y por eso puede haber huecos entre productos.

Borrar un registro desplazando todos los siguientes sería lentísimo en un fichero grande, así que las bajas son lógicas: se pone la marca de borrado a true y el hueco queda libre para un alta futura con ese código.

Requisitos

  • El programa crea un fichero temporal vacío y ejecuta una orden por línea (la orden puede ir en minúsculas; las líneas vacías se ignoran; una orden desconocida escribe Orden desconocida).

  • ALTA código;nombre;precio;stock: el código es un entero de 1 a 9999, el nombre no puede estar vacío, el precio es un número no negativo con hasta dos decimales y el stock un entero no negativo (si no, Datos no válidos). Si ya hay un producto activo con ese código, Ya existe el producto N. Si no, se escribe el registro en su posición y se escribe Alta del producto N en el byte B, añadiendo (nombre recortado a 30 caracteres) si el nombre era más largo.

  • PRECIO código precio cambia solo el precio, sin tocar el resto del registro: Precio del producto N: antes → después (o Datos no válidos / No existe el producto N).

  • BAJA código pone la marca de borrado: Baja lógica del producto N, o No existe el producto N si no está activo. VER código escribe N · nombre · precio · S uds o No existe el producto N.

  • LISTAR escribe Productos activos: A y, con dos espacios delante, cada producto activo por orden de código con el formato de VER.

  • INFORME escribe Fichero: T bytes, H huecos de registro, A activos, L libres o borrados · valor del stock X, donde H es el tamaño del fichero entre 77 y el valor es la suma de precio × stock de los activos. Los precios se escriben con dos decimales, coma decimal y €.

  • Un registro está activo si el fichero llega hasta él, el código guardado en él es el suyo (los huecos que se crean al escribir más allá del final están llenos de ceros) y no tiene la marca de borrado.

Formato de la entrada

  • Una orden por línea: ALTA, PRECIO, BAJA, VER, LISTAR o INFORME.

Estructura de un registro (77 bytes)
CampoTipoBytesDesplazamiento
códigoint40
nombre30 × char604
preciodouble864
stockint472
borradoboolean176

Ejemplo: Altas, cambios y bajas

Entrada

ALTA 1;Ratón inalámbrico;9.95;20
ALTA 3;Teclado mecánico;49.90;5
ALTA 1;Repetido;1;1
LISTAR
PRECIO 3 44.50
BAJA 1
VER 1
VER 3
VER 2
INFORME

Salida esperada

Alta del producto 1 en el byte 0
Alta del producto 3 en el byte 154
Ya existe el producto 1
Productos activos: 2
  1 · Ratón inalámbrico · 9,95 € · 20 uds
  3 · Teclado mecánico · 49,90 € · 5 uds
Precio del producto 3: 49,90 € → 44,50 €
Baja lógica del producto 1
No existe el producto 1
3 · Teclado mecánico · 44,50 € · 5 uds
No existe el producto 2
Fichero: 231 bytes, 3 huecos de registro, 1 activos, 2 libres o borrados · valor del stock 222,50 €

Ejemplo: Datos que no valen

Entrada

ALTA 0;Cero;1;1
ALTA 10;;1;1
ALTA 10;Monitor;-5;1
ALTA 10;Monitor;99.999;1
ALTA abc
PRECIO 7 10
BAJA 7
listar
BORRAR 1

Salida esperada

Datos no válidos
Datos no válidos
Datos no válidos
Datos no válidos
Datos no válidos
No existe el producto 7
No existe el producto 7
Productos activos: 0
Orden desconocida
Código de partida (java)
import java.io.File;import java.io.IOException;import java.io.RandomAccessFile;import java.util.Locale;import java.util.Scanner; public class Main {    static final int NOMBRE = 30;                          // caracteres del nombre    static final int TAM = 4 + NOMBRE * 2 + 8 + 4 + 1;     // código + nombre + precio + stock + borrado = 77 bytes     static RandomAccessFile f;     /** Acceso directo: el registro del código N empieza en el byte (N - 1) × 77. */    static long posicion(int codigo) {        return (long) (codigo - 1) * TAM;    }     static String euros(double x) {        return String.format(Locale.ROOT, "%.2f", x).replace('.', ',') + " €";    }     static void orden(String linea) throws IOException {        // TODO: ALTA, PRECIO, BAJA, VER, LISTAR e INFORME con seek, read… y write…    }     public static void main(String[] args) throws IOException {        File fichero = File.createTempFile("productos", ".dat");        fichero.deleteOnExit();        try (RandomAccessFile raf = new RandomAccessFile(fichero, "rw")) {            f = raf;            Scanner sc = new Scanner(System.in);            while (sc.hasNextLine()) {                String linea = sc.nextLine().trim();                if (!linea.isEmpty()) orden(linea);            }        }    }}