Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 15 additions & 69 deletions src/search/merge_and_shrink/merge_strategy_factory_sccs.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,10 @@
using namespace std;

namespace merge_and_shrink {
static bool compare_sccs_increasing(
const vector<int> &lhs, const vector<int> &rhs) {
return lhs.size() < rhs.size();
}

static bool compare_sccs_decreasing(
const vector<int> &lhs, const vector<int> &rhs) {
return lhs.size() > rhs.size();
}

MergeStrategyFactorySCCs::MergeStrategyFactorySCCs(
const shared_ptr<AbstractTask> &task, const OrderOfSCCs &order_of_sccs,
const shared_ptr<AbstractTask> &task,
const shared_ptr<MergeSelector> &merge_selector, utils::Verbosity verbosity)
: MergeStrategyFactory(task, verbosity),
order_of_sccs(order_of_sccs),
merge_selector(merge_selector) {
: MergeStrategyFactory(task, verbosity), merge_selector(merge_selector) {
}

unique_ptr<MergeStrategy> MergeStrategyFactorySCCs::compute_merge_strategy(
Expand All @@ -54,23 +42,6 @@ unique_ptr<MergeStrategy> MergeStrategyFactorySCCs::compute_merge_strategy(
}
vector<vector<int>> sccs(sccs::compute_maximal_sccs(cg));

// Put the SCCs in the desired order.
switch (order_of_sccs) {
case OrderOfSCCs::TOPOLOGICAL:
// SCCs are computed in topological order.
break;
case OrderOfSCCs::REVERSE_TOPOLOGICAL:
// SCCs are computed in topological order.
reverse(sccs.begin(), sccs.end());
break;
case OrderOfSCCs::DECREASING:
sort(sccs.begin(), sccs.end(), compare_sccs_decreasing);
break;
case OrderOfSCCs::INCREASING:
sort(sccs.begin(), sccs.end(), compare_sccs_increasing);
break;
}

if (log.is_at_least_normal()) {
log << "SCCs of the causal graph:" << endl;
}
Expand Down Expand Up @@ -110,24 +81,8 @@ bool MergeStrategyFactorySCCs::requires_goal_distances() const {

void MergeStrategyFactorySCCs::dump_strategy_specific_options() const {
if (log.is_at_least_normal()) {
log << "Merge order of sccs: ";
switch (order_of_sccs) {
case OrderOfSCCs::TOPOLOGICAL:
log << "topological";
break;
case OrderOfSCCs::REVERSE_TOPOLOGICAL:
log << "reverse topological";
break;
case OrderOfSCCs::DECREASING:
log << "decreasing";
break;
case OrderOfSCCs::INCREASING:
log << "increasing";
break;
}
log << endl;

log << "Merge strategy for merging within sccs: " << endl;
log << "Merge strategy for merging individual SCCs/partitions: "
<< endl;
merge_selector->dump_options(log);
}
}
Expand All @@ -153,14 +108,17 @@ class MergeStrategyFactorySCCsFeature
"In a nutshell, it computes the maximal strongly connected "
"components (SCCs) of the causal graph, "
"obtaining a partitioning of the task's variables. Every such "
"partition is then merged individually, using the specified fallback "
"merge strategy, considering the SCCs in a configurable order. "
"Afterwards, all resulting composite abstractions are merged to form "
"the final abstraction, again using the specified fallback merge "
"strategy and the configurable order of the SCCs.");

add_option<OrderOfSCCs>(
"order_of_sccs", "how the SCCs should be ordered", "topological");
"partition is then merged individually, using the score-based merge "
"strategy specified via the merge_selector option. Once all partitions "
"have been merged, the resulting product factors are merged according "
"to the score-based merge strategy.");
document_note(
"Note regarding how partitions are considered",
"Originally, SCC partitions are worked on 'one after the other' "
"in an order that could be specified and which defaulted to topological "
"order. In issue1171, this was changed to 'allowing working on any "
"partition', i.e., all pairs of factors in each partition form the set "
"of candidates scored by the score-based merge strategy.");
add_option<shared_ptr<TaskIndependentMergeSelector>>(
"merge_selector", "the fallback merge strategy to use");
add_merge_strategy_options_to_feature(*this);
Expand All @@ -170,23 +128,11 @@ class MergeStrategyFactorySCCsFeature
const plugins::Options &opts) const override {
return components::make_auto_task_independent_component<
MergeStrategyFactorySCCs, MergeStrategyFactory>(
opts.get<OrderOfSCCs>("order_of_sccs"),
opts.get<shared_ptr<TaskIndependentMergeSelector>>(
"merge_selector"),
get_merge_strategy_arguments_from_options(opts));
}
};

static plugins::FeaturePlugin<MergeStrategyFactorySCCsFeature> _plugin;

static plugins::TypedEnumPlugin<OrderOfSCCs> _enum_plugin(
{{"topological",
"according to the topological ordering of the directed graph "
"where each obtained SCC is a 'supervertex'"},
{"reverse_topological",
"according to the reverse topological ordering of the directed "
"graph where each obtained SCC is a 'supervertex'"},
{"decreasing", "biggest SCCs first, using 'topological' as tie-breaker"},
{"increasing",
"smallest SCCs first, using 'topological' as tie-breaker"}});
}
9 changes: 0 additions & 9 deletions src/search/merge_and_shrink/merge_strategy_factory_sccs.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,14 @@
namespace merge_and_shrink {
class MergeSelector;

enum class OrderOfSCCs {
TOPOLOGICAL,
REVERSE_TOPOLOGICAL,
DECREASING,
INCREASING
};

class MergeStrategyFactorySCCs : public MergeStrategyFactory {
OrderOfSCCs order_of_sccs;
std::shared_ptr<MergeSelector> merge_selector;
protected:
virtual std::string name() const override;
virtual void dump_strategy_specific_options() const override;
public:
MergeStrategyFactorySCCs(
const std::shared_ptr<AbstractTask> &task,
const OrderOfSCCs &order_of_sccs,
const std::shared_ptr<MergeSelector> &merge_selector,
utils::Verbosity verbosity);
virtual std::unique_ptr<MergeStrategy> compute_merge_strategy(
Expand Down
105 changes: 53 additions & 52 deletions src/search/merge_and_shrink/merge_strategy_sccs.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

#include "factored_transition_system.h"
#include "merge_selector.h"
#include "merge_tree.h"
#include "merge_tree_factory.h"
#include "transition_system.h"

#include <algorithm>
Expand All @@ -16,71 +14,74 @@ namespace merge_and_shrink {
MergeStrategySCCs::MergeStrategySCCs(
const FactoredTransitionSystem &fts,
const shared_ptr<MergeSelector> &merge_selector,
vector<vector<int>> &&non_singleton_cg_sccs)
vector<vector<int>> &&unfinished_clusters)
: MergeStrategy(fts),
merge_selector(merge_selector),
non_singleton_cg_sccs(move(non_singleton_cg_sccs)) {
unfinished_clusters(move(unfinished_clusters)) {
}

MergeStrategySCCs::~MergeStrategySCCs() {
}

pair<int, int> MergeStrategySCCs::get_next() {
if (current_ts_indices.empty()) {
/*
We are currently not dealing with merging all factors of an SCC, so
we need to either get the next one or allow merging any existing
factors of the FTS if there is no SCC left.
*/
if (non_singleton_cg_sccs.empty()) {
// We are done dealing with all SCCs, allow merging any factors.
current_ts_indices.reserve(fts.get_num_active_entries());
for (int ts_index : fts) {
current_ts_indices.push_back(ts_index);
}
} else {
/*
There is another SCC we have to deal with. Store its factors so
that we merge them over the next iterations.
*/
vector<int> &current_scc = non_singleton_cg_sccs.front();
assert(current_scc.size() > 1);
current_ts_indices = move(current_scc);
non_singleton_cg_sccs.erase(non_singleton_cg_sccs.begin());
static void compute_merge_candidates(
const vector<int> &indices, vector<pair<int, int>> &merge_candidates) {
for (size_t i = 0; i < indices.size(); ++i) {
int ts_index1 = indices[i];
for (size_t j = i + 1; j < indices.size(); ++j) {
int ts_index2 = indices[j];
merge_candidates.emplace_back(ts_index1, ts_index2);
}
} else {
// Add the most recent product to the current index set.
current_ts_indices.push_back(fts.get_size() - 1);
}
}

// Compute all merge candidates for the current set of indices.
vector<pair<int, int>> merge_candidates;
merge_candidates.reserve(
(current_ts_indices.size() * (current_ts_indices.size() - 1)) / 2);
assert(current_ts_indices.size() > 1);
for (size_t i = 0; i < current_ts_indices.size(); ++i) {
int ts_index1 = current_ts_indices[i];
assert(fts.is_active(ts_index1));
for (size_t j = i + 1; j < current_ts_indices.size(); ++j) {
int ts_index2 = current_ts_indices[j];
assert(fts.is_active(ts_index2));
merge_candidates.emplace_back(ts_index1, ts_index2);
pair<int, int> MergeStrategySCCs::get_next() {
if (unfinished_clusters.empty()) {
// We merged all clusters.
return merge_selector->select_merge(fts);
} else {
// There are clusters we still have to deal with.
vector<pair<int, int>> merge_candidates;

// Compute merge candidate pairs for each cluster.
vector<int> factor_to_cluster(fts.get_size(), -1);
for (size_t cluster_index = 0;
cluster_index < unfinished_clusters.size(); ++cluster_index) {
const vector<int> &cluster = unfinished_clusters[cluster_index];
for (int factor : cluster) {
factor_to_cluster[factor] = cluster_index;
}
compute_merge_candidates(cluster, merge_candidates);
}
}

// Select the next merge for the current set of indices.
pair<int, int> next_pair = merge_selector->select_merge_from_candidates(
fts, move(merge_candidates));
// Select the next merge from the allowed merge candidates.
pair<int, int> next_pair = merge_selector->select_merge_from_candidates(
fts, move(merge_candidates));

// Get the cluster from which we selected the next merge.
int affected_cluster_index = factor_to_cluster[next_pair.first];
assert(affected_cluster_index == factor_to_cluster[next_pair.second]);

// Remove the two merged indices from that cluster.
vector<int> &affected_cluster =
unfinished_clusters[affected_cluster_index];
for (vector<int>::iterator it = affected_cluster.begin();
it != affected_cluster.end();) {
if (*it == next_pair.first || *it == next_pair.second) {
it = affected_cluster.erase(it);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I read https://en.cppreference.com/w/cpp/container/vector/erase correctly, this invalidates the iterator it. I doubt a typical implementation would care, but this is probably still invalid C++. I suggest you access the vector with an index instead, i.e., for (size_t i = 0; i < affected_cluster.size(); ).

@silvansievers silvansievers Aug 11, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exactly do you think is the problem? The parameter it gets invalidated, yes, but the return value is a new valid iterator. The example in the link uses the exact same if-else-pattern as the code here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, I missed the assignment to it. So this works. Looking at this a bit more closely now, I am a bit queasy because this is a code smell -- repeatedly calling erase on a vector moves the rest of the vector on every erasure, and in generally this makes such filtering O(n^2) where it is O(n) when using STL algorithms like erase_if, which is what things like the C++ core guidelines would recommend in a case like this. Of course here it's still O(n) because we only remove two things, but erase_if (with a lambda) would still be the more idiomatic C++ approach.

Now that we require c++20, there is a nice special version of erase_if that just takes a vector and a predicate as an argument and does all the resizing etc.

Something like this could replace the for loop and be more efficient as well:

erase_if(affected_cluster, [&](int elem) {
    return elem == next_pair.first || elem == next_pair.second; });```

} else {
++it;
}
}

// Remove the two merged indices from the current index set.
for (vector<int>::iterator it = current_ts_indices.begin();
it != current_ts_indices.end();) {
if (*it == next_pair.first || *it == next_pair.second) {
it = current_ts_indices.erase(it);
if (affected_cluster.empty()) {
// If the cluster got empty, remove it.
unfinished_clusters.erase(
unfinished_clusters.begin() + affected_cluster_index);
} else {
++it;
// Otherwise, add the index of the to-be-created product factor.
affected_cluster.push_back(fts.get_size());
}
return next_pair;
}
return next_pair;
}
}
6 changes: 2 additions & 4 deletions src/search/merge_and_shrink/merge_strategy_sccs.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ namespace merge_and_shrink {
class MergeSelector;
class MergeStrategySCCs : public MergeStrategy {
std::shared_ptr<MergeSelector> merge_selector;
std::vector<std::vector<int>> non_singleton_cg_sccs;

std::vector<int> current_ts_indices;
std::vector<std::vector<int>> unfinished_clusters;
public:
MergeStrategySCCs(
const FactoredTransitionSystem &fts,
const std::shared_ptr<MergeSelector> &merge_selector,
std::vector<std::vector<int>> &&non_singleton_cg_sccs);
std::vector<std::vector<int>> &&unfinished_clusters);
virtual ~MergeStrategySCCs() override;
virtual std::pair<int, int> get_next() override;
};
Expand Down
Loading