|
@@ -1,12 +1,14 @@
|
|
|
import CycleRemoveStepError from "../../errors/optimizer/CycleRemoveStepError.js";
|
|
import CycleRemoveStepError from "../../errors/optimizer/CycleRemoveStepError.js";
|
|
|
-import Edge from "../../graph/edge/Edge.js";
|
|
|
|
|
-import Graph from "../../graph/Graph.js";
|
|
|
|
|
-import Node from "../../graph/node/Node.js";
|
|
|
|
|
import AlgorithmStep from "../AlgorithmStep.js";
|
|
import AlgorithmStep from "../AlgorithmStep.js";
|
|
|
import {SiguiyamaContext} from "../siguiyama/SiguiyamaContext.js";
|
|
import {SiguiyamaContext} from "../siguiyama/SiguiyamaContext.js";
|
|
|
|
|
+import Node from "../../graph/node/Node.js";
|
|
|
|
|
+import Graph from "../../graph/Graph.js";
|
|
|
|
|
+import Edge from "../../graph/edge/Edge.js";
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Greedy Cycle Removal Algorithm
|
|
|
|
|
|
|
+ * Шаг удаления циклов из графа, преобразование исходного ориентированного графа в ациклический
|
|
|
|
|
+ * В данной реализации шага преобразование графа в ациклический происходит при помощи топологической сортировки
|
|
|
|
|
+ * @see [Послойное рисование графа. Алгоритм компоновки](https://ru.wikipedia.org/wiki/Послойное_рисование_графа)
|
|
|
*/
|
|
*/
|
|
|
export default class CycleRemoveStep extends AlgorithmStep<SiguiyamaContext> {
|
|
export default class CycleRemoveStep extends AlgorithmStep<SiguiyamaContext> {
|
|
|
public constructor() {
|
|
public constructor() {
|
|
@@ -18,59 +20,73 @@ export default class CycleRemoveStep extends AlgorithmStep<SiguiyamaContext> {
|
|
|
if(!graph)
|
|
if(!graph)
|
|
|
throw new CycleRemoveStepError("Graph not found!");
|
|
throw new CycleRemoveStepError("Graph not found!");
|
|
|
|
|
|
|
|
- context.feedbackSet = this.removeCycles(graph);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- protected removeCycles(graph: Graph<Node, Edge<Node>>) : NonNullable<SiguiyamaContext["feedbackSet"]> {
|
|
|
|
|
- const nodes = graph.getNodes(), edges = graph.getEdges();
|
|
|
|
|
|
|
+ const sortedNodes = this.greedyCycleRemovalOrder(graph);
|
|
|
|
|
|
|
|
- const middleNodes: Node[] = [], l: Node[] = [], r: Node[] = [];
|
|
|
|
|
- const feedbackSet: NonNullable<SiguiyamaContext["feedbackSet"]> = [];
|
|
|
|
|
|
|
+ context.feedbackSet = this.createFeedbackSet(sortedNodes, graph);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- for(const node of nodes) {
|
|
|
|
|
- const isSource = graph.isSourceNode(node);
|
|
|
|
|
- if(isSource) {
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Строит линейный порядок вершин для эвристического удаления циклов (greedy cycle removal).
|
|
|
|
|
+ *
|
|
|
|
|
+ * Порядок собирается из трёх частей:
|
|
|
|
|
+ * - `l` (левая часть): все источники (`source`) в порядке обхода;
|
|
|
|
|
+ * - `r` (правая часть): все стоки (`sink`) через `unshift`;
|
|
|
|
|
+ * - `middle`: остальные вершины, отсортированные по убыванию `d⁺(v) - d⁻(v)`.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param graph Ориентированный граф.
|
|
|
|
|
+ * @returns Линейный порядок вершин.
|
|
|
|
|
+ */
|
|
|
|
|
+ private greedyCycleRemovalOrder(graph: Graph<Node, Edge<Node>>) : Node[] {
|
|
|
|
|
+ const l: Node[] = [], r: Node[] = [], middle: Node[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ for(const node of graph.getNodes()) {
|
|
|
|
|
+ if(graph.isSinkNode(node)) {
|
|
|
|
|
+ r.unshift(node);
|
|
|
|
|
+ } else if(graph.isSourceNode(node)) {
|
|
|
l.push(node);
|
|
l.push(node);
|
|
|
- continue;
|
|
|
|
|
|
|
+ } else {
|
|
|
|
|
+ middle.push(node);
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const isSink = graph.isSinkNode(node);
|
|
|
|
|
- if(isSink) {
|
|
|
|
|
- r.unshift(node);
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const getDegreeDiff = (node: Node) : number => {
|
|
|
|
|
+ const outDegree = graph.getNodeOutputs(node).length;
|
|
|
|
|
+ const inDegree = graph.getNodeInputs(node).length;
|
|
|
|
|
|
|
|
- middleNodes.push(node);
|
|
|
|
|
|
|
+ return outDegree - inDegree;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- middleNodes.sort((a, b) => this.calculateScore(graph, b) - this.calculateScore(graph, a));
|
|
|
|
|
|
|
+ middle.sort((a, b) => getDegreeDiff(b) - getDegreeDiff(a));
|
|
|
|
|
|
|
|
- while(middleNodes.length > 0) {
|
|
|
|
|
- const node = middleNodes[0]!;
|
|
|
|
|
|
|
+ for(const node of middle)
|
|
|
l.push(node);
|
|
l.push(node);
|
|
|
- middleNodes.splice(0, 1);
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- const sortedNodes = [...l, ...r.reverse()];
|
|
|
|
|
|
|
+ return [...l, ...r];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Формирует feedback set относительно заданного линейного порядка и инвертирует обратные рёбра.
|
|
|
|
|
+ * Ребро `(u -> v)` считается обратным, если индекс `u` в `sortedNodes` больше индекса `v`.
|
|
|
|
|
+ * @param orderedNodes
|
|
|
|
|
+ * @param graph Граф, в котором могут быть инвертированы рёбра.
|
|
|
|
|
+ * @returns Массив рёбер, которые были инвертированы.
|
|
|
|
|
+ */
|
|
|
|
|
+ protected createFeedbackSet(orderedNodes: Node[], graph: Graph<Node, Edge<Node>>) : NonNullable<SiguiyamaContext["feedbackSet"]> {
|
|
|
|
|
+ const feedbackSet: NonNullable<SiguiyamaContext["feedbackSet"]> = [];
|
|
|
|
|
+
|
|
|
|
|
+ for(const edge of graph.getEdges()) {
|
|
|
|
|
+ const from = edge.getFrom();
|
|
|
|
|
+ const fromIndex = orderedNodes.indexOf(from);
|
|
|
|
|
|
|
|
- for(const edge of edges) {
|
|
|
|
|
- const from = edge.getFrom(), to = edge.getTo();
|
|
|
|
|
- const fromIndex = sortedNodes.findIndex((node) => node.getId() === from.getId());
|
|
|
|
|
- const toIndex = sortedNodes.findIndex((node) => node.getId() === to.getId());
|
|
|
|
|
|
|
+ const to = edge.getTo();
|
|
|
|
|
+ const toIndex = orderedNodes.indexOf(to);
|
|
|
|
|
|
|
|
if(fromIndex > toIndex) {
|
|
if(fromIndex > toIndex) {
|
|
|
- edge.setFrom(to).setTo(from);
|
|
|
|
|
feedbackSet.push(edge);
|
|
feedbackSet.push(edge);
|
|
|
|
|
+ edge.setFrom(to).setTo(from);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return feedbackSet;
|
|
return feedbackSet;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- private calculateScore(graph: Graph<Node, Edge<Node>>, node: Node) : number {
|
|
|
|
|
- const outDegree = graph.getNodeOutputs(node).length;
|
|
|
|
|
- const inDegree = graph.getNodeInputs(node).length;
|
|
|
|
|
-
|
|
|
|
|
- return outDegree - inDegree;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
|
|
+}
|