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-26T11:55:33.491671 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_662e3f91b0e54302bd517528b1503fcf mixing cluster_202138e45b184bb694b665d85ca2d686 cost 91410172d32b4b32ac3e0318f7fc777f 0 559d6be40bd54c96a369fce11af39c62 H 91410172d32b4b32ac3e0318f7fc777f--559d6be40bd54c96a369fce11af39c62 b42365223447435684a7215d4b553519 1 3a434ac504e74b41b5d4bb002f505aba 559d6be40bd54c96a369fce11af39c62--3a434ac504e74b41b5d4bb002f505aba cb9a857dedc64a6190ebe3208965291c 3a434ac504e74b41b5d4bb002f505aba--cb9a857dedc64a6190ebe3208965291c 628a96c396c54a0cb5f9d7ffba80ff0e cb9a857dedc64a6190ebe3208965291c--628a96c396c54a0cb5f9d7ffba80ff0e a961fc7d097c4a048d09a77cde2e9cd3 628a96c396c54a0cb5f9d7ffba80ff0e--a961fc7d097c4a048d09a77cde2e9cd3 a7b82b7c651c44e2ae41aef531760660 a961fc7d097c4a048d09a77cde2e9cd3--a7b82b7c651c44e2ae41aef531760660 1dd1b4e76f634570912cf0f36eaea21b a7b82b7c651c44e2ae41aef531760660--1dd1b4e76f634570912cf0f36eaea21b 57d2f17dacc4411e94c314383afe45a0 1dd1b4e76f634570912cf0f36eaea21b--57d2f17dacc4411e94c314383afe45a0 1712eefacd134a379e4477fd16a4e568 57d2f17dacc4411e94c314383afe45a0--1712eefacd134a379e4477fd16a4e568 992f17e726d34d3186128032e7915cc2 1712eefacd134a379e4477fd16a4e568--992f17e726d34d3186128032e7915cc2 b5d03f8d5d774609a9134f1052087601 992f17e726d34d3186128032e7915cc2--b5d03f8d5d774609a9134f1052087601 1c43fef61e724a2893bf1a7fc44ddd5a b5d03f8d5d774609a9134f1052087601--1c43fef61e724a2893bf1a7fc44ddd5a 4c8d6ed1b8334466948adaa329e0a6eb 1c43fef61e724a2893bf1a7fc44ddd5a--4c8d6ed1b8334466948adaa329e0a6eb d51cbcc197414efebca4dc60974b61a0 4c8d6ed1b8334466948adaa329e0a6eb--d51cbcc197414efebca4dc60974b61a0 889219fa7c5244589a8705419dc2f7eb d51cbcc197414efebca4dc60974b61a0--889219fa7c5244589a8705419dc2f7eb 8bcbbbbc880f47e59c7e5839d3a63986 889219fa7c5244589a8705419dc2f7eb--8bcbbbbc880f47e59c7e5839d3a63986 ae00f7a526e3436bb0686287ea66c48e 8bcbbbbc880f47e59c7e5839d3a63986--ae00f7a526e3436bb0686287ea66c48e 9ec34b1449734c6d82aca45e00ae6ed6 ae00f7a526e3436bb0686287ea66c48e--9ec34b1449734c6d82aca45e00ae6ed6 c0e020dcf5a24aed8e18e032f17f83ac 9ec34b1449734c6d82aca45e00ae6ed6--c0e020dcf5a24aed8e18e032f17f83ac fe3a516e2fef4e03b9ca0aec2295ad8f RX(b0) c0e020dcf5a24aed8e18e032f17f83ac--fe3a516e2fef4e03b9ca0aec2295ad8f fa3e0ef641404e9eacc7fd0bfb81cf88 fe3a516e2fef4e03b9ca0aec2295ad8f--fa3e0ef641404e9eacc7fd0bfb81cf88 5065cfab29114154ae8ae7358a6153a0 4bef2e6003ad4034b28661ac14eb98ce H b42365223447435684a7215d4b553519--4bef2e6003ad4034b28661ac14eb98ce 73f0641e2683446fa236a4bb7357fb46 2 649bb65e0bda492e8b4452c785d177f0 X 4bef2e6003ad4034b28661ac14eb98ce--649bb65e0bda492e8b4452c785d177f0 649bb65e0bda492e8b4452c785d177f0--3a434ac504e74b41b5d4bb002f505aba f4984d5f8be043438ab8d8d16284910d RZ(g0) 649bb65e0bda492e8b4452c785d177f0--f4984d5f8be043438ab8d8d16284910d 4bd62aafd4024a6288d33105d67d91c8 X f4984d5f8be043438ab8d8d16284910d--4bd62aafd4024a6288d33105d67d91c8 4bd62aafd4024a6288d33105d67d91c8--628a96c396c54a0cb5f9d7ffba80ff0e c673ae21a7d94eab94dcd10d7c767204 4bd62aafd4024a6288d33105d67d91c8--c673ae21a7d94eab94dcd10d7c767204 29519a986653464a81a6c93b3f1384ec c673ae21a7d94eab94dcd10d7c767204--29519a986653464a81a6c93b3f1384ec 8c040b50faf24de39013b19b7f6cfa2e 29519a986653464a81a6c93b3f1384ec--8c040b50faf24de39013b19b7f6cfa2e e933237a69cc48b1b227f8dce74c560e 8c040b50faf24de39013b19b7f6cfa2e--e933237a69cc48b1b227f8dce74c560e 0292d68ec2ff49e187bddb983812b6a9 e933237a69cc48b1b227f8dce74c560e--0292d68ec2ff49e187bddb983812b6a9 beaa10e2504247da8948ac5e432ffe7a 0292d68ec2ff49e187bddb983812b6a9--beaa10e2504247da8948ac5e432ffe7a d4632bcdf20c478fb54f90f3aeb18ad1 beaa10e2504247da8948ac5e432ffe7a--d4632bcdf20c478fb54f90f3aeb18ad1 cd6decb8abf4475383608f66b7dfb6b5 d4632bcdf20c478fb54f90f3aeb18ad1--cd6decb8abf4475383608f66b7dfb6b5 897e25781d824ca583e1ab9915f9c9fc cd6decb8abf4475383608f66b7dfb6b5--897e25781d824ca583e1ab9915f9c9fc 84540270158f4a66b1bf43515e934f05 897e25781d824ca583e1ab9915f9c9fc--84540270158f4a66b1bf43515e934f05 fa2fa01631794f929ef76b9c21937f86 84540270158f4a66b1bf43515e934f05--fa2fa01631794f929ef76b9c21937f86 9ab03a16c3164256bbdafe52635c92c6 fa2fa01631794f929ef76b9c21937f86--9ab03a16c3164256bbdafe52635c92c6 f7f9d4fcb37d47a483604f94af875935 9ab03a16c3164256bbdafe52635c92c6--f7f9d4fcb37d47a483604f94af875935 564bf7715e8a456ea0da3b4f7c54141a f7f9d4fcb37d47a483604f94af875935--564bf7715e8a456ea0da3b4f7c54141a a5296b2fee1940e4acc35bdf0a83a3fc 564bf7715e8a456ea0da3b4f7c54141a--a5296b2fee1940e4acc35bdf0a83a3fc 4763018715cf4bbba8e5c7e492807b74 RX(b0) a5296b2fee1940e4acc35bdf0a83a3fc--4763018715cf4bbba8e5c7e492807b74 4763018715cf4bbba8e5c7e492807b74--5065cfab29114154ae8ae7358a6153a0 9467fb98232042ed89d4d36369974f53 7c93178715e945049542e715cbcc44c6 H 73f0641e2683446fa236a4bb7357fb46--7c93178715e945049542e715cbcc44c6 469db4a6f3ac4cd4b7dea44cf8bc80f6 3 6ebefa9f85ed40398ea44edeb753e4be 7c93178715e945049542e715cbcc44c6--6ebefa9f85ed40398ea44edeb753e4be 61d58cb44aa24dc89938f9a7781994fd 6ebefa9f85ed40398ea44edeb753e4be--61d58cb44aa24dc89938f9a7781994fd 3411ce33eb234ff78c9d1a28d00799a3 61d58cb44aa24dc89938f9a7781994fd--3411ce33eb234ff78c9d1a28d00799a3 0fcb5caee4134e3686fef5e08fbca240 X 3411ce33eb234ff78c9d1a28d00799a3--0fcb5caee4134e3686fef5e08fbca240 0fcb5caee4134e3686fef5e08fbca240--a961fc7d097c4a048d09a77cde2e9cd3 301d98cd4e1e48f4944963807a09c4f0 RZ(g0) 0fcb5caee4134e3686fef5e08fbca240--301d98cd4e1e48f4944963807a09c4f0 84d82e85347d47da934dff20edcf2cd7 X 301d98cd4e1e48f4944963807a09c4f0--84d82e85347d47da934dff20edcf2cd7 84d82e85347d47da934dff20edcf2cd7--1dd1b4e76f634570912cf0f36eaea21b 5f3aa0fb8dc94715a6a017a0b7c4c94b 84d82e85347d47da934dff20edcf2cd7--5f3aa0fb8dc94715a6a017a0b7c4c94b 8a8e727eae8344a587726c9135ea276d 5f3aa0fb8dc94715a6a017a0b7c4c94b--8a8e727eae8344a587726c9135ea276d ef9ffae38923489ab3ada0bdc8773537 8a8e727eae8344a587726c9135ea276d--ef9ffae38923489ab3ada0bdc8773537 539fdc11a60745d6bd897110784b5f38 X ef9ffae38923489ab3ada0bdc8773537--539fdc11a60745d6bd897110784b5f38 539fdc11a60745d6bd897110784b5f38--d4632bcdf20c478fb54f90f3aeb18ad1 6048a4ae86d24f3688f51625133174f1 RZ(g0) 539fdc11a60745d6bd897110784b5f38--6048a4ae86d24f3688f51625133174f1 b3f3ad697ec0458d86b88502eb9b1bf7 X 6048a4ae86d24f3688f51625133174f1--b3f3ad697ec0458d86b88502eb9b1bf7 b3f3ad697ec0458d86b88502eb9b1bf7--897e25781d824ca583e1ab9915f9c9fc b56902e502ae441dac0862f27780b5ad b3f3ad697ec0458d86b88502eb9b1bf7--b56902e502ae441dac0862f27780b5ad 5e4c24f49c7046de90985817481b1256 b56902e502ae441dac0862f27780b5ad--5e4c24f49c7046de90985817481b1256 d3e0902c1ecb43868dc7c5836c10b1a2 5e4c24f49c7046de90985817481b1256--d3e0902c1ecb43868dc7c5836c10b1a2 102655cfe2da442e94b2f58ad46a9a4d d3e0902c1ecb43868dc7c5836c10b1a2--102655cfe2da442e94b2f58ad46a9a4d 4ecd55258c864ebd8f3dd33a1cd3aa7b 102655cfe2da442e94b2f58ad46a9a4d--4ecd55258c864ebd8f3dd33a1cd3aa7b 1d2827c9b28e4d86b9e35d57b3cd99d5 4ecd55258c864ebd8f3dd33a1cd3aa7b--1d2827c9b28e4d86b9e35d57b3cd99d5 ddf087de5f8145c083eedb0ed4499d41 RX(b0) 1d2827c9b28e4d86b9e35d57b3cd99d5--ddf087de5f8145c083eedb0ed4499d41 ddf087de5f8145c083eedb0ed4499d41--9467fb98232042ed89d4d36369974f53 59d471534d634bc0b0ad075aececefcc d00038b546104dd4bc066e3704f04c73 H 469db4a6f3ac4cd4b7dea44cf8bc80f6--d00038b546104dd4bc066e3704f04c73 9d3307c436e94e4d93d3813c79c0de4a d00038b546104dd4bc066e3704f04c73--9d3307c436e94e4d93d3813c79c0de4a 22e12d7aab3c476b8b89789141e58de5 9d3307c436e94e4d93d3813c79c0de4a--22e12d7aab3c476b8b89789141e58de5 aea4ef208e674cff9bf5f8227daa3e4c 22e12d7aab3c476b8b89789141e58de5--aea4ef208e674cff9bf5f8227daa3e4c 385829403ed847ac86c21371f698fc02 aea4ef208e674cff9bf5f8227daa3e4c--385829403ed847ac86c21371f698fc02 66f520c9700d4de9b3e66999d5bdde5b 385829403ed847ac86c21371f698fc02--66f520c9700d4de9b3e66999d5bdde5b c63064916a364227a2994cc1d2a11cb2 66f520c9700d4de9b3e66999d5bdde5b--c63064916a364227a2994cc1d2a11cb2 0435a6895fbf4706bf4a454e27f4061f X c63064916a364227a2994cc1d2a11cb2--0435a6895fbf4706bf4a454e27f4061f 0435a6895fbf4706bf4a454e27f4061f--57d2f17dacc4411e94c314383afe45a0 35ee8db624bf4fce98f0bc539cb8f778 RZ(g0) 0435a6895fbf4706bf4a454e27f4061f--35ee8db624bf4fce98f0bc539cb8f778 cf63cf4d7e99431cab990c0c49cabd20 X 35ee8db624bf4fce98f0bc539cb8f778--cf63cf4d7e99431cab990c0c49cabd20 cf63cf4d7e99431cab990c0c49cabd20--992f17e726d34d3186128032e7915cc2 661b3a2ee53d4763ace2d00195f34209 cf63cf4d7e99431cab990c0c49cabd20--661b3a2ee53d4763ace2d00195f34209 4a29ca5144c14ddcbf635b56b18230d7 661b3a2ee53d4763ace2d00195f34209--4a29ca5144c14ddcbf635b56b18230d7 26630e0d55b14f88bfe12622a94bf9de 4a29ca5144c14ddcbf635b56b18230d7--26630e0d55b14f88bfe12622a94bf9de 756a33a84e1f4c03bee7cb11fca58386 X 26630e0d55b14f88bfe12622a94bf9de--756a33a84e1f4c03bee7cb11fca58386 756a33a84e1f4c03bee7cb11fca58386--84540270158f4a66b1bf43515e934f05 ca328ae8790e41aba772c69975fe3708 RZ(g0) 756a33a84e1f4c03bee7cb11fca58386--ca328ae8790e41aba772c69975fe3708 6558045f828a494a9b999bbe5af43605 X ca328ae8790e41aba772c69975fe3708--6558045f828a494a9b999bbe5af43605 6558045f828a494a9b999bbe5af43605--9ab03a16c3164256bbdafe52635c92c6 fddd28f7888f432cb98aeca44e2958c8 X 6558045f828a494a9b999bbe5af43605--fddd28f7888f432cb98aeca44e2958c8 fddd28f7888f432cb98aeca44e2958c8--102655cfe2da442e94b2f58ad46a9a4d 30355ba833644b128b2bebd23e419dc3 RZ(g0) fddd28f7888f432cb98aeca44e2958c8--30355ba833644b128b2bebd23e419dc3 2543426d78fd45648e2089f137fa25f0 X 30355ba833644b128b2bebd23e419dc3--2543426d78fd45648e2089f137fa25f0 2543426d78fd45648e2089f137fa25f0--1d2827c9b28e4d86b9e35d57b3cd99d5 9344e79d124347669214892e137f56c6 RX(b0) 2543426d78fd45648e2089f137fa25f0--9344e79d124347669214892e137f56c6 9344e79d124347669214892e137f56c6--59d471534d634bc0b0ad075aececefcc

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-26T11:55:37.391893 image/svg+xml Matplotlib v3.10.3, https://matplotlib.org/

References


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