Задача

Сломайте быструю сортировку

сортировка разделяй и властвуй безопасность олимпиады

Сервис принимает от пользователей списки чисел и сортирует их вот этой функцией — быстрой сортировкой с разбиением Хоара, где опорным берётся средний элемент куска. Функция возвращает число сделанных сравнений:

def quicksort(a):
    count = 0
    stack = [(0, len(a) - 1)]
    while stack:
        lo, hi = stack.pop()
        if hi <= lo:
            continue
        p = (lo + hi) // 2
        a[lo], a[p] = a[p], a[lo]
        x, i, j = a[lo], lo - 1, hi + 1
        while True:
            i += 1
            count += 1
            while a[i] < x:
                i += 1
                count += 1
            j -= 1
            count += 1
            while x < a[j]:
                j -= 1
                count += 1
            if i >= j:
                break
            a[i], a[j] = a[j], a[i]
        stack.append((j + 1, hi))
        stack.append((lo, j))
    return count

На случайных данных ей хватает порядка n log₂ n сравнений, на уже отсортированных — тоже. Владельцы сервиса уверены, что так будет всегда. Покажите, что они ошибаются: составьте перестановку чисел от 1 до n, на которой функция сделает не меньше ⌊n²/4⌋ сравнений.

Вход

Одно число n (1 ≤ n ≤ 1000).

Выход

n различных чисел от 1 до n через пробел. Если подходящих перестановок несколько, выведите любую.

Примеры

Пример 1

Вход
8
Выход
5 3 7 1 2 4 6 8

Ограничения

Время · Python
4000 мс
Время · Go
1000 мс
Память
256 МБ
Проверка
10 тестов

Разбор

Открывайте по ступеням: каждая следующая подсказывает больше. Можно сразу к решению — но задача, решённая с одной подсказкой, запоминается лучше.

1Подсказка 1

Отсортированный и развёрнутый входы здесь не годятся: средний элемент отсортированного куска — его медиана, и разбиение выходит идеальным. Нужна перестановка, где опорный раз за разом оказывается почти самым маленьким в своём куске.

2Подсказка 2

Выводить формулу такой перестановки не обязательно. Можно запустить ту же самую сортировку на «неизвестных» числах и придумывать их значения по ходу — отвечать на каждое сравнение так, как невыгодно сортировке, но без противоречий.

3Подсказка 3

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

4Идея

Это противник-убийца Дугласа Макилроя (статья «A Killer Adversary for Quicksort», 1999). Все элементы начинают газом. Застывший элемент всегда меньше газа. Когда встречаются два газа, застывает один из них, получая следующее наименьшее значение, — и выбирать надо «кандидата в опорные»: газ, который последним пережил сравнение. Опорный элемент участвует в каждом сравнении разбиения, поэтому застывает первым и оказывается меньше всех остальных газов. Разбиение отрезает от куска один-два элемента, и рекурсия уходит на глубину порядка n.

Ответы противника никогда не противоречат друг другу: застывшие значения больше не меняются, газ больше любого застывшего, а сравнение двух газов всегда начинается с того, что один из них застывает. В конце газом остаётся один элемент, он получает наибольшее значение — выходит перестановка. Если теперь отсортировать её по-настоящему, функция сделает в точности те же сравнения, что и при подсказках противника.

5Разбор

Эталон — та же функция из условия, только вместо a[i] < x вызывается less(a[i], x) противника, а в массиве лежат номера элементов, значения которых хранятся отдельно. Сортировка двигает номера, значения остаются на местах — и после окончания value[k] + 1 и есть число, которое нужно поставить на место k.

Результат с запасом: для n = 1000 нужно 250 000 сравнений, а перестановка противника заставляет сделать 501 498 — почти n²/2, больше, чем у пузырька. Для первого примера, n = 8, — 42 сравнения вместо 16 необходимых.

Самое ценное здесь — что противник ничего не знает об устройстве сортировки: ни как она выбирает опорный, ни как разбивает. Он только отвечает на сравнения. Поэтому тот же код ломает и выбор первого элемента, и медиану трёх. Защищает от него только случайность: если опорный выбирается случайно и противник не видит бросков, перестановка, построенная заранее, работает как обычные случайные данные. Подробнее — в статье о быстрой сортировке.

5+Решение
6Сложность

Противник сам выполняет атакуемую сортировку, поэтому стоит столько же, сколько её худший случай: O(n²) сравнений, для n = 1000 — полмиллиона, доли секунды даже на Python.

7Типичные ошибки
  • Отсортированный или развёрнутый вход. Он губит сортировку с опорным первым элементом, а здесь опорный — средний, и такой вход для неё лучший.
  • Значения противника без единицы. Противник раздаёт значения от 0 до n − 1, а нужна перестановка чисел от 1 до n.
  • Другая сортировка в симуляции. Противник строит вход против той функции, которую запускают. Порядок обхода кусков неважен — набор сравнений от него не зависит, — а вот другое разбиение или другой выбор опорного дадут перестановку, губительную для другой сортировки.

Алгоритмы задачи