In the official SOWPODS Scrabble dictionary...

In [2]:
var fetch = require('node-fetch');

var src = 'https://raw.githubusercontent.com/jesstess/Scrabble/master/scrabble/sowpods.txt';
var dict;

// fetch and clean the dictionary
fetch(src)
    .then(resp => resp.text())
    .then(text => {
        dict = text.split(/\r*\n/g);
        dict = dict.filter(word => word.length); // remove any empty words
    
        console.log(dict.length + ' words in total');
    });
267751 words in total

What words contain "UU"?

In [2]:
dict.filter(word => /UU/.test(word));
Out[2]:
[ 'BUSUUTI',
  'BUSUUTIS',
  'CARDUUS',
  'CARDUUSES',
  'CONTINUUM',
  'CONTINUUMS',
  'DUUMVIR',
  'DUUMVIRAL',
  'DUUMVIRATE',
  'DUUMVIRATES',
  'DUUMVIRI',
  'DUUMVIRS',
  'INDIVIDUUM',
  'LITUUS',
  'LITUUSES',
  'MENSTRUUM',
  'MENSTRUUMS',
  'MUTUUM',
  'MUTUUMS',
  'MUUMUU',
  'MUUMUUS',
  'PARAMENSTRUUM',
  'PARAMENSTRUUMS',
  'RESIDUUM',
  'RESIDUUMS',
  'SQUUSH',
  'SQUUSHED',
  'SQUUSHES',
  'SQUUSHING',
  'TRIDUUM',
  'TRIDUUMS',
  'ULTRAVACUUM',
  'ULTRAVACUUMS',
  'VACUUM',
  'VACUUMED',
  'VACUUMING',
  'VACUUMS',
  'WELTANSCHAUUNG',
  'WELTANSCHAUUNGS' ]

What is the big-O complexity of the solution?

The solution is $O(n)$ for an input word of length $n$ (with the coefficient of $n$ depending on the regex matcher implementation).

What is another way of solving the problem?

Other ways of solving the problem are either directly equivalent to or less efficient than this method.
For example, we could implement filtering and regex matching ourselves ($O(n)$):

In [ ]:
var UU = [];

for (var word of dict) {
    for (var i = 0; i < word.length - 1; i++) {
        // equivalent to regex matching
        if (word[i] == 'U') {
            if (word[i+1] == 'U') {
                UU.push(word);
                break;
                
            } else {
                i++; // skip the following letter, since it can't be the start of 'UU'
            }
        }
    }
}

Is there a faster solution?

There is no faster solution: in the worst case (the word doesn't contain "UU"), all $n$ letters in a word must be examined to check for a match with this regular expression.

What words contain "Q" without "U"?

In [3]:
// match any number of characters other than 'U', then 'Q', then any number of characters other than 'U'
dict.filter(word => /^[^U]*Q[^U]*$/.test(word));
Out[3]:
[ 'FAQIR',
  'FAQIRS',
  'INQILAB',
  'INQILABS',
  'MBAQANGA',
  'MBAQANGAS',
  'NIQAB',
  'NIQABS',
  'QABALA',
  'QABALAH',
  'QABALAHS',
  'QABALAS',
  'QABALISM',
  'QABALISMS',
  'QABALIST',
  'QABALISTIC',
  'QABALISTS',
  'QADI',
  'QADIS',
  'QAID',
  'QAIDS',
  'QAIMAQAM',
  'QAIMAQAMS',
  'QALAMDAN',
  'QALAMDANS',
  'QANAT',
  'QANATS',
  'QASIDA',
  'QASIDAS',
  'QAT',
  'QATS',
  'QAWWAL',
  'QAWWALI',
  'QAWWALIS',
  'QAWWALS',
  'QI',
  'QIBLA',
  'QIBLAS',
  'QIGONG',
  'QIGONGS',
  'QINDAR',
  'QINDARKA',
  'QINDARS',
  'QINTAR',
  'QINTARS',
  'QIS',
  'QOPH',
  'QOPHS',
  'QORMA',
  'QORMAS',
  'QWERTIES',
  'QWERTY',
  'QWERTYS',
  'SHEQALIM',
  'SHEQEL',
  'SHEQELS',
  'TALAQ',
  'TALAQS',
  'TRANQ',
  'TRANQS',
  'TSADDIQ',
  'TSADDIQIM',
  'TSADDIQS',
  'TZADDIQ',
  'TZADDIQIM',
  'TZADDIQS',
  'WAQF',
  'WAQFS',
  'YAQONA',
  'YAQONAS' ]

What is the big-O complexity of the solution?

The solution is $O(n)$ for an input word of length $n$ (with the coefficient of $n$ depending on the regex matcher implementation).

What is another way of solving the problem?

Other ways of solving the problem are either directly equivalent to or less efficient than this method.
For example, we could filter by words containing "Q" and then by words not containing "U" ($O(n^2)$):

In [ ]:
dict.filter(word => /Q/.test(word))
    .filter(word => !/U/.test(word));

Is there a faster solution?

There is no faster solution: in the worst case (the word doesn't contain either "Q" or "U"), all $n$ letters in a word must be examined to check for a match with this regular expression.

What letters, if any, never appear doubled?

In [4]:
var undoubled = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // letters that have not appeared doubled

for (var word of dict) {
    for (var i = 0; i < word.length - 1; i++) {
        // if all the letters have already appeared doubled, stop looking
        if (!undoubled) {
            break;
        }
        
        if (word[i] == word[i+1]) {
            // remove a doubled letter
            undoubled = undoubled.replace(word[i], '');
            i++;
        }
    }
}

console.log(undoubled.split(''));
[ 'Q', 'X' ]

What is the big-O complexity of the solution?

The solution is $O(n)$ for a set of input words of total length $n$ (that is, $n$ is the sum of the lengths of all of the words in the set).
Alternatively, the solution is $O(m)$ for a set of input words of size $m$ (that is, there are $m$ words in the set).

What is another way of solving the problem?

Other ways of solving the problem are either directly equivalent to or less efficient than this method.
For example, we could search for each pair of doubled letters individually ($O(n)$, with a larger coefficient of $n$ than in the original solution):

In [ ]:
var undoubled = [],
    doubled;

for (var letter of 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') {
    var regex = new RegExp(letter + letter); // construct a regex matching the current letter, doubled
    doubled = false;
    
    // search for the doubled letter in each word
    for (var word of dict) {
        if (regex.test(word)) {
            doubled = true;
            break;
        }
    }

    // if the letter has not appeared doubled in any word, add it to undoubled    
    if (!doubled) {
        undoubled.push(letter);
    }
}

Is there a faster solution?

There is no faster solution: in the worst case (no letters ever appear doubled), every letter of every word must be examined to look for a doubled letter.

What is the longest palindrome?

In [5]:
var maxLen = 0, 
    maxPal = [];

for (var word of dict) {
    var wordLen = word.length;

    // advance the start and end indices until they meet
    for (var start = 0, end = wordLen - 1; 
         // if the letters at start and end do not match, the word is not a palindrome
         start < end && word[start] == word[end];
         start++, end--);
    
    // if the start and end indices met, the word is a palindrome
    if (start >= end) {
        if (wordLen > maxLen) {
            // this palindrome is longer than the previous longest
            maxLen = wordLen;
            maxPal = [word];

        } else if (wordLen == maxLen) {
            // this palindrome is as long as the previous longest
            maxPal.push(word);
        }
    }
}

console.log(maxPal);
[ 'ROTAVATOR' ]

What is the big-O complexity of the solution?

The solution is $O(n)$ for a set of input words of total length $n$ (that is, $n$ is the sum of the lengths of all of the words in the set).

What is another way of solving the problem?

Other ways of solving the problem are either directly equivalent to or less efficient than this method.
For example, we could sort the words by decreasing length and then search for a palindrome from the start. With a typical comparison-based sorting algorithm, in the worst case (none of the words are palindromes), this is $O(m\log(m) + n)$, where $m$ is the total number of input words.

In [ ]:
// make a copy of the list of words and sort it in-place by decreasing length
var sortDict = [...dict];
sortDict.sort((wordA, wordB) => wordA.length > wordB.length ? -1 : 1);

var maxPal = [],
    maxLen = 0;
    
for (var word of sortDict) {
    // we have checked all the words of the same or greater length as the longest 
    // palindrome, so stop looking
    if (word.length < maxLen) {
        break;
    }
    
    // advance the start and end indices until they meet
    for (var start = 0, end = word.length - 1;
         // if the letters at start and end do not match, the word is not a palindrome
         start < end && word[start] == word[end];
         start++, end--);
    
    // if the start and end indices met, the word is a palindrome
    if (start >= end) {
        maxPal.push(word);
        maxLen = word.length;
    }
}

Is there a faster solution?

There is no faster solution: in the worst case (every word is a palindrome), every letter of each word must be examined to verify that it is a palindrome.

What words contain all of the vowels and Y, in any order?

In [6]:
var all = [],
    // map of vowels and Y to powers of 2
    bits = { 'A': 1, 'E': 2, 'I': 4, 'O': 8, 'U': 16, 'Y': 32 };
                        
for (var word of dict) {
    vowels = 0;
    
    for (var letter of word) {      
        // if the letter is a vowel or Y, set the bit in vowels corresponding to this letter
        if (letter in bits) {
            vowels |= bits[letter];
        }
        
        // if all vowels and Y have been found in the word, stop looking
        if (vowels == 63) {
            all.push(word);
            break;
        }
    }
}

console.log(all);
[ 'ABSTEMIOUSLY',
  'ACTINOMYCETOUS',
  'ADVENTITIOUSLY',
  'AERONAUTICALLY',
  'ALUMINOTHERMY',
  'AMARYLLIDACEOUS',
  'AMBIDEXTROUSLY',
  'ANEUPLOIDY',
  'ANTIREGULATORY',
  'APODYTERIUM',
  'APODYTERIUMS',
  'AUDIOMETRICALLY',
  'AUDIOMETRY',
  'AUTHORITATIVELY',
  'AUTOECIOUSLY',
  'AUTOTETRAPLOIDY',
  'AUTOTYPIES',
  'AYUNTAMIENTO',
  'AYUNTAMIENTOS',
  'BASIDIOMYCETOUS',
  'BEHAVIOURALLY',
  'BIMOLECULARLY',
  'BUOYANCIES',
  'CALYCOIDEOUS',
  'CLEISTOGAMOUSLY',
  'COEDUCATIONALLY',
  'COEQUALITY',
  'COEVOLUTIONARY',
  'COMMUNICATIVELY',
  'COMMUTATIVELY',
  'CONCEPTUALITY',
  'CONSEQUENTIALLY',
  'CONSUETUDINARY',
  'CONSULTATIVELY',
  'CONTINUATIVELY',
  'COPULATIVELY',
  'CORYNEBACTERIUM',
  'COULOMETRICALLY',
  'COUNTERACTIVELY',
  'COUNTERCYCLICAL',
  'COUNTERRALLYING',
  'CYANOBACTERIUM',
  'CYLINDRACEOUS',
  'CYTOMEGALOVIRUS',
  'DAGUERREOTYPIES',
  'DAGUERREOTYPING',
  'DAGUERREOTYPIST',
  'DELUSIONARY',
  'DENUNCIATORY',
  'DEVOLUTIONARY',
  'DIALYPETALOUS',
  'DICHLAMYDEOUS',
  'DOCUMENTARILY',
  'DUODECIMALLY',
  'EDACIOUSLY',
  'EDUCATIONALLY',
  'EFFICACIOUSLY',
  'ELOCUTIONARY',
  'ELUCIDATORY',
  'ENANTIOSTYLOUS',
  'ENCOURAGINGLY',
  'ENUNCIATORY',
  'EQUANIMOUSLY',
  'EQUATIONALLY',
  'EQUATORIALLY',
  'EQUILIBRATORY',
  'EQUINOCTIALLY',
  'EQUIPONDERANCY',
  'EQUIPROBABILITY',
  'EQUIVOCALITY',
  'EQUIVOCALLY',
  'EQUIVOCATINGLY',
  'EQUIVOCATORY',
  'ERYSIPELATOUS',
  'EUCARYOTIC',
  'EUDIOMETRICALLY',
  'EUGEOSYNCLINAL',
  'EUKARYOTIC',
  'EULOGISTICALLY',
  'EUPHONICALLY',
  'EUPHORICALLY',
  'EVOLUTIONARILY',
  'EVOLUTIONARY',
  'EXCLUSIONARY',
  'EXCOMMUNICATORY',
  'EXPOSTULATINGLY',
  'FACETIOUSLY',
  'GELATINOUSLY',
  'GEMMIPAROUSLY',
  'GENITOURINARY',
  'GESTICULATORY',
  'GRANDILOQUENTLY',
  'GREGARIOUSLY',
  'HALFSERIOUSLY',
  'HELLACIOUSLY',
  'HETEROSEXUALITY',
  'HOMOSEXUALITY',
  'HYALURONIDASE',
  'HYALURONIDASES',
  'HYDROPNEUMATIC',
  ... 149 more items ]

What is the big-O complexity of the solution?

The solution is $O(n)$ for an input word of length $n$.

What is another way of solving the problem?

Other ways of solving the problem are either directly equivalent to or less efficient than this method.
For example, we could search that a word contains each vowel and Y, independently ($O(n)$, with a larger coefficient of $n$ than in the original solution):

In [ ]:
dict.filter(word => (/A/.test(word) && 
                     /E/.test(word) && 
                     /I/.test(word) && 
                     /O/.test(word) && 
                     /U/.test(word) && 
                     /Y/.test(word)));

Is there a faster solution?

There is no faster solution: in the worst case (either the word contains all of the vowels, with a unique vowel at the end of the word, or the word contains all but one of the vowels, with a consonant at the end of the word), all $n$ letters in a word must be examined.

What words contain all of the vowels and Y, in alphabetical order?

In [7]:
// match any number of non-vowel characters, then A, then any number of non-vowel characters or A,
// then E, then any number of non-vowel characters or E, etc.
dict.filter(word => /^[^AEIOUY]*A[^EIOUY]*E[^AIOUY]*I[^AEOUY]*O[^AEIUY]*U[^AEIOY]*Y[^AEIOU]*$/.test(word));
Out[7]:
[ 'ABSTEMIOUSLY',
  'ADVENTITIOUSLY',
  'FACETIOUSLY',
  'HALFSERIOUSLY' ]

What is the big-O complexity of the solution?

The solution is $O(n)$ for an input word of length $n$ (with the coefficient of $n$ depending on the regex matcher implementation).

What is another way of solving the problem?

Other ways of solving the problem are either directly equivalent to or less efficient than this method.
For example, we could remove all of the letters that are not vowels or Y, then check that the result is "AEIOUY", possibly with repeated letters ($O(n)$):

In [ ]:
dict.filter(word => /^A+E+I+O+U+Y+$/.test(word.replace(/[^AEIOUY]/g, '')));

Is there a faster solution?

There is no faster solution: in the worst case (the word contains all of the vowels in alphabetical order, without Y or with the only Y at the end of the word), all $n$ letters in a word must be examined to check for a match with this regular expression.

What letter makes the most appearances in a single word, and what is that word?

In [9]:
// return the letter(s) that appears most frequently in word, and the number of appearances
function freqLetter(word) {
    var freqs = {};
    
    // map each letter to the number of times it appears in the word
    for (var letter of word) {
        freqs[letter] = freqs[letter] ? (freqs[letter]+1) : 1;
    }
    
    var maxFreq = 0, 
        maxLetter = [];
    
    // find the letter(s) with the most appearances
    for (var letter in freqs) {
        if (freqs[letter] > maxFreq) {
            maxFreq = freqs[letter];
            maxLetter = [letter];
            
        } else if (freqs[letter] == maxFreq) {
            maxLetter.push(letter);
        }
    }
    
    return { maxFreq, maxLetter };
}

// maxLetterAll: the letter(s) that appear most frequently in any word, mapped 
// to a list of the word(s) in which they appear most frequently
var maxFreqAll = 0, 
    maxLetterAll = {};

for (var word of dict) {
    // find the most frequent letter(s) in this word
    var { maxFreq, maxLetter } = freqLetter(word);
    
    // this word has a letter(s) that appears more frequently than the previous most
    // frequent letter(s)
    if (maxFreq > maxFreqAll) {
        maxFreqAll = maxFreq;
        
        maxLetterAll = {};
        for (var l of maxLetter) {
            maxLetterAll[l] = [word];
        }
        
    // this word has a letter(s) that appears with the same frequency as the previous
    // most frequent letter(s)
    } else if (maxFreq == maxFreqAll) {
        
        for (var l of maxLetter) {
            if (!maxLetterAll[l]) {
                maxLetterAll[l] = [];
            }
            maxLetterAll[l].push(word);
        }
    }
}

console.log(maxLetterAll);
{ S: 
   [ 'CLASSLESSNESSES',
     'POSSESSEDNESSES',
     'SENSELESSNESSES',
     'STRESSLESSNESS',
     'SUCCESSLESSNESS' ] }

What is the big-O complexity of the solution?

What is another way of solving the problem?

Is there a faster solution?

What words are the longest anagrams of each other?

In [9]:
// map of letters to unique primes
var primes = { 'A': 2, 'B': 3, 'C': 5, 'D': 7, 'E': 11, 'F': 13, 'G': 17, 'H': 19, 'I': 23,
               'J': 29, 'K': 31, 'L': 37, 'M': 41, 'N': 43, 'O': 47, 'P': 53, 'Q': 59, 'R': 61,
               'S': 67, 'T': 71, 'U': 73, 'V': 79, 'W': 83, 'X': 89, 'Y': 97, 'Z': 101 };
 
// hash each word as a product of the primes corresponding to its letters
var hashmap = {};
for (var word of dict) {
    var key = word.split('').reduce((prod, letter) => primes[letter] * prod, 1);

    if (hashmap[key]) {
        hashmap[key].push(word);
    } else {
        hashmap[key] = [word];
    }
}

var maxLen = 0, 
    maxAna = [];
for (var key in hashmap) {
    // multiple words had the same hash (i.e., these words are anagrams of each other)
    if (hashmap[key].length > 1) {
        var anaLen = hashmap[key][0].length; // length of the words
    
        // these words are the same length as the previous longest anagrams
        if (anaLen == maxLen) {
            maxAna.push(hashmap[key]);
            
        // these words are longer than the previous longest anagrams
        } else if (anaLen > maxLen) {
            maxLen = anaLen;
            maxAna = [hashmap[key]];
        }
    }
}

console.log(maxAna);
[ [ 'ALGORITHMICALLY', 'LOGARITHMICALLY' ],
  [ 'ALTITUDINARIANS', 'LATITUDINARIANS' ],
  [ 'ATTENTIVENESSES', 'TENTATIVENESSES' ],
  [ 'AUTORADIOGRAPHS', 'RADIOAUTOGRAPHS' ],
  [ 'AUTORADIOGRAPHY', 'RADIOAUTOGRAPHY' ],
  [ 'BIOPSYCHOLOGIES', 'PSYCHOBIOLOGIES' ],
  [ 'CHROMATOGRAPHER', 'RECHROMATOGRAPH' ],
  [ 'CONCESSIONAIRES', 'CONCESSIONARIES' ],
  [ 'CONSERVATIONIST', 'CONVERSATIONIST' ],
  [ 'CORRELATIVENESS', 'OVERCENTRALISES' ],
  [ 'COUNTERCHARMING', 'COUNTERMARCHING' ],
  [ 'DECIMALISATIONS', 'IDIOMATICALNESS', 'MEDICALISATIONS' ],
  [ 'DECIMALIZATIONS', 'MEDICALIZATIONS' ],
  [ 'DEMANDINGNESSES', 'MADDENINGNESSES' ],
  [ 'ELECTROMAGNETIC', 'MAGNETOELECTRIC' ],
  [ 'ENDEARINGNESSES', 'ENGRAINEDNESSES' ],
  [ 'GAMOGENETICALLY', 'GEOMAGNETICALLY' ],
  [ 'GEOHYDROLOGISTS', 'HYDROGEOLOGISTS' ],
  [ 'GRAMOPHONICALLY',
    'MONOGRAPHICALLY',
    'NOMOGRAPHICALLY',
    'PHONOGRAMICALLY' ],
  [ 'IMPERSCRIPTIBLE', 'IMPRESCRIPTIBLE' ],
  [ 'MACROPHOTOGRAPH', 'PHOTOMACROGRAPH' ],
  [ 'MICROPHOTOGRAPH', 'PHOTOMICROGRAPH' ],
  [ 'PATHOPHYSIOLOGY', 'PHYSIOPATHOLOGY' ],
  [ 'PHOTOTELEGRAPHS', 'TELEPHOTOGRAPHS' ],
  [ 'PHOTOTELEGRAPHY', 'TELEPHOTOGRAPHY' ],
  [ 'RELATIVISATIONS', 'REVITALISATIONS' ],
  [ 'RELATIVIZATIONS', 'REVITALIZATIONS' ],
  [ 'RUMMELGUMPTIONS', 'RUMMLEGUMPTIONS' ],
  [ 'STATELESSNESSES', 'TASTELESSNESSES' ] ]

What is the big-O complexity of the solution?

What is another way of solving the problem?

Is there a faster solution?

Scrabble cheater

Write a program that takes a Scrabble rack as an argument and returns all valid Scrabble words that can be constructed from that rack, along with their Scrabble scores, sorted by score.

In [10]:
function cheat(rack) {  
    // map of letters to unique primes
    const primes = { 'A': 2, 'B': 3, 'C': 5, 'D': 7, 'E': 11, 'F': 13, 'G': 17, 'H': 19, 'I': 23,
                     'J': 29, 'K': 31, 'L': 37, 'M': 41, 'N': 43, 'O': 47, 'P': 53, 'Q': 59, 'R': 61,
                     'S': 67, 'T': 71, 'U': 73, 'V': 79, 'W': 83, 'X': 89, 'Y': 97, 'Z': 101 };
    
    // hash the rack as a product of primes
    var rackHash = rack.split('').reduce((prod, letter) => primes[letter] * prod, 1);

    var valid = [], 
        factoredHash, subset;

    // for each word, check whether the rack hash can be (at least partially) factored by its letters
    for (var word of dict) {
        factoredHash = rackHash; // rack hash being factored
        subset = true;           // whether this word is a subset of the rack

        for (var letter of word) {
            // check if the prime corresponding to this letter is a factor of the rack hash (prime product)
            if (factoredHash % primes[letter] == 0) {
                // the letter is in the rack, so factor it out from the hash ("remove it from the rack")
                factoredHash /= primes[letter];
                
            } else {
                // the letter is not in the rack, so this word can't be made from this rack
                subset = false;
                break;
            }
        }
        
        if (subset) {
            valid.push(word);
        }
    }
    
    // scores for each letter in Scrabble
    const scores = { 'A': 1, 'C': 3, 'B': 3, 'E': 1, 'D': 2, 'G': 2,
                     'F': 4, 'I': 1, 'H': 4, 'K': 5, 'J': 8, 'M': 3,
                     'L': 1, 'O': 1, 'N': 1, 'Q': 10, 'P': 3, 'S': 1,
                     'R': 1, 'U': 1, 'T': 1, 'W': 4, 'V': 4, 'Y': 4,
                     'X': 8, 'Z': 10 }
    
    // map valid words to their scores
    valid = valid.map(word => ([word.split('').reduce((sum, letter) => scores[letter] + sum, 0),
                                word]));
    // sort valid words by decreasing score
    valid.sort((a, b) => a[0] > b[0] ? -1 : 1);
    
    return valid;
}

console.log(cheat('ZAEFIEE'));
[ [ 17, 'FEEZE' ],
  [ 17, 'FEAZE' ],
  [ 16, 'FAZE' ],
  [ 15, 'FEZ' ],
  [ 15, 'FIZ' ],
  [ 12, 'ZEE' ],
  [ 12, 'ZEA' ],
  [ 11, 'ZA' ],
  [ 6, 'FIE' ],
  [ 6, 'FEE' ],
  [ 6, 'FAE' ],
  [ 5, 'IF' ],
  [ 5, 'FA' ],
  [ 5, 'EF' ],
  [ 5, 'FE' ],
  [ 2, 'EE' ],
  [ 2, 'AE' ],
  [ 2, 'AI' ],
  [ 2, 'EA' ] ]

Bonus

Modify your program to allow blank tiles, which can be used as any letter but contribute no points to the word score.

In [11]:
// use '_' to represent a blank tile in the rack
function cheatWithBlanks(rack) {
    // map of letters to unique primes
    // (map blank tiles to 1 so that they do not contribute to the rack hash)
    const primes = { 'A': 2, 'B': 3, 'C': 5, 'D': 7, 'E': 11, 'F': 13, 'G': 17, 'H': 19, 'I': 23,
                     'J': 29, 'K': 31, 'L': 37, 'M': 41, 'N': 43, 'O': 47, 'P': 53, 'Q': 59, 'R': 61,
                     'S': 67, 'T': 71, 'U': 73, 'V': 79, 'W': 83, 'X': 89, 'Y': 97, 'Z': 101, '_': 1 };
    
    // hash the rack as a product of primes
    var rackHash = rack.split('').reduce((prod, letter) => primes[letter] * prod, 1);
    
    // count the blank tiles
    var blanks = rack.match(/_/g);
    blanks = blanks ? blanks.length : 0;

    // scores for each letter in Scrabble
    const scores = { 'A': 1, 'C': 3, 'B': 3, 'E': 1, 'D': 2, 'G': 2,
                     'F': 4, 'I': 1, 'H': 4, 'K': 5, 'J': 8, 'M': 3,
                     'L': 1, 'O': 1, 'N': 1, 'Q': 10, 'P': 3, 'S': 1,
                     'R': 1, 'U': 1, 'T': 1, 'W': 4, 'V': 4, 'Y': 4,
                     'X': 8, 'Z': 10 }
    
    var valid = [], 
        factoredHash, blanksLeft, score, subset;

    // for each word, check whether the rack hash can be (at least partially) factored by its letters
    for (var word of dict) {
        factoredHash = rackHash; // rack hash being factored
        blanksLeft = blanks;     // how many blank tiles are left to use
        score = 0;               // score for this word
        subset = true;           // whether this word is a subset of the rack

        for (var letter of word) {
            // check if the prime corresponding to this letter is a factor of the rack hash (prime product)
            if (factoredHash % primes[letter] == 0) {
                // the letter is in the rack, so factor it out from the hash ("remove it from the rack")
                factoredHash /= primes[letter];
    
                score += scores[letter]; // add this letter to the score
                
            } else if (blanksLeft) {
                // the letter is not in the rack, but we can use a blank tile to represent it
                blanksLeft -= 1;
                
            } else {
                // the letter is not in the rack, and we have no blank tiles left
                subset = false;
                break;
            }
        }
        
        if (subset) {
            valid.push([score, word]);
        }
    }
    
    // sort valid words by decreasing score
    valid.sort((a, b) => a[0] > b[0] ? -1 : 1);
    
    return valid;
}

console.log(cheatWithBlanks('ZAEFIE_'));
[ [ 17, 'FEAZES' ],
  [ 17, 'FEAZED' ],
  [ 17, 'FEAZE' ],
  [ 17, 'FRIEZE' ],
  [ 16, 'FEEZE' ],
  [ 16, 'FRIZE' ],
  [ 16, 'FAZES' ],
  [ 16, 'FUZEE' ],
  [ 16, 'FAZE' ],
  [ 16, 'FEZES' ],
  [ 16, 'HAFIZ' ],
  [ 16, 'FAZED' ],
  [ 15, 'FIZ' ],
  [ 15, 'ZARF' ],
  [ 15, 'FEZ' ],
  [ 15, 'FIZZ' ],
  [ 15, 'FUZE' ],
  [ 15, 'FRIZ' ],
  [ 15, 'ZIFF' ],
  [ 13, 'AVIZE' ],
  [ 13, 'ZOEAE' ],
  [ 13, 'PEAZE' ],
  [ 13, 'WEIZE' ],
  [ 13, 'PEIZE' ],
  [ 13, 'LEAZE' ],
  [ 13, 'RAZEE' ],
  [ 13, 'BAIZE' ],
  [ 13, 'AIZLE' ],
  [ 13, 'TEAZE' ],
  [ 13, 'ZAIRE' ],
  [ 13, 'SEIZE' ],
  [ 13, 'SEAZE' ],
  [ 13, 'AZIDE' ],
  [ 13, 'AZINE' ],
  [ 13, 'MAIZE' ],
  [ 13, 'CEAZE' ],
  [ 12, 'DAZE' ],
  [ 12, 'ZOEA' ],
  [ 12, 'MAZE' ],
  [ 12, 'ZITE' ],
  [ 12, 'GEEZ' ],
  [ 12, 'ZEA' ],
  [ 12, 'ZETA' ],
  [ 12, 'NAZI' ],
  [ 12, 'HAZE' ],
  [ 12, 'NAZE' ],
  [ 12, 'ZILA' ],
  [ 12, 'MZEE' ],
  [ 12, 'RIZA' ],
  [ 12, 'KAZI' ],
  [ 12, 'JEEZ' ],
  [ 12, 'MEZE' ],
  [ 12, 'RAZE' ],
  [ 12, 'ZEAL' ],
  [ 12, 'ZATI' ],
  [ 12, 'ZEAS' ],
  [ 12, 'SIZE' ],
  [ 12, 'BIZE' ],
  [ 12, 'ZEES' ],
  [ 12, 'ZINE' ],
  [ 12, 'LAZE' ],
  [ 12, 'ZEE' ],
  [ 12, 'ADZE' ],
  [ 12, 'ZEIN' ],
  [ 12, 'PIZE' ],
  [ 12, 'IZAR' ],
  [ 12, 'ZEZE' ],
  [ 12, 'GAZE' ],
  [ 11, 'ZEK' ],
  [ 11, 'ZEL' ],
  [ 11, 'BEZ' ],
  [ 11, 'ZIN' ],
  [ 11, 'BIZ' ],
  [ 11, 'ZAS' ],
  [ 11, 'MIZ' ],
  [ 11, 'LEZ' ],
  [ 11, 'ZED' ],
  [ 11, 'AZO' ],
  [ 11, 'RIZ' ],
  [ 11, 'CAZ' ],
  [ 11, 'SEZ' ],
  [ 11, 'JIZ' ],
  [ 11, 'WIZ' ],
  [ 11, 'ZIP' ],
  [ 11, 'ZAG' ],
  [ 11, 'ZIT' ],
  [ 11, 'ZAP' ],
  [ 11, 'SAZ' ],
  [ 11, 'REZ' ],
  [ 11, 'ZIZ' ],
  [ 11, 'ZAX' ],
  [ 11, 'ADZ' ],
  [ 11, 'ZOA' ],
  [ 11, 'ZEP' ],
  [ 11, 'ZIG' ],
  [ 11, 'ZEX' ],
  [ 11, 'ZA' ],
  [ 10, 'ZO' ],
  [ 8, 'FERIAE' ],
  [ 8, 'FAERIE' ],
  ... 330 more items ]