Minimal Spanning Tree Example
The Minimum Spanning Tree problem finds the minimum-weight subset of edges connecting all nodes without forming cycles. Classical algorithms (Kruskal, Prim) solve it in polynomial time; the QUBO formulation enables quantum approaches.
import getpass
import os
import numpy as np
from dotenv import load_dotenv
from luna_quantum.algorithms import SCIP
from luna_usecases.minimal_spanning_tree import (
MinimalSpanningTreeCollection,
MinimalSpanningTreeData,
MinimalSpanningTreeFormulation,
MinimalSpanningTreeInstance,
)
load_dotenv()
if "LUNA_API_KEY" not in os.environ:
os.environ["LUNA_API_KEY"] = getpass.getpass("Enter your Luna API key: ")
Create Data
Define a 5-node weighted graph. Edge weights represent connection costs.
adj = np.array(
[
[0, 2, 4, 0, 0],
[2, 0, 3, 7, 0],
[4, 3, 0, 0, 5],
[0, 7, 0, 0, 1],
[0, 0, 5, 1, 0],
]
)
node_names = ["A", "B", "C", "D", "E"]
data = MinimalSpanningTreeData.from_adjacency_matrix(adjacency_matrix=adj, node_names=node_names)
print(data.to_string())
Plot Data
Visualize the weighted graph.
Create Formulation
Select edges that connect all nodes with minimum total weight and no cycles.
Minimal Spanning Tree Formulation:
Nodes: 5
Edges: 6
Decision Variables:
y[i,j] in {0,1} for each edge (i,j)
y[i,j] = 1 if edge (i,j) is in the tree
f[i,j], f[j,i] in {0, ..., 4} for each edge (i,j)
f[i,j] = directed flow on edge from i to j
Total: 6 binary + 12 integer = 18 variables
Objective:
minimize sum_False w_ij * y[i,j]
Constraints:
1. Exactly n-1 edges selected (1 constraint):
sum_{(i,j)} y[i,j] == 4
2. Flow conservation at non-root nodes (4 constraints):
sum_j f[j,v] - sum_j f[v,j] == 1 for all v != root
3. Flow capacity (6 constraints):
f[i,j] + f[j,i] <= 4 * y[i,j] for all edges (i,j)
4. Root sends n-1 units of flow (1 constraint):
sum_j f[root,j] - sum_j f[j,root] == 4
5. Flow bounds (24 constraints):
0 <= f[i,j] <= 4 for all directed edges
Create Instance
Combine data and formulation into a solvable instance.
instance = MinimalSpanningTreeInstance(data=data, formulation=formulation)
print(instance.to_string())
Data:Minimal Spanning Tree Data:
Nodes: 5
Edges: 6
Formulation:Minimal Spanning Tree Formulation:
Nodes: 5
Edges: 6
Decision Variables:
y[i,j] in {0,1} for each edge (i,j)
y[i,j] = 1 if edge (i,j) is in the tree
f[i,j], f[j,i] in {0, ..., 4} for each edge (i,j)
f[i,j] = directed flow on edge from i to j
Total: 6 binary + 12 integer = 18 variables
Objective:
minimize sum_False w_ij * y[i,j]
Constraints:
1. Exactly n-1 edges selected (1 constraint):
sum_{(i,j)} y[i,j] == 4
2. Flow conservation at non-root nodes (4 constraints):
sum_j f[j,v] - sum_j f[v,j] == 1 for all v != root
3. Flow capacity (6 constraints):
f[i,j] + f[j,i] <= 4 * y[i,j] for all edges (i,j)
4. Root sends n-1 units of flow (1 constraint):
sum_j f[root,j] - sum_j f[j,root] == 4
5. Flow bounds (24 constraints):
0 <= f[i,j] <= 4 for all directed edges
Formulate Model
Translate the instance into a mathematical optimization model.
Solve and Interpret
Solve the model with SCIP and interpret the raw result into a use-case-specific solution.
scip = SCIP()
job = scip.run(model)
sol = job.result()
uc_solution = instance.interpret(sol)
print(uc_solution.to_string())
/Users/maximilianjanetschek/PycharmProjects/luna-usecases/.venv/lib/python3.13/site-packages/rich/live.py:260:
UserWarning: install "ipywidgets" for Jupyter support
warnings.warn('install "ipywidgets" for Jupyter support')
2026-05-29 11:35:18 INFO Sleeping for 5.0 seconds. Waiting and checking a function in a loop.
Minimal Spanning Tree Solution:
Total weight: 11.0
Valid: True
Tree edges: [('A', 'B'), ('B', 'C'), ('C', 'E'), ('D', 'E')]
Plot Solution
Visualize the optimal solution.
Collections
Generate a benchmark collection of random instances for batch processing.
collection = MinimalSpanningTreeCollection.from_random(
min_nodes=4,
max_nodes=6,
num_instances=2,
seed=42,
)
model = collection.instances[0].formulate()
print(model)
Model: minimal_spanning_tree<minimal_spanning_tree>
Minimize
7.903351786733379 * y_0_1 + 1.6214616397519919 * y_0_2
+ 9.3202717031276 * y_0_3 + 2.248919008072832 * y_1_3
Subject To
n_minus_1_edges: y_0_1 + y_0_2 + y_0_3 + y_1_3 == 3
flow_conservation_1: f_0_1 - f_1_0 - f_1_3 + f_3_1 == 1
flow_conservation_2: f_0_2 - f_2_0 == 1
flow_conservation_3: f_0_3 - f_3_0 + f_1_3 - f_3_1 == 1
flow_capacity_0_1: -3 * y_0_1 + f_0_1 + f_1_0 <= 0
flow_capacity_0_2: -3 * y_0_2 + f_0_2 + f_2_0 <= 0
flow_capacity_0_3: -3 * y_0_3 + f_0_3 + f_3_0 <= 0
flow_capacity_1_3: -3 * y_1_3 + f_1_3 + f_3_1 <= 0
root_flow: f_0_1 - f_1_0 + f_0_2 - f_2_0 + f_0_3 - f_3_0 == 3
Bounds
0 <= f_0_1 <= 3
0 <= f_1_0 <= 3
0 <= f_0_2 <= 3
0 <= f_2_0 <= 3
0 <= f_0_3 <= 3
0 <= f_3_0 <= 3
0 <= f_1_3 <= 3
0 <= f_3_1 <= 3
Binary
y_0_1 y_0_2 y_0_3 y_1_3
Integer
f_0_1 f_1_0 f_0_2 f_2_0 f_0_3 f_3_0 f_1_3 f_3_1