Lesson 01 / 10
Finite Automata
Measuring the state budget by exhaustive count: in a universe of 31 strings, 1-state automata recognize 2 distinct languages, 2-state ones 26, 3-state ones 1054; against the 2-to-the-31 languages that could be written over that universe, this is a share of 0.0000004908.
Contents
The Advanced Algorithms course deliberately left two questions open. In the traveling salesman and Hamiltonian path lessons, the question “why is this problem hard” was referred to this course in a dedicated section each; the course’s closing then asked: for some problems, is not knowing anything better than brute force a fact about our knowledge, or about the problems themselves. Both debts look at the same place. The first asks about the difficulty of a single problem, the second about where difficulty stands in general. That course’s final measurement showed that testing a candidate path costs 11 steps, while showing that no such path exists costs an average of 11,601 steps — and as was stated plainly there, this was an observation, not a proof. Naming the observation and saying where it turns into a proof is this course’s job.
This course is not a speed course. What it asks is not how many steps a procedure spends, but what a model can never do at all. But a theory’s results cannot be proved by running them. This is why what is measured here is not the theory itself, but what a finite budget can and cannot say. In every lesson, three numbers sit side by side: the budget, what the budget answers, and what it fails to answer. The course’s rule follows from this: a result’s number is written together with its budget, and an unsolvability claim whose budget is not written counts as unmeasured. A finite run never says “this language cannot be recognized” anywhere; it can only say it was not recognized for this k.
State Count as Budget
The first model is the one that has the least. A finite automaton reads its input once, from left to right, keeps nothing it has read, and sits in only one of a finite number of states at any time. It has no memory; the state count takes memory’s place. The definition of a state machine was established in the Software Architecture and Modeling and Representation courses and is not repeated here; the question here is different: how many languages can be recognized.
- MC1 — The alphabet has two symbols. The universe is the finite set of all strings not exceeding a given length limit, and the empty string is in the universe. The universe of strings not exceeding length 4 contains 31 strings.
- MC2 — A language is a subset of the universe. The number of languages that can be written over a 31-string universe is .
- MC3 — The automaton is built deterministic: the start state is 0, the transition table assigns a single state to every state-symbol pair, and the accepting set is a subset of the states. A string is recognized if the state reached after reading it is in the accepting set.
- MC4 — All k-state automata can be counted: the combination of transition tables with accepting sets. 2 automata for k=1, 64 for k=2, 5832 for k=3.
- MC5 — The measure is an exhaustive count: there is no estimate, no
sampling. No measurement in this course uses
randomortime; what is measured is steps, not duration.
A concrete example settles the definition. A two-state automaton can hold the answer to “is the number of a’s read so far even” in its state: every a flips the state, every b leaves it as it is, and the accepting set is the even state. This automaton neither stores the input nor reads it back; yet it correctly classifies every string of every length. This is what it means to recognize a language: deciding whether a string is in the language, with finite memory and a single pass.
"""Finite automaton: ALL k-state automata are counted, the languages they recognize are collected.""" from itertools import product ALPHABET = ("a", "b") def strings(max_length): output = [""] for n in range(1, max_length + 1): output += ["".join(d) for d in product(ALPHABET, repeat=n)] return output def run_automaton(transitions, accepting, string, start=0): state = start for symbol in string: state = transitions[(state, symbol)] return state in accepting def automata(k): """All k-state deterministic automata: transition table x accepting set.""" cells = [(state, symbol) for state in range(k) for symbol in ALPHABET] for targets in product(range(k), repeat=len(cells)): transitions = dict(zip(cells, targets)) for accept_mask in range(1 << k): accepting = {i for i in range(k) if accept_mask >> i & 1} yield transitions, accepting def recognizable_languages(k, universe): """The number of DISTINCT languages recognized by k-state automata over the universe.""" languages = set() for transitions, accepting in automata(k): languages.add(frozenset(s for s in universe if run_automaton(transitions, accepting, s))) return languages U = strings(4) print("universe: number of strings not exceeding length 4 =", len(U)) print("states automata distinct languages share of universe's writable languages") for k in (1, 2, 3): languages = recognizable_languages(k, U) print(f" {k:3d} {sum(1 for _ in automata(k)):6d} {len(languages):16d}" f" {len(languages) / 2 ** len(U):.10f}") print("total number of languages writable over the universe: 2^31 =", 2 ** 31)
universe: number of strings not exceeding length 4 = 31
states automata distinct languages share of universe's writable languages
1 2 2 0.0000000009
2 64 26 0.0000000121
3 5832 1054 0.0000004908
total number of languages writable over the universe: 2^31 = 2147483648
Three numbers side by side. Budget: state count 1, 2, 3 — 2, 64, and 5832 automata respectively. What the budget answers: these automata settle 2, 26, and 1054 distinct languages over the 31-string universe. What the budget fails to answer: languages remain; the three-state budget’s share is 0.0000004908.
As the budget rises to 5832 automata, the recognized-language count rises from 2 to 1054, a 527-fold increase. The covered share, by contrast, rises from one in a billion to half in a million. The model class grows, and the ratio is still close to zero. The second point worth noting is that 5832 automata produce only 1054 distinct languages: most automata are copies of one another, because unreachable states and equivalent states rewrite the same language.
What Changes as the Budget Grows
The ratio above was measured in a single universe, and a ratio measured in a single universe is not a result. A budget sweep is mandatory in this course: every measurement is repeated at at least three budget values, and whether the result changes with the budget is recorded. The second axis swept here is the universe’s length limit.
- MC6 — The budget sweep is done along two axes: state count and the universe’s length limit. A budget’s adequacy can only be read once both are written together.
"""Budget sweep: as the universe grows, how much does the same state budget cover.""" from itertools import product ALPHABET = ("a", "b") def strings(max_length): output = [""] for n in range(1, max_length + 1): output += ["".join(d) for d in product(ALPHABET, repeat=n)] return output def run_automaton(transitions, accepting, string): state = 0 for symbol in string: state = transitions[(state, symbol)] return state in accepting def automata(k): cells = [(state, symbol) for state in range(k) for symbol in ALPHABET] for targets in product(range(k), repeat=len(cells)): transitions = dict(zip(cells, targets)) for accept_mask in range(1 << k): yield transitions, {i for i in range(k) if accept_mask >> i & 1} def recognizable_languages(k, universe): return {frozenset(s for s in universe if run_automaton(transitions, accepting, s)) for transitions, accepting in automata(k)} print("length strings k=1 k=2 k=3 languages in universe share k=3 covers") for length in (1, 2, 3, 4): U = strings(length) counts = [len(recognizable_languages(k, U)) for k in (1, 2, 3)] print(f"{length:6d} {len(U):7d} {counts[0]:3d} {counts[1]:3d} {counts[2]:4d}" f" {2 ** len(U):20d} {counts[2] / 2 ** len(U):.10f}")
length strings k=1 k=2 k=3 languages in universe share k=3 covers
1 3 2 8 8 8 1.0000000000
2 7 2 26 116 128 0.9062500000
3 15 2 26 690 32768 0.0210571289
4 31 2 26 1054 2147483648 0.0000004908
This table is a lesson’s result on its own. In the three-string universe with length limit 1, three-state automata recognize every language: the covered share is 1.0000000000. At length 2 the share is 0.9062500000, at length 3 it is 0.0210571289, at length 4 it is 0.0000004908. Same budget, same model, four measurements — and the whole path between “sufficient” and “nothing.” A budget’s adequacy is not a property of the budget; it is a relationship between the budget and the universe.
The second column is even more instructive. Two-state automata recognize 26 languages at length 2, and still 26 at lengths 3 and 4. As the universe rises from 7 strings to 31, the recognized-language count does not move at all. This saturation is not a measurement flaw: because the number of distinguishable states with two states is exhausted, every new string added to the universe falls into one of the 26 languages that already exist. Growing the budget sometimes changes everything, sometimes nothing; and which one it is can only be seen by measuring.
How Many States Are Needed to Recognize a Language
Until now the question was “how many languages does k states recognize.” The reverse is more useful: how many states, at minimum, does a given language require. The tool for this is the residual language: which suffixes, after a given prefix, put the string into the language. If two prefixes carry the same residual language, no automaton is required to separate them; if they carry different residual languages, an automaton is required to put them in different states. The count of distinct residual languages is called a distinguishability class.
- MC7 — A prefix’s residual language is the set of suffixes in the universe that, when appended after that prefix, put the string into the language.
- MC8 — Only a suffix whose both extensions stay in the universe can separate two prefixes. A suffix that goes past the length limit does not force the automaton into anything.
- MC9 — The exhaustive count is made cheaper here: accepting sets are not scanned separately; once a transition table is given, the accepting decision each string requires is collected, and if no conflict arises, that table suffices. The result is the same as trying accepting sets one by one.
"""Residual language, distinguishability class, and exhaustive count side by side.""" from itertools import product ALPHABET = ("a", "b") def strings(max_length): output = [""] for n in range(1, max_length + 1): output += ["".join(d) for d in product(ALPHABET, repeat=n)] return output def residual_language(prefix, target, universe): """Which suffixes, after the prefix, put the string into the language.""" return frozenset(s for s in universe if prefix + s in target) def distinguishability_classes(target, universe): """Shared definition's tool: the number of distinguishability classes.""" prefixes = {s[:i] for s in universe for i in range(len(s) + 1)} return len({residual_language(p, target, universe) for p in prefixes}) def separated_prefixes(target, universe): """Only a suffix whose BOTH extensions stay in the universe can separate two prefixes.""" universe_set = set(universe) prefixes = sorted({s[:i] for s in universe for i in range(len(s) + 1)}, key=lambda p: (len(p), p)) signature = {p: tuple((1 if p + s in target else 0) if p + s in universe_set else -1 for s in universe) for p in prefixes} selected = [] for p in prefixes: if all(any(x >= 0 and y >= 0 and x != y for x, y in zip(signature[p], signature[q])) for q in selected): selected.append(p) return len(selected) def recognizer_exists(k, target, universe): """All k-state transition tables are scanned; the accepting set is chosen for consistency.""" cells = [(state, symbol) for state in range(k) for symbol in ALPHABET] for targets in product(range(k), repeat=len(cells)): transitions = dict(zip(cells, targets)) required, consistent = {}, True for s in universe: state = 0 for symbol in s: state = transitions[(state, symbol)] wanted = s in target if required.setdefault(state, wanted) != wanted: consistent = False break if consistent: return True return False def smallest_k(target, universe, upper=5): for k in range(1, upper + 1): if recognizer_exists(k, target, universe): return k return None def even_a(s): return s.count("a") % 2 == 0 def ends_with_ab(s): return s.endswith("ab") print("language length strings class separated prefix exhaustive") for name, predicate in (("even a's ", even_a), ("ends with ab ", ends_with_ab)): for length in (2, 3, 4): U = strings(length) target = frozenset(s for s in U if predicate(s)) print(f"{name} {length:7d} {len(U):4d} {distinguishability_classes(target, U):4d}" f" {separated_prefixes(target, U):12d} {smallest_k(target, U):9d}")
language length strings class separated prefix exhaustive even a's 2 7 5 2 2 even a's 3 15 7 2 2 even a's 4 31 9 2 2 ends with ab 2 7 4 3 3 ends with ab 3 15 6 3 3 ends with ab 4 31 9 3 3
Three columns say three separate things, and two of them correct each other. The class count grows as 5, 7, 9 for the “even a’s” language; yet the exhaustive count shows that an automaton exists that recognizes the same language with two states. The difference comes from the universe’s cutoff: prefixes close to the length limit get artificially separated because their suffixes run past the edge of the universe. The class count is an over-count and cannot be read as the state count on its own. The separated-prefix count accounts for this cutoff and matches the exhaustive count exactly in all six measurements — this is an agreement, not a proof, and the budget it was tested at goes up to length limit 4.
This correction is the course’s own rule applied to itself. Class counting is a cheap calculation and the exhaustive count is expensive; whether the cheap calculation stands in for the expensive one is not assumed, it is tested. The same path is followed in the next three lessons: the cheap measure is written first, then compared against the exhaustive count within a budget, and where it diverges is not hidden.
The concept of a regular language is established here not by definition but by measurement. A language is regular if the required state count does not grow as the universe grows: “even a’s” stays at 2 across all three universes, “ends with ab” stays at 3. For these two languages a fixed budget can be chosen, and the chosen budget suffices at every length. The next lesson applies the same three measures to a language that does not carry this property.
Summary
- A finite automaton’s budget is its state count; all k-state deterministic automata can be counted, and there are 2, 64, and 5832 of them for k=1, 2, 3 respectively.
- Over the 31-string universe of length at most 4, these automata recognize 2, 26, and 1054 distinct languages; the number of languages writable over the universe is 2 to the 31, and three states cover a share of 0.0000004908.
- The budget sweep shows that a budget’s adequacy depends on the universe: three states cover the whole universe at length limit 1, and half in a million at length limit 4.
- The number of languages two-state automata recognize freezes at 26 past length 2; growing the universe changes nothing for this budget.
- The count of residual-language classes is an over-count: for “even a’s” the class count comes out to 9, while the exhaustive count shows two states suffice; both extensions of a distinguishing suffix must stay in the universe.
- A language is regular if the required state count does not grow with the universe; it stays constant across all three universes for these two languages.
Next Step
The next lesson applies the same three measures to a language that requires counting: strings with an equal number of a’s followed by an equal number of b’s. What will be asked is this: if the required state count grows with the universe, is growing the budget a solution, or is it not the budget but the model class that needs to change. The answer comes when a three-line production rule does what an entire family of automata cannot.
To keep your progress and take notes, Log in
My notes
Log in to take notes.