Problem overview
The method canConstruct(ransomNote: String, magazine: String) returns true if the note can be built from letters of the magazine, where each letter of the magazine can be used only once.
With ransomNote = "aa" and magazine = "abb" the answer is false: the note needs two as and the magazine has one. With magazine = "aab" the answer is true.
The statement asks for O(n + m) time and O(Σ) extra space, where Σ is the size of the alphabet. The map only ever holds one entry per distinct letter.
Approach: compare letter counts
The order of the letters is irrelevant, in both strings. The note "abba" can be built from "ababab" because the magazine has at least as many of each letter as the note needs. In general, the answer is true exactly when, for every letter, the magazine contains at least as many copies as the note does.
A hash map from character to count records how many times each letter occurs. Build one map for the note and one for the magazine, each with a single pass over its string. Then go through the letters and check that the note never needs more of a letter than the magazine has. Searching the magazine for each letter of the note would take O(n * m); counting makes it linear.
One map would also be enough: count the magazine, then walk through the note and use up one copy of each letter, answering false as soon as a letter has no copies left. This lesson builds two maps and compares them, which keeps the counting step identical for both strings and the check separate.
Step by step
- Create two empty maps,
ransomFreqandmagazineFreq, asscala.collection.mutable.Map[Char, Int](). The defaultMapin Scala is immutable. The mutable one can be updated in place. - Count the note. A
forloop goes through each charactercofransomNote. Inside it,ransomFreq(c)is set toransomFreq.getOrElse(c, 0) + 1. A character seen for the first time starts at 0, because reading a missing key withransomFreq(c)would throw. - Count the magazine the same way into
magazineFreq. The namecexists only inside each loop, so reusing it is fine. - Loop over the alphabet. A
forloop withcharCoderuns from'a'.toIntto'z'.toInt. The range written withtoincludes its upper bound. - Convert back to a character.
val c = charCode.toChargives the letter for the current code. - Compare. If
ransomFreq.getOrElse(c, 0)is greater thanmagazineFreq.getOrElse(c, 0), the magazine can't supply that letter, and the method returnsfalse. A letter missing from either map reads as 0. - Succeed. If the loop finishes without returning, every letter has enough copies, so the method returns
true.
The solution in Scala
class Solution {
def canConstruct(ransomNote: String, magazine: String): Boolean = {
val ransomFreq = scala.collection.mutable.Map[Char, Int]()
val magazineFreq = scala.collection.mutable.Map[Char, Int]()
for (c <- ransomNote) {
ransomFreq(c) = ransomFreq.getOrElse(c, 0) + 1
}
for (c <- magazine) {
magazineFreq(c) = magazineFreq.getOrElse(c, 0) + 1
}
for (charCode <- 'a'.toInt to 'z'.toInt) {
val c = charCode.toChar
if (ransomFreq.getOrElse(c, 0) > magazineFreq.getOrElse(c, 0)) {
return false
}
}
return true
}
}
The Scala details worth knowing:
- Strings as collections.
for (c <- ransomNote)visits the characters of the string one by one, each as aChar. CharandInt. ACharis a number underneath.'a'.toIntis 97 and'z'.toIntis 122, and98.toCharis'b'. Looping over the codes and converting each one back is how the solution walks the alphabet.- Counting with
getOrElse.m(c) = m.getOrElse(c, 0) + 1is the standard counting idiom: the map update adds the key if it's new and replaces the count otherwise. returninside afor. The loop body is passed toforeachas a function, so in Scala 3.5 the compiler reports a deprecation warning about non-local returns forreturn false. The code still works, andreturnleavescanConstruct. The warning points toscala.util.boundaryas the replacement.- Lowercase only. The final loop checks only the letters
'a'to'z', so the solution assumes lowercase input. A note such as"A"with the magazine"a"would returntrue, because the uppercase letter is never checked.
Complexity
Time: O(n + m). Counting the note takes n steps, counting the magazine takes m steps, and the final loop runs 26 times. Each map operation takes constant time on average.
Space: O(Σ). Each map holds at most one entry per distinct letter, so with a 26-letter alphabet it never exceeds 26 entries.
Edge cases and common mistakes
- An empty note.
""can be built from any magazine, even"", so the result istrue. - An empty magazine.
"a"from""isfalse. - The order doesn't matter.
"abba"from"ababab"istrue. - Each letter only once. Checking only that every letter of the note appears somewhere in the magazine would say
truefor"aa"and"abb". The counts are what make the answerfalse. - Reading a missing key.
ransomFreq(c)throws for a letter that never appeared, so usegetOrElse(c, 0). - Mixing up the two maps in the comparison. The note's count must not be greater than the magazine's.
Testing
"aa"from"abb"givesfalse."aa"from"aab"givestrue."abba"from"ababab"givestrue."a"from""givesfalse.""from""givestrue.
