<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[if{then}else]]></title><description><![CDATA[Using computers to procrastinate in interesting ways]]></description><link>https://ifthenelse.io/</link><image><url>https://ifthenelse.io/favicon.png</url><title>if{then}else</title><link>https://ifthenelse.io/</link></image><generator>Ghost 5.70</generator><lastBuildDate>Tue, 07 Apr 2026 20:59:22 GMT</lastBuildDate><atom:link href="https://ifthenelse.io/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[An efficient Boggle solver using a trie]]></title><description><![CDATA[<p>I like word games - classics such as Scrabble and Boggle, and contemporary ones such as Bananagrams and Wordle are often played or discussed ewith my family and friends. Playing these games over recent months I have observed and become interested in the distinct tactics between players and whether these</p>]]></description><link>https://ifthenelse.io/an-efficient-boggle-solver-using-a-trie/</link><guid isPermaLink="false">6407d626310b9737caab4d96</guid><category><![CDATA[pyword]]></category><category><![CDATA[python]]></category><category><![CDATA[algorithms]]></category><category><![CDATA[Word search solvers]]></category><category><![CDATA[puzzles]]></category><dc:creator><![CDATA[Connor Newton]]></dc:creator><pubDate>Wed, 08 Mar 2023 21:36:29 GMT</pubDate><content:encoded><![CDATA[<p>I like word games - classics such as Scrabble and Boggle, and contemporary ones such as Bananagrams and Wordle are often played or discussed ewith my family and friends. Playing these games over recent months I have observed and become interested in the distinct tactics between players and whether these could be quantified and analysed.</p><p>For example, I&apos;m <em>not bad</em> (<em>*brushes shoulder*</em>)<em> </em>at Boggle, thanks to an apparent gift for identifying three letter words (each worth 1 point), while my typical opponents (suck it, S&#xF3;nia, Mum!) identify fewer, longer words which are arguably worth less proportional to the time taken to identify them.</p><p>Similarly my Bananagrams grids tend to be more tightly packed collections of shorter words than loose, sprawling layouts of far more interesting and impressive ones.</p><p>Perhaps, I thought, there could be patterns emerging, and if so how do these different playing styles align with what is actually available to play or score? Is it true that there aren&apos;t enough 4, 5 and 6+ words on the Boggle grid to make them worth looking for, or do I just suck at finding them?</p><p>And so in pursuit of knowledge, and more importantly advantage in future games, writing a solver for one of these games seemed like a good first step. Conveniently, the notion for such a solver had already occurred to me...</p><h2 id="the-rules-of-boggle">The rules of Boggle</h2><p>For the uninitiated, are quite simple. Letters on the 4x4 game grid are randomized and a three minute game timer started.</p><p>Unlike a simple word search where consecutive letters of a word follow a straight line, words in Boggle can be made up of a path of letters that weaves in many directions as long as consecutive letters are adjacent (including diagonally) and no letters are used more than once.</p><p>In the English version there is a special character <em>qu</em> which when used counts as the two letters under the condition that both must be used and in that order. This is interesting as this two-letter character informs the appropriate implementation of the trie for this application.</p><p>For scoring - longer words are worth more points. And any words found by more than one player are worth nothing. <em>Bummer</em>.</p><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th># Letters</th>
<th>Score</th>
</tr>
</thead>
<tbody>
<tr>
<td>&lt; 3</td>
<td>0</td>
</tr>
<tr>
<td>3-4</td>
<td>1</td>
</tr>
<tr>
<td>5</td>
<td>2</td>
</tr>
<tr>
<td>6</td>
<td>3</td>
</tr>
<tr>
<td>7</td>
<td>5</td>
</tr>
<tr>
<td>8+</td>
<td>11</td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><h2 id="a-dictionary-trie">A dictionary trie</h2><p>A <a href="https://en.wikipedia.org/wiki/Trie?ref=ifthenelse.io">trie</a>, or prefix tree, encodes the data for the keys it contains in the edges between nodes. The key associated with a node in the tree is defined by its position in the tree and is derived by traversing the tree from root to leaves and concatenating the values from the edges traversed.</p><p>This means that nodes in the trie have children associated with keys with a common prefix. It is this property that makes the trie useful for optimising word search problems when it is used to represent the dictionary of valid words.</p><figure class="kg-card kg-code-card"><pre><code class="language-python">@dataclass
class Trie(MutableSet[Sequence[str]]):
  next: Dict[str, Trie] = field(default_factory=dict)
  ok: bool = False
  
  def add(self, key: Sequence[str]) -&gt; None:
    ...
</code></pre><figcaption>Node structure for trie in Python</figcaption></figure><figure class="kg-card kg-code-card"><pre><code class="language-yaml">next:
  d:
    next:
      i:
        next:
          g:
            ok: yes
      o:
        ok: yes
        next:
          g:
            ok: yes
      u:
        next:
          g:
            ok: yes
  qu:  # [1]
  
    next:
      o:
        ok: yes</code></pre><figcaption>Trie data describing words &quot;do&quot;, &quot;dog&quot;, &quot;dig&quot;, &quot;dug&quot; and &quot;quo&quot;.</figcaption></figure><p>A boolean <em>ok</em> value associated with each node marks nodes where the prefix is a valid element in the set described by the trie. This is necessary in a dictionary trie where not all prefixes are valid words. In this trie, &quot;do&quot;, &quot;dog&quot;, &quot;dig&quot; and &quot;dug&quot; are all valid words whereas their mutual prefix &quot;d&quot; is not. </p><p>The structure of the trie can be used to efficiently prune word searches by traversing its edges corresponding to the characters encountered while exploring the game space. If there is no edge corresponding to a character that would be appendend during the search, then any words prefixed by such a result can never be a solution and thus the path can be pruned.</p><p><sup>[1] Note how the <em>qu</em> character prefixing the word <em>quo</em> has informed the Python implementation. There is no <code>char</code> type in Python, only <code>str</code> which also satisfies <code>Iterable[str]</code> . Iterating <code>&quot;quo&quot;</code> will &#xA0;yield only <code>&quot;q&quot;</code>, <code>&quot;u&quot;</code> and <code>&quot;o&quot;</code> and never <code>&quot;qu&quot;</code> meaning that the elements of the <code>Set</code> represented by <code>Trie</code> must be <code>Sequence[str]</code> rather than just <code>str</code> as one might expect.</sup></p><h2 id="solving-the-game">Solving the game</h2><p>Potential prefixes can be generated by initiating a search from each position in the game grid and traversing according to the game rules:</p><ul><li>Adjacent letters have <em>x and y</em> position values +/- 1 of the current position provided that position is on the grid.</li><li>Letters (i.e. positions) may not be re-used. This can be enforced by keeping the current prefix as a sequence of positions as a property of the search cursor.</li></ul><p>Paths representing impossible prefixes in the dictionary are pruned by keeping the trie node representing the most recent character in the path as the other property of the search cursor. A traversal is only valid if the adjacent letter is a child of the cursor trie node.</p><h3 id="worked-example">Worked example</h3><p>Consider the dictionary trie</p><figure class="kg-card kg-code-card"><pre><code class="language-yaml">next:
  d:
    next:
      i:
        next:
          d:
            ok: yes
          e:
            ok: yes
            next:
              d:
                ok: yes</code></pre><figcaption>Trie data describing words &quot;did&quot;, &quot;die&quot; and &quot;died&quot;.</figcaption></figure><p>and 2x2 game grid</p><pre><code>    x=0 x=1
y=0   d   i
y=1   e   d</code></pre><p>The search is seeded with cursors representing positions where the letter is the first character of a possible prefix in the dictionary. Since all words in the dictionary begin with <em>d</em> only those letters on the grid are eligible giving cursors</p><pre><code>[(0,0)] .d
[(1,1)] .d</code></pre><p><code>.d</code> is not marked as <em>ok</em> and so while neither cursor represents a path of a valid words the search continues from each. Note that despite referring to the same trie node, the cursors are distinct given their paths.</p><p>From both positions the only valid traversal is to the <em>i</em> giving</p><pre><code>[(0,0),(1,0)] .d.i
[(1,1),(1,0)] .d.i</code></pre><p>and again yielding no valid words.</p><p>Iterating once more yields the first valid words</p><pre><code>[(0,0),(1,0),(1,1)] .d.i.d
[(0,0),(1,0),(0,1)] .d.i.e
[(1,1),(1,0),(0,0)] .d.i.d
[(1,1),(1,0),(0,1)] .d.i.e</code></pre><p>and notably distinct paths to the same valid word, though this does not score additional points in the game.</p><p>One more iteration yields</p><pre><code>[(0,0),(1,0),(0,1),(1,1)] .d.i.e.d
[(1,1),(1,0),(0,1),(0,0)] .d.i.e.d</code></pre><p>and the conclusion of the search.</p><h3 id="reference-algorithm">Reference algorithm</h3><pre><code class="language-python">Char = str
Position = Tuple[int, int]
Path = Sequence[Position]
Cursor = Tuple[Path, Trie]


class Grid(MutableMapping[Position, Char]):
    def __init__(self, size: Tuple[int, int] = (4, 4)) -&gt; None:
        self.size = size
        ...
    
    def adj(self, to: Position) -&gt; Iterator[Tuple[Position, Char]]: ...  


def solve(tr: Trie, g: Grid) -&gt; Iterator[Path]:
    # Seed search with valid prefixes starting with each letter.
    s: List[Cursor] = [  # Stack
        ((c,), u) for c in g if (u := tr.next.get(g[c])) is not None
    ]
    
    while s:
        p, u = s.pop()
        
        # Solution if is a word.
        if u.ok:
            yield p
            
        # Extend search with possible nexts from unused adjacents.
        for c, w in g.adj(p[-1]):
            if c not in p and (v := u.next.get(w)) is not None:
                s.append(((*p, c), v))</code></pre><p>A <a href="https://github.com/phyrwork/pyword/blob/master/pyword/boggle.py?ref=ifthenelse.io">complete implemenation</a> is available in my <a href="https://github.com/phyrwork/pyword?ref=ifthenelse.io">pyword</a> repository on GitHub, along with solvers for some other word search games.</p>]]></content:encoded></item><item><title><![CDATA[Computing earliest-arrival paths in temporal graphs]]></title><description><![CDATA[Shortest path is a frequently used tool, particularly regarding route planning. But in some graph problems, the edges change as time progresses and so we can not use popular algorithms such as Djikstra. Instead we must use a class of algorithms which consider the properties of temporal edges.]]></description><link>https://ifthenelse.io/computing-earliest-arrival-paths-in-temporal-graphs/</link><guid isPermaLink="false">6407d626310b9737caab4d95</guid><category><![CDATA[graphs]]></category><category><![CDATA[golang]]></category><dc:creator><![CDATA[Connor Newton]]></dc:creator><pubDate>Tue, 13 Nov 2018 19:35:22 GMT</pubDate><content:encoded><![CDATA[<p>(Need a quick reference? Scroll down for the algorithm pseudocode and a Go implementation.)</p><p>Shortest path analysis, and variants thereof, is a useful tool many of us use every day, particularly with regard to making travel decisions - &quot;If I leave now, how long will it take to drive to work?&quot; is a question we might ask our sat. nav. having overslept slightly and waking up to a 9am meeting invite that you&apos;re pretty sure wasn&apos;t there as you left yesterday.</p><p>Despite being disguised as a time-related question (&quot;how long will it take to get there?&quot;) this problem is more easily treated as a distance-related<sup>1</sup> question since the routes that are available tend not to change depending on what time it is.</p><p>Dijkstra&apos;s shortest path algorithm [1] is a famous example of a solution to such a question - it operates on a graph and works by pruning the edges until each edge that remains in the tree leads to the next node on the shortest path to the destination. This graph may then serve as a useful reference to lookup the shortest path to the destination from any node, provided that the graph from which this tree was derived does not change.</p><p>If however, like me, you prefer to take the bus instead, the question might be reframed to be something like: &quot;If I leave, what is the fastest route to work by public transport?&quot;. At first glance this seems like it could be the same problem, but there is a key difference in that a bus departs from specific locations at specific times and once it has departed, it can not be caught. In other words, the routes that are available to take change as time progresses. A graph that describes this kind of behaviour is not &apos;static&apos; - instead it is described as time variant, dynamic, or temporal.</p><p>As a result we can not apply Djikstra or other algorithms that operate on static graphs - in a temporal graph these method are not reliable. Even though the <em>shortest</em> path may go next via one node, delays between opportunities to traverse the edges on this path may mean that the <em>fastest</em> path may be via a different one. It is also possible that the shortest path might never be available when time is considered. Instead we must look to a class of algorithms which take into account the properties of temporal edges.</p><p>Edges in a temporal graph differ from their static graph counterparts by having the properties of <em>starting time</em> and <em>traversal time</em>. These are the times at which the edge is able to be traversed and how much time will have elapsed by the moment of arriving at the adjacent node. In the context of catching a bus, for example, the starting time could be the scheduled deparature time and the traversal time the expected duration to the next stop. Traversal time can be seen as analogous to edge weight in non-temporal graphs, but starting time is fundamentally different in that once it is passed the edge may never be traversed - cue flash back to the trauma of running for, and then missing, a bus by merely seconds.</p><h2 id="computing-earliest-arrival-times-in-temporal-graphs">Computing earliest-arrival times in temporal graphs</h2><p>The authors of [2] propose an algorithm for computing the earliest-arrival <em>time</em> from the start node <em>x</em> to every other node <em>v</em> starting at time <em>t<sub>&#x3B1;</sub></em> and bounded by end time <em>t<sub>&#x3C9;</sub></em>.</p><p>Here is an equivalent pseudocode for [2] (<em>Algorithm 1</em>):</p><pre><code>procedure earliest(G, x, t&#x3B1;, t&#x3C9;):

	for each vertex v in G do:
		t[v] &#x2190; &#x221E;
	t[x] &#x2190; t&#x3B1;
	
	for each edge e = (u, v, t, &#x3BB;) in edge stream of G do:
		if t + &#x3BB; &#x2264; t&#x3C9; and t &#x2265; t[u] then:
			if t + &#x3BB; &lt; t[v] then:
				t[v] &#x2190; t + &#x3BB;
		else if t &#x2265; t&#x3C9;:
			break loop
	
	return t[v] for each v &#x2208; V
			
</code></pre><p>Let&apos;s dissect the algorithm.</p><pre><code>for each vertex v in G do:
	t[v] &#x2190; &#x221E;
t[x] &#x2190; t&#x3B1;
</code></pre><p>In other words, maintain a map of earliest-arrival at each destination nodes, <em>t[v]</em>. Assume that <em>x</em> is reachable by time <em>t<sub>&#x3B1;</sub></em>, so set earliest-arrival at <em>x</em>, <em>t[x] = t<sub>&#x3B1;</sub></em>.</p><p><em>t[v]</em> now represents the earliest-arrival time at <em>v</em>. By extension, <em>t[v]</em> is also the earliest-departure at <em>v</em>, since it is not possible to depart from a node you have not yet arrived at. If <em>t[v] = &#x221E;</em>, then <em>v</em> is not reachable from <em>x</em> starting at time <em>t<sub>&#x3B1;</sub></em> <sup>2</sup>.</p><pre><code>for each edge e = (u, v, t, &#x3BB;) in edge stream of G do:
</code></pre><p>The algorithm processes what [2] describes as the &apos;edge stream&apos; representation of the graph. This is simply the set of edges in the graph ordered by starting time, <em>t</em>. If we process the edges in ascending time order, then it is not necessary to store any historical results - the map will be consistent for the start time of every edge.</p><pre><code>if t + &#x3BB; &#x2264; t&#x3C9; and t &#x2265; t[u] then:
</code></pre><p>If the edge ending time (starting time, <em>t</em>, plus traversal time <em>&#x3BB;</em>), <em>t + &#x3BB;</em>, is outside the scope of the search then ignore it.</p><p>If the edge starting time is before the earliest-arrival time of the starting node then it is impossible to reach in time to traverse it, so also ignore it.</p><pre><code>if t + &#x3BB; &lt; t[v] then:
	t[v] &#x2190; t + &#x3BB;
</code></pre><p>If the edge ending time is before the earliest-arrival time of the ending node, then a new earliest-arrival time has been found.</p><pre><code>else if t &#x2265; t&#x3C9;:
	break loop
</code></pre><p>Given that we&apos;re processing edges in ascending time order, this is just a short-circuit to avoid processing/ignoring the remainder of the edge stream.</p><pre><code>return t[v] for each v &#x2208; V
</code></pre><p>After all edges have been processed then the resulting map, <em>t[v]</em> is the map of earliest-arrival time at <em>v</em>, starting at <em>u</em> at time <em>t<sub>&#x3B1;</sub></em>.</p><h2 id="computing-earliest-arrival-paths-in-temporal-graphs">Computing earliest-arrival paths in temporal graphs</h2><p>In most applications where knowing the earliest-arrival time is useful, knowing the <em>path</em> taken to realise that arrival-time is also required. [2], however, does not describe steps for constructing the earliest-arrival path.</p><p>Constructing the earliest-arrival path is possible with no modifications to the algorithm by maintaining the earliest-arrival path alongside the earliest-arrival time for each destination node.</p><p>Here is an extended pseudocode including earliest-arrival path construction:</p><pre><code>procedure earliest(G, x, t&#x3B1;, t&#x3C9;):

	for each vertex v in G do:
		t[v] &#x2190; &#x221E;
	t[x] &#x2190; t&#x3B1;
	p[x] &#x2190; {} // Empty list
	
	for each edge e = (u, v, t, &#x3BB;) in edge stream of G do:
		if t + &#x3BB; &#x2264; t&#x3C9; and t &#x2265; t[u] then:
			if t + &#x3BB; &lt; t[v] then:
				t[v] &#x2190; t + &#x3BB;
				p[v] &#x2190; append(p[u], u)
		else if t &#x2265; t&#x3C9;:
			break loop
	
	return t[v], append(p[v], v) for each v &#x2208; V
</code></pre><p>And a brief explanation of why this works:</p><pre><code>p[x] &#x2190; {} // Empty list
</code></pre><p><em>p[x]</em> represents the nodes prior to <em>x</em> in the earliest-arrival path. Since <em>x</em> is the starting node, no nodes have been visited beforehand to get to <em>x</em>.</p><pre><code>if t + &#x3BB; &lt; t[v] then:
	p[v] &#x2190; append(p[u], u)
</code></pre><p>Every time a new earliest-arrival time for <em>v</em> is discovered, we know that time <em>t[v]</em> is the edge ending time <em>t + &#x3BB;</em>. It follows that the new earliest-arrival path preceding <em>v</em>, <em>p[v]</em> is the earliest-arrival path preceding <em>u</em>, <em>p[u]</em> plus <em>u</em> itself.</p><pre><code>return append(p[v], v) for each v &#x2208; V
</code></pre><p>It should be clear that the complete earliest-arrival path from <em>x</em> to <em>v</em> is the earliest-arrival path preceding <em>v</em>, <em>p[v]</em> plus <em>v</em> itself.</p><h3 id="computing-earliest-arrival-paths-in-temporal-graphs-a-go-implementation">Computing earliest-arrival paths in temporal graphs - a Go implementation</h3><p>The following is an implementation in Go, the structure of which is inspired by similar algorithms from the excellent Gonum scientific and numerical library [3].</p><pre><code>package path

import (
	&quot;gonum.org/v1/gonum/graph&quot;
	&quot;gonum.org/v1/gonum/graph/temporal&quot;
)

type Earliest struct {
	from  graph.Node
	at    uint64
	until uint64
	nodes map[int64]struct {
		earliest uint64
		via      []int64
	}
}

func (e *Earliest) set(v graph.Node, t uint64, p []int64) {
	e.nodes[v.ID()] = struct {
		earliest uint64
		via      []int64
	}{
		t,
		p,
	}
}

func EarliestArrivalFrom(g graph.TemporalStream, from graph.Node, at uint64, until uint64) Earliest {
	earliest := Earliest{
		from:  from,
		at:    at,
		until: until,
		nodes: make(map[int64]struct {
			earliest uint64
			via      []int64
		}),
	}
	earliest.set(from, at, []int64{})
	s := g.LineStream()
	for s.Next() {
		l := s.TemporalLine()
		u := l.From()
		uid := u.ID()
		eu, ok := earliest.nodes[uid]
		tl := l.At()
		dtl := l.Duration()
		if !ok {
			continue
		}
		if tl+dtl &lt;= until &amp;&amp; tl &gt;= eu.earliest {
			v := l.To()
			vid := v.ID()
			ev, ok := earliest.nodes[vid]
			if !ok || tl+dtl &lt; ev.earliest {
				earliest.set(v, tl+dtl, append(eu.via, uid))
			}
		} else if tl &gt;= until {
			break
		}
	}
	return earliest
}
</code></pre><p><sup>1</sup> Here &apos;distance&apos; refers to the time-distance cost of traversing a length of road with a particular speed limit.</p><p><sup>2</sup> I recommend avoiding floating-point representations of time (ain&apos;t nobody got time for debugging issues due to rounding errors), so get creative with representing infinity in the language of your choice e.g. in my Go implementation I use an unsigned type to represent discrete time and consider <em>v</em> not in <code>map[v]uint</code> to mean infinity.</p><p>[1] <a href="https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm?ref=ifthenelse.io">Djikstra&apos;s shortest path algorithm</a></p><p>[2] <a href="https://www.vldb.org/pvldb/vol7/p721-wu.pdf?ref=ifthenelse.io">Wu et al, Path Problems in Temporal Graphs</a></p><p>[3] <a href="https://github.com/gonum/gonum?ref=ifthenelse.io">Gonum (github.com)</a></p>]]></content:encoded></item></channel></rss>