查找给定数字所属的数字(范围)集,而不使用循环

时间:2015-10-19 13:45:37

标签: c++ algorithm hash

根据以下标准,我很难决定使用哪种算法或哪种算法来查找对象: 有两个类:' 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的TileSet哪个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!

我可以在这种情况下使用哪种算法?这有什么公式吗?

5 个答案:

答案 0 :(得分:7)

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

在这个使用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)

答案 1 :(得分:2)

由于您的磁贴集不会更改,因此最佳策略是进行一些预先计算,以便更快地进行查找。我可以看到几种很好的方法。

查找表

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

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

现在按照图块ID查找设置,您只需查找

setLookup[tile.id]

二进制搜索

如果您的tile id不是整数,或者可能太大而查找表变得不切实际,则第二种方法有效。然后事先对所有tileset进行排序,使其first增加(或last s增加,这相当于集合不重叠),然后使用二分搜索找到给定的tileset瓷砖ID。但是,如果你真的有一些tileset,这可能不比顺序查找快,你必须测试它。

静态关联

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

请注意,通过说“不要改变”,我的意思是“不要经常改变”。如果允许更改,但很少,您可以实现任何假定它不会更改的解决方案,并在每次更改时执行完整的重新计算。

答案 2 :(得分:2)

对于这个问题,我会使用优化的二叉树搜索,考虑间隔的大小。    如果tile id具有均匀分布,则可能有意义地最小化确定所需的比较计数    TileSet的TileSet具有更大的间隔。这个想法提醒了哈夫曼编码算法,其中构建了二叉树    更频繁符号的编码方式,树中的路径被最小化

考虑以下示例。

鉴于TileSets:

[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
    
  4. 正如所见,第三种方法需要的比较少于其他方法。

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

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

    在左右部分执行拆分,直到构建树。

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

答案 3 :(得分:1)

快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;
    }
}

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

NB:如果您没有设置这些优化标记,我建议的方法会比简单的方法稍快或甚至更慢。

答案 4 :(得分:0)

首先,我对瓷砖集进行排序。例如。首先是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]
    }
}