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

Автор Eman_sh, история, 3 месяца назад, По-английски

Tutorial: Lazy Deletion with Priority Queue (C++ & Python)

Hi everyone! I'm Eman, and this is my very first blog on Codeforces. Today, I'd like to share a simple tutorial on Lazy Deletion using a Priority Queue.

The Problem: Why do we need this?

Normally, you cannot delete an element from the middle of a priority queue. You can only delete the top element. However, in many competitive programming problems, we need to add elements, get the maximum (or minimum) element, and delete specific elements quickly.

The Idea: Lazy Deletion

Instead of looking for the element to delete it right away, we delay the deletion! We do not point to the element to delete it. Instead, we record in another place that it is deleted. When this element reaches the top of the priority queue, only then we delete it.

One way to do this is to create two priority queues:

  1. A main priority queue for the elements, called pq.

  2. A second priority queue to put the elements you want to delete in, called deleted_pq.

How to handle the operations:

  • When you add an element: you push it into pq.

  • When you want to delete an element: you push it into deleted_pq.

  • When you want to get the largest element in the main pq: you use a while loop to compare the top of pq with the top of deleted_pq. If they are equal, this means the element is deleted, so you pop from both of them. We repeat this until the top element in the main queue is not a deleted element.

C++ Code

Here is the C++ implementation. It is very useful if you want to avoid the slow constant factor of std::multiset when you only need to add, delete, and get the maximum element.

#include <iostream>
#include <queue>

using namespace std;

priority_queue<int> pq;
priority_queue<int> deleted_pq;

// Function to clean the top from deleted elements
void clean_top() {
    // While both tops are equal, the current element is deleted
    while (!pq.empty() && !deleted_pq.empty() && pq.top() == deleted_pq.top()) {
        pq.pop();
        deleted_pq.pop();
    }
}

void add_element(int x) {
    pq.push(x);
}

void remove_element(int x) {
    deleted_pq.push(x);
}

int get_max() {
    clean_top(); // Clean the top first
    if (!pq.empty()) {
        return pq.top();
    }
    return -1; // If the queue is empty
}

Why is this important for Python?

This trick is a lifesaver for Python users in Competitive Programming. Python does not have a built-in multiset. If you use list.remove() to delete an element, it takes $$$O(N)$$$ time, which often leads to Time Limit Exceeded (TLE).

By using Lazy Deletion with the heapq library, you can add, remove, and get the maximum/minimum in just $$$O(\log N)$$$ time.

Important Note: The heapq library in Python is a Min-Heap by default. To make it work as a Max-Heap (like the C++ example), we multiply the elements by -1 when adding them, and multiply by -1 again when we get them.

import heapq

pq = []
deleted_pq = []

def clean_top():
    # Compare the smallest element (which is the largest absolute value)
    while pq and deleted_pq and pq[0] == deleted_pq[0]:
        heapq.heappop(pq)
        heapq.heappop(deleted_pq)

def add_element(x):
    # Multiply by -1 to simulate a Max-Heap
    heapq.heappush(pq, -x)

def remove_element(x):
    # Record the element in the deleted list the same way
    heapq.heappush(deleted_pq, -x)

def get_max():
    clean_top() # Make sure the current top is not deleted
    if pq:
        return -pq[0] # Multiply by -1 again to get the original value
    return -1

Time Complexity Summary:

  • Add: $$$O(\log N)$$$
  • Lazy Remove: $$$O(\log N)$$$
  • Get Max: $$$O(\log N)$$$ (Amortized)

I hope this tutorial is clear and helpful.

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

»
3 месяца назад, скрыть # |
 
Проголосовать: нравится 0 Проголосовать: не нравится

Auto comment: topic has been updated by Eman_sh (previous revision, new revision, compare).

»
3 месяца назад, скрыть # |
 
Проголосовать: нравится 0 Проголосовать: не нравится

why not just use a set?

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

    A set can't replace this in general because it only stores unique values. A multiset can, but the author already mentioned that priority_queue with lazy deletion often has better constant factors than multiset.

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

why dont you just mark it as deleted with a bool instead of using another pq

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

    The reason we don't use a simple boolean array (like is_deleted[x] = true;) is because of duplicate elements.

    If all elements are guaranteed to be unique, a boolean array would work perfectly. However, if we push the number 5 into the priority queue twice, and we only want to remove one of them, setting is_deleted[5] = true; would accidentally mark both of them as deleted.

    Using a Map

    To handle duplicates without a second priority queue, we can use a std::map to keep track of the frequencies of the deleted elements.

    Memory Optimization Note: A naive approach would use deleted_counts[pq.top()] > 0 to check for deleted elements. However, this accidentally inserts the element into the map with a value of 0 if it doesn't already exist, which wastes memory over time.

    To optimize memory usage, we should use map::find() to check if the element exists without inserting it. Furthermore, once the frequency of a deleted element reaches 0, we should remove it completely using erase() to keep our map as small as possible.

    #include <iostream>
    #include <queue>
    #include <map>
    
    using namespace std;
    
    priority_queue<int> pq;
    map<int, int> deleted_counts;
    
    // Function to clean the top from deleted elements
    void clean_top() {
        while (!pq.empty()) {
            auto it = deleted_counts.find(pq.top());
            
            // If the top element is in the map and has a deletion count > 0
            if (it != deleted_counts.end() && it->second > 0) {
                it->second--; // Decrease its deletion count
                
                // If the count becomes 0, erase it from the map to save memory
                if (it->second == 0) {
                    deleted_counts.erase(it);
                }
                
                pq.pop(); // Remove it from the priority queue
            } else {
                // The top element is not deleted, so we stop cleaning
                break;
            }
        }
    }
    
    void add_element(int x) {
        pq.push(x);
    }
    
    void remove_element(int x) {
        deleted_counts[x]++; // Increase the deletion frequency of x
    }
    
    int get_max() {
        clean_top(); // Clean the top first before reading
        if (!pq.empty()) {
            return pq.top();
        }
        return -1; // If the queue is empty
    }
    
    

    Both approaches (the second priority queue and the map) are perfectly valid for handling duplicates. Using a second priority queue is sometimes slightly faster in C++ because std::map has a higher constant factor.

»
3 месяца назад, скрыть # |
 
Проголосовать: нравится 0 Проголосовать: не нравится

Why not use a set for marking deletion instead of a second PQ? If duplicates are necessary you could use a hashmap instead with the number of elements to delete