Apuntes DAM
Volver al inicio

Cifrado de Vigenère con el alfabeto español

Ejercicio de JavaDifícilUnos 60 minutos

Implementa el cifrado de Vigenère sobre las 27 letras del alfabeto español (con la Ñ): claves validadas, cifrar y descifrar conservando mayúsculas, minúsculas y signos, y un análisis de frecuencias de letras. Cadenas, char, aritmética modular y métodos.

  • String y StringBuilder
  • char y Character
  • indexOf
  • Aritmética modular
  • Métodos con parámetros
  • Arrays de contadores

Enunciado

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.

Qué tiene que hacer el programa

  1. 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).
  2. 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.
  3. 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.
  4. DESCIFRAR texto escribe Descifrado: … deshaciendo el desplazamiento.
  5. Si no hay clave guardada, CIFRAR y DESCIFRAR escriben Falta la clave; si hay clave pero no texto, Falta el texto.
  6. 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.

Entrada

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

Datos de referencia

Posición de cada letra
ABCDEFGHIJKLMNÑOPQRSTUVWXYZ
01234567891011121314151617181920212223242526

Ejemplos de ejecución

Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.

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 por consola

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

Órdenes en minúsculas

Entrada

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

Salida por consola

Clave guardada (3 letras)
Falta el texto
Descifrado: Hlo hlo
Letras: 4 · Más frecuentes: A 2, B 2
Letras: 0
Orden desconocida: BORRAR

Guía paso a paso

Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.

1. Separar la orden del argumento

El texto a cifrar puede tener espacios, así que no sirve split(" "): busca el primer espacio con indexOf(' ') y corta con substring. Lo que queda antes es la orden y lo de después, el argumento.

2. La posición de cada letra

Guarda el alfabeto en una constante String y usa indexOf para pasar de letra a número y charAt para volver. indexOf devuelve -1 si el carácter no es una letra del alfabeto: así sabes qué hay que copiar tal cual.

java
int pos = ALFABETO.indexOf(Character.toUpperCase(c));
if (pos < 0) { sb.append(c); continue; }
3. Desplazar con módulo

La letra cifrada es (pos + desplazamiento) % 27. Al descifrar restas, y el resultado puede ser negativo: en Java -3 % 27 vale -3, no 24. Sumar 27 antes del último módulo lo arregla en los dos sentidos con la misma fórmula.

java
char nueva = ALFABETO.charAt(((pos + sentido * desplazamiento) % N + N) % N);
4. La clave solo avanza con letras

Lleva un contador k aparte del índice del texto y súmale 1 solo cuando cifres una letra. La letra de la clave que toca es clave.charAt(k % clave.length()).

5. Las tres más frecuentes

Cuenta las apariciones en un int[27]. Para sacar las tres mayores sin ordenar, repite tres veces: busca la mayor que no hayas usado todavía (con > estricto gana la primera del alfabeto en un empate) y márcala como usada.

Resuélvelo aquí

El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 2 casos ocultos que buscan los errores típicos.

☕JavaCifrado de Vigenère con el alfabeto españolDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
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
⏳
Test oculto #3
⏳
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada

Solución explicada

Ver la solución completa
java
1import java.util.Scanner;
2
3public class Main {
4    static final String ALFABETO = "ABCDEFGHIJKLMNÑOPQRSTUVWXYZ";
5    static final int N = ALFABETO.length();   // 27 letras
6
7    /** La clave en mayúsculas, o null si tiene algo que no sea una letra del alfabeto. */
8    static String validarClave(String texto) {
9        String clave = texto.toUpperCase();
10        if (clave.isEmpty()) return null;
11        for (char c : clave.toCharArray()) {
12            if (ALFABETO.indexOf(c) < 0) return null;
13        }
14        return clave;
15    }
16
17    /** Cifra (sentido = 1) o descifra (sentido = -1). Solo cambian las letras del alfabeto, que conservan su caja. */
18    static String vigenere(String texto, String clave, int sentido) {
19        StringBuilder sb = new StringBuilder();
20        int k = 0;   // posición en la clave: solo avanza con las letras
21        for (char c : texto.toCharArray()) {
22            int pos = ALFABETO.indexOf(Character.toUpperCase(c));
23            if (pos < 0) {
24                sb.append(c);
25                continue;
26            }
27            int desplazamiento = ALFABETO.indexOf(clave.charAt(k % clave.length()));
28            char nueva = ALFABETO.charAt(((pos + sentido * desplazamiento) % N + N) % N);
29            sb.append(Character.isLowerCase(c) ? Character.toLowerCase(nueva) : nueva);
30            k++;
31        }
32        return sb.toString();
33    }
34
35    /** «Letras: 23 · Más frecuentes: O 5, Ñ 3, E 2». */
36    static String analizar(String texto) {
37        int[] veces = new int[N];
38        int letras = 0;
39        for (char c : texto.toCharArray()) {
40            int pos = ALFABETO.indexOf(Character.toUpperCase(c));
41            if (pos >= 0) {
42                veces[pos]++;
43                letras++;
44            }
45        }
46        if (letras == 0) return "Letras: 0";
47        StringBuilder sb = new StringBuilder("Letras: " + letras + " · Más frecuentes: ");
48        boolean[] usada = new boolean[N];
49        for (int vuelta = 0; vuelta < 3; vuelta++) {
50            int mejor = -1;
51            for (int i = 0; i < N; i++) {
52                // Con «>» gana la primera en el alfabeto si hay empate
53                if (!usada[i] && veces[i] > 0 && (mejor < 0 || veces[i] > veces[mejor])) mejor = i;
54            }
55            if (mejor < 0) break;
56            usada[mejor] = true;
57            if (vuelta > 0) sb.append(", ");
58            sb.append(ALFABETO.charAt(mejor)).append(' ').append(veces[mejor]);
59        }
60        return sb.toString();
61    }
62
63    public static void main(String[] args) {
64        Scanner sc = new Scanner(System.in);
65        String clave = null;
66        while (sc.hasNextLine()) {
67            String linea = sc.nextLine().trim();
68            if (linea.isEmpty()) continue;
69            int espacio = linea.indexOf(' ');
70            String orden = (espacio < 0 ? linea : linea.substring(0, espacio)).toUpperCase();
71            String argumento = espacio < 0 ? "" : linea.substring(espacio + 1).trim();
72
73            switch (orden) {
74                case "CLAVE" -> {
75                    String nueva = validarClave(argumento);
76                    if (nueva == null) {
77                        System.out.println("Clave no válida: solo letras de la A a la Z y la Ñ");
78                    } else {
79                        clave = nueva;
80                        System.out.println("Clave guardada (" + clave.length() + (clave.length() == 1 ? " letra)" : " letras)"));
81                    }
82                }
83                case "CIFRAR", "DESCIFRAR" -> {
84                    if (clave == null) System.out.println("Falta la clave");
85                    else if (argumento.isEmpty()) System.out.println("Falta el texto");
86                    else if (orden.equals("CIFRAR")) System.out.println("Cifrado: " + vigenere(argumento, clave, 1));
87                    else System.out.println("Descifrado: " + vigenere(argumento, clave, -1));
88                }
89                case "ANALIZAR" -> System.out.println(analizar(argumento));
90                default -> System.out.println("Orden desconocida: " + orden);
91            }
92        }
93    }
94}

Toda la lógica del cifrado está en un único método, vigenere, que cifra o descifra según el signo del parámetro sentido. Descifrar es desplazar hacia atrás, así que no hace falta escribir dos algoritmos.

El alfabeto como cadena convierte el problema en aritmética: indexOf da la posición, la suma con módulo hace la vuelta de la Z a la A, y charAt devuelve la letra. Con la Ñ dentro, el módulo es 27 en lugar de 26; nada más cambia.

La expresión ((x % N) + N) % N es la forma segura de calcular un módulo que nunca sea negativo en Java, C o JavaScript, donde el operador % conserva el signo del dividendo.

El análisis de frecuencias es la herramienta con la que se rompen estos cifrados: en español la E y la A son las letras más frecuentes, y en un texto largo cifrado con una clave corta esas frecuencias se pueden recuperar letra a letra de la clave.

Para ir más allá

  • Añade CESAR n texto (Vigenère con una clave de una sola letra) y comprueba que es un caso particular.
  • Rompe un cifrado de César por fuerza bruta: prueba los 27 desplazamientos y elige el que más letras E y A produzca.
  • Admite vocales con tilde cifrándolas como su vocal sin tilde y conservando la tilde al descifrar.

Dónde se explica