Skip to content

Module 3: Introduction to Quantum Algorithms

🎓 Learning Objectives

  • Master Qiskit programming for quantum circuits
  • Implement Deutsch-Jozsa algorithm
  • Implement Bernstein-Vazirani algorithm
  • Implement Simon's algorithm
  • Understand quantum advantage in these algorithms

This module introduces fundamental quantum algorithms that demonstrate quantum advantage. You'll learn to implement these algorithms using Qiskit and understand when quantum computing provides speedup over classical methods.

Module Overview

These algorithms show that quantum computers can solve certain problems faster than classical computers. Understanding them is crucial for quantum algorithm design.

Qiskit Programming

Qiskit is IBM's open-source quantum computing framework. Let's master the essentials.

Installation and Setup

# Install Qiskit
# pip install qiskit qiskit-aer qiskit-visualization

from qiskit import QuantumCircuit, Aer, execute, IBMQ
from qiskit.visualization import plot_histogram, plot_circuit, plot_bloch_multivector
from qiskit.quantum_info import Statevector, Operator
import numpy as np
import matplotlib.pyplot as plt

print("Qiskit version:", qiskit.__version__)

Basic Circuit Creation

# Create a quantum circuit
qc = QuantumCircuit(2, 2)  # 2 qubits, 2 classical bits

# Apply gates
qc.h(0)      # Hadamard on qubit 0
qc.cx(0, 1)  # CNOT: control=0, target=1
qc.measure([0, 1], [0, 1])  # Measure qubits to classical bits

# Visualize
print("Circuit:")
print(qc.draw())

# Execute on simulator
simulator = Aer.get_backend('qasm_simulator')
job = execute(qc, simulator, shots=1000)
result = job.result()
counts = result.get_counts(qc)

print(f"\nResults: {counts}")
plot_histogram(counts)

Advanced Qiskit Features

# Statevector simulation
simulator_sv = Aer.get_backend('statevector_simulator')
job = execute(qc, simulator_sv)
result = job.result()
statevector = result.get_statevector()
print(f"Statevector: {statevector}")

# Unitary simulation
simulator_unitary = Aer.get_backend('unitary_simulator')
job = execute(qc, simulator_unitary)
result = job.result()
unitary = result.get_unitary()
print(f"Unitary matrix shape: {unitary.shape}")

# Custom initial states
qc_custom = QuantumCircuit(1)
qc_custom.initialize([1/np.sqrt(2), 1j/np.sqrt(2)], 0)  # Custom state

Qiskit Backends

  • qasm_simulator: Simulates measurements (returns counts)
  • statevector_simulator: Returns full quantum state
  • unitary_simulator: Returns unitary matrix
  • Real hardware: Access via IBM Quantum

Deutsch-Jozsa Algorithm

The Deutsch-Jozsa algorithm determines if a function is constant or balanced using only one query (vs 2ⁿ⁻¹+1 classical queries).

Problem Statement

Given a function f: {0,1}ⁿ → {0,1} that is either: - Constant: f(x) = 0 for all x OR f(x) = 1 for all x - Balanced: f(x) = 0 for exactly half the inputs, f(x) = 1 for the other half

Determine which type f is.

Classical vs Quantum

def classical_deutsch_jozsa(f, n):
    """Classical algorithm: requires 2^(n-1) + 1 queries"""
    results = []
    for i in range(2**(n-1) + 1):
        x = format(i, f'0{n}b')
        results.append(f(x))

    if all(r == results[0] for r in results):
        return "constant"
    else:
        return "balanced"

# Quantum: only 1 query needed!

Quantum Implementation

def deutsch_jozsa_oracle(f_type='constant', n=3):
    """Create oracle for Deutsch-Jozsa algorithm"""
    qc = QuantumCircuit(n+1)  # n input qubits + 1 ancilla

    if f_type == 'constant':
        # Constant function: f(x) = 0 or f(x) = 1
        # For f(x) = 1, flip ancilla
        qc.x(n)  # f(x) = 1 for all x
    else:
        # Balanced function: f(x) = x₀ ⊕ x₁ ⊕ ... ⊕ xₙ₋₁
        for i in range(n):
            qc.cx(i, n)  # XOR into ancilla

    return qc

def deutsch_jozsa_algorithm(f_type='constant', n=3):
    """Implement Deutsch-Jozsa algorithm"""
    qc = QuantumCircuit(n+1, n)

    # Initialize ancilla in |1⟩
    qc.x(n)

    # Apply Hadamard to all qubits
    for i in range(n+1):
        qc.h(i)

    # Apply oracle
    oracle = deutsch_jozsa_oracle(f_type, n)
    qc = qc.compose(oracle)

    # Apply Hadamard to input qubits
    for i in range(n):
        qc.h(i)

    # Measure input qubits
    qc.measure(range(n), range(n))

    return qc

# Test constant function
qc_constant = deutsch_jozsa_algorithm('constant', n=3)
print("Deutsch-Jozsa: Constant Function")
print(qc_constant.draw())

simulator = Aer.get_backend('qasm_simulator')
job = execute(qc_constant, simulator, shots=1000)
result = job.result()
counts = result.get_counts(qc_constant)
print(f"\nResults: {counts}")
print("All zeros → constant function")

# Test balanced function
qc_balanced = deutsch_jozsa_algorithm('balanced', n=3)
print("\nDeutsch-Jozsa: Balanced Function")
job = execute(qc_balanced, simulator, shots=1000)
result = job.result()
counts = result.get_counts(qc_balanced)
print(f"Results: {counts}")
print("Non-zero result → balanced function")

Understanding the Algorithm

def deutsch_jozsa_explanation():
    """Step-by-step explanation"""
    print("Deutsch-Jozsa Algorithm Steps:")
    print("1. Initialize: |0⟩ⁿ|1⟩")
    print("2. Apply H⊗ⁿ⁺¹: Creates superposition")
    print("3. Apply oracle: |x⟩|y⟩ → |x⟩|y ⊕ f(x)⟩")
    print("4. Apply H⊗ⁿ to input: Interference happens")
    print("5. Measure: |00...0⟩ → constant, else → balanced")
    print("\nQuantum Advantage:")
    print("- Classical: 2^(n-1) + 1 queries")
    print("- Quantum: 1 query")
    print("- Exponential speedup!")

deutsch_jozsa_explanation()

Quantum Advantage

Deutsch-Jozsa shows exponential speedup: - Classical: Need to check 2ⁿ⁻¹ + 1 inputs - Quantum: Only 1 query needed - Speedup: Exponential (2ⁿ⁻¹ vs 1)

Bernstein-Vazirani Algorithm

The Bernstein-Vazirani algorithm finds a hidden string s using only 1 query (vs n classical queries).

Problem Statement

Given a function f(x) = s·x (mod 2) where s is a hidden string, find s.

Implementation

def bernstein_vazirani_oracle(s):
    """Create oracle for Bernstein-Vazirani"""
    n = len(s)
    qc = QuantumCircuit(n+1)

    # f(x) = s·x mod 2
    # Apply CNOT from qubit i to ancilla if s[i] = 1
    for i, bit in enumerate(s):
        if bit == '1':
            qc.cx(i, n)

    return qc

def bernstein_vazirani_algorithm(s):
    """Implement Bernstein-Vazirani algorithm"""
    n = len(s)
    qc = QuantumCircuit(n+1, n)

    # Initialize ancilla in |1⟩
    qc.x(n)

    # Apply Hadamard to all qubits
    for i in range(n+1):
        qc.h(i)

    # Apply oracle
    oracle = bernstein_vazirani_oracle(s)
    qc = qc.compose(oracle)

    # Apply Hadamard to input qubits
    for i in range(n):
        qc.h(i)

    # Measure input qubits
    qc.measure(range(n), range(n))

    return qc

# Test with hidden string s = "101"
hidden_string = "101"
qc_bv = bernstein_vazirani_algorithm(hidden_string)
print("Bernstein-Vazirani Algorithm")
print(f"Hidden string: {hidden_string}")
print(qc_bv.draw())

simulator = Aer.get_backend('qasm_simulator')
job = execute(qc_bv, simulator, shots=1000)
result = job.result()
counts = result.get_counts(qc_bv)
print(f"\nResults: {counts}")
print(f"Measured string: {max(counts, key=counts.get)}")
print("Should match hidden string!")

Classical vs Quantum

def classical_bernstein_vazirani(n):
    """Classical algorithm: requires n queries"""
    s = ""
    for i in range(n):
        # Query with input that has 1 only at position i
        x = ['0'] * n
        x[i] = '1'
        x_str = ''.join(x)
        # In real scenario, would call oracle
        # result = oracle(x_str)
        # s += str(result)
    return s

print("Bernstein-Vazirani Comparison:")
print(f"Classical: {n} queries needed")
print(f"Quantum: 1 query needed")
print(f"Speedup: {n}x")

Bernstein-Vazirani Insight

The algorithm works because: 1. Superposition explores all inputs simultaneously 2. Oracle encodes s in the phase 3. Hadamard transforms phase to amplitude 4. Measurement reveals s directly

Simon's Algorithm

Simon's algorithm finds a hidden period s such that f(x) = f(x ⊕ s) for all x.

Problem Statement

Given a function f: {0,1}ⁿ → {0,1}ⁿ such that f(x) = f(y) if and only if y = x or y = x ⊕ s, find the hidden string s.

Implementation

def simon_oracle(s):
    """Create oracle for Simon's algorithm"""
    n = len(s)
    qc = QuantumCircuit(2*n)

    # Simplified oracle: f(x) = x ⊕ s if x < 2^(n-1), else x
    # In practice, this would be a black box
    for i in range(n):
        if s[i] == '1':
            for j in range(n):
                qc.cx(i, n+j)

    return qc

def simon_algorithm(s, n_rounds=2):
    """Implement Simon's algorithm"""
    n = len(s)
    qc = QuantumCircuit(2*n, n)

    # Apply Hadamard to first n qubits
    for i in range(n):
        qc.h(i)

    # Apply oracle
    oracle = simon_oracle(s)
    qc = qc.compose(oracle)

    # Apply Hadamard to first n qubits again
    for i in range(n):
        qc.h(i)

    # Measure first n qubits
    qc.measure(range(n), range(n))

    return qc

# Test with hidden string s = "11"
hidden_string = "11"
qc_simon = simon_algorithm(hidden_string)
print("Simon's Algorithm")
print(f"Hidden string: {hidden_string}")
print(qc_simon.draw())

simulator = Aer.get_backend('qasm_simulator')
job = execute(qc_simon, simulator, shots=1000)
result = job.result()
counts = result.get_counts(qc_simon)
print(f"\nResults: {counts}")

# Collect measurements and solve linear system
measurements = list(counts.keys())[:n]
print(f"Measurements: {measurements}")
print("Solve: s·y = 0 mod 2 for all measurements y")

Solving for Hidden String

def solve_simon(measurements):
    """Solve linear system to find hidden string"""
    import numpy as np
    from scipy.linalg import null_space

    # Convert measurements to matrix
    n = len(measurements[0])
    A = []
    for m in measurements:
        row = [int(bit) for bit in m]
        A.append(row)

    A = np.array(A)
    print(f"Linear system matrix:\n{A}")

    # Find null space (solutions to Ax = 0)
    # In practice, use Gaussian elimination mod 2
    null = null_space(A)
    print(f"Null space: {null}")

    return null

# Example usage
measurements = ['01', '10']  # Example measurements
solution = solve_simon(measurements)

Simon's Algorithm Complexity

  • Classical: O(2^(n/2)) queries (birthday paradox)
  • Quantum: O(n) queries
  • Speedup: Exponential

Complete Example: Comparing All Algorithms

def compare_algorithms():
    """Compare all three algorithms"""
    print("Algorithm Comparison:")
    print("\n1. Deutsch-Jozsa:")
    print("   Problem: Constant or balanced function?")
    print("   Classical: 2^(n-1) + 1 queries")
    print("   Quantum: 1 query")
    print("   Speedup: Exponential")

    print("\n2. Bernstein-Vazirani:")
    print("   Problem: Find hidden string s")
    print("   Classical: n queries")
    print("   Quantum: 1 query")
    print("   Speedup: n-fold")

    print("\n3. Simon's:")
    print("   Problem: Find hidden period s")
    print("   Classical: O(2^(n/2)) queries")
    print("   Quantum: O(n) queries")
    print("   Speedup: Exponential")

compare_algorithms()

Practice Exercises

Exercise 1: Implement Deutsch-Jozsa

# Create a Deutsch-Jozsa oracle for a 4-qubit balanced function
# where f(x) = x₀ ⊕ x₁ ⊕ x₂ ⊕ x₃

def exercise_deutsch_jozsa():
    qc = QuantumCircuit(5, 4)  # 4 input + 1 ancilla

    # Your code here
    qc.x(4)  # Initialize ancilla
    for i in range(5):
        qc.h(i)

    # Balanced oracle: XOR all inputs
    for i in range(4):
        qc.cx(i, 4)

    for i in range(4):
        qc.h(i)

    qc.measure(range(4), range(4))

    return qc

qc_ex = exercise_deutsch_jozsa()
simulator = Aer.get_backend('qasm_simulator')
job = execute(qc_ex, simulator, shots=1000)
result = job.result()
counts = result.get_counts(qc_ex)
print("Exercise 1 Results:", counts)

Exercise 2: Bernstein-Vazirani Variant

# Find hidden string s = "1101" using Bernstein-Vazirani

def exercise_bernstein_vazirani():
    s = "1101"
    n = len(s)
    qc = QuantumCircuit(n+1, n)

    # Your code here
    qc.x(n)
    for i in range(n+1):
        qc.h(i)

    for i, bit in enumerate(s):
        if bit == '1':
            qc.cx(i, n)

    for i in range(n):
        qc.h(i)

    qc.measure(range(n), range(n))

    return qc

qc_ex2 = exercise_bernstein_vazirani()
job = execute(qc_ex2, simulator, shots=1000)
result = job.result()
counts = result.get_counts(qc_ex2)
print("Exercise 2 Results:", counts)
print("Expected: 1101")

Key Takeaways

  • Qiskit provides powerful tools for quantum programming
  • Deutsch-Jozsa shows exponential speedup for function classification
  • Bernstein-Vazirani finds hidden strings with 1 query
  • Simon's algorithm finds hidden periods efficiently
  • ✅ These algorithms demonstrate quantum advantage over classical methods

Next Steps

Continue to Module 4: Quantum Fourier Transform and Related Algorithms to learn about: - Quantum Fourier Transform - Shor's algorithm - Grover's search algorithm

📚 Official Documentation
  1. Qiskit Textbook - Deutsch-Jozsa
  2. Qiskit Textbook - Bernstein-Vazirani
  3. Qiskit Textbook - Simon's Algorithm
  4. Qiskit API Reference
📖 Essential Articles
  1. Deutsch-Jozsa Algorithm
  2. Bernstein-Vazirani Algorithm
  3. Simon's Problem

Last Updated: November 2024