Important! ex1.cpp is a much better way to do this!
$ g++ ex1.cpp -o ex1 $ ./ex1 usage: ./ex1 in1 in2 ... $ ./ex1 a b aa ab bb abaab ababb accept a reject b reject aa accept ab reject bb accept abaab reject ababb
Comment 1:
Performance-wise, ex0.cpp is a disaster. A single
transition might take as many as |Σ||Q| steps --- i.e. we
check each character/state combination. If we give each alphabet symbol an
index, we can represent the transition function δ as a
table (i.e. a 2D array), just like we do when writing out
5-tuples for machines. That way, each transition is done in
a single table lookup which, as you've learned in data
structures, is a constant time operation. ex1.cpp does this.
Similarly, in ex0.cpp, determining whether we're in
an accepting state can take as many as |W| steps. Once
again, clever use of arrays can make that constant time, and
that's what ex1.cpp does.
Comment 2:
ex1.cpp is vastly better
than that of ex0.cpp in another, possibly more
important, way. In ex0.cpp the transitions and
accepting states are embedded in code, i.e. in
if/else-if/else statement and || and && expressions.
In ex1.cpp, the transitions and accepting states
are embedded in data, i.e. in the 2D
array delta and 1D array W.
[This is an example of data-driven programming which
is something we will see Python makes easy for us programmers
to do.]
Why is this so much better? Imagine you want to change the
machine by changing which states are accepting states? Or by
changing some of the transitions, which program is easier to
modify? What if I gave you a totally different machine with
four states and alphabet $\{a,b,c\}$, which program would be
easier to adapt?
Important! The big takeaway here is that it is trivial to convert a finite automaton into extremely efficient code to accept or reject strings by executing the machine. In fact, we can automate the conversion. So, while it is true that Finite Automata are very simple, limited models of computation, we love it when we can design FAs to solve our problems, because we can automatically convert them to extremely efficient code. With more powerful models of computing, converting to code becomes more and more difficult, and efficiency becomes harder and harder (to the point of being impossible) to achieve.
Rembember: a language is a set. Therefore, if L1 and L2 are languages, the expression L1∩L2 is well-defined — it makes sense. Same with ∪, – and . In contrast, if M1 and M2 are finite automata, M1∩M2 is nonsense — it is not well-defined — because M1 and M2 are tuples, not sets! If someone writes "M1∩M2", they probably mean "L(M1)∩L(M2)" ... but then again, maybe not. You don't know. So give them a smack upside the head to provide helpful corrective feedback.
Algorithm: ComplementFrom our informal discussions on this algorithm, it should be clear that it does what it's supposed to, i.e. that it produces a machine M' accepting the complement of the language accepted by M. A strictly formal proof of the algorithm's correctness relies on the concept of configuration, which we'll need to even give a formal definition of what it means for a machine to accept a string! On the other hand, hopefully this algorithm is so obviously correct that no further proof is needed.
Input: DFA $M = (Q,\Sigma,\delta,s,W)$
Output: DFA $M'$ such that $L(M') = \overline{L(M)}$$M' = (Q,\Sigma,\delta,s,Q-W)$
ACTIVITY:
{w ∈ {I,J,K}* | |w| = 10}This reads "the set of all strings w over {I,J,K} such that the length of w is 10".
{w ∈ {0,1}* | w = wR}This reads "the set of all strings w over {0,1} such that w equals w reversed.
{xw ∈ {a,b,c}* | x = a and w = wR}Is a in this language? Why? How about λ?
s:Z → ZThe prototype you should already know. The definition says that for any x from the function's domain, the function produces x+1.
s(x) = x+1
Often we end up breaking things up into cases when we define functions. For example, we might give the following definition for the "signed successor function" ss.
As I said before, you get a fair amount of flexibility in how you define these things. Here's an example defining a function f that takes a string and concatenates it with its reverse, with the hitch that if the string has odd length, the last character isn't doubled up in the result string. For example: f(ab) = abba, but f(abb) = abbba. Let Σ = {a,b}. We'll define f in a few different ways:
Algorithm: IntersectionOnce again, a formal proof that this algorithm really constructs a machine accepting L(M1) ∩ L(M2) will have to wait. We're familiar enough with the idea of this algorithm that we probably don't need any more convincing.
Input: DFA $M_1 = (Q_1,\Sigma,\delta_1,s_1,W_1)$ and DFA $M_2 = (Q_2,\Sigma,\delta_2,s_2,W_2)$
Output: DFA $M$ such that $L(M) = L(M_1) \cap L(M_2)$$M = (Q,\Sigma,\delta,s,W)$, where
- $Q = Q_1 \times Q_2$
- $\delta:Q \times \Sigma \rightarrow Q$
$\delta((p,q),a) = \left( \delta_1(q,a), \delta_2(p,a) \right)$- $s = (s_1,s_2)$
- $W = W_1 \times W_2$
All the variables describing the input are like parameters to a function, i.e. they are values you can use to construct your output result --- you do not have to give them values in your algorithm. All variables appearing in your output description that are not part of the input are variables you must give values to. For example, in the intersection algorithm the variable Σ appears in the output expression, but nowhere in the body of the algorithm do I give Σ a value. No problem, because Σ is given to us as input to the algorithm. The variable W, on the other hand, is not given to us as input and it appears in the output expression. Therefore, we must define W, i.e. give it a value, which we do on the last line with "W = W1 x W2".