js-slang
Version:
Javascript-based implementations of Source, written in Typescript
189 lines • 8.07 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.DirectedGraph = void 0;
const assert_1 = require("../../utils/assert");
/**
* Represents a directed graph which disallows self-loops.
*/
class DirectedGraph {
constructor() {
this.differentKeysError = new Error('The keys of the adjacency list & the in-degree maps are not the same. This should never occur.');
this.adjacencyList = new Map();
}
/**
* Adds a directed edge to the graph from the source node to
* the destination node. Self-loops are not allowed.
*
* @param sourceNode The name of the source node.
* @param destinationNode The name of the destination node.
*/
addEdge(sourceNode, destinationNode) {
if (sourceNode === destinationNode) {
throw new Error('Edges that connect a node to itself are not allowed.');
}
const neighbours = this.adjacencyList.get(sourceNode) ?? new Set();
neighbours.add(destinationNode);
this.adjacencyList.set(sourceNode, neighbours);
// Create an entry for the destination node if it does not exist
// in the adjacency list. This is so that the set of keys of the
// adjacency list is the same as the set of nodes in the graph.
if (!this.adjacencyList.has(destinationNode)) {
this.adjacencyList.set(destinationNode, new Set());
}
}
/**
* Returns whether the directed edge from the source node to the
* destination node exists in the graph.
*
* @param sourceNode The name of the source node.
* @param destinationNode The name of the destination node.
*/
hasEdge(sourceNode, destinationNode) {
if (sourceNode === destinationNode) {
throw new Error('Edges that connect a node to itself are not allowed.');
}
const neighbours = this.adjacencyList.get(sourceNode) ?? new Set();
return neighbours.has(destinationNode);
}
/**
* Calculates the in-degree of every node in the directed graph.
*
* The in-degree of a node is the number of edges coming into
* the node.
*/
calculateInDegrees() {
const inDegrees = new Map();
for (const neighbours of this.adjacencyList.values()) {
for (const neighbour of neighbours) {
const inDegree = inDegrees.get(neighbour) ?? 0;
inDegrees.set(neighbour, inDegree + 1);
}
}
// Handle nodes which have an in-degree of 0.
for (const node of this.adjacencyList.keys()) {
if (!inDegrees.has(node)) {
inDegrees.set(node, 0);
}
}
return inDegrees;
}
/**
* Finds a cycle of nodes in the directed graph. This operates on the
* invariant that any nodes left over with a non-zero in-degree after
* Kahn's algorithm has been run is part of a cycle.
*
* @param inDegrees The number of edges coming into each node after
* running Kahn's algorithm.
*/
findCycle(inDegrees) {
// First, we pick any arbitrary node that is part of a cycle as our
// starting node.
let startingNodeInCycle = null;
for (const [node, inDegree] of inDegrees) {
if (inDegree !== 0) {
startingNodeInCycle = node;
break;
}
}
// By the invariant stated above, it is impossible that the starting
// node cannot be found. The lack of a starting node implies that
// all nodes have an in-degree of 0 after running Kahn's algorithm.
// This in turn implies that Kahn's algorithm was able to find a
// valid topological ordering & that the graph contains no cycles.
(0, assert_1.default)(startingNodeInCycle !== null, 'There are no cycles in this graph. This should never happen.');
const cycle = [startingNodeInCycle];
// Then, we keep picking arbitrary nodes with non-zero in-degrees until
// we pick a node that has already been picked.
while (true) {
const currentNode = cycle[cycle.length - 1];
const neighbours = this.adjacencyList.get(currentNode);
if (neighbours === undefined) {
throw this.differentKeysError;
}
// By the invariant stated above, it is impossible that any node
// on the cycle has an in-degree of 0 after running Kahn's algorithm.
// An in-degree of 0 implies that the node is not part of a cycle,
// which is a contradiction since the current node was picked because
// it is part of a cycle.
(0, assert_1.default)(neighbours.size > 0, `Node '${currentNode}' has no incoming edges. This should never happen.`);
let nextNodeInCycle = null;
for (const neighbour of neighbours) {
if (inDegrees.get(neighbour) !== 0) {
nextNodeInCycle = neighbour;
break;
}
}
// By the invariant stated above, if the current node is part of a cycle,
// then one of its neighbours must also be part of the same cycle. This
// is because a cycle contains at least 2 nodes.
(0, assert_1.default)(nextNodeInCycle !== null, `None of the neighbours of node '${currentNode}' are part of the same cycle. This should never happen.`);
// If the next node we pick is already part of the cycle,
// we drop all elements before the first instance of the
// next node and return the cycle.
const nextNodeIndex = cycle.indexOf(nextNodeInCycle);
const isNodeAlreadyInCycle = nextNodeIndex !== -1;
cycle.push(nextNodeInCycle);
if (isNodeAlreadyInCycle) {
return cycle.slice(nextNodeIndex);
}
}
}
/**
* Returns a topological ordering of the nodes in the directed
* graph if the graph is acyclic. Otherwise, returns null.
*
* To get the topological ordering, Kahn's algorithm is used.
*/
getTopologicalOrder() {
let numOfVisitedNodes = 0;
const inDegrees = this.calculateInDegrees();
const topologicalOrder = [];
const queue = [];
for (const [node, inDegree] of inDegrees) {
if (inDegree === 0) {
queue.push(node);
}
}
while (true) {
const node = queue.shift();
// 'node' is 'undefined' when the queue is empty.
if (node === undefined) {
break;
}
numOfVisitedNodes++;
topologicalOrder.push(node);
const neighbours = this.adjacencyList.get(node);
if (neighbours === undefined) {
throw this.differentKeysError;
}
for (const neighbour of neighbours) {
const inDegree = inDegrees.get(neighbour);
if (inDegree === undefined) {
throw this.differentKeysError;
}
inDegrees.set(neighbour, inDegree - 1);
if (inDegrees.get(neighbour) === 0) {
queue.push(neighbour);
}
}
}
// If not all nodes are visited, then at least one
// cycle exists in the graph and a topological ordering
// cannot be found.
if (numOfVisitedNodes !== this.adjacencyList.size) {
const firstCycleFound = this.findCycle(inDegrees);
return {
isValidTopologicalOrderFound: false,
topologicalOrder: null,
firstCycleFound
};
}
return {
isValidTopologicalOrderFound: true,
topologicalOrder,
firstCycleFound: null
};
}
}
exports.DirectedGraph = DirectedGraph;
//# sourceMappingURL=directedGraph.js.map
;