
de.sormuras.bartholdy.util.AcyclicDirectedGraph Maven / Gradle / Ivy
package de.sormuras.bartholdy.util;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Consumer;
import java.util.function.Function;
public class AcyclicDirectedGraph {
private abstract static class Base implements Comparable {
final String id;
Base(String id) {
this.id = id;
}
@Override
public int compareTo(T o) {
return id.compareTo(o.id);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
return id.equals(((Base) other).id);
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public String toString() {
return id;
}
}
static class Edge extends Base {
final Node source;
final Node target;
Edge(Node source, Node target) {
super(source.id + '\t' + target.id);
this.source = source;
this.target = target;
}
@Override
public String toString() {
return source.id + "->" + target.id;
}
}
static class Node extends Base {
final Set inbounds;
final Set outbounds;
Node(String id) {
super(id);
this.inbounds = new TreeSet<>();
this.outbounds = new TreeSet<>();
}
}
private final Map nodes;
private final Set edges;
private final Set antis;
private final Set banned;
public AcyclicDirectedGraph(Set nodeIds) {
this.nodes = new HashMap<>();
this.edges = new TreeSet<>();
this.antis = new TreeSet<>();
this.banned = new TreeSet<>();
nodeIds.forEach(id -> nodes.put(id, new Node(id)));
}
public void addEdge(String sourceId, String targetId) {
var source = node(sourceId);
var target = node(targetId);
if (source == target) {
throw new CyclicEdgeException("Same node: " + source + " == " + target);
}
// create edge and check if it is illegal
var edge = new Edge(source, target);
if (banned.contains(edge) || antis.contains(edge)) {
throw new CyclicEdgeException(edge, edges);
}
// add edge to set of "good" edges, i.e. the acyclic directed graph
if (!edges.add(edge)) {
// duplicated edge, nothing further to do
return;
}
// ban anti-edge and potentially remove it from banned set
var anti = new Edge(target, source);
antis.add(anti);
banned.remove(anti);
// remember node's connections
source.outbounds.add(target);
target.inbounds.add(source);
// ban all other
var sources = new TreeSet();
var targets = new TreeSet();
walk(source, node -> node.outbounds, sources::add);
walk(target, node -> node.inbounds, targets::add);
for (var from : sources) {
for (var to : targets) {
if (from == to) {
continue;
}
var ban = new Edge(from, to);
if (antis.contains(ban)) {
continue;
}
banned.add(ban);
}
}
}
private Node node(String id) {
var node = nodes.get(id);
if (node == null) {
throw new IllegalArgumentException("Unknown node: " + id);
}
return node;
}
private void walk(Node root, Function> nodes, Consumer consumer) {
for (var node : nodes.apply(root)) {
consumer.accept(node);
walk(node, nodes, consumer);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy