Finite State Machine

Prerequisite knowledge: Language and decision problems

Finite State Machine (Finite State Machine, FSM, also abbreviated as an automaton below) is the simplest class of computational models, characterized by extremely limited descriptive power and resources. Automata are widely used in OI and computer science, and their ideas appear in many string algorithms, so it is recommended to learn automata before studying string algorithms such as KMP, the Aho–Corasick automaton, and SAM.

Introduction to Automata

First, let us understand what automata are used for: an automaton is a mathematical model for determining whether a signal sequence satisfies a particular pattern or rule.

Some of the terms in this sentence can be explained more concretely. A “signal sequence” is a sequence of signals arranged in order, such as each character of a string from beginning to end, each number of an array from 11 to nn, or each digit of a number from most significant to least significant. “Determining whether a rule is satisfied” can be understood as asking whether we care about the sequence belonging to a particular set. This set is defined by rules that we specify in advance, such as “all binary strings of even length” or “all palindromes.”

Sometimes we need to answer questions of this kind: does a given sequence have a certain property? For example, is a binary number odd, is a string a palindrome, or is it a subsequence of another string? An automaton is a mathematical tool for solving such questions.

The way an automaton works is very similar to a flowchart. Suppose you want to order a cup of milk tea on a food-delivery platform; all your choices form a sequence. The following flowchart is an example:

order fsm
order fsm

For example, if your sequence of choices is “open the ordering interface -> choose milk tea -> have enough money for the milk tea,” then the states you pass through in order might be “food-delivery platform -> ordering interface -> pay for the milk tea -> bought the milk tea.” In this way, our “milk-tea automaton” determines, based on our choices, whether we bought the milk tea. We can also see that there may be more than one way to reach a state. Likewise, if you did not buy the milk tea, you might have exited directly from the ordering interface, or you might not have had enough money and therefore failed to buy it.

Using this automaton, we divided signal sequences into two classes: signal sequences that bought the milk tea and signal sequences that did not. By looking at the final state, we completed a decision problem.

Although we just used a flowchart as an analogy for how an automaton works, the flowchart itself is only an intuitive visualization tool and does not constitute the mathematical definition of an automaton. To characterize the structure of an automaton more precisely, we need to abstract the elements of the flowchart. After abstraction, we find that the structure of the flowchart can actually be simplified to a directed graph, where each vertex represents a state and each directed edge represents a transition between states.

Thus, the core structure of an automaton can be formally viewed as a directed graph, which we call a state graph.

An automaton works similarly to a flowchart, with the following differences: every vertex of an automaton is a decision vertex; a vertex of an automaton is just a state, not a task; and an edge of an automaton can accept multiple characters (not limited to T or F).

For example, the automaton for “determining whether a binary number is even” is as follows:

example fsm
example fsm

Starting from the initial vertex, accept the binary sequence of the number from the most significant digit to the least significant digit, and then see where you end up. If you end at a red-circled vertex, the number is even; otherwise, it is not.

Here, we should emphasize that we will repeatedly use terms such as “character” and “character set” below. This does not mean that automata can only be applied in the string domain: a character need not be a letter such as abc⋯z\tt abc\cdots z; it may also represent a choice.

If we need to determine the relationship between one finite signal sequence and another signal sequence (for example, whether the other signal sequence is a subsequence of a signal sequence), a common method is to construct an automaton for that finite signal sequence. This will be discussed when learning KMP.

Note that an automaton is only a mathematical model, and is neither an algorithm nor a data structure. There are many ways to implement the same automaton, and they may have different time and space complexities.

Next, you can either continue studying automata on this page, or learn concrete examples such as KMP, the Aho–Corasick automaton, or SAM.

FSMs are divided into two categories: deterministic finite automata and nondeterministic finite automata.

Deterministic Finite Automata

A deterministic finite automaton (Deterministic Finite Automaton, DFA) is characterized by a deterministic decision process. Taking the “milk-tea automaton” as an example, as soon as you open the ordering interface, you enter the ordering interface; unexpected events such as a network crash preventing it from opening or a dead phone screen do not occur.

DFA

A DFA is a five-tuple (Q,Σ,δ,q0,F)(Q,\Sigma,\delta,q_0,F) consisting of:

  1. A finite state set QQ. If a DFA is viewed as a directed graph, its states correspond to the vertices of the graph.
  2. An alphabet Σ\Sigma. The automaton can input only these characters.
  3. A transition function δ:Q×Σ→Q\delta:Q\times \Sigma \to Q, which is a function that accepts two parameters and returns a value. Its first parameter and return value are states, and its second parameter is a character from the alphabet. If a DFA is viewed as a directed graph, its transition function corresponds to the edges between vertices, with a character on each edge.
  4. An initial state q0∈Qq_0\in Q, which is a distinguished state. In different articles, the initial state is generally denoted by ss, start\textit{start}, or q0q_0; this article uses q0q_0.
  5. An accepting state set F⊆QF\subseteq Q, which is a set of distinguished states.

A DFA can be represented simply by the following structure:

Reference implementation
// Deterministic Finite Automaton (DFA)
struct DFA {
  int m;                                // Alphabet size.
  int n;                                // Number of states.
  int q0;                               // Initial state.
  std::vector<std::vector<int>> trans;  // Transitions: trans[c][q].
  std::vector<int> acc;                 // Acceptance labels per state:
                                        // - 0 = non-accepting

  DFA(int m, int n = 0, int q0 = 0)
      : m(m), n(n), q0(q0), trans(m, std::vector<int>(n)), acc(n) {}

  // Returns minimized DFA via Hopcroft's algorithm.
  DFA hopcroft_minimize() const;
};

The process of finding the state sequence of an input string ww in a DFA and determining whether it is accepted is called computation.

DFA computation

Let M=(Q,Σ,δ,q0,F)M=(Q,\Sigma,\delta,q_0,F) be a DFA, and let w=w1w2⋯wn∈Σ∗w=w_1w_2\cdots w_n\in\Sigma^* be a string. If there exists a sequence of states r0,r1,⋯ ,rnr_0,r_1,\cdots,r_n in QQ such that

  • r0=q0r_0=q_0,
  • δ(ri,wi+1)=ri+1\delta(r_i,w_{i+1})=r_{i+1} holds for every i=0,1,⋯ ,n−1i=0,1,\cdots,n-1,
  • rn∈Fr_n\in F,

then MM is said to accept (accepts) ww. Otherwise, MM is said to reject ww.

When a DFA reads a string, it transitions one character at a time from the initial state according to the transition function. If, after reading all characters of a string, it is in an accepting state, we say that the DFA accepts the string; otherwise, we say that the DFA rejects the string.

Formal language

A formal language (or simply a language) over a character set Σ\Sigma is a set of strings LL over Σ\Sigma.

Language recognized by an automaton

For an automaton MM, the language L(M)L(M) it recognizes is defined as the set of all strings it accepts, {w∣M accepts w}\{w\mid M\text{ accepts }w\}.

Not every language can be recognized by a DFA.

Regular language

If a language can be recognized by some DFA, it is called a regular language.

As mentioned above, an automaton can be represented by a state graph. The following is a DFA that accepts exactly the strings a\tt a, ab\tt ab, and aac\tt aac:

Figure: fsm3

(The dead state is omitted from the figure; all transitions not shown point to that dead state.)

Nondeterministic Finite Automata

A nondeterministic finite automaton1 (Nondeterministic Finite Automaton, NFA) is a natural generalization of a DFA. In an NFA, for any state and any character, there may be zero, one, or multiple successor states. At the same time, the NFA discussed in this section allows epsilon transitions, meaning that a state can transition to one of its successors without consuming any character.

For example, consider the “milk-tea automaton” again. After placing the order, even if you have enough money for the milk tea, poor network conditions may cause you not to buy it; this gives multiple successors. It is also possible that, because you were slow, the input string (that is, the sequence of operations) is the same but the milk tea has sold out, so you do not buy it; this is the presence of an epsilon transition, which may or may not be taken. The above automaton can be modified slightly to implement this behavior:

order nfa
order nfa

Clearly, every DFA is an NFA, so an NFA can recognize at least all regular languages. But as an extension of a DFA, can an NFA recognize more languages? In fact, it cannot; we will discuss the equivalence of DFAs and NFAs later.

NFA

Let P(Q)\mathcal{P}(Q) denote the power set of QQ. Let ε∉Σ\varepsilon\notin\Sigma denote the empty string, and write Σε=Σ∪{ε}\Sigma_\varepsilon = \Sigma\cup\{\varepsilon\}. An NFA is a five-tuple (Q,Σ,δ,q0,F)(Q,\Sigma,\delta,q_0,F) consisting of:

  1. A finite state set QQ,
  2. An alphabet Σ\Sigma,
  3. A transition function δ:Q×Σε→P(Q)\delta:Q\times \Sigma_{\varepsilon} \to \mathcal{P}(Q), a function that accepts two parameters and returns a state set. The first parameter is a state, the second parameter is a character from the alphabet, and the return value is the set of all possible successor states (which may be empty),
  4. An initial state q0∈Qq_0\in Q,
  5. An accepting state set F⊆QF\subseteq Q.

The computation of an NFA is equivalent to running multiple DFAs simultaneously. At each step, all possibilities are enumerated; finally, as long as one branch reaches an accepting state, the NFA accepts the entire string.

NFA computation

Let N=(Q,Σ,δ,q0,F)N=(Q,\Sigma,\delta,q_0,F) be an NFA. The string ww can be represented as y1y2⋯ym∈Σε∗y_1y_2\cdots y_m\in\Sigma^*_\varepsilon. If there exists a sequence of states r0,r1,⋯ ,rmr_0,r_1,\cdots,r_m in QQ such that

  • r0=q0r_0=q_0,
  • ri+1∈δ(ri,yi+1)r_{i+1}\in\delta(r_i,y_{i+1}) holds for every i=0,1,⋯ ,m−1i=0,1,\cdots,m-1,
  • rm∈Fr_m\in F,

then NN is said to accept ww. Otherwise, NN is said to reject ww.

Because epsilon transitions are allowed, when representing ww as y1y2⋯ym∈Σε∗y_1y_2\cdots y_m\in\Sigma^*_\varepsilon, any number of epsilon symbols may be inserted. For example, the string abc\mathtt{abc} can be represented as aεbcεε∈Σε∗\mathtt{a}\varepsilon\mathtt{bc}\varepsilon\varepsilon\in\Sigma^*_\varepsilon. Unlike a DFA, where each input corresponds to only one result, each input to an NFA may correspond to multiple results, forming a set of results.

Equivalence of DFAs and NFAs

We call two automata equivalent if and only if they recognize the same language. DFAs and NFAs are equivalent, meaning that every NFA is equivalent to some DFA; therefore, the class of languages recognized by NFAs is also the class of all regular languages. Every DFA can be directly viewed as an NFA; conversely, an NFA can be converted into a DFA using powerset construction.

Powerset construction

Suppose the NFA is N=(Q,Σ,δ,q0,F)N = (Q, \Sigma, \delta, q_0, F). Define E(q)E(q) as the set of states reachable from state qq by following only ε\varepsilon transitions.

Construct the DFA M=(Q′,Σ,δ′,E(q0),F′)M = (Q', \Sigma, \delta', E(q_0), F'), where:

  • Finite state set Q′=P(Q)Q' = \mathcal{P}(Q),
  • Transition function δ′:Q′×Σ→Q′\delta' : Q' \times \Sigma \to Q' satisfies δ′(S,c)=⋃q∈S, q′∈δ(q,c)E(q′)\delta'(S, c) = \bigcup_{q \in S,~q' \in \delta(q, c)} E(q'),
  • Accepting state set F′={S⊆Q∣S∩F≠∅}F' = \{ S \subseteq Q \mid S \cap F \neq \varnothing \}.

Clearly, at every step of the computation, the state occupied by MM corresponds to the set of states that NN may occupy.

Although NFAs and DFAs have the same language-recognition power, NFAs are still useful. This is because, for some regular languages, an NFA requires far fewer states than a DFA. For example, one can construct an NFA with nn states whose corresponding minimal DFA has Θ(2n)\Theta(2^n) states. In this case, directly computing the NFA has better time complexity.

Time Complexity of Computing DFAs and NFAs

Suppose the given string has length nn, the automaton has ss states, and the alphabet size is constant. Then, clearly, the time complexity of DFA computation is O(n)O(n); we only need to simulate the process above.

The time complexity of naive NFA computation is O(ns2)O(ns^2), because we need to consider every possible successor and the complexity required to merge states. Of course, bitsets or the Method of Four Russians can be used to optimize the computation to O(ns2w)O\left(\dfrac{ns^2}{w}\right) or O(ns2w⋅log⁡n)O\left(\dfrac{ns^2}{w\cdot \log n}\right).

Regular Expressions and Regular Languages

This section discusses the definitions and properties of regular expressions and regular languages, and examines the relationship between regular expressions and FSMs.

Regular Expressions

A regular expression is another commonly used way to describe regular languages. Although we can see this name in many modern languages (such as Python), those languages actually implement a superset of regular expressions.

Regular expressions

Given a character set Σ\Sigma, a regular expression is a string of symbols inductively defined by the following rules:

  1. Any character c∈Σc \in \Sigma is a regular expression;
  2. The empty-string symbol ε\varepsilon is a regular expression;
  3. The empty-language symbol ∅\varnothing is a regular expression;
  4. If R1R_1 and R2R_2 are regular expressions, then (R1+R2)(R_1 + R_2), (R1R2)(R_1 R_2) (also written as (R1⋅R2)(R_1 \cdot R_2)), and (R1∗)(R_1^\ast) are regular expressions.

The goal of a regular expression is to describe a language using these symbols. Every regular expression has a corresponding formal language.

Language represented by a regular expression

Let the formal language corresponding to each regular expression RR be L(R)L(R). Then:

  1. If R=cR = c, where c∈Σc \in \Sigma, then L(R)={c}L(R) = \{c\};
  2. If R=εR = \varepsilon, then L(R)={ε}L(R) = \{\varepsilon\};
  3. If R=∅R = \varnothing, then L(R)=∅L(R) = \varnothing;
  4. If R=(R1+R2)R = (R_1 + R_2), then L(R)=L(R1)∪L(R2)L(R) = L(R_1) \cup L(R_2);
  5. If R=(R1R2)=(R1⋅R2)R = (R_1 R_2) = (R_1\cdot R_2), then L(R)={uv∣u∈L(R1), v∈L(R2)}L(R) = \{ uv \mid u \in L(R_1),~ v \in L(R_2) \}, where uvuv denotes concatenating the two strings in order;
  6. If R=(R1∗)R = (R_1^\ast), then L(R)={u1u2⋯un∣ui∈L(R1), n∈N+}∪{ε}L(R) = \{u_1 u_2 \cdots u_n \mid u_i \in L(R_1),\ n \in \mathbf{N}_+\}\cup\{\varepsilon\}, also called the Kleene star (or Kleene closure), abbreviated as the closure.

Of course, once the precedence of the operations has been specified, these parentheses can be omitted when doing so causes no ambiguity.

Example

Let L(R1)={0, 01}L(R_1) = \{0,\ 01\} and L(R2)={ε, 1, 11, 111, … }L(R_2) = \{\varepsilon,\ 1,\ 11,\ 111,\ \dots\}. Then:

  • L(R1R2)={0, 01, 011, 0111, … }L(R_1R_2) = \{0,\ 01,\ 011,\ 0111,\ \dots\},
  • R2∗=R2R_2^\ast = R_2,
  • L(R1+R2)={0, 01, ε, 1, 11, 111, … }L(R_1 + R_2) = \{0,\ 01,\ \varepsilon,\ 1,\ 11,\ 111,\ \dots\}.

Every regular expression can be converted into an NFA through Thompson's construction, and every DFA can be converted into a regular expression through the State Elimination Method2. Therefore, regular expressions and FSMs are equivalent.

Regular Languages

In this subsection, rather than considering particular regular expressions, we consider regular expressions parameterized by variables (where the variables may be arbitrary regular languages). Applying the algebraic laws of regular expressions helps simplify regular expressions.

Algebraic properties of regular languages
  1. Commutativity of union: L+M=M+LL + M = M + L
  2. Associativity of union: (L+M)+N=L+(M+N)(L + M) + N = L + (M + N)
  3. Associativity of concatenation: (LM)N=L(MN)(LM)N = L(MN)
  4. ∅\varnothing is the identity element for union: ∅+L=L+∅=L\varnothing + L = L + \varnothing = L
  5. ε\varepsilon is the identity element for concatenation: εL=Lε=L\varepsilon L = L \varepsilon = L
  6. ∅\varnothing is an absorbing element for concatenation: ∅L=L∅=∅\varnothing L = L \varnothing = \varnothing
  7. Distributivity: L(M+N)=LM+LNL(M + N) = LM + LN,(M+N)L=ML+NL(M + N)L = ML + NL
  8. Idempotence of union: L+L=LL + L = L
  9. Laws related to closure: (L∗)∗=L∗(L^\ast)^\ast = L^\ast,∅∗=ε\varnothing^\ast = \varepsilon,ε∗=ε\varepsilon^\ast = \varepsilon

The closure properties of regular languages are also important. These properties allow us to start from some simple automata and, through certain operations, construct finite-state machines (FSMs) that recognize other languages. In short, closure properties can serve as tools for constructing complex FSMs.

For the closure properties of regular languages, we have:

Closure properties of regular languages

Let L,ML,M be two regular languages over the character set Σ\Sigma, and let the mapping h:Σ→Σ∗h:\Sigma\to\Sigma^* be given. Define the homomorphism of a string s=s1s2⋯sns=s_1s_2\cdots s_n as h(s)=h(s1)h(s2)⋯h(sn)h(s)=h(s_1)h(s_2)\cdots h(s_n). Then:

  1. The union L+ML + M of two regular languages is regular;
  2. The concatenation LMLM of two regular languages is regular;
  3. The closure L∗L^* of a regular language is regular;
  4. The complement Σ∗∖L\Sigma^*\setminus L of a regular language is regular;
  5. The intersection L∩ML\cap M of two regular languages is regular;
  6. The difference L∖ML\setminus M of two regular languages is regular;
  7. The reversal LR={sn⋯s2s1∣s=s1s2⋯sn∈L}L^R=\{s_n\cdots s_2s_1 \mid s=s_1s_2\cdots s_n\in L\} of a regular language is regular;
  8. The homomorphic image h(L)={h(s)∣s∈L}h(L)=\{h(s)\mid s\in L\} of a regular language is regular;
  9. The inverse homomorphic image h−1(L)={s∈Σ∗∣h(s)∈L}h^{-1}(L) = \{ s \in \Sigma^\ast \mid h(s) \in L \} of a regular language is regular.

A simple consequence is that every finite language is regular. In fact, a Trie is an automaton that recognizes it.

Myhill–Nerode Theorem

The Myhill–Nerode theorem provides a criterion for determining whether a language is regular. It describes the structural properties of regular languages through the concept of equivalence classes.

Nerode equivalence relation

For a language LL and any strings x,y∈Σ∗x,y\in \Sigma^\ast, if for every z∈Σ∗z\in\Sigma^*, we have xz∈L  ⟺  yz∈Lxz\in L\iff yz\in L, then the strings xx and yy are said to be equivalent with respect to LL, denoted by x≡Lyx\equiv_L y.

In other words, if, for two strings xx and yy, after appending the same arbitrary string zz (including the empty string) to xx and yy, they are always either both in LL or both not in LL, then we say that xx and yy are equivalent with respect to LL.

According to the definition above, we partition the set of all finite strings into one or more equivalence classes. If and only if the number of these equivalence classes is finite, we can use them to construct a DFA that recognizes the language. The number of states of this DFA equals the number of equivalence classes. Moreover, this number of states is the smallest among all DFAs that can recognize the language. This is the Myhill–Nerode theorem.

Myhill–Nerode theorem

A language LL is regular if and only if the number of equivalence classes into which Σ∗\Sigma^\ast is partitioned by the equivalence relation ≡L\equiv_L is finite.

For any DFA that recognizes the language LL, any two strings xx and yy that drive it to the same state must belong to the same equivalence class.

Consequently, the number of equivalence classes is the number of states of the minimal DFA that recognizes LL. Every equivalence class corresponds to exactly one state in the minimal DFA. This minimal DFA is unique up to isomorphism.

This theorem provides a way to construct a DFA using an equivalence relation:

  • The state set consists of all equivalence classes obtained from the partition induced by the equivalence relation. Choose any representative string for each equivalence class (for example, a shortest string).
  • To construct the transition function, append the transition character to the chosen representative string and find the state corresponding to the equivalence class containing the resulting string; this is the successor state of the transition. Since all strings in the same equivalence class are equivalent, the choice of representative string does not affect the transition result.
  • The initial state is the equivalence class corresponding to the empty string ε\varepsilon.
  • The accepting state set is the set of equivalence classes whose representative strings belong to the given language.

As a classic example, a suffix automaton is a minimal DFA constructed using the Myhill–Nerode theorem.

The Myhill–Nerode theorem is usually applied to construct a DFA for an infinite regular language. In many cases, the problem conditions are simple enough that we need only examine the set of strings whose lengths are not too large, and can then construct an automaton recognizing the entire language.

Example Problem

This section introduces the practical application of the Myhill–Nerode theorem through an example problem.

P12294 [THUPC 2025 Finals] A 01 string, n ternary operators, final value 1 (Enhanced Version)

The ternary operation table s0s1⋯s7s_0s_1\cdots s_7 for a,b,ca,b,c (where ss consists only of 0,10,1) means that if the bit at position a+2b+4ca+2b+4c of ss is 11, it returns 11; otherwise, it returns 00.

Given the operation table ss and qq 0101 strings of length 2n+12n+1, for each 0101 string you need to answer:

Whether it is possible to perform nn operations, each replacing three consecutive digits with the corresponding operation value, so that the result of the operation is 11; provide a construction or determine that it is impossible.

1≤2n+1≤105, ∑(2n+1)≤3×1051\le 2n+1\le 10^5,~\sum(2n+1)\le 3\times 10^5.

Solution

The set of 0101 strings that can be reduced to 11 is a regular language (that is, there exists a DFA that can determine whether a 0101 string can be reduced to 11)3. Therefore, consider using the Myhill–Nerode theorem. Since the conditions are simple, experiments show that we need only partition 0101 strings of length ≤9\le 9 into equivalence classes; when testing whether two strings are equivalent, it is enough to enumerate suffixes of length ≤6\le 6. If, after appending any suffix of length ≤6\le 6 to two strings, they can either both be reduced to the string we want or neither can, then the two strings are equivalent.

Each transition is equivalent to appending a new 0101 character to the current string and then replacing the new string by the shortest string in its equivalence class. Construct an automaton according to these transitions. This automaton can determine in O(n)O(n) time whether a string of length nn has an operation sequence whose result is 11. At the same time, the automaton has very few states.

For convenience, we construct 66 automata. These 66 automata represent whether it is possible, through some operation sequence, to obtain 0,1,00,01,10,110,1,00,01,10,11, respectively. Over all possible operation tables, the maximum number of states of an automaton is 4747.

Using the automata and appropriate preprocessing, binary lifting or a cat tree can be used to implement static range queries asking whether an operation sequence exists whose result is 11 on an interval. A query takes O(log⁡n)O(\log n) with the former and O(1)O(1) with the latter.

Consider using divide and conquer to solve the construction problem. Let f(l,r,t)f(l,r,t) denote a construction that combines interval [l,r][l,r] into t∈{0,1,00,01,10,11}t\in\{{0,1,00,01,10,11}\}. We use heuristic splitting: maintain two pointers i,ji,j, one scanning from left to right and the other from right to left, and enumerate the split point mid\textit{mid} as either ii or jj. For t∈{0,1}t\in\{{0,1}\}, enumerate how tt is split into left and right parts, with one part of tt having length 22 and the other part of tt having length 11. (For example, for the median operation table s=00010111s=00010111, 11 can be split into 0101 and 11.) For t∈{00,01,10,11}t\in\{{00,01,10,11}\}, typetype is directly split into the left and right parts.

If the two parts obtained this way are t1t_1 and t2t_2, further check whether [l,mid][l,mid] can generate t1t_1 and [mid+1,r][\textit{mid}+1,r] can generate t2t_2; if so, recurse immediately. With O(1)O(1) cat-tree queries, the complexity of this heuristic-splitting construction is O(nlog⁡n)O(n\log n); otherwise, using binary lifting for the checks, the construction complexity is O(nlog⁡2n)O(n\log^2n).

If a cat tree is used, the total complexity is O(n∣Q∣log⁡n+nlog⁡n)O(n|Q|\log n+n\log n), where ∣Q∣≤47|Q|\le 47. For convenience, the reference code uses binary lifting and reduces constants through low-level blocking, giving a total complexity of O(n∣Q∣log⁡n+nlog⁡2n)O(n|Q|\log n+n\log^2 n).

Reference code
#include <array>
#include <iostream>
#include <unordered_map>
#include <vector>

constexpr int L = 3;
std::array<int, 1 << L> op;
constexpr int X = (1 << L) - 2;

// Build a DFA that accepts binary strings which evaluate to a given string.
//   0b000 → "00"
//   0b001 → "01"
//   0b010 → "10"
//   0b011 → "11"
//   0b100 → "0"
//   0b101 → "1"
DFA build(int x) {
  constexpr int L1 = 9, L2 = 6;
  // The result for string `st` of length `len` is stored at (1 << len) | st.
  const auto idx = [](int len, int st) -> int { return (1 << len) | st; };

  // Compute which bit strings are accepted using dynamic programming.
  std::vector<bool> acc(1 << (L1 + L2 + 1));
  acc[(x >> 2) ? idx(1, x & 1) : idx(2, x)] = true;

  for (int len = (x >> 2) ? 3 : 4; len <= L1 + L2; len += 2) {
    for (int st = 0; st < (1 << len); ++st) {
      for (int i = 0; i + L <= len; ++i) {
        // Replace st[i..i+2] using the op[] rule, and check if the result is
        // accepted. Result = st[0..i-1] + op[st[i..i+2]] + st[i+3..end]
        int pr =
            ((((st >> (i + L)) << 1) | op[(st >> i) & ((1 << L) - 1)]) << i) |
            (st & ((1 << i) - 1));
        if (acc[idx(len - 2, pr)]) {
          acc[idx(len, st)] = true;
          break;
        }
      }
    }
  }

  // Construct a DFA using the Myhill-Nerode theorem.
  DFA dfa(2);
  std::unordered_map<std::vector<bool>, int>
      mp;                // Maps characteristic vectors to state IDs.
  std::vector<int> sts;  // Representative 01 strings for each state.
  std::vector<int> ids(1 << (L1 + 1));  // Maps 01 strings to DFA state IDs.

  for (int len = 0; len <= L1; ++len) {
    for (int st = (1 << len); st < (2 << len); ++st) {
      // Construct characteristic vector for this prefix.
      std::vector<bool> key(1 << (L2 + 1));
      for (int nxt = 0; nxt <= L2; ++nxt)
        std::copy(acc.begin() + (st << nxt), acc.begin() + ((st + 1) << nxt),
                  key.begin() + (1 << nxt));
      if (!mp.count(key)) {
        mp[key] = dfa.n++;
        sts.push_back(st);
        dfa.acc.push_back(key[idx(0, 0)]);
      }
      ids[st] = mp[key];
    }
  }

  // Build transitions.
  for (int c = 0; c < 2; ++c) {
    dfa.trans[c].resize(dfa.n);
    for (int i = 0; i < dfa.n; ++i) dfa.trans[c][i] = ids[(sts[i] << 1) | c];
  }

  return dfa;
}

std::vector<DFA> dfa;

// Initialize the DFA's.
void init() {
  std::string s;
  std::cin >> s;
  for (int i = 0; i < (1 << L); ++i) op[i] = s[i] - '0';
  std::swap(op[1], op[4]);
  std::swap(op[3], op[6]);
  for (int x = 0; x < X; ++x) dfa.push_back(build(x));
}

std::string s;
constexpr int B = 5;  // Block size is 2^B.
std::vector<std::vector<std::array<std::vector<int>, X>>> pre;

void precompute() {
  int n = s.size();
  for (int i = 0; i < n; ++i) s[i] -= '0';
  int nn = ((n - 1) >> B) + 1;
  pre.clear();

  // Compute transitions over single blocks (2^B characters) for each DFA.
  pre.emplace_back(nn);
  for (int i = 0; i < nn; ++i) {
    for (int x = 0; x < X; ++x) {
      pre[0][i][x].resize(dfa[x].n);
      for (int j = 0; j < dfa[x].n; ++j) {
        int cr = j;
        for (int ii = (i << B); ii < ((i + 1) << B) && ii < n; ++ii)
          cr = dfa[x].trans[s[ii]][cr];
        pre[0][i][x][j] = cr;
      }
    }
  }

  // Build higher-level transitions using binary lifting.
  for (int d = 1; (1 << d) <= nn; ++d) {
    pre.emplace_back(nn - (1 << d) + 1);
    for (int i = 0; i <= nn - (1 << d); ++i) {
      for (int x = 0; x < X; ++x) {
        pre[d][i][x].resize(dfa[x].n);
        for (int j = 0; j < dfa[x].n; ++j)
          pre[d][i][x][j] =
              pre[d - 1][i + (1 << (d - 1))][x][pre[d - 1][i][x][j]];
      }
    }
  }
}

// Check if the x-th DFA accepts s[l...r].
bool check(int l, int r, int x) {
  if (l > r) return false;
  int cr = dfa[x].q0;
  if ((l >> B) == (r >> B)) {
    for (; l <= r; ++l) cr = dfa[x].trans[s[l]][cr];
  } else {
    for (; (l & ((1 << B) - 1)) && (l <= r); ++l) cr = dfa[x].trans[s[l]][cr];
    int d = (r >> B) - (l >> B);
    for (int z = 0; z < (int)pre.size(); ++z)
      if ((d >> z) & 1) {
        cr = pre[z][l >> B][x][cr];
        l += 1 << (z + B);
      }
    for (; l <= r; ++l) cr = dfa[x].trans[s[l]][cr];
  }
  return dfa[x].acc[cr];
}

// Construct expression for s[l...r].
void calc(int l, int r, int x) {
  if (l == r) return (void)(std::cout << (x & 1));
  if (x >> 2) {
    for (int i = l, j = r; i <= j; i += 2, j -= 2) {
      for (int k = 0; k < (1 << L); ++k) {
        if (op[k] != (x & 1)) continue;
        if (check(l, i, (k >> 2) | 4) && check(i + 1, r, k & 3)) {
          std::cout << '(';
          calc(l, i, (k >> 2) | 4);
          calc(i + 1, r, k & 3);
          std::cout << ')';
          return;
        }
        if (check(l, j - 1, k >> 1) && check(j, r, (k & 1) | 4)) {
          std::cout << '(';
          calc(l, j - 1, k >> 1);
          calc(j, r, (k & 1) | 4);
          std::cout << ')';
          return;
        }
      }
    }
  } else {
    for (int i = l, j = r; i <= j; i += 2, j -= 2) {
      if (check(l, i, (x >> 1) | 4) && (check(i + 1, r, (x & 1) | 4))) {
        calc(l, i, (x >> 1) | 4);
        calc(i + 1, r, (x & 1) | 4);
        return;
      }
      if (check(l, j - 1, (x >> 1) | 4) && check(j, r, (x & 1) | 4)) {
        calc(l, j - 1, (x >> 1) | 4);
        calc(j, r, (x & 1) | 4);
        return;
      }
    }
  }
}

void solve() {
  std::cin >> s;
  int n = s.size();
  precompute();
  if (!check(0, n - 1, 0b101)) {
    std::cout << -1 << '\n';
    return;
  }
  calc(0, n - 1, 0b101);
  std::cout << '\n';
}

int main() {
  init();
  int q;
  std::cin >> q;
  for (; q; --q) {
    solve();
  }
  return 0;
}

Exercises

DFA Minimization

As mentioned earlier, two DFAs are equivalent if and only if they recognize the same regular language. According to the languages they recognize, all DFAs are partitioned into infinitely many equivalence classes. When running algorithms such as DP over DP, the ∣Q∣|Q| of the constructed DFA may be too large, making the transition complexity of the outer DP too high. Therefore, it is often necessary to find the minimal DFA in the equivalence class containing the given DFA, in order to reduce the transition complexity of the outer DP.

The Myhill–Nerode theorem from the previous section provides one construction method. However, for some more complex problems, directly constructing a DFA through the Myhill–Nerode theorem requires traversing a very large set of strings and takes a great deal of time. Therefore, we need a method that can start from an already constructed DFA (usually easier to obtain) and directly construct a minimal DFA. This is called the DFA minimization problem.

A commonly used algorithm for DFA minimization is the Hopcroft algorithm. Since the Myhill–Nerode theorem states that, for any DFA recognizing a language, any strings that drive it to the same state must be Nerode-equivalent, all Nerode-equivalent strings correspond to the same state in the minimal DFA. Thus, every state of the minimal DFA must be a set of states in the current DFA. We can start from the state set of the existing DFA and partition it into equivalence classes without examining specific strings. Hopcroft's algorithm starts from the coarsest partition {F,Q∖F}\{F,Q\setminus F\} and uses a series of witnesses AA to refine this partition until it can no longer be refined. This is the core idea of Hopcroft's algorithm.

A witness AA is a state set such that it and its complement Q∖AQ\setminus A necessarily correspond to different Nerode equivalence classes. In other words, there exists a string s∈Σ∗s\in\Sigma^* such that, after reading ss starting from the states in AA and from the states in Q∖AQ\setminus A, respectively, all states in AA are accepting while all states in Q∖AQ\setminus A are non-accepting, or vice versa. Therefore, if two states u,v∈Qu,v\in Q under some character cc have exactly one transition into the witness AA and the other transition outside AA, that is, exactly one of δ(u,c)∈A\delta(u,c)\in A and δ(v,c)∈A\delta(v,c)\in A holds, then u,vu,v likewise cannot belong to the same Nerode equivalence class: exactly one of the states δ(u,cs)\delta(u,cs) and δ(v,cs)\delta(v,cs) is accepting. Thus, the partition can be refined using whether δ(u,c)∈A\delta(u,c)\in A holds. Specifically, let

Sx={u∣u∈Px, δ(u,c)∈A}.S_x = \{u\mid u\in P_x,~\delta(u,c)\in A\}.

If both SxS_x and Px∖SxP_x\setminus S_x are nonempty, then the state set PxP_x in the current partition can be refined into SxS_x and Px∖SxP_x\setminus S_x.

Initially, put the accepting state set FF into the witness set WW as a witness, namely, W←{F}W\gets\{F\}, and maintain the current partition as P←{F, Q∖F}P\gets\{F,~Q\setminus F\}. The initial witness is clearly valid: states in FF and Q∖FQ\setminus F can never be equivalent. Each time, arbitrarily remove a set AA from the witness set WW to refine the current partition. Enumerate all characters c∈Σc\in\Sigma. For every state set PxP_x in the current partition PP, compute the SxS_x described above. If Sx≠∅S_x\neq\varnothing and ∣Sx∣≠∣Px∣|S_x|\neq|P_x|, then PxP_x can be further split into SxS_x and Px∖SxP_x\setminus S_x, and they directly replace PxP_x in PP.

Whenever a finer partition is obtained, new witnesses are obtained as well. In principle, both newly obtained SxS_x and Px∖SxP_x\setminus S_x could be put into the witness set WW for later verification. However, this is not necessary. It is easy to see that, for the three witnesses Px,Sx,Px∖SxP_x,S_x,P_x\setminus S_x, verifying any two is enough to guarantee correctness: the result has only three possibilities, δ(u,c)∈Sx\delta(u,c)\in S_x, δ(u,c)∈Px∖Sx\delta(u,c)\in P_x\setminus S_x, and δ(u,c)∉Px\delta(u,c)\notin P_x, and partitioning a set into three parts requires only two tests. Therefore, when splitting PxP_x into SxS_x and Px∖SxP_x\setminus S_x, if PxP_x is still in the witness set WW, this means that the witness PxP_x has not yet been tested, so PxP_x in WW must be replaced by the two witnesses SxS_x and Px∖SxP_x\setminus S_x; otherwise, the current partition is necessarily equivalent to the result of having already tested PxP_x4, so it is sufficient to put the smaller of SxS_x and Px∖SxP_x\setminus S_x into the witness set WW. As with heuristic splitting, this gives good complexity.

Writing the above process as pseudocode gives:

Algorithm Hopcroft’s Algorithm(Q,Σ,δ,q0,F):Input. DFA A=(Q,Σ,δ,q0,F).Output. A partition of Q into equivalence classes of the minimal DFA.Method. 1P←{F,  Q∖F}2W←{F}3while W≠∅4choose and remove any A∈W5for each c∈Σ6S←{q∈Q∣δ(q,c)∈A}7for each Y∈P such that S∩Y≠∅ and Y∖S≠∅8Y1←S∩Y, Y2←Y∖S9P←(P∖{Y})∪{Y1,Y2}10if Y∈W11W←(W∖{Y})∪{Y1,Y2}12else13add the smaller of Y1 and Y2 to W14return P\begin{array}{l} \textbf{Algorithm } \text{Hopcroft's Algorithm}(Q, \Sigma, \delta, q_0, F): \\ \textbf{Input. } \text{DFA } A=(Q, \Sigma, \delta, q_0, F). \\ \textbf{Output. } \text{A partition of } Q \text{ into equivalence classes of the minimal DFA.} \\ \textbf{Method. } \\ \begin{array}{ll} 1 & P \gets \{F,\; Q \setminus F\} \\ 2 & W \gets \{F\} \\ 3 & \textbf{while } W \ne \varnothing \\ 4 & \quad \text{choose and remove any } A \in W \\ 5 & \quad \textbf{for each } c \in \Sigma \\ 6 & \quad \quad S \gets \{ q \in Q \mid \delta(q,c) \in A \} \\ 7 & \quad \quad \textbf{for each } Y \in P \text{ such that } S \cap Y \ne \varnothing \text{ and } Y \setminus S \ne \varnothing \\ 8 & \quad \quad \quad Y_1 \gets S \cap Y,~Y_2 \gets Y \setminus S \\ 9 & \quad \quad \quad P \gets (P \setminus \{Y\}) \cup \{Y_1, Y_2\} \\ 10 & \quad \quad \quad \textbf{if } Y \in W \\ 11 & \quad \quad \quad \quad W \gets (W \setminus \{Y\}) \cup \{Y_1, Y_2\} \\ 12 & \quad \quad \quad \textbf{else} \\ 13 & \quad \quad \quad \quad \text{add the smaller of } Y_1 \text{ and } Y_2 \text{ to } W \\ 14 & \textbf{return } P \end{array} \end{array}

When implementing the algorithm, the bottleneck is computing SS. It is infeasible to iterate over all q∈Qq\in Q and then determine whether δ(q,c)∈A\delta(q,c)\in A. Therefore, before running the algorithm, preprocess the reverse transition edges {q∈Q∣δ(q,c)=a}\{q\in Q\mid \delta(q,c)=a\}. Then, using these reverse transitions, iterating over a∈Aa\in A yields the set SS. This guarantees that each transition δ(q,c)=a\delta(q,c)=a is traversed only when aa belongs to some witness; and the witness-selection method described above guarantees that, in the sequence of witnesses containing aa actually used by the algorithm, A1⊃A2⊃⋯⊃AkA_1\supset A_2\supset\cdots\supset A_k, each preceding set is at least twice the size of the following one, so k∈O(log⁡n)k\in O(\log n). In other words, each transition edge is traversed at most O(log⁡n)O(\log n) times, and the total number of transitions is n∣Σ∣n|\Sigma|, so the total complexity is O(n∣Σ∣log⁡n)O(n|\Sigma|\log n).

The reference implementation is as follows:5

Reference implementation
// DFA minimization via Hopcroft's algorithm.
// Complexity: O(n * m * log(n)).
DFA DFA::hopcroft_minimize() const {
  // Construct inverse transition maps:
  // - pre[c] stores states sorted by the target of transition c.
  // - pos[c][s] is the start index in pre[c] of transitions going to state s.
  std::vector<std::vector<int>> pre(m), pos(m);
  for (int c = 0; c < m; ++c) {
    pre[c].assign(n, 0);
    pos[c].assign(n + 1, 0);
    // Counting sort.
    for (int i = 0; i < n; ++i) ++pos[c][trans[c][i]];
    for (int i = 0; i < n; ++i) pos[c][i + 1] += pos[c][i];
    for (int i = 0; i < n; ++i) pre[c][--pos[c][trans[c][i]]] = i;
  }

  // Partition element structure:
  // - os: starting index in the state list.
  // - sz: number of states in this class.
  // - cnt: temporary count of marked states during refinement.
  struct EquivClasses {
    int os, sz, cnt;

    EquivClasses(int os, int sz, int cnt) : os(os), sz(sz), cnt(cnt) {}
  };

  // Partition and helper data structures.
  std::vector<EquivClasses> ec;  // Current list of equivalence classes.
  std::vector<int> ids(n);       // Permutation of states, grouped by ECs.
  std::vector<int> par(n);       // Maps state to its EC index.
  std::vector<bool> tag(n);      // Temporary marking for splitting.
  std::queue<int> evidences;     // Worklist of ECs to check.

  // Initial partition by acceptance label.
  std::iota(ids.begin(), ids.end(), 0);
  std::sort(ids.begin(), ids.end(),
            [&](int l, int r) { return acc[l] < acc[r]; });
  for (int l = 0, r; l < n; l = r) {
    for (r = l; r < n && acc[ids[r]] == acc[ids[l]]; ++r)
      par[ids[r]] = ec.size();
    if (l) evidences.push(ec.size());  // Add all but first class to worklist.
    ec.emplace_back(l, r - l, 0);
  }

  // Refinement loop.
  while (!evidences.empty()) {
    int cr = evidences.front();
    evidences.pop();
    for (int c = 0; c < m; ++c) {
      std::vector<int> todo;
      for (int i = ec[cr].os; i < ec[cr].os + ec[cr].sz; ++i) {
        for (int k = pos[c][ids[i]]; k < pos[c][ids[i] + 1]; ++k) {
          int j = pre[c][k];
          if (!tag[j]) {
            if (!ec[par[j]].cnt) todo.push_back(par[j]);
            ++ec[par[j]].cnt;
            tag[j] = true;
          }
        }
      }
      // Perform splits.
      for (int i : todo) {
        int ti = i;
        if (ec[i].cnt != ec[i].sz) {
          // Split into two: larger vs smaller segment.
          bool majority_tagged = ec[i].cnt * 2 >= ec[i].sz;
          int mid =
              std::partition(ids.begin() + ec[i].os,
                             ids.begin() + ec[i].os + ec[i].sz,
                             [&](int x) { return tag[x] == majority_tagged; }) -
              ids.begin() - ec[i].os;

          // Assign new EC index to the smaller segment.
          for (int j = ec[i].os + mid; j < ec[i].os + ec[i].sz; ++j)
            par[ids[j]] = ec.size();

          evidences.push(ec.size());
          if (!majority_tagged) ti = ec.size();
          ec.emplace_back(ec[i].os + mid, ec[i].sz - mid, 0);
          ec[i].sz = mid;
        }
        // Clear temporary counters and tags.
        ec[i].cnt = 0;
        for (int j = ec[ti].os; j < ec[ti].os + ec[ti].sz; ++j)
          tag[ids[j]] = false;
      }
    }
  }

  // Build minimized DFA.
  DFA res(m, ec.size(), par[q0]);
  for (const auto& e : ec) {
    int i = ids[e.os];  // Representative state.
    res.acc[par[i]] = acc[i];
    for (int c = 0; c < m; ++c) res.trans[c][par[i]] = par[trans[c][i]];
  }
  return res;
}

This reference implementation allows automaton states to carry labels with arbitrary integer values, rather than merely the binary labels “accept” and “reject”. As the reference implementation shows, the only differences from the basic Hopcroft algorithm are the construction of the initial partition and the evidence set. This extended automaton is also called a Moore machine. One application of it appears in the second example in this section.

Examples

This section introduces the practical application of DFA minimization techniques through two examples.

Example

Given a length-nn 01?01? string aa, with initial variable x=0x = 0, traverse each position aia_i in order and perform the following operations:

  1. If ai=0a_i = 0, set x←x−lowbit(x)x \gets x - \text{lowbit}(x);
  2. If ai=1a_i = 1, set x←x+lowbit(2k−1−x)x \gets x + \text{lowbit}(2^k - 1 - x);
  3. If ai=?a_i = ?, choose either 00 or 11, corresponding to one of the two operations above.

If finally x∈[0,r]x \in [0, r], the operation sequence is called good.

For each j=1…nj = 1 \ldots n, find the number of complete “good” sequences under the condition that aj=0a_j = 0 is forced. In particular, when aj=1a_j = 1, the answer is 00.

1≤n≤105, 1≤k≤20, 0≤r<2k1\le n\le 10^5,~1\le k\le 20,~0\le r<2^k. Output the result modulo 998244353998244353.

Solution

Consider the naïve DP. Let fi,jf_{i,j} denote the number of ways to start from x=0x=0, perform the operations over [1,i][1,i], and currently have value jj. Let gi,jg_{i,j} denote the number of ways to start from x=jx=j, perform the operations over [i,n][i,n], and end with x∈[0,r]x \in [0, r]. The answer when ai=0a_i=0 is ∑jfi−1,jgi+1,j−lowbit(j)\sum_j f_{i-1,j}g_{i+1,j - \text{lowbit}(j)}. The complexity is O(n2k)O(n2^k).

We can directly construct a DFA from the transitions of jj, run DFA minimization, and then perform DP.

Reference code
#include <iostream>
#include <string>
#include <vector>

int main() {
  constexpr int M = 998244353;
  // Initialize the DFA.
  const auto lowbit = [](int x) { return x & -x; };
  int n, k, r;
  std::cin >> n >> k >> r;
  DFA raw_dfa(2, 1 << k);
  for (int x = 0; x < (1 << k); ++x) {
    raw_dfa.trans[0][x] = x - lowbit(x);
    raw_dfa.trans[1][x] = x + lowbit((1 << k) - 1 - x);
    if (x <= r) raw_dfa.acc[x] = 1;
  }
  // DFA minimization.
  auto dfa = raw_dfa.hopcroft_minimize();
  // DP.
  std::string s;
  std::cin >> s;
  for (int i = 0; i < n; ++i) s[i] -= '0';
  std::vector<std::vector<int>> f(n + 1, std::vector<int>(dfa.n, 0));
  f[0][dfa.q0] = 1;
  for (int i = 0; i < n; ++i) {
    for (int c = 0; c < 2; ++c) {
      if (s[i] != 1 - c) {
        for (int j = 0; j < dfa.n; ++j) {
          (f[i + 1][dfa.trans[c][j]] += f[i][j]) %= M;
        }
      }
    }
  }
  std::vector<std::vector<int>> g(n + 1, std::vector<int>(dfa.n, 0));
  for (int j = 0; j < dfa.n; ++j) {
    if (dfa.acc[j]) g[n][j] = 1;
  }
  for (int i = n - 1; i >= 0; --i) {
    for (int c = 0; c < 2; ++c) {
      if (s[i] != 1 - c) {
        for (int j = 0; j < dfa.n; ++j) {
          (g[i][j] += g[i + 1][dfa.trans[c][j]]) %= M;
        }
      }
    }
  }
  for (int i = 0; i < n; ++i) {
    long long res = 0;
    if (s[i] != 1) {
      for (int j = 0; j < dfa.n; ++j) {
        (res += (long long)f[i][j] * g[i + 1][dfa.trans[0][j]]) %= M;
      }
    }
    std::cout << res << '\n';
  }
  return 0;
}
Minimal Subset Difference

Define f(n)f(n) as the minimum possible absolute value of the result obtained by inserting plus or minus signs between all the digits of the decimal number nn.

There are TT queries. Each query gives l,r,kl, r, k; find the number of values satisfying l≤m≤rl \le m \le r and f(m)≤kf(m) \le k, with the values denoted by mm.

1≤T≤5×1041 \le T \le 5\times 10^4, 1≤l≤r≤10181 \le l \le r \le 10^{18}, 0≤k≤90 \le k \le 9.

Solution

First, here is a greedy method for computing f(n)f(n). Consider a number from its most significant digit to its least significant digit; initially, let the sum of the constructed number be 00. At each digit, if the number currently formed is negative, add the current digit; if it is positive, subtract the current digit. With this procedure, the absolute value of the greedily computed f(n)f(n) is always at most 99. Therefore, the absolute value of the true f(n)f(n) is also at most 99.

Let us think further about the largest value that can be formed during the process of constructing the final answer. Since the answer is certainly at most 99, there are only 1818 digits, and at most 99 can be added or subtracted at each step, any value formed during the process must be at most 9090; otherwise it could not be reduced back down at the end. In fact, this upper bound can be lowered further6.

Consider naïve DP over DP. First, consider how the inner DP determines the answer for a number: define gi,cg_{i,c} to indicate, using only the first ii digits of the number, whether cc can be formed. According to the preceding discussion, we only need to retain values of cc at most 9090. If the current digit is assigned vv, the transitions are:

gi+1,c+v←gi,c, gi+1,∣c−v∣←gi,c.g_{i+1,c+v}\gets g_{i,c},~ g_{i+1,|c-v|}\gets g_{i,c}.

The outer DP is a digit DP. Take differences of the queries. Let the state be flen,lim,staf_{\textit{len},\textit{lim},\textit{sta}}; its subscripts respectively indicate that the first len\textit{len} positions have been considered, whether an upper-bound constraint is active, and that the current automaton state is sta\textit{sta}, and so on.

Unlike an ordinary DFA, we need to record the corresponding answer for every automaton state. A brute-force search shows that the inner DP has only 1956419564 states. We can then directly run DFA minimization, reducing the number of states to 715715.

We now precompute all digit-DP answers with lim=0\textit{lim}=0; for multiple test cases, we only need to handle the lim=1\textit{lim}=1 case, so the answers can be obtained quickly.

The time complexity is O(∣S∣∣Σ∣log⁡∣S∣+(∣Q∣∣Σ∣+T)∣Σ∣log⁡10V)O(|S||\Sigma|\log |S|+(|Q||\Sigma|+T)|\Sigma|\log_{10} V) (∣S∣=19564|S|=19564, ∣Q∣=715|Q|=715).

Reference code
#include <bitset>
#include <cmath>
#include <iostream>
#include <unordered_map>
#include <vector>

constexpr int B = 10, L = 91;

using state = std::bitset<L>;
DFA raw_dfa(B), dfa(B);
std::unordered_map<state, int> ids;

// Construct a DFA by DFS.
int dfs(const state& cr) {
  if (ids.count(cr)) return ids[cr];
  int id = ids[cr] = raw_dfa.n++;
  // Check if accepted.
  for (int i = 0; i < B; ++i) {
    if (cr[i]) {
      raw_dfa.acc.push_back(i);
      break;
    }
  }
  // Construct transitions recursively.
  for (int c = 0; c < B; ++c) {
    raw_dfa.trans[c].push_back(0);
  }
  for (int c = 0; c < B; ++c) {
    state nt;
    for (int i = 0; i < L; ++i) {
      if (cr[i]) {
        if (i + c < L) nt[i + c] = true;
        nt[std::abs(i - c)] = true;
      }
    }
    raw_dfa.trans[c][id] = dfs(nt);
  }
  return id;
};

// DP.
std::vector<long long> memo;
std::vector<int> nums;

long long sol(int x, int len, bool lim, int k) {
  if (!len) return dfa.acc[x] <= k;
  auto key = (x * 20 + len) * B + k;
  if (!lim && memo[key] != -1) return memo[key];
  long long res = 0;
  for (int c = 0; c <= (lim ? nums[len - 1] : B - 1); ++c)
    res += sol(dfa.trans[c][x], len - 1, lim && c == nums[len - 1], k);
  return lim ? res : memo[key] = res;
};

long long calc(long long n, int k) {
  nums.clear();
  for (; n; n /= B) nums.push_back(n % B);
  return sol(dfa.q0, nums.size(), true, k);
};

int main() {
  // Construct a DFA.
  for (int c = 0; c < B; ++c) raw_dfa.trans[c].reserve(20000);
  dfs(1);
  // DFA minimization.
  dfa = raw_dfa.hopcroft_minimize();
  memo.assign(dfa.n * 20 * B, -1);
  // Queries.
  int t;
  std::cin >> t;
  for (; t; --t) {
    long long l, r;
    int k;
    std::cin >> l >> r >> k;
    std::cout << (calc(r, k) - calc(l - 1, k)) << '\n';
  }
  return 0;
}

Exercises

Common Applications of Automata

This section lists some common applications of automata in algorithmic competitions7.

Trie

Trie is the first automaton encountered by most OIers. It accepts exactly the elements of the specified set of strings. Its transition function is given by the edges of the Trie, and an accepting state is the state reached when each string is inserted into the Trie.

KMP Automaton

KMP algorithm can be viewed as an automaton. The KMP automaton based on the string ss accepts exactly the strings having ss as a suffix, and its accepting state is ∣s∣|s|.

Transition function:

δ(i,c)={i+1s[i+1]=c0s[1]≠c∧i=0δ(π(i),c)s[i+1]≠c∧i>0\delta(i, c)= \begin{cases} i+1&s[i+1]=c\\ 0&s[1]\ne c\land i=0\\ \delta(\pi(i),c)&s[i+1]\ne c\land i>0 \end{cases}

AC Automaton

AC automaton accepts exactly the strings that have some element of the specified set of strings as a suffix. In other words, Trie + KMP.

Suffix Automaton

Suffix automaton accepts exactly the suffixes of the specified string.

Generalized Suffix Automaton

Generalized suffix automaton accepts exactly the suffixes of some element in the specified set of strings. In other words, Trie + SAM.

The relationship between the generalized SAM and the SAM is the same as that between the AC automaton and the KMP automaton.

Palindromic Automaton

Palindromic automaton is somewhat special: it cannot be conveniently defined as an automaton.

If a definition is needed, it accepts exactly the center and right half of every palindromic substring of a string.

For an odd-length palindrome, “center and right part” has its literal meaning; for an even-length palindrome, it is defined as a special character followed by the right part. This definition may look strange, but it makes PAM a genuine automaton rather than merely two trees.

Subsequence Automaton

Subsequence automaton accepts exactly the subsequences of the specified string.

DP over DP

DP over DP is an application of automata. It can be viewed as a technique that first constructs an automaton through an inner DP and then uses DP on that automaton in the outer layer to implement counting and optimization tasks.

Automata and matching are inseparably related, and a basic idea in matching is: “If this string does not work, try seeing whether its suffix does.” Therefore, many automata (KMP, AC automaton, SAM, PAM) have the concept of suffix links.

A state corresponds to several strings. Its suffix link points to the state corresponding to the longest common proper suffix of the strings represented by that state in the automaton. In general, suffix links form a tree, and suffix-link trees of different automata share some properties; these are worth noting while learning.

Further Reading

Footnotes

  1. In this definition, we allow transitions between states through the empty character (ε\varepsilon), so more precisely this is a nondeterministic finite automaton with ε\varepsilon-transitions (NFA-ε\varepsilon). Some textbooks simply call it an NFA; for brevity, we use that convention here. In theory, NFA and NFA-ε\varepsilon are distinguished, but in practice they have the same computational power. ↩

  2. See Section 3.2 of National Training Team 2021 paper by Xu Zhe'an: A Discussion of Finite State Automata and Their Applications. ↩

  3. See the official editorial. ↩

  4. Here, “equivalent to” means that although PxP_x may not actually have been tested, testing PxP_x on the current partition would not yield any improvement. Put simply, in the tree of evidence sets produced by splitting sets, one of its ancestors and all the side branches along the path have already been tested; therefore, by induction, it is equivalent to having tested it as well. ↩

  5. One detail in the algorithm implementation is that, for an evidence set AA, the set may have already split into BB and CC after only part of the characters have been tested. Assume without loss of generality that ∣B∣≥∣C∣|B|\ge |C|. In the reference implementation, the smaller set CC is inserted at the end of the evidence queue, while the larger evidence set BB replaces set AA at its original position. When the algorithm continues, it in fact only uses evidence set BB to test the remaining characters. This is correct because, for characters already tested, at least the two sets AA and CC have been verified; for characters not yet tested, at least the two sets BB and CC have been verified. ↩

  6. See Example 5.2 in National Training Team 2021 paper by Xu Zhe'an: A Discussion of Finite State Automata and Their Applications. ↩

  7. This article requires its definition of an automaton to be complete: every state must have a transition on every character. Descriptions of these string-related automata usually omit the mismatch state; Trie and SAM are examples. To conform to the definition given here, a mismatch state must be added explicitly to the descriptions of these automata. ↩

This content comes from OI Wiki: Finite State Machine. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.