C++中置换组合的库函数

Posted

技术标签:

【中文标题】C++中置换组合的库函数【英文标题】:Library function for Permutation and Combination in C++ 【发布时间】:2010-02-06 03:37:16 【问题描述】:

C++ 中使用最广泛的现有库是什么,可以提供 n 个元素中 k 个元素的所有组合和排列?

我问的不是算法,而是现有的库或方法。

谢谢。

【问题讨论】:

复制:***.com/questions/1205744/… 【参考方案1】:

我决定在这里测试 dman 和 Charles Bailey 的解决方案。我将它们分别称为解决方案 A 和 B。我的测试是访问vector<int> size = 100, 5 的每个组合。这是测试代码:

测试代码

struct F

    unsigned long long count_;

    F() : count_(0) 

    bool operator()(std::vector<int>::iterator, std::vector<int>::iterator)
    ++count_; return false;
;

int main()

    typedef std::chrono::high_resolution_clock Clock;
    typedef std::chrono::duration<double> sec;
    typedef std::chrono::duration<double, std::nano> ns;
    int n = 100;
    std::vector<int> v(n);
    std::iota(v.begin(), v.end(), 0);
    std::vector<int>::iterator r = v.begin() + 5;
    F f;
    Clock::time_point t0 = Clock::now();
    do
    
        f(v.begin(), r);
     while (next_combination(v.begin(), r, v.end()));
    Clock::time_point t1 = Clock::now();
    sec s0 = t1 - t0;
    ns pvt0 = s0 / f.count_;
    std::cout << "N = " << v.size() << ", r = " << r-v.begin()
              << ", visits = " << f.count_ << '\n'
              << "\tnext_combination total = " << s0.count() << " seconds\n"
              << "\tnext_combination per visit = " << pvt0.count() << " ns";

所有代码都是在 2.8 GHz Intel Core i5 上使用 clang++ -O3 编译的。

解决方案 A

解决方案 A 导致无限循环。即使我使n 非常小,这个程序也永远不会完成。后来因为这个原因被否决了。

解决方案 B

这是一个编辑。解决方案 B 在编写此答案的过程中发生了变化。起初它给出了错误的答案,并且由于非常及时的更新,它现在给出了正确的答案。打印出来:

N = 100, r = 5, visits = 75287520
    next_combination total = 4519.84 seconds
    next_combination per visit = 60034.3 ns

解决方案 C

接下来我尝试了N2639 的解决方案,它看起来与解决方案 A 非常相似,但工作正常。我将此解决方案称为 C 并打印出来:

N = 100, r = 5, visits = 75287520
    next_combination total = 6.42602 seconds
    next_combination per visit = 85.3531 ns

解决方案 C 比解决方案 B 快 703 倍。

解决方案 D

终于找到了解决方案 D here。此解决方案具有不同的签名/样式,称为for_each_combination,其用法与std::for_each 非常相似。上面的驱动代码在定时器调用之间变化如下:

Clock::time_point t0 = Clock::now();
f = for_each_combination(v.begin(), r, v.end(), f);
Clock::time_point t1 = Clock::now();

解决方案 D 打印出来:

N = 100, r = 5, visits = 75287520
    for_each_combination = 0.498979 seconds
    for_each_combination per visit = 6.62765 ns

解决方案 D 比解决方案 C 快 12.9 倍,比解决方案 B 快 9000 多倍。

我认为这是一个相对较小的问题:只有 7500 万次访问。随着访问次数增加到数十亿,这些算法之间的性能差异继续扩大。解决方案 B 已经很笨拙了。解决方案 C 最终变得笨拙。解决方案 D 是访问我所知道的所有组合的性能最高的算法。

link showing solution D 还包含其他几种算法,用于枚举和访问具有各种属性(循环、可逆等)的排列。这些算法中的每一个都是以性能作为目标之一而设计的。请注意,这些算法都不要求初始序列按排序顺序排列。元素甚至不必是LessThanComparable

【讨论】:

感谢您在我的回答中发现错误。请注意,我不会因为您在另一个答案中提到我的名字而收到通知,因此为了将您的反对票与旧问题经常吸引的随机无法解释的反对票区分开来,如果您对我的错误答案发表评论会很有帮助这样我就可以知道我的错误了。 注意到了,抱歉我缺乏礼仪。我是这里的新手,会相应地修改我的行为,谢谢。 既然我已经详细阅读了您的链接,请向我 +1。我的回答是针对最小的实现工作(仅限 C++03)。这个答案给出了一个解决方案,实际上对于非平凡的输入长度是可以接受的。 仅供参考,我在我的固定代码上运行了您的测试用例并得到了visits = 75287520,这更好,但next_combination total = 3418.53 seconds 显然更糟。不过,我认为我的机器比 i5 和 gcc 早一两代,而不是 clang。 我对 g++-4.2 的正确性进行了健全性检查,并通过您更新的代码得到了正确的答案。不过我没有在那里进行性能测试。【参考方案2】:

组合:来自Mark Nelson's article 在同一主题上,我们有next_combination 排列:来自STL,我们有std::next_permutation

   template <typename Iterator>
   inline bool next_combination(const Iterator first, Iterator k, const Iterator last)
   
      if ((first == last) || (first == k) || (last == k))
         return false;
      Iterator itr1 = first;
      Iterator itr2 = last;
      ++itr1;
      if (last == itr1)
         return false;
      itr1 = last;
      --itr1;
      itr1 = k;
      --itr2;
      while (first != itr1)
      
         if (*--itr1 < *itr2)
         
            Iterator j = k;
            while (!(*itr1 < *j)) ++j;
            std::iter_swap(itr1,j);
            ++itr1;
            ++j;
            itr2 = k;
            std::rotate(itr1,j,last);
            while (last != j)
            
               ++j;
               ++itr2;
            
            std::rotate(k,itr2,last);
            return true;
         
      
      std::rotate(first,k,last);
      return false;
   

【讨论】:

如果您使用大量数据,这种方法很快就会变得低效,因为对 std::rotate 的调用变得昂贵。如果您真的需要快速组合生成,您不想旋转所有数据。有关执行此操作的示例,请查找格雷码:en.wikipedia.org/wiki/Gray_code。 @shuttle87:首先,您不必旋转大量数据,旋转索引或指向数据的指针通常就足够了。其次,您不太可能必须枚举超过 20 个元素的排列。甚至20!迭代几乎无法管理,30!是完全不可行的。因此,您不必旋转超过大约 20 个元素。 itr1 = k; 行可能包含错误,因为它使上面的itr1--; 无效。【参考方案3】:

此答案提供了最小的实施工作解决方案。如果您想检索大输入范围的组合,它的性能可能无法接受。

标准库有std::next_permutation,您可以轻松地从它构建一个next_k_permutation,然后从它构建一个next_combination

template<class RandIt, class Compare>
bool next_k_permutation(RandIt first, RandIt mid, RandIt last, Compare comp)

    std::sort(mid, last, std::tr1::bind(comp, std::tr1::placeholders::_2
                                            , std::tr1::placeholders::_1));
    return std::next_permutation(first, last, comp);

如果您没有tr1::bindboost::bind,则需要构建一个函数对象,将参数交换到给定的比较。当然,如果您只对next_combinationstd::less 变体感兴趣,那么您可以直接使用std::greater

template<class RandIt>
bool next_k_permutation(RandIt first, RandIt mid, RandIt last)

    typedef typename std::iterator_traits<RandIt>::value_type value_type;

    std::sort(mid, last, std::greater< value_type >());
    return std::next_permutation(first, last);

这是next_combination 的一个相对安全的版本。如果您可以保证[mid, last) 的范围与调用next_combination 后的顺序一样,那么您可以使用更简单的:

template<class BiDiIt, class Compare>
bool next_k_permutation(BiDiIt first, BiDiIt mid, BiDiIt last, Compare comp)

    std::reverse(mid, last);
    return std::next_permutation(first, last, comp);

这也适用于双向迭代器以及随机访问迭代器。

要输出组合而不是 k-permutations,我们必须确保每个组合只输出一次,因此只有当它是按顺序的 k-permutation 时,我们才会返回一个组合。

template<class BiDiIt, class Compare>
bool next_combination(BiDiIt first, BiDiIt mid, BiDiIt last, Compare comp)

    bool result;
    do
    
        result = next_k_permutation(first, mid, last, comp);
     while (std::adjacent_find( first, mid,
                             std::tr1::bind(comp, std::tr1::placeholders::_2
                                                , std::tr1::placeholders::_1) )
                                                                        != mid );
    return result;

替代方案是使用反向迭代器而不是参数交换 bind 调用,或者如果 std::less 是正在使用的比较,则显式使用 std::greater

【讨论】:

在你的 next_combination 算法中,你的意思是:result = next_k_permutation(first, mid, last, comp); ? @HowardHinnant:是的,我愿意。谢谢。至少现在它应该给出正确的结果,即使它与您的解决方案相比具有垃圾性能。 就实施工作和小例子而言,这是最好的选择【参考方案4】:

以上@查尔斯·贝利:

我可能是错的,但我认为上面的前两个算法不会删除 first 和 mid 之间的重复项?也许我不知道如何使用它。

4 选择 2 示例:12 3412 43(排序后)13 24(排序后) next_permutation)13 42(排序后)14 23(next_permutation 后)14 32(排序后) 21 34(在 next_permutation 之后)

所以我在返回之前添加了一个检查以查看斜体的值是否正常,但绝对不会想到你写的部分(非常优雅!谢谢!)。

没有完全测试,只是粗略的测试..


template
bool next_combination(RandIt first, RandIt mid, RandIt last)

    typedef typename std::iterator_traits< RandIt >::value_type value_type;
    std::sort(mid, last, std::greater< value_type >() );
    while(std::next_permutation(first, last))
        if(std::adjacent_find(first, mid, std::greater< value_type >() ) == mid)
            return true;
        
        std::sort(mid, last, std::greater< value_type >() );
    return false;

【讨论】:

感谢您发现我使用了错误的组合定义,显然我考虑得不够仔细。但请注意,除非您真的对我的回答发表评论,否则我不会收到通知。【参考方案5】:

也许它已经在以前的答案中说明了,但是在这里我找不到关于参数类型的完整通用方法,而且我也没有在 Boost 之外的现有库例程中找到它。这是我在测试用例构建过程中需要的一种通用方法,用于具有广泛各种参数变化的场景。也许它对你也有帮助,至少对于类似的情况。 (可用于有疑问的微小变化的排列和组合)

#include <vector>
#include <memory>

class SingleParameterToVaryBase

public:

  virtual bool varyNext() = 0;
  virtual void reset() = 0;
;

template <typename _DataType, typename _ParamVariationContType>
class SingleParameterToVary : public SingleParameterToVaryBase

public:

  SingleParameterToVary(
    _DataType& param,
    const _ParamVariationContType& valuesToVary) :
      mParameter(param)
    , mVariations(valuesToVary)
  
    if (mVariations.empty())
      throw std::logic_error("Empty variation container for parameter");
    reset();
  

  // Step to next parameter value, return false if end of value vector is reached
  virtual bool varyNext() override
  
    ++mCurrentIt;

    const bool finished = mCurrentIt == mVariations.cend();

    if (finished)
    
      return false;
    
    else
    
      mParameter = *mCurrentIt;
      return true;
    
  

  virtual void reset() override
  
    mCurrentIt = mVariations.cbegin();
    mParameter = *mCurrentIt;
  

private:

  typedef typename _ParamVariationContType::const_iterator ConstIteratorType;

  // Iterator to the actual values this parameter can yield
  ConstIteratorType mCurrentIt;

  _ParamVariationContType mVariations;

  // Reference to the parameter itself
  _DataType& mParameter;
;

class GenericParameterVariator

public:

  GenericParameterVariator() : mFinished(false)
  
    reset();
  

  template <typename _ParameterType, typename _ParameterVariationsType>
  void registerParameterToVary(
    _ParameterType& param,
    const _ParameterVariationsType& paramVariations)
  
    mParametersToVary.push_back(
      std::make_unique<SingleParameterToVary<_ParameterType, _ParameterVariationsType>>(
        param, paramVariations));
  

  const bool isFinished() const  return mFinished; 

  void reset()
  
    mFinished = false;
    mNumTotalCombinationsVisited = 0;

    for (const auto& upParameter : mParametersToVary)
      upParameter->reset();
  

  // Step into next state if possible
  bool createNextParameterPermutation()
  
    if (mFinished || mParametersToVary.empty())
      return false;

    auto itPToVary = mParametersToVary.begin();
    while (itPToVary != mParametersToVary.end())
    
      const auto& upParameter = *itPToVary;

      // If we are the very first configuration at all, do not vary.
      const bool variedSomething = mNumTotalCombinationsVisited == 0 ? true : upParameter->varyNext();
      ++mNumTotalCombinationsVisited;

      if (!variedSomething)
      
        // If we were not able to vary the last parameter in our list, we are finished.
        if (std::next(itPToVary) == mParametersToVary.end())
        
          mFinished = true;
          return false;
        

        ++itPToVary;
        continue;
      
      else
      
        if (itPToVary != mParametersToVary.begin())
        
          // Reset all parameters before this one
          auto itBackwd = itPToVary;
          do
          
            --itBackwd;
            (*itBackwd)->reset();
           while (itBackwd != mParametersToVary.begin());
        

        return true;
      
    

    return true;
  

private:

  // Linearized parameter set
  std::vector<std::unique_ptr<SingleParameterToVaryBase>> mParametersToVary;

  bool mFinished;
  size_t mNumTotalCombinationsVisited;

;

可能的用法:

GenericParameterVariator paramVariator;

  size_t param1;
  int param2;
  char param3;

  paramVariator.registerParameterToVary(param1, std::vector<size_t> 1, 2 );
  paramVariator.registerParameterToVary(param2, std::vector<int> -1, -2 );
  paramVariator.registerParameterToVary(param3, std::vector<char> 'a', 'b' );

  std::vector<std::tuple<size_t, int, char>> visitedCombinations;
  while (paramVariator.createNextParameterPermutation())
    visitedCombinations.push_back(std::make_tuple(param1, param2, param3));

生成:

(1, -1, 'a')
(2, -1, 'a')
(1, -2, 'a')
(2, -2, 'a')
(1, -1, 'b')
(2, -1, 'b')
(1, -2, 'b')
(2, -2, 'b')

当然,这可以进一步优化/专业化。例如,如果你想避免有效的重复,你可以简单地添加一个散列方案和/或一个避免函子。此外,由于参数作为引用保存,因此可以考虑通过删除复制/赋值构造函数和运算符来保护生成器免受可能的错误使用。

时间复杂度在理论置换复杂度范围内。

【讨论】:

以上是关于C++中置换组合的库函数的主要内容,如果未能解决你的问题,请参考以下文章

C++ 中有没有 LONG转化STRING STRING 转化LONG 的库函数

c++ 标准库函数都有哪些?

C/C++ C++调用用C库函数理解

VC中function函数解析

编写一个自己的库函数strcmp(),用来实现实现两个字符串的比较

cmath库函数