class FiniteStateAutomaton:
"""A finite state automaton
Parameters
----------
alphabet
states
initial_state
final states
transition_graph
"""
def complement(self) -> 'FiniteStateAutomaton':
"""
construct the complement of this FSA
Returns
-------
FiniteStateAutomaton
A new FSA that accepts the complement language
Notes
-----
The FSA must be deterministic and complete for this operation
to work correctly. If not, it will be converted first.
"""
# First ensure we have a complete DFA
= self._deepcopy()
fsa if not fsa.is_deterministic():
= fsa.to_dfa()
fsa
if not fsa.is_complete():
= fsa.make_complete()
fsa
# Complement the set of final states
= fsa._states.difference(fsa._final_states)
new_final_states
return FiniteStateAutomaton(
fsa._alphabet,
fsa._states,
fsa._initial_state,
new_final_states,
fsa._transition_function.transition_graph )
The intersection operation via complement
In defining phonological patterns, it can be really useful to have operations that go beyond the regular operations: union, concatenation, and kleene star. In this section, we’ll see how to construct the complement of a language, and how to use the complement to construct the intersection of two languages. The reason these are going to be useful for us is that they allow us to express constraints against possible patterns. Remember that it was very hard to express constraints against entire strings using regular expressions. It turns out to be a lot easier using FSAs using the complement and intersection operations.
Complement
Given a deterministic finite automaton (DFA) \(G = \langle Q, \Sigma, \delta, q_0, F \rangle\) recognizing \(A = \mathbb{L}(G)\), we construct \(G' = \text{complement}(G) = \langle Q, \Sigma, \delta, q_0, Q \setminus F \rangle\) recognizing \(\overline{A} = \Sigma^* \setminus A = \mathbb{L}(G') = \mathbb{L}(\text{complement}(G))\):
- The states \(Q\), alphabet \(\Sigma\), transition function \(\delta\), and initial state \(q_0\) remain the same
- The set of final states becomes \(F' = Q \setminus F\) (the complement of \(F\) with respect to \(Q\))
Note that for the complement operation to work correctly, the automaton must be a complete DFA (every state has a transition for every symbol in the alphabet). If the NFA is not deterministic or not complete, it must first be converted to a complete DFA.
Intersection
Given NFAs \(G_1 = \langle Q_1, \Sigma_1, \delta_1, q_1, F_1 \rangle\) recognizing \(A = \mathbb{L}(G_1)\) and \(G_2 = \langle Q_2, \Sigma_2, \delta_2, q_2, F_2 \rangle\) recognizing \(B = \mathbb{L}(G_2)\), we can construct an NFA \(G = \text{intersection}(G_1, G_2)\) recognizing \(A \cap B = \mathbb{L}(G) = \mathbb{L}(\text{intersection}(G_1, G_2))\) using De Morgan’s laws:
\[A \cap B = \overline{\overline{A} \cup \overline{B}}\]
This means we can implement intersection using the operations we already have:
- Compute \(\overline{A}\) using the complement operation on \(M_1\)
- Compute \(\overline{B}\) using the complement operation on \(M_2\)
- Compute \(\overline{A} \cup \overline{B}\) using the union operation
- Compute \(\overline{\overline{A} \cup \overline{B}}\) using the complement operation again
class FiniteStateAutomaton:
"""A finite state automaton
Parameters
----------
alphabet
states
initial_state
final states
transition_graph
"""
def __and__(self, other):
return self.intersection(other)
def intersection(self, other: 'FiniteStateAutomaton') -> 'FiniteStateAutomaton':
"""
compute the intersection of this FSA with another
Parameters
----------
other : FiniteStateAutomaton
The FSA to intersect with
Returns
-------
FiniteStateAutomaton
A new FSA that accepts the intersection language
"""
# Using De Morgan's laws: A ∩ B = ¬(¬A ∪ ¬B)
= self.complement()
not_self = other.complement()
not_other = not_self.union(not_other)
union return union.complement()
Direct Product Construction
Alternatively, we can compute the intersection more efficiently using a direct product construction:
Given NFAs \(G_1 = \langle Q_1, \Sigma_1, \delta_1, q_1, F_1 \rangle\) and \(G_2 = \langle Q_2, \Sigma_2, \delta_2, q_2, F_2 \rangle\), we construct \(G = \text{direct\_intersection}(G_1, G_2) = \langle Q, \Sigma, \delta, q_0, F \rangle\) recognizing \(A \cap B\):
- Define \(Q = Q_1 \times Q_2\) (the Cartesian product of the state sets)
- Define \(\Sigma = \Sigma_1 \cap \Sigma_2\) (the intersection of the alphabets)
- Define the initial state \(q_0 = (q_1, q_2)\)
- Define the set of final states \(F = F_1 \times F_2\) (pairs where both components are final states)
- Define the transition function: \[\delta((p_1, p_2), \sigma) = \{(q_1, q_2) \mid q_1 \in \delta_1(p_1, \sigma) \text{ and } q_2 \in \delta_2(p_2, \sigma)\}\]
class FiniteStateAutomaton:
"""A finite state automaton
Parameters
----------
alphabet
states
initial_state
final states
transition_graph
"""
def direct_intersection(self, other: 'FiniteStateAutomaton') -> 'FiniteStateAutomaton':
"""
compute the intersection of this FSA with another using direct product construction
Parameters
----------
other : FiniteStateAutomaton
The FSA to intersect with
Returns
-------
FiniteStateAutomaton
A new FSA that accepts the intersection language
"""
# Relabel states to ensure uniqueness
= self._relabel_fsas(other)
fsa1, fsa2
# Common alphabet
= fsa1._alphabet.intersection(fsa2._alphabet)
new_alphabet
# Create the product state space
= set()
new_states = {}
new_transition_graph = set()
new_final_states
# Initial state is the pair of initial states
= (fsa1._initial_state, fsa2._initial_state)
new_initial_state
# Construct the product automaton
= [new_initial_state]
stack = set()
visited
while stack:
= stack.pop()
current_state if current_state in visited:
continue
visited.add(current_state)
new_states.add(current_state)
# Check if this is a final state in the product
if (current_state[0] in fsa1._final_states and
1] in fsa2._final_states):
current_state[
new_final_states.add(current_state)
# Compute transitions for each symbol
for symbol in new_alphabet:
= fsa1._transition_function.get_transitions(current_state[0], symbol)
next_states1 = fsa2._transition_function.get_transitions(current_state[1], symbol)
next_states2
# For each pair of next states
for q1 in next_states1:
for q2 in next_states2:
= (q1, q2)
next_state
# Add the transition
if current_state not in new_transition_graph:
= {}
new_transition_graph[current_state] if symbol not in new_transition_graph[current_state]:
= set()
new_transition_graph[current_state][symbol]
new_transition_graph[current_state][symbol].add(next_state)
# Add to stack if not visited
if next_state not in visited:
stack.append(next_state)
# Create the new transition function
= TransitionFunction(new_transition_graph)
new_transition_function
return FiniteStateAutomaton(
new_alphabet,
new_states,
new_initial_state,
new_final_states,
new_transition_function )
This direct construction is more efficient than using De Morgan’s laws and complementation, particularly for large automata, since it avoids multiple determinization steps which can cause exponential state growth.