Skip to content

Commit b63aead

Browse files
authored
Add a utility for finding minimal topological sorts (#6884)
Reuse the code implementing Kahn's topological sort algorithm with a new configuration that uses a min-heap to always choose the best available element. Also add wrapper utilities that can find topological sorts of graphs with arbitrary element types, not just indices.
1 parent aa75698 commit b63aead

File tree

3 files changed

+170
-9
lines changed

3 files changed

+170
-9
lines changed

src/support/topological_orders.cpp

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,21 @@
1414
* limitations under the License.
1515
*/
1616

17+
#include <algorithm>
1718
#include <cassert>
1819

1920
#include "topological_orders.h"
2021

2122
namespace wasm {
2223

2324
TopologicalOrders::Selector
24-
TopologicalOrders::Selector::select(TopologicalOrders& ctx) {
25+
TopologicalOrders::Selector::select(TopologicalOrders& ctx,
26+
SelectionMethod method = InPlace) {
2527
assert(count >= 1);
2628
assert(start + count <= ctx.buf.size());
29+
if (method == MinHeap) {
30+
ctx.buf[start] = ctx.popChoice();
31+
}
2732
auto selection = ctx.buf[start];
2833
// The next selector will select the next index and will not be able to choose
2934
// the vertex we just selected.
@@ -33,7 +38,12 @@ TopologicalOrders::Selector::select(TopologicalOrders& ctx) {
3338
for (auto child : ctx.graph[selection]) {
3439
assert(ctx.indegrees[child] > 0);
3540
if (--ctx.indegrees[child] == 0) {
36-
ctx.buf[next.start + next.count++] = child;
41+
if (method == MinHeap) {
42+
ctx.pushChoice(child);
43+
} else {
44+
ctx.buf[next.start + next.count] = child;
45+
}
46+
++next.count;
3747
}
3848
}
3949
return next;
@@ -69,7 +79,7 @@ TopologicalOrders::Selector::advance(TopologicalOrders& ctx) {
6979
}
7080

7181
TopologicalOrders::TopologicalOrders(
72-
const std::vector<std::vector<size_t>>& graph)
82+
const std::vector<std::vector<size_t>>& graph, SelectionMethod method)
7383
: graph(graph), indegrees(graph.size()), buf(graph.size()) {
7484
if (graph.size() == 0) {
7585
return;
@@ -86,13 +96,19 @@ TopologicalOrders::TopologicalOrders(
8696
auto& first = selectors.back();
8797
for (size_t i = 0; i < graph.size(); ++i) {
8898
if (indegrees[i] == 0) {
89-
buf[first.count++] = i;
99+
if (method == MinHeap) {
100+
pushChoice(i);
101+
} else {
102+
buf[first.count] = i;
103+
}
104+
++first.count;
90105
}
91106
}
92107
// Initialize the full stack of selectors.
93108
while (selectors.size() < graph.size()) {
94-
selectors.push_back(selectors.back().select(*this));
109+
selectors.push_back(selectors.back().select(*this, method));
95110
}
111+
selectors.back().select(*this, method);
96112
}
97113

98114
TopologicalOrders& TopologicalOrders::operator++() {
@@ -117,4 +133,16 @@ TopologicalOrders& TopologicalOrders::operator++() {
117133
return *this;
118134
}
119135

136+
void TopologicalOrders::pushChoice(size_t choice) {
137+
choiceHeap.push_back(choice);
138+
std::push_heap(choiceHeap.begin(), choiceHeap.end(), std::greater<size_t>{});
139+
}
140+
141+
size_t TopologicalOrders::popChoice() {
142+
std::pop_heap(choiceHeap.begin(), choiceHeap.end(), std::greater<size_t>{});
143+
auto choice = choiceHeap.back();
144+
choiceHeap.pop_back();
145+
return choice;
146+
}
147+
120148
} // namespace wasm

src/support/topological_orders.h

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
#ifndef wasm_support_topological_orders_h
1818
#define wasm_support_topological_orders_h
1919

20+
#include <cassert>
2021
#include <cstddef>
2122
#include <optional>
23+
#include <unordered_map>
2224
#include <vector>
2325

2426
namespace wasm {
@@ -36,7 +38,8 @@ struct TopologicalOrders {
3638

3739
// Takes an adjacency list, where the list for each vertex is a sorted list of
3840
// the indices of its children, which will appear after it in the order.
39-
TopologicalOrders(const std::vector<std::vector<size_t>>& graph);
41+
TopologicalOrders(const std::vector<std::vector<size_t>>& graph)
42+
: TopologicalOrders(graph, InPlace) {}
4043

4144
TopologicalOrders begin() { return TopologicalOrders(graph); }
4245
TopologicalOrders end() { return TopologicalOrders({}); }
@@ -47,11 +50,16 @@ struct TopologicalOrders {
4750
bool operator!=(const TopologicalOrders& other) const {
4851
return !(*this == other);
4952
}
50-
const std::vector<size_t>& operator*() { return buf; }
51-
const std::vector<size_t>* operator->() { return &buf; }
53+
const std::vector<size_t>& operator*() const { return buf; }
54+
const std::vector<size_t>* operator->() const { return &buf; }
5255
TopologicalOrders& operator++();
5356
TopologicalOrders operator++(int) { return ++(*this); }
5457

58+
protected:
59+
enum SelectionMethod { InPlace, MinHeap };
60+
TopologicalOrders(const std::vector<std::vector<size_t>>& graph,
61+
SelectionMethod method);
62+
5563
private:
5664
// The input graph given as an adjacency list with edges from vertices to
5765
// their dependent children.
@@ -64,6 +72,9 @@ struct TopologicalOrders {
6472
// sequence of selected vertices followed by a sequence of possible choices
6573
// for the next vertex.
6674
std::vector<size_t> buf;
75+
// When we are finding the minimal topological order, store the possible
76+
// choices in this separate min-heap instead of directly in `buf`.
77+
std::vector<size_t> choiceHeap;
6778

6879
// The state for tracking the possible choices for a single vertex in the
6980
// output order.
@@ -78,19 +89,86 @@ struct TopologicalOrders {
7889

7990
// Select the next available vertex, decrement in-degrees, and update the
8091
// sequence of available vertices. Return the Selector for the next vertex.
81-
Selector select(TopologicalOrders& ctx);
92+
Selector select(TopologicalOrders& ctx, SelectionMethod method);
8293

8394
// Undo the current selection, move the next selection into the first
8495
// position and return the new selector for the next position. Returns
8596
// nullopt if advancing wraps back around to the original configuration.
8697
std::optional<Selector> advance(TopologicalOrders& ctx);
8798
};
8899

100+
void pushChoice(size_t);
101+
size_t popChoice();
102+
89103
// A stack of selectors, one for each vertex in a complete topological order.
90104
// Empty if we've already seen every possible ordering.
91105
std::vector<Selector> selectors;
92106
};
93107

108+
// A utility for finding a single topological order of a graph.
109+
struct TopologicalSort : private TopologicalOrders {
110+
TopologicalSort(const std::vector<std::vector<size_t>>& graph)
111+
: TopologicalOrders(graph) {}
112+
113+
const std::vector<size_t>& operator*() const {
114+
return TopologicalOrders::operator*();
115+
}
116+
};
117+
118+
// A utility for finding the topological order that is as close as possible to
119+
// the original order of elements. Internally uses a min-heap to choose the best
120+
// available next element.
121+
struct MinTopologicalSort : private TopologicalOrders {
122+
MinTopologicalSort(const std::vector<std::vector<size_t>>& graph)
123+
: TopologicalOrders(graph, MinHeap) {}
124+
125+
const std::vector<size_t>& operator*() const {
126+
return TopologicalOrders::operator*();
127+
}
128+
};
129+
130+
// A utility that finds a topological sort of a graph with arbitrary element
131+
// types.
132+
template<typename T, typename TopoSort = TopologicalSort>
133+
struct TopologicalSortOf {
134+
std::vector<T> order;
135+
136+
// The value of the iterators must be a pair of an element and an iterable of
137+
// its children.
138+
template<typename It> TopologicalSortOf(It begin, It end) {
139+
std::unordered_map<T, size_t> indices;
140+
std::vector<T> elements;
141+
// Assign indices to each element.
142+
for (auto it = begin; it != end; ++it) {
143+
auto inserted = indices.insert({it->first, elements.size()});
144+
assert(inserted.second && "unexpected repeat element");
145+
elements.push_back(inserted.first->first);
146+
}
147+
// Collect the graph in terms of indices.
148+
std::vector<std::vector<size_t>> indexGraph;
149+
indexGraph.reserve(elements.size());
150+
for (auto it = begin; it != end; ++it) {
151+
indexGraph.emplace_back();
152+
for (const auto& child : it->second) {
153+
indexGraph.back().push_back(indices.at(child));
154+
}
155+
}
156+
// Compute the topological order and convert back to original elements.
157+
order.reserve(elements.size());
158+
auto indexOrder = *TopoSort(indexGraph);
159+
for (auto i : indexOrder) {
160+
order.emplace_back(std::move(elements[i]));
161+
}
162+
}
163+
164+
const std::vector<T>& operator*() const { return order; }
165+
};
166+
167+
// A utility that finds the minimum topological sort of a graph with arbitrary
168+
// element types.
169+
template<typename T>
170+
using MinTopologicalSortOf = TopologicalSortOf<T, MinTopologicalSort>;
171+
94172
} // namespace wasm
95173

96174
#endif // wasm_support_topological_orders_h

test/gtest/topological-orders.cpp

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,58 @@ TEST(TopologicalOrdersTest, Diamond) {
9999
};
100100
EXPECT_EQ(results, expected);
101101
}
102+
103+
TEST(MinTopologicalSortTest, Empty) {
104+
Graph graph(0);
105+
EXPECT_EQ(*MinTopologicalSort(graph), std::vector<size_t>{});
106+
}
107+
108+
TEST(MinTopologicalSortTest, Unconstrained) {
109+
Graph graph(3);
110+
MinTopologicalSort order(graph);
111+
std::vector<size_t> expected{0, 1, 2};
112+
EXPECT_EQ(*MinTopologicalSort(graph), expected);
113+
}
114+
115+
TEST(MinTopologicalSortTest, Reversed) {
116+
Graph graph(3);
117+
graph[2].push_back(1);
118+
graph[1].push_back(0);
119+
std::vector<size_t> expected{2, 1, 0};
120+
EXPECT_EQ(*MinTopologicalSort(graph), expected);
121+
}
122+
123+
TEST(MinTopologicalSortTest, OneBeforeZero) {
124+
Graph graph(3);
125+
graph[1].push_back(0);
126+
// 2 last because it is greater than 1 and 0
127+
std::vector<size_t> expected{1, 0, 2};
128+
EXPECT_EQ(*MinTopologicalSort(graph), expected);
129+
}
130+
131+
TEST(MinTopologicalSortTest, TwoBeforeOne) {
132+
Graph graph(3);
133+
graph[2].push_back(1);
134+
// 0 first because it is less than 2 and 1
135+
std::vector<size_t> expected{0, 2, 1};
136+
EXPECT_EQ(*MinTopologicalSort(graph), expected);
137+
}
138+
139+
TEST(MinTopologicalSortTest, TwoBeforeZero) {
140+
Graph graph(3);
141+
graph[2].push_back(0);
142+
// 1 first because it is less than 2 and zero is not eligible.
143+
std::vector<size_t> expected{1, 2, 0};
144+
EXPECT_EQ(*MinTopologicalSort(graph), expected);
145+
}
146+
147+
TEST(MinTopologicalSortTest, Strings) {
148+
std::map<std::string, std::vector<std::string>> graph{
149+
{"animal", {"mammal"}},
150+
{"cat", {}},
151+
{"dog", {}},
152+
{"mammal", {"cat", "dog"}}};
153+
std::vector<std::string> expected{"animal", "mammal", "cat", "dog"};
154+
EXPECT_EQ(*MinTopologicalSortOf<std::string>(graph.begin(), graph.end()),
155+
expected);
156+
}

0 commit comments

Comments
 (0)