EduBrick

Сортировка стеком

Вагоны, тупик и один разъезд. Задача, где жадность единственно возможна, — а количество ответов оказывается числом Каталана.

4 мин

Вагоны стоят на первом пути в каком-то порядке. Разрешено загонять вагоны с первого пути в тупик и вытаскивать их из тупика на второй путь. Обратно — нельзя. Требуется получить на втором пути вагоны в порядке 1,2,,n1, 2, \ldots, n.

Тупик работает как стек: вагон, загнанный последним, выйдет первым. Значит, вопрос звучит так: какие перестановки сортируются одним стеком?

Почему жадность здесь не выбор, а необходимость

Обычно жадный алгоритм надо доказывать. Здесь доказывать нечего: на каждом шаге допустимо ровно одно осмысленное действие.

Пусть мы уже вывели вагоны 1,,k11, \ldots, k-1 и ищем вагон kk.

  • Если kk лежит на вершине тупика — снимаем и выводим. Не сделать этого нельзя: пока kk не выведен, дальше двигаться некуда, а закопать его глубже мы не можем.
  • Если kk ещё на первом пути — придётся загнать в тупик все вагоны до него включительно. Других способов добраться до kk нет.
  • Если kk в тупике, но не на вершине — проиграли. Сверху лежит вагон с большим номером, он обязан выйти раньше, а нужен нам позже.

Выбора нет ни в одной ветке. Поэтому алгоритм, который просто следует этим правилам, либо приводит к цели, либо доказывает, что цели нет.

bool sortable(const vector<int>& a) {
    int n = a.size();
    vector<int> st;
    int need = 1, i = 0;
    while (need <= n) {
        if (!st.empty() && st.back() == need) { st.pop_back(); need++; continue; }
        if (i == n) return false;          // на первом пути пусто, а нужного нет
        st.push_back(a[i++]);
    }
    return true;
}

Каждый вагон входит в стек не более одного раза и выходит не более одного раза, поэтому O(n)O(n) — несмотря на вложенный цикл. Это амортизированная оценка в чистом виде.

Проверено: на всех перестановках длины до восьми жадный алгоритм совпал с полным перебором состояний.

Критерий

Есть и локальное описание. Перестановка сортируется стеком тогда и только тогда, когда в ней нет образца 231 — то есть нет тройки индексов i<j<ki < j < k с ak<ai<aja_k < a_i < a_j.

Смысл прямой: aia_i попадёт в стек раньше aja_j и окажется под ним, а вывести надо сначала aka_k, потом aia_i, потом aja_j. Вагон aja_j лежит сверху и мешает.

Проверять критерий напрямую не нужно — симуляция и короче, и быстрее. Но он объясняет следующий факт.

Сколько таких перестановок

nn сортируемых всего
1 1 1
2 2 2
3 5 6
4 14 24
5 42 120
6 132 720
7 429 5 040
8 1 430 40 320
9 4 862 362 880

Это в точности числа Каталана — те же, что считают правильные скобочные последовательности. Совпадение не случайное: последовательность действий со стеком — это и есть скобочная последовательность, где «загнать в тупик» — открывающая скобка, «вытащить» — закрывающая.

Соответствие взаимно однозначное, поэтому и количества совпадают. Посчитано перебором всех перестановок до девяти элементов.

Заодно это оценка, насколько задача жёсткая: доля сортируемых перестановок Cn/n!C_n / n! падает экспоненциально. При n=9n = 9 это уже 1,3%.

Вариации

Два стека или дек вместо одного стека. Класс сортируемых перестановок расширяется, критерий усложняется, а жадность перестаёт быть единственно возможной — появляется настоящий выбор, и задача становится содержательно другой.

Нужен не ответ «да/нет», а последовательность операций. Тот же алгоритм, только записываем каждое действие: push при загоне в тупик, pop при выводе. Длина протокола ровно 2n2n.

Целевой порядок не 1n1 \ldots n, а произвольный. Перенумеруйте вагоны так, чтобы целевой порядок стал возрастающим, и запустите то же самое.