使用std::min_element()时保存函数求值

Saving function evaluations while using std::min_element()

本文关键字:保存 函数 std min element 使用      更新时间:2023-10-16

假设给定一个二维点的向量,并期望找到具有最小欧几里得范数的点。

这些点被提供为具有以下typedef std::pair<double, double> point_tstd::vector<point_t> points。可以使用计算范数

double norm(point_t p)
{
    return pow(p.first, 2) + pow(p.second, 2);
}

我自己写循环,我会做以下事情:

auto leastPoint = points.cend();
auto leastNorm = std::numeric_limits<double>::max();
for (auto iter = points.cbegin(), end = points.cend(); iter != end; ++iter)
{
    double const currentNorm = norm(*iter);
    if (currentNorm < leastNorm)
    {
        leastNorm = currentNorm;
        leastPoint = iter;
    }
}

但是应该使用STL算法,而不是连接自己的循环,所以我很想使用以下方法:

auto const leastPoint = std::min_element(points.cbegin(), points.cend(),
    [](point_t const lhs, point_t const rhs){ return norm(lhs) < norm(rhs); });

但有一点需要注意:如果n = points.size(),那么第一个实现需要norm()n评估,但第二个实现则需要2n-2评估。(至少如果使用这种可能的实现方式)

所以我的问题是,是否存在任何STL算法,我可以用它来找到这一点,但只有nnorm()的评估?

注意:

  • 我知道Oh的复杂性是一样的,但后者会导致两倍的评估
  • 仅仅为了使用STL算法,创建一个单独的向量并用距离填充它似乎有点过头了——对此有不同的意见
  • edit:我实际上需要一个向量元素的迭代器来擦除这一点

您可以使用std::accumulate(在algorithm标头中):

累计接收:

  • range
  • initial value
  • binary operator(可选,如果未通过,则调用operator+

initial valuerange的每个元素将被馈送到operator中,操作员将返回initial value的类型的结果,该结果将被馈送至具有该范围的下一个元素的operator的下一调用中,依此类推

示例代码(使用C++11测试GCC 4.9.0):

#include <algorithm>
#include <iostream>
#include <vector>
#include <cmath>
typedef std::pair<double, double> point_t;
struct norm_t {
    point_t p;
    double norm;
};
double norm(const point_t& p) {
    return std::pow(p.first, 2) + std::pow(p.second, 2);
}
norm_t min_norm(const norm_t& x, const point_t& y) {
    double ny = norm(y);
    if (ny < x.norm)
        return {y, ny};
    return x;
}
int main() {
    std::vector<point_t> v{{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10}};
    norm_t first_norm{v[0], norm(v[0])};
    auto min_norm_point =
        std::accumulate(v.begin(), v.end(), first_norm, min_norm);
    std::cout << "(" << min_norm_point.p.first << "," << min_norm_point.p.second
              << "): " << min_norm_point.norm << 'n';
}

您可以在functor中缓存最小范数,以避免额外的计算(请注意:我使用的是关于std::min_element实现的信息)。第二个元素是找到的最小元素,第一个元素是迭代元素。

struct minimum_norm {
    minimum_norm() : cached_norm(-1) {}
    bool operator()(const point_t& first, const point_t& second) {
        if (cached_norm == -1)
            cached_norm = norm(second);
        double norm_first = norm(first);
        if (norm_first < cached_norm) {
            cached_norm = norm_first;
            return true;
        }
        return false;
    }
private:
    double cached_norm;
};
int main()
{
    std::vector<point_t> v{{3, 4}, {5, 6}, {1, 2}, {7, 8}, {9, 10}};
    auto result = std::min_element(std::begin(v), std::end(v), minimum_norm());
    std::cout << "min element at: " << std::distance(std::begin(v), result) << std::endl;
}

这就是boost迭代器库中的boost::transform_iterator所要解决的问题。然而,修饰迭代器方法存在局限性,C++标准委员会范围工作组正在考虑将范围添加到标准中,这可能会允许一种更实用的管道方法,例如,在不需要临时存储的情况下转换为min_element。

Eric Niebler在他的博客上发布了一些关于靶场的有趣帖子。

不幸的是,考虑到min_element的典型实现方式,transform_iterator并不能完全解决您的问题——每次比较都会取消对两个迭代器的引用,因此您的函数最终仍会被频繁调用。您可以使用boost迭代器_adaptor来实现类似于"caching_transform_iterator"的东西,这样可以避免对每个解引用进行重新计算,但对于norm()这样的东西来说,这可能会有些过头了。如果你有一个更昂贵的计算,这可能是一个有用的技术。

编辑:尽管如此,我还是误解了这个问题。

我认为你认为min_element将执行2N-2比较的假设是错误的

根据min_element的c++引用,您可以看到该算法基本上执行N个比较,这是未排序数组的最小值。

以下是www.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;
}