Problem Description:
Crypt kicker algorithm is a well-known encryption algorithm but less secure. This UVa problem is to decrypt lines based on this method. The problem statement is as follows:
843 Crypt Kicker
A common but insecure method of encrypting text is to permute the letters of the alphabet. That is, in the text, each letter of the alphabet is consistently replaced by some other letter. So as to ensure that the encryption is reversible, no two letters are replaced by the same letter. Your task is to decrypt several encoded lines of text, assuming that each line uses a different set of replacements, and that all words in the decrypted text are from a dictionary of known words.
Input
The input consists of a line containing an integer n, followed by n lower case words, one per line, in alphabetical order. These n words comprise the dictionary of words which may appear in the decrypted text. Following the dictionary are several lines of input. Each line is encrypted as described above. There are no more than 1000 words in the dictionary. No word exceeds 16 letters. The encrypted lines contain only lower case letters and spaces and do not exceed 80 characters in length.
Output
Decrypt each line and print it to standard output. If there is more than one solution, any will do. If there is no solution, replace every letter of the alphabet by an asterisk.
Sample Input
6 and dick jane puff spot yertle bjvg xsb hxsn xsb qymm xsb rqat xsb pnetfn xxxx yyy zzzz www yyyy aaa bbbb ccc dddddd
Sample Output
dick and jane and puff and spot and yertle
**** * **** * **** * **** * ******
What went wrong?
I had created a Java code to solve it. The algorithm I used is illustrated below. The code seems to be correct. It passes all test cases I had fed to it including uDebug test cases. However, when submitting my code to UVA online judge, it always returns me WRONG ANSWER verdict! Can anyone identify my problem? Does the code have a hidden flaw? or it is the online judge problem?!
Any Help is Highly HIGHLY Appreciated!
Algorithm description:
A first idea coming in mind to solve this problem is to permute all the letters in the alphabet in order to find a mapping. However, this solution is computationally huge. A better Idea is to use backtracking. You map an encrypted word to a dictionary word which has the same length and pattern as the encrypted word. [ to show what word patterns means: 'abc' might be mapped to 'her' however, it cannot be mapped to 'see' as the pattern is dissimilar " three distinct letters word cannot be mapped to two distinct letters word" I took this beautiful idea from this discussion ]. If you find a mapping to this first word, then move to the next word and map it. If the second word did not have a solution then go back to the first word and try another mapping and so on. If there is no suitable mapping to the first word, then declare no solution. For the mapping, I mapped longest words first as they are harder to map.
The code:
Here is my code. I tried to comment out important lines. However, if you find any confusion just ask and I will explain with full details. Thanks,
import java.util.*;
public class P843 {
public static void main(String[] args) {
Scanner c = new Scanner(System.in);
int dictionarySize = c.nextInt();
c.nextLine();
// get the dictionary from the input stream:
String[] words = new String[dictionarySize];
for (int i = 0; i < words.length; i++)
words[i] = c.nextLine();
// get encrypted input
String line = c.nextLine();
while (c.hasNextLine()) {
if (line.length() == 0) {
System.out.println();
line = c.nextLine();
continue;
}
ArrayList<String> eWordsList = new ArrayList<>();
for(String eWord : line.split(" "))
if(eWord.length() != 0)
eWordsList.add(eWord);
String[] eWords = eWordsList.toArray(new String[0]);
String[] dWords = decryptWords(eWords, words);
for (int i = 0; i < dWords.length; i++)
if (i != dWords.length - 1)
System.out.print(dWords[i] + " ");
else
System.out.println(dWords[i]);
line = c.nextLine();
}
}
private static String[] decryptWords(String[] eWords, String[] words) {
String[] dWords = new String[eWords.length];
// get copy of the dWords so that the original version not destroyed
String[] eWordsCopy = Arrays.copyOf(eWords, eWords.length);
// sort by length from the greatest to the smallest
Arrays.sort(eWordsCopy, new Comparator<String>() {
@Override
public int compare(String w1, String w2) {
return Integer.compare(w2.length(), w1.length());
}
});
// initialize a key array to hold decrypted letters:
char[][] keyArray = new char[2][26];
for (int i = 0; i < 26; i++) {
keyArray[0][i] = (char) ('a' + i);
keyArray[1][i] = '*';
}
// restore keyArray original values if there is no solution to all words
if (!matchWords(words, eWordsCopy, keyArray))
for (int i = 0; i < 26; i++) {
keyArray[0][i] = (char) ('a' + i);
keyArray[1][i] = '*';
}
for (int i = 0; i < eWords.length; i++) {
StringBuilder temp = new StringBuilder();
for (int j = 0; j < eWords[i].length(); j++)
temp.append(keyArray[1][eWords[i].charAt(j) - 97]);
dWords[i] = temp.toString();
}
return dWords;
}
private static boolean matchWords(String[] words, String[] eWords, char[][] keyArray) {
ArrayList<String> promisingWords = new ArrayList<>();
String eWord = eWords[0];
// store the current state of keyArray
char[][] originalKeyArray = new char[2][26];
for (int i = 0; i < keyArray.length; i++)
if (keyArray[i].length >= 0) System.arraycopy(keyArray[i], 0, originalKeyArray[i], 0, keyArray[i].length);
// get promising words that may match
for (String word : words)
if (word.length() == eWord.length()
&& wordPattern(word).equals(wordPattern(eWord)))
promisingWords.add(word);
for (String word : promisingWords) {
if (mapWord(eWord, word, keyArray)) {
if (eWords.length > 1) {
// recursive call:
if (matchWords(words, Arrays.copyOfRange(eWords, 1, eWords.length), keyArray))
return true;
else {
// remove the word from the dictionary to try another one
for (int i = 0; i < keyArray.length; i++)
if (keyArray[i].length >= 0)
System.arraycopy(originalKeyArray[i], 0, keyArray[i], 0, keyArray[i].length);
}
}
// if there are now decrypted words, then return true
else
return true;
}
}
// if there is no word mapped, then return false
return false;
}
private static boolean mapWord(String eWord, String word, char[][] keyArray) {
// store the current state of keyArray
char[][] originalKeyArray = new char[2][26];
for (int i = 0; i < keyArray.length; i++)
if (keyArray[i].length >= 0) System.arraycopy(keyArray[i], 0, originalKeyArray[i], 0, keyArray[i].length);
// check one-to-one from the decrypted word to the dictionary word:
for (int i = 0; i < eWord.length(); i++)
if ((keyArray[1][eWord.charAt(i) - 97] != word.charAt(i)
&& keyArray[1][eWord.charAt(i) - 97] != '*')
|| !isLetterMapped(eWord.charAt(i), word.charAt(i), keyArray)) {
// restore original array back
for (int j = 0; j < keyArray.length; j++)
if (keyArray[j].length >= 0)
System.arraycopy(originalKeyArray[j], 0, keyArray[j], 0, keyArray[j].length);
return false;
}
// update the key array:
else
keyArray[1][eWord.charAt(i) - 97] = word.charAt(i);
return true;
}
private static boolean isLetterMapped(char eLetter, char letter, char[][] keyArray) {
for (int i = 0; i < 26; i++)
if (keyArray[1][i] == letter && keyArray[0][i] != eLetter)
return false;
return true;
}
// generate a word pattern
private static String wordPattern(String word) {
if (word.length() > 0) {
StringBuilder mapped = new StringBuilder();
int count = 0;
HashMap<Character, Character> mapping = new HashMap<>();
for (int i = 0; i < word.length(); i++)
if (!mapping.containsKey(word.charAt(i)))
mapping.put(word.charAt(i), (char) (48 + count++));
for (int i = 0; i < word.length(); i++)
mapped.append(mapping.get(word.charAt(i)));
return mapped.toString();
} else {
return "";
}
}
}