Regular Languages are Decidable

Regular languages are decidable: given any two regular languages A and B, an algorithm can determine whether A and B contain the same strings.

The decision algorithm exploits the fact that set operations can be performed on regular languages, based on transformations of finite automata. Because a regular language in any form (regular expression, DFA, and NFA) can be freely converted to any other form, these operations on automata are fully general.

Because these set operations yield a finite automaton as a result, the results of set operations on regular languages are also regular languages.

Let’s say that A and B are finite automata recognizing regular languages A and B. (I.e., we use “A” to mean both a regular language and a finite automaton that recognizes language “A”.) We can transform the automata A and/or B to construct an automaton that recognizes the languages

A ∪ B, the union of A and B

AC, the complement of A, meaning all strings (over some alphabet) that are not in A

A ∩ B, the intersection of A and B

A - B (set difference), all strings that are in A but not in B

In addition to the possibility of performing set operations on finite automata, it is also possible to check any finite automaton to find out whether or not it recognizes a nonempty set of strings. Using this check, we can find out, for any regular language, whether or not that language is empty.

A decision procedure to check the equivalence of regular languages A and B is

(A = B) ≡ ( (A - B = ∅) and (B - A = ∅) )

In other words, A is equivalent to B if and only if the set differences (A - B) and (B - A) are both empty.

Constructions for Set Operations

Here are constructions for set operations on regular languages.

Union (A ∪ B): The basic idea is to create new start and accepting states, connect the new start state to the start states for A and B using epsilon transitions, and connect the accepting states of A and B to the new accepting state using epsilon transitions. After these modifications are done, all of the original start and accepting states of A and B become non-start, non-accepting states. The resulting NFA recognizes the union of A and B.

Complement (AC): Given a DFA recognizing language A, create an explicit “reject” state, which is a non-accepting state. In all states of A where there is no explicit transition on a particular input symbol, create an explicit transition to the reject state on that symbol. In the reject state, self-transitions on each possible input symbol lead back to the reject state. The result of these transformations is a DFA that recognizes the same language as the original DFA, but every state has a transition on every input symbol. By changing each accepting state to a non-accepting state, and changing each non-accepting state to an accepting state, we create a DFA that recognize the complement of A.

Intersection (A ∩ B): Intersection can be synthesized using the constructions for union and complement:

A ∩ B = ( (AC) ∪ (BC) )C

In other words, the intersection of A and B is the complement of the union of the complements of A and B.

Difference (A - B): Difference can be synthesized using the constructions for intersection and complement

A - B = A ∩ BC

In other words, A - B is the set of all strings that are in A and also in the complement of B.

Determining If An Automaton Recognizes a Nonempty Language

An automaton recognizes a nonempty language if and only if an accepting state is reachable from the start state.

The following algorithm determines if an accepting state is reachable from the start state:

public boolean recognizesNonEmptyLanguage(FiniteAutomaton fa) {
    // If any path from the start state leads to an accepting state,
    // then the automaton accepts at least one string.

    LinkedList<State> workList = new LinkedList<State>();
    Set<State> seen = new TreeSet<State>();

    workList.add(fa.getStartState());

    while (!workList.isEmpty()) {
        State s = workList.removeFirst();
        seen.add(s);

        if (s.isAccepting()) {
            return true;
        }

        for (Transition t : fa.getTransitions(s)) {
            if (!seen.contains(t.getToState())) {
                workList.addLast(t.getToState());
            }
        }
    }

    return false;
}