Intro
A non-uniform undirected hypergraph is a generalization of an undirected graph. It is defined as \$H = (X, E)\$, where \$X\$ is the set of vertices and \$E \subseteq \mathcal{P}(X)\$ is the set of undirected hyperedges. In this setting, we equip \$E\$ with a weight function \$W \colon E \rightarrow \mathbb{R}_{>0}\$. Non-uniformity simply implies that the sizes of hyperedges may vary throughout the hypergraph.
A path in a hypergraph is a sequence \$P = (v_1, e_1, v_2, e_2, \ldots, e_{n-1}, v_n), v_i \in X, e_i \in E\$, and its weight is $$ W(P) = \sum_{i = 1}^{n - 1} E(e_i). $$
This post is about building the hypergraphs and doing shortest path searches via Dijkstra's algorithm.
Code
io.github.coderodde.graph.hyper.HyperGraphEdge.java:
package io.github.coderodde.graph.hyper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* This class defines a hyperedge in a hypergraph.
*
* @param <I> the type of the node identity object.
* @param <J> the type of the edge identity object.
* @param <W> the type of the weights.
*
* @author Rodion "rodde" Efremov
* @version 1.0.0 (Sep 24, 2025)
* @since 1.0.0 (Sep 24, 2025)
*/
public final class HyperGraphEdge<I, J, W> {
private final J id;
private final W weight;
final List<HyperGraphNode<I, J, W>> edgeNodes = new ArrayList<>();
public HyperGraphEdge(J id, W weight) {
this.id = Objects.requireNonNull(id);
this.weight = Objects.requireNonNull(weight);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("{");
boolean first = true;
for (HyperGraphNode<I, J, W> node : edgeNodes) {
if (first) {
first = false;
} else {
sb.append(", ");
}
sb.append(node.toString());
}
sb.append(": weight = ");
sb.append(weight);
sb.append("}");
return sb.toString();
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null) {
return false;
}
if (!getClass().equals(obj.getClass())) {
return false;
}
HyperGraphEdge<I, J, W> other = (HyperGraphEdge<I, J, W>) obj;
return id.equals(other.id) && weight.equals(other.weight);
}
public J getId() {
return id;
}
public W getWeight() {
return weight;
}
public void connectNode(HyperGraphNode<I, J, W> node) {
Objects.requireNonNull(node);
edgeNodes.add(node);
node.edges.add(this);
}
public boolean containsNode(HyperGraphNode<I, J, W> node) {
return edgeNodes.contains(node);
}
public void disconnectNode(HyperGraphNode<I, J, W> node) {
Objects.requireNonNull(node);
edgeNodes.remove(node);
node.edges.remove(this);
}
public void clear() {
for (HyperGraphNode<I, J, W> node : edgeNodes) {
node.edges.remove(this);
}
edgeNodes.clear();
}
public List<HyperGraphNode<I, J, W>> getIncidentHyperNodes() {
return Collections.unmodifiableList(edgeNodes);
}
}
io.github.coderodde.graph.hyper.HyperGraphNode.java:
package io.github.coderodde.graph.hyper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* This class defines a node in a hypergraph.
*
* @param <I> the type of the identity object.
* @param <W> the type of the weights.
*
* @author Rodion "rodde" Efremov
* @version 1.0.0 (Sep 24, 2025)
* @since 1.0.0 (Sep 24, 2025)
*/
public final class HyperGraphNode<I, J, W> {
private final I id;
protected final List<HyperGraphEdge<I, J, W>> edges = new ArrayList<>();
public HyperGraphNode(I id) {
this.id = Objects.requireNonNull(id);
}
public I getId() {
return id;
}
@Override
public String toString() {
return id.toString();
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null) {
return false;
}
if (!getClass().equals(obj.getClass())) {
return false;
}
HyperGraphNode<I, J, W> other = (HyperGraphNode<I, J, W>) obj;
return id.equals(other.id);
}
public List<HyperGraphEdge<I, J, W>> getIncidentHyperEdges() {
return Collections.unmodifiableList(edges);
}
}
io.github.coderodde.graph.hyper.HyperGraphPath.java:
package io.github.coderodde.graph.hyper;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* This class implements a hypergraph path, which is a list of nodes and a list
* of hyperedges.
*
* @param <I> the identity object type.
* @param <W> the weight type.
*
* @author Rodion "rodde" Efremov
* @version 1.0.0 (Oct 1, 2025)
* @since 1.0.0 (Oct 1, 2025)
*/
public final class HyperGraphPath<I, J, W> {
private final W weight;
private final List<HyperGraphNode<I, J, W>> nodes;
private final List<HyperGraphEdge<I, J, W>> edges;
public HyperGraphPath(List<HyperGraphNode<I, J, W>> nodes,
List<HyperGraphEdge<I, J, W>> edges,
WeightFunction<W> weightFunction) {
Objects.requireNonNull(nodes);
Objects.requireNonNull(edges);
if (nodes.isEmpty()) {
if (!edges.isEmpty()) {
throw new IllegalArgumentException("Invalid hyper graph path");
} else {
this.nodes = nodes;
this.edges = edges;
this.weight = weightFunction.zero();
}
} else {
if (nodes.size() - 1 != edges.size()) {
throw new IllegalArgumentException(
"Node count, edge count mismatch");
}
check(nodes, edges);
this.nodes = nodes;
this.edges = edges;
W w = weightFunction.zero();
for (HyperGraphEdge<I, J, W> edge : edges) {
w = weightFunction.apply(w, edge.getWeight());
}
this.weight = w;
}
}
@Override
public String toString() {
if (isNonExistent()) {
return "[: weight = 0]";
}
StringBuilder sb = new StringBuilder();
sb.append("[");
boolean first = true;
for (int i = 0; i < edges.size(); ++i) {
if (first) {
first = false;
} else {
sb.append(", ");
}
HyperGraphNode<I, J, W> node = nodes.get(i);
HyperGraphEdge<I, J, W> edge = edges.get(i);
sb.append(node.toString());
sb.append(", ");
sb.append(edge.toString());
}
sb.append(", ");
sb.append(nodes.get(nodes.size() - 1).toString());
sb.append(": total weight = ");
sb.append(weight);
sb.append("]");
return sb.toString();
}
public W getWeight() {
return weight;
}
public boolean isNonExistent() {
return nodes.isEmpty();
}
public List<HyperGraphNode<I, J, W>> getPathHyperNodes() {
return Collections.unmodifiableList(nodes);
}
public List<HyperGraphEdge<I, J, W>> getPathHyperEdges() {
return Collections.unmodifiableList(edges);
}
private static <I, J, W> void check(List<HyperGraphNode<I, J, W>> nodes,
List<HyperGraphEdge<I, J, W>> edges) {
for (int i = 0; i < nodes.size() - 1; ++i) {
HyperGraphNode<I, J, W> node1 = nodes.get(i);
HyperGraphNode<I, J, W> node2 = nodes.get(i + 1);
HyperGraphEdge<I, J, W> edge = edges.get(i);
if (!edge.containsNode(node1)) {
throw new IllegalArgumentException("Invalid hyper graph path");
}
if (!edge.containsNode(node2)) {
throw new IllegalArgumentException("Invalid hyper graph path");
}
}
if (!edges.get(edges.size() - 1)
.containsNode(nodes.get(nodes.size() - 1))) {
throw new IllegalArgumentException("Invalid hyper graph path");
}
}
}
io.github.coderodde.graph.hyper.HyperGraphPathfinder.java:
package io.github.coderodde.graph.hyper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Set;
/**
* This class implements a point-to-point Dijkstra's algorithm over a
* hypergraph.
*
* @author Rodion "rodde" Efremov
* @version 1.0.0 (Oct 1, 2025)
* @since 1.0.0 (Oct 1, 2025)
*/
public final class HyperGraphPathfinder {
public static <I, J, W>
HyperGraphPath<I, J, W> find(HyperGraphNode<I, J, W> source,
HyperGraphNode<I, J, W> target,
WeightFunction<W> weights) {
Queue<HeapNode<I, J, W>> open = new PriorityQueue<>();
Set<HyperGraphNode<I, J, W>> closed = new HashSet<>();
Map<HyperGraphNode<I, J, W>, W> distances = new HashMap<>();
Map<HyperGraphNode<I, J, W>, HyperGraphNode<I, J, W>> parents =
new HashMap<>();
open.add(new HeapNode(source, weights.zero(), weights));
distances.put(source, weights.zero());
parents.put(source, null);
while (!open.isEmpty()) {
HyperGraphNode<I, J, W> current = open.remove().node;
if (current.equals(target)) {
List<HyperGraphNode<I, J, W>> shortestPathNodes =
tracebackPath(current, parents);
List<HyperGraphEdge<I, J, W>> shortestPathHyperEdges =
inferPathEdges(shortestPathNodes, weights);
return new HyperGraphPath<>(shortestPathNodes,
shortestPathHyperEdges,
weights);
}
closed.add(current);
for (HyperGraphEdge<I, J, W> edge
: current.getIncidentHyperEdges()) {
for (HyperGraphNode<I, J, W> child
: edge.getIncidentHyperNodes()) {
if (closed.contains(child)) {
continue;
}
W tentative =
weights.apply(edge.getWeight(),
distances.get(current));
if (!distances.containsKey(child) ||
weights.compare(distances.get(child),
tentative) > 1) {
distances.put(child, tentative);
parents.put(child, current);
open.add(new HeapNode(child,
tentative,
weights));
}
}
}
}
throw new IllegalStateException("The hyper graph is disconnected");
}
private static <I, J, W>
List<HyperGraphNode<I, J, W>>
tracebackPath(HyperGraphNode<I, J, W> target,
Map<HyperGraphNode<I, J, W>,
HyperGraphNode<I, J, W>> parents) {
List<HyperGraphNode<I, J, W>> path = new ArrayList<>();
HyperGraphNode<I, J, W> current = target;
while (current != null) {
path.addLast(current);
current = parents.get(current);
}
Collections.reverse(path);
return path;
}
private static <I, J, W>
List<HyperGraphEdge<I, J, W>>
inferPathEdges(List<HyperGraphNode<I, J, W>> shortestPathNodes,
WeightFunction<W> weights) {
List<HyperGraphEdge<I, J, W>> shortestPathEdges =
new ArrayList<>(shortestPathNodes.size() - 1);
for (int i = 0; i < shortestPathNodes.size() - 1; ++i) {
HyperGraphNode<I, J, W> node1 = shortestPathNodes.get(i);
HyperGraphNode<I, J, W> node2 = shortestPathNodes.get(i + 1);
HyperGraphEdge<I, J, W> edge = inferPathEdge(node1,
node2,
weights);
shortestPathEdges.add(edge);
}
return shortestPathEdges;
}
private static <I, J, W> HyperGraphEdge<I, J, W>
inferPathEdge(HyperGraphNode<I, J, W> node1,
HyperGraphNode<I, J, W> node2,
WeightFunction<W> weightFunction) {
W smallestWeight = weightFunction.max();
HyperGraphEdge<I, J, W> smallestHyperEdge = null;
for (HyperGraphEdge<I, J, W> relay : node1.edges) {
if (relay.edgeNodes.contains(node2)) {
W currentWeight = relay.getWeight();
if (weightFunction.compare(smallestWeight, currentWeight) > 0) {
smallestWeight = currentWeight;
smallestHyperEdge = relay;
}
}
}
if (smallestWeight == null) {
throw new IllegalStateException("Should not get here");
}
return smallestHyperEdge;
}
private static final class HeapNode<I, J, W>
implements Comparable<HeapNode<I, J, W>> {
HyperGraphNode<I, J, W> node;
W g;
WeightFunction<W> weightFunction;
HeapNode(HyperGraphNode<I, J, W> node,
W g,
WeightFunction<W> weightFunction) {
this.node = node;
this.g = g;
this.weightFunction = weightFunction;
}
@Override
public int compareTo(HeapNode<I, J, W> o) {
return weightFunction.compare(g, o.g);
}
}
}
io.github.coderodde.graph.hyper.WeightFunction.java:
package io.github.coderodde.graph.hyper;
/**
* This interface defines the API for the weight functions.
*
* @param <W> the type of the weight.
*
* @author Rodion "rodde" Efremov
* @version 1.0.0 (Oct 2, 2025)
* @since 1.0.0 (Oct 2, 2025)
*/
public interface WeightFunction<W> {
W zero();
W max();
W apply(W w1, W w2);
int compare(W weight1, W weight2);
}
io.github.coderodde.graph.hyper.demo.Demo.java:
package io.github.coderodde.graph.hyper.demo;
import io.github.coderodde.graph.hyper.HyperGraphEdge;
import io.github.coderodde.graph.hyper.HyperGraphNode;
import io.github.coderodde.graph.hyper.HyperGraphPath;
import io.github.coderodde.graph.hyper.HyperGraphPathfinder;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
*
* @author Rodion "rodde" Efremov
* @version 1.0.0 (Oct 1, 2025)
* @since 1.0.0 (Oct 1, 2025)
*/
public final class Demo {
private static final int HYPER_NODES = 1_000_000;
private static final int HYPER_EDGES = 400_000;
private static final int MINIMUM_HYPER_EDGE_SIZE = 2;
private static final int MAXIMUM_HYPER_EDGE_SIZE = 8;
private static final int MINIMUM_HYPER_EDGE_WEIGHT = 1;
private static final int MAXIMUM_HYPER_EDGE_WEIGHT = 10;
private static final Random RND = new Random(13L);
public static void main(String[] args) {
long ta = System.currentTimeMillis();
List<HyperGraphNode<Integer, Integer, Integer>> hyperGraph =
getRandomHyperGraph();
long tb = System.currentTimeMillis();
System.out.println(
"Constructed the demo hyper graph in "
+ (tb - ta)
+ " milliseconds.");
HyperGraphNode<Integer, Integer, Integer> source = choose(hyperGraph);
HyperGraphNode<Integer, Integer, Integer> target = choose(hyperGraph);
ta = System.currentTimeMillis();
HyperGraphPath<Integer, Integer, Integer> path =
HyperGraphPathfinder.find(source,
target,
new IntegerWeightFunction());
tb = System.currentTimeMillis();
printPath(path);
System.out.println("Duration: " + (tb - ta) + " milliseconds.");
}
private static void printPath(HyperGraphPath<Integer,
Integer,
Integer> path) {
for (int i = 0; i < path.getPathHyperEdges().size(); ++i) {
System.out.println(path.getPathHyperNodes().get(i));
System.out.println(path.getPathHyperEdges().get(i));
}
System.out.println(
path.getPathHyperNodes()
.get(path.getPathHyperNodes().size() - 1)
.toString());
System.out.println("Path weight = " + path.getWeight());
}
private static List<HyperGraphNode<Integer,
Integer,
Integer>> getRandomHyperGraph() {
List<HyperGraphNode<Integer, Integer, Integer>> nodes =
new ArrayList<>();
createHyperNodes(nodes);
createHyperEdges(nodes);
return nodes;
}
private static
void createHyperNodes(List<HyperGraphNode<Integer,
Integer,
Integer>> nodes) {
for (int id = 0; id < HYPER_NODES; ++id) {
nodes.add(new HyperGraphNode<>(id));
}
}
private static
void createHyperEdges(List<HyperGraphNode<Integer,
Integer,
Integer>> nodes) {
for (int i = 0; i < HYPER_EDGES; ++i) {
Integer weight = getRandomWeight();
HyperGraphEdge<Integer, Integer, Integer> edge =
new HyperGraphEdge<>(i, weight);
int hyperEdgeSize = getHyperEdgeSize();
for (int j = 0; j < hyperEdgeSize; ++j) {
HyperGraphNode<Integer, Integer, Integer> node = choose(nodes);
edge.connectNode(node);
}
}
}
private static int getRandomWeight() {
return MINIMUM_HYPER_EDGE_WEIGHT +
RND.nextInt(MAXIMUM_HYPER_EDGE_WEIGHT + 1);
}
private static int getHyperEdgeSize() {
return MINIMUM_HYPER_EDGE_SIZE + RND.nextInt(MAXIMUM_HYPER_EDGE_SIZE -
MINIMUM_HYPER_EDGE_SIZE +
1);
}
private static HyperGraphNode<Integer, Integer, Integer>
choose(List<HyperGraphNode<Integer, Integer, Integer>> nodes) {
return nodes.get(RND.nextInt(nodes.size()));
}
}
io.github.coderodde.graph.hyper.demo.IntegerWeightFunction.java:
package io.github.coderodde.graph.hyper.demo;
import io.github.coderodde.graph.hyper.WeightFunction;
/**
* This class defines a weight function over integer valued weights.
*
* @author Rodion "rodde" Efremov
* @version 1.0.0 (Oct 1, 2025)
* @since 1.0.0 (Oct 1, 2025)
*/
public final class IntegerWeightFunction implements WeightFunction<Integer> {
@Override
public Integer zero() {
return 0;
}
@Override
public Integer max() {
return Integer.MAX_VALUE;
}
@Override
public Integer apply(Integer w1, Integer w2) {
return w1 + w2;
}
@Override
public int compare(Integer weight1, Integer weight2) {
return Integer.compare(weight1,
weight2);
}
}
io.github.coderodde.graph.hyper.HyperGraphPathfinderTest.java:
package io.github.coderodde.graph.hyper;
import io.github.coderodde.graph.hyper.demo.IntegerWeightFunction;
import org.junit.Test;
import static org.junit.Assert.*;
public class HyperGraphPathfinderTest {
private final IntegerWeightFunction weightFunction =
new IntegerWeightFunction();
@Test
public void find() {
HyperGraphNode<Integer, Integer, Integer> node1 = new HyperGraphNode<>(1);
HyperGraphNode<Integer, Integer, Integer> node2 = new HyperGraphNode<>(2);
HyperGraphNode<Integer, Integer, Integer> node3 = new HyperGraphNode<>(3);
HyperGraphEdge<Integer, Integer, Integer> edge1 = new HyperGraphEdge<>(1, 3);
HyperGraphEdge<Integer, Integer, Integer> edge2 = new HyperGraphEdge<>(2, 4);
edge1.connectNode(node1);
edge1.connectNode(node2);
edge2.connectNode(node2);
edge2.connectNode(node3);
HyperGraphPath<Integer, Integer, Integer> path =
HyperGraphPathfinder.find(node1,
node3,
weightFunction);
assertEquals(3, path.getPathHyperNodes().size());
assertEquals(2, path.getPathHyperEdges().size());
assertEquals((Integer) 7, path.getWeight());
assertEquals(node1, path.getPathHyperNodes().get(0));
assertEquals(node2, path.getPathHyperNodes().get(1));
assertEquals(node3, path.getPathHyperNodes().get(2));
assertEquals(edge1, path.getPathHyperEdges().get(0));
assertEquals(edge2, path.getPathHyperEdges().get(1));
}
@Test(expected = IllegalStateException.class)
public void throwOnDisconnectedGraph() {
HyperGraphNode<Integer, Integer, Integer> node1 = new HyperGraphNode<>(1);
HyperGraphNode<Integer, Integer, Integer> node2 = new HyperGraphNode<>(2);
HyperGraphPathfinder.find(node1, node2, weightFunction);
}
}
Typical output
Upon running the demo, you may see something like this:
Constructed the demo hyper graph in 1608 milliseconds.
390911
{390911, 849968, 177123, 682640, 746861, 103098, 771371: weight = 7}
746861
{216808, 292096, 857104, 670690, 664252, 746861, 177132, 271731: weight = 3}
857104
{857104, 182243, 151240, 315206, 358042, 766286, 671534, 423391: weight = 2}
182243
{669894, 861245, 838714, 786917, 419295, 182243, 451992: weight = 3}
786917
{446311, 379586, 450054, 210074, 38918, 159222, 786917, 338431: weight = 1}
210074
{586810, 973842, 210074, 666084, 925230, 258039: weight = 3}
586810
{16707, 899620, 657094, 105614, 463659, 837351, 586810: weight = 1}
657094
{518289, 720484, 361585, 610, 657094, 200696: weight = 1}
720484
{430901, 418517, 433285, 184287, 765154, 830803, 720484, 581154: weight = 3}
430901
{132047, 286114, 254149, 430901: weight = 10}
132047
Path weight = 34
Duration: 4659 milliseconds.
Critique request
As always, tell me anything that comes to mind.