为什么std::set比std::map慢?

How is std::set slower than std::map?

本文关键字:std map set 为什么      更新时间:2023-10-16

我试图从acm.timus.ru解决这个问题,它基本上希望我输出给定字符串(最大长度5000)的不同子字符串的数量。

我即将提出的解决方案是非常低效的,并且在限制条件下注定要超过时间限制。然而,这两个解决方案的唯一不同之处在于(至少在我看来/理解的范围内),一个使用std::map<long long, bool>,而另一个使用std::set <long long>(参见最后一个for循环的开头)。其余部分是相同的(您可以使用任何不同的工具进行检查)。地图解决方案的结果是"测试3超过时间限制",而集合解决方案的结果是"测试2超过时间限制",这意味着在测试2上,地图解决方案比集合解决方案运行得更快。如果我选择Microsoft Visual Studio 2010编译器,情况就是如此。如果我选择GCC,那么两种解决方案都会在测试3上导致TLE。

我不是在问如何有效地解决这个问题。我要问的是是如何解释使用std::map明显比使用std::set更有效。我只是没有看到这种现象的机制,希望有人能提出一些见解。

Code1 (uses map, TLE 3):

#include <iostream>
#include <map>
#include <string>
#include <vector>
using namespace std;
int main ()
{
   string s;
   cin >> s;
   vector <long long> p;
   p.push_back(1);
   for (int i = 1; i < s.size(); i++)
      p.push_back(31 * p[i - 1]);
   vector <long long> hash_temp;
   hash_temp.push_back((s[0] - 'a' + 1) * p[0]);
   for (int i = 1; i < s.size(); i++)
      hash_temp.push_back((s[i] - 'a' + 1) * p[i] + hash_temp[i - 1]);   
   int n = s.size();   
   int answer = 0;
   for (int i = 1; i <= n; i++)
   {
      map <long long, bool> hash_ans;
      for (int j = 0; j < n - i + 1; j++)
      {
         if (j == 0)
            hash_ans[hash_temp[j + i - 1] * p[n - j - 1]] = true;
         else
            hash_ans[(hash_temp[j + i - 1] - hash_temp[j - 1]) * p[n - j - 1]] = true;
      }
      answer += hash_ans.size();
   }
   cout << answer;
}

Code2 (using set, TLE 2):

#include <iostream>
#include <string>
#include <vector>
#include <set>
using namespace std;
int main ()
{
   string s;
   cin >> s;
   vector <long long> p;
   p.push_back(1);
   for (int i = 1; i < s.size(); i++)
      p.push_back(31 * p[i - 1]);
   vector <long long> hash_temp;
   hash_temp.push_back((s[0] - 'a' + 1) * p[0]);
   for (int i = 1; i < s.size(); i++)
      hash_temp.push_back((s[i] - 'a' + 1) * p[i] + hash_temp[i - 1]);   
   int n = s.size();   
   int answer = 0;
   for (int i = 1; i <= n; i++)
   {
      set <long long> hash_ans;
      for (int j = 0; j < n - i + 1; j++)
      {
         if (j == 0)
            hash_ans.insert(hash_temp[j + i - 1] * p[n - j - 1]);
         else
            hash_ans.insert((hash_temp[j + i - 1] - hash_temp[j - 1]) * p[n - j - 1]);
      }
      answer += hash_ans.size();
   }
   cout << answer;
}

我看到的实际差异(如果我错过了什么请告诉我)是,在地图的情况下,你做

hash_ans[key] = true;

而在set的情况下,你做

hash_ans.insert(key);

在这两种情况下,都插入一个元素,除非该元素已经存在,且在其中不做任何操作。在这两种情况下,查找都需要找到相应的元素,并在失败时插入它。实际上,在每种实现中,容器都将使用树,这使得查找同样昂贵。更重要的是,c++标准实际上要求set::insert()map::operator[]()的复杂度为O(log n),因此两种实现的复杂度应该相同。

那么,一个人表现更好的原因是什么呢?一个区别是,在一种情况下,底层树的节点包含string,而在另一种情况下,它包含pair<string const, bool>。由于这对包含一个字符串,它一定更大,对机器的RAM接口施加更大的压力,所以这并不能解释加速。它所能做的就是扩大节点大小,这样其他节点就会被挤出缓存线,这对多核系统的性能不利。

总而言之,我想尝试以下几点:

  1. 在集合
    中使用相同的数据我会这样做与struct data: string {bool b};即捆绑字符串在一个结构体,应该有一个类似的二进制布局作为地图的元素。作为比较器,使用less<string>,这样只有字符串实际参与比较。

  2. 在地图上使用insert()
    我不认为这应该是一个问题,但是插入可能会导致参数的副本,即使最后没有插入发生。我希望它不会,所以我不太相信这会改变任何事情。

  3. 关闭调试
    大多数实现都有诊断模式,在该模式下对迭代器进行验证。您可以使用它来捕获c++只说"未定义行为"的错误,耸耸肩并崩溃。

  4. 读取代码
    如果set和map的实现具有不同的质量和优化水平,这可以解释差异。在引擎盖下,我希望地图和设置都建立在同一类型的树上,所以这里也不太希望。

在这种情况下,set只比map快一点点。我还是不认为你们应该把ttle 2或ttle 3说得那么严重。如果你很接近时间限制,就可能发生同样的解决方案的时间限制在一个给定的提交上,测试2的时间限制,下一次测试3的时间限制。我有一些解决方案在时间限制内通过了测试,我敢打赌,如果我重新提交它们,它们会失败的。

这个问题我用Ukonen后缀树解决了

这取决于使用的实现算法。通常集合是使用映射实现的,只使用key字段。在这种情况下,与使用map相比,使用set会有很小的开销。