C++ 在不使用循环的情况下,查找给定数字所属的数字集(范围)

C++ 在不使用循环的情况下,查找给定数字所属的数字集(范围),c++,algorithm,hash,C++,Algorithm,Hash,根据以下标准确定查找对象的算法或算法类型时,我感到困惑: 有两个类:“TileSets”和“Tile”。TileSet有两个int属性:firstTileId和lastTileId,而Tile有一个int属性:id,如下所示: struct TileSet { int firstTileId, lastTileId; } struct Tile { int id; } 该应用程序应具有不超过10个瓷砖集(通常为3-5个)和10.000多个瓷砖。速度对于确定具有给定id的磁贴属于哪个磁贴设置

根据以下标准确定查找对象的算法或算法类型时,我感到困惑: 有两个类:“TileSets”和“Tile”。TileSet有两个int属性:firstTileId和lastTileId,而Tile有一个int属性:id,如下所示:

struct TileSet { int firstTileId, lastTileId; } 

struct Tile { int id; }
该应用程序应具有不超过10个瓷砖集(通常为3-5个)和10.000多个瓷砖。速度对于确定具有给定id的磁贴属于哪个磁贴设置至关重要。第一个和最后一个id属性在将tileset添加到向量后不会更改,并且它们不会相互重叠,例如:{{1,25},{26,125},{126,781},{782,789}…}。正如我们所见,瓷砖范围内也没有孔洞。Tiles向量没有排序,也不能排序。我当前的实现(伪短代码)是:

Vector t=10.000+瓷砖
向量ts=大小为2次幂的数字的瓷砖集,至少大于6
对于tileIndex=0;tileIndex如果(ts[TileSiteIndex].firstTileId>=t[tileIndex].id&&t[tileIndex].id的磁贴集不变,那么最好的策略是进行一些预计算,以实现更快的查找。我可以看到一些很好的方法

查找表 如果平铺id是整数,并且不够大,您可以创建一个查找表。对于每个id,您只需记录该id所属的平铺集的数量。类似于

for set in tilesets
    for id=set.first to set.last
        setLookup[id] = set.number
现在,要找到一个由tile id组成的集合,只需查找

setLookup[tile.id]
二进制搜索 如果磁贴ID不是整数或太大以至于无法使用查找表,则第二种方法可以工作。然后,您可以提前对所有磁贴集进行排序,使它们的
第一个
增加(或
最后一个
增加,这相当于两个集不重叠),然后使用二进制搜索查找给定磁贴id的磁贴集。但是,如果您确实有几个磁贴集,这可能不会比顺序查找快,您必须对其进行测试

静态关联 最后,如果您的磁贴ID也没有更改,那么我不明白您为什么不能提前将磁贴与磁贴集完全关联。只需在
磁贴
类中有一个额外的字段,用于存储
磁贴集
编号(或引用或其他任何内容)



请注意,“不要改变”的意思是“不要经常改变”。如果允许更改,但非常罕见,则您可以实施任何假定不更改的解决方案,并在每次更改时进行完整的重新计算。

首先,我会对磁贴集进行排序。例如,首先按
firstTileId
排序,然后按
lastTileId
排序。然后您可以使用二进制搜索(未经测试的代码,请注意):

自动查找文件集索引(常量向量和集合,
大小开始,大小结束,
常数(瓷砖和价值)
->符号整数{
if(start==end)返回-1;
尺寸=开始+(结束-开始)/2;
如果(设置[mid].firstTileId t[tileIndex].id)
中途返回;
if(设置[mid].firstTileId>t[tileIndex].id)
返回findTileSetIndex(集合、开始、中间、值);
返回findTileSetIndex(集合、中间、结束、值);
}
用于(自动和平铺:t){
自动tileSetIndex=findTileSetIndex(ts,0,ts.size(),t);
如果(TILESTENDEX>0){
//t属于ts[tilestendex]
}
}

您应该使用使用优化存储和算法的间隔容器

在这个使用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)) {}
};
演出 使用默认大小运行时(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
以0.064s运行的。这包括进行冗余查找(
ts.find(hit)
)的输出所花费的时间

更新-更高的卷 使用更高的卷和更具体的定时输出进行更多性能测试:


对于这个问题,我将使用优化的二叉树搜索,并考虑区间的大小。 如果磁贴ID具有均匀分布,则有必要尽量减少确定磁贴ID所需的比较次数 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
总间隔长度(间隔总和)为:

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

  • 一个接一个的比较(如在您的问题中实现的) 如果瓷砖属于第一个瓷砖集,则要找到第一个瓷砖集需要一次比较; 如果磁贴属于第二个磁贴集,则需要进行两次比较, 如果磁贴属于第三个磁贴集,则需要进行三次比较,依此类推:

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

            / \
          /     \
        /         \
       / \       /  \
      /   \     /    \
     /\   /\   /\    /\
    2  7 25 5 9 100 5  1
    
    每条路径进行3次比较,因此:

    C2 = 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
    
  • 正如所见,第三种方法比其他方法需要更少的比较

    树的构建方式如下:将平铺集分成两部分,这样左部分和右部分的权重之和之间的差异就最小化了。 例如:

    [2,7,25,5,9,100,5,1] => [2,7,25,5,9],[100,5,1]
    
    在左、右部件上执行拆分,直到生成树为止


    当某些tileSet比其他tileSet宽得多时,这种方法是有益的。

    快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;
        }
      }
    }
    
    10000个磁贴和8个磁贴范围的计时,同时cpu速度固定在其基本频率:

    Trivial: 0.147607 ms
    Optimized: 0.014068 ms
    
    Trivial: 0.043876 ms
    Optimized: 0.004328 ms
    
    允许cpu节流至其最高频率时的计时:

    Trivial: 0.147607 ms
    Optimized: 0.014068 ms
    
    Trivial: 0.043876 ms
    Optimized: 0.004328 ms
    
    H
    C3 = (2*4+7*4+25*3+5*3+9*3+100*2+5*3+1*3)/154 = 2.41
    
    [2,7,25,5,9,100,5,1] => [2,7,25,5,9],[100,5,1]
    
    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++ -std=c++11 -O3 -march=native
    
    Trivial: 0.147607 ms
    Optimized: 0.014068 ms
    
    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;
        }
    }