【问题标题】:Bit Masking to select Vector/Set Elements?位掩码选择向量/集元素?
【发布时间】:2016-12-29 05:17:52
【问题描述】:

目标

我正在尝试使用容器(例如向量、集合等)提取元素,但我没有使用索引,而是使用位掩码技术。

场景:

vector<string> alphabets {"a", "b", "c", "d", "e"};

测试用例:

  1. 输入:5(等效位掩码:00101

    输出:新向量{"c", "e"}

  2. 输入13(位掩码:01101

    输出向量:{"b", "c", "e"}

朴素的工作解决方案:

vector<string*> extract(int mask){
    vector<string*> result;
    bitset<n> bits(mask);
    for (int j = 0; j < n; ++j) {
        if (bits[j]){
            result.push_back(&alphabets[j]);
        }
    }
}

进一步改进

  • 时间复杂度
  • 空间复杂度
  • 概念/想法??
  • API 可用性??

示例用例。

置换 a,b,c,d,e 的所有组合,从而将 a,b,c,d,e 包裹在一个容器上。 (Generating combinations in c++ 问题中提到了其他方法。)

#include <vector>
#include <iostream>
#include <algorithm>
#include <string>
#include <bitset>

using namespace std;


int main(){
    const int n = 5;

    vector<string> alphabets {"a", "b", "c", "d", "e"};

    for ( int i = 0; i < pow(2, n); ++i){
        vector<string*> result;
        bitset<n> bits(i);
        for (int j = 0; j < n; ++j) {
            if (bits[j]){
                result.push_back(&alphabets[j]);
            }
        }

        for (auto r: result){
            cout << *r;
        }
        cout << endl;
    }
    return 0;
}

【问题讨论】:

  • 问题是什么?
  • 我首先对性能有点担心,因为我正在尝试解决一个需要我蛮力所有组合的 NP 完全问题的小问题集。因此,解决这个问题只是解决了我更大问题的一小部分。 (即有一个更好的置换所有组合的解决方案)
  • 您可以使用库函数来获取排列,这通常比您自己的实现更快:std::next_permutation
  • 我不确定您是否真的需要变量 result 来解决您的小中间问题。 bitset&lt;n&gt;alphabet 类型的变量bits 具有result 的信息内容。例如,bitset 的联合比 vector&lt;string*&gt; 的联合效率高得多(它可以使用无符号位或“|”运算符)。

标签: c++ vector set bitmask


【解决方案1】:

如果您更喜欢性能而不是可读性,我认为这是一个合理的起点。

尝试 1

基本上我会避免任何内存分配。

#include <string>
#include <vector>
#include <bitset>
#include <iostream>
#include <iterator>
#include <tuple>
#include <array>


template<class From, std::size_t N>
auto
select(From const& from, std::bitset<N> const& bits)
{
    std::array<const std::string*, N> result { nullptr };
    auto i = std::begin(result);
    std::size_t found;
    std::size_t count = found = bits.count();
    std::size_t index = 0;
    while (count)
    {
        if (bits.test(index)) {
            *i++ = &from[index];
            --count;
        }
        ++index;
    }
    return std::make_tuple(found, result);
}

int main()
{

    std::vector<std::string> alphabet = { "a", "b", "c", "d", "e", "f", "g", "h" };

    for (unsigned x = 0 ; x < 256 ; ++x)
    {
        auto info = select(alphabet, std::bitset<8>(x));
        auto ptrs = std::get<1>(info).data();
        auto size = std::get<0>(info);
        while(size--)
        {
            std::cout << *(*ptrs++) << ", ";
        }

        std::cout << '\n';
    }
}

尝试 2 - 运行时查找以纳秒为单位...

在这里,我在编译时预先计算所有可能的字母表。

运行时间当然快得令人眼花缭乱。但是,一个多于 14 个字符的字母可能需要一段时间才能编译...

更新:警告!当我将字母表大小设置为 16 以消耗 32GB 内存时,停止桌面上的所有其他应用程序并需要重新启动我的 macbook,然后才能执行其他任何操作。您已收到警告。

#include <string>
#include <vector>
#include <bitset>
#include <iostream>
#include <iterator>
#include <tuple>
#include <array>


template<class From, std::size_t N>
auto
select(From const& from, std::bitset<N> const& bits)
{
    std::array<const std::string*, N> result { nullptr };
    auto i = std::begin(result);
    std::size_t found;
    std::size_t count = found = bits.count();
    std::size_t index = 0;
    while (count)
    {
        if (bits.test(index)) {
            *i++ = &from[index];
            --count;
        }
        ++index;
    }
    return std::make_tuple(found, result);
}

template<std::size_t Limit>
struct alphabet
{
    constexpr alphabet(std::size_t mask)
    : size(0)
    , data { }
    {
        for (std::size_t i = 0 ; i < Limit ; ++i)
        {
            if (mask & (1 << i))
                data[size++] = char('a' + i);
        }
    }

    std::size_t size;
    char data[Limit];

    friend decltype(auto) operator<<(std::ostream& os, alphabet const& a)
    {
        auto sep = "";
        for (std::size_t i = 0 ; i < a.size; ++i)
        {
            std::cout << sep << a.data[i];
            sep = ", ";
        }
        return os;
    }
};

template<std::size_t Limit>
constexpr alphabet<Limit> make_iteration(std::size_t mask)
{
    alphabet<Limit> result { mask };
    return result;
}

template<std::size_t Limit, std::size_t...Is>
constexpr auto make_iterations(std::index_sequence<Is...>)
{
    constexpr auto result_space_size = sizeof...(Is);
    std::array<alphabet<Limit>, result_space_size> result
    {
        make_iteration<Limit>(Is)...
    };
    return result;
}

template<std::size_t Limit>
constexpr auto make_iterations()
{
    return make_iterations<Limit>(std::make_index_sequence<std::size_t(1 << Limit) - 1>());
}

int main()
{
    static constexpr auto alphabets = make_iterations<8>();
    for(const auto& alphabet : alphabets)
    {
        std::cout << alphabet << std::endl;
    }
}

尝试 3

使用指向匹配元素的指针的非常基本的固定容量容器。我添加了 constexpr。这不会改善大多数情况,但肯定会改善静态分配的选择。

#include <vector>
#include <bitset>
#include <iostream>
#include <iterator>
#include <tuple>
#include <array>

namespace quick_and_dirty {

template<class T, std::size_t Capacity>
struct fixed_capacity_vector
{
    using value_type = T;

    constexpr fixed_capacity_vector()
    : store_()
    , size_(0)
    {}

    constexpr auto push_back(value_type v)
    {
        store_[size_] = std::move(v);
        ++ size_;
    }

    constexpr auto begin() const { return store_.begin(); }
    constexpr auto end() const { return begin() + size_; }

private:
    std::array<T, Capacity> store_;
    std::size_t size_;
};

} // namespace quick_and_dirty

template<class From, std::size_t N>
constexpr
auto
select(From const& from, std::bitset<N> const& bits)
{
    using value_type = typename From::value_type;
    using ptr_type = std::add_pointer_t<std::add_const_t<value_type>>;

    auto result = quick_and_dirty::fixed_capacity_vector<ptr_type, N>();

    std::size_t count = bits.count();

    for (std::size_t index = 0 ; count ; ++index)
    {
        if (bits.test(index)) {
            result.push_back(&from[index]);
            --count;
        }
    }
    return result;
}

int main()
{

    std::vector<std::string> alphabet = { "a", "b", "c", "d", "e", "f", "g", "h" };

    for (unsigned x = 0 ; x < 256 ; ++x)
    {
        for(auto p : select(alphabet, std::bitset<8>(x)))
        {
            std::cout << (*p) << ", ";
        }

        std::cout << '\n';
    }
}

【讨论】:

    【解决方案2】:

    好吧,那么您的示例可以更短(更短一点,它可能无法证明您真正需要什么)(编辑:我计划将字符直接输出到cout,而不是使用@987654322 @vector,然后我就忘记了,当时我正在重写代码......所以它仍然和你的相似):

    #include <vector>
    #include <iostream>
    #include <string>
    
    int main() {
        const std::vector<std::string> alphabets {"a", "b", "c", "d", "e"};
        const unsigned N = alphabets.size();
        const unsigned FULL_N_MASK = 1 << N;
    
        for (unsigned mask = 0; mask < FULL_N_MASK; ++mask) {
    
            std::vector<const std::string*> result;
            unsigned index = 0, test_mask = 1;
            while (index < N) {
                if (mask & test_mask) result.push_back(&alphabets[index]);
                ++index, test_mask <<= 1;
            }
    
            for (auto r : result) std::cout << *r;
            std::cout << std::endl;
        }
        return 0;
    }
    

    这让我有点奇怪,为什么你需要从掩码中提取 result 向量,也许你可以只使用 mask 本身,并在内部循环中获取特定的字符串(就像我在我正在构建result)。

    我的代码中的主要变化是省略了bitset&lt;n&gt; bits(i); 初始化,因为您已经在i 中拥有这些位,可以使用 C 语言按位运算符 (&lt;&lt; &gt;&gt; &amp; ^ | ~) 轻松访问,无需将它们转换为再次与bitset 相同。

    bitset 的使用是有意义的,如果n 太大而无法将掩码放入某些常规类型(如uint32_t)。即使这样,对于相当小的固定n,我也可能会使用一些 64/128/256b 无符号整数(您的目标平台上可用的整数)。


    关于速度:你当然无法击败++maskstd::next_permutation 将比单个本地机器代码指令慢,即使它以相同的方式实现 32/64 以下的大小。

    但问题是,如果您可以围绕该位掩码构建算法,以有效利用该优势。

    许多国际象棋引擎使用各种棋盘值的位掩码编码,以轻松检查某些字段是否被占用,或者一步进行一些初步的回合可用性检查,例如获取所有可以在下一回合中占据对手数字的棋子:@ 987654337@ - 5 个原生 CPU 位运算符,如果结果为 0,你知道你的 pawn 不能攻击任何东西。或者你有可以拿走的对手棋子的位掩码。与幼稚地循环遍历所有棋子,检查对手棋子的 [+-1, +1] 字段并将它们添加到一些动态分配的内存中,例如vector

    或者是否存在能够修剪组合树并提前退出的算法,完全跳过一些重要的排列子集,这很可能比完整的++mask扫描更快。

    【讨论】:

    • 好吧,终于明白你的技术了,我们的算法还是差不多的,只是你直接执行位掩码。并且 test_mask 是我内部 for 循环的替代品。但是,是的,我同意这更好。在时间复杂度方面,我们的算法仍然执行相同的时间复杂度。但你的速度更快。
    • @Yeo:是的,我无法改进算法,这是未知的。如果您想要完整的排列,这是非常理想的(有问题的位是向量中的结果,而不是排列的计算)。这可能比预先计算所有排列并存储在内存中更快,因为位掩码和字母仅使用非常有限的内存,不会过多地破坏缓存。但是超过 90% 的 CPU 时间将花费在您的真实算法上,该算法正在处理这些排列。所以你有点过早地优化了东西,可能是在错误的地方。
    猜你喜欢
    • 2022-01-08
    • 2015-12-13
    • 1970-01-01
    • 1970-01-01
    • 2021-12-11
    • 2014-04-21
    • 2019-05-13
    • 2013-11-27
    • 2021-10-03
    相关资源
    最近更新 更多