Q# Intermediate Free 3/7 in series 40 minutes

Implementing Grover's Algorithm in Q#

Build a complete implementation of Grover's quantum search algorithm in Q#, including oracle design, reflection operations, and execution on the Azure Quantum Resource Estimator and full-state simulator.

What you'll learn

  • Q#
  • Grover's algorithm
  • quantum search
  • Azure Quantum
  • quantum oracles

Prerequisites

  • Python proficiency
  • Beginner quantum computing concepts (superposition, entanglement)
  • Linear algebra basics

What Grover’s Algorithm Does

Classical search over an unsorted database of N items requires O(N) queries on average. Grover’s algorithm finds a marked item in O(sqrt(N)) queries, a quadratic speedup. For a 3-qubit database (8 items), classical search needs up to 8 queries while Grover’s needs roughly 2.

The algorithm works by repeatedly applying two reflections:

  1. Oracle reflection: flips the phase of the marked state(s).
  2. Diffusion reflection: reflects about the uniform superposition, amplifying the amplitude of marked states.

After approximately (pi/4) * sqrt(N) iterations, measuring the qubits yields the marked item with high probability.

Mathematical Derivation of Grover’s Algorithm

Understanding why Grover’s algorithm works requires a geometric picture in a two-dimensional subspace. This section walks through the full amplitude amplification derivation.

The Initial State

Start with n qubits in the |0> state and apply Hadamard gates to create the uniform superposition:

|s> = H^n |0...0> = (1/sqrt(N)) * sum_{x=0}^{N-1} |x>

where N = 2^n. Every computational basis state has equal amplitude 1/sqrt(N).

The Two-Dimensional Subspace

Suppose there are k marked items (items the oracle recognizes as solutions). Define two orthogonal states:

  • |w>: the uniform superposition over all k marked states, |w> = (1/sqrt(k)) * sum_{x in marked} |x>
  • |s’>: the uniform superposition over all N-k unmarked states, |s’> = (1/sqrt(N-k)) * sum_{x not marked} |x>

The initial state |s> lies in the plane spanned by |w> and |s’>. We can write:

|s> = sin(theta) |w> + cos(theta) |s'>

where sin(theta) = sqrt(k/N). For large N with a single marked item, theta is approximately sqrt(1/N), a small angle. The initial state is nearly perpendicular to |w> and almost entirely along |s’>.

The Oracle Operator

The oracle O applies a phase flip to marked states:

O|x> = -|x>  if x is marked
O|x> =  |x>  if x is not marked

In the {|w>, |s’>} basis, O acts as a reflection about |s’>. It negates the |w> component while leaving the |s’> component unchanged:

O = I - 2|w><w|

The Diffusion Operator

The Grover diffusion operator is:

D = 2|s><s| - I

This is a reflection about the initial state |s>. In practice, the circuit implements D = H^n (2|0><0| - I) H^n. The inner part (2|0><0| - I) flips the phase of every basis state except |0…0>, then the surrounding Hadamards rotate this into a reflection about |s>.

Geometric Picture: Rotation in the Plane

Each Grover iteration applies G = D * O. Since O reflects about |s’> and D reflects about |s>, the composition of two reflections is a rotation. The rotation angle is 2*theta, where theta = arcsin(sqrt(k/N)).

After t iterations, the state becomes:

G^t |s> = sin((2t+1)*theta) |w> + cos((2t+1)*theta) |s'>

The amplitude on |w> grows with each iteration as the state vector rotates toward |w> in the two-dimensional plane.

Optimal Number of Iterations

The success probability after t iterations is:

P(success) = sin^2((2t+1)*theta)

This reaches its maximum (close to 1) when (2t+1)*theta is approximately pi/2, giving:

t_opt = floor(pi / (4*theta) - 1/2)

For small theta (large N relative to k), theta is approximately sqrt(k/N), so:

t_opt = floor(pi/4 * sqrt(N/k))

For the 3-qubit case with N=8 and k=1: theta = arcsin(sqrt(1/8)) = arcsin(0.354) = 0.3614 radians. Then t_opt = floor(pi/(4*0.3614) - 0.5) = floor(2.17 - 0.5) = floor(1.67) = 2 (rounding to 2 because the exact formula with floor gives 2 iterations). The success probability is sin^2(5 * 0.3614) = sin^2(1.807) = 0.945, about 94.5%.

Why Only Quadratic Speedup?

A common misconception holds that quantum computers “try all answers simultaneously” and thus provide exponential speedup. Grover’s algorithm demonstrates the actual situation: the amplitudes of non-solution states must be carefully drained into solution states through constructive interference. This interference process takes O(sqrt(N)) steps, not O(1). The BBBV theorem (Bennett, Bernstein, Brassard, Vazirani) proves that no quantum algorithm can search an unstructured database faster than O(sqrt(N)), so Grover’s speedup is provably optimal.

Q# Type System and Operations

Q# provides a rich type system designed specifically for quantum programming. Understanding it helps you write correct and composable quantum code.

Primitive Types

  • Qubit: represents a quantum bit. You cannot create qubits directly; you allocate them with the use statement. You cannot clone them (no-cloning theorem), copy them, or inspect their state outside of measurement.
  • Result: the outcome of a qubit measurement, either Zero or One.
  • Bool: standard boolean, true or false.
  • Int: 64-bit signed integer.
  • BigInt: arbitrary-precision integer, useful for classical computations on large numbers.
  • Double: 64-bit floating-point number.
  • String: text, mainly used with Message() for debugging output.
  • Range: a sequence like 1 .. 5 or 0 .. 2 .. 10 (start, step, end).

Arrays and Tuples

Arrays use bracket syntax: Qubit[], Int[], Bool[]. Access elements with array[index]. Slicing uses ranges: register[0 .. 2] gives the first three elements. Arrays in Q# are immutable by default; use mutable for arrays you need to modify.

Tuples group values: (Int, Bool), (Qubit[], Int). Function and operation signatures heavily use tuples for multiple parameters.

Operations vs. Functions

This distinction is central to Q#:

  • Operations can have quantum side effects. They allocate qubits, apply gates, and perform measurements. Declared with the operation keyword.
  • Functions are purely classical. They compute values, perform math, and transform data structures. They cannot touch qubits. Declared with the function keyword.

The compiler enforces this boundary. A function cannot call an operation. This separation allows the compiler and runtime to reason about which parts of your code interact with quantum hardware.

Functor Annotations

Operations can declare support for automatic generation of adjoint and controlled variants:

  • is Adj: the compiler generates the adjoint (inverse) of the operation by reversing the gate sequence and replacing each gate with its adjoint. This works for operations composed entirely of self-adjoint or adjointable gates.
  • is Ctl: the compiler generates a controlled version that takes an array of control qubits. Every gate in the body gains an additional control.
  • is Adj + Ctl: supports both. Most oracle and diffusion operations need this annotation so that higher-level operations can use them in controlled or adjoint contexts.
// This operation supports both Adjoint and Controlled functors
operation PrepareState(register : Qubit[]) : Unit is Adj + Ctl {
    ApplyToEachCA(H, register);  // supports Adj + Ctl
    CNOT(register[0], register[1]);
}

// The compiler auto-generates:
// Adjoint PrepareState: applies Adjoint CNOT then Adjoint H to each
// Controlled PrepareState: each gate becomes controlled

The use Statement

The use statement allocates qubits initialized to |0> and releases them at the end of the enclosing scope. The runtime enforces that qubits must be returned to |0> before release (or measured with MResetEachZ, which measures and resets).

// Allocate a single qubit
use q = Qubit();
// ... use q ...
Reset(q);  // return to |0> before scope ends

// Allocate an array of qubits
use register = Qubit[5];
// ... use register ...
ResetAll(register);

If you attempt to release a qubit that is not in the |0> state, the simulator raises an error. This catches a common class of bugs where entangled or superposed qubits leak out of scope.

Project Setup

Create a Q# project:

dotnet new console -lang Q# -o GroverSearch
cd GroverSearch

This creates Program.qs and a .csproj file. Open Program.qs and replace its contents.

Full Implementation

namespace GroverSearch {
    open Microsoft.Quantum.Canon;
    open Microsoft.Quantum.Intrinsic;
    open Microsoft.Quantum.Math;
    open Microsoft.Quantum.Convert;
    open Microsoft.Quantum.Measurement;

    // Oracle that marks the state |101> (decimal 5) in a 3-qubit register.
    // It flips an ancilla qubit if and only if the register holds |101>.
    operation MarkTarget(register : Qubit[], target : Qubit) : Unit
    is Adj + Ctl {
        // |101> means q[0]=1, q[1]=0, q[2]=1
        // Apply X to q[1] so we can use a plain Toffoli for the all-ones check
        within {
            X(register[1]);
        } apply {
            Controlled X(register, target);
        }
    }

    // Phase oracle: marks |101> by flipping its phase using an ancilla
    operation PhaseOracle(register : Qubit[]) : Unit
    is Adj + Ctl {
        use ancilla = Qubit();
        within {
            X(ancilla);
            H(ancilla);
        } apply {
            MarkTarget(register, ancilla);
        }
    }

    // Grover diffusion (inversion about average):
    // H^n * (2|0><0| - I) * H^n
    operation GroverDiffusion(register : Qubit[]) : Unit {
        within {
            ApplyToEachA(H, register);
            ApplyToEachA(X, register);
        } apply {
            Controlled Z(register[0 .. Length(register) - 2],
                         register[Length(register) - 1]);
        }
    }

    // One full Grover iteration: oracle then diffusion
    operation GroverIteration(register : Qubit[]) : Unit {
        PhaseOracle(register);
        GroverDiffusion(register);
    }

    // Main Grover search operation
    operation RunGroverSearch() : Result[] {
        let nQubits = 3;
        // Optimal number of iterations for N=8, 1 marked item: floor(pi/4 * sqrt(8)) = 2
        let nIterations = 2;

        use register = Qubit[nQubits];

        // Initialize uniform superposition
        ApplyToEach(H, register);

        // Apply Grover iterations
        for _ in 1 .. nIterations {
            GroverIteration(register);
        }

        // Measure and return results
        return MResetEachZ(register);
    }

    @EntryPoint()
    operation Main() : Unit {
        let nRuns = 20;
        mutable successCount = 0;

        for run in 1 .. nRuns {
            let results = RunGroverSearch();
            let bits = ResultArrayAsBoolArray(results);
            // |101> in little-endian order is [true, false, true]
            let found = bits[0] and not bits[1] and bits[2];
            if found {
                set successCount += 1;
            }
            let label = if found { "(found!)" } else { "" };
            Message($"Run {run}: {results} {label}");
        }

        Message($"Found |101> in {successCount}/{nRuns} runs.");
    }
}

Understanding the Oracle

The oracle for a specific target state is a conditional phase flip. The cleanest approach uses the within ... apply conjugation pattern:

within {
    X(register[1]);  // flip q[1] so |101> becomes |111>
} apply {
    Controlled X(register, ancilla);  // flip ancilla if register is |111>
}
// The `within` block automatically applies X(register[1]) again (adjoint)
// restoring the register while keeping the ancilla flipped

This is a standard technique: transform the target state into |111…1>, apply a controlled-NOT on all qubits (a multi-controlled Toffoli), then undo the transformation. The Adj + Ctl annotation on MarkTarget tells the Q# compiler to auto-generate the adjoint and controlled variants.

Oracle Construction Patterns

There are several ways to build oracles in Q#. Each pattern has different trade-offs in ancilla usage, gate depth, and composability.

Pattern 1: Within/Apply Conjugation (Bit-Flip Oracle)

This is the pattern shown in the main implementation. It marks a specific computational basis state by:

  1. Flipping qubits corresponding to 0-bits in the target so the target maps to |111…1>.
  2. Applying a multi-controlled NOT (Toffoli generalization) on an ancilla.
  3. Undoing the flips (automatically handled by within).

The helper function that converts an integer index to the correct set of X gates:

// Converts a target index into X-gate flips so the target state maps to |111...1>
// Then applies a multi-controlled NOT, then undoes the flips.
operation MarkByIndex(register : Qubit[], target : Qubit, index : Int) : Unit
is Adj + Ctl {
    let bits = IntAsBoolArray(index, Length(register));
    within {
        for i in 0 .. Length(register) - 1 {
            if not bits[i] {
                X(register[i]);
            }
        }
    } apply {
        Controlled X(register, target);
    }
}

For index = 5 (binary 101), IntAsBoolArray(5, 3) returns [true, false, true] in little-endian order. The loop applies X only to qubit 1 (the false bit), transforming |101> into |111>. The multi-controlled X then triggers only for that state.

Pattern 2: Phase Kickback Without Explicit Ancilla Management

The phase oracle in the main implementation uses the phase kickback trick: prepare an ancilla in the |-> = (|0> - |1>)/sqrt(2) state, flip it conditionally, and let the phase kick back onto the register. The within ... apply block handles this cleanly:

operation PhaseOracleViaKickback(register : Qubit[], markOp : (Qubit[], Qubit) => Unit is Adj + Ctl) : Unit
is Adj + Ctl {
    use ancilla = Qubit();
    within {
        // Prepare |-> state: X then H maps |0> to |->
        X(ancilla);
        H(ancilla);
    } apply {
        // When markOp flips the ancilla for a marked state,
        // the phase (-1) kicks back onto the register
        markOp(register, ancilla);
    }
    // within/apply undoes H and X, returning ancilla to |0>
}

The key insight: flipping a qubit in the |-> state introduces a global phase of -1. Since the flip is conditional on the register state, only the marked state picks up this phase. The ancilla returns to |0> after the within block uncomputes.

Pattern 3: Truth-Table Oracle for Boolean Predicates

For more complex search problems, you want an oracle that marks all states satisfying a boolean predicate, not just a single index. The approach uses ancilla qubits to compute intermediate boolean values:

// Oracle that marks states where the majority of bits are 1
// For 3 qubits: marks |011>, |101>, |110>, |111>
operation MajorityOracle(register : Qubit[], target : Qubit) : Unit
is Adj + Ctl {
    // Majority of 3 bits: (x0 AND x1) OR (x0 AND x2) OR (x1 AND x2)
    // Use ancilla qubits to compute each AND clause
    use ancillas = Qubit[3];
    within {
        // Compute x0 AND x1
        CCNOT(register[0], register[1], ancillas[0]);
        // Compute x0 AND x2
        CCNOT(register[0], register[2], ancillas[1]);
        // Compute x1 AND x2
        CCNOT(register[1], register[2], ancillas[2]);
        // OR the results: flip target if ANY ancilla is 1
        // We use the trick: OR(a,b,c) = NOT(AND(NOT a, NOT b, NOT c))
        ApplyToEachA(X, ancillas);
    } apply {
        // ancillas are now flipped: all-1 means none of the AND clauses were true
        // Controlled X flips target when all ancillas are 1 (original: no clause true)
        Controlled X(ancillas, target);
    }
    // After within/apply, the X on target is set when NO clause is true
    // We need the opposite, so add a final X flip
    X(target);
}

This pattern generalizes to any boolean function. The critical requirement is that all intermediate computation on ancilla qubits must be uncomputed (the within block handles this), leaving only the final answer on the target qubit.

Amplitude Amplification Visualization

To build intuition for how Grover’s algorithm evolves the quantum state, you can use DumpMachine() to inspect amplitudes after each iteration. The following code tracks the probability of measuring |101> through successive iterations.

namespace GroverVisualization {
    open Microsoft.Quantum.Canon;
    open Microsoft.Quantum.Intrinsic;
    open Microsoft.Quantum.Math;
    open Microsoft.Quantum.Convert;
    open Microsoft.Quantum.Diagnostics;

    // Same oracle and diffusion as before
    operation MarkTarget(register : Qubit[], target : Qubit) : Unit
    is Adj + Ctl {
        within {
            X(register[1]);
        } apply {
            Controlled X(register, target);
        }
    }

    operation PhaseOracle(register : Qubit[]) : Unit
    is Adj + Ctl {
        use ancilla = Qubit();
        within {
            X(ancilla);
            H(ancilla);
        } apply {
            MarkTarget(register, ancilla);
        }
    }

    operation GroverDiffusion(register : Qubit[]) : Unit {
        within {
            ApplyToEachA(H, register);
            ApplyToEachA(X, register);
        } apply {
            Controlled Z(register[0 .. Length(register) - 2],
                         register[Length(register) - 1]);
        }
    }

    @EntryPoint()
    operation TrackAmplitudes() : Unit {
        let nQubits = 3;
        let maxIterations = 5;

        use register = Qubit[nQubits];

        // Create uniform superposition
        ApplyToEach(H, register);

        Message("After 0 iterations (uniform superposition):");
        DumpMachine();

        for iter in 1 .. maxIterations {
            // Apply one Grover iteration
            PhaseOracle(register);
            GroverDiffusion(register);

            Message($"After {iter} iteration(s):");
            DumpMachine();
        }

        ResetAll(register);
    }
}

Running this on the full-state simulator produces output like the following. The amplitudes shown are for each basis state (in little-endian qubit order):

After 0 iterations (uniform superposition):
  |000> : 0.3536 + 0.0000i   (probability: 12.50%)
  |001> : 0.3536 + 0.0000i   (probability: 12.50%)
  |010> : 0.3536 + 0.0000i   (probability: 12.50%)
  |011> : 0.3536 + 0.0000i   (probability: 12.50%)
  |100> : 0.3536 + 0.0000i   (probability: 12.50%)
  |101> : 0.3536 + 0.0000i   (probability: 12.50%)  <-- target
  |110> : 0.3536 + 0.0000i   (probability: 12.50%)
  |111> : 0.3536 + 0.0000i   (probability: 12.50%)

After 1 iteration(s):
  |101> probability: ~78.1%   (other states: ~3.1% each)

After 2 iterations:
  |101> probability: ~94.5%   (other states: ~0.8% each)   <-- OPTIMAL

After 3 iterations:
  |101> probability: ~32.8%   (other states: ~9.6% each)   <-- past optimal!

After 4 iterations:
  |101> probability: ~0.5%    (other states: ~14.2% each)

After 5 iterations:
  |101> probability: ~54.7%   (other states: ~6.5% each)   <-- rising again

Notice that the probability peaks at iteration 2 and then drops sharply. By iteration 4, the probability is near zero because the state has rotated past |w> and is now close to |s’> again. This oscillatory behavior is a key feature: applying too many iterations is just as bad as applying too few.

The theoretical probabilities match the formula P(t) = sin^2((2t+1)*theta) where theta = arcsin(sqrt(1/8)):

Iterations (t)(2t+1)*thetaP(success)
00.361412.5%
11.084278.1%
21.807094.5%
32.529832.8%
43.25260.5%

Running on the Simulator

dotnet run

Expected output (probabilistic, but the marked state should appear in roughly 95% of runs with 2 iterations for N=8):

Run 1: [One, Zero, One] (found!)
Run 2: [One, Zero, One] (found!)
...
Found |101> in 19/20 runs.

The theoretical success probability for 2 iterations on an 8-item database is about 96%.

Searching with Multiple Marked Items

If k items are marked (k > 1), the optimal number of iterations drops to approximately (pi/4) * sqrt(N/k). With k unknown you can use quantum counting (itself based on quantum phase estimation) to estimate k first, then choose the iteration count adaptively.

Marking Two States Simultaneously

To search for multiple items, compose individual marking operations. The following oracle marks both |011> (index 3) and |101> (index 5):

namespace MultiMarkedGrover {
    open Microsoft.Quantum.Canon;
    open Microsoft.Quantum.Intrinsic;
    open Microsoft.Quantum.Math;
    open Microsoft.Quantum.Convert;
    open Microsoft.Quantum.Measurement;

    // Marks a specific index by flipping the target qubit
    operation MarkByIndex(register : Qubit[], target : Qubit, index : Int) : Unit
    is Adj + Ctl {
        let bits = IntAsBoolArray(index, Length(register));
        within {
            for i in 0 .. Length(register) - 1 {
                if not bits[i] {
                    X(register[i]);
                }
            }
        } apply {
            Controlled X(register, target);
        }
    }

    // Oracle that marks BOTH |011> and |101>
    operation MarkTwoTargets(register : Qubit[], target : Qubit) : Unit
    is Adj + Ctl {
        MarkByIndex(register, target, 3);  // marks |011>
        MarkByIndex(register, target, 5);  // marks |101>
    }

    // Phase oracle using phase kickback
    operation PhaseOracleTwoTargets(register : Qubit[]) : Unit
    is Adj + Ctl {
        use ancilla = Qubit();
        within {
            X(ancilla);
            H(ancilla);
        } apply {
            MarkTwoTargets(register, ancilla);
        }
    }

    operation GroverDiffusion(register : Qubit[]) : Unit {
        within {
            ApplyToEachA(H, register);
            ApplyToEachA(X, register);
        } apply {
            Controlled Z(register[0 .. Length(register) - 2],
                         register[Length(register) - 1]);
        }
    }

    @EntryPoint()
    operation Main() : Unit {
        let nQubits = 3;
        // For N=8 and k=2 marked items:
        // theta = arcsin(sqrt(2/8)) = arcsin(0.5) = pi/6
        // t_opt = floor(pi/(4*pi/6) - 0.5) = floor(1.5 - 0.5) = 1
        let nIterations = 1;
        let nRuns = 100;
        mutable successCount = 0;

        for run in 1 .. nRuns {
            use register = Qubit[nQubits];
            ApplyToEach(H, register);

            for _ in 1 .. nIterations {
                PhaseOracleTwoTargets(register);
                GroverDiffusion(register);
            }

            let results = MResetEachZ(register);
            let bits = ResultArrayAsBoolArray(results);

            // Check for |011> (bits: [true, true, false]) or |101> (bits: [true, false, true])
            let found011 = bits[0] and bits[1] and not bits[2];
            let found101 = bits[0] and not bits[1] and bits[2];

            if found011 or found101 {
                set successCount += 1;
            }
        }

        Message($"Found a marked state in {successCount}/{nRuns} runs.");
        Message($"Expected success rate: ~100% (sin^2(3*pi/6) = sin^2(pi/2) = 1.0)");
    }
}

Derivation for k=2

With N=8 and k=2 marked items:

  • theta = arcsin(sqrt(k/N)) = arcsin(sqrt(2/8)) = arcsin(1/2) = pi/6
  • Optimal iterations: t_opt = floor(pi/(4theta) - 1/2) = floor(pi/(4pi/6) - 1/2) = floor(3/2 - 1/2) = 1
  • Success probability after 1 iteration: sin^2((21+1) * pi/6) = sin^2(3pi/6) = sin^2(pi/2) = 1.0

This is a special case: with k=2 and N=8, a single Grover iteration produces a success probability of exactly 100%. The empirical success rate from the simulation confirms this.

Note that composing the two MarkByIndex calls works correctly because each call flips the ancilla for its respective target state. If the register holds |011>, the first call flips the ancilla and the second does nothing. If it holds |101>, the first does nothing and the second flips the ancilla. If it holds neither, both leave the ancilla unchanged.

Quantum Counting

Quantum counting answers the question: “How many solutions exist?” without finding them. It combines Grover’s operator with quantum phase estimation (QPE) to estimate the number of marked items k.

The Core Idea

The Grover operator G = D * O has two eigenvalues in the {|w>, |s’>} subspace:

exp(+2i*theta)  and  exp(-2i*theta)

where sin(theta) = sqrt(k/N). If you apply QPE to G, you learn theta, and from theta you compute k = N * sin^2(theta).

Structure of the Quantum Counting Circuit

The algorithm uses two registers:

  1. Counting register: m qubits that hold the phase estimate. More counting qubits give higher precision in estimating k.
  2. Search register: n qubits on which the Grover operator acts.

The circuit:

  1. Initialize the counting register in |0>^m and the search register in |s> = H^n|0>^n.
  2. Apply Hadamard to all counting qubits.
  3. For each counting qubit j (from 0 to m-1), apply G^(2^j) controlled on counting qubit j.
  4. Apply inverse QFT to the counting register.
  5. Measure the counting register to obtain an estimate of theta.
// Conceptual structure of quantum counting in Q#
// (Requires a QFT library and controlled power-of-Grover implementation)

operation QuantumCounting(
    searchRegister : Qubit[],
    countingRegister : Qubit[],
    groverIteration : (Qubit[]) => Unit is Adj + Ctl
) : Unit {
    let m = Length(countingRegister);

    // Step 1: Uniform superposition on search register
    ApplyToEach(H, searchRegister);

    // Step 2: Hadamard on counting register
    ApplyToEach(H, countingRegister);

    // Step 3: Controlled powers of Grover iteration
    for j in 0 .. m - 1 {
        let power = 1 <<< j;  // 2^j
        for _ in 1 .. power {
            Controlled groverIteration([countingRegister[j]], searchRegister);
        }
    }

    // Step 4: Inverse QFT on counting register
    Adjoint QFT(countingRegister);
}

Interpreting the Result

The measurement of the counting register gives an integer y in {0, 1, …, 2^m - 1}. The estimated angle is:

theta_est = pi * y / 2^m

The number of solutions is then:

k_est = N * sin^2(theta_est)

For m counting qubits, the precision in theta scales as O(1/2^m). With 4 counting qubits for an N=8 search space, you can distinguish between k=0, 1, 2, … solutions reliably.

Quantum counting is valuable because it allows you to determine the optimal number of Grover iterations before running the full search, avoiding the problem of overshooting described in the amplitude amplification visualization section.

Grover’s algorithm provides a quadratic speedup over classical unstructured search. Here is a quantitative comparison showing the number of oracle queries needed:

Database Size (N)Classical Queries (avg)Grover QueriesSpeedup Factor
8422x
643265.3x
1,0245122520.5x
1,048,576 (2^20)524,288804652x
~1 billion (2^30)~500 million25,736~19,400x

The classical average is N/2 (check items one by one until you find the marked one). Grover’s count is ceil(pi/4 * sqrt(N)). Both assume k=1 marked item.

Quadratic, Not Exponential

The speedup factor grows as sqrt(N), not as N. For N = 1024, the speedup is about 20x, not 1024x. This has important practical implications:

  • Grover’s algorithm does not break symmetric cryptography outright. A 256-bit key requires 2^256 classical brute-force queries. Grover reduces this to 2^128 quantum queries, which is still astronomically large. The standard response is to double key lengths (e.g., AES-256 instead of AES-128).
  • For practical database search, the overhead of running a quantum computer (gate times, error correction, qubit initialization) means the quadratic speedup only pays off for very large N where the sqrt(N) savings outweigh constant-factor overhead.
  • The real power of Grover’s algorithm lies in its use as a subroutine inside other quantum algorithms (amplitude amplification), not as a standalone database search tool.

Real Oracle for Satisfiability

One of the most compelling applications of Grover’s algorithm is solving Boolean satisfiability (SAT) problems. Given a Boolean formula, Grover’s algorithm can find a satisfying assignment in O(sqrt(2^n)) oracle calls, where n is the number of variables.

Example: A 3-Variable SAT Instance

Consider the formula: (x0 OR x1) AND (NOT x0 OR x2)

The satisfying assignments are:

  • x0=0, x1=1, x2=0 (clause 1: 0 OR 1 = 1, clause 2: 1 OR 0 = 1)
  • x0=0, x1=1, x2=1 (clause 1: 0 OR 1 = 1, clause 2: 1 OR 1 = 1)
  • x0=1, x1=0, x2=1 (clause 1: 1 OR 0 = 1, clause 2: 0 OR 1 = 1)
  • x0=1, x1=1, x2=0 does NOT satisfy (clause 2: 0 OR 0 = 0)
  • x0=1, x1=1, x2=1 (clause 1: 1 OR 1 = 1, clause 2: 0 OR 1 = 1)
  • x0=0, x1=0, x2=0 does NOT satisfy (clause 1: 0 OR 0 = 0)
  • x0=0, x1=0, x2=1 does NOT satisfy (clause 1: 0 OR 0 = 0)
  • x0=1, x1=0, x2=0 does NOT satisfy (clause 2: 0 OR 0 = 0)

So 4 out of 8 assignments satisfy the formula.

Q# Implementation of the SAT Oracle

namespace GroverSAT {
    open Microsoft.Quantum.Canon;
    open Microsoft.Quantum.Intrinsic;
    open Microsoft.Quantum.Math;
    open Microsoft.Quantum.Convert;
    open Microsoft.Quantum.Measurement;

    // Computes OR of two qubits into a target qubit using ancilla logic.
    // OR(a, b) = NOT(AND(NOT a, NOT b))
    operation ComputeOR(a : Qubit, b : Qubit, target : Qubit) : Unit
    is Adj + Ctl {
        within {
            // Flip a and b so CCNOT computes AND(NOT a, NOT b)
            X(a);
            X(b);
        } apply {
            CCNOT(a, b, target);
        }
        // target now holds AND(NOT a, NOT b)
        // Flip it to get OR(a, b) = NOT(AND(NOT a, NOT b))
        X(target);
    }

    // Oracle for (x0 OR x1) AND (NOT x0 OR x2)
    // Marks states where the formula is satisfied
    operation SATOracle(register : Qubit[], target : Qubit) : Unit
    is Adj + Ctl {
        // register[0] = x0, register[1] = x1, register[2] = x2
        // clause1 = x0 OR x1
        // clause2 = NOT x0 OR x2
        use clauseResults = Qubit[2];

        within {
            // Compute clause 1: x0 OR x1
            ComputeOR(register[0], register[1], clauseResults[0]);

            // Compute clause 2: NOT x0 OR x2
            // NOT x0 is achieved by flipping x0 temporarily
            within {
                X(register[0]);
            } apply {
                ComputeOR(register[0], register[2], clauseResults[1]);
            }
        } apply {
            // AND all clause results: flip target if all clauses are satisfied
            Controlled X(clauseResults, target);
        }
    }

    // Phase oracle wrapper
    operation SATPhaseOracle(register : Qubit[]) : Unit
    is Adj + Ctl {
        use ancilla = Qubit();
        within {
            X(ancilla);
            H(ancilla);
        } apply {
            SATOracle(register, ancilla);
        }
    }

    operation GroverDiffusion(register : Qubit[]) : Unit {
        within {
            ApplyToEachA(H, register);
            ApplyToEachA(X, register);
        } apply {
            Controlled Z(register[0 .. Length(register) - 2],
                         register[Length(register) - 1]);
        }
    }

    @EntryPoint()
    operation Main() : Unit {
        let nQubits = 3;
        // N=8, k=4 satisfying assignments
        // theta = arcsin(sqrt(4/8)) = arcsin(sqrt(0.5)) = pi/4
        // t_opt = floor(pi/(4*pi/4) - 0.5) = floor(1 - 0.5) = 0
        // With k = N/2, a single iteration overshoots!
        // t = 0 means just measure the uniform superposition (50% success)
        // t = 1 gives sin^2(3*pi/4) = 50% as well
        // For this heavily-satisfiable formula, even random guessing works well.
        // Let's use 1 iteration to demonstrate the circuit.
        let nIterations = 1;
        let nRuns = 100;
        mutable successCount = 0;

        for _ in 1 .. nRuns {
            use register = Qubit[nQubits];
            ApplyToEach(H, register);

            for _ in 1 .. nIterations {
                SATPhaseOracle(register);
                GroverDiffusion(register);
            }

            let results = MResetEachZ(register);
            let bits = ResultArrayAsBoolArray(results);

            // Verify the formula classically
            let x0 = bits[0];
            let x1 = bits[1];
            let x2 = bits[2];
            let clause1 = x0 or x1;
            let clause2 = (not x0) or x2;
            let satisfied = clause1 and clause2;

            if satisfied {
                set successCount += 1;
            }
        }

        Message($"Found a satisfying assignment in {successCount}/{nRuns} runs.");
        Message("For this formula with 4/8 solutions, random baseline is 50%.");
    }
}

The key takeaway: Grover’s algorithm transforms SAT from O(2^n) to O(sqrt(2^n)) oracle queries. For harder instances where k is much smaller than N (few satisfying assignments), the speedup is dramatic. A SAT instance with 50 variables and a single solution requires about 2^50 classical checks (~10^15) but only about 2^25 Grover iterations (~33 million), combined with the oracle evaluation cost per iteration.

Running on the Resource Estimator

The Azure Quantum Resource Estimator tells you the fault-tolerant resource requirements without executing on real hardware. Add a resource estimation profile:

dotnet add package Microsoft.Quantum.Qir.ResourceEstimation

Then in the .csproj set:

<ExecutionTarget>microsoft.resourceestimation</ExecutionTarget>

Run with:

dotnet run

Resource Estimation Deep Dive

The estimator outputs T-gate counts, qubit counts, and depth: critical metrics for understanding when your algorithm becomes viable on error-corrected hardware. For the 3-qubit Grover circuit you will see a modest T-count; scaling to 20+ qubits reveals the exponential resource costs that motivate fault-tolerant architectures.

For the 3-qubit Grover search (marking |101> with 2 iterations), the Resource Estimator produces output similar to the following (exact numbers depend on the compiler’s gate decomposition and the error budget you specify):

MetricValueExplanation
Logical qubits53 search + 1 ancilla + 1 for Toffoli decomposition
T-gate count~40Each Toffoli decomposes into 7 T gates; 2 iterations with oracle + diffusion
T-depth~16Sequential depth of T gates (determines runtime)
Code distance15Surface code distance for target error rate of 10^-3
Physical qubits per logical qubit~450(2d+1)^2 for distance d=15 with surface code
Total physical qubits~3,0005 logical qubits * ~450 physical + magic state factories
Magic state factories1Produces T states via distillation
Physical qubits for factories~5,000Dominates total qubit count for small circuits
Total physical qubits~8,000Search qubits + factory overhead
Runtime~2 msAt a 1-microsecond gate cycle time

Each metric reveals something important:

  • T-gate count determines the runtime cost because T gates require expensive magic state distillation in surface-code-based error correction. Clifford gates (H, CNOT, S) are essentially free by comparison.
  • Code distance is chosen by the estimator to achieve a target logical error rate. Higher distance means more physical qubits per logical qubit but lower error rates. For a total circuit volume of ~200 logical gates, a distance of 15 achieves error rates around 10^-10 per logical gate.
  • Magic state factories are specialized circuits that distill high-fidelity T states from noisy physical T gates. For small circuits, the factory footprint exceeds the computation footprint. This is a fundamental overhead of fault-tolerant quantum computing.
  • Physical qubit count is the number you compare against hardware roadmaps. Current devices have ~1,000 physical qubits; the ~8,000 needed here is within reach of near-term plans, but larger Grover instances (20+ qubits) require millions of physical qubits.

As you scale to larger problem sizes, the T-gate count grows because each additional search qubit increases the Toffoli gate size (an n-qubit Toffoli decomposes into O(n) standard Toffoli gates, each costing 7 T gates). The physical qubit count and runtime grow correspondingly.

Generalizing to Arbitrary Targets

To search for a different state, only the oracle changes. Here is a parameterized version:

operation MarkArbitraryTarget(
    register : Qubit[],
    target : Qubit,
    markedIndex : Int
) : Unit is Adj + Ctl {
    let bits = IntAsBoolArray(markedIndex, Length(register));
    within {
        for (qubit, bit) in Zipped(register, bits) {
            if not bit { X(qubit); }
        }
    } apply {
        Controlled X(register, target);
    }
}

Pass markedIndex = 5 for |101>, markedIndex = 3 for |011>, and so on. The rest of the algorithm is unchanged.

Q# Library Functions Reference

This section documents every Q# standard library function and operation used in this tutorial, with type signatures and explanations.

ApplyToEach

operation ApplyToEach<'T>(singleElementOperation : ('T => Unit), register : 'T[]) : Unit

Applies an operation to every element of an array. Used to apply H to all qubits in a register: ApplyToEach(H, register). This is the non-adjointable version; use it when the operation does not need is Adj support.

ApplyToEachA

operation ApplyToEachA<'T>(singleElementOperation : ('T => Unit is Adj), register : 'T[]) : Unit is Adj

The adjointable version of ApplyToEach. Required inside within blocks because the compiler needs to generate the adjoint of the entire block. When you write ApplyToEachA(H, register) inside a within block, the compiler knows it can reverse this step by applying Adjoint H to each qubit (which is just H again, since H is self-adjoint).

Controlled X (CNOT / Toffoli / Multi-Controlled NOT)

Controlled X(controls : Qubit[], target : Qubit) : Unit is Adj + Ctl

Applies X to the target qubit if and only if all control qubits are in the |1> state. With one control qubit, this is a CNOT. With two controls, it is a Toffoli (CCNOT). With n controls, it is an n-controlled NOT. The compiler decomposes multi-controlled gates into elementary gates automatically.

Controlled Z

Controlled Z(controls : Qubit[], target : Qubit) : Unit is Adj + Ctl

Applies Z to the target qubit controlled on all control qubits being |1>. In the diffusion operator, this implements the phase flip of |111…1>. Note that controlled-Z is symmetric in its qubits, so the choice of which qubit is “target” is arbitrary.

MResetEachZ

operation MResetEachZ(register : Qubit[]) : Result[]

Measures each qubit in the computational (Z) basis and resets it to |0>. Returns an array of Result values. This is the preferred way to end a computation because it satisfies Q#‘s requirement that qubits be in |0> when released. Equivalent to calling M followed by Reset on each qubit individually, but more concise.

ResultArrayAsBoolArray

function ResultArrayAsBoolArray(results : Result[]) : Bool[]

Converts an array of Result values to an array of Bool values. One maps to true, Zero maps to false. This is a pure function (no quantum effects). Useful for classical post-processing of measurement results.

IntAsBoolArray

function IntAsBoolArray(number : Int, bits : Int) : Bool[]

Converts an integer to its binary representation as a boolean array in little-endian order. IntAsBoolArray(5, 3) returns [true, false, true] because 5 = 12^0 + 02^1 + 1*2^2. The least significant bit is at index 0. This matches Q#‘s qubit ordering convention.

Zipped

function Zipped<'T1, 'T2>(left : 'T1[], right : 'T2[]) : ('T1, 'T2)[]

Combines two arrays into an array of tuples, pairing elements by index. Zipped([1, 2, 3], [true, false, true]) returns [(1, true), (2, false), (3, true)]. The arrays must have the same length. Used in MarkArbitraryTarget to iterate over qubits and their corresponding target bits simultaneously.

Length

function Length<'T>(array : 'T[]) : Int

Returns the number of elements in an array. A pure function with no side effects. Used frequently to determine register sizes and loop bounds.

Message

function Message(msg : String) : Unit

Prints a string to the classical output. Used for debugging and reporting results. String interpolation uses the $"..." syntax: Message($"Result: {value}").

DumpMachine

operation DumpMachine() : Unit

Outputs the full quantum state vector to the console. Only available on the full-state simulator (not on hardware or the resource estimator). Invaluable for debugging because it shows amplitudes of all basis states without collapsing the superposition. In production, you cannot inspect quantum states without measurement; DumpMachine is a simulator-only diagnostic.

Debugging Q# Programs

Q# provides several tools for understanding what your quantum program does during simulation.

Using DumpMachine() for State Inspection

DumpMachine() prints the full state vector, including complex amplitudes and probabilities for each basis state. Insert it at any point in your code to see the quantum state:

use q = Qubit();
H(q);
Message("After H:");
DumpMachine();  // Shows |0> and |1> each with amplitude 0.7071

X(q);
Message("After X:");
DumpMachine();  // Shows |0> and |1> with swapped amplitudes

Reset(q);

This is the single most useful debugging tool for quantum programs. When your oracle does not mark the right state, insert DumpMachine() calls before and after the oracle to see exactly which amplitudes flip sign.

Using Message() for Classical Logging

Message() prints strings during execution. Combine it with string interpolation for informative output:

let n = 3;
let iterations = 2;
Message($"Running Grover with {n} qubits and {iterations} iterations");

let results = MResetEachZ(register);
Message($"Measurement results: {results}");

Writing Unit Tests

Q# supports unit tests with the @Test("QuantumSimulator") attribute. Tests run on the simulator and can use assertion operations to verify quantum states:

namespace GroverTests {
    open Microsoft.Quantum.Canon;
    open Microsoft.Quantum.Intrinsic;
    open Microsoft.Quantum.Diagnostics;

    // Test that the oracle marks exactly the state |101>
    @Test("QuantumSimulator")
    operation TestOracleMarksCorrectState() : Unit {
        use register = Qubit[3];
        use target = Qubit();

        // Prepare |101>: set qubit 0 and qubit 2 to |1>
        X(register[0]);
        X(register[2]);

        // Apply the oracle
        MarkTarget(register, target);

        // The target should now be |1> because the register is |101>
        AssertQubit(One, target);

        // Clean up
        Reset(target);
        ResetAll(register);
    }

    // Test that the oracle does NOT mark |110>
    @Test("QuantumSimulator")
    operation TestOracleDoesNotMarkWrongState() : Unit {
        use register = Qubit[3];
        use target = Qubit();

        // Prepare |110>: set qubit 1 and qubit 2 to |1>
        X(register[1]);
        X(register[2]);

        // Apply the oracle
        MarkTarget(register, target);

        // The target should still be |0>
        AssertQubit(Zero, target);

        // Clean up
        ResetAll(register);
    }

    // Test that the full Grover search finds |101> with high probability
    @Test("QuantumSimulator")
    operation TestGroverSuccessRate() : Unit {
        mutable successCount = 0;
        let nTrials = 100;

        for _ in 1 .. nTrials {
            let results = RunGroverSearch();
            let bits = ResultArrayAsBoolArray(results);
            if bits[0] and not bits[1] and bits[2] {
                set successCount += 1;
            }
        }

        // With 2 iterations on N=8, success probability is ~94.5%
        // Require at least 80% to account for statistical fluctuation
        Fact(successCount > 80, $"Success rate too low: {successCount}/100");
    }
}

Run tests with:

dotnet test

The AssertQubit operation checks that a qubit is in the expected state and raises an error if not. The Fact function asserts a boolean condition with a custom error message.

Common Mistakes

This section covers errors that frequently appear when implementing Grover’s algorithm in Q#.

Off-by-One in Iteration Count

The optimal iteration count is floor(pi/4 * sqrt(N/k)), not floor(pi/4 * sqrt(N)) when there are k marked items. A more subtle error is confusing N with n: N = 2^n is the number of database items, while n is the number of qubits. Writing floor(pi/4 * sqrt(n)) instead of floor(pi/4 * sqrt(N)) drastically underestimates the iteration count.

// WRONG: using number of qubits instead of database size
let nQubits = 10;
let badIterations = Floor(PI() / 4.0 * Sqrt(IntAsDouble(nQubits)));  // 2 iterations

// CORRECT: using database size N = 2^n
let N = 1 <<< nQubits;  // 1024
let goodIterations = Floor(PI() / 4.0 * Sqrt(IntAsDouble(N)));  // 25 iterations

Little-Endian Bit Ordering

Q# uses little-endian qubit ordering. The least significant bit is qubit 0. This means:

  • IntAsBoolArray(5, 3) returns [true, false, true], not [true, false, true] in big-endian.
  • The state |101> in Q# means qubit 0 = 1, qubit 1 = 0, qubit 2 = 1, which is the integer 12^0 + 02^1 + 1*2^2 = 5.
  • If you build your oracle thinking qubit 0 is the most significant bit, you will mark the wrong state.

When checking measurement results, match the Q# convention:

let bits = ResultArrayAsBoolArray(results);
// bits[0] is the LEAST significant bit
// To check for decimal 5 (binary 101):
let isTarget = bits[0] and not bits[1] and bits[2];  // CORRECT

// NOT this (big-endian assumption):
// let isTarget = bits[2] and not bits[1] and bits[0];  // WRONG for Q#

Missing Adj+Ctl on Oracle Operations

The oracle and any operation called within a within ... apply block or as a controlled operation must support the required functors. If you forget the is Adj + Ctl annotation:

// WRONG: missing functor support
operation MyOracle(register : Qubit[], target : Qubit) : Unit {
    // ...
}

// The compiler will error when you try to use this in:
// within { ... } apply { MyOracle(register, target); }
// or: Controlled MyOracle(controls, (register, target));

The fix is simple but easy to forget:

// CORRECT: declare Adj + Ctl support
operation MyOracle(register : Qubit[], target : Qubit) : Unit
is Adj + Ctl {
    // ...
}

Note that the compiler can only auto-generate the adjoint if every operation called inside the body also supports Adj. If you call a non-adjointable operation, you need to provide a manual adjoint implementation using the adjoint keyword.

Qubit Lifetime Errors

Q# requires qubits to be in the |0> state when they go out of scope. A common mistake is measuring a qubit and then continuing to use the register, or forgetting to reset:

// WRONG: qubit not reset before scope ends
use q = Qubit();
H(q);
// Scope ends here; q might be |0> or |1> -- runtime error!

// CORRECT: measure and reset
use q = Qubit();
H(q);
let result = MResetZ(q);  // Measures AND resets to |0>

// Also correct: just reset without reading
use q = Qubit();
H(q);
Reset(q);

Functions vs. Operations

Functions cannot perform quantum operations. If you accidentally declare your oracle as a function instead of an operation, the compiler rejects any quantum gate calls inside it:

// WRONG: function cannot apply quantum gates
function BadOracle(register : Qubit[]) : Unit {
    X(register[0]);  // Compiler error: cannot call operation from function
}

// CORRECT: use operation
operation GoodOracle(register : Qubit[]) : Unit is Adj + Ctl {
    X(register[0]);
}

The distinction exists so the compiler can guarantee that functions have no quantum side effects, enabling classical optimization and compile-time evaluation.

Too Many Iterations (Overshooting)

Applying more than the optimal number of Grover iterations causes the state to rotate past the target and the success probability drops. This is not just a minor inefficiency; it can make the algorithm fail completely:

Iterations:    0     1      2       3       4      5
P(success):  12.5%  78.1%  94.5%  32.8%   0.5%  54.7%

At 4 iterations, the success probability drops to 0.5%, which is worse than random guessing. If you run Grover’s algorithm with an unknown number of solutions, you need a strategy to avoid this. Common approaches include:

  1. Quantum counting (described above): estimate k first, then choose the optimal iteration count.
  2. Exponential search: try 1, 2, 4, 8, … iterations, measuring after each. The expected total number of queries remains O(sqrt(N/k)).
  3. Randomized iteration count: pick a random number of iterations from a distribution that guarantees a constant probability of success regardless of k.

Connecting to Azure Quantum

To run on real hardware through Azure Quantum, replace the execution target in .csproj with a hardware provider target (e.g., ionq.simulator, quantinuum.hqs-lt-s1) and authenticate with:

az login
az quantum workspace set --resource-group myRG --workspace-name myWorkspace --location eastus
dotnet run

The Q# tooling submits the compiled job and streams results back to the terminal.

Key Takeaways

  • Grover’s algorithm achieves a provably optimal quadratic speedup for unstructured search, rotating the state vector by 2*theta per iteration in a two-dimensional subspace.
  • Q# expresses quantum subroutines as operations with adjoint and controlled variants the compiler derives automatically.
  • The within ... apply pattern cleanly encodes conjugation, the backbone of most oracle constructions.
  • Grover’s algorithm is modular: swap the oracle and the core diffusion loop remains unchanged. The SAT oracle example demonstrates this composability.
  • The amplitude amplification process is periodic: too many iterations is as harmful as too few. Always compute the optimal count from pi/4 * sqrt(N/k).
  • Quantum counting extends Grover’s algorithm to estimate the number of solutions before searching, solving the unknown-k problem.
  • The Resource Estimator is a free, fast way to understand fault-tolerant resource requirements before committing to hardware time. T-gate counts and magic state factory overhead dominate the cost of fault-tolerant circuits.
  • Debugging with DumpMachine() and testing with @Test assertions catches oracle and circuit errors before they waste hardware time.

Was this tutorial helpful?