2227. Encrypt and Decrypt Strings
Problem Description
You need to implement a data structure that can encrypt and decrypt strings based on a given cipher system.
You're given three inputs:
keys
: An array of unique charactersvalues
: An array of strings (each of length 2) that correspond to the encrypted form of each character inkeys
dictionary
: An array of valid original strings that can appear after decryption
Encryption Process: To encrypt a string, you replace each character with its corresponding encrypted value:
- For each character
c
in the input string, find its indexi
wherekeys[i] == c
- Replace
c
withvalues[i]
(which is a 2-character string) - If any character in the input string doesn't exist in
keys
, the encryption fails and returns an empty string""
For example, if keys = ['a', 'b']
and values = ["aa", "bb"]
, then encrypting "ab"
would give "aabb"
.
Decryption Process: To decrypt a string, you reverse the process by replacing every 2-character substring (at even indices) with its original character:
- Take each substring of length 2 at even positions (0, 2, 4, ...)
- Find the index
i
wherevalues[i]
equals this substring - Replace the substring with
keys[i]
- Note: Multiple characters might encrypt to the same value, so one encrypted string could decrypt to multiple possible original strings
The key challenge is that decryption isn't unique - multiple original strings might encrypt to the same result.
Required Methods:
Encrypter(keys, values, dictionary)
: Initialize the encrypter with the given parametersencrypt(word1)
: Encryptword1
and return the encrypted stringdecrypt(word2)
: Return the count of how many strings indictionary
would encrypt toword2
The clever part of the solution is pre-computing all encrypted forms of dictionary words during initialization, then simply looking up the count when decrypt()
is called, rather than trying to decrypt and check each possibility.
Intuition
The key insight comes from understanding the asymmetry between encryption and decryption. While encryption is straightforward (each character maps to exactly one encrypted value), decryption is complex because multiple characters might encrypt to the same value, creating ambiguity.
Initially, you might think to implement decrypt()
by actually decrypting the string and checking all possible original strings against the dictionary. However, this becomes complicated because:
- We'd need to handle multiple possible decryptions due to non-unique mappings
- We'd have to generate all possible combinations when multiple characters map to the same encrypted value
- Each decrypt call would require significant computation
The breakthrough comes from reversing our perspective: instead of decrypting word2
to find matches in the dictionary, why not encrypt everything in the dictionary first and count how many encrypt to word2
?
This approach is brilliant because:
- Encryption is deterministic - each dictionary word has exactly one encrypted form
- We can pre-compute all encrypted dictionary words once during initialization
- The
decrypt()
function becomes a simple lookup operation
Think of it like this: if you want to know how many English words become "xyz" when encoded, instead of trying to decode "xyz" in all possible ways and checking which are valid English words, you encode all English words first and count how many become "xyz".
By using a Counter
to store the frequency of each encrypted dictionary word, we transform a complex decryption problem into a simple counting problem. The decrypt()
function just returns cnt[word2]
- the pre-computed count of how many dictionary words encrypt to word2
.
This pre-computation strategy trades initialization time and space for extremely fast decrypt operations, which is often the right tradeoff when decrypt might be called many times.
Learn more about Trie patterns.
Solution Approach
The solution uses two hash tables to efficiently handle encryption and decryption operations:
- Hash Table
mp
: Maps each character to its encrypted value - Hash Table
cnt
: Stores the frequency of encrypted forms of all dictionary words
Constructor Implementation:
def __init__(self, keys: List[str], values: List[str], dictionary: List[str]):
self.mp = dict(zip(keys, values))
self.cnt = Counter(self.encrypt(v) for v in dictionary)
dict(zip(keys, values))
creates a mapping wheremp[keys[i]] = values[i]
. This gives us O(1) lookup for encryption.- We iterate through each word
v
in the dictionary, encrypt it using ourencrypt
method, and count the frequency of each encrypted result using Python'sCounter
. - Time complexity: O(n + m Γ l) where n is the length of keys, m is the dictionary size, and l is the average word length.
Encryption Implementation:
def encrypt(self, word1: str) -> str:
res = []
for c in word1:
if c not in self.mp:
return ''
res.append(self.mp[c])
return ''.join(res)
- For each character
c
inword1
, we look up its encrypted value inmp
. - If any character doesn't exist in
mp
, encryption is impossible, so we return an empty string. - Otherwise, we append the 2-character encrypted value to our result list.
- Finally, we join all encrypted values to form the complete encrypted string.
- Time complexity: O(k) where k is the length of
word1
.
Decryption Implementation:
def decrypt(self, word2: str) -> int:
return self.cnt[word2]
- Since we pre-computed how many dictionary words encrypt to each possible string, decryption is just a lookup.
- We return
self.cnt[word2]
, which gives the count of dictionary words that encrypt toword2
. - If
word2
isn't incnt
, Python's Counter returns 0 by default. - Time complexity: O(1) for the lookup operation.
Why This Approach Works:
The clever trick is recognizing that we don't need to actually decrypt strings. Instead, we pre-encrypt all valid dictionary words and count their encrypted forms. This transforms the complex problem of "which dictionary words could this decrypt to?" into the simple problem of "how many dictionary words encrypt to this?"
The space complexity is O(n + m Γ l) for storing the mappings and encrypted dictionary counts, where n is the number of keys, m is the dictionary size, and l is the average length of encrypted words.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to understand how the solution works.
Given:
keys = ['a', 'b', 'c']
values = ["ei", "zf", "ei"]
(note: 'a' and 'c' both encrypt to "ei")dictionary = ["abcd", "acbd", "adbc", "badc"]
Step 1: Initialization
First, we create the character-to-encryption mapping:
mp = {'a': "ei", 'b': "zf", 'c': "ei"}
Next, we encrypt each dictionary word and count frequencies:
"abcd"
β"ei" + "zf" + "ei" + ?
β Can't encrypt 'd' (not in keys) β returns""
"acbd"
β"ei" + "ei" + "zf" + ?
β Can't encrypt 'd' β returns""
"adbc"
β Can't encrypt 'd' β returns""
"badc"
β"zf" + "ei" + ?
β Can't encrypt 'd' β returns""
Since 'd' isn't in our keys, none of these words can be encrypted. Let's modify the dictionary to ["abc", "bac", "cab", "cba"]
:
"abc"
β"ei" + "zf" + "ei"
="eizfei"
"bac"
β"zf" + "ei" + "ei"
="zfeiei"
"cab"
β"ei" + "ei" + "zf"
="eieizf"
"cba"
β"ei" + "zf" + "ei"
="eizfei"
Our counter becomes:
cnt = {"eizfei": 2, "zfeiei": 1, "eieizf": 1}
Step 2: Encrypt Operation
If we call encrypt("cab")
:
- 'c' β look up in
mp
β"ei"
- 'a' β look up in
mp
β"ei"
- 'b' β look up in
mp
β"zf"
- Join them:
"eieizf"
Step 3: Decrypt Operation
If we call decrypt("eizfei")
:
- Simply look up in
cnt["eizfei"]
β returns2
This tells us that 2 dictionary words ("abc"
and "cba"
) encrypt to "eizfei"
.
Why this is clever:
Notice that "eizfei"
could theoretically decrypt to multiple strings because both 'a' and 'c' map to "ei"
. The encrypted string "eizfei"
could represent:
"abc"
(ei=a, zf=b, ei=c)"cba"
(ei=c, zf=b, ei=a)"aba"
(ei=a, zf=b, ei=a)"cbc"
(ei=c, zf=b, ei=c)
But we only care about which ones are in our dictionary! By pre-encrypting the dictionary words, we avoid the complex task of generating all possible decryptions and checking each one. Instead, we just count how many dictionary words produce this encrypted result.
Solution Implementation
1from typing import List
2from collections import Counter
3
4
5class Encrypter:
6 def __init__(self, keys: List[str], values: List[str], dictionary: List[str]):
7 """
8 Initialize the Encrypter with encryption mappings and a dictionary.
9
10 Args:
11 keys: List of characters to be encrypted
12 values: List of corresponding encrypted strings for each key
13 dictionary: List of valid words to be used for decryption counting
14 """
15 # Create a mapping from each key character to its encrypted value
16 self.encryption_map = dict(zip(keys, values))
17
18 # Pre-compute encrypted versions of all dictionary words
19 # and count how many dictionary words encrypt to each result
20 self.encrypted_word_count = Counter(
21 self.encrypt(word) for word in dictionary
22 )
23
24 def encrypt(self, word1: str) -> str:
25 """
26 Encrypt a word by replacing each character with its mapped value.
27
28 Args:
29 word1: The word to encrypt
30
31 Returns:
32 The encrypted string, or empty string if any character cannot be encrypted
33 """
34 encrypted_parts = []
35
36 # Process each character in the word
37 for char in word1:
38 # If character has no encryption mapping, return empty string
39 if char not in self.encryption_map:
40 return ''
41 # Add the encrypted value for this character
42 encrypted_parts.append(self.encryption_map[char])
43
44 # Join all encrypted parts into final encrypted word
45 return ''.join(encrypted_parts)
46
47 def decrypt(self, word2: str) -> int:
48 """
49 Return the count of dictionary words that could encrypt to the given string.
50
51 Args:
52 word2: The encrypted string to decrypt
53
54 Returns:
55 Number of possible original words from the dictionary
56 """
57 # Return pre-computed count of dictionary words that encrypt to word2
58 return self.encrypted_word_count[word2]
59
60
61# Your Encrypter object will be instantiated and called as such:
62# obj = Encrypter(keys, values, dictionary)
63# param_1 = obj.encrypt(word1)
64# param_2 = obj.decrypt(word2)
65
1class Encrypter {
2 // Map to store character to string encryption mappings
3 private Map<Character, String> encryptionMap = new HashMap<>();
4 // Map to store encrypted strings and their count from dictionary
5 private Map<String, Integer> encryptedWordCount = new HashMap<>();
6
7 /**
8 * Constructor to initialize the Encrypter with keys, values, and dictionary
9 * @param keys Array of characters to be encrypted
10 * @param values Array of corresponding encrypted strings for each key
11 * @param dictionary Array of valid words to pre-process for decryption
12 */
13 public Encrypter(char[] keys, String[] values, String[] dictionary) {
14 // Build the encryption mapping from keys to values
15 for (int i = 0; i < keys.length; i++) {
16 encryptionMap.put(keys[i], values[i]);
17 }
18
19 // Pre-compute encrypted versions of all dictionary words
20 // and count how many words encrypt to the same result
21 for (String word : dictionary) {
22 String encryptedWord = encrypt(word);
23 encryptedWordCount.merge(encryptedWord, 1, Integer::sum);
24 }
25 }
26
27 /**
28 * Encrypts the given word using the encryption mapping
29 * @param word1 The word to be encrypted
30 * @return The encrypted string, or empty string if any character cannot be encrypted
31 */
32 public String encrypt(String word1) {
33 StringBuilder encryptedResult = new StringBuilder();
34
35 // Process each character in the word
36 for (char character : word1.toCharArray()) {
37 // Check if the character can be encrypted
38 if (!encryptionMap.containsKey(character)) {
39 return ""; // Return empty string if character has no mapping
40 }
41 // Append the encrypted value for this character
42 encryptedResult.append(encryptionMap.get(character));
43 }
44
45 return encryptedResult.toString();
46 }
47
48 /**
49 * Returns the count of possible original words from dictionary that could
50 * have been encrypted to produce the given encrypted string
51 * @param word2 The encrypted string to decrypt
52 * @return The number of possible original words
53 */
54 public int decrypt(String word2) {
55 // Return the pre-computed count, or 0 if not found
56 return encryptedWordCount.getOrDefault(word2, 0);
57 }
58}
59
60/**
61 * Your Encrypter object will be instantiated and called as such:
62 * Encrypter obj = new Encrypter(keys, values, dictionary);
63 * String param_1 = obj.encrypt(word1);
64 * int param_2 = obj.decrypt(word2);
65 */
66
1class Encrypter {
2public:
3 // Map to store count of encrypted strings from dictionary
4 unordered_map<string, int> encryptedCount;
5 // Map to store character to string value mapping for encryption
6 unordered_map<char, string> charToValueMap;
7
8 /**
9 * Constructor: Initializes the encrypter with keys, values, and dictionary
10 * @param keys: List of characters that can be encrypted
11 * @param values: Corresponding encrypted values for each key
12 * @param dictionary: List of valid words to pre-process for decryption
13 */
14 Encrypter(vector<char>& keys, vector<string>& values, vector<string>& dictionary) {
15 // Build the character to value mapping for encryption
16 for (int i = 0; i < keys.size(); ++i) {
17 charToValueMap[keys[i]] = values[i];
18 }
19
20 // Pre-compute encrypted versions of all dictionary words
21 // and count occurrences of each encrypted string
22 for (const auto& word : dictionary) {
23 string encryptedWord = encrypt(word);
24 if (!encryptedWord.empty()) {
25 encryptedCount[encryptedWord]++;
26 }
27 }
28 }
29
30 /**
31 * Encrypts a given word by replacing each character with its mapped value
32 * @param word1: The word to encrypt
33 * @return: Encrypted string, or empty string if any character cannot be encrypted
34 */
35 string encrypt(string word1) {
36 string encryptedResult = "";
37
38 // Process each character in the input word
39 for (char currentChar : word1) {
40 // Check if the character exists in our mapping
41 if (charToValueMap.find(currentChar) == charToValueMap.end()) {
42 // Character cannot be encrypted, return empty string
43 return "";
44 }
45 // Append the encrypted value to result
46 encryptedResult += charToValueMap[currentChar];
47 }
48
49 return encryptedResult;
50 }
51
52 /**
53 * Returns the count of dictionary words that encrypt to the given string
54 * @param word2: The encrypted string to decrypt
55 * @return: Number of possible original words from dictionary
56 */
57 int decrypt(string word2) {
58 // Return the pre-computed count for this encrypted string
59 // Returns 0 if the encrypted string doesn't exist in the map
60 return encryptedCount.count(word2) ? encryptedCount[word2] : 0;
61 }
62};
63
64/**
65 * Your Encrypter object will be instantiated and called as such:
66 * Encrypter* obj = new Encrypter(keys, values, dictionary);
67 * string param_1 = obj->encrypt(word1);
68 * int param_2 = obj->decrypt(word2);
69 */
70
1// Map to store character to encryption value mappings
2const encryptionMap: Map<string, string> = new Map();
3
4// Map to store encrypted word frequency count
5const encryptedWordCount: Map<string, number> = new Map();
6
7/**
8 * Initializes the encrypter with keys, values, and dictionary
9 * @param keys - Array of characters to be encrypted
10 * @param values - Array of corresponding encryption values for each key
11 * @param dictionary - Array of valid words that can be formed after decryption
12 */
13function initializeEncrypter(keys: string[], values: string[], dictionary: string[]): void {
14 // Clear existing maps
15 encryptionMap.clear();
16 encryptedWordCount.clear();
17
18 // Build the encryption mapping from keys to values
19 for (let i = 0; i < keys.length; i++) {
20 encryptionMap.set(keys[i], values[i]);
21 }
22
23 // Pre-compute encrypted versions of all dictionary words
24 // and count how many words encrypt to the same result
25 for (const word of dictionary) {
26 const encryptedWord = encrypt(word);
27
28 // Only count valid encryptions (non-empty results)
29 if (encryptedWord !== '') {
30 const currentCount = encryptedWordCount.get(encryptedWord) || 0;
31 encryptedWordCount.set(encryptedWord, currentCount + 1);
32 }
33 }
34}
35
36/**
37 * Encrypts a word by replacing each character with its mapped value
38 * @param word - The word to encrypt
39 * @returns The encrypted string, or empty string if any character cannot be encrypted
40 */
41function encrypt(word: string): string {
42 let encryptedResult = '';
43
44 // Process each character in the word
45 for (const character of word) {
46 // Check if the character has an encryption mapping
47 if (!encryptionMap.has(character)) {
48 // Return empty string if any character cannot be encrypted
49 return '';
50 }
51
52 // Append the encrypted value to the result
53 encryptedResult += encryptionMap.get(character);
54 }
55
56 return encryptedResult;
57}
58
59/**
60 * Returns the number of possible decryptions for an encrypted word
61 * @param word - The encrypted word to decrypt
62 * @returns The count of valid dictionary words that could produce this encryption
63 */
64function decrypt(word: string): number {
65 // Return the pre-computed count, or 0 if not found
66 return encryptedWordCount.get(word) || 0;
67}
68
Time and Space Complexity
Time Complexity:
-
__init__
:O(n * k + m)
wheren
is the length of the dictionary,k
is the average length of words in the dictionary, andm
is the length of the keys array. Creating the mapping takesO(m)
time, and encrypting all dictionary words takesO(n * k)
time since each word needs to be processed character by character. -
encrypt
:O(k)
wherek
is the length of the input word. Each character lookup in the mapping isO(1)
, and we process each character once. -
decrypt
:O(1)
since it's just a dictionary lookup in the Counter object.
Space Complexity: O(n * k + m)
- The mapping
self.mp
storesm
key-value pairs:O(m)
- The Counter
self.cnt
stores up ton
encrypted strings, where each encrypted string can be up to2k
characters long (since each character maps to a 2-character string based on the problem context):O(n * k)
- Overall space complexity is
O(n * k + m)
, which can be simplified toO(n + m)
when considering thatk
is typically bounded by a constant in most practical scenarios.
The reference answer O(n + m)
assumes that the average word length k
is constant or bounded, which is a reasonable assumption for most dictionary problems.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Attempting to Actually Decrypt the String
The Pitfall:
A natural instinct is to implement the decrypt
method by actually reversing the encryption process - parsing the encrypted string into 2-character chunks, finding matching values, and building all possible original strings. This approach has several problems:
- One encrypted string can map to multiple original strings (when different characters encrypt to the same value)
- The decryption tree can grow exponentially with the string length
- You'd need to check each decrypted possibility against the dictionary
Why It's Wrong:
# INCORRECT approach - trying to actually decrypt
def decrypt(self, word2: str) -> int:
# This would require complex backtracking to find all possibilities
possible_words = []
# Parse word2 into 2-char chunks and try to reverse map...
# This becomes exponentially complex!
The Solution: Pre-compute all encrypted forms of dictionary words during initialization, then simply look up the count. This transforms an exponential problem into O(1) lookup.
2. Not Handling Empty Strings in Counter
The Pitfall: When encrypting dictionary words in the constructor, some words might fail to encrypt (return empty string). If you don't handle this, the Counter might include empty strings.
# PROBLEMATIC: Empty strings get counted self.encrypted_word_count = Counter( self.encrypt(word) for word in dictionary ) # If encrypt returns "", Counter will count empty strings
The Solution: Filter out empty strings before counting:
self.encrypted_word_count = Counter( encrypted for word in dictionary if (encrypted := self.encrypt(word)) )
3. Assuming One-to-One Mapping
The Pitfall: Incorrectly assuming that each character maps to a unique encrypted value, or that each encrypted value maps back to a unique character. The problem explicitly allows multiple characters to encrypt to the same value.
Why It Matters:
If keys = ['a', 'b']
and values = ['xx', 'xx']
, both 'a' and 'b' encrypt to 'xx'. So 'xx' could decrypt to either 'a' or 'b', and 'xxxx' could decrypt to 'aa', 'ab', 'ba', or 'bb'.
4. Incorrect String Parsing in Decryption Logic
The Pitfall: If you were to implement actual decryption, you might incorrectly parse the encrypted string:
# WRONG: Assumes we need to decrypt at odd positions or variable lengths
for i in range(0, len(word2), 2):
chunk = word2[i:i+2] # What if len(word2) is odd?
The Solution: The problem guarantees that encrypted strings have even length (each character becomes 2 characters), but our approach avoids this parsing entirely by pre-computing encrypted forms.
5. Not Using Efficient Data Structures
The Pitfall: Using lists and linear search instead of hash tables:
# INEFFICIENT: O(n) lookup for each character
def encrypt(self, word1: str) -> str:
result = []
for c in word1:
for i, key in enumerate(self.keys): # O(n) search
if key == c:
result.append(self.values[i])
break
else:
return ''
The Solution:
Use a dictionary for O(1) lookup: self.encryption_map = dict(zip(keys, values))
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
Recommended Readings
Trie Introduction Imagine you have a magic dictionary where as soon as you start spelling a word it immediately tells you how many words that start with those letters How cool would that be This is exactly the kind of magic a data structure called trie performs What's a Trie
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!