Phase 2.1

Deutsch-Jozsa Algorithm

Your first quantum algorithm - cleaner than any classical solution

🎯 Why Learn This First?

Deutsch-Jozsa is the simplest quantum algorithm that beats classical computing. It's like the "Hello World" of quantum algorithms.

Classical Approach

Query function multiple times (worst case: half the inputs + 1)

⏱️ O(2^(n-1) + 1) queries

Quantum Approach

Query function exactly once using superposition

⚑ 1 query (guaranteed)

Key Insight: This is not about speedβ€”it's about query complexity. You ask the question once and get the answer with certainty.

❓ The Problem We're Solving

You're given a black box function f(x) that takes an n-bit input and returns 0 or 1.

The function is guaranteed to be one of two types:

Constant Function

Returns the same output for all inputs

f(0) = 0, f(1) = 0, f(2) = 0, ... β†’ all 0s
f(0) = 1, f(1) = 1, f(2) = 1, ... β†’ all 1s

Balanced Function

Returns 0 for half the inputs, 1 for the other half

f(0) = 0, f(1) = 0, f(2) = 1, f(3) = 1
(exactly 50% zeros, 50% ones)

Your Task: Determine whether f is constant or balanced.

πŸ’» Classical Solution (Why It's Slow)

Without quantum tricks, you have to query the function multiple times:

// For 3-bit input (8 possible values)
results = []
for i in range(5):  # Need to check 5 values minimum
    results.append(f(i))

if all_same(results):
    return "Constant"
else:
    return "Balanced"

Why 5 queries? If you see 4 zeros, the 5th could still be 0 (constant) or 1 (balanced). You need at least half + 1 checks.

Scaling Problem: For n bits, worst case = 2^(n-1) + 1 queries
n=10 bits β†’ 513 queries
n=20 bits β†’ 524,289 queries

βš›οΈ Quantum Solution (The Elegant Way)

The quantum algorithm queries f exactly once using superposition.

Step-by-Step Circuit

  1. Initialize: Start with |0⟩ on n input qubits and |1⟩ on 1 output qubit
    |Οˆβ‚€βŸ© = |0⟩ⁿ |1⟩
  2. Apply Hadamard to all qubits: Create superposition
    |Οˆβ‚βŸ© = HβŠ—βΏβΊΒΉ |Οˆβ‚€βŸ©
         = (1/√2ⁿ) Ξ£β‚“ |x⟩ Β· (|0⟩-|1⟩)/√2

    Now you're querying all possible inputs simultaneously

  3. Apply the black box function UαΆ : Evaluate f(x) for all x at once
    |Οˆβ‚‚βŸ© = UαΆ  |Οˆβ‚βŸ©
         = (1/√2ⁿ) Ξ£β‚“ (-1)^f(x) |x⟩

    This encodes f(x) as a phase (not a classical value)

  4. Apply Hadamard again to input qubits: Extract global pattern
    |Οˆβ‚ƒβŸ© = HβŠ—βΏ |Οˆβ‚‚βŸ©
  5. Measure input qubits:
    • If all qubits are |0⟩ β†’ function is constant
    • If any qubit is |1⟩ β†’ function is balanced
Why This Works: The second Hadamard layer acts as an "interference filter." For constant functions, all paths constructively interfere at |0⟩ⁿ. For balanced functions, they interfere destructively, scattering to other states.

🎨 Visual Intuition

Think of it like this:

Classical: Sampling Strategy

πŸ” Check box 1 β†’ check box 2 β†’ check box 3...

You're peeking into individual boxes one by one.

Quantum: Global Pattern Detection

🌊 Shake all boxes at once β†’ listen to resonance

If they all vibrate in sync β†’ constant

If they cancel out β†’ balanced

The quantum computer doesn't look at individual outputs. It detects the global symmetry of the function.

πŸ’» Implement It Yourself (Qiskit)

Here's the complete working code:

from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.primitives import Sampler

def deutsch_jozsa(n, oracle):
    """
    n = number of input bits
    oracle = function that adds the black box to the circuit
    """
    # Create circuit
    qr = QuantumRegister(n + 1, 'q')
    cr = ClassicalRegister(n, 'c')
    qc = QuantumCircuit(qr, cr)
    
    # Step 1: Initialize output qubit to |1⟩
    qc.x(qr[n])
    
    # Step 2: Apply Hadamard to all qubits
    for i in range(n + 1):
        qc.h(qr[i])
    
    qc.barrier()
    
    # Step 3: Apply the oracle (black box)
    oracle(qc, qr)
    
    qc.barrier()
    
    # Step 4: Apply Hadamard to input qubits
    for i in range(n):
        qc.h(qr[i])
    
    # Step 5: Measure input qubits
    for i in range(n):
        qc.measure(qr[i], cr[i])
    
    return qc

# Example: Constant function (always returns 0)
def constant_oracle(qc, qr):
    # Do nothing - this means f(x) = 0 for all x
    pass

# Example: Balanced function (returns xβ‚€ XOR x₁)
def balanced_oracle(qc, qr):
    qc.cx(qr[0], qr[2])  # CNOT from first input to output
    qc.cx(qr[1], qr[2])  # CNOT from second input to output

# Run the algorithm
n = 2  # 2 input bits
circuit = deutsch_jozsa(n, balanced_oracle)

# Execute
sampler = Sampler()
job = sampler.run(circuit, shots=1)
result = job.result()
counts = result.quasi_dists[0]

# Interpret result
measured = max(counts, key=counts.get)
if measured == 0:
    print("Function is CONSTANT")
else:
    print("Function is BALANCED")
Try it: Replace balanced_oracle with constant_oracle and run again. You'll always get the correct answer in 1 shot!

🎯 When Does This Actually Matter?

Real-World Applications (Rare)

Honestly? Deutsch-Jozsa is mostly a teaching tool. The problem it solves is artificial.

However, the techniques it uses appear in:

  • βœ… Simon's Algorithm (finds hidden periodicities)
  • βœ… Bernstein-Vazirani (recovers hidden bit strings)
  • βœ… Grover's Algorithm (unstructured search)

What You Actually Learn

1. Phase Kickback

How to encode information in phases instead of amplitudes

2. Quantum Interference

Using Hadamard transforms to extract global patterns

3. Oracle-Based Algorithms

How to build circuits around black-box functions

Bottom line: Master this, and Grover/Shor will make much more sense.

⚠️ Common Mistakes

❌ Mistake 1: "Quantum computer evaluates all inputs"

Wrong. It doesn't evaluate f(0), f(1), f(2)... separately.

Correct: It evaluates the global interference pattern of all f(x) simultaneously.

❌ Mistake 2: "This proves quantum is always faster"

Wrong. This is about query complexity, not runtime.

Correct: For many problems, classical algorithms are still better (e.g., sorting, matrix multiplication).

❌ Mistake 3: "I can use this to speed up database searches"

Wrong. Real databases don't have "balanced vs constant" structure.

Correct: For unstructured search, use Grover's algorithm (next lesson).

πŸ› οΈ Practice Exercises

Exercise 1: Constant Oracle

Task: Create an oracle for a constant function that always returns 1.

Show Solution
def constant_one_oracle(qc, qr):
    # Flip the output qubit
    qc.x(qr[-1])  # Now f(x) = 1 for all x

Exercise 2: Balanced Oracle

Task: Create an oracle for f(x) = xβ‚€ (just copy the first input bit).

Show Solution
def copy_first_bit_oracle(qc, qr):
    qc.cx(qr[0], qr[-1])  # CNOT: if xβ‚€=1, flip output

Exercise 3: Test Your Understanding

Question: For n=3 bits, what's the classical worst-case vs quantum queries?

Show Answer

Classical: 2^(3-1) + 1 = 5 queries

Quantum: 1 query

Savings: 5Γ— reduction (modest for small n, exponential for large n)

πŸš€ What's Next?

You've learned:

  • βœ… How quantum algorithms use superposition + interference
  • βœ… Phase kickback technique
  • βœ… Building circuits with oracles

Next up: Grover's Search Algorithm β€” the most practical quantum algorithm for real-world problems.