Skip to content

Solving MaxCut with QAOA

This tutorial shows how to solve the maximum cut (MaxCut) combinatorial optimization problem on a graph using the Quantum Approximate Optimization Algorithm (QAOA), first introduced by Farhi et al. in 2014 1.

Given an arbitrary graph, the MaxCut problem consists in finding a graph cut which partitions the nodes into two disjoint sets, such that the number of edges in the cut is maximized. This is a very common combinatorial optimization problem known to be computationally hard (NP-hard).

The graph used for this tutorial is an unweighted graph randomly generated using the networkx library with a certain probability \(p\) of having an edge between two arbitrary nodes (known as Erdős–Rényi graph).

import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
import random

# ensure reproducibility
seed = 42
np.random.seed(seed)
random.seed(seed)

# Create random graph
n_nodes = 4
edge_prob = 0.8
graph = nx.gnp_random_graph(n_nodes, edge_prob)

nx.draw(graph)
2025-05-12T10:18:41.858957 image/svg+xml Matplotlib v3.10.3, https://matplotlib.org/

The goal of the MaxCut algorithm is to maximize the following cost function:

\[\mathcal{C}(p) = \sum_{\alpha}^m \mathcal{C}_{\alpha}(p)\]

where \(p\) is a given cut of the graph, \(\alpha\) is an index over the edges and \(\mathcal{C}_{\alpha}(p)\) is written such that if the nodes connected by the \(\alpha\) edge are in the same set, it returns \(0\), otherwise it returns \(1\). We will represent a cut \(p\) as a bitstring of length \(N\), where \(N\) is the number of nodes, and where the bit in position \(i\) shows to which partition node \(i\) belongs. We assign value 0 to one of the partitions defined by the cut and 1 to the other. Since this choice is arbitrary, every cut is represented by two bitstrings, e.g. "0011" and "1100" are equivalent.

Since in this tutorial we are only dealing with small graphs, we can find the maximum cut by brute force to make sure QAOA works as intended.

# Function to calculate the cost associated with a cut
def calculate_cost(cut: str, graph: nx.graph) -> float:
    """Returns the cost of a given cut (represented by a bitstring)"""
    cost = 0
    for edge in graph.edges():
        (i, j) = edge
        if cut[i] != cut[j]:
            cost += 1
    return cost


# Function to get a binary representation of an int
get_binary = lambda x, n: format(x, "b").zfill(n)

# List of all possible cuts
all_possible_cuts = [bin(k)[2:].rjust(n_nodes, "0") for k in range(2**n_nodes)]

# List with the costs associated to each cut
all_costs = [calculate_cost(cut, graph) for cut in all_possible_cuts]

# Get the maximum cost
maxcost = max(all_costs)

# Get all cuts that correspond to the maximum cost
maxcuts = [get_binary(i, n_nodes) for i, j in enumerate(all_costs) if j == maxcost]
print(f"The maximum cut is represented by the bitstrings {maxcuts}, with a cost of {maxcost}")
The maximum cut is represented by the bitstrings ['0011', '0101', '0110', '1001', '1010', '1100'], with a cost of 4

The QAOA quantum circuit

The Max-Cut problem can be solved by using the QAOA algorithm. QAOA belongs to the class of Variational Quantum Algorithms (VQAs), which means that its quantum circuit contains a certain number of parametrized quantum gates that need to be optimized with a classical optimizer. The QAOA circuit is composed of two operators:

  • The cost operator \(U_c\): a circuit generated by the cost Hamiltonian which encodes the cost function described above into a quantum circuit. The solution to the optimization problem is encoded in the ground state of the cost Hamiltonian \(H_c\). The cost operator is simply the evolution of the cost Hamiltonian parametrized by a variational parameter \(\gamma\) so that \(U_c = e^{i\gamma H_c}.\)
  • The mixing operator \(U_b\): a simple set of single-qubit rotations with adjustable angles which are tuned during the classical optimization loop to minimize the cost

The cost Hamiltonian of the MaxCut problem can be written as:

\[H_c = \frac12 \sum_{\langle i,j\rangle} (\mathbb{1} - Z_iZ_j)\]

where \(\langle i,j\rangle\) represents the edge between nodes \(i\) and \(j\). The solution of the MaxCut problem is encoded in the ground state of the above Hamiltonian.

The QAOA quantum circuit consists of a number of layers, each layer containing a cost and a mixing operator. Below, the QAOA quantum circuit is defined using qadence operations. First, a layer of Hadamard gates is applied to all qubits to prepare the initial state \(|+\rangle ^{\otimes n}\). The cost operator of each layer can be built "manually", implementing the \(e^{iZZ\gamma}\) terms with CNOTs and a \(\rm{RZ}(2\gamma)\) rotation, or it can also be automatically decomposed into digital single and two-qubits operations via the .digital_decomposition() method. The decomposition is exact since the Hamiltonian generator is diagonal.

from qadence import tag, kron, chain, RX, RZ, Z, H, CNOT, I, add
from qadence import HamEvo, QuantumCircuit, Parameter

n_qubits = graph.number_of_nodes()
n_edges = graph.number_of_edges()
n_layers = 6

# Generate the cost Hamiltonian
zz_ops = add(Z(edge[0]) @ Z(edge[1]) for edge in graph.edges)
cost_ham = 0.5 * (n_edges * kron(I(i) for i in range(n_qubits)) - zz_ops)


# QAOA circuit
def build_qaoa_circuit(n_qubits, n_layers, graph):
    layers = []
    # Layer of Hadamards
    initial_layer = kron(H(i) for i in range(n_qubits))
    layers.append(initial_layer)
    for layer in range(n_layers):

        # cost layer with digital decomposition
        # cost_layer = HamEvo(cost_ham, f"g{layer}").digital_decomposition(approximation="basic")
        cost_layer = []
        for edge in graph.edges():
            (q0, q1) = edge
            zz_term = chain(
                CNOT(q0, q1),
                RZ(q1, Parameter(f"g{layer}")),
                CNOT(q0, q1),
            )
            cost_layer.append(zz_term)
        cost_layer = chain(*cost_layer)
        cost_layer = tag(cost_layer, "cost")

        # mixing layer with single qubit rotations
        mixing_layer = kron(RX(i, f"b{layer}") for i in range(n_qubits))
        mixing_layer = tag(mixing_layer, "mixing")

        # putting all together in a single ChainBlock
        layers.append(chain(cost_layer, mixing_layer))

    final_b = chain(*layers)
    return QuantumCircuit(n_qubits, final_b)


circuit = build_qaoa_circuit(n_qubits, n_layers, graph)

# Print a single layer of the circuit
%3 cluster_d66c2c90cb6641499a1a693024ca1cc6 mixing cluster_20afcd2ecb5045d797c0c13b53d2c49f cost b9247efaa99a48c99f65cd4e6ca9b256 0 115e38f6401447b3bea07e1fe67c5dd0 H b9247efaa99a48c99f65cd4e6ca9b256--115e38f6401447b3bea07e1fe67c5dd0 229ba5e511d44e2486f4f109f5e8a5cc 1 a930c54d605b40e7b6b6fb153affc7b2 115e38f6401447b3bea07e1fe67c5dd0--a930c54d605b40e7b6b6fb153affc7b2 2478b6d21e764f709bb192c3bbd946cf a930c54d605b40e7b6b6fb153affc7b2--2478b6d21e764f709bb192c3bbd946cf 12d4015849bb4368b0a1b7a170ff8720 2478b6d21e764f709bb192c3bbd946cf--12d4015849bb4368b0a1b7a170ff8720 978ac71240834b6dbe465a02350ddd3c 12d4015849bb4368b0a1b7a170ff8720--978ac71240834b6dbe465a02350ddd3c 33dd3174f7954e4996ec34d0d1ef7f24 978ac71240834b6dbe465a02350ddd3c--33dd3174f7954e4996ec34d0d1ef7f24 95602c41b5184ec88115a60093f66ecb 33dd3174f7954e4996ec34d0d1ef7f24--95602c41b5184ec88115a60093f66ecb c686634d52e34d6da6431a5175d6996e 95602c41b5184ec88115a60093f66ecb--c686634d52e34d6da6431a5175d6996e 3dae4adedd46417fba99e24eb5bcfcb5 c686634d52e34d6da6431a5175d6996e--3dae4adedd46417fba99e24eb5bcfcb5 2483515f1f7648a09464eedf60b541a0 3dae4adedd46417fba99e24eb5bcfcb5--2483515f1f7648a09464eedf60b541a0 e8a8d44bae54494c8ab32d88b34e163f 2483515f1f7648a09464eedf60b541a0--e8a8d44bae54494c8ab32d88b34e163f 83f273a212334518ae45b67b1447545b e8a8d44bae54494c8ab32d88b34e163f--83f273a212334518ae45b67b1447545b 7cbbc9a990864338943d749faeadc929 83f273a212334518ae45b67b1447545b--7cbbc9a990864338943d749faeadc929 a1ca02a588064011aea5d9a3ea9e97bd 7cbbc9a990864338943d749faeadc929--a1ca02a588064011aea5d9a3ea9e97bd 81d6e32c411d43d499d14a38f762e705 a1ca02a588064011aea5d9a3ea9e97bd--81d6e32c411d43d499d14a38f762e705 f104d9cc93c84638bd158c72972fbc15 81d6e32c411d43d499d14a38f762e705--f104d9cc93c84638bd158c72972fbc15 9e7509ce26e345ca97ba4a6011dce8e3 f104d9cc93c84638bd158c72972fbc15--9e7509ce26e345ca97ba4a6011dce8e3 ad9456f634474aa9aaabcb24a40edd35 9e7509ce26e345ca97ba4a6011dce8e3--ad9456f634474aa9aaabcb24a40edd35 b2b9af0901914bcb842450fca9c5a215 ad9456f634474aa9aaabcb24a40edd35--b2b9af0901914bcb842450fca9c5a215 360c5a3efdd84afe9a41c2ea750a706d RX(b0) b2b9af0901914bcb842450fca9c5a215--360c5a3efdd84afe9a41c2ea750a706d 21203d6aaf5840869249cfc7845fe5f4 360c5a3efdd84afe9a41c2ea750a706d--21203d6aaf5840869249cfc7845fe5f4 d6382a3216004bb19f74648f4b4affa8 cf7fc9e60c214af7bb7472f2cefa34b7 H 229ba5e511d44e2486f4f109f5e8a5cc--cf7fc9e60c214af7bb7472f2cefa34b7 2acede9000a1439f9e9f58139d6e6d0e 2 7528087378f9441fb4124156065fd338 X cf7fc9e60c214af7bb7472f2cefa34b7--7528087378f9441fb4124156065fd338 7528087378f9441fb4124156065fd338--a930c54d605b40e7b6b6fb153affc7b2 45258f62555a447191a5805de2a71b07 RZ(g0) 7528087378f9441fb4124156065fd338--45258f62555a447191a5805de2a71b07 38963bfdb19b4ac0a083368adf5595f0 X 45258f62555a447191a5805de2a71b07--38963bfdb19b4ac0a083368adf5595f0 38963bfdb19b4ac0a083368adf5595f0--12d4015849bb4368b0a1b7a170ff8720 248051d7864a418bbc8108e918865080 38963bfdb19b4ac0a083368adf5595f0--248051d7864a418bbc8108e918865080 90159149bffe4ee598b2787a3bfa3f99 248051d7864a418bbc8108e918865080--90159149bffe4ee598b2787a3bfa3f99 7695c1df2e3245e08572e0d9c27e9dda 90159149bffe4ee598b2787a3bfa3f99--7695c1df2e3245e08572e0d9c27e9dda fd750555f6864b0998a6258df59c5304 7695c1df2e3245e08572e0d9c27e9dda--fd750555f6864b0998a6258df59c5304 9da8c021061a4d88aa8963d169eb890a fd750555f6864b0998a6258df59c5304--9da8c021061a4d88aa8963d169eb890a 24539fe45cea41c58640f6b9f900e900 9da8c021061a4d88aa8963d169eb890a--24539fe45cea41c58640f6b9f900e900 2c10e086ffa44b80a7a6607ee07e23ab 24539fe45cea41c58640f6b9f900e900--2c10e086ffa44b80a7a6607ee07e23ab 16040445fb6342a8961f91b777a57cea 2c10e086ffa44b80a7a6607ee07e23ab--16040445fb6342a8961f91b777a57cea 1e906c56efbf49baa1932a30156efa81 16040445fb6342a8961f91b777a57cea--1e906c56efbf49baa1932a30156efa81 b06f5afef7a84ddfb21ed69cd41d6684 1e906c56efbf49baa1932a30156efa81--b06f5afef7a84ddfb21ed69cd41d6684 58b2322c1e5949c2a21e289c6ff0de53 b06f5afef7a84ddfb21ed69cd41d6684--58b2322c1e5949c2a21e289c6ff0de53 8270a255bbc04ba1a348f0836e5badfe 58b2322c1e5949c2a21e289c6ff0de53--8270a255bbc04ba1a348f0836e5badfe f509f16b8f2f4c19b889536e73053de4 8270a255bbc04ba1a348f0836e5badfe--f509f16b8f2f4c19b889536e73053de4 84fcc625c2174d63b1b4541a74dc8ab7 f509f16b8f2f4c19b889536e73053de4--84fcc625c2174d63b1b4541a74dc8ab7 1248775a044544ba9575f5eeb1ae4b04 84fcc625c2174d63b1b4541a74dc8ab7--1248775a044544ba9575f5eeb1ae4b04 70690f5a67eb426089b2d498d2c51f31 RX(b0) 1248775a044544ba9575f5eeb1ae4b04--70690f5a67eb426089b2d498d2c51f31 70690f5a67eb426089b2d498d2c51f31--d6382a3216004bb19f74648f4b4affa8 f1a5cc9d12224011b229940d90d0efa8 a15326e6bee3440a8c799dab877dbb11 H 2acede9000a1439f9e9f58139d6e6d0e--a15326e6bee3440a8c799dab877dbb11 e1d9ea1b28c3410ba9ce764eea479152 3 d3b1e2771d5040b69eef47f12638ef9d a15326e6bee3440a8c799dab877dbb11--d3b1e2771d5040b69eef47f12638ef9d e678520fe90d45edae1056fda70b8ec2 d3b1e2771d5040b69eef47f12638ef9d--e678520fe90d45edae1056fda70b8ec2 37028e8719714059a8e8e8edeabf0461 e678520fe90d45edae1056fda70b8ec2--37028e8719714059a8e8e8edeabf0461 160318c12d9947a88ed757eae744de58 X 37028e8719714059a8e8e8edeabf0461--160318c12d9947a88ed757eae744de58 160318c12d9947a88ed757eae744de58--978ac71240834b6dbe465a02350ddd3c 701f42a999b0419f8264261a0c8e41b2 RZ(g0) 160318c12d9947a88ed757eae744de58--701f42a999b0419f8264261a0c8e41b2 2ac3986fe3b6407b9d08b01d9cdb8467 X 701f42a999b0419f8264261a0c8e41b2--2ac3986fe3b6407b9d08b01d9cdb8467 2ac3986fe3b6407b9d08b01d9cdb8467--95602c41b5184ec88115a60093f66ecb d58107fb4cca4a2487092c6729c14796 2ac3986fe3b6407b9d08b01d9cdb8467--d58107fb4cca4a2487092c6729c14796 df13438a040348ad930c17bf56716c6c d58107fb4cca4a2487092c6729c14796--df13438a040348ad930c17bf56716c6c 9bc55faf35864da2b1bc9ecad0c9551d df13438a040348ad930c17bf56716c6c--9bc55faf35864da2b1bc9ecad0c9551d 1e239d386593429093321cc4e0c723bb X 9bc55faf35864da2b1bc9ecad0c9551d--1e239d386593429093321cc4e0c723bb 1e239d386593429093321cc4e0c723bb--2c10e086ffa44b80a7a6607ee07e23ab d0faa4614dd4434792edf6b7e7d0b2b5 RZ(g0) 1e239d386593429093321cc4e0c723bb--d0faa4614dd4434792edf6b7e7d0b2b5 0088e394dd2a4163b17231229c8aa6d9 X d0faa4614dd4434792edf6b7e7d0b2b5--0088e394dd2a4163b17231229c8aa6d9 0088e394dd2a4163b17231229c8aa6d9--1e906c56efbf49baa1932a30156efa81 6866261a0eed4ef583f2fc90ea79e058 0088e394dd2a4163b17231229c8aa6d9--6866261a0eed4ef583f2fc90ea79e058 4121f6c9ed494783bcf4e75cdc2e5d92 6866261a0eed4ef583f2fc90ea79e058--4121f6c9ed494783bcf4e75cdc2e5d92 c10aa8531b204fd0a191838bc05100ea 4121f6c9ed494783bcf4e75cdc2e5d92--c10aa8531b204fd0a191838bc05100ea 8ebe01187ae94e4aa47f03cbc13feeb4 c10aa8531b204fd0a191838bc05100ea--8ebe01187ae94e4aa47f03cbc13feeb4 eda9e7b9dc0a4a99875cb86333a26719 8ebe01187ae94e4aa47f03cbc13feeb4--eda9e7b9dc0a4a99875cb86333a26719 bb450626af894d688ac85d7227c3f8e1 eda9e7b9dc0a4a99875cb86333a26719--bb450626af894d688ac85d7227c3f8e1 42d1368f89be4e37b1703df4f114e0a6 RX(b0) bb450626af894d688ac85d7227c3f8e1--42d1368f89be4e37b1703df4f114e0a6 42d1368f89be4e37b1703df4f114e0a6--f1a5cc9d12224011b229940d90d0efa8 d8316a9722764ec7b0e3b2a3ab1eb52a bd4d90d466ff4c3389f3485dddd9b0ba H e1d9ea1b28c3410ba9ce764eea479152--bd4d90d466ff4c3389f3485dddd9b0ba 70c051bb73fb41dc93c01b0597342c87 bd4d90d466ff4c3389f3485dddd9b0ba--70c051bb73fb41dc93c01b0597342c87 371f2e09fdd84b4d90322183d6d3a316 70c051bb73fb41dc93c01b0597342c87--371f2e09fdd84b4d90322183d6d3a316 6e2bf09a2c964d41bdbc6cf960a677ac 371f2e09fdd84b4d90322183d6d3a316--6e2bf09a2c964d41bdbc6cf960a677ac 148eb55b4ca3410793d487d9f9c4d946 6e2bf09a2c964d41bdbc6cf960a677ac--148eb55b4ca3410793d487d9f9c4d946 6b1a821f36224e678b576ec5d6a9ee48 148eb55b4ca3410793d487d9f9c4d946--6b1a821f36224e678b576ec5d6a9ee48 eba7b82f9f564818a3b36708af96054c 6b1a821f36224e678b576ec5d6a9ee48--eba7b82f9f564818a3b36708af96054c 1dc981c07f344b87929d8a9301151c33 X eba7b82f9f564818a3b36708af96054c--1dc981c07f344b87929d8a9301151c33 1dc981c07f344b87929d8a9301151c33--c686634d52e34d6da6431a5175d6996e 6529fe0a47564b1688e4426c672f721f RZ(g0) 1dc981c07f344b87929d8a9301151c33--6529fe0a47564b1688e4426c672f721f cbf4d84a7a8b486c9937e21fa834c599 X 6529fe0a47564b1688e4426c672f721f--cbf4d84a7a8b486c9937e21fa834c599 cbf4d84a7a8b486c9937e21fa834c599--2483515f1f7648a09464eedf60b541a0 b26cdb6f52e5469b8f7b12fc5a1d35bf cbf4d84a7a8b486c9937e21fa834c599--b26cdb6f52e5469b8f7b12fc5a1d35bf 057be2eae7e6489d82d76c1a2287410d b26cdb6f52e5469b8f7b12fc5a1d35bf--057be2eae7e6489d82d76c1a2287410d a651a47f8996451abd2e76286cc9d660 057be2eae7e6489d82d76c1a2287410d--a651a47f8996451abd2e76286cc9d660 67ccec53584b481bba438be587ce1cb0 X a651a47f8996451abd2e76286cc9d660--67ccec53584b481bba438be587ce1cb0 67ccec53584b481bba438be587ce1cb0--b06f5afef7a84ddfb21ed69cd41d6684 84361462aec344eda5a782ad4cc7d362 RZ(g0) 67ccec53584b481bba438be587ce1cb0--84361462aec344eda5a782ad4cc7d362 fdedffbfe04c42f193089c8cd10d8eb4 X 84361462aec344eda5a782ad4cc7d362--fdedffbfe04c42f193089c8cd10d8eb4 fdedffbfe04c42f193089c8cd10d8eb4--8270a255bbc04ba1a348f0836e5badfe 3c0afd0d9ef74fb9824a89c58c898460 X fdedffbfe04c42f193089c8cd10d8eb4--3c0afd0d9ef74fb9824a89c58c898460 3c0afd0d9ef74fb9824a89c58c898460--8ebe01187ae94e4aa47f03cbc13feeb4 2cc156c6923e466ab0267248da75d3a3 RZ(g0) 3c0afd0d9ef74fb9824a89c58c898460--2cc156c6923e466ab0267248da75d3a3 19d0bd4c6593477db56240b5a04cb981 X 2cc156c6923e466ab0267248da75d3a3--19d0bd4c6593477db56240b5a04cb981 19d0bd4c6593477db56240b5a04cb981--bb450626af894d688ac85d7227c3f8e1 b5031ff9416d480ebb49d2891205e199 RX(b0) 19d0bd4c6593477db56240b5a04cb981--b5031ff9416d480ebb49d2891205e199 b5031ff9416d480ebb49d2891205e199--d8316a9722764ec7b0e3b2a3ab1eb52a

Train the QAOA circuit to solve MaxCut

Given the QAOA circuit above, one can construct the associated Qadence QuantumModel and train it using standard gradient based optimization.

The loss function to be minimized reads:

\[\mathcal{L} =-\langle \psi | H_c| \psi \rangle= -\frac12 \sum_{\langle i,j\rangle} \left(1 - \langle \psi | Z_i Z_j | \psi \rangle \right)\]

where \(|\psi\rangle(\beta, \gamma)\) is the wavefunction obtained by running the QAQA quantum circuit and the sum runs over the edges of the graph \(\langle i,j\rangle\).

import torch
from qadence import QuantumModel

torch.manual_seed(seed)


def loss_function(model: QuantumModel):
    # The loss corresponds to the expectation
    # value of the cost Hamiltonian
    return -1.0 * model.expectation().squeeze()


# initialize the parameters to random values
model = QuantumModel(circuit, observable=cost_ham)
model.reset_vparams(torch.rand(model.num_vparams))
initial_loss = loss_function(model)
print(f"Initial loss: {initial_loss}")

# train the model
n_epochs = 100
lr = 0.1

optimizer = torch.optim.Adam(model.parameters(), lr=lr)

for i in range(n_epochs):
    optimizer.zero_grad()
    loss = loss_function(model)
    loss.backward()
    optimizer.step()
    if (i + 1) % (n_epochs // 10) == 0:
        print(f"MaxCut cost at iteration {i+1}: {-loss.item()}")
Initial loss: -2.1782381363858794
MaxCut cost at iteration 10: 3.7470706807026417
MaxCut cost at iteration 20: 3.8378810288930216
MaxCut cost at iteration 30: 3.9424197899236133
MaxCut cost at iteration 40: 3.9981256255766002
MaxCut cost at iteration 50: 3.996470528508214
MaxCut cost at iteration 60: 3.9991374608876606
MaxCut cost at iteration 70: 3.9994678542919555
MaxCut cost at iteration 80: 3.999872558672829
MaxCut cost at iteration 90: 3.9999475834121063
MaxCut cost at iteration 100: 3.9999793311641003

Qadence offers some convenience functions to implement this training loop with advanced logging and metrics track features. You can refer to this tutorial for more details.

Results

Given the trained quantum model, one needs to sample the resulting quantum state to recover the bitstring with the highest probability which corresponds to the maximum cut of the graph.

samples = model.sample(n_shots=100)[0]
most_frequent = max(samples, key=samples.get)

print(f"Most frequently sampled bitstring corresponding to the maximum cut: {most_frequent}")

# let's now draw the cut obtained with the QAOA procedure
colors = []
labels = {}
for node, b in zip(graph.nodes(), most_frequent):
    colors.append("green") if int(b) == 0 else colors.append("red")
    labels[node] = "A" if int(b) == 0 else "B"

nx.draw_networkx(graph, node_color=colors, with_labels=True, labels=labels)
Most frequently sampled bitstring corresponding to the maximum cut: 1001 2025-05-12T10:18:45.635607 image/svg+xml Matplotlib v3.10.3, https://matplotlib.org/

References


  1. Farhi et al. - A Quantum Approximate Optimization Algorithm