1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6

# теория · шаг 2 из 6

Отсечения: по оценке, по симметрии и по порядку переменных

Коротко

Отсечение - это условие, при котором ветка не раскрывается. Их три вида. По допустимости - кандидат противоречит принятым решениям. По оценке - даже в лучшем случае ветка не даст ответа (сумма уже больше цели; взяв весь остаток, до цели не дотянуть). По симметрии - ветка повторяет уже рассмотренную с точностью до зеркального отражения. Замер на сумме подмножества из двадцати чисел: полное дерево 2 097 151 узел, с двумя отсечениями по оценке 15 017, плюс сортировка по убыванию - 6047. Это в 347 раз меньше. Сортировка по ВОЗРАСТАНИЮ на тех же данных делает перебор в восемь с половиной раз дороже, чем по убыванию.

Если ты различаешь отсечение по допустимости, по оценке и по симметрии, помнишь, что порядок кандидатов меняет цену в разы, и знаешь ловушку симметрии при нечётном n, - листай до «Теперь сам».

Как думаешь, какое отсечение сильнее

Задача: в списке двадцать чисел, надо посчитать, сколько подмножеств даёт в сумме ровно 97. Полный перебор - два миллиона узлов, в предел шагов движка он не укладывается.

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

На что это похоже

Отсечение - это как закрытая дверь в коридоре с комнатами. Можно войти в каждую комнату и убедиться, что выхода нет. А можно прочитать табличку на входе в коридор: «тупик» - и не входить вовсе. Табличка не делает коридор короче; она избавляет от необходимости его проходить.

Разница между видами отсечений - это разница между табличками. «Здесь пол провалился» - отсечение по допустимости, факт о текущем состоянии. «Дальше только комнаты меньше нужной» - отсечение по оценке, рассуждение о том, чего в ветке быть НЕ МОЖЕТ. «Этот коридор - зеркало того, который ты уже прошёл» - отсечение по симметрии.

Механизм: три вида и что каждый требует

ПО ДОПУСТИМОСТИ
условие: кандидат противоречит уже принятым решениям
пример:  ферзь под ударом; цвет совпал с соседним; ресурс занят
требует: уметь проверить ЧАСТИЧНОЕ решение
даёт:    всё поддерево продолжений этого кандидата

ПО ОЦЕНКЕ
условие: даже в самом лучшем случае ветка не даст ответа лучше найденного
пример:  сумма уже больше цели; остатка не хватит; оценка хуже лучшего найденного
требует: оценку, которая НЕ ЗАНИЖАЕТ возможности ветки
даёт:    тем больше, чем раньше оценка становится безнадёжной

ПО СИММЕТРИИ
условие: ветка повторяет уже рассмотренную с точностью до преобразования
пример:  зеркальная половина доски; равные элементы; поворот фигуры
требует: ТОЧНОГО счёта, сколько ветвей склеивается в одну
даёт:    постоянный множитель (вдвое, в восемь раз - сколько симметрий)

Про последнюю строку. У квадратной доски симметрий восемь: четыре поворота и четыре отражения. В этом уроке разобрана одна, зеркало слева направо, и множитель у неё - двойка; для остальных счёт кратности ещё тоньше, и браться за них стоит только после того, как освоена эта.

Отдельно стоит порядок кандидатов. Он не отсекает ничего сам, но решает, насколько рано сработают отсечения по оценке: если сначала пробовать большие числа, сумма быстрее превысит цель, и отсечение сработает выше по дереву.

Правило

Отсечение по оценке требует ДОПУСТИМОЙ оценки: она обязана быть не хуже (для минимума - не больше, для максимума - не меньше) любого реально достижимого в этой ветке результата. Такая оценка обещает ветке не меньше, чем та способна дать, - она оптимистична, и это правильно. Опасна обратная ошибка: оценка, которая ЗАНИЖАЕТ возможности ветки, отсекает ветку, в которой был ответ, и ошибку не видно - программа просто выдаёт не оптимум. Отсечение по симметрии требует точного счёта кратности: при чётном n у ферзей зеркало даёт ровно двойку, при нечётном - двойку для всех столбцов кроме центрального и единицу для центрального, и забытая единица теряет 54 решения из 352. Порядок кандидатов меняет цену перебора в разы, не меняя ответа: на сумме подмножества сортировка по убыванию даёт 25 996 шагов, по возрастанию - 210 830. И общее правило всего раздела: выигрыш отсечения ЗАМЕРЯЕТСЯ, а не предполагается, потому что проверка стоит шаг на каждом узле.

Разбор: два отсечения по оценке на сумме подмножества

Данные: двадцать чисел 41, 32, 27, 19, 55, 12, 38, 7, 24, 61, 16, 45, 9, 33, 28, 51, 14, 22, 36, 5, цель 97. Правильный ответ - 179 подмножеств.

Начинаем с полного перебора «берём - не берём», он нам уже знаком:

def rec(i, summa):
    if i == len(a):
        if summa == cel:
            nashli.append(1)
        return 0
    rec(i + 1, summa + a[i])
    rec(i + 1, summa)
    return 0

Первое отсечение - по превышению. Все числа положительны, значит сумма только растёт: если она уже больше цели, дальше смысла нет.

    if summa > cel:
        return 0

Второе - по остатку. Заранее посчитаем хвостовые суммы: hvost[i] - сумма всех элементов от i до конца. Если даже забрать весь хвост целиком, до цели не дотянуть - ветка мертва. Обе проверки ставятся первыми строками функции, до проверки дна, и программа целиком выглядит так:

hvost = [0] * (len(a) + 1)
i = len(a) - 1
while i >= 0:
    hvost[i] = hvost[i + 1] + a[i]
    i = i - 1

def rec(i, summa):
    if summa > cel:
        return 0                      # превышение: сумма назад не вернётся
    if summa + hvost[i] < cel:
        return 0                      # остаток: даже со всем хвостом не дотянуть
    if i == len(a):
        if summa == cel:
            nashli.append(1)
        return 0
    rec(i + 1, summa + a[i])
    rec(i + 1, summa)
    return 0

nashli = []
rec(0, 0)
print(len(nashli))

Замер. Узлы - это вызовы rec, то есть размер фактически обойдённого дерева:

вариант                              узлов      шагов       ответ
полное дерево (без отсечений)     2 097 151   не уложился     -
+ отсечение по превышению            18 661     73 126       179
+ отсечение по остатку               15 017     62 684       179
+ сортировка по убыванию              6 047     25 996       179
те же два отсечения, но сортировка
по ВОЗРАСТАНИЮ                       51 615    210 830       179

Строки с плюсом накапливаются: каждая добавляет своё к предыдущей. Последняя строка - отдельный опыт: оба отсечения на месте, а числа отсортированы в обратную сторону.

Ответ везде один и тот же - это обязательное свойство отсечения, и именно его проверяют тестами. А цена между удачным и неудачным порядком отличается в восемь с половиной раз (51 615 узлов против 6047), и в 347 раз между лучшей строкой и полным деревом.

Теперь ответ на вопрос из начала урока, и он с подвохом. Самый большой шаг - первое отсечение: с двух миллионов узлов до 18 661, в 112 раз. Остаток добавил ещё 1,24 раза, сортировка по убыванию - ещё 2,5. А вариант три в чистом виде, «отсортировать и ничего больше не менять», не даёт НИЧЕГО: без отсечений дерево полное при любом порядке. Сортировка не отбрасывает ни одной ветки сама - она делает так, чтобы сумма превышала цель как можно раньше, и тогда раньше срабатывает отсечение по превышению. Зато неудачный порядок способен съесть заработанное: исходный, случайный порядок дал 15 017 узлов, порядок по возрастанию - 51 615, втрое хуже, потому что мелкие числа в начале долго не дают сумме дорасти до цели. Порядок кандидатов - бесплатная оптимизация, которую чаще всего забывают.

Разбор: отсечение по остатку, которое сначала проиграло

Вот деталь, которую легко не заметить. Отсечение по остатку на двадцати числах сократило узлы с 18 661 до 15 017 и шаги с 73 126 до 62 684 - польза очевидна. А на первых двенадцати числах того же списка вышло так:

n = 12, цель 97            узлов    шагов
+ превышение                1 525    5 929
+ превышение и остаток      1 417    5 982   узлов МЕНЬШЕ, шагов БОЛЬШЕ

Узлов стало меньше на 108, а шагов - больше на 53. Вторая проверка выполняется на каждом из 1417 оставшихся узлов, а экономит только 108 отсечённых - и на коротком списке не окупилась. На первых шестнадцати числах то же самое: 5947 узлов вместо 6077, а шагов 24 799 вместо 23 105. В плюс она выходит только на двадцати.

Отсюда правило, которое стоит унести: отсечение - это сделка. Левая часть сделки - стоимость проверки, умноженная на число узлов; правая - сэкономленные поддеревья. На маленьких данных левая часть побеждает, на больших правая, и переломная точка зависит от задачи. Проверять надо на тех данных, на которых программа будет работать, и не по одному размеру, а по нескольким.

Разбор: симметрия, которая молча теряет решения

Доска ферзей зеркально симметрична: любому решению соответствует отражённое. Значит можно ставить первого ферзя только в левую половину строки, а найденные решения удвоить. Замер на восьми ферзях подтверждает: 52 577 шагов вместо 101 172 и 1029 узлов вместо 2057, то есть почти вдвое, ответ 92.

Теперь то же самое на девяти ферзях. Правильный ответ - 352. Два естественных способа применить симметрию дают:

первая строка, столбцы 0..3 (половина без центра), ответ × 2   →  298
первая строка, столбцы 0..4 (половина с центром),  ответ × 2   →  406
правильно:  2 × 149 (столбцы 0..3)  +  54 (столбец 4)          →  352

Ни один из первых двух не верен, и оба выглядят правдоподобно. Причина в том, что центральный столбец сам себе зеркало: решения с первым ферзём в центре при отражении переходят В СЕБЯ, и удваивать их нельзя. При чётном n центрального столбца нет, поэтому там работает простое удвоение - и именно поэтому ошибку так легко не заметить: на восьми ферзях всё верно, на девяти уже нет.

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

Что ломается без этого

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

Второе: отсечение, зависящее от знака данных. if summa > cel: return 0 верно только потому, что все числа положительны. Появись в списке отрицательные - отсечение станет неверным, и часть ответов потеряется. Условие применимости надо писать рядом в комментарии, иначе через полгода его никто не вспомнит.

Третье: проверка остатка без заранее посчитанных хвостов. Если считать остаток циклом внутри rec, то проверка стоит O(n) на узел, и отсечение почти наверняка не окупится. Хвостовые суммы считаются один раз за O(n) до перебора.

Четвёртое: удвоить решения и не проверить на маленьком случае. Разобранная выше потеря 54 решений из 352 - типичная цена.

Пятое: отсортировать и забыть, что порядок ответов изменился. Если задача просит не число наборов, а сами наборы, после сортировки они будут выданы в другом порядке и в «отсортированных» номерах. Либо сортируй вместе с индексами, либо восстанавливай исходные номера на выходе.

Зачем это в работе

Первый сюжет - подбор под ограничения. «Собрать заказ из позиций склада ровно на нужную сумму», «набрать группу серверов с суммарной памятью не меньше N и ценой не больше M» - это сумма подмножества с двумя отсечениями, и разница между наивным кодом и кодом с отсечениями - это разница между «не досчитало» и «ответ за секунду».

Второй - чужие решатели: программы, которым дают список правил, а они сами подбирают значения, которые всем правилам удовлетворяют (составляют расписание, раскладывают задачи по машинам). Когда такой решатель «думает подозрительно долго», причина почти всегда в том, что отсечения не срабатывают: оценка слабая или порядок переменных неудачный. Поэтому у таких инструментов и есть настройки вида «эвристика выбора переменной» - это ровно тот порядок кандидатов, который в нашем замере дал разницу в восемь с половиной раз.

Третий - разговор о цене. «Добавим проверку, станет быстрее» - утверждение, которое надо мерить. Замер из этого урока (n = 12: узлов меньше, шагов больше) - готовый аргумент в таком разговоре.

Теперь сам

Задача: найти в списке положительных чисел подмножество с МАКСИМАЛЬНОЙ суммой, не превышающей C. Какое отсечение по оценке здесь допустимо, а какое нет?

Ответ. Допустимо отсечение по остатку в сторону рекорда: если summa + hvost[i] не больше уже найденного рекорда, ветка не может его улучшить - и это честно, потому что hvost[i] - это самое большое, что в ней вообще достижимо.

if summa + hvost[i] <= rekord:
    return 0            # даже забрав весь хвост, рекорд не побить
if summa > C:
    return 0            # допустимость: за границу вылезли

Обрати внимание на подпись второй проверки. В задаче про точную сумму та же строка была отсечением по ОЦЕНКЕ: превышать цель никто не запрещал, мы рассуждали «сумма только растёт, значит до цели уже не вернуться». Здесь превышение C запрещено самим условием, поэтому это отсечение по ДОПУСТИМОСТИ: факт о текущем наборе, а не рассуждение о будущем.

А вот чего делать НЕЛЬЗЯ: отсекать по «оценке» вида summa + a[i] > C в смысле «этот элемент не влезает, значит и дальше смысла нет». Элемент не влез - это повод пропустить ЕГО, а не всю ветку: следующие элементы могут быть меньше и влезть. Такое «отсечение» теряет ответы, и на отсортированном по убыванию списке теряет их особенно охотно - потому что мелкие элементы стоят в конце.

Проверить себя просто: полный перебор на восьми числах даёт эталон, отсечённая версия обязана выдать то же число. Расхождение - значит отсечение не допустимо.

Главное

Три вида отсечений дают выигрыш разного масштаба и требуют разной аккуратности. По допустимости - самое простое и самое надёжное: кандидат противоречит принятым решениям. По оценке - самое сильное, но оценка обязана быть ДОПУСТИМОЙ, то есть никогда не обещать ветке меньше, чем та способна дать; заниженная оценка молча отбрасывает оптимум. По симметрии - постоянный множитель и самая коварная арифметика: девять ферзей дают 298 или 406 вместо 352, если забыть, что центральный столбец сам себе зеркало. Отдельно от всех трёх стоит порядок кандидатов: он ничего не отсекает, но решает, насколько рано сработают отсечения, и на сумме подмножества даёт разницу в восемь раз (25 996 шагов по убыванию против 210 830 по возрастанию). И главный замер: отсечение - сделка, у которой есть цена (шаг на каждом узле) и выигрыш (отсечённые поддеревья), поэтому на двенадцати числах проверка остатка узлов сократила, а шагов добавила.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.