<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dean Gilley</title>
    <description>The latest articles on DEV Community by Dean Gilley (@dean_gilley).</description>
    <link>https://dev.to/dean_gilley</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3891420%2F7a833fd8-f4e8-49cc-a6b8-d6a06afc2d11.jpg</url>
      <title>DEV Community: Dean Gilley</title>
      <link>https://dev.to/dean_gilley</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dean_gilley"/>
    <language>en</language>
    <item>
      <title>Word ladders the right way: BFS, bidirectional search, and why Dijkstra is overkill</title>
      <dc:creator>Dean Gilley</dc:creator>
      <pubDate>Thu, 23 Apr 2026 19:15:03 +0000</pubDate>
      <link>https://dev.to/dean_gilley/word-ladders-the-right-way-bfs-bidirectional-search-and-why-dijkstra-is-overkill-2ipb</link>
      <guid>https://dev.to/dean_gilley/word-ladders-the-right-way-bfs-bidirectional-search-and-why-dijkstra-is-overkill-2ipb</guid>
      <description>&lt;h1&gt;
  
  
  Word ladders the right way: BFS, bidirectional search, and why Dijkstra is overkill
&lt;/h1&gt;

&lt;p&gt;If youâ€™ve ever spent a lunch break procrastinating with a word ladder puzzleâ€”transforming "COLD" to "WARM" one letter at a timeâ€”youâ€™ve essentially been performing a graph traversal. Itâ€™s a classic computer science problem that feels simple on the surface but quickly reveals the difference between a "naive" implementation and a production-ready one.&lt;/p&gt;

&lt;p&gt;Whether you are building a tool for &lt;a href="https://wordlewonk.com/" rel="noopener noreferrer"&gt;Wordlewonk&lt;/a&gt;â€”another 5-letter word puzzleâ€”or just brushing up on your algorithm skills, understanding how to navigate these graphs efficiently is a rite of passage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Graph Modeling Problem
&lt;/h2&gt;

&lt;p&gt;A word ladder is an unweighted graph. Each word is a node, and an edge exists between two nodes if they differ by exactly one character. &lt;/p&gt;

&lt;p&gt;The naive approach is to iterate through your entire dictionary (letâ€™s say 10,000 words) and compare every word against every other word. If the Hamming distance is 1, you add an edge. This is an $O(N^2 \cdot L)$ operation, where $N$ is the number of words and $L$ is the word length. For a small dictionary, this is fine. For a large one, youâ€™re looking at millions of unnecessary comparisons.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wildcard Bucket Insight
&lt;/h2&gt;

&lt;p&gt;Instead of comparing every word to every other word, we can use a "wildcard bucket" index. Think of it as a hash map where the keys are the "patterns" and the values are lists of words that fit that pattern.&lt;/p&gt;

&lt;p&gt;For the word "CAT," you generate three keys: &lt;code&gt;_AT&lt;/code&gt;, &lt;code&gt;C_T&lt;/code&gt;, and &lt;code&gt;CA_&lt;/code&gt;. You store these in a dictionary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_graph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;buckets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
            &lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;buckets&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, finding neighbors is $O(L)$ instead of $O(N)$. To find all words one step away from "CAT," you just look up the lists for &lt;code&gt;_AT&lt;/code&gt;, &lt;code&gt;C_T&lt;/code&gt;, and &lt;code&gt;CA_&lt;/code&gt;. Youâ€™ve turned a massive $O(N^2)$ pre-processing step into a clean $O(N \cdot L)$ index.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Dijkstra is Overkill
&lt;/h2&gt;

&lt;p&gt;When developers first encounter this, they often reach for Dijkstraâ€™s algorithm. Dijkstra is designed to find the shortest path in a &lt;em&gt;weighted&lt;/em&gt; graph. But in a word ladder, every step costs exactly 1. &lt;/p&gt;

&lt;p&gt;When all edge weights are equal, Dijkstra is just a slower version of Breadth-First Search (BFS). BFS is guaranteed to find the shortest path in an unweighted graph, and it does so with a simpler priority queue (or just a standard &lt;code&gt;collections.deque&lt;/code&gt;). Don't overcomplicate your codebase with weights you don't have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bidirectional BFS: Cutting the Search Space
&lt;/h2&gt;

&lt;p&gt;If you are searching for a path between "COLD" and "WARM," a standard BFS expands in a circle, growing exponentially. If the path length is $d$ and the branching factor is $b$, the complexity is $O(b^d)$.&lt;/p&gt;

&lt;p&gt;Bidirectional BFS runs two simultaneous searches: one from the start word and one from the target word. When the two frontiers intersect, youâ€™ve found your path. The complexity drops to $O(b^{d/2} + b^{d/2})$, which is significantly faster.&lt;/p&gt;

&lt;p&gt;Here is a concise implementation of a BFS-based word ladder solver using the wildcard bucket approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_neighbors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;neighbors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
        &lt;span class="n"&gt;neighbors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;neighbors&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_ladder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;([(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;])])&lt;/span&gt;
    &lt;span class="n"&gt;visited&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popleft&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;neighbor&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;get_neighbors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;neighbor&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;visited&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;visited&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;neighbor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;neighbor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;neighbor&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Production Considerations
&lt;/h2&gt;

&lt;p&gt;If youâ€™re building this for a real-world applicationâ€”perhaps to power a &lt;a href="https://a2zwords.com/" rel="noopener noreferrer"&gt;daily word puzzle companion blog&lt;/a&gt;â€”the basic BFS won't be enough. Youâ€™ll need to account for a few "real world" edge cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Disconnected Components:&lt;/strong&gt; Not every word can reach every other word. Your solver needs to handle cases where the target is unreachable gracefully, rather than spinning until the memory limit is hit.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Pre-cached Common Endpoints:&lt;/strong&gt; If you have a set of "popular" words, pre-calculate the paths between them. This turns a search into a $O(1)$ lookup.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Memory Management:&lt;/strong&gt; If your dictionary is massive, storing every edge in memory can be expensive. The wildcard bucket approach is memory-efficient because you only store the index, not the explicit adjacency list.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Word ladders are a fantastic way to practice graph theory because they force you to think about the structure of your data before you write the search logic. By indexing your data correctly, you move from "brute force" to "elegant engineering." Happy coding!&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>python</category>
      <category>graphs</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Spelling correction at scale: Levenshtein distance, BK-trees, and symmetric deletion</title>
      <dc:creator>Dean Gilley</dc:creator>
      <pubDate>Thu, 23 Apr 2026 19:15:01 +0000</pubDate>
      <link>https://dev.to/dean_gilley/spelling-correction-at-scale-levenshtein-distance-bk-trees-and-symmetric-deletion-4c1p</link>
      <guid>https://dev.to/dean_gilley/spelling-correction-at-scale-levenshtein-distance-bk-trees-and-symmetric-deletion-4c1p</guid>
      <description>&lt;h1&gt;
  
  
  Spelling correction at scale: Levenshtein distance, BK-trees, and symmetric deletion
&lt;/h1&gt;

&lt;p&gt;If youâ€™ve ever built a search feature, youâ€™ve likely started with the "naive" approach: iterate through every word in your dictionary, calculate the edit distance, and pick the one with the lowest score. It works great for a list of 500 words. But when you scale to a full English dictionary of 200,000+ entriesâ€”like the one powering &lt;a href="https://a2zdictionary.com/" rel="noopener noreferrer"&gt;a2zdictionary.com&lt;/a&gt;â€”that approach hits a wall.&lt;/p&gt;

&lt;p&gt;Letâ€™s look at how to move from $O(N)$ brute force to sub-millisecond lookups.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Foundation: Levenshtein Distance
&lt;/h2&gt;

&lt;p&gt;The Levenshtein distance measures the minimum number of single-character edits (insertions, deletions, or substitutions) required to change one word into another. The classic way to compute this is using dynamic programming.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;levenshtein&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;dist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is elegant, but itâ€™s $O(M \times N)$ for every comparison. If you have a dictionary of 200,000 words, you are performing millions of operations just to correct a single typo. This is why your &lt;a href="https://a2zwordfinder.com/" rel="noopener noreferrer"&gt;a2zwordfinder.com&lt;/a&gt; implementation might feel sluggish when a user mistypes a query.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The Scaling Wall
&lt;/h2&gt;

&lt;p&gt;When you run this against a 200k-word dictionary, you are performing $O(N)$ operations per request. Even with a fast language like C++ or Rust, the latency adds up. You aren't just calculating distance; you are calculating it for every single word in the corpus. We need a way to prune the search space so we don't look at words that are obviously too far away.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. BK-Trees: Pruning the Search Space
&lt;/h2&gt;

&lt;p&gt;A BK-tree (Burkhard-Keller tree) is a metric-space index. It exploits the &lt;strong&gt;triangle inequality&lt;/strong&gt;: $d(x, z) \leq d(x, y) + d(y, z)$.&lt;/p&gt;

&lt;p&gt;In a BK-tree, you pick a root word. Every other word is added as a child based on its distance from the parent. If you are searching for a word with a maximum distance of $n$, you only need to traverse branches where the distance between your query and the node falls within the range $[d - n, d + n]$. This allows you to discard entire subtrees of the dictionary.&lt;/p&gt;

&lt;p&gt;Here is a simplified implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BKTree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dist_func&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dist_func&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dist_func&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tree&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="n"&gt;curr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tree&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dist_func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;curr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;curr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="n"&gt;curr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;curr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;curr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dist_func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dist&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dist&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nf"&gt;_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="nf"&gt;_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By pruning, you avoid calculating the Levenshtein distance for the vast majority of the dictionary.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The SymSpell Trick: Symmetric Delete
&lt;/h2&gt;

&lt;p&gt;While BK-trees are a massive improvement, they still require tree traversal. If you need absolute maximum performance, you use &lt;strong&gt;Symmetric Delete (SymSpell)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The core insight of SymSpell is to pre-compute all possible deletions for every word in your dictionary up to a certain edit distance (usually 2). &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pre-computation:&lt;/strong&gt; For every word in your dictionary, generate all variations by deleting 1 or 2 characters. Store these in a hash map where the key is the "deleted" version and the value is a list of original words that produce this deletion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lookup:&lt;/strong&gt; When a user searches for a word, you generate the deletions for the &lt;em&gt;query&lt;/em&gt; and check if they exist in your hash map.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because you are looking up keys in a hash map rather than traversing a tree or calculating distances, the complexity drops to $O(1)$ (or $O(k)$ where $k$ is the number of possible deletions). You aren't calculating distances at runtime; you are simply performing a set intersection.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Performance Reality Check
&lt;/h2&gt;

&lt;p&gt;To put this into perspective, here is how these approaches stack up when searching a 200,000-word dictionary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Naive Brute Force:&lt;/strong&gt; ~80ms per query. This is unusable for real-time search-as-you-type interfaces.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;BK-Tree:&lt;/strong&gt; ~2ms per query. Excellent for most applications and very memory-efficient.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;SymSpell:&lt;/strong&gt; ~0.1ms per query. This is the gold standard for high-traffic production systems. It trades memory (to store the massive hash map of deletions) for extreme speed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are building a tool where users expect instant feedback, start with a BK-tree to get a feel for metric-space indexing. If you find yourself hitting a bottleneck at scale, move to the Symmetric Delete approach. Your users will thank you for the sub-millisecond response times.&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>python</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Boggle solver: DFS meets the trie, and why naive recursion blows up</title>
      <dc:creator>Dean Gilley</dc:creator>
      <pubDate>Thu, 23 Apr 2026 18:58:33 +0000</pubDate>
      <link>https://dev.to/dean_gilley/building-a-boggle-solver-dfs-meets-the-trie-and-why-naive-recursion-blows-up-590p</link>
      <guid>https://dev.to/dean_gilley/building-a-boggle-solver-dfs-meets-the-trie-and-why-naive-recursion-blows-up-590p</guid>
      <description>&lt;h1&gt;
  
  
  Building a Boggle solver: DFS meets the trie, and why naive recursion blows up
&lt;/h1&gt;

&lt;p&gt;If youâ€™ve ever spent a Sunday afternoon hunched over a 4x4 grid of plastic letter cubes, you know the frantic, high-stakes joy of Boggle. As developers, our instinct isn't just to play the gameâ€”itâ€™s to automate it.&lt;/p&gt;

&lt;p&gt;But building a Boggle solver is a classic trap. It looks like a simple graph traversal problem, but if you approach it with a naive mindset, youâ€™ll quickly find your CPU spinning its wheels while your program chokes on the sheer volume of invalid paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Naive Approach: The "Dictionary Lookup" Trap
&lt;/h2&gt;

&lt;p&gt;The rules of Boggle are straightforward: you start at any cell, move to any of the 8 adjacent neighbors, and build a word without reusing a cell in the current path.&lt;/p&gt;

&lt;p&gt;The naive approach is to perform a Depth-First Search (DFS) from every single cell on the board. At every step of the recursion, you check if your current string exists in a dictionary. If it does, you add it to your results. If itâ€™s a prefix of a valid word, you keep going.&lt;/p&gt;

&lt;p&gt;Here is the problem: if you use a standard Python &lt;code&gt;set&lt;/code&gt; or a list for your dictionary, you have no way of knowing if a path is "dead" until youâ€™ve already traversed it. You might spend thousands of cycles exploring a path like &lt;code&gt;Q-Z-X-J...&lt;/code&gt; before realizing that no word in the English language starts with those letters. &lt;/p&gt;

&lt;p&gt;You are essentially brute-forcing the entire state space of the board. With 16 cells and up to 8 neighbors each, the number of possible paths grows factorially. You aren't just searching for words; you are searching for &lt;em&gt;every possible sequence of letters&lt;/em&gt;, which is a recipe for a timeout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter the Trie: Pruning the Search Space
&lt;/h2&gt;

&lt;p&gt;To solve this efficiently, we need to stop exploring paths that aren't going anywhere. We need a data structure that tells us, "Stop here, there are no words starting with this sequence."&lt;/p&gt;

&lt;p&gt;Enter the &lt;strong&gt;Trie&lt;/strong&gt; (or prefix tree). By inserting your entire dictionary into a trie, you gain the ability to validate prefixes in $O(L)$ time, where $L$ is the length of the word. More importantly, you can prune your DFS the moment your current path deviates from a valid branch in the trie.&lt;/p&gt;

&lt;p&gt;If you are looking for a dictionary API you can query for validation, &lt;a href="https://a2zwordfinder.com/" rel="noopener noreferrer"&gt;a2zwordfinder.com&lt;/a&gt; is a great reference for what valid words look like. Similarly, &lt;a href="https://lettersintowords.com/" rel="noopener noreferrer"&gt;lettersintowords.com&lt;/a&gt; is another GWN word tool that uses similar prefix-trie ideas to handle anagrams and board games.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Implementation
&lt;/h2&gt;

&lt;p&gt;Instead of passing a string down the recursion stack, we pass a reference to the current node in our trie. If the trie node doesn't have a child corresponding to the next letter on the board, we stop immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;solve_boggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trie&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
    &lt;span class="n"&gt;found_words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dfs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visited&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;char&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="n"&gt;next_node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;next_node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;found_words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;visited&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dr&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="n"&gt;nr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;dr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;dc&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;nr&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;nc&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;visited&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="nf"&gt;dfs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next_node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visited&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;visited&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="nf"&gt;dfs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trie&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;found_words&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By passing the &lt;code&gt;next_node&lt;/code&gt; down the stack, we effectively "lockstep" our board traversal with our dictionary structure. If the trie doesn't have the letter, the recursion terminates instantly. This optimization typically results in a 100-1000x speedup compared to checking a flat list or set at every step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works
&lt;/h2&gt;

&lt;p&gt;The trie acts as a filter. In the naive approach, you explore the entire tree of possibilities. With the trie, you only explore the branches that actually exist in the English language. You are no longer searching the board; you are searching the intersection of the board's geometry and the dictionary's structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Iâ€™d add next
&lt;/h2&gt;

&lt;p&gt;If youâ€™re looking to take this project further, here are three features that would turn this script into a competitive tool:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;The "Qu" Tile:&lt;/strong&gt; In real Boggle, the "Q" cube is actually "Qu." Youâ€™ll need to adjust your logic to treat "Qu" as a single unit, or your solver will never find words like "Queen" or "Quiet."&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Scoring Logic:&lt;/strong&gt; Not all words are created equal. Implement a scoring function based on word length (e.g., 3-4 letters = 1 point, 5 letters = 2 points, etc.) and sort your output to prioritize the high-value finds.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Memoization:&lt;/strong&gt; If you find yourself running this on massive boards, consider memoizing the &lt;code&gt;(r, c, trie_node)&lt;/code&gt; state. While the &lt;code&gt;visited&lt;/code&gt; set makes this tricky, you can often cache results for sub-grids if you are processing multiple boards with the same dictionary.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Building a Boggle solver is a rite of passage for many developers. It teaches you that the difference between a slow, clunky script and a high-performance engine isn't just raw computeâ€”itâ€™s choosing the right data structure to prune your search space before the work even begins. Happy coding!&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>python</category>
      <category>datastructures</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Crossword helper internals: regex vs trie for pattern matching</title>
      <dc:creator>Dean Gilley</dc:creator>
      <pubDate>Thu, 23 Apr 2026 18:56:52 +0000</pubDate>
      <link>https://dev.to/dean_gilley/crossword-helper-internals-regex-vs-trie-for-pattern-matching-2k7m</link>
      <guid>https://dev.to/dean_gilley/crossword-helper-internals-regex-vs-trie-for-pattern-matching-2k7m</guid>
      <description>&lt;h1&gt;
  
  
  Crossword helper internals: regex vs trie for pattern matching
&lt;/h1&gt;

&lt;p&gt;If youâ€™ve ever spent a Sunday morning staring at a crossword puzzle, you know the frustration of having a word like &lt;code&gt;C_A_E&lt;/code&gt; and absolutely no idea what fits. As developers, our first instinct is to build a tool to solve it. But when you move from a simple script to a production-grade word finder, you quickly hit a wall: &lt;strong&gt;how do you search a dictionary of 100,000+ words efficiently?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I recently spent some time refactoring a crossword solver, and I learned that the choice between a Regex-based approach and a Trie-based approach is a classic study in the trade-off between memory, startup time, and query latency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Regex Approach: The "Quick and Dirty"
&lt;/h2&gt;

&lt;p&gt;The most intuitive way to solve &lt;code&gt;C_A_E&lt;/code&gt; is to convert the pattern into a Regular Expression. You replace the underscores with a wildcard (like &lt;code&gt;.&lt;/code&gt;) and anchor the string.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_with_regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dictionary&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Convert C_A_E to ^c.a.e$
&lt;/span&gt;    &lt;span class="n"&gt;regex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dictionary&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why it works
&lt;/h3&gt;

&lt;p&gt;Itâ€™s incredibly simple. You donâ€™t need to pre-process your data, and the code is readable. If you are building a small tool or a quick prototype, this is the way to go.&lt;/p&gt;

&lt;h3&gt;
  
  
  The bottleneck
&lt;/h3&gt;

&lt;p&gt;The problem is that &lt;code&gt;re.match&lt;/code&gt; is an $O(n)$ operation relative to the size of your dictionary. For every single query, your CPU has to iterate through every word in your list, compile the regex, and perform the match. On a dictionary of 100,000 words, a single lookup takes roughly &lt;strong&gt;50ms&lt;/strong&gt;. That might sound fast, but if youâ€™re building a site like &lt;a href="https://a2zwordfinder.com/" rel="noopener noreferrer"&gt;a2zwordfinder.com&lt;/a&gt;, where users expect instant, type-ahead results, that latency adds up quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trie Approach: The "Spatial Index"
&lt;/h2&gt;

&lt;p&gt;A Trie (or prefix tree) is a tree-like data structure where each node represents a character. By traversing the tree, you can prune entire branches that don't match your pattern.&lt;/p&gt;

&lt;p&gt;To handle crossword patterns, we don't just store words; we store them in a way that respects position. If we are looking for &lt;code&gt;C_A_E&lt;/code&gt;, we only traverse the branch starting with &lt;code&gt;C&lt;/code&gt;, then skip the next node (the wildcard), move to &lt;code&gt;A&lt;/code&gt;, and so on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TrieNode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;children&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_word&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_trie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[[]]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_word&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="n"&gt;char&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;child_char&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child_node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;search_trie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child_node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;child_char&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;search_trie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why it wins
&lt;/h3&gt;

&lt;p&gt;The Trie turns your search into an $O(k)$ operation, where $k$ is the length of the pattern. Because you are only visiting nodes that could possibly match, you aren't scanning the entire dictionary. &lt;/p&gt;

&lt;p&gt;In my benchmarks, once the Trie is built (which takes about &lt;strong&gt;200ms&lt;/strong&gt; on startup), the query time drops to roughly &lt;strong&gt;0.1ms&lt;/strong&gt;. That is a 500x speedup over the regex approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trade-off: When to use which?
&lt;/h2&gt;

&lt;p&gt;Choosing between these two isn't about which is "better," but about the constraints of your application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Regex if:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Memory is tight:&lt;/strong&gt; A Trie can consume significant RAM because of the overhead of storing thousands of node objects.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Your dictionary is small:&lt;/strong&gt; If youâ€™re only searching through a few thousand words, the overhead of building a Trie isn't worth the performance gain.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;You need flexibility:&lt;/strong&gt; Regex allows for complex patterns (like "starts with C, ends with E, and contains at least two vowels") that are much harder to implement in a standard Trie.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use a Trie if:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;You are building a high-traffic service:&lt;/strong&gt; If you look at professional-grade tools like &lt;a href="https://puzzledepot.com/" rel="noopener noreferrer"&gt;Puzzle Depot&lt;/a&gt;, they prioritize sub-millisecond response times. A Trie is essential for providing a snappy user experience.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;You have a static dictionary:&lt;/strong&gt; If your word list doesn't change often, you can build the Trie once at server startup and keep it in memory.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;You need "Search-as-you-type":&lt;/strong&gt; The speed of the Trie allows you to filter results in real-time as the user types, which is a massive UX win.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Building a crossword helper taught me that performance optimization is rarely about finding the "fastest" algorithm and almost always about understanding the &lt;strong&gt;lifecycle of your data&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;If youâ€™re just starting out, stick with Regex. Itâ€™s clean, maintainable, and gets the job done. But if you find yourself hitting that 50ms latency wall and your users are starting to notice, itâ€™s time to reach for a Trie. Itâ€™s a bit more work to implement, but the performance gains are undeniable.&lt;/p&gt;

&lt;p&gt;Have you built a word-finding tool? Did you go the Regex route or build a custom index? Let me know in the comments!&lt;/p&gt;

</description>
      <category>python</category>
      <category>algorithms</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Solving Wordle with information theory: entropy, guess trees, and why greedy wins</title>
      <dc:creator>Dean Gilley</dc:creator>
      <pubDate>Thu, 23 Apr 2026 18:56:03 +0000</pubDate>
      <link>https://dev.to/dean_gilley/solving-wordle-with-information-theory-entropy-guess-trees-and-why-greedy-wins-3ed7</link>
      <guid>https://dev.to/dean_gilley/solving-wordle-with-information-theory-entropy-guess-trees-and-why-greedy-wins-3ed7</guid>
      <description>&lt;h1&gt;
  
  
  Solving Wordle with information theory: entropy, guess trees, and why greedy wins
&lt;/h1&gt;

&lt;p&gt;If you spent any time on the internet in early 2022, you likely saw the viral sensation that was Wordle. While most players were relying on intuition or a lucky "ADIEU" opener, the engineering community was busy treating the game as a classic information theory problem.&lt;/p&gt;

&lt;p&gt;At its core, Wordle is a game of reducing uncertainty. Every time you submit a guess, the game provides a feedback pattern (gray, yellow, or green). This feedback acts as a filter, narrowing down the set of possible secret words. To solve the game efficiently, we don't just want to guess words; we want to maximize the &lt;em&gt;information&lt;/em&gt; we gain from each guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Math: Shannon Entropy
&lt;/h2&gt;

&lt;p&gt;In information theory, we measure the "surprise" or information content of an event using Shannon entropy. For a given guess, we want to choose a word that splits the remaining possible secret words into the most balanced "buckets" of feedback patterns.&lt;/p&gt;

&lt;p&gt;If a guess splits the remaining words into $N$ possible feedback patterns, where each pattern $i$ occurs with probability $p_i$, the entropy $H$ is defined as:&lt;/p&gt;

&lt;p&gt;$$H = -\sum_{i=1}^{N} p_i \log_2(p_i)$$&lt;/p&gt;

&lt;p&gt;A guess that results in a uniform distribution of feedback patternsâ€”where every possible outcome is equally likelyâ€”maximizes entropy. This is the "gold standard" for a first guess.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing Entropy in Python
&lt;/h3&gt;

&lt;p&gt;Calculating this is surprisingly straightforward. We need to simulate the feedback for every possible secret word in our list and group them by the resulting pattern.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_entropy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;possible_words&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;patterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;possible_words&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# get_feedback returns a tuple like (0, 1, 2, 0, 0)
&lt;/span&gt;        &lt;span class="n"&gt;patterns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get_feedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;patterns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;possible_words&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;entropy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
        &lt;span class="n"&gt;entropy&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;entropy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why TARES beats ADIEU
&lt;/h2&gt;

&lt;p&gt;Many players swear by "ADIEU" because it hits four vowels. However, from an information theory perspective, "ADIEU" is suboptimal. It is a "vowel-heavy" strategy that often results in highly skewed feedback. You might get a lot of yellow letters, but you haven't effectively partitioned the remaining word space.&lt;/p&gt;

&lt;p&gt;"TARES," on the other hand, is a powerhouse. It hits high-frequency consonants (T, R, S) and a common vowel (A, E). When you run the entropy calculation across the standard Wordle dictionary, "TARES" (or "SALET" or "CRANE") consistently provides a higher average information gain. It doesn't just tell you what &lt;em&gt;is&lt;/em&gt; in the word; it effectively eliminates large swaths of the dictionary that &lt;em&gt;aren't&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If you want to see how these strategies hold up against different word lists, &lt;a href="https://wordlewonk.com/" rel="noopener noreferrer"&gt;if you want to play with real Wordle variants&lt;/a&gt;, you can test your own openers against various constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Greedy vs. Minimax: The Search for Perfection
&lt;/h2&gt;

&lt;p&gt;The "greedy" approachâ€”picking the word with the highest entropy at each stepâ€”is incredibly effective. It will solve the vast majority of Wordle puzzles in 3 to 4 guesses. However, it is not mathematically "optimal."&lt;/p&gt;

&lt;p&gt;A greedy solver only looks one step ahead. It optimizes for the &lt;em&gt;next&lt;/em&gt; guess, not the &lt;em&gt;final&lt;/em&gt; guess. To achieve the theoretical minimum of 3.42 guesses, you need a &lt;strong&gt;Minimax&lt;/strong&gt; approach.&lt;/p&gt;

&lt;p&gt;A Minimax solver builds a full decision tree. It asks: "If I pick word X, what is the worst-case scenario for the &lt;em&gt;remaining&lt;/em&gt; number of guesses?" It then chooses the move that minimizes that worst-case outcome.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;minimax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;possible_words&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;possible_words&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;possible_words&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Branching factor: simulate all possible feedback patterns
&lt;/span&gt;    &lt;span class="c1"&gt;# This is computationally expensive!
&lt;/span&gt;    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;get_all_possible_patterns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;subset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;filter_words&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;possible_words&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;guess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max_depth_for_subset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subset&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While the greedy approach is a simple loop, the Minimax approach requires a recursive search through a massive state space. It is the difference between a quick script and a heavy-duty search algorithm. If you ever get stuck on a particularly brutal daily puzzle, &lt;a href="https://a2zwords.com/" rel="noopener noreferrer"&gt;a2zwords.com&lt;/a&gt; can serve as a helpful companion tool to see what the "optimal" path might have looked like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving to Production
&lt;/h2&gt;

&lt;p&gt;If you were to build a production-grade Wordle solver, you would quickly run into performance bottlenecks. Here is how you would scale it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Memoization/Caching:&lt;/strong&gt; The state space of Wordle is finite. You should cache the entropy results for every &lt;code&gt;(guess, remaining_word_list)&lt;/code&gt; tuple. Once you've calculated the entropy of "CRANE" for a specific subset of words, you never need to calculate it again.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Bitmasking:&lt;/strong&gt; Instead of storing words as strings, represent them as bitmasks. Comparing a guess to a secret word becomes a series of bitwise AND/OR operations, which are orders of magnitude faster than string manipulation.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Polyglot Dictionaries:&lt;/strong&gt; A production solver should handle multiple languages. By decoupling the solver logic from the dictionary file (using JSON or SQLite), you can swap between English, Spanish, or even custom word lists without changing a line of code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Wordle is a perfect example of how a simple game can be transformed into a rigorous exercise in computer science. Whether you prefer the quick-and-dirty greedy approach or the exhaustive perfection of a Minimax tree, the math remains the same: itâ€™s all about maximizing the information you extract from every single guess.&lt;/p&gt;

</description>
      <category>python</category>
      <category>algorithms</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How anagram solvers actually work: algorithms behind the scenes</title>
      <dc:creator>Dean Gilley</dc:creator>
      <pubDate>Tue, 21 Apr 2026 23:16:20 +0000</pubDate>
      <link>https://dev.to/dean_gilley/how-anagram-solvers-actually-work-algorithms-behind-the-scenes-2hec</link>
      <guid>https://dev.to/dean_gilley/how-anagram-solvers-actually-work-algorithms-behind-the-scenes-2hec</guid>
      <description>&lt;h1&gt;
  
  
  How anagram solvers actually work: algorithms behind the scenes
&lt;/h1&gt;

&lt;p&gt;If youâ€™ve ever built a word game or a tool to help with Scrabble, youâ€™ve likely run into the "anagram problem." Given a string of characters, how do you efficiently find every valid word in the dictionary that can be formed using those letters?&lt;/p&gt;

&lt;p&gt;A naive approachâ€”generating every possible permutation of the input string and checking if each exists in a setâ€”is a recipe for disaster. For a 10-letter word, youâ€™re looking at 3,628,800 permutations. Thatâ€™s not just slow; itâ€™s unusable. &lt;/p&gt;

&lt;p&gt;To build a production-grade anagram solver, we need to shift our thinking from &lt;em&gt;permutation&lt;/em&gt; to &lt;em&gt;canonical representation&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Canonical Sorted-Key Approach
&lt;/h2&gt;

&lt;p&gt;The most efficient way to solve for exact anagrams is to normalize the dictionary. If two words are anagrams, they contain the exact same characters with the same frequencies. Therefore, if you sort the letters of any word alphabetically, all anagrams of that word will result in the same "key."&lt;/p&gt;

&lt;p&gt;For example, "listen" and "silent" both become "eilnst" when sorted.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Implementation
&lt;/h3&gt;

&lt;p&gt;We preprocess our dictionary into a hash map (or dictionary in Python) where the key is the sorted string and the value is a list of matching words.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_anagram_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word_list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;word_list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Canonicalize: sort the letters
&lt;/span&gt;        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
        &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;

&lt;span class="c1"&gt;# Lookup is O(1) after O(n log n) preprocessing
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_anagrams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach is incredibly fast. The lookup time is effectively $O(L \log L)$ where $L$ is the length of the input word (due to the sorting step). Once sorted, the hash map lookup is $O(1)$.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://a2zwordfinder.com/" rel="noopener noreferrer"&gt;Try it live&lt;/a&gt; to see how this canonical mapping handles complex dictionary lookups in real-time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Moving Beyond Exact Matches: The Trie
&lt;/h2&gt;

&lt;p&gt;The sorted-key approach is perfect for finding full-word anagrams, but what if you want to find words that can be formed from a &lt;em&gt;subset&lt;/em&gt; of your letters? Or what if you want to support "wildcard" tiles?&lt;/p&gt;

&lt;p&gt;This is where the &lt;strong&gt;Trie (Prefix Tree)&lt;/strong&gt; shines. A Trie stores words as a tree structure where each node represents a character. &lt;/p&gt;

&lt;p&gt;Instead of sorting, you traverse the tree. If you have the letters "a, r, t," you start at the root and move to the 'a' branch, then 'r', then 't'. If you hit a node marked as a "word end," youâ€™ve found a valid word.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why use a Trie?
&lt;/h3&gt;

&lt;p&gt;Tries are excellent for partial matches and prefix searching. If you are building a game like Boggle or a crossword helper, you can prune the search space early. If the current path in your Trie doesn't exist, you stop searching that branch immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TrieNode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isEndOfWord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Searching for words that can be formed by a set of letters&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;findWords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;letters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentWord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isEndOfWord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentWord&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;char&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;letters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;letters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;findWords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;letters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentWord&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;letters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Backtrack&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is significantly more flexible than the sorted-key method, though it requires more memory to store the tree structure. For a more visual look at how these letter-based constraints work in practice, check out &lt;a href="https://lettersintowords.com/" rel="noopener noreferrer"&gt;lettersintowords.com&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bit-Vector Optimization
&lt;/h2&gt;

&lt;p&gt;If you are working in a memory-constrained environment or need to perform millions of subset checks per second, you can represent a word's character count using a &lt;strong&gt;bit-vector&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Since there are only 26 letters in the English alphabet, you can map each letter to a specific bit position. However, a simple bit-mask only tells you if a letter is &lt;em&gt;present&lt;/em&gt;. To handle anagrams, you need a frequency count. You can store the count of each letter in a fixed-size array (or a 64-bit integer if the counts are small).&lt;/p&gt;

&lt;p&gt;Comparing if word A is a subset of word B becomes a simple vector subtraction:&lt;br&gt;
&lt;code&gt;if (wordA_counts[i] &amp;lt;= wordB_counts[i])&lt;/code&gt; for all &lt;code&gt;i&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the "secret sauce" behind high-performance engines that need to check if a rack of tiles can form a specific word without traversing a massive tree structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Iâ€™d try next
&lt;/h2&gt;

&lt;p&gt;If I were scaling this for a massive dictionary (like the Scrabble Tournament Word List), Iâ€™d look into &lt;strong&gt;Bloom Filters&lt;/strong&gt;. They allow you to check if a word &lt;em&gt;might&lt;/em&gt; exist in the dictionary with very little memory overhead, acting as a fast-fail layer before hitting the more expensive Trie or Hash Map.&lt;/p&gt;

&lt;p&gt;Iâ€™d also experiment with &lt;strong&gt;DAWGs (Directed Acyclic Word Graphs)&lt;/strong&gt;. A DAWG is essentially a compressed Trie. Since many words share suffixes (like "-ing" or "-tion"), a DAWG merges these nodes, drastically reducing the memory footprint of your dictionary while maintaining the same lookup speed as a standard Trie.&lt;/p&gt;

&lt;p&gt;Whether you choose the simplicity of the sorted-key hash map or the power of a Trie, the key is understanding the trade-off between your dictionary's memory footprint and the speed of your search. Happy coding!&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>python</category>
      <category>javascript</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
