Skip to main content

Theory of Computation — Diagnostic Tests

Unit Tests

UT-1: Finite State Machine Design

Question: Design a finite state machine (FSM) that accepts binary strings containing an even number of 0s. (a) Draw the state transition diagram. (b) Write the state transition table. (c) Trace the FSM with the input 10110 and state whether it is accepted.

Solution:

(a) State transition diagram:

States: S0 (start, even 0s -- accepting), S1 (odd 0s -- rejecting).

Transitions:

  • From S0: on 0 \to S1, on 1 \to S0
  • From S1: on 0 \to S0, on 1 \to S1
0 1
+------+ +------+
| | | |
v |1 v |1
(S0)* ---1--> (S1) ---1--> (S1)
^ 0 ^ 0
| |
+------0-----+

More clearly:

1 0
(S0)* ---> (S0)* [on 1, stay at S0]
(S0)* ---> (S1) [on 0, go to S1]
(S1) ---> (S1) [on 1, stay at S1]
(S1) ---> (S0)* [on 0, go to S0]

(b) State transition table:

Current StateInput 0Input 1
S0 (accepting)S1S0
S1 (rejecting)S0S1

(c) Trace with input 10110:

StepInputCurrent StateNext State
0-S0 (start)-
11S0S0
20S0S1
31S1S1
41S1S1
50S1S0

Final state: S0 (accepting). The string 10110 is accepted because it contains two 0s (even number).


UT-2: Regular Expressions

Question: Write regular expressions for: (a) all binary strings that start with 01, (b) all binary strings of length exactly 3 that contain at least one 1, (c) all strings over {a,b}\{a, b\} that do NOT contain the substring aba. For (c), explain why this cannot be done with a standard regular expression without negation.

Solution:

(a) Strings starting with 01: 01(0|1)*

This matches: 01, 010, 011, 0101, 0110, etc. The (0|1)* allows any binary sequence (including empty) after the initial 01.

(b) Strings of length exactly 3 with at least one 1:

All length-3 binary strings: (0|1)(0|1)(0|1) = 8 strings.

Strings with no 1s: 000.

Strings with at least one 1: all except 000. Regular expression: (0|1)(0|1)(0|1) minus 000.

Direct approach: enumerate all valid strings: 001|010|011|100|101|110|111

Compact approach: (0|1)(0|1)(1|(0(0|1)1)) -- this is getting complex. Simpler:

((0|1)(0|1)1) | ((0|1)1(0|1)) | (1(0|1)(0|1)) -- covers all positions where 1 appears.

Even simpler: 1(0|1)(0|1) | 0 1(0|1) | 00 1 =1(01)201(01)001= 1(0\|1)^2 \| 01(0\|1) \| 001.

(c) Strings over {a,b}\{a, b\} not containing aba:

This requires specifying all allowed strings, which is possible but complex. The complement (strings containing aba) has the regular expression: (a|b)*aba(a|b)*.

A standard regular expression (without negation) for "not containing aba" can be constructed by building a DFA for the language and converting to a regular expression. The DFA has states representing "how much of aba we have seen at the end of the string so far":

States: S0 (start, nothing matched), S1 (last char was 'a'), S2 (last two chars were 'ab'), dead state (saw 'aba').

The regex from the DFA (excluding the dead state) is complex but well-defined. The point is that regular expressions can express any regular language (recognised by a DFA), and "strings not containing aba" is a regular language. However, expressing it directly is cumbersome -- it is much easier to write the complement (a|b)*aba(a|b)* and use negation in implementations that support it (e.g., regex engines with negative lookahead: ^((?!aba).)*$).


UT-3: Turing Machine Trace

Question: A Turing machine has the following transition function for adding 1 to a binary number (stored least significant bit first on the tape, with blanks represented by _):

  • In state q0, read 0: write 1, move R, go to q_accept
  • In state q0, read 1: write 0, move L, stay in q0
  • In state q0, read _: write 1, move R, go to q_accept

Trace the Turing machine with the input _110_ (representing binary 011 =3= 3). What is the result? What decimal number does it represent?

Solution:

Initial tape: _ 1 1 0 _ (head on first 1, state q0)

Wait, the number is stored LSB first, so _110_ represents bits 1, 1, 0 from left to right, which is binary 0112=310011_2 = 3_{10}.

Tape layout: [_] [1] [1] [0] [_] -- but the head position needs clarification. Let me assume the head starts at the rightmost bit (LSB):

Tape: 1 1 0 _ with head on 0 (the LSB).

Step 1: State q0, read 0. Write 1, move R, go to q_accept. Tape: 1 1 1 _. Head on _.

The machine halts. Result: 111 (LSB first) =1112=710= 111_2 = 7_{10}.

Wait, but 011 =3= 3, adding 1 gives 100 =4= 4. Let me re-examine.

If 110 in LSB-first means: bit 0 =1= 1, bit 1 =1= 1, bit 2 =0= 0. So the number is 1×20+1×21+0×22=1+2+0=31 \times 2^0 + 1 \times 2^1 + 0 \times 2^2 = 1 + 2 + 0 = 3. Adding 1 gives 4=10024 = 100_2. In LSB-first: 001.

Let me retrace with the correct initial tape. The tape stores 110 (LSB first) for the number 3.

Tape: [1] [1] [0] [_] with head on position 0 (the LSB, which is 1).

Step 1: State q0, read 1. Write 0, move L, stay in q0. Tape: [0] [1] [0] [_]. Head moves left to position -1.

Step 2: State q0, read _. Write 1, move R, go to q_accept. Tape: [1] [0] [1] [0] [_]. Head on position 0.

Result: 101 (LSB first) =1×20+0×21+1×22=1+0+4=5= 1 \times 2^0 + 0 \times 2^1 + 1 \times 2^2 = 1 + 0 + 4 = 5.

Hmm, that gives 5, not 4. There is an issue with my tape setup. Let me reconsider.

The correct setup: the tape should be _110_ where the number is between the blanks. Head starts at the rightmost digit.

Tape: _ [1] [1] [0] _

If the head starts at 0 (position 3):

Step 1: q0, read 0. Write 1, move R, q_accept. Tape: _ [1] [1] [1] _. Result: 111 (MSB first) =7= 7.

But this is MSB first, not LSB first. In MSB first: 011 =3= 3. Adding 1: 100 =4= 4.

The issue is my initial interpretation. Let me use MSB first:

011 with head on the LSB (rightmost 1):

Step 1: q0, read 1. Write 0, move L. Tape: 0 1 0. Head on the middle 1.

Step 2: q0, read 1. Write 0, move L. Tape: 0 0 0. Head on the 0.

Step 3: q0, read 0. Write 1, move R, q_accept. Tape: 1 0 0. Head on 0.

Result: 100 =4= 4. Correct!

The Turing machine correctly adds 1 to the binary number 011 (=3= 3), producing 100 (=4= 4).

Integration Tests

IT-1: Regular Languages and FSM (with Programming Constructs)

Question: Write a Python function that simulates the FSM from UT-1 (accepts binary strings with even number of 0s). Then, write a regular expression that matches the same language. Use both to test the strings "", "0", "00", "101", "1110", "01010". Compare the time complexity of the FSM approach vs the regex approach for an input of length nn.

Solution:

def accepts_even_zeros(s):
state = 0
for char in s:
if char == '0':
state = 1 - state
return state == 0

import re

pattern = re.compile(r'^(1*01*01*)*$')

test_strings = ["", "0", "00", "101", "1110", "01010"]

for s in test_strings:
fsm_result = accepts_even_zeros(s)
regex_result = bool(pattern.fullmatch(s))
print(f"'{s}': FSM={fsm_result}, Regex={regex_result}")

Expected output:

'': FSM=True, Regex=True
'0': FSM=False, Regex=False
'00': FSM=True, Regex=True
'101': FSM=True, Regex=True
'1110': FSM=False, Regex=False
'01010': FSM=True, Regex=True

Time complexity comparison:

FSM approach: O(n)O(n) -- one pass through the string, O(1)O(1) work per character. Memory: O(1)O(1) (just one state variable).

Regex approach: depends on the implementation. For the regex ^(1*01*01*)*$, a naive backtracking engine could have exponential time in the worst case because the nested * operators create many possible match paths. A DFA-based regex engine (like Google's RE2) compiles the regex to a DFA first and achieves O(n)O(n) matching time, but with potentially exponential compilation time.

The FSM approach is always O(n)O(n) time and O(1)O(1) space, making it more predictable and efficient. For simple validation rules, a hand-coded FSM or state variable is often preferred over regex for performance-critical applications.


IT-2: Halting Problem and Decidability (with Software Engineering)

Question: (a) State the halting problem and explain the proof by contradiction that it is undecidable. (b) Explain why a compiler cannot detect all infinite loops in a program. (c) Despite the halting problem being undecidable, static analysis tools can detect SOME infinite loops. Give an example of a detectable infinite loop pattern and explain why it does not contradict the halting problem.

Solution:

(a) Halting problem: Given an arbitrary program PP and input II, determine whether PP halts (terminates) when run on II.

Proof by contradiction: Assume a function halts(P, I) exists that returns True if program PP halts on input II, and False otherwise.

Construct a paradoxical program trouble(P):

function trouble(P):
if halts(P, P):
loop forever
else:
halt

Now ask: does trouble(trouble) halt?

  • If halts(trouble, trouble) returns True (it halts), then trouble loops forever. Contradiction.
  • If halts(trouble, trouble) returns False (it loops), then trouble halts. Contradiction.

Both cases lead to contradictions, so the assumption that halts exists is false. The halting problem is undecidable.

(b) A compiler cannot detect all infinite loops because if it could, it would solve the halting problem. Specifically, to detect an infinite loop, the compiler would need to determine whether a program (or part of a program) halts -- which is undecidable. Any compiler that claims to detect all infinite loops would either miss some (false negatives) or incorrectly flag terminating code as infinite (false positives).

(c) Detectable pattern example: while True: pass -- this is trivially an infinite loop because the condition is a constant True.

Another example: x = 0; while x >= 0: x += 1 -- a static analyser can detect that x only increases and the condition x >= 0 is always true (for an integer that starts non-negative), making this loop infinite.

This does NOT contradict the halting problem because: the halting problem says NO algorithm can decide halting for ALL possible programs. Static analysis tools that detect specific patterns are correct for those patterns but incomplete -- they cannot detect all infinite loops. The halting problem only proves that a universal detector is impossible, not that some detectors for some cases are impossible. There exist infinite loops that no static analyser can detect (e.g., loops whose termination depends on deep mathematical properties like the Collatz conjecture).


IT-3: Automata and Complexity Classes (with Complexity Analysis)

Question: (a) Design a deterministic finite automaton (DFA) that accepts all binary strings ending in 101. (b) Design a pushdown automaton (PDA) that accepts the language {anbn:n0}\{a^n b^n : n \geq 0\}. (c) Explain why the language in (b) cannot be accepted by a DFA. (d) State which complexity class (P, NP) the following belong to: sorting, graph colouring (decision), primality testing.

Solution:

(a) DFA for strings ending in 101:

States: S0 (start, no relevant suffix), S1 (last char was '1'), S2 (last two chars were '10'), S3 (last three chars were '101' -- accepting).

Transitions:

FromOn 0On 1
S0S0S1
S1S2S1
S2S0S3
S3*S2S1

Trace with 1101: S0 1\xrightarrow{1} S1 1\xrightarrow{1} S1 0\xrightarrow{0} S2 1\xrightarrow{1} S3* (accept).

Trace with 1010: S0 1\xrightarrow{1} S1 0\xrightarrow{0} S2 1\xrightarrow{1} S3* 0\xrightarrow{0} S2 (reject).

(b) PDA for {anbn}\{a^n b^n\}:

States: q0 (start), q1 (reading b's, popping), q_accept.

  • Start in q0, stack initially empty.
  • Read 'a' in q0: push 'a' onto stack, stay in q0.
  • Read 'b' in q0 (stack not empty): transition to q1, pop 'a'.
  • Read 'b' in q1 (stack not empty): pop 'a', stay in q1.
  • Read 'b' in q1 (stack empty): reject (more b's than a's).
  • End of input in q1 with empty stack: accept.
  • Read 'a' in q1: reject (a after b).

(c) Why no DFA can accept anbna^n b^n: A DFA has a finite number of states. For the language {anbn}\{a^n b^n\}, the DFA would need to "remember" exactly how many aa's were read to verify the same number of bb's follow. For arbitrary nn, this requires unbounded memory (counting arbitrarily high). A DFA's finite state set cannot store an arbitrarily large count. This is proven by the pumping lemma for regular languages: a sufficiently long string in the language can be "pumped" (a substring repeated) and must remain in the language. For anbna^n b^n, pumping the aa section breaks the balance, producing a string not in the language -- proving it is not regular.

(d) Complexity classes:

  • Sorting: P (polynomial time). Many O(nlogn)O(n \log n) algorithms exist (merge sort, quicksort average case).
  • Graph colouring (decision version): NP-complete. Determining if a graph can be coloured with kk colours is in NP (a valid colouring is a polynomial-time verifiable certificate) and is NP-hard (all NP problems reduce to it for k3k \geq 3).
  • Primality testing: P (since the AKS algorithm, 2002). It was known to be in NP (a certificate of compositeness -- a factor -- is verifiable in polynomial time) and was known to be in co-NP, and is now known to be in P.