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-06-12T18:24:27.763686 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_08af060e37794488a21e0b7395d5f7c8 mixing cluster_29bfcddb5b764c38adda8d19ec0dfbed cost 328512db9c104d5794b4ef3f33445346 0 c336b9e2773741d19613437d031a4a20 H 328512db9c104d5794b4ef3f33445346--c336b9e2773741d19613437d031a4a20 90348cf8427543f4b37fd55442de12b2 1 f2a485f0654840489be78fb87dcbeb02 c336b9e2773741d19613437d031a4a20--f2a485f0654840489be78fb87dcbeb02 4d4e6c56c85f497fa96002493f0c1d4a f2a485f0654840489be78fb87dcbeb02--4d4e6c56c85f497fa96002493f0c1d4a 1b7b0cf9b85d44d3baaed709f9367e12 4d4e6c56c85f497fa96002493f0c1d4a--1b7b0cf9b85d44d3baaed709f9367e12 5b954357e35d4912996004fa631f003e 1b7b0cf9b85d44d3baaed709f9367e12--5b954357e35d4912996004fa631f003e be4f5c5733c6479eb7268ea36435f2e3 5b954357e35d4912996004fa631f003e--be4f5c5733c6479eb7268ea36435f2e3 b141dbda3f4a4976b5e270784227cf36 be4f5c5733c6479eb7268ea36435f2e3--b141dbda3f4a4976b5e270784227cf36 f54328cfdfc147f8a82163039661f8e1 b141dbda3f4a4976b5e270784227cf36--f54328cfdfc147f8a82163039661f8e1 fcfd50668ce844578267c6b1632fdf8f f54328cfdfc147f8a82163039661f8e1--fcfd50668ce844578267c6b1632fdf8f ea29676722af4f2aab9565cbf656fb84 fcfd50668ce844578267c6b1632fdf8f--ea29676722af4f2aab9565cbf656fb84 9bfa79f800e84871a5416d0113824078 ea29676722af4f2aab9565cbf656fb84--9bfa79f800e84871a5416d0113824078 eb24a6d538f5474a830a5d6a89f8a453 9bfa79f800e84871a5416d0113824078--eb24a6d538f5474a830a5d6a89f8a453 5af7d3deb9b2470b9aa5a381fd7cbfb7 eb24a6d538f5474a830a5d6a89f8a453--5af7d3deb9b2470b9aa5a381fd7cbfb7 06a009e99eb645a99f92f804de224f14 5af7d3deb9b2470b9aa5a381fd7cbfb7--06a009e99eb645a99f92f804de224f14 429ea28f78f14862bbe828caa3f44e10 06a009e99eb645a99f92f804de224f14--429ea28f78f14862bbe828caa3f44e10 fa5bfb277fc64b13b474b9f0051e4d07 429ea28f78f14862bbe828caa3f44e10--fa5bfb277fc64b13b474b9f0051e4d07 6eecd7eb9fec4e48ba0921f55f6689fd fa5bfb277fc64b13b474b9f0051e4d07--6eecd7eb9fec4e48ba0921f55f6689fd 81f00e0e01374dc4b64fcbc364de595e 6eecd7eb9fec4e48ba0921f55f6689fd--81f00e0e01374dc4b64fcbc364de595e b3cb9563718847e09f5d46079bf75fbb 81f00e0e01374dc4b64fcbc364de595e--b3cb9563718847e09f5d46079bf75fbb 9e3dc53fa257441488e1b192da30d7b4 RX(b0) b3cb9563718847e09f5d46079bf75fbb--9e3dc53fa257441488e1b192da30d7b4 c5e66879321343428f7a93faa90e0789 9e3dc53fa257441488e1b192da30d7b4--c5e66879321343428f7a93faa90e0789 7a362d6bcb5748a0859391f4b619ce60 22833dbc9c874eceb63f44e1d72fd0a7 H 90348cf8427543f4b37fd55442de12b2--22833dbc9c874eceb63f44e1d72fd0a7 1b463e216bab40fea23c28e6ddd80110 2 99150a5ffeef4cb3a5bd80e66113857c X 22833dbc9c874eceb63f44e1d72fd0a7--99150a5ffeef4cb3a5bd80e66113857c 99150a5ffeef4cb3a5bd80e66113857c--f2a485f0654840489be78fb87dcbeb02 f1bf89ad43b647b496ff45fe8bd9b692 RZ(g0) 99150a5ffeef4cb3a5bd80e66113857c--f1bf89ad43b647b496ff45fe8bd9b692 3c589cd781e34dd1bd1bca065f393071 X f1bf89ad43b647b496ff45fe8bd9b692--3c589cd781e34dd1bd1bca065f393071 3c589cd781e34dd1bd1bca065f393071--1b7b0cf9b85d44d3baaed709f9367e12 ca199762ab0542aeb7da57823d46fd3e 3c589cd781e34dd1bd1bca065f393071--ca199762ab0542aeb7da57823d46fd3e fa1a79806781483583a5eeb9ed2ed894 ca199762ab0542aeb7da57823d46fd3e--fa1a79806781483583a5eeb9ed2ed894 fee9edc3c018428097fef741ea933603 fa1a79806781483583a5eeb9ed2ed894--fee9edc3c018428097fef741ea933603 a030c12fb3ed4a97ae123a203058e6bd fee9edc3c018428097fef741ea933603--a030c12fb3ed4a97ae123a203058e6bd 4b05fa633981430eb58f37a1941ece76 a030c12fb3ed4a97ae123a203058e6bd--4b05fa633981430eb58f37a1941ece76 4c2a836617d04aee9d232a7565084186 4b05fa633981430eb58f37a1941ece76--4c2a836617d04aee9d232a7565084186 bcb48bc8304245e8895c52e6e2f3dfcd 4c2a836617d04aee9d232a7565084186--bcb48bc8304245e8895c52e6e2f3dfcd a8764edef91447d2959f4f5fbee2ed68 bcb48bc8304245e8895c52e6e2f3dfcd--a8764edef91447d2959f4f5fbee2ed68 2f27f00f6b4845929c97ca182de10315 a8764edef91447d2959f4f5fbee2ed68--2f27f00f6b4845929c97ca182de10315 e30dc3ab4d3b4f718e91cd2b7baab42c 2f27f00f6b4845929c97ca182de10315--e30dc3ab4d3b4f718e91cd2b7baab42c 304aff35d7684241a7ad5acfbee08114 e30dc3ab4d3b4f718e91cd2b7baab42c--304aff35d7684241a7ad5acfbee08114 448b0894b7df42048fb5b691dcdf8c3f 304aff35d7684241a7ad5acfbee08114--448b0894b7df42048fb5b691dcdf8c3f 24d2d397169944599a9f127e949a2150 448b0894b7df42048fb5b691dcdf8c3f--24d2d397169944599a9f127e949a2150 8e321842ae3849ff9a654254833cf2cb 24d2d397169944599a9f127e949a2150--8e321842ae3849ff9a654254833cf2cb 22495d0d41484237aa59d7a18a0e1ec5 8e321842ae3849ff9a654254833cf2cb--22495d0d41484237aa59d7a18a0e1ec5 581284bb46904fc284bdcc3655ee0a0e RX(b0) 22495d0d41484237aa59d7a18a0e1ec5--581284bb46904fc284bdcc3655ee0a0e 581284bb46904fc284bdcc3655ee0a0e--7a362d6bcb5748a0859391f4b619ce60 0c66d4a8e11d439d9a670b3a361e0674 e8fd63db5b6b42e994df68d5fcff48f0 H 1b463e216bab40fea23c28e6ddd80110--e8fd63db5b6b42e994df68d5fcff48f0 24a6cf549e724adb8c1cd985e9a17b5b 3 cf088c73b999479db6e5fdb0e8a189d5 e8fd63db5b6b42e994df68d5fcff48f0--cf088c73b999479db6e5fdb0e8a189d5 53a6470bc49b469a890c6ae063ff703b cf088c73b999479db6e5fdb0e8a189d5--53a6470bc49b469a890c6ae063ff703b 647f928b5f23403ea5090fdd366ffae6 53a6470bc49b469a890c6ae063ff703b--647f928b5f23403ea5090fdd366ffae6 c1b717c14a654aedb4e6c3f27d7b1d02 X 647f928b5f23403ea5090fdd366ffae6--c1b717c14a654aedb4e6c3f27d7b1d02 c1b717c14a654aedb4e6c3f27d7b1d02--5b954357e35d4912996004fa631f003e fa3da9052e45488da59ea1c11dfca297 RZ(g0) c1b717c14a654aedb4e6c3f27d7b1d02--fa3da9052e45488da59ea1c11dfca297 f762c9a0a8f94a89a5494300faf570d1 X fa3da9052e45488da59ea1c11dfca297--f762c9a0a8f94a89a5494300faf570d1 f762c9a0a8f94a89a5494300faf570d1--b141dbda3f4a4976b5e270784227cf36 966ea16edd1c4bbc96d43099c6612a41 f762c9a0a8f94a89a5494300faf570d1--966ea16edd1c4bbc96d43099c6612a41 34ab3bf16b1a493393e2550e64ed0949 966ea16edd1c4bbc96d43099c6612a41--34ab3bf16b1a493393e2550e64ed0949 ae28a3b3634c48ab8b920f8475ec9017 34ab3bf16b1a493393e2550e64ed0949--ae28a3b3634c48ab8b920f8475ec9017 08e84cc38cd541f8a65c1dd1a69af248 X ae28a3b3634c48ab8b920f8475ec9017--08e84cc38cd541f8a65c1dd1a69af248 08e84cc38cd541f8a65c1dd1a69af248--bcb48bc8304245e8895c52e6e2f3dfcd bbe305027a354e70930b706d96bba223 RZ(g0) 08e84cc38cd541f8a65c1dd1a69af248--bbe305027a354e70930b706d96bba223 79d68781febb4875935f969c226cee23 X bbe305027a354e70930b706d96bba223--79d68781febb4875935f969c226cee23 79d68781febb4875935f969c226cee23--2f27f00f6b4845929c97ca182de10315 4ebc8e1315094f2b8fccb6fd794ee72b 79d68781febb4875935f969c226cee23--4ebc8e1315094f2b8fccb6fd794ee72b 154986c85295475d8d3b29c3c5d74c5d 4ebc8e1315094f2b8fccb6fd794ee72b--154986c85295475d8d3b29c3c5d74c5d dd01b7200d654792ac289f9f5a3ba260 154986c85295475d8d3b29c3c5d74c5d--dd01b7200d654792ac289f9f5a3ba260 99efebcab4cc486a9e38fe85af28ae54 dd01b7200d654792ac289f9f5a3ba260--99efebcab4cc486a9e38fe85af28ae54 2db9f66e1db64f2eb0d2b04e705451f6 99efebcab4cc486a9e38fe85af28ae54--2db9f66e1db64f2eb0d2b04e705451f6 f026d920b22544d3aa1a91508ce0bb7d 2db9f66e1db64f2eb0d2b04e705451f6--f026d920b22544d3aa1a91508ce0bb7d c5cce78b1a7448a285e9be07237d5ce6 RX(b0) f026d920b22544d3aa1a91508ce0bb7d--c5cce78b1a7448a285e9be07237d5ce6 c5cce78b1a7448a285e9be07237d5ce6--0c66d4a8e11d439d9a670b3a361e0674 545ecc28ed234e1bbbb0a4ed10ce14d4 d1c3525a9082441dbee0abe4d7f9e6d6 H 24a6cf549e724adb8c1cd985e9a17b5b--d1c3525a9082441dbee0abe4d7f9e6d6 b7124347160d4a8e9fdad274baa2f118 d1c3525a9082441dbee0abe4d7f9e6d6--b7124347160d4a8e9fdad274baa2f118 131139fc84e746ec9ac8758e7c6059ee b7124347160d4a8e9fdad274baa2f118--131139fc84e746ec9ac8758e7c6059ee d29b8607ec164041937a52ca756c646a 131139fc84e746ec9ac8758e7c6059ee--d29b8607ec164041937a52ca756c646a 304b5b7fc0504a2898ba51f3eddfed15 d29b8607ec164041937a52ca756c646a--304b5b7fc0504a2898ba51f3eddfed15 87d882535d2e43db81186ed99f93a06b 304b5b7fc0504a2898ba51f3eddfed15--87d882535d2e43db81186ed99f93a06b 0d24f48cb9774f66bac0f51667afbd46 87d882535d2e43db81186ed99f93a06b--0d24f48cb9774f66bac0f51667afbd46 8a3f60fe5dce4816be3c241272e0b8ab X 0d24f48cb9774f66bac0f51667afbd46--8a3f60fe5dce4816be3c241272e0b8ab 8a3f60fe5dce4816be3c241272e0b8ab--f54328cfdfc147f8a82163039661f8e1 e532b570406b4f529d01c624b2f84a32 RZ(g0) 8a3f60fe5dce4816be3c241272e0b8ab--e532b570406b4f529d01c624b2f84a32 cc659f39dd2548cc8b18984d588fb770 X e532b570406b4f529d01c624b2f84a32--cc659f39dd2548cc8b18984d588fb770 cc659f39dd2548cc8b18984d588fb770--ea29676722af4f2aab9565cbf656fb84 d9736accaa0f4384b8d62d6b7a3ab15b cc659f39dd2548cc8b18984d588fb770--d9736accaa0f4384b8d62d6b7a3ab15b 726b466527a747e3815128cf7bb7cf38 d9736accaa0f4384b8d62d6b7a3ab15b--726b466527a747e3815128cf7bb7cf38 a00c1c9a92324012bbbf2e7f982fb9b7 726b466527a747e3815128cf7bb7cf38--a00c1c9a92324012bbbf2e7f982fb9b7 cca199513c6943b3a3b68e2d50855623 X a00c1c9a92324012bbbf2e7f982fb9b7--cca199513c6943b3a3b68e2d50855623 cca199513c6943b3a3b68e2d50855623--e30dc3ab4d3b4f718e91cd2b7baab42c e828ec80d5534dfa9154ec69071e9788 RZ(g0) cca199513c6943b3a3b68e2d50855623--e828ec80d5534dfa9154ec69071e9788 4d3cc2db046f4854b32f4bf1a1d58244 X e828ec80d5534dfa9154ec69071e9788--4d3cc2db046f4854b32f4bf1a1d58244 4d3cc2db046f4854b32f4bf1a1d58244--448b0894b7df42048fb5b691dcdf8c3f ad73d6e3e64b43858ebadfaeccb42242 X 4d3cc2db046f4854b32f4bf1a1d58244--ad73d6e3e64b43858ebadfaeccb42242 ad73d6e3e64b43858ebadfaeccb42242--99efebcab4cc486a9e38fe85af28ae54 4cecf252c18e47aa869e661d2666da5b RZ(g0) ad73d6e3e64b43858ebadfaeccb42242--4cecf252c18e47aa869e661d2666da5b 4d791c7e447c4d47be85a00bf0165024 X 4cecf252c18e47aa869e661d2666da5b--4d791c7e447c4d47be85a00bf0165024 4d791c7e447c4d47be85a00bf0165024--f026d920b22544d3aa1a91508ce0bb7d 32fc169a9f1d4f089b1acda85acaf3fc RX(b0) 4d791c7e447c4d47be85a00bf0165024--32fc169a9f1d4f089b1acda85acaf3fc 32fc169a9f1d4f089b1acda85acaf3fc--545ecc28ed234e1bbbb0a4ed10ce14d4

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.

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-06-12T18:24:32.168988 image/svg+xml Matplotlib v3.10.3, https://matplotlib.org/

References


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