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

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

Оптимальная подструктура: второе условие, без которого ответ неверен

Коротко

Перекрытия подзадач недостаточно. Нужно второе условие - оптимальная подструктура: оптимальный ответ задачи должен собираться из ОПТИМАЛЬНЫХ ответов подзадач. У кратчайшего пути это так (любой кусок кратчайшего пути сам кратчайший), поэтому Флойд-Уоршелл работает. У самого длинного ПРОСТОГО пути - нет: оптимальный q→r длиной 3 ребра и оптимальный r→t длиной 1 вместе дают не путь длины 4, а вообще не путь - они проходят через одну вершину дважды. Признак беды - подзадачи НЕ НЕЗАВИСИМЫ: ресурс, использованный в одной, запрещён в другой. Без второго условия таблица заполняется, программа работает и отвечает неверно.

Если ты умеешь проверить оптимальную подструктуру и объяснить, почему она есть у кратчайшего пути и нет у самого длинного простого, листай до «Теперь сам».

Как думаешь, всегда ли из лучших частей получается лучшее целое

Звучит как самоочевидное: чтобы получить лучший маршрут из A в C через B, возьми лучший маршрут A→B и лучший B→C. Для кратчайших путей так и есть - именно на этом стоит весь раздел про пути.

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

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

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

Механизм: два условия и как их проверять

ДВА УСЛОВИЯ ПРИМЕНИМОСТИ ДИНАМИЧЕСКОГО ПРОГРАММИРОВАНИЯ

1. ПЕРЕКРЫТИЕ ПОДЗАДАЧ
   одна и та же подзадача нужна многим - иначе запоминать нечего,
   и достаточно обычного деления пополам

2. ОПТИМАЛЬНАЯ ПОДСТРУКТУРА
   оптимальный ответ целого содержит оптимальные ответы подзадач
   ⇒ можно считать подзадачи независимо и склеивать

первое условие про ЦЕНУ, второе про ПРАВИЛЬНОСТЬ.
нет первого - динамика не нужна. нет второго - динамика НЕВЕРНА
КАК ПРОВЕРЯТЬ ВТОРОЕ УСЛОВИЕ

взять оптимальный ответ целого, разрезать его по предполагаемой подзадаче
и спросить: кусок - оптимальный ответ для своей подзадачи?
    да  → подструктура есть, можно писать переход
    нет → найдёшь контрпример из трёх-четырёх элементов

признак беды: подзадачи делят ОБЩИЙ РЕСУРС
    вершины, которые нельзя посещать дважды
    место в рюкзаке, которое одно на всех
    время, которое нельзя занять двумя задачами
ПОЧЕМУ У РЮКЗАКА ПОДСТРУКТУРА ВСЁ-ТАКИ ЕСТЬ
место - общий ресурс, но мы вписали его В СОСТОЯНИЕ: подзадача «первые i
предметов при остатке c». Два разных остатка - два разных состояния,
и они уже независимы. Так лечится большинство случаев: общий ресурс
становится ещё одним параметром состояния

Правило

Динамическое программирование применимо при ДВУХ условиях. Перекрытие подзадач отвечает за цену: без него достаточно обычной рекурсии. Оптимальная подструктура отвечает за правильность: оптимальный ответ целого должен складываться из оптимальных ответов подзадач. Проверяют её так - берут оптимальный ответ, разрезают по подзадаче и смотрят, оптимален ли кусок. Беда приходит, когда подзадачи делят общий ресурс: у кратчайшего пути подструктура есть, у самого длинного ПРОСТОГО пути её нет, потому что вершины нельзя использовать дважды. Типовое лечение - внести общий ресурс в СОСТОЯНИЕ (остаток места, номер шага, маска использованного), и тогда подзадачи становятся независимыми. Если внести не удаётся - задача, скорее всего, NP-трудна.

Разбор: кратчайший путь против самого длинного простого

Кратчайший путь. Пусть лучший путь из q в t проходит через r. Тогда его кусок от q до r - лучший путь из q в r: иначе мы заменили бы кусок на более короткий и получили бы путь из q в t короче лучшего. Противоречие. Отсюда и переход Флойда-Уоршелла из одиннадцатого раздела: «лучший путь через первые k вершин» склеивается из двух таких же.

Теперь самый длинный ПРОСТОЙ путь (простой - значит без повторного посещения вершин). Граф направленный, пять рёбер:

q → r      r → t      q → s      s → t      t → r

Перебором находим самые длинные простые пути:

q → t :  2 ребра, и таких путей ДВА: q → s → t  и  q → r → t
q → r :  3 ребра: q → s → t → r
r → t :  1 ребро: r → t

То, что у q → t оптимальных путей два, здесь важно: рассуждение дальше использует второй из них.

А теперь попробуем собрать ответ «по рецепту»: лучший путь из q в t через r = лучший q→r плюс лучший r→t:

q → s → t → r   +   r → t   =   q → s → t → r → t

вершина t встречается ДВАЖДЫ - это вообще не простой путь.
рецепт дал не «путь длиной 4», а недопустимый ответ

И наоборот: возьмём второй оптимальный путь q → r → t (2 ребра). Его кусок q → r - это ОДНО ребро, а самый длинный путь из q в r содержит три. Оптимум целого содержит неоптимальную часть - значит подструктуры нет.

причина в одной фразе: у этих подзадач ОБЩИЙ РЕСУРС - множество
непосещённых вершин. Выбор в первой половине меняет правила во второй

Самый длинный простой путь - NP-трудная задача, и это не совпадение: внести «множество использованных вершин» в состояние можно (об этом глава про ДП по маскам), но тогда состояний становится 2^n, и дешёвого решения не получается.

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

Вторая история - про жадность и динамику, и она ближе к работе. Задача: выбрать максимальное число непересекающихся занятий. В разделе 14 мы доказали, что тут работает жадность. А теперь то же, но у занятий есть ЦЕННОСТЬ, и максимизировать надо сумму.

занятия (начало, конец, ценность):
  A (0, 2, ценность 1)      B (1, 10, ценность 100)

жадность по раннему концу: берёт A (конец 2), после него B недоступно → 1
оптимум:                   взять только B                            → 100

Отношение ответов здесь 1 к 100, и его можно сделать любым: критерий, оптимальный для ЧИСЛА занятий, про ценность не знает ничего. Это тот же контрпример, что в разделе 14, - и он показывает, что взвешенная задача требует другого алгоритма, а не другого критерия.

Подструктура здесь ЕСТЬ, и это видно из формулировки перехода: отсортируем занятия по концу и для каждого i спросим - берём его или нет.

dp[i] = max( dp[i−1],                        не берём i
             ценность(i) + dp[p(i)] )        берём i, где p(i) - последнее
                                              занятие, закончившееся до начала i

Оптимальный ответ для первых i занятий содержит оптимальный ответ для первых p(i) - потому что занятия с номерами больше p(i) просто недоступны, и подзадачи независимы. Общий ресурс (время) внесён в состояние через сортировку по концу: номер занятия и есть «сколько времени уже занято».

Сравни с предыдущим абзацем: там внести ресурс в состояние дешево было нельзя, здесь - можно, и задача решается за Θ(n·log n) (сортировка по концу плюс двоичный поиск p(i) для каждого занятия). Оговорка, чтобы не осталось ложного впечатления: взвешенный выбор занятий НИКОГДА не был NP-трудным - просто жадность для него неверна. Удачное состояние не «снимает NP-трудность», оно лишь позволяет решить задачу динамикой; целый рюкзак - пример обратного, там состояние нашлось, а задача осталась NP-трудной.

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

Первое: проверить только перекрытие и начать писать таблицу. Таблица заполнится, программа выдаст число, и число будет неверным - без всяких падений. Второе: проверять подструктуру «на примерах»: прогон ничего не доказывает, потому что склейка оптимумов часто СЛУЧАЙНО совпадает с правильным ответом. В графе из разбора выше это видно прямо: для пары q → t рецепт «оптимум плюс оптимум» через s даёт верный ответ 2, и только через r - неверный 4. Один путь проверили - и ошибку не увидели. Третье: не заметить общий ресурс. Признак - в формулировке есть слово «не более одного раза», «не повторяя», «каждый ровно один раз». Четвёртое: внести ресурс в состояние и не заметить, что состояний стало 2^n: формально верно, практически неприменимо при n больше двадцати. Пятое: решить, что NP-трудность - это приговор: у таких задач остаются приближения из раздела 14 и точные методы с отсечениями из раздела 16.

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

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

Теперь сам

Задача: дана последовательность чисел, надо выбрать подпоследовательность с максимальной суммой, в которой НЕТ двух соседних элементов. Есть ли у неё оптимальная подструктура? Напиши переход.

Ответ. Есть. Состояние - номер элемента: dp[i] = лучшая сумма на первых i элементах.

dp[i] = max( dp[i−1],              i-й не берём
             a[i] + dp[i−2] )      i-й берём, тогда (i−1)-й запрещён
база: dp[0] = 0, dp[1] = max(0, a[1])

Проверка подструктуры: оптимальный ответ на первых i элементах либо не содержит i-го - тогда это оптимальный ответ на первых i−1, - либо содержит, и тогда остаток обязан быть оптимальным ответом на первых i−2. Иначе мы заменили бы остаток на лучший и улучшили целое. Общий ресурс («соседний элемент занят») локален: он связывает только соседей, и это связывание уже учтено в переходе.

пример: 3, 2, 7, 10
dp[1] = 3
dp[2] = max(3, 2) = 3
dp[3] = max(3, 7 + 3) = 10
dp[4] = max(10, 10 + 3) = 13     ответ 13 - это 3 + 10

А вот задача, которая выглядит почти так же и подструктуры НЕ имеет: выбрать подпоследовательность без двух соседних так, чтобы сумма была максимальной, НО взятых элементов было ровно k. Пока k не внесён в состояние, рецепт неверен: оптимум для первых i элементов с любым числом взятых не годится как часть ответа с ограничением. Лечится это ровно тем же приёмом - вторым параметром состояния dp[i][k], и цена становится Θ(n·k).

Главное

Для динамического программирования нужны ДВА условия: перекрытие подзадач (отвечает за цену) и оптимальная подструктура (отвечает за правильность). Второе проверяют так: разрезать оптимальный ответ по подзадаче и спросить, оптимален ли кусок. У кратчайшего пути подструктура есть - отсюда Флойд-Уоршелл; у самого длинного ПРОСТОГО пути нет: оптимальный q→r (3 ребра) плюс оптимальный r→t (1 ребро) дают путь, дважды проходящий через t, а настоящий оптимум q→t содержит НЕоптимальный кусок q→r. Причина - общий ресурс (непосещённые вершины). Типовое лечение: внести ресурс в состояние (остаток места, номер по порядку, число взятых) - так взвешенный выбор занятий становится Θ(n·log n), а соседи в последовательности - Θ(n). Если ресурс вносится только как 2^n-маска, задача, скорее всего, NP-трудна.

Комментарии

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

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

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