Posted on Apiary – the place where bees, conservation, and self‑governing AI agents meet the world of algorithms.
Introduction
When a researcher asks “Does the word honey appear in this 2‑million‑character transcript of a bee‑monitoring sensor?” the answer must be instantaneous. In the same way, an autonomous AI agent that logs its own actions needs to know, in real time, whether a particular command pattern has already occurred. The naïve “scan‑and‑compare” approach—checking each possible position—fails spectacularly once the data size climbs into the millions or billions. What we need is a data structure that pre‑processes the text once, then answers any substring existence question in time proportional only to the length of the query string, not to the size of the whole corpus.
Enter the suffix automaton (also known as the minimal deterministic finite automaton of all substrings). First described independently by Blumer et al. (1985) and by Crochemore & Lecroq (1997), the suffix automaton gives us a compact, linear‑size representation of every substring of a text. Building it costs O(N) time and memory, where N is the length of the original string, and each query runs in O(|P|), where P is the pattern we are checking. In practice, the constant factors are tiny: an automaton for a 10 MB DNA sequence typically occupies less than 30 MB of RAM and answers a 50‑character pattern in under a microsecond on a modern laptop.
In this pillar article we will:
- Explain why substring queries matter in conservation, AI, and everyday computing.
- Walk through the theory that turns a set of substrings into a minimal DFA.
- Show a step‑by‑step construction algorithm that works online (character by character).
- Prove the tight bounds on the number of states and edges.
- Demonstrate how to answer existence, counting, and enumeration queries.
- Compare the suffix automaton with related structures like the suffix-tree and suffix-array.
- Provide concrete implementation tips in C++ and Python, with pitfalls to avoid.
- Highlight real‑world applications— from bee‑population monitoring to AI‑agent log analysis.
By the end you will have a ready‑to‑use toolbox for turning massive strings into lightning‑fast substring indexes, and you’ll see how the same ideas help protect pollinators and keep autonomous systems transparent.
1. What Is a Substring Query?
A substring of a string S is any contiguous block of characters taken from S. Formally, S[i…j] for 0 ≤ i ≤ j < |S|. The classic problem is:
**Given a fixed text T and many patterns P₁, P₂, …, Pₖ, decide for each Pᵢ whether it occurs as a substring of T.
When |T| is small (a few hundred characters) we can afford to scan T for each pattern. But most modern workloads involve large‑scale texts:
| Domain | Typical size of T | Example |
|---|---|---|
| Genomics | 3 × 10⁹ (human genome) | Search for disease‑related motifs |
| Environmental monitoring | 10⁶–10⁸ (sensor logs) | Detect the pattern “hive‑temp‑> 35°C” |
| AI‑agent orchestration | 10⁸ (log of actions) | Spot repeated command sequences |
| Web indexing | 10⁹–10¹² (crawled pages) | Find all occurrences of a keyword |
A naïve scan costs O(|T|·|P|) per query. For a 10⁸‑character log and a 30‑character pattern, that’s 3 × 10⁹ character comparisons—far too much for an interactive system.
1.1 Naïve Approaches and Their Limits
| Method | Pre‑processing | Query time | Memory | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Direct scan | none | O( | T | · | P | ) | O(1) | ||||||
| Rolling hash (Rabin‑Karp) | O( | T | ) to compute hashes | O( | P | ) expected | O( | T | ) (hash array) | ||||
| Suffix tree | O( | T | ) | O( | P | ) | 2· | T | edges (≈ 40 × | T | bytes) | ||
| Suffix array + LCP | O( | T | log | T | ) build | O( | P | log | T | ) | O( | T | ) integers |
| Suffix automaton | **O( | T | )** | **O( | P | )** | **≤ 3· | T | – 4 states** |
The suffix automaton matches the best query time (linear in P) while keeping the construction time and memory linear in T, with a small constant. It also has the pleasant property of being deterministic: each state has at most one outgoing edge per alphabet symbol, which simplifies implementation and debugging.
2. From DFA to Suffix Automaton
A deterministic finite automaton (DFA) is a 5‑tuple (Q, Σ, δ, q₀, F) where:
- Q – a finite set of states,
- Σ – the input alphabet (e.g.,
{a,b,c,…}), - δ: Q × Σ → Q – the transition function,
- q₀ ∈ Q – the start state,
- F ⊆ Q – the set of accepting states.
If we feed a string x into a DFA, we follow the transitions defined by δ. If we end in an accepting state, the DFA accepts x. For substring queries we want a DFA that **accepts exactly the set of substrings of a given text T**.
2.1 The Naïve DFA of All Substrings
One could construct a DFA that has a distinct path for every possible substring of T. In the worst case (e.g., T = “aaaa…a” of length N), there are N(N+1)/2 ≈ N²/2 distinct substrings, leading to a DFA with Θ(N²) states—unacceptable for large N.
2.2 Minimization Gives the Suffix Automaton
The key observation is that many of those paths are equivalent: they lead to the same set of possible continuations. Minimizing the naïve DFA (using Hopcroft’s algorithm, O(|Q| log |Q|)) merges equivalent states, collapsing the explosion to a linear‑size automaton. The result is called the suffix automaton (SA) of T.
Formally, the suffix automaton is the minimal DFA that recognizes the language
L(T) = { x | x is a substring of T } .
Because it is minimal, every state corresponds to an equivalence class of substrings that share the same set of possible extensions. This property makes the SA not only compact but also powerful for counting and enumerating substrings.
2.3 Relationship to Other Structures
- The SA is the dual of the suffix tree: each state of the SA corresponds to a node in the suffix tree, but the edges are reversed and merged.
- A suffix array can be used to simulate the SA’s queries, but the SA offers O(1) transition time per character, while a suffix array needs a binary search (log |T|) for each step.
- The SA also underlies the Aho-Corasick automaton for multi‑pattern matching; the latter can be seen as the product of several suffix automatons.
3. Building the Suffix Automaton – An Online Algorithm
The classic construction runs online, i.e., it processes the text character by character and updates the automaton incrementally. The algorithm is sometimes called the “incremental construction” or “Ukkonen‑style” method for SA (not to be confused with Ukkonen’s suffix‑tree algorithm). Below we present the version popularized by D. Gusfield (1997) and later refined by Crochemore & Lecroq (1997).
3.1 Core Data per State
Each state v stores:
| Field | Meaning |
|---|---|
len(v) | Length of the longest string represented by v (the longest substring that ends at this state). |
link(v) | Suffix link (also called fail or parent) pointing to the state that represents the longest proper suffix of the strings of v. |
next(v)[c] | Transition dictionary: for each character c ∈ Σ, the state reached from v by reading c. |
occ(v) (optional) | Number of occurrences of the substrings represented by v in the original text (computed later). |
All fields are integers; next can be a hash map (for large alphabets) or a fixed array (for small alphabets like DNA {A,C,G,T}).
3.2 Pseudocode
function buildSA(string S):
create initial state 0
len(0) = 0
link(0) = -1 // root has no suffix link
last = 0 // points to the state representing whole S processed so far
for each character ch in S:
cur = new state
len(cur) = len(last) + 1
// step 1: add transition from last via ch
p = last
while p != -1 and next(p)[ch] is undefined:
next(p)[ch] = cur
p = link(p)
// step 2: fix suffix link of cur
if p == -1:
link(cur) = 0
else:
q = next(p)[ch]
if len(p) + 1 == len(q):
link(cur) = q
else:
// clone state
clone = new state
len(clone) = len(p) + 1
next(clone) = copy of next(q)
link(clone) = link(q)
while p != -1 and next(p)[ch] == q:
next(p)[ch] = clone
p = link(p)
link(q) = link(cur) = clone
last = cur
Explanation of the three phases:
- Extend the automaton by adding a new state
curthat represents the whole prefix processed so far (S[0…i]). - Propagate the new transition
p --ch--> curbackwards along suffix links until we encounter a state that already has a transition onch. This ensures that every suffix of the new prefix can be extended bych. - Fix the suffix link of
cur. If the target stateqalready satisfies the length condition (len(p) + 1 == len(q)), we simply linkcurtoq. Otherwise we cloneqto create a new intermediate state with a shorterlen, preserving determinism.
The algorithm touches each character a constant number of times, leading to O(N) total work. The number of created states is at most 2·N – 1, but a tighter bound (proved later) shows it never exceeds 2·N – 1 and often is close to N for random strings.
3.3 Example: Building SA for “ababa”
| Step | New character | States created | Suffix links (parent) | Notable transitions |
|---|---|---|---|---|
| 0 | – | state 0 (root) | link(0) = ‑1 | – |
| 1 | a | state 1 | link(1) = 0 | 0 –a→ 1 |
| 2 | b | state 2 | link(2) = 0 | 1 –b→ 2, 0 –b→ 2 |
| 3 | a | state 3 | link(3) = 1 | 2 –a→ 3, 0 –a→ 1 (already) |
| 4 | b | state 4 | link(4) = 2 | 3 –b→ 4, 1 –b→ 2 (already) |
| 5 | a | state 5 (clone of 3) + state 6 | link(5) = 1, link(6) = 3 | 4 –a→ 5, 2 –a→ 3 (remains) |
The final automaton has 7 states (including the root) and 12 transitions, far fewer than the 15 substrings of “ababa”. Each state groups substrings with the same continuation set, e.g., state 3 represents the substrings “aba” and “ba”, both of which can be extended only by “b”.
4. Formal Properties and Size Bounds
Understanding the theoretical limits of the suffix automaton helps us predict memory usage for large datasets.
4.1 Number of States
Theorem 1 (State bound). For a string of length N over any alphabet, the suffix automaton contains at most 2·N – 1 states.
Proof Sketch. Each iteration creates at most two states: the new state cur and possibly a clone. The root is counted once, so after N iterations we have ≤ 2·N – 1. Moreover, each state (except the root) has a distinct len value, and len ranges from 1 to N, guaranteeing no more than N distinct lengths. The cloning step only occurs when a previously created state needs to be split, which can happen at most N – 1 times. ∎
In practice, for random strings over a moderate alphabet (size ≥ 5), the automaton size tends to be close to N because cloning is rare. For highly repetitive strings (e.g., “aaaa…a”), the bound is tight: the automaton for “aⁿ” has exactly 2·N – 1 states.
4.2 Number of Transitions
Theorem 2 (Edge bound). The total number of transitions (edges) in a suffix automaton is ≤ 3·N – 4.
Proof Sketch. Each new character adds at most one outgoing edge from the newly created state (cur). Cloning may copy all outgoing edges of the original state q. However, each original edge can be copied at most once, because after a clone is created the original edge never gets copied again. Summing the contributions yields ≤ 3·N – 4. ∎
Thus the memory consumption is linear with a small constant (≈ 3) for the adjacency maps. Using a compact representation (e.g., vector of pairs for each state) can shrink the footprint to under 20 bytes per character for DNA alphabets, well within typical server memory limits.
4.3 Minimality
The suffix automaton is minimal among all DFAs that accept exactly the set of substrings of T. Minimality follows from the Myhill‑Nerode theorem: two substrings are equivalent iff they have the same set of possible continuations. The construction merges exactly those equivalent states, no more, no less.
4.4 Additional Useful Quantities
len(v)gives the length of the longest substring reaching v.link(v)points to the state representing the longest proper suffix. Traversing suffix links from a state yields all suffixes of a substring, in decreasing length.occ(v)(computed by a post‑order DP) tells how many times the substrings of v appear in T. This is valuable for frequency analysis, e.g., finding the most common bee‑behavior motif.
5. Answering Queries with the Suffix Automaton
Once the automaton is built, any substring query reduces to a simple walk.
5.1 Existence (Yes/No)
function exists(SA, pattern P):
state = 0 // start at root
for each character ch in P:
if next(state)[ch] is undefined:
return false
state = next(state)[ch]
return true
The loop costs O(|P|) time, independent of the size of the original text. For a pattern of length 100, the function performs at most 100 hash lookups or array accesses.
5.2 Counting Occurrences
If occ(v) has been pre‑computed (see Section 5.3), we can return the exact frequency:
function countOccurrences(SA, pattern P):
state = walk(P) // same as exists()
if state == null: return 0
return occ(state)
The DP to compute occ runs once after construction:
for each state v in order of decreasing len:
if link(v) != -1:
occ(link(v)) += occ(v)
We initialize occ(v) = 1 for each state that corresponds to an end position (i.e., each time we add a new character, we increment occ(last)). The algorithm runs in O(N) time and yields the exact number of occurrences for every substring.
5.3 Lexicographic Enumeration
Because the automaton is deterministic, we can enumerate all substrings in lexicographic order by a depth‑first traversal that respects alphabetical ordering of outgoing edges. The number of distinct substrings equals
Σ_{v ≠ root} (len(v) – len(link(v)))
This formula counts, for each state, the number of new substrings contributed by its longest string minus the longest string of its suffix link. In the “ababa” example, the sum yields 15, matching the actual count.
5.4 Finding the Longest Repeated Substring
A longest repeated substring corresponds to a state with occ(v) ≥ 2 and maximal len(v). Traversing all states once gives the answer in O(N) time. For a 10 GB log of AI‑agent actions, this operation can reveal the most common command sequence that may indicate a bug or a policy loop.
5.5 Example: Bee‑Temperature Log
Suppose a hive sensor records a stream of temperature alerts encoded as letters:
T = "HTLHTLHTLHTL" // H = high, T = normal, L = low
We build the suffix automaton for T. To check whether the pattern “HTLHT” ever appeared, we walk five steps; the automaton returns true in microseconds. To know how many times it appeared, we look at occ(state) after the walk and obtain 2 (positions 0 and 4). This enables rapid alerts: if a dangerous temperature pattern appears more than once within a day, the system can trigger a protective response.
6. Comparing the Suffix Automaton to Related Structures
| Feature | Suffix Automaton | Suffix Tree | Suffix Array + LCP | Rolling Hash | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Construction time | O(N) (linear, simple) | O(N) (Ukkonen) but higher constant | O(N log N) (often O(N) with induced sorting) | O(N) (hashes) | ||||||||
| Memory usage | ≤ 3·N edges, ~2·N states | 2·N nodes + 2·N edges (≈ 20·N bytes) | N integers (4 N bytes) + LCP (4 N) | N integers (hashes) | ||||||||
| Query time (existence) | O( | P | ) deterministic | O( | P | ) (follow tree edges) | O( | P | log N) (binary search) | O( | P | ) expected (collision risk) |
| Counting occurrences | O(1) after DP | O(1) after leaf count | O(log N) (range query) | O( | P | ) (re‑hash) | ||||||
| Supports dynamic updates | Yes (online) | Yes (Ukkonen) but complex | No (static) | Yes (re‑hash) | ||||||||
| Implementation difficulty | Moderate (hash maps) | High (edge splits) | Moderate (sorting) | Easy |
When to pick a suffix automaton?
- When you need fast, deterministic queries on a single large string.
- When you want dynamic insertion of characters (e.g., a streaming sensor feed).
- When you also need occurrence counts for many patterns without extra data structures.
When a suffix tree may still be preferable?
- When you need to retrieve the actual positions of each occurrence (the tree stores leaf indices directly).
- When you work with a small alphabet and can afford the larger constant factor; the tree’s explicit edges can be more cache‑friendly for some workloads.
Suffix arrays shine for static massive collections (e.g., whole‑genome indexing) because they compress well and can be combined with FM‑index techniques. However, the suffix automaton remains the most straightforward choice for online substring queries.
7. Implementation Details – From Theory to Code
Below we outline practical considerations for turning the pseudocode into production‑grade code. We give snippets in both C++ (for performance‑critical pipelines) and Python (for rapid prototyping).
7.1 Choosing the Transition Container
- Small alphabets (DNA,
{A,C,G,T}) – use a fixed‑size arrayint next[4]. - Medium alphabets (ASCII letters) – a
std::array<int, 52>works. - Large or sparse alphabets (Unicode text, sensor IDs) – use
std::unordered_map<char,int>in C++ or a Pythondict.
The array approach eliminates hash overhead and yields better cache locality, but it inflates memory for large alphabets. A hybrid approach stores a small array for the most common characters and falls back to a map for the rest.
7.2 C++ Implementation (≈ 150 lines)
struct State {
int len = 0; // longest string length
int link = -1; // suffix link
std::unordered_map<char,int> nxt; // transitions
long long occ = 0; // occurrence count (optional)
};
class SuffixAutomaton {
public:
std::vector<State> st;
int last; // state representing whole string
SuffixAutomaton(size_t maxLen = 0) {
st.reserve(2*maxLen);
st.push_back(State()); // root = 0
last = 0;
}
void extend(char c) {
int cur = (int)st.size();
st.push_back(State());
st[cur].len = st[last].len + 1;
st[cur].occ = 1; // each end position contributes one occurrence
int p = last;
while (p != -1 && !st[p].nxt.count(c)) {
st[p].nxt[c] = cur;
p = st[p].link;
}
if (p == -1) {
st[cur].link = 0;
} else {
int q = st[p].nxt[c];
if (st[p].len + 1 == st[q].len) {
st[cur].link = q;
} else {
int clone = (int)st.size();
st.push_back(State());
st[clone] = st[q]; // copy transitions
st[clone].len = st[p].len + 1;
st[clone].occ = 0; // clones start with 0 occ
while (p != -1 && st[p].nxt[c] == q) {
st[p].nxt[c] = clone;
p = st[p].link;
}
st[q].link = st[cur].link = clone;
}
}
last = cur;
}
void build(const std::string& s) {
for (char c : s) extend(c);
}
// propagate occurrence counts (post‑process)
void compute_occurrences() {
// bucket sort by length
std::vector<std::vector<int>> bucket(st.back().len + 1);
for (int i = 0; i < (int)st.size(); ++i)
bucket[st[i].len].push_back(i);
for (int l = (int)bucket.size() - 1; l > 0; --l) {
for (int v : bucket[l]) {
int p = st[v].link;
if (p != -1) st[p].occ += st[v].occ;
}
}
}
// existence query
bool contains(const std::string& pat) const {
int v = 0;
for (char c : pat) {
auto it = st[v].nxt.find(c);
if (it == st[v].nxt.end()) return false;
v = it->second;
}
return true;
}
// count occurrences (requires compute_occurrences())
long long count(const std::string& pat) const {
int v = 0;
for (char c : pat) {
auto it = st[v].nxt.find(c);
if (it == st[v].nxt.end()) return 0;
v = it->second;
}
return st[v].occ;
}
};
Key points:
occis accumulated after the automaton is built; this two‑pass approach keeps the online construction fast.- The bucket sort by length ensures that we process states in decreasing
len, which is required for correct propagation. - The
extendmethod is the heart of the algorithm; it follows the pseudocode closely, with the addition of occurrence initialization.
7.3 Python Implementation (≈ 70 lines)
class State:
__slots__ = ('len', 'link', 'next', 'occ')
def __init__(self):
self.len = 0
self.link = -1
self.next = {}
self.occ = 0
class SuffixAutomaton:
def __init__(self):
self.st = [State()] # root
self.last = 0
def extend(self, ch: str):
cur = len(self.st)
self.st.append(State())
self.st[cur].len = self.st[self.last].len + 1
self.st[cur].occ = 1
p = self.last
while p != -1 and ch not in self.st[p].next:
self.st[p].next[ch] = cur
p = self.st[p].link
if p == -1:
self.st[cur].link = 0
else:
q = self.st[p].next[ch]
if self.st[p].len + 1 == self.st[q].len:
self.st[cur].link = q
else:
clone = len(self.st)
self.st.append(State())
self.st[clone].len = self.st[p].len + 1
self.st[clone].next = self.st[q].next.copy()
self.st[clone].link = self.st[q].link
while p != -1 and self.st[p].next.get(ch) == q:
self.st[p].next[ch] = clone
p = self.st[p].link
self.st[q].link = self.st[cur].link = clone
self.last = cur
def build(self, s: str):
for ch in s:
self.extend(ch)
def compute_occurrences(self):
# bucket by length
max_len = max(st.len for st in self.st)
bucket = [[] for _ in range(max_len + 1)]
for idx, st in enumerate(self.st):
bucket[st.len].append(idx)
for l in range(max_len, 0, -1):
for v in bucket[l]:
link = self.st[v].link
if link != -1:
self.st[link].occ += self.st[v].occ
def contains(self, pat: str) -> bool:
v = 0
for ch in pat:
if ch not in self.st[v].next:
return False
v = self.st[v].next[ch]
return True
def count(self, pat: str) -> int:
v = 0
for ch in pat:
if ch not in self.st[v].next:
return 0
v = self.st[v].next[ch]
return self.st[v].occ
The Python version trades raw speed for readability. For production workloads you can compile it with Cython or switch to the C++ version via a Python extension.
7.4 Pitfalls to Avoid
| Pitfall | Symptom | Fix |
|---|---|---|
Forgot to reset last after each extend | Queries return false even for existing substrings. | Ensure last = cur at the end of extend. |
| Cloning without copying transitions | Automaton becomes nondeterministic, leading to infinite loops. | Use next.clone = next(q) (deep copy). |
Using int for occurrence counts on huge texts | Overflow when counting occurrences of short substrings (e.g., “a” in a 10⁹‑character file). | Switch to long long (C++) or Python int. |
Using unordered_map with default hash on char | Poor performance on Unicode strings (slow hash). | Provide a custom hash or store integer IDs for characters. |
| Neglecting to propagate occurrences | count always returns 1. | Call compute_occurrences() after the build phase. |
8. Real‑World Applications: Bees, AI Agents, and Conservation
The suffix automaton is more than a theoretical curiosity; it solves concrete problems in the domains that Apiary cares about.
8.1 Monitoring Hive Sensor Streams
Modern hives are equipped with temperature, humidity, vibration, and acoustic sensors that emit a continuous stream of symbols (e.g., H for high temperature, V for a vibration spike). Researchers often need to know whether a dangerous pattern—say, “HHVHH” (two high temperatures followed by a vibration, then two more highs)—has occurred within the last week.
- Build the suffix automaton for the last N days (e.g., N = 10⁷ characters).
- Run an existence query for the pattern.
- If the pattern exists, retrieve
occ(state)to know how many times it happened.
Because the automaton is incremental, as new sensor data arrives we simply call extend(newChar); the structure stays up‑to‑date without rebuilding from scratch. This enables real‑time alerts and historical analysis with negligible latency.
8.2 Analyzing AI‑Agent Logs
Self‑governing AI agents often log their decisions as a sequence of tokens (MOVE, EAT, REST). Detecting repeated sub‑plans can reveal loops or emergent strategies. For instance, a reinforcement‑learning robot may inadvertently repeat the command sequence “TURN‑LEFT, MOVE‑FORWARD, TURN‑RIGHT” many times, indicating a sub‑optimal policy.
- Encode each token as a short integer (e.g., 0‑255).
- Build a suffix automaton over the log.
- Query for candidate sub‑plans, or enumerate the top‑k most frequent substrings using the
occvalues.
The result feeds back into the agent’s meta‑learning module, allowing it to prune inefficient loops automatically—a step toward self‑optimizing AI.
8.3 Conservation Genomics
In conservation genetics, scientists compare DNA fragments from endangered bee populations to reference genomes. The suffix automaton can index a reference genome (≈ 3 × 10⁹ bases for honeybee) and then answer presence queries for millions of short markers (e.g., 50‑bp SNP primers) in linear time per marker.
- Build the automaton once (takes a few minutes on a 16‑core server).
- Feed every marker through the
containsroutine. - Count how many times each marker appears (
occ) to estimate copy number variations.
The speed and low memory footprint make this approach feasible even on commodity hardware, accelerating field‑to‑lab pipelines for rapid biodiversity assessment.
8.4 Bridging to conservation-algorithms
The suffix automaton fits neatly into the broader toolbox of conservation algorithms: it is deterministic, easy to reason about, and can be combined with other structures like Bloom filters for approximate membership testing when memory is ultra‑tight. Moreover, its deterministic nature aligns with the transparent decision‑making required for AI agents that manage natural resources, ensuring that every query’s answer can be traced back to a concrete path in the automaton.
Why It Matters
Fast substring queries are the hidden engine behind many of the data‑driven actions we take to protect pollinators and guide autonomous systems. By turning an enormous raw string into a compact, minimal DFA, the suffix automaton gives us instantaneous insight: we can spot dangerous temperature patterns before a hive collapses, detect looping behavior in an AI agent before it wastes resources, and count genetic markers that signal a population’s health—all with linear‑time preprocessing and deterministic, constant‑factor query performance.
In a world where data volumes grow faster than Moore’s law, the suffix automaton stands out as a lean yet powerful solution. It embodies the Apiary philosophy: smart algorithms serving a smarter planet. Whether you are a researcher, a conservationist, or an AI developer, mastering this structure equips you with a versatile tool that turns streams of symbols into actionable knowledge—fast, reliable, and ready for the challenges of tomorrow.