Scrabble 单词查找器






4.51/5 (9投票s)
用匿名类型、分组、空值延续和合并运算符测试你的 Linq 水平
引言
我最近知道我有一个有效的 8 个字母的单词(我的 7 个字母加上棋盘上的一个字母)——“Word With Friends”单词查找器告诉我我可以写出这个 8 个字母的单词——但我不知道这个单词是什么。
我手头有这些字母,包括棋盘上的 's'
List<char> myLetters = new List<char>() { 't', 'r', 'o', 'i', 'l', 'b', 's', 's' };
使用 More Words 网站,搜索“以 s 开头的 8 个字母的单词”,我得到了 3480 个符合该标准的单词。但是哪个与我的字母匹配?
第一次尝试
鉴于我知道这个单词以 's' 开头和结尾,我做了以下操作
string[] words = File.ReadAllLines("words.txt");
var f1 = words.Where(w => w[0] == 's' && w[7] == 's');
var f2 = (from word in f1
where myLetters.All(c => word.Any(wc => wc == c))
select word).ToList();
f2.ForEach(w => Console.WriteLine(w));
瞧,这就是我的单词:“strobils”——
1. 裸子植物或无种子维管植物(如木贼或石松)的球果。
2. 各种类似结构,例如啤酒花的雌花序,由被重叠的绿色苞片遮盖的小花组成。
但是,我是在知道首尾字母的情况下预先过滤了整个单词列表。如果我不使用该过滤器
var f2 = (from word in <font color="#FF0000">words</font>
where myLetters.All(c => word.Any(wc => wc == c))
select word).ToList();
我得到了这个列表
sorbitol
strobila
strobile
strobili
strobils
请注意,有些单词包含重复的字母,有些单词包含我没有的其他字母。
第二次尝试
那么,如何精确匹配可用的字母呢?嗯,统计它们,并确保可能单词中每个字母的计数与我拥有的字母的计数相匹配。这给了我我的字母计数
var occurences = myLetters.Select(c => new { letter = c, count = myLetters.Count(o => o == c) });
请注意,我们有两个 's' 的条目。让我们清理一下
var occurences = myLetters.Distinct().
Select(c => new { letter = c, count = myLetters.Count(o => o == c) });
同样,我们对每个可能的单词执行相同的操作(ToList()
使调试器更易读)
var wordLetterCounts = words.Select(word =>
new
{
word = word,
counts =
word.GroupBy(c => c).Select((a, b) =>
new
{
letter = a.Key,
count = word.Count(c => c == a.Key)
}).ToList()
});
列表中的前两个单词
现在我们可以匹配测试单词中字母的计数与我可用的字母
var matches = wordLetterCounts.Where(wlc =>
wlc.counts.All(letterCount =>
occurences.Single(o => o.letter == letterCount.letter).count == letterCount.count));
var listOfMatches = matches.ToList();
糟糕
这里的问题是,我每个字母的出现次数列表(elephant,我在代码中错误地拼写了“occurrences”)可能不包含测试单词中的字母。
第三次尝试
空值延续和空值合并运算符来拯救(也修正了拼写错误)
var matches = wordLetterCounts.Where(wlc =>
wlc.counts.All(letterCount =>
(occurrences.SingleOrDefault(o =>
o.letter == letterCount.letter)?.count ?? 0) == letterCount.count));
成功!
替代实现
一个“更简单”的替代 Linq 语句将各个部分组合在一起,而无需创建临时匿名类型 wordLetterCounts
var matches = words.Select(word =>
{
return new { word = word, matches =
word.GroupBy(c => c).Select((a, b) => new { letter = a.Key, count = word.Count(c => c == a.Key) })
.All(q => (occurrences.FirstOrDefault(o => o.letter == q.letter)?.count ?? 0) == q.count) };
}).Where(word => word.matches);
一个简单的 UI
using System;
using System.Data;
using System.Linq;
using System.Windows.Forms;
namespace WordFinderUI
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
tbWordList.MaxLength = Int32.MaxValue;
}
private void btnFind_Click(object sender, EventArgs e)
{
var myLetters = tbMyLetters.Text.ToList();
var occurrences = myLetters.
Distinct().
Select(c => new { letter = c, count = myLetters.Count(o => o == c) });
var words = tbWordList.Text.Split(new char[] { '\r' }).Select(w=>w.Trim());
var matches = words.Select(word =>
{
return new
{
word = word,
matches =
word.GroupBy(c => c).
Select((a, b) => new { letter = a.Key, count = word.Count(c => c == a.Key) })
.All(q => (occurrences.FirstOrDefault(o => o.letter == q.letter)?.count ?? 0) == q.count)
};
}).Where(word => word.matches);
tbMatches.Text = matches.Count() == 0 ?
"No Matches!" : String.Join("\r\n", matches.Select(m => m.word)); }
}
}
这里唯一值得注意的是
tbWordList.MaxLength = Int32.MaxValue;
TextBox
的默认长度为 32767 个字符,而其中一些单词列表要长得多。这给你 2GB 的字符!
结论
现在,当我试图找到 7 个字母的单词时,我可以“作弊”了!