Week 2: 2017/01/23-27

In [1]:
from tock import *

Monday reading

Read 1.1.

Sipser uses an automatic door to illustrate a finite automaton. (The door is the type that swings open toward you, not the type that slides open.) This example is good because its state is something visible. But this automaton doesn't have accept states, because the input string has no end and the automaton's job is not to accept or reject strings. So the door is best thought of as an analogy more than a strict example.

The section "The regular operations" is slightly unusual. These closure properties are important, and where he's going with this is that these closure properties will be used to prove that any regular expression can be compiled into a finite automaton. But using DFAs, this proof is not easy and he gives up halfway. Sipser uses this as a motivation for nondeterminism, which is introduced in the next section. The proof of both Theorem 1.25 and 1.26 will turn out to be very easy.

On the other hand, the proof of Theorem 1.25 has a footnote (3) regarding closure under intersection. This is an important result and the Cartesian product construction is the standard way to prove it.

Tuesday class

Deterministic finite automata

In the first class, I said that we would study a sequence of kinds of "computers" leading up to Turing machines. Today we'll look at the simplest of these, finite automata, which are only allowed to make a single left-to-right pass through the input.

  • The machine is fed a paper tape with a string written on it.
  • At each time step, it reads one symbol and moves one square to the right.
  • It has only a finite number of states (another way of saying this: it uses only $O(1)$ memory).
  • When it reaches the end of the string, it decides whether to accept or reject the string.

In lecture notes I'll be using a toolkit called Tock, which displays and runs automata inside Jupyter notebooks. You are welcome to use it to tinker with automata or to write your homework assignments.

Tock reads files in CSV format (or in a graph format called TGF):

In [2]:
%cat dfa-m1.csv
    , 0  , 1
>q1 , q1 , q2
@q2 , q3 , q2
 q3 , q2 , q2
In [3]:
m1 = read_csv('dfa-m1.csv')

Then you can display it as a table or as a graph:

In [4]:
to_table(m1)
Out[4]:
0 1
>q1 q1 q2
@q2 q3 q2
q3 q2 q2
In [5]:
g = to_graph(m1)
g
%3 _START 0 q1 _START->0 0->0 0 2 q2 0->2 1 1 q3 1->2 0 1 2->1 0 2->2 1

And you can run it on a string:

In [6]:
run(m1, "1 1 0 1")
%3 _START 3 q1 _START->3 0 1 0->1 1 8 1->8 0 2 q2 4 q2 2->4 3->2 5 q3 4->5 6 q2 5->6 7 7->0 1 9 8->9 1

Above is a graph that represents the run of the automaton on an input string. Each node is a configuration of the automaton (that is, being in a particular state at a particular time), and above the nodes are the input symbols that are read. Since last node has a double circle, the automaton is in the accept state at the end of the input.

In [7]:
run(m1, "1 0 1 0 0 0")
%3 _START 1 q1 _START->1 0 q2 2 q3 0->2 1->0 3 q2 2->3 4 q3 3->4 5 q2 4->5 6 q3 5->6 7 8 7->8 1 10 8->10 0 9 11 9->11 0 10->9 0 12 12->7 0 13 13->12 1

This time, the automaton rejected the string (because the last node does not have a double circle).

More example DFAs from the book:

In [8]:
m2 = read_csv('dfa-m2.csv')
m2
%3 _START 0 q1 _START->0 0->0 0 1 q2 0->1 1 1->0 0 1->1 1
In [9]:
m3 = read_csv('dfa-m3.csv')
m3
%3 _START 0 q1 _START->0 0->0 0 1 q2 0->1 1 1->0 0 1->1 1
In [10]:
m4 = read_csv('dfa-m4.csv')
m4
%3 _START 0 s _START->0 3 r1 0->3 b 4 q1 0->4 a 1 q2 1->1 b 1->4 a 2 r2 2->2 a 2->3 b 3->2 a 3->3 b 4->1 b 4->4 a
In [11]:
m5 = read_csv('dfa-m5.csv')
m5
%3 _START 2 q0 _START->2 0 q2 0->0 0 1 q1 0->1 2 0->2 1 RESET 1->0 1 1->1 0 1->2 2 RESET 2->0 2 2->1 1 2->2 0 RESET

Question. Write a DFA that recognizes legal C identifiers (e.g., variable names). For simplicity, assume the alphabet $\Sigma=\{\mathtt{f},\mathtt{i},\mathtt{n},\mathtt{t},\mathtt{0}\}$. Don't worry about reserved words yet.

Solution from class:

In [12]:
m_identifier = read_csv('identifier.csv')
m_identifier
%3 _START 0 q1 _START->0 1 q3 0->1 0 2 q2 0->2 f i n t 1->1 0 f i n t 2->2 0 f i n t

Question. Write another DFA that recognizes C reserved words. For simplicity, just handle the reserved words $\mathtt{if}$ and $\mathtt{int}$.

Solution from class:

In [13]:
m_reserved = read_csv('reserved.csv')
m_reserved
%3 _START 0 r1 _START->0 1 r3 0->1 0 f n t 2 r2 0->2 i 1->1 0 f i n t 2->1 0 i t 3 r4 2->3 f 4 r5 2->4 n 3->1 0 f i n t 4->1 0 f i n 4->3 t

Question. Modify the last DFA so that it recognizes everything but C reserved words.

In [14]:
m_nonreserved = read_csv('nonreserved.csv')
m_nonreserved
%3 _START 0 r1 _START->0 3 r3 0->3 0 f n t 4 r2 0->4 i 1 r4 1->3 0 f i n t 2 r5 2->1 t 2->3 0 f i n 3->3 0 f i n t 4->1 f 4->2 n 4->3 0 i t

The product construction

The proof of Theorem 1.25 (closure under union) involves a construction sometimes called the product construction. It is used in a lot of proofs. For instance, its most well-known application is probably to prove closure under intersection (footnote 3). The basic idea is that you can use a finite automaton to simulate two (or more) other finite automata.

Question. Combine the identifier DFA and the no-reserved-word DFA into a single DFA.

Wednesday reading

Read 1.2.

Thursday class

Homework 1 is due this evening! Also, please remember to have your project teams registered by today.

Nondeterministic finite automata

Turing machines and deterministic finite automata were inspired by mechanical devices. It's easy to imagine building them, and many people have. But a nondeterministic automaton seems profoundly unnatural -- like bilocation. However, mathematically, nondeterminism is pretty natural concept. A NFA accepts a string iff there exists an accepting path for it; when a DFA accepts a string, there exists a unique path for it. Thought of this way, the NFA actually has the simpler definition. We will come back to the idea of nondeterminism repeatedly, but it will become of paramount importance in Unit IV.

In [15]:
n1 = read_csv('nfa-n1.csv')
to_graph(n1)
%3 _START 0 q1 _START->0 0->0 0 1 3 q2 0->3 1 1 q4 1->1 0 1 2 q3 2->1 1 3->2 0 ε
In [16]:
run(n1, "0 1 0 1 1 0")
%3 _START 24 q1 _START->24 0 9 0->9 0 1 1->0 1 2 4 2->4 0 3 3->2 1 4->1 1 5 5->3 0 6 q1 7 q3 8 q3 10 q2 10->7 10->8 11 q4 12 q1 12->6 13 q3 14 q4 13->14 14->11 15 q2 15->13 16 q1 16->10 16->12 17 q4 17->14 18 q3 19 q1 19->15 19->16 20 q3 20->17 21 q1 21->19 22 q2 22->18 22->20 23 q1 23->21 23->22 24->23

Now the run graph (basically the same as Figure 1.29 but rotated 90 degrees) looks more complicated: because of nondeterminism, the graph has branches and there is more than one node at each time step. Also, because of epsilon transitions, there are edges within time steps.

In order for an NFA to accept a string, only one path needs to accept it. The other paths can dead-end, or they can even loop forever, like this:

In [17]:
n = read_csv("nfa-loop.csv")
to_graph(n)
%3 _START 0 q1 _START->0 1 q3 0->1 ε 2 q2 0->2 0 1->1 ε
In [18]:
run(n, "0")
%3 _START 4 q1 _START->4 0 q2 1 q3 1->1 2 3 2->3 0 4->0 4->1

Intuitively, this means that when one of the branches of the computation hits an accept state, all the other branches can quit. But if none of the branches hit an accept state, then how do we know when to kill a looping branch?

In [19]:
run(n, "1")
%3 _START 0 q1 _START->0 1 q3 0->1 1->1 2 3 2->3 1

The point of this digression is just to reinforce the caution not to try to think of NFAs as physical machines. But they are very useful mathematical concepts.

Question. Last time, we saw how, if a DFA recognizes language $L$, then flipping the accept and non-accept states causes it to recognize $L^C$. Do the same thing to $N_1$ and see what language it recognizes. What happened?

More example NFAs from the book:

In [20]:
n2 = read_csv('nfa-n2.csv')
n2
%3 _START 0 q1 _START->0 0->0 0 1 2 q2 0->2 1 1 q3 3 q4 1->3 0 1 2->1 0 1
In [21]:
n3 = read_csv('nfa-n3.csv')
n3
%3 _START 0 q1 _START->0 4 q4 0->4 ε 5 q2 0->5 ε 1 q3 1->5 0 2 q5 3 q6 2->3 0 3->4 0 4->2 0 5->1 0
In [22]:
n4 = read_csv('nfa-n4.csv')
n4
%3 _START 0 q1 _START->0 1 q3 0->1 ε 2 q2 0->2 b 1->0 a 2->1 a b 2->2 a

Equivalence with DFA

NFAs seem like they might be a lot more powerful than DFAs, in the sense that they might accept more languages than DFAs do. (Recall: the question is not whether NFAs can accept more strings, because clearly we can design either a DFA or a NFA that accepts all strings. The question is whether NFAs can accept more languages.) Perhaps surprisingly, the answer is no. Any NFA can be converted to an equivalent DFA by what is commonly known as the subset construction.

In [23]:
n4 = read_csv('nfa-n4.csv')
d4 = determinize(n4)
to_graph(d4)
%3 _START 5 {q1,q3} _START->5 0 {q1,q2,q3} 0->0 a 3 {q2,q3} 0->3 b 1 {} 1->1 a b 2 {q3} 2->1 b 2->5 a 3->0 a 3->2 b 4 {q2} 4->2 b 4->3 a 5->4 b 5->5 a

Let's run the determinized automaton on a string and compare it with the run of the nondeterministic automaton on the same string. Do you see how they relate to each other?

In [24]:
run(d4, "b a b a")
%3 _START 9 {q1,q3} _START->9 0 {q3} 1 {q1,q3} 0->1 2 3 5 3->5 b 4 4->3 a 5->2 a 6 6->4 b 7 {q2} 8 {q2,q3} 7->8 8->0 9->7
In [25]:
run(n4, "b a b a")
%3 _START 12 q1 _START->12 0 2 0->2 a 1 q2 8 q3 1->8 10 q2 1->10 3 6 3->6 b 4 4->0 b 5 q3 9 q1 5->9 6->4 a 7 q3 9->7 10->5 11 q3 12->1 12->11