The Partition Problem — QAOA Worked Example

qc/algorithms

A concrete, end-to-end derivation of QAOA applied to the partition problem: given a set of numbers, split them into two groups whose sums are as close as possible. It’s NP-hard classically (verifying a candidate split is trivial — sum both sides — but finding one requires checking splits in the worst case) yet easy to verify, making it a textbook “classically verifiable problem” for demonstrating quantum advantage. The derivation goes QUBO → Ising in three steps: encode each element with a binary variable (which subset it’s in), write the imbalance cost , then swap to spin variables via and minimize instead of to avoid absolute values. Expanding and dropping the constant diagonal term (using ) leaves — replacing spins with Pauli-Z operators gives the cost Hamiltonian , with each coupling weighted by the product of the two numbers it connects. Key insight: this is the same graph-based recipe as MaxCut — the partition problem is MaxCut on a complete graph whose edge weights are — which is why the implementation reaches for MaxCut tooling rather than writing a bespoke encoder.

from qiskit_addon_opt_mapper.applications import Maxcut
from qiskit_addon_opt_mapper.translators import to_ising
from qopt_best_practices.circuit_library import annotated_qaoa_ansatz
 
# graph: networkx.Graph with edge weights = a_i * a_j
maxcut = Maxcut(graph)
partition_hamiltonian = to_ising(maxcut.to_quadratic_program())  # SparsePauliOp
circuit = annotated_qaoa_ansatz(partition_hamiltonian, reps=layers)

API notes

  • to_ising takes an OptimizationProblem, not a Maxcut object directly — call .to_quadratic_program() (or equivalent) on the Maxcut application first.
  • annotated_qaoa_ansatz (from qopt_best_practices) produces a circuit with the QAOA cost/mixer layers annotated (boxed), which the transpiler can then unroll with UnrollBoxes() — this preserves layer structure through generate_preset_qaoa_pass_manager for SWAP-strategy-aware routing, instead of losing it to generic optimization passes.
  • For a hardware-native graph (one that already matches the backend’s coupling map after a greedy edge-coloring), SwapStrategy(cmap, ()) — an empty strategy — is valid: no SWAPs are needed because the problem graph and the hardware graph coincide after coloring.

Bonus: scaling to larger instances

At 1600 nodes, brute-force partition assignment from raw sampling accumulates bit-flip errors. A local-search refinement pass (swap_partitions) improves the best sampled bitstring by trying single-node swaps between the two partitions and keeping any swap that reduces the imbalance — cheap classical post-processing layered on top of the quantum result rather than an alternative to it. Because this refinement is itself a graph operation over thousands of edges, the notebook converts the NetworkX graph to Rustworkx first — Rustworkx is a Rust-backed graph library with the same conceptual API as NetworkX but built for exactly this kind of large-graph performance case.

Self-Check

  • Why does minimizing avoid the need to handle an absolute value, and what step of the derivation drops the constant term that produces?
  • Why is the partition problem a good “classically verifiable” candidate for demonstrating quantum advantage, when finding the answer is NP-hard?
  • What would go wrong if you passed a Maxcut object directly to to_ising instead of first converting it to a quadratic program?