代码之家  ›  专栏  ›  技术社区  ›  Armen Tsirunyan

std::set怎么比std::map慢?

  •  5
  • Armen Tsirunyan  · 技术社区  · 13 年前

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

    我即将提出的解决方案效率极低,而且在受到限制的情况下,注定会出现“超过时限”的判决。然而,这两种解决方案的唯一不同之处(至少在我看来/理解的范围内)是 std::map<long long, bool> ,而其他用途 std::set <long long> (请参阅最后一个for循环的开头。其余部分相同,您可以通过任何diff工具进行检查)。映射解决方案会导致“测试3超过时间限制”,而设置解决方案会造成“测试2超出时间限制”。这意味着测试2使得映射解决方案在其上的工作速度比设置解决方案更快。如果我选择Microsoft Visual Studio 2010编译器,就会出现这种情况。如果我选择GCC,那么两种解决方案都会在测试3中产生TLE。

    我不是在问如何有效地解决这个问题。我在问什么 是如何解释使用 std::map 显然比使用更有效 std::set 。我只是看不到这种现象的机制,希望有人能有任何见解。

    代码1(使用地图,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;
    }
    

    代码2(使用集合,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;
    }
    
    3 回复  |  直到 13 年前
        1
  •  2
  •   Ulrich Eckhardt    13 年前

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

    hash_ans[key] = true;
    

    而在设定的情况下

    hash_ans.insert(key);
    

    在这两种情况下,都会插入一个元素,除非它已经存在,否则它什么也不做。在这两种情况下,查找都需要找到相应的元素,并在失败时插入它。实际上,在每一个实现中,容器都将使用一个树,这使得查找同样昂贵。更重要的是,C++标准实际上需要 set::insert() 和 map::operator[]() 复杂性为O(logn),因此两种实现的复杂性应该相同。

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

    总之,我会尝试一些事情:

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

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

    3. 关闭调试
      大多数实现都有一个诊断模式,在该模式中迭代器得到验证。你可以用它来捕捉C++只说“未定义的行为”、耸耸肩并崩溃的错误。这种模式通常不满足复杂性保证,并且总是有一些开销。

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

        2
  •  1
  •   Ivaylo Strandjev    13 年前

    在这种情况下,我想一套只比地图快一点点。尽管如此,我认为你不应该那么重视TLE 2或TLE 3,因为这并不是什么大事。如果您对给定提交的测试2的相同解决方案时间限制和下一次测试3的时间限制感到不满,则可能会发生这种情况。我有一些解决方案在时间限制内通过了测试,我敢打赌,如果我重新提交它们,它们就会失败。

    这个特殊的问题是我用Ukonen-Sufix树解决的。

        3
  •  1
  •   OlivierD    13 年前

    这取决于所使用的实现算法。通常,集合是使用仅使用关键字字段的映射来实现的。在这种情况下,使用集合而不是使用映射会有很小的开销。