Подтвердить что ты не робот

Поиск минимального элемента на основе преобразованного значения

Вот эта задача пришла ко мне из обзора кода. Я хочу выбрать минимальное значение из набора, основываясь на специальном предикате сравнения. Вот так:

struct Complex { ... };

float calcReduction(Complex elem);

Complex findMinValueWithPredicates(const std::vector<Complex>& values)
{
  auto it = std::min_element(values.begin(), values.end(), 
                             [](const Complex& a, const Complex& b) { 
                               return calcReduction(a) < calcReduction(b); 
                             });

  if (it == values.end()) throw std::runtime_error("");

  return *it;
}

Здесь я нахожу минимальный элемент, основанный на предикате. Этот предикат вычисляет уменьшение обоих значений до float, а затем сравнивает эти поплавки. Отлично работает, выглядит аккуратно.

Вы видите проблему? Да, для набора элементов N calcReduction() называется 2N раз, тогда как достаточно вычислить его только N раз - один раз для каждого элемента.

Одним из способов решения этой проблемы является запись явных вычислений:

Complex findMinValueExplicit(const std::vector<Complex>& values)
{
  float minReduction = std::numeric_limits<float>::max();
  Complex minValue;

  for (Complex value : values)
  {
    float reduction = calcReduction(value);
    if (reduction < minReduction)
    {
      minReduction = reduction;
      minValue = value;
    }
  }

  if (minReduction == std::numeric_limits<float>::max()) throw std::runtime_error("");

  return minValue;
}

Он работает нормально, и мы вызываем N только calcReduction(). Тем не менее, это выглядит слишком многословным, и намерение не является таким ясным, по сравнению с явным вызовом min_element. Потому что, когда вы вызываете min_element, действительно легко догадаться, что вы найдете минимальный элемент, знаете ли.

Единственная идея, которую я имею сейчас, - создать собственный алгоритм, например min_element_with_reduction, принять диапазон и функцию сокращения. Звучит разумно, но мне интересно, есть ли готовые решения.

Любые идеи о том, как решить эту задачу с явным намерением и некоторыми готовыми решениями? Boost приветствуется. С++ 17 и диапазоны интересны для просмотра.

4b9b3361

Ответ 1

Вы можете использовать boost::range library.

auto reductionLambda = [](const Complex& a) { return calcReduction(a); };
auto it = boost::range::min_element(values | boost::adaptors::transformed( 
                             std::ref(reductionLambda));

Диапазоны сами по себе должны поступать на стандартный С++ с С++ 17.

Edit

Как мы выяснили в комментариях, это также сделает преобразование дважды.

Итак, здесь что-то весело:

#include <boost/iterator/iterator_adaptor.hpp>
#include <boost/assign.hpp>
#include <algorithm>
#include <iostream>
#include <vector>
#include <functional>


template <class Iterator, class UnaryFunction>
class memoizing_transform_iterator
  : public boost::iterator_adaptor<
        memoizing_transform_iterator<Iterator, UnaryFunction> // Derived
      , Iterator                                              // Base
      , std::decay_t<decltype(std::declval<UnaryFunction>()(std::declval<typename Iterator::value_type>()))> // Value
      , boost::forward_traversal_tag    // CategoryOrTraversal
    >
{
 public:
    memoizing_transform_iterator() {}

    explicit memoizing_transform_iterator(Iterator iter, UnaryFunction f)
      : memoizing_transform_iterator::iterator_adaptor_(iter), fun(f) {}

    static int total;
 private:
    friend class boost::iterator_core_access;
    void increment() { ++this->base_reference(); memoized = false; }

    using MemoType = std::decay_t<decltype(std::declval<UnaryFunction>()(std::declval<typename Iterator::value_type>()))>;      

    MemoType& dereference() const 
    {
        if (!memoized) {
            ++total;
            memoized = true;
            memo = fun(*this->base());
        }
        return memo;
    }

    UnaryFunction fun;
    mutable bool memoized = false;
    mutable MemoType memo;
};


template <class Iterator, class UnaryFunction>
auto make_memoizing_transform_iterator(Iterator i, UnaryFunction&& f)
{
    return memoizing_transform_iterator<Iterator, UnaryFunction>(i, f);
}



template<class I, class U>
int memoizing_transform_iterator<I, U>::total = 0;


// THIS IS COPIED FROM LIBSTDC++
template<typename _ForwardIterator>
   _ForwardIterator
     min_el(_ForwardIterator __first, _ForwardIterator __last)
     {
       if (__first == __last)
     return __first;
       _ForwardIterator __result = __first;
       while (++__first != __last)
     if (*__first < *__result)
       __result = __first;
       return __result;
     }


int main(int argc, const char* argv[])
{
    using namespace boost::assign;

    std::vector<int> input;
    input += 2,3,4,1,5,6,7,8,9,10;


    auto transformLambda = [](const int& a) { return a*2; };


    auto begin_it = make_memoizing_transform_iterator(input.begin(), std::ref(transformLambda));
    auto end_it = make_memoizing_transform_iterator(input.end(), std::ref(transformLambda));
    std::cout << *min_el(begin_it, end_it).base() << "\n";

    std::cout <<begin_it.total;

    return 0;
}

В основном я реализовал итератор, который запоминает результат вызова функтора преобразования. Странная часть состоит в том, что, по крайней мере, в онлайн-компиляторах, итераторы копируются до того, как их разыменованные значения сравниваются (таким образом, побеждая цель memoizing). Однако, когда я просто копировал реализацию из libstdС++, она работает так, как ожидалось. Возможно, вы могли бы попробовать его на настоящей машине? Живой пример здесь.

Небольшое редактирование: Я тестировал VS2015 и работает, как ожидалось, с помощью std::min_element.

Ответ 2

Единственное, чего не хватает, это мета-итератор.

Мета-итератор принимает итератор и создает итератор, содержащий его копию. Он передает все операции через содержащийся в нем итератор, за исключением случаев, когда разыменовывает вместо него копию содержащегося итератора.

С какой-либо осторожностью используемый для этого код также работает над созданием итератора над size_t или int или аналогичным torsor -likes.

template<class It, class R>
struct reduced_t {
  It it;
  R r;
  friend bool operator<( reduced_t const& lhs, reduced_t const& rhs ) {
    return lhs.r < rhs.r;
  }
};
template<class It, class F>
reduced_t<It, std::result_of_t<F(typename std::iterator_traits<It>::reference)>>
reducer( It it, F&& f ) {
  return {it, std::forward<F>(f)(*it)};
}

template<class It, class F>
It reduce( It begin, It end, F&& f ) {
  if (begin==end)
    return begin;

  return std::accumulate(
    meta_iterator(std::next(begin)), meta_iterator(end),
    reducer(begin, f),
    [&](
      auto&& reduced, // reduced_t<blah...> in C++11
      It i
    ) {
      auto r2 = reducer( i, f );
      return (std::min)(reduced, r2);
    }
  ).it;
};

f(*it) вызывается ровно один раз на итератор.

Я бы не назвал это... очевидным. Фокус в том, что мы используем accumulate и мета-итераторы для реализации min_element, тогда мы можем иметь accumulate работать с преобразованными элементами (которые вызываются один раз и возвращаются).

Вы можете переписать его в стиле программирования на основе стека с помощью примитивов, но есть много примитивов для записи. Может быть, post range-v3.


В этот момент я представляю себе какую-то мощную композиционную библиотеку программирования. Если бы я это сделал, мы могли бы сделать следующее:

reducer( X, f ) можно переписать graph( deref |then| f )(X), используя order_by( get_n_t<1> ) для упорядочения.

Вызов accumulate может читать accumulate( skip_first(range), g(begin(range)), get_least( order_by( get_n_t<1> ) ) ).

Не уверен, что это яснее.

Ответ 3

Здесь решение с использованием (уже работает с range-v3 library, реализация автором предстоящие диапазоны TS)

#include <range/v3/all.hpp>
#include <iostream>
#include <limits>

using namespace ranges::v3;

int main()
{
    auto const expensive = [](auto x) { static int n; std::cout << n++ << " "; return x; };
    auto const v = view::closed_iota(1,10) | view::transform(expensive); 

    auto const m1 = *min_element(v);
    std::cout << "\n" << m1 << "\n";

    auto const inf = std::numeric_limits<int>::max();    
    auto const min = [](auto x, auto y) { return std::min(x, y); };

    auto const m2 = accumulate(v, inf, min);
    std::cout << "\n" << m2 << "\n";    
}

Live On Coliru с выходом:

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 
1
19 20 21 22 23 24 25 26 27 28 
1

Как вы можете видеть, с помощью min_element выполняется сравнение 2N, но с использованием accumulate только N.

Ответ 4

Если вы берете minElem в качестве параметра лямбда, вы можете использовать min_element

Complex findMinValueWithPredicates(const std::vector<Complex>& values)
{
  float minElem = std::numeric_limits<float>::max();
  auto it = std::min_element(values.begin(), values.end(),
                             [&minElem](const Complex& a, const Complex& b) {
                               float tmp = calcReduction(a);
                               if (tmp < minElem) {
                                  minElem = tmp;
                                  return true;
                               }
                               return false;
                             });

  if (it == values.end()) throw std::runtime_error("");

  return *it;
}

Изменить: Почему это работает, когда b не используется? 25.4.7.21 min_element

21 Возвраты: первый итератор я в диапазоне [первый, последний], такой, что для каждого итератора j в диапазоне [первый, последний] следующее соответствующие условия выполняются:! (* j < * i) или comp (* j, * i) == false. Возвращает последний, если первый == последний.

потому что b должен был быть назван smallestYet (код из cplusplus.com)

template <class ForwardIterator>
  ForwardIterator min_element ( ForwardIterator first, ForwardIterator last )
{
  if (first==last) return last;
  ForwardIterator smallest = first;

  while (++first!=last)
    if (*first<*smallest)    // or: if (comp(*first,*smallest)) for version (2)
      smallest=first;
  return smallest;
}

Что привело меня к новой любимой цитате:

"В информатике есть только 10 сложных проблем: кэш-аннулирование, именование вещей и ошибки "один за другим".

  • кто-то прокомментировал, что мы можем быть вне очереди, поскольку мы не используем b.
  • Я беспокоюсь, что кешированный minElem может быть не прав.
  • И я понял, что имя b должно быть более значимым, поскольку это "разыменованный итератор до наименьшего элемента" или smallestYet.
  • Наконец, не все понимают двоичный код, когда он не написан с помощью "b" в конце.

Ответ 5

Вот еще один вариант, но он по-прежнему эффективно является вашим вторым решением. Честно говоря, это не выглядит ясным, но кому-то это может понравиться. (Я использую std::pair<float, Complex>, чтобы сохранить результат восстановления и уменьшенное значение.)

std::pair<float, Complex> result{std::numeric_limits<float>::max(), {}};
auto output_function = [&result](std::pair<float, Complex> candidate) {
    if (candidate.first < result.first)
        result = candidate;
};
std::transform(values.begin(), values.end(), 
               boost::make_function_output_iterator(output_function),
               [](Complex x) { return std::make_pair(calcReduction(x), x); });

P.S. Если ваш calcReduction стоит дорого, считаете ли вы кэширование результатов в Complex объектах? Это приведет к несколько более сложной реализации, но вы сможете использовать простой std::min_element, который делает ваши намерения ясными.