You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
136 lines
5.0 KiB
136 lines
5.0 KiB
/*
|
|
* A greedy heuristic for finding a feedback arc set for a graph. A feedback
|
|
* arc set is a set of edges that can be removed to make a graph acyclic.
|
|
* The algorithm comes from: P. Eades, X. Lin, and W. F. Smyth, "A fast and
|
|
* effective heuristic for the feedback arc set problem." This implementation
|
|
* adjusts that from the paper to allow for weighted edges.
|
|
*
|
|
* @see https://github.com/dagrejs/dagre/blob/master/lib/greedy-fas.js
|
|
*/
|
|
import { Graph } from '@antv/graphlib';
|
|
import RawList from './data/list';
|
|
class List extends RawList {
|
|
}
|
|
const DEFAULT_WEIGHT_FN = () => 1;
|
|
export const greedyFAS = (g, weightFn) => {
|
|
var _a;
|
|
if (g.getAllNodes().length <= 1)
|
|
return [];
|
|
const state = buildState(g, weightFn || DEFAULT_WEIGHT_FN);
|
|
const results = doGreedyFAS(state.graph, state.buckets, state.zeroIdx);
|
|
return (_a = results
|
|
.map((e) => g.getRelatedEdges(e.v, 'out').filter(({ target }) => target === e.w))) === null || _a === void 0 ? void 0 : _a.flat();
|
|
};
|
|
const doGreedyFAS = (g, buckets, zeroIdx) => {
|
|
let results = [];
|
|
const sources = buckets[buckets.length - 1];
|
|
const sinks = buckets[0];
|
|
let entry;
|
|
while (g.getAllNodes().length) {
|
|
while ((entry = sinks.dequeue())) {
|
|
removeNode(g, buckets, zeroIdx, entry);
|
|
}
|
|
while ((entry = sources.dequeue())) {
|
|
removeNode(g, buckets, zeroIdx, entry);
|
|
}
|
|
if (g.getAllNodes().length) {
|
|
for (let i = buckets.length - 2; i > 0; --i) {
|
|
entry = buckets[i].dequeue();
|
|
if (entry) {
|
|
results = results.concat(removeNode(g, buckets, zeroIdx, entry, true));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return results;
|
|
};
|
|
const removeNode = (g, buckets, zeroIdx, entry, collectPredecessors) => {
|
|
var _a, _b;
|
|
const results = [];
|
|
if (g.hasNode(entry.v)) {
|
|
(_a = g.getRelatedEdges(entry.v, 'in')) === null || _a === void 0 ? void 0 : _a.forEach((edge) => {
|
|
const weight = edge.data.weight;
|
|
const uEntry = g.getNode(edge.source);
|
|
if (collectPredecessors) {
|
|
// this result not really care about in or out
|
|
results.push({ v: edge.source, w: edge.target, in: 0, out: 0 });
|
|
}
|
|
if (uEntry.data.out === undefined)
|
|
uEntry.data.out = 0;
|
|
// @ts-ignore
|
|
uEntry.data.out -= weight;
|
|
assignBucket(buckets, zeroIdx, Object.assign({ v: uEntry.id }, uEntry.data));
|
|
});
|
|
(_b = g.getRelatedEdges(entry.v, 'out')) === null || _b === void 0 ? void 0 : _b.forEach((edge) => {
|
|
const weight = edge.data.weight;
|
|
const w = edge.target;
|
|
const wEntry = g.getNode(w);
|
|
if (wEntry.data.in === undefined)
|
|
wEntry.data.in = 0;
|
|
// @ts-ignore
|
|
wEntry.data.in -= weight;
|
|
assignBucket(buckets, zeroIdx, Object.assign({ v: wEntry.id }, wEntry.data));
|
|
});
|
|
g.removeNode(entry.v);
|
|
}
|
|
return collectPredecessors ? results : undefined;
|
|
};
|
|
const buildState = (g, weightFn) => {
|
|
const fasGraph = new Graph();
|
|
let maxIn = 0;
|
|
let maxOut = 0;
|
|
g.getAllNodes().forEach((v) => {
|
|
fasGraph.addNode({
|
|
id: v.id,
|
|
data: { v: v.id, in: 0, out: 0 },
|
|
});
|
|
});
|
|
// Aggregate weights on nodes, but also sum the weights across multi-edges
|
|
// into a single edge for the fasGraph.
|
|
g.getAllEdges().forEach((e) => {
|
|
const edge = fasGraph
|
|
.getRelatedEdges(e.source, 'out')
|
|
.find((edge) => edge.target === e.target);
|
|
const weight = (weightFn === null || weightFn === void 0 ? void 0 : weightFn(e)) || 1;
|
|
if (!edge) {
|
|
fasGraph.addEdge({
|
|
id: e.id,
|
|
source: e.source,
|
|
target: e.target,
|
|
data: {
|
|
weight,
|
|
},
|
|
});
|
|
}
|
|
else {
|
|
fasGraph.updateEdgeData(edge === null || edge === void 0 ? void 0 : edge.id, Object.assign(Object.assign({}, edge.data), { weight: edge.data.weight + weight }));
|
|
}
|
|
// @ts-ignore
|
|
maxOut = Math.max(maxOut, (fasGraph.getNode(e.source).data.out += weight));
|
|
// @ts-ignore
|
|
maxIn = Math.max(maxIn, (fasGraph.getNode(e.target).data.in += weight));
|
|
});
|
|
const buckets = [];
|
|
const rangeMax = maxOut + maxIn + 3;
|
|
for (let i = 0; i < rangeMax; i++) {
|
|
buckets.push(new List());
|
|
}
|
|
const zeroIdx = maxIn + 1;
|
|
fasGraph.getAllNodes().forEach((v) => {
|
|
assignBucket(buckets, zeroIdx, Object.assign({ v: v.id }, fasGraph.getNode(v.id).data));
|
|
});
|
|
return { buckets, zeroIdx, graph: fasGraph };
|
|
};
|
|
const assignBucket = (buckets, zeroIdx, entry) => {
|
|
if (!entry.out) {
|
|
buckets[0].enqueue(entry);
|
|
}
|
|
else if (!entry['in']) {
|
|
buckets[buckets.length - 1].enqueue(entry);
|
|
}
|
|
else {
|
|
buckets[entry.out - entry['in'] + zeroIdx].enqueue(entry);
|
|
}
|
|
};
|
|
//# sourceMappingURL=greedy-fas.js.map
|