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.
