Skip to content

Commit 22639d3

Browse files
perf: improve perf of is_acyclic
1 parent cacc96d commit 22639d3

5 files changed

Lines changed: 90 additions & 39 deletions

File tree

include/libsemigroups/detail/path-iterators.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@ namespace libsemigroups {
230230
using iterator_category = std::forward_iterator_tag;
231231

232232
private:
233+
// TODO(1) maybe try replacing _can_reach_target with std::unordered_set,
234+
// then we can use the output of ancestors_of directly
233235
std::vector<bool> _can_reach_target;
234236
value_type _edges;
235237
WordGraph<Node> const* _word_graph;

include/libsemigroups/detail/path-iterators.tpp

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -301,37 +301,12 @@ namespace libsemigroups {
301301
template <typename Node>
302302
void const_pstilo_iterator<Node>::init_can_reach_target() {
303303
if (_can_reach_target.empty()) {
304-
std::vector<std::vector<node_type>> in_neighbours(
305-
_word_graph->number_of_nodes(), std::vector<node_type>({}));
306-
for (auto n = _word_graph->cbegin_nodes();
307-
n != _word_graph->cend_nodes();
308-
++n) {
309-
for (auto e = _word_graph->cbegin_targets(*n);
310-
e != _word_graph->cend_targets(*n);
311-
++e) {
312-
if (*e != UNDEFINED) {
313-
in_neighbours[*e].push_back(*n);
314-
}
315-
}
316-
}
317-
318304
_can_reach_target.resize(_word_graph->number_of_nodes(), false);
319-
_can_reach_target[_target] = true;
320-
std::vector<node_type>& todo = in_neighbours[_target];
321-
std::vector<node_type> next;
322-
323-
while (!todo.empty()) {
324-
for (auto& m : todo) {
325-
if (_can_reach_target[m] == 0) {
326-
_can_reach_target[m] = true;
327-
next.insert(next.end(),
328-
in_neighbours[m].cbegin(),
329-
in_neighbours[m].cend());
330-
}
331-
}
332-
std::swap(next, todo);
333-
next.clear();
334-
}
305+
auto ancestors
306+
= word_graph::ancestors_of_no_checks(*_word_graph, _target);
307+
std::for_each(ancestors.begin(), ancestors.end(), [this](auto n) {
308+
_can_reach_target[n] = true;
309+
});
335310
}
336311
}
337312

include/libsemigroups/word-graph.hpp

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2200,6 +2200,10 @@ namespace libsemigroups {
22002200
[[nodiscard]] std::unordered_set<Node1>
22012201
nodes_reachable_from(WordGraph<Node1> const& wg, Node2 source);
22022202

2203+
template <typename Node1, typename Node2>
2204+
[[nodiscard]] std::unordered_set<Node1>
2205+
ancestors_of(WordGraph<Node1> const& wg, Node2 source);
2206+
22032207
//! \brief Returns the std::unordered_set of nodes reachable from a given
22042208
//! node in a word graph.
22052209
//!
@@ -2225,6 +2229,10 @@ namespace libsemigroups {
22252229
[[nodiscard]] std::unordered_set<Node1>
22262230
nodes_reachable_from_no_checks(WordGraph<Node1> const& wg, Node2 source);
22272231

2232+
template <typename Node1, typename Node2>
2233+
[[nodiscard]] std::unordered_set<Node1>
2234+
ancestors_of_no_checks(WordGraph<Node1> const& wg, Node2 source);
2235+
22282236
//! \brief Returns the number of nodes reachable from a given node in a
22292237
//! word graph.
22302238
//!
@@ -2824,7 +2832,7 @@ namespace libsemigroups {
28242832
// always have an odd number of arguments, so we check that it's even
28252833
// here (the argument x and an odd number of further arguments).
28262834
WordGraph<Node> xy;
2827-
operator()(xy, x, std::forward<Args>(args)...);
2835+
operator()(xy, x, std::forward<Args>(args)...);
28282836
return xy;
28292837
}
28302838

@@ -2859,7 +2867,7 @@ namespace libsemigroups {
28592867
return is_subrelation(x, static_cast<Node>(0), y, static_cast<Node>(0));
28602868
}
28612869
}; // JoinerMeeterCommon
2862-
} // namespace detail
2870+
} // namespace detail
28632871

28642872
//! \ingroup word_graph_group
28652873
//! \brief Class for taking joins of word graphs.

include/libsemigroups/word-graph.tpp

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -774,9 +774,10 @@ namespace libsemigroups {
774774
Node1 next_preorder_num = 0;
775775
std::vector<Node1> postorder(N, N);
776776
Node1 next_postorder_num = 0;
777-
// TODO(1) there should be a better way of doing this
777+
778+
auto ancestors = ancestors_of_no_checks(wg, target);
778779
for (auto n : wg.nodes()) {
779-
if (!is_reachable(wg, n, static_cast<Node1>(target))) {
780+
if (ancestors.count(n) == 0) {
780781
preorder[n] = N + 1;
781782
}
782783
}
@@ -875,6 +876,55 @@ namespace libsemigroups {
875876
return nodes_reachable_from_no_checks(wg, source);
876877
}
877878

879+
template <typename Node1, typename Node2>
880+
std::unordered_set<Node1> ancestors_of_no_checks(WordGraph<Node1> const& wg,
881+
Node2 target) {
882+
static_assert(sizeof(Node2) <= sizeof(Node1));
883+
using label_type = typename WordGraph<Node1>::label_type;
884+
885+
size_t const N = wg.number_of_nodes();
886+
size_t const M = wg.out_degree();
887+
888+
// Reverse the WordGraph and then just find the nodes reachable from
889+
// target in the reversed graph. Since the reverse of a WordGraph is no
890+
// longer a WordGraph we use a vector of vectors here. Alternatively, we
891+
// could use the technique used in WordGraphWithSources (the sources are
892+
// essentially the reversed graph) to create the reversed graph (or just
893+
// use it if we know it already, like in ToddCoxeter).
894+
std::vector<std::vector<Node1>> in_neighbours(N, std::vector<Node1>({}));
895+
for (Node1 s = 0; s < N; ++s) {
896+
for (label_type a = 0; a < M; ++a) {
897+
auto t = wg.target_no_checks(s, a);
898+
if (t != UNDEFINED) {
899+
in_neighbours[t].push_back(s);
900+
}
901+
}
902+
}
903+
904+
std::unordered_set<Node1> seen;
905+
std::stack<Node1> stack;
906+
stack.push(target);
907+
908+
while (!stack.empty()) {
909+
Node1 s = stack.top();
910+
stack.pop();
911+
if (seen.insert(s).second) {
912+
for (auto t : in_neighbours[s]) {
913+
stack.push(t);
914+
}
915+
}
916+
}
917+
return seen;
918+
}
919+
920+
template <typename Node1, typename Node2>
921+
std::unordered_set<Node1> ancestors_of(WordGraph<Node1> const& wg,
922+
Node2 target) {
923+
static_assert(sizeof(Node2) <= sizeof(Node1));
924+
throw_if_node_out_of_bounds(wg, static_cast<Node1>(target));
925+
return ancestors_of_no_checks(wg, target);
926+
}
927+
878928
template <typename Node1, typename Node2, typename Iterator>
879929
Node1 follow_path(WordGraph<Node1> const& wg,
880930
Node2 from,
@@ -1552,8 +1602,8 @@ namespace libsemigroups {
15521602
_uf.init(xnum_nodes_reachable_from_root + ynum_nodes_reachable_from_root);
15531603
_uf.unite(xroot, yroot + xnum_nodes_reachable_from_root);
15541604

1555-
// The stack can't be empty if this function runs to the end so no need to
1556-
// do anything.
1605+
// The stack can't be empty if this function runs to the end so no need
1606+
// to do anything.
15571607
LIBSEMIGROUPS_ASSERT(_stck.empty());
15581608
// 0 .. x.number_of_nodes() - 1, x.number_of_nodes() ..
15591609
// x.number_of_nodes() + y.number_of_nodes() -1
@@ -1599,9 +1649,9 @@ namespace libsemigroups {
15991649
ynum_nodes_reachable_from_root,
16001650
yroot);
16011651
_uf.normalize();
1602-
// It can be that _uf is equivalent to [0, 0, 2] at this point (and there's
1603-
// no way for it to not be like this, because 2 doesn't belong to the class
1604-
// of 0), and so we require the following lookup.
1652+
// It can be that _uf is equivalent to [0, 0, 2] at this point (and
1653+
// there's no way for it to not be like this, because 2 doesn't belong
1654+
// to the class of 0), and so we require the following lookup.
16051655
_lookup.resize(xnum_nodes_reachable_from_root);
16061656
LIBSEMIGROUPS_ASSERT(_lookup.size() == xnum_nodes_reachable_from_root);
16071657
std::fill(_lookup.begin(), _lookup.end(), static_cast<Node>(UNDEFINED));

tests/test-todd-coxeter.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
#include "Catch2-3.8.0/catch_amalgamated.hpp" // for TEST_CASE
2424
#include "libsemigroups/constants.hpp"
25+
#include "libsemigroups/word-graph.hpp"
2526
#include "test-main.hpp" // for LIBSEMIGROUPS_TEST_CASE
2627

2728
#include "libsemigroups/bmat8.hpp"
@@ -928,6 +929,16 @@ namespace libsemigroups {
928929

929930
REQUIRE(tc.number_of_classes() == 6);
930931
REQUIRE(index_of(tc, {1}) == index_of(tc, {2}));
932+
REQUIRE(tc.word_graph().number_of_nodes() == 7);
933+
REQUIRE(tc.word_graph().target(0, 0) == 1);
934+
auto pred = word_graph::ancestors_of_no_checks(tc.word_graph(), 1);
935+
std::vector result(pred.begin(), pred.end());
936+
std::sort(result.begin(), result.end());
937+
REQUIRE(result == std::vector<uint32_t>({0, 1, 2, 3, 4, 5, 6}));
938+
auto desc = word_graph::nodes_reachable_from(tc.word_graph(), 1);
939+
result.assign(desc.begin(), desc.end());
940+
std::sort(result.begin(), result.end());
941+
REQUIRE(result == std::vector<uint32_t>({1, 2, 3, 4, 5, 6}));
931942
}
932943

933944
LIBSEMIGROUPS_TEST_CASE("ToddCoxeter",
@@ -3191,6 +3202,11 @@ namespace libsemigroups {
31913202

31923203
REQUIRE(H.number_of_classes() == 16'384);
31933204

3205+
REQUIRE(word_graph::is_reachable(H.word_graph(), 0, 0));
3206+
REQUIRE(word_graph::ancestors_of_no_checks(H.word_graph(), 0).size()
3207+
== 16'384);
3208+
REQUIRE(!word_graph::is_acyclic(H.word_graph(), 0, 0));
3209+
31943210
// The following no longer works
31953211
// REQUIRE(class_of(H, "").size_hint() == POSITIVE_INFINITY);
31963212
REQUIRE((class_of(H, "") | rx::take(50) | rx::to_vector())

0 commit comments

Comments
 (0)