【问题标题】:Find which set of numbers (range) a given number belongs to, without using loops查找给定数字属于哪一组数字(范围),而不使用循环
【发布时间】:2016-01-17 21:35:59
【问题描述】:

我很困惑根据以下标准决定使用哪种或哪种算法来查找对象: 有 2 个类:“TileSets”和“Tile”。 TileSet 有 2 个 int 属性:firstTileId 和 lastTileId,而 Tile 有一个 int 属性:id,像这样:

struct TileSet { int firstTileId, lastTileId; } 

struct Tile { int id; }

应用程序应该有不超过 10 个 TileSet(通常是 3-5 个)和 10.000+ 个 Tiles。速度对于确定具有给定 id 的 Tile 属于哪个 TileSet 至关重要。将tileset添加到向量后,第一个和最后一个id属性不会改变,并且它们不会相互重叠,例如:{{1, 25}, {26, 125}, {126, 781}, { 782, 789}...}。如我们所见,瓷砖范围内也没有孔。瓷砖矢量不是有序的,也不是。我目前的实现(一种伪短代码)是:

Vector t = 10.000+ tiles
Vector ts = tilesets with a size of a number of a power of 2 number bigger than 6, at least
for tileIndex = 0; tileIndex < t.size; tileIndex++, do:
   for tilesetIndex = 0; tilesetIndex < ts.size; tilesetIndex++, do:
      if (ts[tilesetIndex].firstTileId >= t[tileIndex].id && t[tileIndex].id <= ts[tilesetIndex].lastTileId) 
         // tile t[tileIndex] belongs to the tileset ts[tilesetIndex]! Done!

对于这种情况,我可以使用哪种算法?这有什么公式吗?

【问题讨论】:

  • 图块集可以交错吗?他们经常改变吗?您是否总是需要为每个图块确定一个集合
  • 我不明白你所说的“交错”和变化是什么意思。在 10.000+ 次迭代的每一次迭代中,我总是必须确定一个具有给定值的图块属于哪个图块集。 Tilesets 是按顺序插入的,换句话说,向量将具有范围(第一个和最后一个 id)的条目,如下所示:{{1, 25}, {26, 125}, {126, 781}, {782, 789}。 ..}。瓦片矢量未排序。
  • 可以有两个tileset,一个从0到100,另一个从50到150?您会看到,它们相互重叠,因为从 50 到 100 的图块属于它们两者。对于“更改”,我的意思是,tileset 是否在您的应用程序运行时一直固定,或者tileset 可以在应用程序运行时更改它们的第一个和最后一个 id?他们多久可以改变一次?
  • @Petr 我相信正确的术语是重叠而不是交错。也许你的意思是交错?
  • @NathanOliver,是的,我的意思是重叠。似乎它与我脑海中的 intersect 混合在一起产生了 interlap :) 虽然some 在线词典说这个词也确实作为同义词存在,可以重叠。

标签: c++ algorithm hash


【解决方案1】:

您将使用一个使用优化存储和算法的区间容器。

在这个使用 Boost ICL 的例子中,我做出了一些“任意”的选择来生成很好的分离 TileSets:

using TileSets = icl::split_interval_set<int>;

struct TileSet : TileSets::interval_type::type {
    TileSet(int b, int e) : TileSets::interval_type(closed(b, e)) {}
};

struct Tile : TileSets::interval_type::type {
    Tile(int id) : TileSets::interval_type(closed(id, id)) {}
};

美妙之处在于高级编码:

Live On Coliru

TileSets gen_tiles   (size_t n = 100000);
TileSets gen_tilesets(size_t n = (2ull << 8) + 1);

#include <iostream>

int main() {
    auto const tiles = gen_tiles   (10);
    auto const ts    = gen_tilesets(30);

    std::cout << ts << "\n----\n";

    for (auto hit : tiles & ts) {
        std::cout << hit.lower() << " hits in tileset " << *ts.find(hit) << "\n";
    }
}

打印

{[8,71)[71,133)[133,206)[206,231)[231,465)[465,467)[467,565](565,581)[581,651](651,907)[907,1000)[1000,1395](1395,1429)[1429,1560](1560,1706)[1706,1819)[1819,1835)[1835,1997)[1997,2124](2124,2328)[2328,2913)[2913,2922)[2922,3043)[3043,3338)[3338,3664](3664,3825](3825,3999)[3999,4320](4320,4506](4506,4561](4561,4593](4593,4668)[4668,5143)[5143,5248](5248,5633)[5633,5925](5925,6012](6012,6076)[6076,6117](6117,6119](6119,6175](6175,6184)[6184,6509)[6509,6804](6804,7081](7081,7220)[7220,7852](7852,8325)[8325,8600](8600,8662](8662,9386](9386,9423)[9423,9489](9489,9657](9657,9700](9700,9738](9738,9833](9833,9923]}
----
1561 hits in tileset (1560,1706)
1835 hits in tileset [1835,1997)
3746 hits in tileset (3664,3825]
4459 hits in tileset (4320,4506]
5969 hits in tileset (5925,6012]
5987 hits in tileset (5925,6012]
7320 hits in tileset [7220,7852]
7797 hits in tileset [7220,7852]
7966 hits in tileset (7852,8325)
9508 hits in tileset (9489,9657]

性能

当使用默认尺寸(2^8+1 个图块集中 100000 个图块)运行时,我的盒子需要 0.034 秒

$ time ./test | tee >(echo "total lines: $(wc -l)") | tail
9987 hits in tileset (9984,9990]
9988 hits in tileset (9984,9990]
9989 hits in tileset (9984,9990]
9990 hits in tileset (9984,9990]
9991 hits in tileset (9990,9995]
9992 hits in tileset (9990,9995]
9993 hits in tileset (9990,9995]
9994 hits in tileset (9990,9995]
9995 hits in tileset (9990,9995]
total lines: 9988

real    0m0.034s
user    0m0.029s
sys 0m0.008s

Live On Coliru在 0.064 秒内运行。这包括输出所花费的时间,这会进行冗余查找 (ts.find(hit))!

更新 - 更高的音量

更高容量和更具体的时序输出的更多性能测试:

Live On Coliru

#include <boost/icl/interval_set.hpp>
#include <boost/icl/split_interval_set.hpp>

namespace icl = boost::icl;

using TileSets = icl::split_interval_set<int>;

struct TileSet : TileSets::interval_type::type {
    TileSet(int b, int e) : TileSets::interval_type(closed(b, e)) {}
};

struct Tile : TileSets::interval_type::type {
    Tile(int id) : TileSets::interval_type(id) {}
};

TileSets gen_tiles   (size_t n = (1ull << 22));
TileSets gen_tilesets(size_t n = (1ull << 12));

#include <iostream>
#include <iomanip>
#include <boost/chrono/chrono_io.hpp>

template <typename F>
auto timed(char const* task, F&& f) {
    using namespace boost::chrono;
    struct _ {
        high_resolution_clock::time_point s;
        const char* task;
        ~_() { std::cout << " -- (" << task << " completed in " << duration_cast<milliseconds>(high_resolution_clock::now() - s) << ")\n"; }
    } timing { high_resolution_clock::now(), task };

    return f();
}

int main() {
    auto const tiles = timed("Generate tiles", [] { return gen_tiles(); });
    auto const ts    = timed("Generate tile sets", [] { return gen_tilesets(); });

    //std::cout << ts << "\n----\n";

    std::cout << "Random tiles generated:    " << tiles.iterative_size() << " across a domain of " << std::setprecision(2) << static_cast<double>(tiles.size()) << "\n";
    std::cout << "Tilesets to match against: " << ts.iterative_size()    << " across a domain of " << std::setprecision(2) << static_cast<double>(tiles.size()) << "\n";

    timed("Query intersection", [&] { std::cout << "Total number of hits: "   << (tiles & ts).iterative_size() << "\n"; });
    timed("Query difference",   [&] { std::cout << "Total number of misses: " << (tiles - ts).iterative_size() << "\n"; });

    //for (auto hit : tiles & ts) {
        //std::cout << hit.lower() << " hits in tileset " << *ts.find(hit) << "\n";
    //}
}

#include <random>

static auto gen_tile_id = [prng=std::mt19937{42}, dist=std::uniform_int_distribution<>()] () mutable 
    { return dist(prng); };

TileSets gen_tiles(size_t n) {
    TileSets r;
    std::generate_n(icl::inserter(r, r.end()), n, [] () -> Tile { return gen_tile_id(); });
    return r;
}

TileSets gen_tilesets(size_t n) {
    TileSets r;
    std::generate_n(icl::inserter(r, r.end()), n, [] () -> TileSet {
                auto b = gen_tile_id(), e = gen_tile_id();
                return { std::min(b,e), std::max(b,e) };
            });
    return r;
}

打印(在我的盒子上):

 -- (Generate tiles completed in 3773 milliseconds)
 -- (Generate tile sets completed in 152 milliseconds)
Random tiles generated:    4190133 across a domain of 4.2e+06
Tilesets to match against: 8191 across a domain of 4.2e+06
Total number of hits: 4187624
 -- (Query intersection completed in 1068 milliseconds)
Total number of misses: 2509
 -- (Query difference completed in 533 milliseconds)

【讨论】:

  • 这是一个“巨大”测试用例的时间安排:找出 2²² 瓦片中的哪些瓦片不会出现在给定的 2¹³ 瓦片集中:需要 540 毫秒。 Live On Coliru takes longer: 1581 milliseconds
  • 感谢合作。我很感激帮助。虽然我不能使用 boost 和“嵌套”解决方案,甚至看起来比这个运行得更快(比较我的机器和你的机器的时间 - i7 3.4 和 4mb ram 上的 30 毫秒)
  • @YvesHenri 你愿意分享你用来比较的代码吗?
  • TimeProfiler:pastebin.com/PUPuzXCQ,TileSetManager:pastebin.com/gtnLfy0S,用法:pastebin.com/TJQvSyEp。我还是不满意。我确信可能有一种方法可以避免存储指向 TileSet 的 X 指针,其中 X 是 Tiles 的总数,使用某种散列函数,例如:pastebin.com/thg8sGzt。最后一个链接举例说明了如何将一个数字转换为 2 的下一个幂。如果我可以对给定的 tileId 进行一些向上取整以找到其对应 TileSet 的索引,那将非常棒(我正在尝试做的事情)!
  • 等一下。范围是连续的吗?!?
【解决方案2】:

由于您的图块集不会更改,因此最好的策略是进行一些预先计算,以加快查找速度。我可以看到几种很好的方法。

查找表

如果 tile id 是整数且不够大,您可以创建一个查找表。对于每个 id,您只需记录该 id 所属的tileset 的数量。像这样的

for set in tilesets
    for id=set.first to set.last
        setLookup[id] = set.number

现在要通过 tile id 查找集合,您只需查找

setLookup[tile.id]

二分查找

如果您的 tile id 不是整数或太大以至于查找表变得不切实际,则第二种方法有效。然后你提前对所有的瓦片集进行排序,使它们的firsts 增加(或lasts 增加,这相当于集合不重叠),然后使用二进制搜索找到给定瓦片 id 的瓦片集。但是,如果您确实有几个瓦片集,这可能不会比顺序查找快,您必须对其进行测试。

静态关联

最后,如果您的图块 ID 也没有改变,那么我不明白为什么您不能提前将图块与图块集完全关联。只需在您的 Tile 类中添加一个额外的字段来存储 TileSet 编号(或引用或其他)。


请注意,“不要改变”是指“不要经常改变”。如果允许更改但很少见,那么您可以实施任何假设它不会更改的解决方案,并在每次发生更改时进行完整的重新计算。

【讨论】:

  • 我有点喜欢构建查找表的想法,但它似乎需要与我在第一篇文章中使用基本“嵌套”解决方案一样多的时间。为此,我创建了一个 TileSetManager,它操作 2 个向量:一个由 TileSet 组成,另一个由 TileSets 指针组成。向管理器添加瓦片集时,它存储在其瓦片集向量中,并通过添加指向先前添加的瓦片集的指针来扩展查找向量,因此当从瓦片中查询瓦片集时,只需执行以下操作: return *lookup[tileId ];
  • Idk 如何在 cmets 和我的手机上使用代码标签 'enter key' 不会在这里创建新行,所以.... 查找向量的创建是这样的: for (int I = set.first; I
  • 这种关联会怎样?!重要的是要注意首先填充tilesets向量然后填充tiles,反之亦然。填充瓦片矢量时,我无法分配瓦片的瓦片集。基本上,tiles 向量被填充了 2 次:一次获取我想要的 tile,另一个获取 tilesets 'id'(同样,在获取 id 时,应用程序还不知道有关 tilesets 的任何信息)
  • 对多个帖子感到抱歉。我是新来的,忘记了“at + name”这个东西,所以我不知道你是否收到通知
【解决方案3】:

对于这个问题,我会使用优化的二叉树搜索,同时考虑到区间的大小。 如果 tile ids 具有均匀分布,则可能需要最小化确定 TileSet 用于间隔较大的 TileSet。这个想法提醒了构建二叉树的霍夫曼编码算法 更频繁符号的编码方式树中的路径被最小化

考虑以下示例。

给定 TileSet:

[0,2), [2,9), [9,34), [34,39), [39,48), [48,148), [148,153), [153,154)

那么间隔的大小是:

2,7,25,5,9,100,5,1

总区间长度(区间之和)为:

length = 154 

让我们估计以下方法的比较次数

  1. 一对一比较(就像在您的问题中实现的那样) 如果 Tile 属于第一个 TileSet,则要找到第一个 TileSet 需要进行一次比较; 如果Tile属于第二个TileSet,需要两次比较, 如果 Tile 属于第三个 TileSet,则需要进行三次比较,以此类推:

    C1 = (2*1 + 7*2 + 25*3 + 5*4 + 9*5 + 100*6 + 5*7 + 1*8)/length = 799/154 = 4.84
    
  2. 二叉树。

            / \
          /     \
        /         \
       / \       /  \
      /   \     /    \
     /\   /\   /\    /\
    2  7 25 5 9 100 5  1
    

    每条路径需要 3 次比较,所以:

    C2 = 3
    
  3. 加权树。

             /  \
           /      \
         / \        \
       /\   \       / \
     /\  \   /\    /  /\
    2  7 25 5  9 100 5  1
    

    比较估计:

    C3 = (2*4+7*4+25*3+5*3+9*3+100*2+5*3+1*3)/154 = 2.41
    

正如所见,第三种方法比其他方法需要更少的比较。

树的构建方式如下:将 TileSet 拆分为两部分,以使左侧和右侧部分的权重总和之间的差异最小化。 举个例子:

[2,7,25,5,9,100,5,1] => [2,7,25,5,9],[100,5,1]

在构建树之前对左右部分执行拆分。

当一些 TileSet 比其他的宽得多时,这种方法是有利可图的。

【讨论】:

    【解决方案4】:

    快 10 倍? 以下是如何让您的代码运行速度提高大约 10 倍(或更多)的方法。我们想在 gcc 的帮助下移除分支,并矢量化我们的内部循环。

    我们要删除循环内的条件:

    for (int i=0; i<10000; ++i) {
      for (int j=0; j<8; j++) {
        if ((tiles[i] >= lowerBounds[j]) &&
            (tiles[i] <= upperBounds[j])) {
          ids[i] = j;
        }
      }
    }
    

    这是一个您可以改进的快速解决方案:

    for (int i=0; i<10000; ++i) {
      for (int j=0; j<8; ++j) {
        short int ld = range[j] - tiles[i] + lowerBounds2[j];
        ld = ld<0?0:ld;
        ld = ld>(range[j]-1)?0:ld;
        ld = ld>1?1:ld;
        ids2[i] += j*ld;
      }
    }
    

    如果您要求 g++ 优化代码,第二个解决方案在 i5-4200U 上的速度大约快 10 倍,因为我们没有时间处理 AVX 内部函数等:

    g++ -std=c++11 -O3 -march=native
    

    10,000 个瓦片和 8 个瓦片范围的计时,而 cpu 速度固定在其基本频率:

    Trivial: 0.147607 ms
    Optimized: 0.014068 ms
    

    允许cpu调到最高频率的时间:

    Trivial: 0.043876 ms
    Optimized: 0.004328 ms
    

    这是(快速而肮脏的)代码,我想你明白了并且可以改进它:

    #include <iostream>
    #include <random>
    #include <chrono>
    #include <cstring>
    
    using namespace std;
    using namespace std::chrono;
    
    int main() {
      short int lowerBounds [8] = {0, 2,  9, 34, 39,  48, 148, 153};
      short int upperBounds [8] = {1, 8, 33, 38, 47, 147, 152, 154};
      short int range       [8] = {3, 8, 26,  6, 10, 101,   6,   3};
      short int lowerBounds2[8] = {-1, 1, 8, 33, 38,  47, 147, 152};
      short int tiles [10000];
      short int ids [10000] = {0};
      short int ids2 [10000] = {0};
    
      // 10,000 random tiles
      default_random_engine gen;
      uniform_int_distribution<short int> dist(0, 154);
      for (int i=0; i<10000; ++i) {
        tiles[i] = dist(gen);
      }
    
      // *** trivial solution
      double bestTime = 1.0;
      for (int r=0; r<100; r++) {
        auto t1 = high_resolution_clock::now();
        for (int i=0; i<10000; ++i) {
          for (int j=0; j<8; j++) {
            if ((tiles[i] >= lowerBounds[j]) &&
                (tiles[i] <= upperBounds[j])) {
              ids[i] = j;
            }
          }
        }
        auto t2 = high_resolution_clock::now();
        auto elapsed = duration_cast<duration<double>>(t2 - t1).count();
        if (elapsed < bestTime)
          bestTime = elapsed;
      }
      cout<<"Trivial: "<<bestTime*1000<<" ms"<<endl;
    
      // *** optimized solution
      bestTime = 1.0;
      for (int r=0; r<100; r++) {
        // ids should be zero for this method
        memset(ids2, 0, 10000*sizeof(short int));
        auto t1 = high_resolution_clock::now();
        for (int i=0; i<10000; ++i) {
          for (int j=0; j<8; ++j) {
            short int ld = range[j] - tiles[i] + lowerBounds2[j];
            ld = ld<0?0:ld;
            ld = ld>(range[j]-1)?0:ld;
            ld = ld>1?1:ld;
            ids2[i] += j*ld;
          }
        }
        auto t2 = high_resolution_clock::now();
        auto elapsed = duration_cast<duration<double>>(t2 - t1).count();
        if (elapsed < bestTime)
          bestTime = elapsed;
      }
      cout<<"Optimized: "<<bestTime*1000<<" ms"<<endl;
    
      // validate
      for (int i=0; i<10000; i++)
        if ((ids[i] - ids2[i]) != 0) {
          cout<<"The results didn't match!"<<endl;
          break;
        }
    }
    

    您还可以使用多线程来获得更多的加速。我认为你很容易实现。

    注意:如果您不设置这些优化标志,我建议的方法将比普通方法稍快甚至可能更慢。

    【讨论】:

      【解决方案5】:

      首先,我会对图块集进行排序。例如。首先是firstTileId,然后是lastTileId。然后就可以使用二分查找了(未经测试的代码,请注意):

      auto findTileSetIndex(const Vector& sets,
                            size_t start, size_t end,
                            const Tile& value)
      -> signed int {
          if(start == end) return -1;
          size_t mid = start + (end-start)/2;
          if(sets[mid].firstTileId <= t[tileIndex].id &&
             sets[mid].lastTileId > t[tileIndex].id)
              return mid;
          if(sets[mid].firstTileId > t[tileIndex].id)
              return findTileSetIndex(sets, start, mid, value);
          return findTileSetIndex(sets, mid, end, value);
      }
      
      for(auto& tile : t) {
          auto tileSetIndex = findTileSetIndex(ts, 0, ts.size(), t);
          if(tileSetIndex > 0) {
             // t belongst to ts[tileSetIndex]
          }
      }
      

      【讨论】:

      • 谢谢。但是,我试图在没有循环的情况下实现这一点。你的解决方案给了我一个 O(log n),而我的给了一个更糟糕的 O(n),我知道,但仍然不是这样。我发布了一个迄今为止最好的解决方案。它使用 Petr 建议的查找“表”。不知何故,tileset 搜索比创建查找向量需要更长的时间......我不知道为什么
      猜你喜欢
      • 2014-09-20
      • 1970-01-01
      • 1970-01-01
      • 2022-01-21
      • 1970-01-01
      • 2020-10-03
      • 2013-09-09
      • 1970-01-01
      • 2014-04-27
      相关资源
      最近更新 更多