greedy-fas.js 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import _ from 'lodash'
  2. import { Graph } from 'graphlibrary'
  3. import List from './data/list'
  4. /*
  5. * A greedy heuristic for finding a feedback arc set for a graph. A feedback
  6. * arc set is a set of edges that can be removed to make a graph acyclic.
  7. * The algorithm comes from: P. Eades, X. Lin, and W. F. Smyth, "A fast and
  8. * effective heuristic for the feedback arc set problem." This implementation
  9. * adjusts that from the paper to allow for weighted edges.
  10. */
  11. const DEFAULT_WEIGHT_FN = _.constant(1)
  12. function greedyFAS (g, weightFn) {
  13. if (g.nodeCount() <= 1) {
  14. return []
  15. }
  16. const state = buildState(g, weightFn || DEFAULT_WEIGHT_FN)
  17. const results = doGreedyFAS(state.graph, state.buckets, state.zeroIdx)
  18. // Expand multi-edges
  19. return _.flatten(_.map(results, function (e) {
  20. return g.outEdges(e.v, e.w)
  21. }), true)
  22. }
  23. function doGreedyFAS (g, buckets, zeroIdx) {
  24. let results = []
  25. const sources = buckets[buckets.length - 1]
  26. const sinks = buckets[0]
  27. let entry
  28. while (g.nodeCount()) {
  29. while ((entry = sinks.dequeue())) { removeNode(g, buckets, zeroIdx, entry) }
  30. while ((entry = sources.dequeue())) { removeNode(g, buckets, zeroIdx, entry) }
  31. if (g.nodeCount()) {
  32. for (let i = buckets.length - 2; i > 0; --i) {
  33. entry = buckets[i].dequeue()
  34. if (entry) {
  35. results = results.concat(removeNode(g, buckets, zeroIdx, entry, true))
  36. break
  37. }
  38. }
  39. }
  40. }
  41. return results
  42. }
  43. function removeNode (g, buckets, zeroIdx, entry, collectPredecessors) {
  44. const results = collectPredecessors ? [] : undefined
  45. _.forEach(g.inEdges(entry.v), function (edge) {
  46. const weight = g.edge(edge)
  47. const uEntry = g.node(edge.v)
  48. if (collectPredecessors) {
  49. results.push({ v: edge.v, w: edge.w })
  50. }
  51. uEntry.out -= weight
  52. assignBucket(buckets, zeroIdx, uEntry)
  53. })
  54. _.forEach(g.outEdges(entry.v), function (edge) {
  55. const weight = g.edge(edge)
  56. const w = edge.w
  57. const wEntry = g.node(w)
  58. wEntry['in'] -= weight
  59. assignBucket(buckets, zeroIdx, wEntry)
  60. })
  61. g.removeNode(entry.v)
  62. return results
  63. }
  64. function buildState (g, weightFn) {
  65. const fasGraph = new Graph()
  66. let maxIn = 0
  67. let maxOut = 0
  68. _.forEach(g.nodes(), function (v) {
  69. fasGraph.setNode(v, { v: v, 'in': 0, out: 0 })
  70. })
  71. // Aggregate weights on nodes, but also sum the weights across multi-edges
  72. // into a single edge for the fasGraph.
  73. _.forEach(g.edges(), function (e) {
  74. const prevWeight = fasGraph.edge(e.v, e.w) || 0
  75. const weight = weightFn(e)
  76. const edgeWeight = prevWeight + weight
  77. fasGraph.setEdge(e.v, e.w, edgeWeight)
  78. maxOut = Math.max(maxOut, fasGraph.node(e.v).out += weight)
  79. maxIn = Math.max(maxIn, fasGraph.node(e.w)['in'] += weight)
  80. })
  81. const buckets = _.range(maxOut + maxIn + 3).map(function () { return new List() })
  82. const zeroIdx = maxIn + 1
  83. _.forEach(fasGraph.nodes(), function (v) {
  84. assignBucket(buckets, zeroIdx, fasGraph.node(v))
  85. })
  86. return { graph: fasGraph, buckets: buckets, zeroIdx: zeroIdx }
  87. }
  88. function assignBucket (buckets, zeroIdx, entry) {
  89. if (!entry.out) {
  90. buckets[0].enqueue(entry)
  91. } else if (!entry['in']) {
  92. buckets[buckets.length - 1].enqueue(entry)
  93. } else {
  94. buckets[entry.out - entry['in'] + zeroIdx].enqueue(entry)
  95. }
  96. }
  97. export default greedyFAS