blob: a3c38192e9d524fb58c3b9334d009bfeae2c31cf [file] [log] [blame]
library graphlib.layout.greedy_fas;
import "dart:math" as Math;
import "dart:collection" show Queue;
import "../graph.dart" show Graph;
import "../alg/common.dart" show weightFunc;
import "util.dart" show flatten, range;
/// 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.
var DEFAULT_WEIGHT_FN = () => 1;
List greedyFAS(Graph g, [weightFunc weightFn]) {
if (g.nodeCount <= 1) {
return [];
}
var state = buildState(g, weightFn == null ? DEFAULT_WEIGHT_FN : weightFn);
var results = _doGreedyFAS(state["graph"], state["buckets"], state["zeroIdx"]);
// Expand multi-edges
return results.map((e) => flatten(g.outEdges(e.v, e.w)));
}
_doGreedyFAS(Graph g, List<Queue> buckets, int zeroIdx) {
var results = [],
sources = buckets[buckets.length - 1],
sinks = buckets[0];
var entry;
while (g.nodeCount != 0) {
while ((entry = sinks.removeLast())) { removeNode(g, buckets, zeroIdx, entry); }
while ((entry = sources.removeLast())) { removeNode(g, buckets, zeroIdx, entry); }
if (g.nodeCount != 0) {
for (var i = buckets.length - 2; i > 0; --i) {
entry = buckets[i].removeLast();
if (entry) {
results.addAll(removeNode(g, buckets, zeroIdx, entry, true));
break;
}
}
}
}
return results;
}
List removeNode(Graph g, List<Queue> buckets, int zeroIdx, entry, [collectPredecessors=false]) {
var results = collectPredecessors ? [] : null;
g.inEdges(entry.v).forEach((edge) {
var weight = g.edgeObj(edge),
uEntry = g.node(edge.v);
if (collectPredecessors) {
results.add({ "v": edge.v, "w": edge.w });
}
uEntry.out -= weight;
assignBucket(buckets, zeroIdx, uEntry);
});
g.outEdges(entry.v).forEach((edge) {
var weight = g.edgeObj(edge),
w = edge.w,
wEntry = g.node(w);
wEntry["in"] -= weight;
assignBucket(buckets, zeroIdx, wEntry);
});
g.removeNode(entry.v);
return results;
}
Map buildState(Graph g, weightFn) {
var fasGraph = new Graph(),
maxIn = 0,
maxOut = 0;
g.nodes.forEach((v) {
fasGraph.setNode(v, { "v": v, "in": 0, "out": 0 });
});
// Aggregate weights on nodes, but also sum the weights across multi-edges
// into a single edge for the fasGraph.
g.edges.forEach((e) {
var prevWeight = fasGraph.edge(e.v, e.w);
if (prevWeight == null) {
prevWeight = 0;
}
var weight = weightFn(e),
edgeWeight = prevWeight + weight;
fasGraph.setEdge(e.v, e.w, edgeWeight);
maxOut = Math.max(maxOut, fasGraph.node(e.v)["out"] += weight);
maxIn = Math.max(maxIn, fasGraph.node(e.w)["in"] += weight);
});
var buckets = range(maxOut + maxIn + 3).map((_) => new Queue());
var zeroIdx = maxIn + 1;
fasGraph.nodes.forEach((v) {
assignBucket(buckets, zeroIdx, fasGraph.node(v));
});
return { "graph": fasGraph, "buckets": buckets, "zeroIdx": zeroIdx };
}
assignBucket(List<Queue> buckets, zeroIdx, entry) {
if (!entry.out) {
buckets[0].add(entry);
} else if (!entry["in"]) {
buckets[buckets.length - 1].add(entry);
} else {
buckets[entry.out - entry["in"] + zeroIdx].add(entry);
}
}