Дерево отрезков с групповыми операциями

В разделе дерева отрезков мы научились решать две похожие задачи:

  • сумма на отрезке и обновление одного элемента
  • добавление на отрезке и вычисление значения на позиции

Но что делать, если мы хотим добавить на отрезке и сумму на отрезке? Тогда нам поможет дерево отрезков с групповыми операциями. Идея заключается в том, что мы будем хранить не только сумму на отрезке, но и “ленивую” информацию о том, сколько мы хотим добавить к каждому элементу на отрезке.

1
2
3
4
5
6
struct Node {
    int sum; // сумма на отрезке
    int add; // сколько мы хотим добавить к каждому элементу на отрезке
};

Node t[2 * N]; // массив, в котором мы будем хранить дерево отрезков

Как поменяется реализация?

Оказыватся, почти никак! Всё что нам нужно - научиться вовремя проталкивать ленивую информацию вниз по дереву - каждый раз перед тем как посетить вершину. Иначе говоря, если мы находимся в вершине $v$ и у нас есть информация о том, сколько мы хотим добавить к каждому элементу на отрезке, то мы должны обновить сумму на отрезке и добавить информацию к левому и правому поддереву. Для этого нам нужно создать функцию push, которая и будет этим заниматься:

1
2
3
4
5
6
7
8
9
10
void push(int v, int l, int r) { // функция, которая будет "проталкивать" информацию вниз
    if (t[v].add != 0) { // если у нас есть информация о том, сколько мы хотим добавить к каждому элементу на отрезке
        if (r - l > 1) { // если мы не находимся в листе
            t[left(v)].add += t[v].add; // добавляем информацию к левому поддереву
            t[right(v)].add += t[v].add; // добавляем информацию к правому поддереву
        }
        t[v].sum += (r - l) * t[v].add; // обновляем сумму на отрезке
        t[v].add = 0; // обнуляем информацию о том, сколько мы хотим добавить к каждому элементу на отрезке
    }
}

Все запросы на сумму и добавление на отрезке будут выглядеть почти так же, как и раньше. Но теперь нам нужно вызывать функцию push, чтобы актуализировать информацию о вершине и о её детях.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int sum(int v, int l, int r, int ql, int qr) {
    push(v, l, r); // +1 строка
    if (ql >= r || qr <= l) {
        return 0;
    } else if (ql <= l && qr >= r) {
        return t[v].sum;
    } else {
        int m = (l + r) / 2;
        return sum(left(v), l, m, ql, qr) + sum(right(v), m, r, ql, qr);
    }
}

void add(int v, int l, int r, int ql, int qr) {
    push(v, l, r); // +1 строка
    if (ql >= r || qr <= l) {
        return;
    } else if (ql <= l && qr >= r) {
        t[v].add += x;
        push(v, l, r); // +2 строка
    } else {
        int m = (l + r) / 2;
        add(left(v), l, m, ql, qr);
        add(right(v), m, r, ql, qr);
        t[v].sum = t[left(v)].sum + t[right(v)].sum;
    }
}

Таким образом, реализация отличается от обычного дерева отрезков лишь тремя дополнительными вызовами push (не забывайте вызывать в add в ДВУХ местах)!

Задачи

  1. На плоскости дано $n$ прямоугольников, заданных координатами левого нижнего и правого верхнего углов. Нужно научиться вычислять площадь объединения прямоугольников за $O(n \log n)$.
  2. На плоскости даны $n$ прямоугольников. Поступают запросы вида “принадлежит ли точка хотя бы одному прямоугольнику”. Нужно научиться отвечать на запросы за $O(\log n)$.