Questions in black, answers in blue.
Remember that a DFA is a machine that reads a string and decides whether it is in a particular language by maintaining a state from a finite state set. Here is a Java class modeling DFA's whose input is strings over the alphabet {a,b}:
public class Dfa
int k; // number of states
bool [] isFinal; // whether each state is final
int [] atrans, btrans; // transitions for a, b: atrans[i] is delta(a,i)
int state; //current state, note start state is always 0
public Dfa (int [] fin, int [] at, int [] bt)
{// creates new DFA with given final states, transformations
k = fin.length;
if ((at.length != k) || (bt.length != k))
throw new ArithmeticException ("DFA constructor : bad input");
isFinal = fin; atrans = at; btrans = bt;
state = 0;}
public boolean accept (String w)
{// returns true iff this DFA accepts w, w must be in {a,b}*
state = 0;
for (int i=0; i < w.length(); i++) {
if (w.charAt(i) == 'a') state = atrans[state];
else if (w.charAt(i) == 'b') state = btrans[state];
else throw new ArithmeticException("DFA: bad input string");}
return isFinal[state];}
public static void main (String[] args)
{// creates sample Dfa with three states, tests it
Dfa d = new Dfa({false, true, false}, {0,1,2}, {1,2,0});
System.out.println("It is " + d.accept("abbabbaa") +
" that abbabbaa is in the language");}
accept
on a string of length n, assuming
that k is a constant? Explain your answer.
The method has a single loop that is executed n times on a string of length n. Inside the loop are Θ(1) operations and outside are Θ(1) more, so the total time is Θ(n).
Many of you noticed that for this particular automaton, the behavior on
a string depends only on the number of b's in the string. In particular,
the string is accepted iff the number of b's is congruent to 1 modulo 3.
So with n=8, the number of accepted strings is (8 choose 1) + (8 choose 4)
+ (8 choose 7) = 8 + (8*7*6*5)/(1*2*3*4) + 8 = 8 + 70 + 8 = 86.
There is another way to solve the problem that generalizes to the algorithm
we use below for arbitary DFA's. As explained below, we need to raise a
particular three by three matrix to the eighth power and read off an entry
of the result. Using repeated squaring and ASCII art:
and the 0,1 entry is 86.
| 1 1 0 |^8 | 1 2 1 |^4 | 5 5 6 |^2 | 85 86 85 |
| 0 1 1 | = | 1 1 2 | = | 6 5 5 | = | 85 85 86 |
| 1 0 1 | | 2 1 1 | | 5 6 5 | | 86 85 85 |
census
for the Dfa
class that takes an int
parameter n and returns the number of strings of length n in the DFA's
language. There is a slow way to do this by brute force, and a more clever
way. Try to do both. Analyze the running time of your methods in terms
of n, again assuming that k is a constant.
The slow method is to cycle through all possible strings of length n, run
the DFA on each, and count the number that are accepted. The easiest way
I know to cycle through all strings is to translate each of the numbers from
0 through 2n-1 into a string by mapping the binary representation
from over {0,1} to {a,b}:
public static String stringNumber (int length, int number)
{// returns number'th string of given length in {a,b}*
String w = "";
for (int i = length - 1; i >= 0; i--)
if (bit(number, i) == 0) w += "a";
else w += "b";
return w;}
public int slowCensus (int n)
{// counts accepted strings of length n by exhaustive search
int count = 0;
for (int i=0; i < power(2,n); i++)
if (accept(stringNumner(n,i))) count++;
return count;}
Here "bit" and "power" are the obvious static methods to get a particular
bit of a number or raise a number to a power. This algorithm takes
Θ(n2n) time, since it executes 2n loops of
Θ(n) time each.
Here's a second method that one could easily discover without knowing
anything about matrices. It's natural to ask whether a census function for
length n helps you compute the census function for length n+1. It doesn't,
because you have to know which state the n-letter prefix of the string
wound up in before you can tell where it wound up. But if you keep an
array telling how many strings wind up in each state, this can be used to
get the answer:
public int [] censusArray (int n)
{// entry i is how many strings of length n take calling DFA to state i
int [] result = new int[k]; //recall k is number of states
if (n == 0) result[0] = 1; // rest of result stays all 0's
else {
int [] previous = censusArray(n-1);
for (int i=0; i < k; i++) {
result[atrans[i]] += previous[i];
result[btrans[i]] += previous[i];}}
return result;}
public int fasterCensus (int n)
{// counts accepted strings of length n
int [] ca = censusArray(n);
int count = 0;
for (int i=0; i < k; i++)
if (isFinal(i)) count += ca[i];
return count;
The time of fasterCensus
is dominated by the single call
to censusArray
. The latter is recursive, making calls to depth
n before reaching the base case. Each call to the program uses time Θ(k)
= Θ(1), so the total time is Θ(n), a vast improvement over
the first version.
If A is the edge-count matrix for the graph underlying the DFA, this
code is essentially computing A^i times the vector [1,0,...,0] for each i
and passing the answer as censusArray(i)
.
But of course we
could compute this matrix power more quickly by repeated squaring.
Our final version does this, making
use of a Matrix class that contains the following
methods (we'll just give the declarations here, meaning and code should be
pretty obvious):
public class Matrix
{// stores a matrix of ints
int rows, columns;
int [][] entry = new int[rows][columns];
public Matrix (int r, int c);
public int getEntry (int i, int j);
public int setEntry (int i, int j, int newvalue);
public Matrix multiply (Matrix m);
public Matrix identity (int size);
public Matrix power (int n)
{// returns (this)^n, as in 6.6 or lecture of 29 October
if (n == 0) return identity(k);
if (n == 1) return this;
Matrix halfpower = power(n/2);
Matrix result = halfpower.multiply(halfpower);
if (n%2 == 1) result = this.multiply(result);
return result;}
{//back to Dfa class...
public Matrix a()
{// returns Matrix counting edges in graph of this DFA
Matrix result = new Matrix(k,k);
for (int i=0; i < k; i++) {
result.setentry(i,atrans[i],result.getEntry(i,atrans[i])++);
result.setentry(i,btrans[i],result.getEntry(i,btrans[i])++);}
return result;}
public int census (int n)
{// returns number of accepted strings of length n,
// uses repeated squaring
Matrix aToTheN = a().power(n);
int count = 0;
for (int i=0; i < k; i++)
if (isFinal[i]) count += aToTheN.getEntry(0,i);
return count;}
Now the running time of census
is dominated by the call
to power
. And this is Θ(log n), because we recurse to that
depth and each call to the program uses time Θ(k3) for
the matrix multiplication. Note that since k is a constant, so is
k3 and so each call to multiply
uses Θ(1) time.
Last modified 29 October 2003