Блог пользователя FAT_

Автор FAT_, история, 2 года назад, По-английски

What do you think of the time complexity of the following code?

multiset<int> S;
for (int i = 0, x; i < n; ++i) {
    cin >> x;
    S.insert(x);
}

for (int i = 0, x; i < n; ++i) {
    cin >> x;
    cout << S.count(x) << " ";
}

For me, I thought it should be $$$O(n \log n)$$$ until yesterday, because I thought STL implemented it by adding a field count to set, but actually this code can be as bad as $$$O(n^2)$$$, because the time complexity of S.count(x) is $$$O(\log n + k)$$$ instead of $$$O(\log n)$$$, where $$$k$$$ is the number of occurrences of x in S.

The solution is to use map in this case:

map<int, int> M;
for (int i = 0, x; i < n; ++i) {
    cin >> x;
    M[x] += 1;
}

for (int i = 0, x; i < n; ++i) {
    cin >> x;
    cout << M.count(x) ? M[x] : 0 << " ";
}

I've stepped into this trap in yesterday's educational round, my solution of D was hacked because of this. I could become master if I didn't made that mistake :(

  • Проголосовать: нравится
  • +121
  • Проголосовать: не нравится

»
2 года назад, скрыть # |
Rev. 2  
Проголосовать: нравится +89 Проголосовать: не нравится

I guess this shouldn't be too surprising, since you're counting occurrences of an item (and a multiset is effectively a balanced binary search tree with duplicates allowed). One way to do this with a treap, is to perform two split operations so that one of your treaps has all elements equal to $$$x$$$, and then outputing the size. I'd imagine a C++ STL BBST generally wouldn't have that kind of functionality.

Edit: I think post brings up a good point since .count() is often used to check the existence of something, so this is a trap people should watch out for (credit to bitset for bringing this up to me).

»
2 года назад, скрыть # |
Rev. 2  
Проголосовать: нравится +19 Проголосовать: не нравится

multiset is overpowered don’t say anything bad about my love >~<

»
2 года назад, скрыть # |
Rev. 2  
Проголосовать: нравится -13 Проголосовать: не нравится

I thought..., but...

I thought, that std::stack.pop() returns last (removed) element.

C++ users, why std::stack.pop_top() doesn't exist? Do you really like writing 2 func_calls instead of 1?

  • »
    »
    2 года назад, скрыть # ^ |
     
    Проголосовать: нравится +5 Проголосовать: не нравится

    Because that is how optimization works. If we just copy from top() as a reference, that turns out as one copy. However if we were to make pop_top(), we would no longer have that element we needed as a reference. So we need to copy once first, and return that, and outside the function there is another copy. Do we like using 2 copies instead of 1? Probably not.

    • »
      »
      »
      2 года назад, скрыть # ^ |
       
      Проголосовать: нравится +27 Проголосовать: не нравится

      Excessive copies can be solved by using std::move. The function can be easily implemented in the library like this:

      auto pop_top() {
        auto __r = std::move(top());
        pop();
        return __r;
      }
      

      RVO will ensure that no copies are ever made.

»
2 года назад, скрыть # |
 
Проголосовать: нравится -20 Проголосовать: не нравится

I always use .count as it's O(N).

»
2 года назад, скрыть # |
 
Проголосовать: нравится +39 Проголосовать: не нравится

Yeah, a common trap.

A way to think about it is: why doesn't multiset just add a count for equal items? Recall that a multiset can store arbitrary objects with arbitrary comparison operation, not just integers. For example, you can have a class for contest participants, and use a multiset to order participants by their current rating. But they are still individual participants, each with their handle and other data. You can't replace the whole class by just the value you use to compare them, and count the occurrences of that value.

If you wish a count, you can make that wish explicit in your program by using map (value -> count).

»
2 года назад, скрыть # |
 
Проголосовать: нравится +68 Проголосовать: не нравится

Notice that proving that it works in $$$O(k + \log n)$$$, and not in $$$O(k \cdot \log n)$$$ is a nice little exercise to check your understanding of balanced binary search trees.

  • »
    »
    2 года назад, скрыть # ^ |
    Rev. 2  
    Проголосовать: нравится -6 Проголосовать: не нравится

    Can't we just say that we are applying binary search to find the pointers, lower_bound pointer and upper_bound pointer (in O(lg(n)), and then we are just finding the distance between the pointers, since the set is not indexed we can only do this in O(k) where k is the actual distance (number of occurrences of the element). So, it will be O(lg n + k).

    Q1 : We can do this in O(lg(n)) using ordered_set (which can tell the index; the number of elements less than this element)?

    Kindly share your proof / s as well.

  • »
    »
    2 года назад, скрыть # ^ |
    Rev. 3  
    Проголосовать: нравится 0 Проголосовать: не нравится

    Would this be a good summary of the proof?

    Spoiler
    • »
      »
      »
      2 года назад, скрыть # ^ |
       
      Проголосовать: нравится 0 Проголосовать: не нравится

      I think you're right, yes. But the fact that the size of this subtree is limited is exactly the place where you need to apply some knowledge of how BBSTs work.

      • »
        »
        »
        »
        2 года назад, скрыть # ^ |
        Rev. 2  
        Проголосовать: нравится 0 Проголосовать: не нравится

        Honestly I would have argued that the size of that subtree is limited to $$$\mathcal{O}(dist+height)$$$ for any general BST. It's just that the painful case exists such that the height is $$$\mathcal{O}(n)$$$, where $$$a$$$ and $$$b$$$ exists on the rightmost node of the left subtree and the leftmost node of the right subtree, so $$$size=\mathcal{O}(dist)$$$ simply does not hold here.

        UPD: Just came up with idea to prove this. A walk from $$$a$$$ to $$$b$$$ is very structured, in fact it is really just Subtree->Path->Subtree. The path is of length $$$\mathcal{O}(height)$$$, and the two subtrees are of size $$$\mathcal{O}(dist)$$$. So this proves it.

»
2 года назад, скрыть # |
 
Проголосовать: нравится +12 Проголосовать: не нравится

Whenever I had to check if an element was present in a set I used .count() instead of find() and one day I used the same on a multiset and got TLE, I also learned it the hard way :)

»
2 года назад, скрыть # |
 
Проголосовать: нравится -9 Проголосовать: не нравится

You may use lower bound (or/and upper bound) for counting in multiset. That will be in log n (hopefully I'm correct)