Grim Fandango
Grim Fandango

Reputation: 2426

How to compose generators with STL algorithms

I have an algorithm which generates combinations from entries of a container and I want to find the combination which minimizes a cost function:

struct Vec { double x; double y; };

double cost( Vec a, Vec b ) {
    double dx = a.x - b.x; 
    double dy = a.y - b.y; 
    return dx*dx + dy*dy;
}


pair<Vec,Vec> get_pair_with_minimum_cost ( vector<Vec> inp, double (*cost_fun)(Vec,Vec) )
{
    pair<Vec,Vec> result;
    double min_cost = FLT_MAX;

    size_t sz = inp.size();
    for(size_t i=0; i<sz; i++) {
        for (size_t j=i; j<sz; j++) {
            double cost = cost_fun(inp[i], inp[j]);
            if (cost < min_cost) {
                min_cost = cost;
                result = make_pair(inp[i], inp[j]);
            }
        }
    }

    return result;
}

vector <Vec> inp = {....};

auto best_pair = get_pair_with_minimum_cost ( inp, cost );

Unfortunately, get_pair_with_minimum_cost() does 2 jobs:

I could break them in two functions, like:

but I do not want the pay the cost of creating and extra container with temporary data (all_combinations).

Questions:

  1. Can I rewrite the generate_all_combinations_of() such that it uses yield or the new std::ranges in such a way that I can combine it with STL algorithms such as find_if, any_of, min_element or even adjacent_pair ?
    The great thing about this 'generator' function is that it is easy to read, so I would like to keep it as readable as possible.
    NB: some of these algorithms need to break the loop.

  2. What is the official name of combining entries this way? It this the combinations used in 'bubble-sort'.

Upvotes: 6

Views: 234

Answers (3)

jorgbrown
jorgbrown

Reputation: 2051

Here's how I would write the function in c++17, using algorithms' min_element function, with no need for a separate container that stores the intermediate results. I know you were looking for a c++20 solution, but this code does work fine under c++20, and perhaps it gives you some ideas about adapting functions to ranges when the range isn't just one of the ranges supplied by c++20's ranges library.

// TwoContainerRanger is an iterable container where the iterator consists
// of two indices that match the given filter, and whose iterators, when
// dereferenced, return the result of calling func with the
// elements of the two containers, at those two indices.
// filter can be nullptr.
template <typename Container1, typename Container2, typename Func>
struct TwoContainerRanger {
  Container1 &c1;
  Container2 &c2;
  const Func &fun;
  bool (*restriction)(size_t i1, size_t i2);

  TwoContainerRanger(Container1 &container1, Container2 &container2,
                     bool (*filter)(size_t i1, size_t i2), const Func &func)
      : c1(container1), c2(container2), fun(func), restriction(filter) {}

  struct Iterator {
    const TwoContainerRanger *gen;
    size_t index1, index2;
    auto &operator++() {
      do {
        if (++index1 == gen->c1.size()) {
          if (++index2 == gen->c2.size()) {
            // we leave both indices pointing to the end
            // to indicate that we have reached the end.
            return *this;
          } else {
            index1 = 0u;
          }
        }
      } while (gen->restriction && gen->restriction(index1, index2) == false);
      return *this;
    }
    bool operator==(const Iterator &other) const = default;
    bool operator!=(const Iterator &other) const = default;
    auto operator*() const {
      return gen->fun(gen->c1[index1], gen->c2[index2]);
    }
  };
  Iterator begin() {
    Iterator b{this, size_t(0) - 1, 0u};
    return ++b;  // automatically applies the restriction
  }
  Iterator end() { return Iterator{this, c1.size(), c2.size()}; }
};

Calling it looks like this:

int main() {
  std::array<Vec, 5> ar = {Vec{0, 0}, Vec{1, 1}, Vec{3, 3}, Vec{7, 7},
                           Vec{3.1, 3.1}};
  TwoContainerRanger tcr{ar, ar, Triangle, cost};

  auto result = std::min_element(tcr.begin(), tcr.end());
  std::cout << "Min was at (" << result.index1 << "," << result.index2
            << "); cost was " << *result << '\n';
}

Here's a demo.

Upvotes: 1

cigien
cigien

Reputation: 60228

Here's how I would write the function in c++20, using range views and algorithms so that there isn't a separate container that stores the intermediate results:

double get_minimum_cost(auto const & inp)
{
  namespace rs = std::ranges;
  namespace rv = std::ranges::views;

  // for each i compute the minimum cost for all j's  
  auto min_cost_from_i = [&](auto i) 
  {

    auto costs_from_i = rv::iota(i + 1, inp.size())
                      | rv::transform([&](auto j) 
                        { 
                          return cost(inp[i], inp[j]); 
                        });

    return *rs::min_element(costs_from_i);
  };

  // compute min costs for all i's
  auto all_costs = rv::iota(0u, inp.size())
                 | rv::transform(min_cost_from_i);

  return *rs::min_element(all_costs);
}

Here's a demo.

Note that the solution doesn't compare the cost between same elements, since the cost function example you showed would have a trivial result of 0. For a cost function that doesn't return 0, you can adapt the solution to generate a range from i instead of i + 1. Also, if the cost function is not symmetric, make that range start from 0 instead of i.

Also, this function has UB if you call it with an empty range, so you should check for that as well.

Upvotes: 2

pratikpc
pratikpc

Reputation: 606

There is http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2168r0.pdf who's development I would follow

If you are using MSVC, and can use their experimental/generator (not sure if others support it yet), you can use

std::experimental::generator<std::size_t> Generate(std::size_t const end){
   for(std::size_t i = 0; i < end; ++i)
      co_yield i;
}

int main(){
   auto vals = Generate(22);
   auto const result = *std::min_element(std::begin(vals),std::end(vals));
   std::cout <<'\n' << " " << result;
}

Here you would need to modify the Generate function to Yield a pair/or to yield cost

(My recommendation would be to Keep things simple and yield the cost)

Then use vals to find min_cost

Ranges

Based on what I can find about the Ranges Proposal, it works on the basis of std::begin and std::end both of which experimental::generator provides

So it should probably work

Upvotes: 1

Related Questions