Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

2227. Encrypt and Decrypt Strings - Leetcode Solution

Code Implementation

class Encrypter:
    def __init__(self, keys, values, dictionary):
        self.char_to_code = {k: v for k, v in zip(keys, values)}
        self.code_to_chars = {}
        for k, v in zip(keys, values):
            self.code_to_chars.setdefault(v, []).append(k)
        self.dictionary = set(dictionary)
        self.encrypted_count = {}
        for word in dictionary:
            enc = self.encrypt(word)
            self.encrypted_count[enc] = self.encrypted_count.get(enc, 0) + 1

    def encrypt(self, word1: str) -> str:
        res = []
        for c in word1:
            if c not in self.char_to_code:
                return ""
            res.append(self.char_to_code[c])
        return ''.join(res)

    def decrypt(self, word2: str) -> int:
        return self.encrypted_count.get(word2, 0)
      
class Encrypter {
public:
    unordered_map<char, string> charToCode;
    unordered_map<string, vector<char>> codeToChars;
    unordered_map<string, int> encryptedCount;

    Encrypter(vector<char>& keys, vector<string>& values, vector<string>& dictionary) {
        for (int i = 0; i < keys.size(); ++i) {
            charToCode[keys[i]] = values[i];
            codeToChars[values[i]].push_back(keys[i]);
        }
        for (auto& word : dictionary) {
            string enc = encrypt(word);
            encryptedCount[enc]++;
        }
    }

    string encrypt(string word1) {
        string res = "";
        for (char c : word1) {
            if (!charToCode.count(c)) return "";
            res += charToCode[c];
        }
        return res;
    }

    int decrypt(string word2) {
        return encryptedCount.count(word2) ? encryptedCount[word2] : 0;
    }
};
      
class Encrypter {
    private Map<Character, String> charToCode = new HashMap<>();
    private Map<String, List<Character>> codeToChars = new HashMap<>();
    private Map<String, Integer> encryptedCount = new HashMap<>();

    public Encrypter(char[] keys, String[] values, String[] dictionary) {
        for (int i = 0; i < keys.length; i++) {
            charToCode.put(keys[i], values[i]);
            codeToChars.computeIfAbsent(values[i], k -> new ArrayList<>()).add(keys[i]);
        }
        for (String word : dictionary) {
            String enc = encrypt(word);
            encryptedCount.put(enc, encryptedCount.getOrDefault(enc, 0) + 1);
        }
    }

    public String encrypt(String word1) {
        StringBuilder sb = new StringBuilder();
        for (char c : word1.toCharArray()) {
            if (!charToCode.containsKey(c)) return "";
            sb.append(charToCode.get(c));
        }
        return sb.toString();
    }

    public int decrypt(String word2) {
        return encryptedCount.getOrDefault(word2, 0);
    }
}
      
class Encrypter {
    constructor(keys, values, dictionary) {
        this.charToCode = {};
        this.codeToChars = {};
        for (let i = 0; i < keys.length; ++i) {
            this.charToCode[keys[i]] = values[i];
            if (!this.codeToChars[values[i]]) this.codeToChars[values[i]] = [];
            this.codeToChars[values[i]].push(keys[i]);
        }
        this.encryptedCount = {};
        for (let word of dictionary) {
            let enc = this.encrypt(word);
            this.encryptedCount[enc] = (this.encryptedCount[enc] || 0) + 1;
        }
    }

    encrypt(word1) {
        let res = [];
        for (let c of word1) {
            if (!(c in this.charToCode)) return "";
            res.push(this.charToCode[c]);
        }
        return res.join('');
    }

    decrypt(word2) {
        return this.encryptedCount[word2] || 0;
    }
}
      

Problem Description

You are given three inputs:

  • keys: An array of characters representing the possible original letters.
  • values: An array of strings, where values[i] is the code for keys[i].
  • dictionary: An array of valid words, all of which can be encrypted using the provided keys and values.
You are to implement a class Encrypter with the following methods:
  • encrypt(word1): Encrypts a string word1 by replacing each character with its corresponding code (from keys and values), concatenating the codes.
  • decrypt(word2): Returns the number of words in dictionary that, when encrypted, equal word2.
Key constraints:
  • Each character in word1 must be present in keys to be encrypted; otherwise, return an empty string.
  • For decryption, count how many words in the dictionary could have produced the encrypted string.

Thought Process

The problem requires us to create a two-way mapping: from character to code (for encryption), and from code to possible characters (for decryption).
For encrypt, the mapping is straightforward: look up each character and append its code.
For decrypt, the brute-force way would be to generate all possible words that could map to the encrypted string, but this is inefficient. Instead, since we have a finite dictionary, we can precompute the encrypted version of every word in the dictionary and simply count the matches.
This approach shifts from brute-force generation to efficient lookup, leveraging the constraints that all valid words are from the given dictionary.

Solution Approach

We design the Encrypter class as follows:

  1. Build the mappings:
    • Create a dictionary from character to code for encryption.
    • Create a dictionary from code to possible characters for (potential) decryption.
  2. Precompute encrypted dictionary:
    • For every word in the dictionary, encrypt it and store the count of each encrypted string.
    • This allows us to answer decrypt queries in O(1) time using a hash map.
  3. Encrypt method:
    • For each character in the input, use the character-to-code mapping.
    • If any character is not in the mapping, return an empty string.
    • Concatenate all codes to form the encrypted string.
  4. Decrypt method:
    • Look up the input encrypted string in the precomputed hash map and return its count.
    • If not found, return 0.
This approach is efficient because it avoids expensive backtracking or permutation generation, leveraging hash maps for constant-time lookups.

Example Walkthrough

Suppose:

  • keys = ['a', 'b', 'c', 'd']
  • values = ['ei', 'zf', 'ei', 'am']
  • dictionary = ['abcd', 'acbd', 'adbc', 'badc', 'dacb', 'cadb', 'cbda', 'abad']
Encrypting "abcd":
  • 'a' → 'ei'
  • 'b' → 'zf'
  • 'c' → 'ei'
  • 'd' → 'am'
  • Result: 'eizfeiam'
Decrypting "eizfeiam":
  • We look up 'eizfeiam' in our precomputed hash map.
  • Count how many words in the dictionary encrypt to 'eizfeiam'.
  • Suppose 'abcd' and 'acbd' both result in 'eizfeiam', so the answer is 2.
This illustrates how the mapping and lookup work in practice.

Time and Space Complexity

Brute-force Approach:

  • For decryption, generating all possible word candidates is exponential in the length of the encrypted string: O(k^n), where k is the average number of possible characters per code and n is the number of code segments.
  • This is infeasible for large dictionaries or long strings.
Optimized Approach:
  • Preprocessing (building the encrypted dictionary): O(D * L), where D is the size of the dictionary and L is the average word length.
  • Encryption: O(L), where L is the length of the input word.
  • Decryption: O(1), just a hash map lookup.
  • Space: O(D), for storing the encrypted forms of all dictionary words.
This makes the solution scalable and efficient.

Summary

The key insight is to avoid brute-force word generation by leveraging the finite dictionary and precomputing all possible encrypted forms. Efficient hash map lookups make both encryption and decryption fast. The solution is elegant because it turns an apparently complex decryption task into a simple counting problem by smart use of preprocessing and data structures.