← Параллель CВсе параллели⬇ Скачать конспект

Занятие 3. Параллель C — Два указателя

Метод двух указателей на подотрезках массива, префиксные суммы, максимальная сумма подотрезка и восстановление ответа. В конце — разбор контеста по сортировкам.
Лекция + разбор контеста≈ 1 ч 55 минЯзык кода: C++

1. Подотрезки и идея двух указателей 0:09

1.1. Подотрезок и подпоследовательность 0:09

Подотрезок массива — несколько элементов, идущих подряд. Для массива $3,1,4,1,5,9$ набор $1,4,1$ — подотрезок, а $3,4,5$ — уже подпоследовательность (элементы не подряд). Сегодня работаем только с подотрезками; подотрезок с границами $l$ и $r$ обозначаем $[l, r]$.

1.2. Какие задачи решает метод 0:52

Широкий класс задач олимпиадного программирования — что-то посчитать для подотрезков: подотрезок с максимальной суммой, подотрезок максимальной длины, в котором ровно $k$ различных элементов, и т. п. «В лоб» это двойной цикл:

C++
for (int l = 0; l < n; ++l)
    for (int r = l; r < n; ++r) {
        // что-то считаем для отрезка [l, r]
        // например: число различных элементов, ans = max(ans, r - l + 1)
    }

2:13 Оба цикла работают за $O(n)$, итого $O(n^2)$ (и ещё стоимость подсчёта на самом отрезке). Хочется за линию.

Интуиция
В переборе выше $r$ каждый раз «прыгает» заново. Идея метода: двигать обе границы только вправо. Сначала сдвинули правую границу, потом снова правую, потом левую — и получили новый подотрезок, не пересчитывая всё с нуля.

2:54 Пример: массив $2,1,3,2,4,1$, отрезок $1,3,2$. Сдвигая границы вправо постепенно, мы можем получить отрезок $3,2,4$ — такой «скользящий» переход и есть основа метода.

2. Задача: максимальная длина подотрезка с не более чем $K$ различными элементами 3:39

2.1. Условие и идея 3:39

4:25 Нужно найти подотрезок максимальной длины, в котором не больше $K$ различных значений. Заводим левую границу $l$ и для окна $[l,r]$ — словарь cnt: cnt[x] — сколько раз $x$ встречается в текущем окне.

5:29 Разберём массив $1,2,1,3,4,2,3$ при $K=2$. Правая граница стоит на нулевом элементе — окно $\{1\}$, различных элементов один. Сдвигаем $r$: окно $1,2$ — два различных; ещё сдвиг: $1,2,1$ — по-прежнему два. Следующий сдвиг даёт $1,2,1,3$ — уже три различных, больше разрешённого $K$. Значит, окно надо сжимать слева, пока различных элементов снова не станет не больше $K$: $2,1,3$ — всё ещё три; $1,3$ — два, окно допустимо, можно снова двигать $r$.

Окно двух указателей при K = 2после r = 210211233442536lr{1,2}: 2 разл. ✓r = 3, добавили 310211233442536lr{1,2,3}: 3 разл. ✗сдвигаем l10211233442536lr{2,1,3}: 3 разл. ✗сдвигаем l ещё10211233442536lr{1,3}: 2 разл. ✓
Пока различных больше $K$, двигаем $l$; когда окно снова допустимо — снова двигаем $r$. Красный — недопустимое окно, синий — допустимое.

2.2. Код 8:18

C++
map<int, int> cnt;      // cnt[x] — сколько раз x в окне [l, r]
int l = 0, ans = 0;
for (int r = 0; r < n; ++r) {
    cnt[a[r]]++;                       // добавили a[r] в окно
    while ((int)cnt.size() > k) {      // окно «плохое» — сжимаем слева
        cnt[a[l]]--;                   // убрали a[l] из окна
        if (cnt[a[l]] == 0)
            cnt.erase(a[l]);           // чтобы cnt.size() считало только присутствующие
        l++;
    }
    ans = max(ans, r - l + 1);         // окно [l, r] корректно
}

2.3. Каркас решения любой задачи на два указателя 11:42

11:42 Решения таких задач выглядят одинаково: сначала правая граница добавляет элемент, потом левая сдвигается, пока окно «плохое», потом пересчитывается ответ. От задачи к задаче меняются только отдельные блоки:

шаблон
текст
l = 0
для r = 0..n-1:
    добавить a[r] в окно
    пока окно плохое:
        удалить a[l] из окна
        l++
    обновить ответ по окну [l, r]

2.4. Почему за $O(n)$ — два цикла, но время линейное 13:49

14:30 Хотя внутри внешнего цикла есть ещё цикл, за всю программу $r$ пройдёт от $0$ до $n$ ровно $n$ шагов (она только растёт). Указатель $l$ тоже только увеличивается — нигде нет l-- — и поэтому за всю программу сделает не более $n$ шагов. Внутренний while делает шаги не «на каждой итерации по $n$», а суммарно не более $n$ за всё время. Итого $n + n = O(n)$ шагов.

Замечание про map
В коде использован map, его операция стоит $O(\log n)$, поэтому полное время — $O(n\log n)$. Если заменить map на unordered_map (в среднем $O(1)$ на операцию), получим линейное время.
Именно while, а не if
Если написать if, то на каждый шаг $r$ левая граница сдвигалась бы не более чем на единицу. Но уже на разобранном примере, чтобы получить допустимое окно, нужно сдвинуть $l$ сразу на две позиции при одном $r$. Поэтому сдвиг — обязательно в цикле while, и условие в нём — то, что делает окно плохим.

17:14 Про корректность ответа: внутри while всегда пишем условие «плохо» (здесь — различных больше $K$), т. е. противоположное тому, что требует задача. На момент обновления ответа окно $[l,r]$ уже корректно, и посчитанная величина (сумма, cnt, …) относится именно к нему — повторно ничего считать не нужно.

2.5. Как работает map в C++ 20:44

20:44 map — «коробка» с парами «ключ → значение», словарь. Подключается через #include <map>:

C++
map<string, int> cnt;     // ключ — string, значение — int
cnt["aba"] = 2;           // положили пару ("aba", 2)
cnt["aba"]++;             // изменили значение: стало 3

3. Подотрезок максимальной длины без повторений 24:53

24:53 Нужно найти подотрезок максимальной длины, где все элементы различны. Для массива $1,2,3,1,2,4$ ответ — $3,1,2,4$ (длина 4). Решаем тем же методом: словарь cnt, левая граница $l=0$, ответ $0$ (изначально).

C++
map<int, int> cnt;
int l = 0, ans = 0;
for (int r = 0; r < n; ++r) {
    cnt[a[r]]++;                 // добавили a[r]
    while (cnt[a[r]] > 1) {      // в окне появился дубликат a[r]
        cnt[a[l]]--;
        l++;
    }
    ans = max(ans, r - l + 1);
}

4. Подотрезок максимальной длины с не более чем $K$ нулями 32:53

32:53 Пример: для $K=2$ нужно найти самый длинный подотрезок, содержащий не более двух нулей (в разобранном на лекции массиве такой подотрезок имел длину 5). Структура кода та же, меняются только добавление/удаление элемента:

C++
int zeros = 0, l = 0, ans = 0;
for (int r = 0; r < n; ++r) {
    if (a[r] == 0) zeros++;          // добавление a[r]
    while (zeros > k) {              // слишком много нулей
        if (a[l] == 0) zeros--;      // удаление a[l]
        l++;
    }
    ans = max(ans, r - l + 1);
}

35:00 Блоки «добавить элемент» и «удалить элемент» здесь отличаются от предыдущих задач (нужен только счётчик нулей, а не словарь), но весь остальной каркас остался прежним.

5. lower_bound и upper_bound 36:23

36:23 Функции из библиотеки <algorithm>. В них передаются указатель (итератор) на начало и на конец подотрезка и число $x$; подотрезок обязательно должен быть отсортирован по возрастанию.

38:25 Чтобы из итератора получить значение, перед ним ставят звёздочку: *lower_bound(...).

lower_bound для x = 610417283124305lower_bound(6)
В отсортированном массиве первый элемент, не меньший $6$, — это $7$ (индекс 2). Для $x=7$ lower_bound тоже указал бы на первую семёрку, а upper_bound — на $8$.

39:05 Пример применения. Есть два отсортированных массива $A$ и $B=2,3,6,8,12$. Для каждого $a_i$ найти в $B$ первый элемент, не меньший $a_i$: для $a_i=7$ это $8$.

C++
for (int i = 0; i < n; ++i) {
    int x = *lower_bound(b.begin(), b.end(), a[i]);  // первый элемент B, не меньший a[i]
    // для a[i] = 7 получим x = 8
}
Граничный случай
Если в $B$ нет элемента, не меньшего $a_i$, lower_bound вернёт b.end(), и разыменовывать такой итератор нельзя. Это замечание — собственное дополнение: на лекции граничный случай не разбирался.

41:10 Зачем две похожие функции? Например, чтобы найти границы подотрезка из семёрок в отсортированном массиве: lower_bound(7) даёт его начало, upper_bound(7) — место сразу после него. Количество семёрок — разность этих итераторов.

6. Два указателя не только на подотрезках: пара с заданной суммой 42:32

42:32 Метод работает и там, где нет подотрезков. Задача: в отсортированном массиве найти два числа с заданной суммой $S$. Пример на лекции: $S=15$, массив $1,2,4,7,11,15$. Ставим $l$ в начало и $r$ в конец и сдвигаем их навстречу друг другу.

Описание на лекции в записи фрагментарное, поэтому ниже — полный корректный разбор.

Важное дополнение: алгоритм встречных указателей
Пусть массив отсортирован по возрастанию, $l=0$, $r=n-1$. Смотрим на сумму $s=a_l+a_r$:Каждым шагом одна из границ сдвигается, и мы безопасно отбрасываем элемент, не имеющий пары. Всего не более $n$ шагов — $O(n)$.
C++
int l = 0, r = n - 1;
while (l < r) {
    int s = a[l] + a[r];
    if (s == S) { /* нашли пару (l, r) */ break; }
    if (s < S) l++;
    else r--;
}

43:55 Задача про количество пар с суммой не больше (или ровно) заданной решается в той же идее: двигаем указатели, пока соответствующее условие нарушено, а когда остановились — все элементы между указателями дают подходящие пары, которые можно посчитать разностью индексов.

Важное дополнение: пары с суммой $\le S$
При тех же $l=0$, $r=n-1$: если $a_l+a_r\le S$, то с $a_l$ подходят все элементы с индексами $l+1,\dots,r$ (они не больше $a_r$), и мы добавляем к ответу $r-l$, после чего делаем $l{+}{+}$; иначе $r{-}{-}$.
C++
long long cntPairs = 0;
int l = 0, r = n - 1;
while (l < r) {
    if (a[l] + a[r] <= S) { cntPairs += r - l; l++; }
    else r--;
}

7. Префиксные суммы 44:36

45:17 Для массива $a=3,1,4,2,5$ построим массив pref: pref[i] — сумма элементов $a_0,\dots,a_i$. Тогда $pref_0=a_0$, $pref_1=3+1=4$, $pref_2=8$, $pref_3=10$, $pref_4=15$.

Массив и его префиксные суммыa3011422354pref304182103154
Сумма подотрезка $[2,3]$ равна $pref_3-pref_1 = 10-4 = 6 = 4+2$.
C++
pref[0] = a[0];
for (int i = 1; i < n; ++i)
    pref[i] = pref[i - 1] + a[i];
$\displaystyle \sum_{i=l}^{r} a_i = pref_r - pref_{l-1}$   (при $l=0$ вычитать нечего: сумма равна $pref_r$)

47:22 Почему так: в $pref_r$ лежит вся сумма от начала до $r$, в $pref_{l-1}$ — сумма от начала до $l-1$. Вычитая вторую из первой, получаем ровно сумму на $[l,r]$. Значит, после предподсчёта за $O(n)$ сумма на любом подотрезке берётся за $O(1)$.

8. Подотрезок с максимальной суммой 48:04

8.1. Через префиксные суммы 48:47

48:47 Это тоже «два указателя», но в другой реализации. Перебираем правую границу $r$; для неё выгоднее всего взять такое $l$, при котором $pref_{l-1}$ минимально среди всех $l\le r$: тогда $pref_r - pref_{l-1}$ максимально. Поэтому поддерживаем минимум префиксных сумм слева.

C++
long long ans = LLONG_MIN, minSum = 0;   // minSum — минимум pref[l-1] по уже рассмотренным l
for (int r = 0; r < n; ++r) {
    ans = max(ans, pref[r] - minSum);    // лучший подотрезок, заканчивающийся в r
    minSum = min(minSum, pref[r]);       // теперь r годится как «l-1» для следующих
}

51:29 Левая граница $l$ в коде не хранится явно: нам важно лишь значение минимума, а минимум по префиксам считается так же, как минимум в обычном массиве (обновляем при каждом новом элементе).

Важное дополнение: начальные значения
Начальное значение minSum = 0 соответствует «пустому префиксу» $pref_{-1}=0$ — подотрезку, начинающемуся с нулевого элемента. Без него потерялись бы подотрезки, начинающиеся в начале массива. Начальное значение ans — минус бесконечность, иначе при массиве из одних отрицательных чисел вышел бы ответ $0$ (пустой подотрезок), которого условие обычно не допускает.

8.2. Алгоритм Кадане 52:50

52:50 Заводим cur — максимальную сумму подотрезка, заканчивающегося в текущем элементе. Переходя к следующему элементу $a_i$, у него два варианта: приклеить его к лучшему подотрезку, заканчивающемуся в $i-1$, либо начать новый подотрезок с самого $a_i$.

$cur_i = \max(cur_{i-1} + a_i,\; a_i), \qquad ans = \max_i cur_i$
C++
long long cur = a[0], ans = a[0];
for (int i = 1; i < n; ++i) {
    cur = max(cur + a[i], (long long)a[i]);  // продолжить или начать заново
    ans = max(ans, cur);                     // cur верен только для текущего i — сразу запоминаем
}

63:07 Почему достаточно двух вариантов: любой подотрезок, заканчивающийся в $i$, либо состоит из одного $a_i$, либо получается приклеиванием $a_i$ к подотрезку, заканчивающемуся в $i-1$. Среди вторых лучший по сумме — тот, у которого максимальна сумма $cur_{i-1}$, поэтому других кандидатов нет. Подотрезки, не заканчивающиеся в $i$, учтены на более ранних шагах через $ans$.

8.3. Восстановление самого подотрезка 55:34

55:34 Часто нужно не только число, но и границы $l$ и $r$ нужного подотрезка. Для этого рядом с ответом сохраняем границы и обновляем их, когда нашли ответ лучше. Пример: найти подотрезок максимальной длины с суммой не больше $K$.

C++
long long sum = 0;
int l = 0, ans = 0, ansL = 0, ansR = -1;   // ansL, ansR — границы лучшего найденного
for (int r = 0; r < n; ++r) {
    sum += a[r];                            // добавили
    while (sum > k) {                       // сумма слишком велика
        sum -= a[l];
        l++;
    }
    if (r - l + 1 > ans) {                  // нашли отрезок лучше — запоминаем границы
        ans = r - l + 1;
        ansL = l;
        ansR = r;
    }
}
// подотрезок — a[ansL..ansR]
Замечание
На лекции начальный ответ задавался как отрезок из одного элемента ($ans=1$, $l=r=0$). Это верно только если $a_0\le K$; в коде выше начальный ответ — «пустой» ($ans=0$), так что корректно и при $a_0>K$. Строчные $l,r$ — текущие границы окна, а запоминаемые «лучшие» границы (на лекции — заглавные $L,R$) — отдельные переменные; их важно не путать.

9. Разбор контеста по сортировкам 64:28

64:28 Задачи А (пузырьковая) и Б (вставками) — прямая реализация сортировок, разобранных на предыдущей лекции; задача D — сортировка подсчётом, задача G — слиянием (были на лекции). Ниже — остальные.

9.1. Задача C. Результаты олимпиады 65:50

65:50 Дано $n$ ($n\le 10^5$) пар (идентификатор, очки). Упорядочить по убыванию очков, а при равенстве очков — по возрастанию идентификатора. Квадратичные сортировки не пройдут, поэтому используем встроенную sort из <algorithm>. Два способа.

67:11 Способ 1: с компаратором. Компаратор — своя функция сравнения, которую передают третьим аргументом. Она получает два элемента и возвращает true, если первый должен стоять раньше второго. По умолчанию пары сортируются по возрастанию первого элемента, при равенстве — по возрастанию второго.

C++
vector<pair<int,int>> v(n);          // (id, score)
for (auto &p : v) cin >> p.first >> p.second;

bool cmp(pair<int,int> a, pair<int,int> b) {   // функция объявляется вне main
    if (a.second != b.second) return a.second > b.second;  // очки по убыванию
    return a.first < b.first;                              // при равенстве id по возрастанию
}

sort(v.begin(), v.end(), cmp);

72:46 Способ 2: без компаратора. Переставим элементы пары и умножим очки на $-1$: храним $(-score, id)$. Сортировка пар по возрастанию сначала упорядочит по $-score$ (то есть по убыванию $score$), при равенстве — по $id$. При выводе знак возвращаем.

C++
vector<pair<int,int>> v(n);          // (-score, id)
for (auto &p : v) {
    int id, sc; cin >> id >> sc;
    p = {-sc, id};
}
sort(v.begin(), v.end());
for (auto &p : v) cout << p.second << " " << -p.first << "\n";

9.2. Задача E. Анаграммы 74:54

74:54 Две строки $A$ и $B$ — анаграммы, если одна получается перестановкой букв другой (например, «шарм» и «марш»). Два способа.

  1. Подсчёт символов. Для каждой строки считаем массив из 26 счётчиков; если счётчики совпадают для обеих — анаграммы. Время $O(n)$.
  2. Сортировка. Отсортировать обе строки (sort(s.begin(), s.end())) и сравнить на равенство. Время $O(n\log n)$.
C++
vector<int> count(const string &s) {
    vector<int> c(26, 0);
    for (char ch : s) c[ch - 'a']++;
    return c;
}
bool isAnagram(const string &a, const string &b) {
    return count(a) == count(b);          // векторы сравниваются целиком
}

78:55 По сути первый способ — это сортировка подсчётом без восстановления строки. Строка — это вектор символов, поэтому к ней применимы все методы обычных векторов.

9.3. Задача F. Разброс 80:21

80:21 Дан массив из $n$ чисел; известно, что разность между максимумом и минимумом не превосходит $10^7$. Нужно отсортировать массив, $n$ велико. Используем сортировку подсчётом, но с смещённой точкой отсчёта: находим минимум $mn$ массива и считаем счётчики по индексу $a_i - mn$.

C++
int mn = *min_element(a.begin(), a.end());
vector<int> cnt(MAXD + 1);            // MAXD = 10^7: размах значений
for (int x : a) cnt[x - mn]++;        // индекс — отклонение от минимума
// восстановление: значение = индекс + mn
int pos = 0;
for (int v = 0; v <= MAXD; ++v)
    while (cnt[v]--) a[pos++] = v + mn;

9.4. Задача H. Зеркальный код 83:48

83:48 Дана строка. Составить из неё (можно не использовать часть букв) самый длинный палиндром, а среди всех таких — лексикографически наименьший. Например, aab → aba. Палиндром читается одинаково слева направо и справа налево.

85:11 Палиндром состоит из двух зеркальных половинок и, возможно, одного «центрального» символа $X$ (нечётная длина). Поэтому все буквы, кроме $X$, должны встречаться парами. Считаем count[26]; в половинку кладём каждую букву по count[c]/2 раз, перебирая буквы по возрастанию — это даёт лексикографически минимальную половинку. Центральный символ — наименьшая буква, встречающаяся нечётное число раз (если такая есть).

C++
string half;
int mid = -1;
for (int c = 0; c < 26; ++c) {
    half.append(cnt[c] / 2, 'a' + c);       // cnt[c]/2 копий буквы
    if (mid == -1 && cnt[c] % 2 == 1) mid = c;   // наименьшая буква с нечётным счётом
}
string rev(half.rbegin(), half.rend());
string ans = half;
if (mid != -1) ans += char('a' + mid);
ans += rev;
Важное дополнение: почему центр — наименьшая нечётная буква
Половинка не зависит от того, какую из «нечётных» букв мы поставим в центр: в неё идёт ровно cnt[c]/2 копий каждой буквы. Значит, два палиндрома максимальной длины с одинаковыми половинками различаются только центральным символом и зеркальной частью после него, а первое различие — в центре. Меньший палиндром получается при наименьшем центре. Метод append(count, ch) добавляет count копий символа — обычное умножение строки на число в C++ не работает.

9.5. Задача I. Перевозки на роверах 89:18

89:18 Дана карта из $n\times m$ чисел — стоимостей заказов $a_i$ (двумерность не важна, считываем в один массив) и несколько роверов; у ровера есть «жадность» $b_j$, и он может выполнить только заказ со стоимостью $a_i \ge b_j$. Каждый заказ достаётся не более чем одному роверу. Максимизировать число выполненных заказов.

91:19 Сортируем и заказы, и роверов по возрастанию и идём двумя указателями: один по роверам, другой по заказам. Если текущий заказ подходит роверу ($a_i\ge b_j$) — назначаем и двигаем оба указателя; иначе пропускаем этот заказ (он слишком дешёвый и для остальных, более жадных роверов тоже не подойдёт) и двигаем только указатель по заказам.

Жадное сопоставление заказов и роверов (собственный пример)заказы a505172103124по возрастаниюроверы b305192103124по возрастанию
Пары: $(5,3)$, $(5,5)$, $(10,9)$, $(12,10)$. Заказ $7$ не подошёл роверу с жадностью $9$, а более жадным роверам он не подходит тем более, поэтому его пропускаем; ровер $12$ остался без заказа. Итого 4 заказа.
C++
sort(a.begin(), a.end());
sort(b.begin(), b.end());
int i = 0, j = 0, cnt = 0;
while (i < (int)a.size() && j < (int)b.size()) {
    if (a[i] >= b[j]) { cnt++; i++; j++; }   // заказ подходит роверу — назначаем
    else i++;                                // заказ слишком дешёвый — пропускаем
}
Замечание
Пример на лекции был записан на доске фрагментарно, поэтому числа выше — собственный пример того же жадного приёма; сам приём (сортировка + два указателя, подходит ли ровер при нестрогом неравенстве) соответствует лекции.

9.6. Задача J. Пирамида мая 94:06

94:06 Строим пирамиду. На вершине — храм (он тоже один из уровней), под ним идут уровни; для устойчивости разность длин оснований соседних уровней должна быть не меньше $3$. Есть нижнее ограничение $x$ на сторону храма и набор доступных блоков. Максимизировать число уровней.

95:30 Сортируем размеры по неубыванию. Находим первый элемент, не меньший $x$ (lower_bound) — это храм. Дальше двигаемся указателем вправо и берём уровень, если его размер хотя бы на $3$ больше размера последнего взятого. Храм входит в ответ.

Пирамида: x = 26300351402413424взяли 3
Храм — $30$ (первый $\ge 26$); $35$ и $40$ отстоят от предыдущего не менее чем на $3$ — берём; $41$ и $42$ отличаются от $40$ меньше чем на $3$ — пропускаем.
C++
sort(a.begin(), a.end());
int i = lower_bound(a.begin(), a.end(), x) - a.begin();   // храм
if (i == n) { cout << 0; return 0; }                        // подходящего храма нет
int last = a[i], cnt = 1;
for (int j = i + 1; j < n; ++j)
    if (a[j] - last >= 3) { cnt++; last = a[j]; }

96:54 Физически «стирать» взятый элемент не нужно: мы просто двигаем указатель вправо, а разность считаем относительно последнего взятого уровня.

9.7. Задача K. Оценка 98:18

98:18 Есть набор строк — «кусков» десятичного числа. Расставить их в таком порядке, чтобы после склейки получилось максимально возможное число. Пример: куски $20$, $04$, $66$ (строки могут начинаться с нуля). Склейка в порядке $20,04,66$ даёт $200466$, а лучше поставить $66$ первым, затем $20$, затем $04$ — получим $662004$.

100:27 Гипотеза «сначала лексикографически большие» неверна: если у одной строки префикс совпадает с другой, длиннее считается большей, и мы можем поставить «99 и много нулей» раньше «99…», хотя поменяв их местами, получили бы число больше.

Правильный предикат
Строка $A$ должна стоять раньше $B$, если конкатенация $A+B$ даёт больше, чем $B+A$ (сравнение — как строк одинаковой длины, то есть лексикографическое сравнение склеек).

103:22 Доказательство от противного: пусть в «оптимальном» порядке нашлась пара кусков, для которой условие не выполнено: $A$ стоит раньше $B$, но $AB<BA$. Тогда если поменять их местами, то (при соседстве) число станет больше — значит порядок не был оптимальным. Противоречие.

C++
vector<string> s(n);
sort(s.begin(), s.end(), [](const string &a, const string &b) {
    return a + b > b + a;               // a должна стоять раньше b
});
for (auto &x : s) cout << x;

106:21 Числа в int не конвертируем: длина склейки огромна, она не влезет ни в какой целочисленный тип, поэтому работаем только со строками.

Важное дополнение: корректность компаратора
Чтобы sort работал корректно, компаратор должен задавать строгий слабый порядок (в частности, быть транзитивным). Сравнение «$a+b>b+a$» именно таким и является: оно эквивалентно сравнению дробей $\dfrac{a}{10^{|a|}-1}$ и $\dfrac{b}{10^{|b|}-1}$ (бесконечные периодические десятичные дроби с периодами $a$ и $b$), а сравнение чисел транзитивно.

9.8. Задача L. Египетские пирамиды 107:01

107:01 Дан набор многоугольников на плоскости (возможно, невыпуклых, с разным числом вершин), гарантируется, что любые два из них вложены друг в друга и их границы не пересекаются и не касаются. Для каждого нужно вывести, во сколько многоугольников он вложен (для самого внешнего — $0$).

110:01 Идея: если один многоугольник вложен в другой, то его самая правая точка (наибольший $x$) строго левее самой правой точки внешнего. Поэтому достаточно найти в каждом многоугольнике самую правую вершину и отсортировать многоугольники по убыванию этой координаты, помня исходный индекс (чтобы вывести ответ в порядке входа). Тогда номер в отсортированном порядке — глубина вложенности: у самого внешнего $0$, у следующего $1$ и т. д.

112:04 На этом разбор закончен. Контест по двум указателям уже опубликован; дедлайн объявят после следующего контеста.