<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="http://blogs.msdn.com/utility/FeedStylesheets/rss.xsl" media="screen"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:wfw="http://wellformedweb.org/CommentAPI/"><channel><title>Fabulous Adventures In Coding : Scrabble</title><link>http://blogs.msdn.com/ericlippert/archive/tags/Scrabble/default.aspx</link><description>Tags: Scrabble</description><dc:language>en-US</dc:language><generator>CommunityServer 2.1 SP1 (Build: 61025.2)</generator><item><title>Five-Dollar Words For Programmers, Part Four: Boustrophedonic</title><link>http://blogs.msdn.com/ericlippert/archive/2009/03/26/five-dollar-words-for-programmers-part-four-boustrophedonic.aspx</link><pubDate>Thu, 26 Mar 2009 16:39:00 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:9502592</guid><dc:creator>Eric Lippert</dc:creator><slash:comments>14</slash:comments><comments>http://blogs.msdn.com/ericlippert/comments/9502592.aspx</comments><wfw:commentRss>http://blogs.msdn.com/ericlippert/commentrss.aspx?PostID=9502592</wfw:commentRss><description>&lt;a href="http://commons.wikimedia.org/wiki/File:Lapis-niger.jpg"&gt;&lt;img title="Boustrophedonic" style="border-top-width: 0px; display: inline; border-left-width: 0px; border-bottom-width: 0px; margin: 0px 10px 0px 0px; border-right-width: 0px" height="240" alt="Boustrophedonic" src="http://blogs.msdn.com/blogfiles/ericlippert/WindowsLiveWriter/FiveDollarWordsForProgrammersPartFourBou_C882/Boustrophedonic_3.jpg" width="172" align="left" border="0"&gt;&lt;/a&gt;  &lt;div class="mine"&gt; &lt;p&gt;Here’s an almost useless but thoroughly delightful five-dollar word. English of course is read left-to-right. Hebrew and Arabic are read right-to-left. A text is &lt;strong&gt;&lt;a href="http://en.wikipedia.org/wiki/Boustrophedonic"&gt;boustrophedonic&lt;/a&gt;&lt;/strong&gt; if it reads left-to-right &lt;em&gt;and&lt;/em&gt; right-to-left, &lt;em&gt;alternating&lt;/em&gt;.  &lt;p&gt;It’s from the Greek βουστροφηδόν meaning “as the ox turns”;&amp;nbsp; you’d plow a field with an ox right to left and then left to right, obviously.  &lt;p&gt;There are a number of ancient languages which were written boustrophedonically, which I’m sure has given members of the Unicode committee many sleepless nights. The example here is a rare early Latin text written boustrophedonically.  &lt;p&gt;What’s the relevance to computer people not on the Unicode committee, given that odds are slim to none that Word will ever support boustrophedonic editing? &lt;em&gt;That’s how most modern dot-matrix and inkjet printers print.&lt;/em&gt; The head goes left-to-right, then prints the image “backwards” right-to-left, and so on.  &lt;p&gt;I discovered this word several years ago when grepping through the Scrabble Tournament Word List post-game to see if HEDONIC was in fact a legal bingo, or if I had played a phony. (It is legal.) But I had partial text matching on, so I hit BOUSTROP&lt;strong&gt;HEDONIC&lt;/strong&gt; first and was intrigued, so I looked it up.  &lt;p&gt;At fifteen letters long, it would run the entire width or height of the board. If OUST, HE and ON were all on the board already in the right place along an edge, you could play the remaining seven letters, get the triple-triple-triple word score plus the bingo bonus, and score 725 points. That would almost double the world record for highest scoring play (CAZIQUES, 392 points).  &lt;p&gt;This seems unlikely, but you never know. Might come in handy.&lt;/p&gt;&lt;/div&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=9502592" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/ericlippert/archive/tags/English+Usage/default.aspx">English Usage</category><category domain="http://blogs.msdn.com/ericlippert/archive/tags/Big+Words/default.aspx">Big Words</category><category domain="http://blogs.msdn.com/ericlippert/archive/tags/Scrabble/default.aspx">Scrabble</category></item><item><title>Santalic tailfans, part two</title><link>http://blogs.msdn.com/ericlippert/archive/2009/02/06/santalic-tailfans-part-two.aspx</link><pubDate>Fri, 06 Feb 2009 21:19:31 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:9402662</guid><dc:creator>Eric Lippert</dc:creator><slash:comments>36</slash:comments><comments>http://blogs.msdn.com/ericlippert/comments/9402662.aspx</comments><wfw:commentRss>http://blogs.msdn.com/ericlippert/commentrss.aspx?PostID=9402662</wfw:commentRss><description>&lt;div class="mine"&gt; &lt;p&gt;As I have said before &lt;a href="http://blogs.msdn.com/ericlippert/archive/tags/Performance/default.aspx"&gt;many times&lt;/a&gt;, there is &lt;a href="http://blogs.msdn.com/ericlippert/archive/2003/10/17/53237.aspx"&gt;only one sensible way&lt;/a&gt; to make a performant application. (As an aside: perfectly good word, performant, deal with it!) That is:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;Set meaningful, measurable, customer-focused goals.  &lt;li&gt;Write the code to be as clear and correct as possible.  &lt;li&gt;Carefully measure your performance against your goals.  &lt;li&gt;Did you meet your goal? Great! Don't waste any time on performance analysis. Spend your valuable time on features, documentation, bug fixing, robustness, security, whatever.  &lt;li&gt;If you did not meet your goal, use tools to discover what the worst-performing fixable thing is, and fix it.&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;Iterate on that process, updating your goals if it proves necessary, until you either have something that meets your goals or you give up.&lt;/p&gt; &lt;p&gt;My explicit goal for my &lt;a href="http://blogs.msdn.com/ericlippert/archive/2009/02/04/a-nasality-talisman-for-the-sultana-analyst.aspx"&gt;little dictionary search program&lt;/a&gt; was that it give results fast enough that I would not be sitting there impatiently waiting for more than a few seconds. That's a very customer-focused goal, me being the only customer of this program. With only a very small amount of tweaking my program met that goal right away, so why would I spend any more time on it? The program takes just under two seconds to report the results for a typical query, which is faster than I can read.&lt;/p&gt; &lt;p&gt;But suppose that we did want to improve the performance of this program for some reason. How? Well, let's go down the list. We have goals, we have very clear code, we can measure the performance easily enough. Suppose we didn't meet the goal. The last thing on the list is "use tools to discover what the slowest fixable thing is".&lt;/p&gt; &lt;p&gt;A commenter conjectured that the performance bottleneck of this program was in the disk I/O. As you can see, every time we do a query we re-read the two megabyte dictionary file dozens of times. This has the benefit of using very little memory; we never have more than one line of the dictionary in memory at a time, instead of the whole four megs it would take to store the dictionary (remember, the dictionary is in ASCII on disk but two-byte Unicode if in strings in memory, so the in-memory size will double.)&lt;/p&gt; &lt;p&gt;That's a conjecture -- a reasonable conjecture -- but nevertheless, it's just a guess. If I've learned one thing about performance analysis it's that my guesses about where the bottleneck is are often wrong. I'll tell you right now that yes, the disk-hitting performance is bad, but it is not the worst thing in this program, not by far. &lt;strong&gt;Where's the real performance bottleneck? Any guesses? Could you know without using tools?&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;Here's the result of a timing profile run of a seven-letter queries with one blank, repeated 20 times:&lt;/p&gt; &lt;p&gt;&lt;strong&gt;43%: Contains&lt;br&gt;&lt;/strong&gt;21%: ReadLine&lt;br&gt;14%: Sort&lt;br&gt;7%: ToUpperInvariant&lt;br&gt;15%: everything else&lt;/p&gt; &lt;p&gt;Holy goodness! The call to the Contains extension method in the query to test whether the dictionary word is in the rack set array is almost half the cost of this program! &lt;/p&gt; &lt;p&gt;Which makes sense, once you stop to think about it. The "Contains" method is by its highly general nature necessarily very naive. When given an array, 99.9% of the time it has to look through the entire 26-item array because 99.9% of the time, the word is not actually going to match any of the possible racks. It cannot take advantage of any "early outs" like you could do if you were doing a linear search on a sorted list. And each time through the array it has to do a full-on string comparison; there's no fancy-pants checks in there that take advantage of string immutability or hash codes or any such thing.&lt;/p&gt; &lt;p&gt;We have a data structure that is designed to rapidly tell you whether a member is contained in a set. And even better, it already does the "distinct" logic. When I replace&lt;/p&gt;&lt;span class="code"&gt; &lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; var racks = (from rack in ReplaceQuestionMarks(originalRack)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; select Canonicalize(rack)).Distinct().ToArray(); &lt;/span&gt; &lt;p&gt;with&lt;/p&gt;&lt;span class="code"&gt; &lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; var racks = new HashSet&amp;lt;string&amp;gt;(&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; from rack in ReplaceQuestionMarks(originalRack)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; select Canonicalize(rack)); &lt;/span&gt; &lt;p&gt;suddenly performance improves massively. The "Contains" drops down to 3% of the total cost, and of course, the total cost is now half of what it was before.&lt;/p&gt; &lt;p&gt;Another subtle point here: notice how when I changed the type of the variable "racks" from "array of string" to "set of string", I didn't have to redundantly change the type thanks to implicit typing of local variables. I want to emphasize the semantics here, not the storage mechanism. If I felt that communicating the storage mechanism was an important part of this code -- because it has such a strong effect on performance -- perhaps I would choose to emphasize the storage by eschewing the "var".&lt;/p&gt; &lt;p&gt;With this change, the program performance improves to about one second per query and the profile now looks like this:&lt;/p&gt; &lt;p&gt;39%: ReadLine&lt;br&gt;23%: Sort&lt;br&gt;11%: ToUpperInvariant&lt;br&gt;7%: the iterator block goo in FileLines&lt;br&gt;5%: ToArray (called by Join)&lt;br&gt;15%: everything else&lt;/p&gt; &lt;p&gt;Now the bottleneck is clearly the combination of repeated file line reading (48%) and the string canonicalization of every dictionary line over and over again (37%). &lt;/p&gt; &lt;p&gt;With this information, we now have data with which to make sensible investments of time and effort. We could cache portions of the dictionary in memory to avoid the repeated disk cost. Is the potential increase in speed worth the potentially massive increase in memory usage? We could be smart about it and, say, only cache the seven- and eight-letter words in memory.&lt;/p&gt; &lt;p&gt;We could also attack the canonicalization performance problem. Should we perhaps precompute an index for the dictionary where every string is already in canonical form? This in effect trades increased disk space, increased program complexity and increased redundancy for decreased time. Should we use a different canonicalization algorithm entirely?&lt;/p&gt; &lt;p&gt;All of these decisions are driven by the fact that I have already exceeded my performance goal, so the answer is "no". Good enough is, by definition, good enough. If I were using this algorithm to actually build a game AI, it would not be good enough anymore and I'd go with some more clever solution. But I'm not. &lt;/p&gt;&lt;/div&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=9402662" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/ericlippert/archive/tags/Performance/default.aspx">Performance</category><category domain="http://blogs.msdn.com/ericlippert/archive/tags/C_2300_/default.aspx">C#</category><category domain="http://blogs.msdn.com/ericlippert/archive/tags/Code+Quality/default.aspx">Code Quality</category><category domain="http://blogs.msdn.com/ericlippert/archive/tags/Scrabble/default.aspx">Scrabble</category></item><item><title>A nasality talisman for the sultana analyst</title><link>http://blogs.msdn.com/ericlippert/archive/2009/02/04/a-nasality-talisman-for-the-sultana-analyst.aspx</link><pubDate>Thu, 05 Feb 2009 00:52:48 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:9396777</guid><dc:creator>Eric Lippert</dc:creator><slash:comments>24</slash:comments><comments>http://blogs.msdn.com/ericlippert/comments/9396777.aspx</comments><wfw:commentRss>http://blogs.msdn.com/ericlippert/commentrss.aspx?PostID=9396777</wfw:commentRss><description>&lt;div class="mine"&gt; &lt;p&gt;The other day my charming wife Leah and I were playing &lt;em&gt;Scrabble Brand Crossword Game&lt;/em&gt; (a registered trademark of Hasbro and Mattel) as is our wont. I went first, drawing the Q and a bunch of vowels. Knowing that the Q is death to hold onto, I immediately opened with QI for 22 points. I silently thanked Miriam-Webster for adding QI, KI and ZA to the OSPD 4th edition.&lt;/p&gt; &lt;p&gt;&lt;a href="http://en.wikipedia.org/wiki/The_Big_Snit"&gt;&lt;img style="border-right: 0px; border-top: 0px; margin: 5px 10px 5px 0px; border-left: 0px; border-bottom: 0px" height="182" alt="BigSnit" src="http://blogs.msdn.com/blogfiles/ericlippert/WindowsLiveWriter/Anasalitytalismanforthesultanaanalyst_C325/BigSnit_3.jpg" width="244" align="left" border="0"&gt;&lt;/a&gt; &lt;/p&gt; &lt;p&gt;My thankfulness was short-lived, as Leah thought for a few moments and then played ANALYST, for the fifty point "bingo" bonus, making QIS for good measure along the way. She then went on to thoroughly cream me. I am not very good at Scrabble.&lt;/p&gt; &lt;p&gt;We were wondering afterwards what all the possible seven and eight letter "bingo" words were that she could have made with the rack AALNST?. (A blank is conventionally written as "?".) I stared at it for a few moments and found SULTANA and SEALANT, but I was suspicious that there were a lot more. So I wrote a program to find out, which I shall share with you now.&lt;/p&gt; &lt;p&gt;(An interesting historical note: solving this problem efficiently was one of the questions I was given during my interviews when I first applied for full-time work at Microsoft; if you want to get a job here, you might need to know this!)&lt;/p&gt; &lt;p&gt;The core of the program is a method SearchDictionary which takes a rack string and returns a sequence containing every word in a dictionary file which can be formed using &lt;strong&gt;all&lt;/strong&gt; the letters in that rack. In this case I wanted to know not just what all the words matching the given rack were, but what all the words matching the given rack that used an existing letter on the board were. In this case the only two letters on the board were Q and I, but let's assume that it could have been any letter on the board.&lt;/p&gt; &lt;p&gt;The main loop of my little console program looks like this:&lt;/p&gt;&lt;span class="code"&gt; &lt;p&gt;public static void Main()&lt;br&gt;{&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; while (true)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Console.Write("Enter rack (use '?' for blank): ");&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; string rack = Console.ReadLine();&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if (rack == "") &lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; break;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Console.WriteLine("{0} : {1}", rack, SearchDictionary(rack).Join());&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; foreach (char c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ")&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Console.WriteLine("{0}+{1} : {2}", rack, c, SearchDictionary(rack + c).Join());&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;br&gt;}&lt;/p&gt;&lt;/span&gt; &lt;p&gt;Pretty straightforward so far. Of course, this is certainly not production quality code. This is a little utility that I hacked together for myself. A production-quality version would have a lot more error handling, for one thing.&lt;/p&gt; &lt;p&gt;The Join method is just a handy helper function that sticks a sequence of strings together:&lt;/p&gt;&lt;span class="code"&gt; &lt;p&gt;private static string Join(this IEnumerable&amp;lt;string&amp;gt; strs)&lt;br&gt;{&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; return string.Join(" ", strs.ToArray());&lt;br&gt;}&lt;/p&gt;&lt;/span&gt; &lt;p&gt;The trick to efficiently searching for anagrams in a dictionary is to realize that all anagrams have the same letters, just in different order. If you "canonicalize" each word so that its letters are uppercase and in alphabetical order, then checking whether one word is an anagram of another is as simple as comparing their canonical forms: &lt;span class="code"&gt; &lt;p&gt;private static string Canonicalize(string s)&lt;br&gt;{&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; char[] chars = s.ToUpperInvariant().ToCharArray();&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Array.Sort(chars);&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; return new string(chars);&lt;br&gt;} &lt;/span&gt; &lt;p&gt;I had a performance goal for this project; I wanted to be able to use the ~2MB 2006 Tournament Word List, searching for racks with up to two blanks, in reasonable human scale time, but not necessarily appearing to be instantaneous. My first naive implementation did not meet this goal so I made a few tweaks to the algorithm until it did, and then I stopped. (It is interesting to think about how this could be made much faster, but that's a subject for another day.) &lt;span class="code"&gt; &lt;p&gt;private static IEnumerable&amp;lt;string&amp;gt; SearchDictionary(string originalRack)&lt;br&gt;{&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; const string dictionary = @"d:\twl06.txt";  &lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; // Calculate all the possible distinct values for the rack.&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; // As an optimization, stuff the resulting racks in an array so &lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; // that we do not recalculate them during the query.  &lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; var racks = (from rack in ReplaceQuestionMarks(originalRack)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; select Canonicalize(rack)).Distinct().ToArray();  &lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; // Check every line in the dictionary to see if it matches&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; // any possible rack.&amp;nbsp; As an optimization, do an early&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; // out if the line length does not match the query length. &lt;/p&gt; &lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; return from line in FileLines(dictionary)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; where line.Length == originalRack.Length&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; where racks.Contains(Canonicalize(line))&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; select line;&lt;br&gt;}&lt;/p&gt;&lt;/span&gt; &lt;p&gt;LINQ queries are awesome. I love how the code reads like a description of what I'm trying to do, rather than how I'm doing it.&lt;/p&gt; &lt;p&gt;We need a way to turn a rack that might contain blanks into a sequence of the 26 or 26x26 possible racks without blanks. Here's a handy recursive method that does so:&lt;/p&gt;&lt;span class="code"&gt; &lt;p&gt;private static IEnumerable&amp;lt;string&amp;gt; ReplaceQuestionMarks(string s)&lt;br&gt;{&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; int index = s.IndexOf('?');&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; if (index == -1)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; yield return s;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; yield break;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; foreach (char c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ")&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; string s2 = s.Substring(0, index) + c.ToString() + s.Substring(index + 1);&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; foreach (string result in ReplaceQuestionMarks(s2))&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; yield return result;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;br&gt;}&lt;/p&gt;&lt;/span&gt; &lt;p&gt;And of course, the code to extract the lines from the dictionary is our &lt;a href="http://blogs.msdn.com/ericlippert/archive/2008/09/08/high-maintenance.aspx "&gt;old friend&lt;/a&gt;: &lt;span class="code"&gt; &lt;p&gt;private static IEnumerable&amp;lt;string&amp;gt; FileLines(string filename)&lt;br&gt;{&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; using (var sr = File.OpenText(filename))&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; while (true)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; string line = sr.ReadLine();&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if (line == null)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; yield break;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; yield return line;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;br&gt;} &lt;/span&gt; &lt;p&gt;And there you go. Six very brief little methods that tell you that Leah could have made the seven letter bingos ANALYST CANTALS LATINAS PLATANS SALTANT SALTPAN SEALANT and SULTANA or by going through the "I", the eight letter bingos ALATIONS ANNALIST FANTAILS LANITALS NASALITY PLATINAS SANTALIC STAMINAL TAILFANS TALISMAN and VALIANTS. &lt;/p&gt;&lt;/div&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=9396777" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/ericlippert/archive/tags/Performance/default.aspx">Performance</category><category domain="http://blogs.msdn.com/ericlippert/archive/tags/C_2300_/default.aspx">C#</category><category domain="http://blogs.msdn.com/ericlippert/archive/tags/Scrabble/default.aspx">Scrabble</category></item></channel></rss>