383. Ransom Note
Problem Description
You are given two strings: ransomNote
and magazine
. Your task is to determine whether you can construct the ransomNote
using only the letters available in magazine
.
The key constraints are:
- Each letter in
magazine
can only be used once - You need to use the exact letters from
magazine
(not just check if the letters exist) - Return
true
ifransomNote
can be constructed frommagazine
, otherwise returnfalse
For example:
- If
ransomNote = "aa"
andmagazine = "aab"
, you can construct the ransom note using two 'a's from the magazine, so returntrue
- If
ransomNote = "aa"
andmagazine = "ab"
, you cannot construct the ransom note because the magazine only has one 'a', so returnfalse
The solution uses a frequency counter to track the available letters in magazine
. As we iterate through each character in ransomNote
, we decrement the count for that character. If at any point a character's count becomes negative, it means we don't have enough of that letter in magazine
to construct the ransomNote
.
Intuition
Think of this problem like cutting letters from a physical magazine to create a ransom note. You have a limited supply of each letter in the magazine, and once you use a letter, it's gone.
The natural approach is to first take inventory of what letters we have available. We count how many of each letter appears in magazine
- this becomes our "letter bank" or available resources.
Then, as we try to build the ransomNote
, we go through it letter by letter. For each letter we need, we check our inventory and "use up" one instance of that letter by decreasing its count.
The key insight is that we don't need to build the actual ransom note - we just need to verify if we have enough letters. So we can simply track whether we ever run out of a needed letter. If at any point we need a letter but our count for it goes negative (meaning we've used more than what was available), we know it's impossible to construct the ransom note.
This is essentially a resource allocation problem: do we have enough resources (letters from magazine) to meet the demand (letters needed for ransomNote)? By tracking the availability and consumption of each letter, we can efficiently determine if construction is possible.
The beauty of this approach is that we can fail fast - as soon as we encounter a letter we don't have enough of, we can immediately return false
without checking the rest of the ransom note.
Solution Approach
We use a hash table (or an array of size 26 for lowercase English letters) to track the frequency of each character in the magazine
. In Python, we can use the Counter
class from the collections module which automatically creates a frequency map.
Here's the step-by-step implementation:
-
Create a frequency counter: First, we count all characters in
magazine
usingCounter(magazine)
. This gives us a dictionary-like object where keys are characters and values are their frequencies. -
Iterate through ransomNote: For each character
c
inransomNote
:- Decrement the count of
c
in our counter by 1:cnt[c] -= 1
- Check if the count becomes negative:
if cnt[c] < 0
- If negative, it means we've tried to use more instances of this character than available in the magazine, so return
False
- Decrement the count of
-
Return True: If we successfully process all characters in
ransomNote
without any count going negative, we can construct the ransom note, so returnTrue
.
The time complexity is O(m + n)
where m
is the length of magazine
and n
is the length of ransomNote
. We need to traverse the magazine once to build the frequency map and then traverse the ransom note once to verify availability.
The space complexity is O(1)
or O(k)
where k
is the number of unique characters. Since we're dealing with lowercase English letters, the maximum unique characters is 26, making it constant space. If using a hash table for any Unicode characters, the space would depend on the number of unique characters in the magazine.
The algorithm efficiently handles the "each letter can only be used once" constraint by tracking exact counts and ensuring we never use more letters than available.
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 with ransomNote = "aab"
and magazine = "baa"
.
Step 1: Create frequency counter for magazine
We count each character in magazine = "baa"
:
- 'b': 1 occurrence
- 'a': 2 occurrences
Our counter looks like: {'b': 1, 'a': 2}
Step 2: Process each character in ransomNote
Now we go through ransomNote = "aab"
character by character:
First character: 'a'
- Current counter:
{'b': 1, 'a': 2}
- Decrement 'a' count:
2 - 1 = 1
- Updated counter:
{'b': 1, 'a': 1}
- Is count negative? No, continue
Second character: 'a'
- Current counter:
{'b': 1, 'a': 1}
- Decrement 'a' count:
1 - 1 = 0
- Updated counter:
{'b': 1, 'a': 0}
- Is count negative? No, continue
Third character: 'b'
- Current counter:
{'b': 1, 'a': 0}
- Decrement 'b' count:
1 - 1 = 0
- Updated counter:
{'b': 0, 'a': 0}
- Is count negative? No, continue
Step 3: Return result
We successfully processed all characters without any count going negative, so we return True
.
Counter-example: If ransomNote = "aab"
and magazine = "ab"
:
- Counter after magazine:
{'a': 1, 'b': 1}
- Process first 'a': counter becomes
{'a': 0, 'b': 1}
- Process second 'a': counter would become
{'a': -1, 'b': 1}
- Since 'a' count is negative, we immediately return
False
- we don't have enough 'a's in the magazine.
Solution Implementation
1from collections import Counter
2
3class Solution:
4 def canConstruct(self, ransomNote: str, magazine: str) -> bool:
5 # Count frequency of each character in magazine
6 char_count = Counter(magazine)
7
8 # Check if each character in ransomNote can be constructed from magazine
9 for char in ransomNote:
10 # Decrement the count for current character
11 char_count[char] -= 1
12
13 # If count becomes negative, magazine doesn't have enough of this character
14 if char_count[char] < 0:
15 return False
16
17 # All characters in ransomNote can be constructed from magazine
18 return True
19
1class Solution {
2 /**
3 * Determines if a ransom note can be constructed from the letters in a magazine.
4 * Each letter in the magazine can only be used once.
5 *
6 * @param ransomNote The string to be constructed
7 * @param magazine The string containing available letters
8 * @return true if ransomNote can be constructed from magazine, false otherwise
9 */
10 public boolean canConstruct(String ransomNote, String magazine) {
11 // Array to store frequency count of each letter (26 lowercase English letters)
12 int[] letterFrequency = new int[26];
13
14 // Count the frequency of each letter in the magazine
15 for (int i = 0; i < magazine.length(); i++) {
16 char currentChar = magazine.charAt(i);
17 // Convert character to index (0-25) by subtracting 'a'
18 int charIndex = currentChar - 'a';
19 letterFrequency[charIndex]++;
20 }
21
22 // Check if ransom note can be constructed by consuming letters from magazine
23 for (int i = 0; i < ransomNote.length(); i++) {
24 char currentChar = ransomNote.charAt(i);
25 // Convert character to index (0-25) by subtracting 'a'
26 int charIndex = currentChar - 'a';
27
28 // Decrement the count for this letter and check if it goes negative
29 letterFrequency[charIndex]--;
30 if (letterFrequency[charIndex] < 0) {
31 // Not enough of this letter in magazine to construct ransom note
32 return false;
33 }
34 }
35
36 // All letters in ransom note can be constructed from magazine
37 return true;
38 }
39}
40
1class Solution {
2public:
3 bool canConstruct(string ransomNote, string magazine) {
4 // Array to store frequency count of each letter (a-z)
5 // Index 0 represents 'a', index 1 represents 'b', and so on
6 int letterCount[26] = {0};
7
8 // Count the frequency of each character in the magazine
9 for (char& ch : magazine) {
10 letterCount[ch - 'a']++;
11 }
12
13 // Check if ransom note can be constructed from magazine letters
14 for (char& ch : ransomNote) {
15 // Decrement the count for current character
16 // If count becomes negative, magazine doesn't have enough of this letter
17 if (--letterCount[ch - 'a'] < 0) {
18 return false;
19 }
20 }
21
22 // All characters in ransom note can be constructed from magazine
23 return true;
24 }
25};
26
1/**
2 * Determines if a ransom note can be constructed from the letters in a magazine.
3 * Each letter in the magazine can only be used once.
4 *
5 * @param ransomNote - The string to be constructed
6 * @param magazine - The string containing available letters
7 * @returns true if the ransom note can be constructed, false otherwise
8 */
9function canConstruct(ransomNote: string, magazine: string): boolean {
10 // Create an array to count occurrences of each letter (26 letters in alphabet)
11 // Index 0 represents 'a', index 1 represents 'b', etc.
12 const letterCount: number[] = Array(26).fill(0);
13
14 // Count each letter in the magazine
15 for (const character of magazine) {
16 // Convert character to array index (0-25) by subtracting ASCII value of 'a' (97)
17 const index: number = character.charCodeAt(0) - 97;
18 letterCount[index]++;
19 }
20
21 // Check if ransom note can be constructed by consuming letters from magazine
22 for (const character of ransomNote) {
23 // Convert character to array index and decrement the count
24 const index: number = character.charCodeAt(0) - 97;
25 letterCount[index]--;
26
27 // If count goes negative, this letter is not available in sufficient quantity
28 if (letterCount[index] < 0) {
29 return false;
30 }
31 }
32
33 // All letters in ransom note were successfully matched
34 return true;
35}
36
Time and Space Complexity
The time complexity is O(m + n)
, where m
is the length of ransomNote
and n
is the length of magazine
. This is because:
- Creating the Counter from
magazine
takesO(n)
time as it needs to iterate through all characters in the magazine string - The for loop iterates through all characters in
ransomNote
, which takesO(m)
time - Each operation inside the loop (dictionary access and comparison) takes
O(1)
time - Total:
O(n) + O(m) = O(m + n)
The space complexity is O(C)
, where C
is the size of the character set. In this problem, since we're dealing with lowercase English letters, C = 26
. The Counter object stores at most C
unique characters from the magazine string, regardless of how long the magazine string is. Even though the Counter is created from a string of length n
, it only stores unique characters and their counts, which is bounded by the size of the alphabet.
Common Pitfalls
1. Checking Character Existence Instead of Count
A frequent mistake is to only check if characters from ransomNote
exist in magazine
without considering their frequencies. This leads to incorrect results when the ransom note requires multiple instances of the same character.
Incorrect approach:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
for char in ransomNote:
if char not in magazine:
return False
return True
This would incorrectly return True
for ransomNote = "aa"
and magazine = "ab"
because it only checks if 'a' exists, not if there are enough 'a's.
Solution: Always track and decrement character counts to ensure each character is used only once.
2. Modifying the Original String
Some developers attempt to remove characters from the magazine string as they're used, which is inefficient and can lead to errors.
Inefficient approach:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
magazine_list = list(magazine)
for char in ransomNote:
if char in magazine_list:
magazine_list.remove(char) # O(n) operation each time
else:
return False
return True
This has O(n²) time complexity because remove()
is O(n) and is called for each character in ransomNote.
Solution: Use a frequency counter which provides O(1) lookup and update operations.
3. Not Handling Empty Strings Properly
While the Counter approach handles empty strings correctly, manual implementations might not consider edge cases like empty ransomNote (should return True
) or empty magazine with non-empty ransomNote (should return False
).
Solution: The Counter approach naturally handles these cases, but if implementing manually, add explicit checks or ensure your logic covers these scenarios.
4. Using Counter Subtraction Incorrectly
Some might try to use Counter's subtraction operation but misunderstand how it works with negative counts.
Potentially confusing approach:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
cnt_ransom = Counter(ransomNote)
cnt_magazine = Counter(magazine)
cnt_ransom.subtract(cnt_magazine)
return all(count <= 0 for count in cnt_ransom.values())
While this works, it's less intuitive and requires understanding that subtract()
keeps negative counts.
Solution: The presented approach of decrementing counts while iterating is clearer and provides early termination when a character is unavailable.
Which of these pictures shows the visit order of a depth-first search?
Recommended Readings
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
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!