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 S1, on 1 S0
- From S1: on 0 S0, on 1 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 State | Input 0 | Input 1 |
|---|---|---|
| S0 (accepting) | S1 | S0 |
| S1 (rejecting) | S0 | S1 |
(c) Trace with input 10110:
| Step | Input | Current State | Next State |
|---|---|---|---|
| 0 | - | S0 (start) | - |
| 1 | 1 | S0 | S0 |
| 2 | 0 | S0 | S1 |
| 3 | 1 | S1 | S1 |
| 4 | 1 | S1 | S1 |
| 5 | 0 | S1 | S0 |
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 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 .
(c) Strings over 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 ). 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 .
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) .
Wait, but 011 , adding 1 gives 100 . Let me re-examine.
If 110 in LSB-first means: bit 0 , bit 1 , bit 2 . So the number is . Adding 1 gives . 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) .
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) .
But this is MSB first, not LSB first. In MSB first: 011 . Adding 1: 100 .
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 . Correct!
The Turing machine correctly adds 1 to the binary number 011 (), producing 100 ().
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 .
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: -- one pass through the string, work per character. Memory: (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 matching time, but with potentially exponential compilation time.
The FSM approach is always time and 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 and input , determine whether halts (terminates) when run on .
Proof by contradiction: Assume a function halts(P, I) exists that returns True if program halts on input , 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), thentroubleloops forever. Contradiction. - If
halts(trouble, trouble)returns False (it loops), thentroublehalts. 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 . (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:
| From | On 0 | On 1 |
|---|---|---|
| S0 | S0 | S1 |
| S1 | S2 | S1 |
| S2 | S0 | S3 |
| S3* | S2 | S1 |
Trace with 1101: S0 S1 S1 S2 S3* (accept).
Trace with 1010: S0 S1 S2 S3* S2 (reject).
(b) PDA for :
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 : A DFA has a finite number of states. For the language , the DFA would need to "remember" exactly how many 's were read to verify the same number of 's follow. For arbitrary , 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 , pumping the section breaks the balance, producing a string not in the language -- proving it is not regular.
(d) Complexity classes:
- Sorting: P (polynomial time). Many algorithms exist (merge sort, quicksort average case).
- Graph colouring (decision version): NP-complete. Determining if a graph can be coloured with colours is in NP (a valid colouring is a polynomial-time verifiable certificate) and is NP-hard (all NP problems reduce to it for ).
- 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.