Previous | Tutorial index | Next
Tutorial 4: Basics of Computability and Computational Complexity
Learning Objective
To be able to explain the basics of computability and computational complexity.
4.1 Introduction: The Theoretical Limits of Programming
As a Python programmer, you will eventually write code that runs too slowly or crashes because you didn't account for edge cases. But some problems are not just difficult to solve—they are impossible to solve algorithmically. Others can be solved, but doing so would take longer than the lifespan of the universe. This tutorial bridges the gap between your Python code and the theoretical foundations of computer science.
You will learn two crucial ideas:
- Computability Theory: Which problems can a computer solve at all? (The limits of the algorithm)
- Computational Complexity Theory: If a problem is solvable, how fast can we solve it? (The limits of efficiency)
Understanding these concepts will save you from trying to solve unsolvable problems and help you recognize when a problem is fundamentally intractable, allowing you to look for approximations instead of exact solutions.
4.2 Computability Theory: What Can Be Computed?
Computability theory asks a fundamental question: Are there problems that no computer program can ever solve, regardless of how much memory or time it has?
In theoretical computer science, a problem is usually defined as a decision problem—a question that has a yes/no answer. For example:
- "Given a number
n, is n prime?" (Yes/No)
- "Given a Python function and an input, will it eventually stop running?"
The specific details of the programming language (Python, C, Java) don't matter for this theory. We need a universal, mathematically precise model of computation.
4.2.2 The Turing Machine (1936)
In 1936, Alan Turing introduced a theoretical device that formalized the concept of "algorithm" and "computation." A Turing Machine is not a physical machine but a mathematical thought experiment that defines what is computable.
Components of a Turing Machine:
- An Infinite Tape: Divided into cells. Each cell holds a symbol (usually
0, 1, or a blank _). This tape serves as the computer's unlimited memory.
- A Read/Write Head: Points to one cell on the tape. It can read the symbol, write a new symbol, and move one step left or right.
- A Finite State Register: Stores the current "state" of the machine. It acts like the Program Counter and internal flags.
- A Transition Function (δ): A set of rules that tells the machine what to do based on the current state and the symbol being read. The rule specifies:
- What symbol to write on the tape.
- Whether to move the head Left or Right.
- Which state to transition to next.
- Accept/Reject States: Special states where the machine halts and outputs a definitive Yes (Accept) or No (Reject).
How it Works:
The machine starts in a defined start state with the input written on the tape. It repeatedly reads the current cell, applies the transition function, writes a new symbol, moves, and changes state. If it reaches an Accept or Reject state, it halts. If it never reaches one, it runs forever.
Why is this relevant to Python?
Python is Turing-complete, meaning it can simulate any Turing Machine. Conversely, a Turing Machine can simulate any Python program (given infinite time and tape). Thus, the limits of the Turing Machine are the limits of Python.
4.2.3 The Universal Turing Machine
Turing went a step further. He showed that a single Turing Machine could be designed to simulate any other Turing Machine by reading the description of that machine from the tape. This is the theoretical equivalent of a stored-program computer—the Universal Turing Machine is the blueprint for every general-purpose CPU and every interpreter (like the Python interpreter itself).
4.2.4 The Church-Turing Thesis
In the 1930s, Alonzo Church (inventor of lambda calculus) and Alan Turing independently proposed what is now known as the Church-Turing Thesis:
"Every effectively calculable function is a computable function by a Turing Machine."
In plain English: Any computation that a human can perform using a well-defined algorithm can be performed by a Turing Machine (and thus by a Python program). It is called a "thesis" rather than a theorem because it relies on the intuitive definition of "effectively calculable," which cannot be formally proven. However, it is universally accepted by computer scientists and serves as the foundation of computing.
4.2.5 Decidable vs. Undecidable Problems
Based on the Turing Machine model, we can categorize problems:
- Decidable (Computable): There exists a Turing Machine (an algorithm) that halts with a "Yes" or "No" answer for every possible input.
- Example: "Is this number prime?" (Yes, we have the Sieve of Eratosthenes).
- Semi-Decidable (Recognizable): There exists a Turing Machine that will say "Yes" if the answer is Yes, but may run forever (or say "No") if the answer is No. It doesn't halt for all inputs.
- Example: The Halting Problem is semi-decidable for the "Yes" case (if it halts, we can just run it and see).
- Undecidable (Uncomputable): There is no Turing Machine that can solve this problem for all possible inputs. No algorithm exists, ever.
- Classic Example: The Halting Problem.
4.2.6 The Halting Problem (The Quintessential Undecidable Problem)
The Problem: Given a description of a program P and an input I, determine whether P(I) will eventually halt (finish) or run forever.
Why it matters to you: This directly applies to Python. We would love to have a perfect debugger that could tell us if our while loop will eventually stop, or if our recursive function will hit a base case. Turing proved this is impossible.
The Proof (By Contradiction / Diagonalization):
Let's assume, for the sake of contradiction, that such a program does exist. Let's call it HALT(P, I). It returns True if P(I) halts, and False if it runs forever.
Now, let's write a new program called TROUBLE(P) (a "troublemaker") that does the following:
- It takes a program
P as its only input.
- It calls
HALT(P, P)—checking whether P halts when given itself as input.
- If
HALT(P, P) returns True (meaning P(P) halts), then TROUBLE goes into an infinite loop (it deliberately runs forever).
- If
HALT(P, P) returns False (meaning P(P) runs forever), then TROUBLE halts immediately (it stops).
Now, what happens if we run TROUBLE(TROUBLE)—we give TROUBLE itself as input?
- If
HALT(TROUBLE, TROUBLE) says "It halts" (True), then TROUBLE should run forever (Step 3). Contradiction.
- If
HALT(TROUBLE, TROUBLE) says "It doesn't halt" (False), then TROUBLE should halt (Step 4). Contradiction.
Because we reached a logical contradiction, our initial assumption—that HALT exists—must be false. Therefore, a perfect Halting Detector is impossible to write in any Turing-complete language, including Python.
4.2.7 Other Undecidable Problems
- Rice's Theorem: Any non-trivial property of a program (e.g., "Does this function ever return a value greater than 5?" or "Does this program contain a security vulnerability?") is undecidable.
- Post Correspondence Problem: A simple matching problem involving strings that is undecidable.
- Equivalence Problem: "Do these two given programs always produce the same output for all inputs?" is undecidable.
4.3 Computational Complexity: How Fast Can We Solve It?
Assuming a problem is decidable, the next question is: How much time and memory does it take to solve it? This is the domain of computational complexity.
4.3.1 Measuring Resources: Time and Space
- Time Complexity: The number of elementary operations (e.g., arithmetic, comparisons, memory access) the algorithm performs as a function of the input size
n.
- Space Complexity: The amount of memory the algorithm uses as a function of the input size
n.
We usually focus on worst-case complexity (the scenario that takes the longest), because we need guarantees for our users.
4.3.2 Big-O Notation (Asymptotic Analysis)
To compare algorithms, we ignore the constant factors and the speed of the hardware. We look at how the runtime scales as n (input size) grows to infinity. This is expressed using Big-O notation.
| Notation |
Name |
Description |
Example in Python |
| O(1) |
Constant |
Runtime is independent of input size. |
Accessing a list element by index: my_list[i] |
| O(log n) |
Logarithmic |
Runtime grows slowly as input grows. |
Binary search on a sorted list. |
| O(n) |
Linear |
Runtime scales directly with input size. |
Searching for an item in an unsorted list (loop once). |
| O(n log n) |
Linearithmic |
Slightly worse than linear. |
Efficient sorting algorithms (Merge Sort, QuickSort). |
| O(n²) |
Quadratic |
Runtime squares as input grows. |
Nested loops (e.g., comparing every item in a list to every other). |
| O(2^n) |
Exponential |
Runtime doubles with each new input element. |
Recursive calculation of Fibonacci (naive), solving the subset-sum problem. |
| O(n!) |
Factorial |
Runtime explodes instantly. |
Solving the Traveling Salesman Problem via brute force (checking all permutations). |
Scaling Visualization (n = 100):
- O(1): 1 operation
- O(log n): ~7 operations
- O(n): 100 operations
- O(n log n): ~664 operations
- O(n²): 10,000 operations
- O(2^n): 1.27 × 10³⁰ operations (Impossible for n=100)
4.3.3 Tractability: Easy vs. Hard
- Tractable: Problems that can be solved in polynomial time (e.g., O(n), O(n²), O(n³)). We consider these "efficient" or "easy" because as input grows, the runtime grows reasonably.
- Intractable: Problems that require super-polynomial time (exponential O(2^n) or factorial O(n!)). These are "hard" because for even moderate input sizes (n=100), the runtime surpasses the age of the universe.
4.3.4 Complexity Classes: P, NP, NP-Complete, and NP-Hard
Formal complexity theory classifies decision problems into classes:
4.3.5 The $1,000,000 Question: P vs. NP
The question "Does P equal NP?" is one of the Millennium Prize Problems (offering $1,000,000). Most computer scientists believe P ≠ NP.
- If P = NP: Suddenly, every problem whose solution can be quickly verified can also be quickly solved. This would break modern cryptography (RSA, AES), revolutionize logistics, drug design, and AI. It would be a scientific revolution.
- If P ≠ NP (as widely suspected): There will always be problems that are easy to check but hard to find. We must rely on heuristics, approximations, and quantum computing for certain tasks.
4.3.6 The Hidden Complexity in Python
Python abstracts complexity, but the underlying algorithms still obey these rules:
- Sorting: Python's
list.sort() uses Timsort—O(n log n) average. This is optimal for comparison-based sorting.
- Dictionary Lookup (
dict): Average O(1), but worst-case O(n) due to hash collisions.
- Nested Loops: Writing nested loops is the most common way to accidentally create O(n²) or worse complexity in your code. Always analyze your loops.
4.4 Summary Table
| Aspect |
Computability Theory |
Computational Complexity Theory |
| Core Question |
Can we write an algorithm to solve this? |
How fast can we solve it? |
| Focus |
Halting, Decidability. |
Runtime, Memory usage. |
| Key Concept |
Turing Machine, Church-Turing Thesis. |
Big-O Notation, P vs NP. |
| Classic Example |
Halting Problem (Undecidable). |
Traveling Salesman Problem (NP-Hard). |
| Practical Implication |
Some bugs (infinite loops) are impossible to detect automatically. |
Some programs cannot run in a reasonable time for large inputs. |
4.5 Quizzes
Quiz 1: Computability Theory
1. Which of the following is the primary purpose of a Turing Machine?
- (A) To be the fastest physical computer ever built.
- (B) To serve as a mathematical model for defining what is computable.
- (C) To store large amounts of data efficiently.
- (D) To network different computers together.
Answer
(B) To serve as a mathematical model for defining what is computable.
2. The Church-Turing Thesis states that:
- (A) Python is faster than C.
- (B) Every effectively calculable function can be computed by a Turing Machine.
- (C) The Halting Problem is solvable on a quantum computer.
- (D) P equals NP.
Answer
(B) Every effectively calculable function can be computed by a Turing Machine.
3. An "Undecidable" problem is one that:
- (A) Takes a very long time to solve (millions of years).
- (B) Has no algorithm that can solve it correctly for all possible inputs.
- (C) Cannot be written in the Python programming language.
- (D) Requires more than 1 GB of memory to run.
Answer
(B) Has no algorithm that can solve it correctly for all possible inputs.
4. What conclusion do we draw from the Halting Problem?
- (A) It is impossible to write a perfect debugger that detects all infinite loops.
- (B) All programs eventually halt.
- (C) Infinite loops only occur in low-level languages like C.
- (D) We can test for infinite loops if we run the program enough times.
Answer
(A) It is impossible to write a perfect debugger that detects all infinite loops.
5. In the proof of the Halting Problem's undecidability, what does the TROUBLE program do if the hypothetical HALT function says that TROUBLE(TROUBLE) would halt?
- (A) It prints "Hello World."
- (B) It halts immediately.
- (C) It deliberately goes into an infinite loop.
- (D) It deletes the operating system.
Answer
(C) It deliberately goes into an infinite loop.
Quiz 2: Complexity Theory and Big-O
6. What is the time complexity of accessing the first element my_list[0] in a Python list?
- (A) O(1)
- (B) O(n)
- (C) O(log n)
- (D) O(n²)
Answer
(A) O(1)
7. You have an algorithm with O(n²) complexity. If it takes 1 second to process 100 items, roughly how long will it take to process 1000 items?
- (A) 10 seconds
- (B) 100 seconds
- (C) 1000 seconds
- (D) 1 second
Answer
(B) 100 seconds (Since 1000 is 10 times larger, 10² = 100 times slower. 1s * 100 = 100s).
8. Which of the following functions grows the fastest as n approaches infinity?
- (A) O(n log n)
- (B) O(n!)
- (C) O(2^n)
- (D) O(n²)
Answer
(B) O(n!)
9. An algorithm that halves the input size at each step (like binary search) is typically:
- (A) O(1)
- (B) O(n)
- (C) O(log n)
- (D) O(n log n)
Answer
(C) O(log n)
Quiz 3: Complexity Classes (P, NP)
10. Which of the following is a characteristic of problems in Class P?
- (A) They require exponential time to solve.
- (B) They are solvable in polynomial time on a deterministic computer.
- (C) They are impossible to verify even if a solution is given.
- (D) They are all NP-Complete.
Answer
(B) They are solvable in polynomial time on a deterministic computer.
11. Which of the following is the classic definition of a problem in Class NP?
- (A) It requires a nondeterministic machine to solve.
- (B) A proposed solution can be verified in polynomial time.
- (C) It can never be solved faster than O(2^n).
- (D) It is impossible to solve.
Answer
(B) A proposed solution can be verified in polynomial time.
12. If a problem is NP-Complete, and we find a polynomial-time algorithm for it, which is true?
- (A) We will have proven that P = NP.
- (B) We will have proven that P ≠ NP.
- (C) Only that specific problem becomes solvable.
- (D) It will have no effect on the rest of computer science.
Answer
(A) We will have proven that P = NP.
13. Which of the following is considered an NP-Hard problem?
- (A) Finding the maximum value in a list.
- (B) Sorting a list of numbers.
- (C) Finding the shortest route that visits every city exactly once (Traveling Salesman - Optimization).
- (D) Looking up a word in a dictionary.
Answer
(C) Finding the shortest route that visits every city exactly once (Traveling Salesman - Optimization).
14. Sorting a list of integers using Python's Timsort has what time complexity in the average case?
- (A) O(n)
- (B) O(n log n)
- (C) O(n²)
- (D) O(2^n)
Answer
(B) O(n log n)
15. The "Subset-Sum" problem (given a set of integers, is there a subset summing to zero?) is in which class?
- (A) P (Polynomial time)
- (B) NP-Complete
- (C) Undecidable
- (D) O(1)
Answer
(B) NP-Complete
4.6 Exercises
Exercise 1: Simulating a Simple Turing Machine
Instructions: You are given a Turing Machine that adds 1 to a binary number. The tape starts with a binary number written on it (e.g., 101). The head starts at the rightmost (least significant) digit. The machine has the following rules (states: q0 = start, q1 = carry, q_accept = halt):
- Rule 1 (q0, 0): Write 1, Move Left, Go to q_accept. (Found a 0, change to 1, done).
- Rule 2 (q0, 1): Write 0, Move Left, Go to q1. (Found a 1, flip to 0, carry the 1).
- Rule 3 (q0, _) [Blank]: Write 1, Stay, Go to q_accept. (Overflow, e.g., 111 + 1 = 1000).
- Rule 4 (q1, 0): Write 1, Move Left, Go to q_accept.
- Rule 5 (q1, 1): Write 0, Move Left, Go to q1. (Keep carrying).
- Rule 6 (q1, _): Write 1, Stay, Go to q_accept.
Task: Trace the execution for the input 101 (binary for 5). Show the state of the tape and the head position after each step. What is the final binary number on the tape?
Answer
Start: Tape `_ 1 0 1 _`. Head over the rightmost '1'. State q0.
1. Read '1'. Rule 2: Write '0', Move Left, Go to q1. Tape: `_ 1 0 0 _`. Head over '0' (middle). State q1.
2. Read '0'. Rule 4: Write '1', Move Left, Go to q_accept. Tape: `_ 1 1 0 _`. Head over '1' (leftmost). State q_accept. Halt.
Final tape reads `110` which is binary for 6. (5+1=6). Correct!
Exercise 2: Big-O Ranking
Instructions: Rank the following functions from slowest growing (most efficient) to fastest growing (least efficient) for large input sizes n:
n², n!, 1, n log n, n³, 2^n, n, log n
Answer
Slowest (Efficient) to Fastest (Inefficient):
1. 1 (O(1))
2. log n (O(log n))
3. n (O(n))
4. n log n (O(n log n))
5. n² (O(n²))
6. n³ (O(n³))
7. 2^n (O(2^n))
8. n! (O(n!))
Exercise 3: Categorizing Python Code by Complexity
Instructions: For each of the following Python code snippets, identify the time complexity using Big-O notation (assume n = len(arr)). Briefly explain your reasoning.
Snippet A:
def find_max(arr):
max_val = arr[0]
for num in arr:
if num > max_val:
max_val = num
return max_val
Snippet B:
def contains_duplicate(arr):
for i in range(len(arr)):
for j in range(i + 1, len(arr)):
if arr[i] == arr[j]:
return True
return False
Snippet C:
def binary_search(arr, target):
low = 0
high = len(arr) - 1
while low <= high:
mid = (low + high) // 2
if arr[mid] == target:
return True
elif arr[mid] < target:
low = mid + 1
else:
high = mid - 1
return False
Answers
- **Snippet A**: O(n) (Linear). It loops through the list exactly once.
- **Snippet B**: O(n²) (Quadratic). It is a nested loop. The inner loop runs roughly `n²/2` times. We drop the constant `1/2`, so O(n²).
- **Snippet C**: O(log n) (Logarithmic). The search space is halved in each iteration of the while loop.
Exercise 4: Decidable vs. Undecidable Classification
Instructions: Classify each of the following problems as Decidable (solvable by an algorithm) or Undecidable (no algorithm exists). Justify your answer in one sentence.
- Determining if a given Python program contains the word
print.
- Determining if a given Python program will print "Hello" to the console.
- Determining if a given Python program will eventually run out of memory.
- Determining if a given number is a perfect square.
- Determining if a given Python program is a virus.
Answers
1. **Decidable**. This is a simple string search, solved by regex or `in` operator.
2. **Undecidable**. Rice's Theorem states that any non-trivial property of a program's output is undecidable.
3. **Undecidable**. Running out of memory is a runtime property related to termination, reducible to the Halting Problem.
4. **Decidable**. We can use Newton's method or integer square root algorithms—polynomial time.
5. **Undecidable**. Determining malicious intent (semantic property) is a non-trivial property of the program's behavior, hence undecidable by Rice's Theorem.
4.7 Homework Questions
Answer the following questions in complete sentences. Each response should be 3–5 sentences unless otherwise specified.
Short Answer Questions
1. Explain in your own words why the Halting Problem is considered undecidable. Use the concept of the "troublemaker" program in your explanation.
Sample Answer
The Halting Problem is undecidable because if we assume a perfect Halting detector exists, we can construct a 'troublemaker' program that intentionally does the opposite of what the detector predicts. If the detector says it halts, the troublemaker loops forever; if the detector says it loops, the troublemaker halts. Feeding this troublemaker to itself creates a logical contradiction, proving that such a detector cannot exist.
2. What is the practical implication of the Church-Turing Thesis for a Python programmer?
Sample Answer
The Church-Turing Thesis implies that Python, being Turing-complete, is theoretically as powerful as any other programming language or computing device. Any algorithm that can be expressed logically can be implemented in Python, provided it is actually computable. However, it also means Python is subject to the same fundamental limitations—it cannot solve undecidable problems any more than any other language can.
3. Distinguish between a problem being "Undecidable" and a problem being "NP-Complete."
Sample Answer
An undecidable problem has no algorithmic solution at all, meaning we cannot write a program that always gives a correct Yes/No answer. An NP-Complete problem is decidable, meaning an algorithm exists that will eventually produce the correct answer, but the best-known algorithms require exponential time in the worst case, making them infeasible for large inputs.
4. Why is verifying a solution often easier than finding it? Provide an example from the NP class.
Sample Answer
Verification is easier because you already have a candidate solution to test, which usually requires checking the candidate against the constraints of the problem. For example, in the Subset-Sum problem, finding a subset that sums to zero is hard, but if someone gives you the subset, you can simply add up the numbers and confirm the sum in linear time.
5. A student writes a Python program with two nested loops. The outer loop runs n times, and the inner loop runs n times. The student argues it is O(n) because "they are just loops." What is wrong with this reasoning, and what is the correct complexity?
Sample Answer
The student has overlooked the multiplicative effect of nested loops. For each iteration of the outer loop, the inner loop executes completely, resulting in `n * n = n²` total executions. The correct complexity is O(n²), not O(n).
Essay Questions
Answer the following questions in 300–500 words each.
6. Imagine you are working for a company that needs to solve the Traveling Salesperson Problem (TSP) for a logistics route involving 200 cities. Given what you know about computational complexity, discuss why a brute-force algorithm (checking every permutation) is impossible. Discuss alternative approaches (heuristics, approximations) and how complexity theory guides your software architecture decisions.
Suggested outline:
- Introduction: TSP and its NP-Hard nature.
- Brute-Force: O(n!) complexity. For n=200, it exceeds the age of the universe.
- Why exact solutions are infeasible: Unless P=NP, no polynomial-time algorithm exists for the exact solution.
- Practical alternatives:
- Heuristics (Nearest Neighbor, 2-opt) that give "good enough" paths quickly.
- Approximation algorithms that guarantee a path within 1.5x the optimal (Christofides algorithm).
- Use of Machine Learning/Reinforcement Learning to learn patterns.
- Conclusion: Complexity theory tells us not to waste time writing a brute-force exact solver for n=200, guiding us toward heuristics.
7. Explain the significance of the P vs. NP problem in the context of modern cybersecurity. Why would most cybersecurity experts prefer that P ≠ NP?
Suggested outline:
- Introduction: What P vs NP means for encryption.
- Modern Cryptography (RSA, AES, Hashing): Relies on the fact that factoring large numbers and reversing hashes is hard (presumably NP-hard or at least outside P).
- If P = NP: An attacker could factor large numbers in polynomial time, break RSA, find pre-images for passwords, and reverse any one-way function. Digital signatures would become useless. The internet infrastructure would collapse overnight.
- If P ≠ NP: The mathematical foundations of current encryption remain secure for the foreseeable future (subject to quantum computing).
- Conclusion: Cybersecurity relies on the assumption that problems are hard to solve but easy to verify.
Research Questions
These questions require additional research beyond the tutorial content.
8. Research the concept of "Gödel's Incompleteness Theorems." How are these theorems philosophically related to the Halting Problem and Turing's proof of undecidability? (Hint: Consider the concept of self-reference and diagonalization).
9. Research the exact nature of the "Cook-Levin Theorem." Why was the Boolean Satisfiability Problem (SAT) chosen as the first NP-Complete problem, and how does the reduction process (polynomial-time reduction) work in theory?
10. Research the current state of quantum computing. How do quantum algorithms (like Shor's algorithm and Grover's algorithm) affect complexity classes? Specifically, does solving a problem in polynomial time on a quantum computer prove that P = NP? (Hint: Look at the definition of BQP - Bounded Quantum Polynomial Time).
Previous | Tutorial index | Next