mirror of
https://github.com/krahets/hello-algo.git
synced 2026-05-04 19:02:47 +08:00
494 lines
45 KiB
Markdown
494 lines
45 KiB
Markdown
# Жадность
|
||
|
||
{width="3.71875in" height="4.8125in"}
|
||
|
||
#### жадные алгоритмы
|
||
|
||
> *Жадный алгоритм* -- это распространенный метод решения задач оптими- зации. Его основная идея заключается в том, чтобы на каждом этапе приня- тия решения выбирать наиболее оптимальный на данный момент вариант, т. е. с жадностью принимать локально оптимальные решения в надежде по- лучить глобально оптимальное решение. Жадные алгоритмы просты и эф- фективны, и они находят широкое применение в решении многих практи- ческих задач.
|
||
>
|
||
> Жадные алгоритмы и динамическое программирование часто использу- ются для решения задач оптимизации. Между ними есть некоторые сходства, например оба метода зависят от свойств оптимальной подструктуры, но их принципы работы различны.
|
||
|
||
- Динамическое программирование для получения текущего решения учитывает все предыдущие решения и использует решения предыдущих подзадач для построения решения текущей подзадачи.
|
||
|
||
- Жадный алгоритм не учитывает предыдущие решения, а просто движет- ся вперед, делая жадные выборы и постепенно сокращая область задачи, пока она не будет решена.
|
||
|
||
> Чтобы лучше понять принцип работы жадного алгоритма, рассмотрим его применение к задаче о размене монет. Она уже была рассмотрена в разделе
|
||
>
|
||
> «Задача о полном рюкзаке», и, вероятно, вы с ней уже знакомы.
|
||
>
|
||
> Жадная стратегия, применяемая в этой задаче, показана на рис. 15.1. Для заданной целевой суммы **мы жадно выбираем монету, которая не превы- шает и наиболее близка к этой сумме**, и повторяем этот шаг, пока не будет достигнута целевая сумма.
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 15.1.** Жадная стратегия для задачи о размене монет
|
||
>
|
||
> Ниже приведен код реализации.
|
||
>
|
||
> \# === File: coin_change_greedy.py ===
|
||
>
|
||
> def coin_change_greedy(coins: list\[int\], amt: int) -\> int: \"\"\" Размен монет: жадный алгоритм.\"\"\"
|
||
>
|
||
> \# Предполагается, что список coins отсортирован. i = len(coins) - 1
|
||
>
|
||
> count = 0
|
||
>
|
||
> \# Выполняем цикл жадного выбора, пока не получим целевую сумму. while amt \> 0:
|
||
>
|
||
> \# Найти монету, меньшую и наиболее близкую к оставшейся сумме. while i \> 0 and coins\[i\] \> amt:
|
||
>
|
||
> i -= 1
|
||
>
|
||
> \# Выбор coins\[i\]. amt -= coins\[i\] count += 1
|
||
>
|
||
> \# Если не найдено решение, вернуть -1. return count if amt == 0 else -1
|
||
>
|
||
> Вы можете невольно воскликнуть: «Эврика!» Жадный алгоритм решает за- дачу размена монет всего за десяток строк кода.
|
||
|
||
1. **Преимущества и ограничения жадных алгоритмов**
|
||
|
||
> **Жадные алгоритмы не только просты в реализации, но и обычно очень эффективны**. Если в приведенном выше коде обозначить минимальный но- минал монеты как *min*(*coins*), то жадный выбор выполняется не более *amt* / *min*(*coins*) раз. Тогда временная сложность составляет *O*(*amt* / *min*(*coins*)). Это на порядок меньше временной сложности решения с использованием дина- мического программирования *O*(*n* × *amt*).
|
||
>
|
||
> Однако **для некоторых комбинаций номиналов монет жадный алгоритм не сможет найти оптимальное решение**. На рис. 15.2 приведены два примера.
|
||
|
||
- **Положительный пример** *coins* = \[1, 5, 10, 20, 50, 100\]: при данной ком- бинации монет для любого *amt* жадный алгоритм сможет найти опти- мальное решение.
|
||
|
||
- **Отрицательный пример** *coins* = \[1, 20, 50\]: если *amt* = 60, жадный алго- ритм найдет комбинацию 50 + 1 × 10, всего 11 монет. Но динамическое программирование может найти оптимальное решение 20 + 20 + 20, все- го 3 монеты.
|
||
|
||
- **Отрицательный пример** *coins* = \[1, 49, 50\]: если *amt* = 98, жадный алгоритм найдет комбинацию 50 + 1 × 48, всего 49 монет. Но динамическое програм- мирование может найти оптимальное решение 49 + 49, всего 2 монеты.
|
||
|
||
> **Комбинация монет Целевая сумма Оптимальное решение**
|
||
>
|
||
> **жадного алгоритма (локальный оптимум)**
|
||
>
|
||
> **Оптимальное решение динамического программирования (глобальный оптимум)**
|
||
>
|
||
> **Рис. 15.2.** Примеры, когда жадный алгоритм не может найти оптимальное решение
|
||
>
|
||
> Таким образом, для задачи размена монет жадный алгоритм не гарантиру- ет нахождение глобально оптимального решения и может привести к очень плохому решению. Для решения этой задачи лучше подходит динамическое программирование.
|
||
>
|
||
> В общем случае жадные алгоритмы применимы в следующих двух ситуациях:
|
||
|
||
1) **можно гарантировать нахождение оптимального решения**: в этом случае жадный алгоритм часто является лучшим выбором, так как он обычно более эффективен, чем методы обратного поиска и динамиче- ского программирования;
|
||
|
||
2) **можно найти приближенное оптимальное решение**: в этом случае жадный алгоритм также применим. Для многих сложных задач поиск глобально оптимального решения очень затруднителен, и возможность найти субоптимальное решение с высокой эффективностью является весьма хорошим результатом.
|
||
|
||
### Свойства жадных алгоритмов
|
||
|
||
> Итак, возникает вопрос: какие задачи подходят для решения с помощью жад- ного алгоритма? Или, иначе говоря, в каких случаях жадный алгоритм может гарантировать нахождение оптимального решения?
|
||
>
|
||
> По сравнению с динамическим программированием условия применения жадного алгоритма более строгие, и они в основном сосредоточены на двух свойствах задачи.
|
||
|
||
1. **Свойство жадного выбора**: жадный алгоритм может гарантировать получение оптимального решения только в случае, если локально опти- мальный выбор всегда приводит к глобально оптимальному решению.
|
||
|
||
2. **Оптимальная подструктура**: оптимальное решение исходной задачи содержит оптимальное решение подзадачи.
|
||
|
||
> Оптимальная подструктура уже была рассмотрена в главе «Динамическое программирование», поэтому здесь не будем повторяться. Стоит отметить, что оптимальная подструктура некоторых задач не всегда очевидна, но их все же можно решить с помощью жадного алгоритма.
|
||
>
|
||
> Основное внимание уделяется методам определения свойства жадного вы- бора. Хотя его описание кажется простым, **на практике доказательство это- го свойства для многих задач является сложной задачей**.
|
||
>
|
||
> Например, в задаче о размене монет мы можем легко привести контрпри- мер для опровержения свойства жадного выбора. Однако доказательство его истинности значительно сложнее. На вопрос «**При каких условиях можно использовать жадный алгоритм для решения задачи размена монет**?» обычно мы можем дать лишь интуитивный или примерный ответ, но не мо- жем предоставить строгое математическое доказательство.
|
||
|
||
### Этапы решения задач жадным алгоритмом
|
||
|
||
> Процесс решения жадных задач можно разделить на следующие три этапа:
|
||
|
||
1) **анализ задачи**: изучение и понимание характеристик задачи, включая определение состояния, цели оптимизации и ограничения. Этот этап также присутствует в методах поиска с возвратом и динамического про- граммирования;
|
||
|
||
2) **определение жадной стратегии**: определение того, как делать жадный выбор на каждом шаге. Эта стратегия позволяет уменьшать размер за- дачи на каждом шаге и в конечном итоге решить всю задачу;
|
||
|
||
3) **доказательство корректности**: обычно требуется доказать нали- чие свойства жадного выбора и оптимальной подструктуры задачи. Этот этап может потребовать использования математических доказа- тельств, таких как метод математической индукции или доказатель- ство от противного.
|
||
|
||
> Определение жадной стратегии является ключевым этапом решения зада- чи, но его реализация может быть непростой по следующим причинам.
|
||
|
||
- **Жадные стратегии для различных задач могут значительно разли- чаться**. Для многих задач жадная стратегия очевидна, и ее можно опре- делить с помощью общего размышления и эмпирических проб. Однако для некоторых сложных задач жадная стратегия может оказаться очень скрытой, что потребует значительного опыта в решении задач и навы- ков работы с алгоритмами.
|
||
|
||
- **Некоторые жадные стратегии могут быть обманчивыми**. Бывает, жадная стратегия разработана с полной уверенностью в ее правильности, код написан и отправлен на выполнение. Но оказывается, что некоторые тестовые примеры не проходят проверку на корректность. Это происхо- дит потому, что разработанная жадная стратегия является лишь частично правильной, как в случае с задачей о размене монет, описанной выше.
|
||
|
||
> Для обеспечения корректности необходимо провести строгое математиче- ское доказательство жадной стратегии, **обычно с использованием метода доказательства от противного или метода математической индукции**.
|
||
>
|
||
> Тем не менее доказательство корректности может оказаться непростой за- дачей. Если нет ясности, обычно выбирается отладка кода на основе тестовых примеров с постепенной модификацией и проверкой жадной стратегии.
|
||
|
||
### Типичные задачи для жадного алгоритма
|
||
|
||
> Жадный алгоритм часто применяется в задачах оптимизации, удовлетворяю- щих свойству жадного выбора и оптимальной подструктуре. Ниже перечисле- ны некоторые типичные задачи для жадного алгоритма.
|
||
|
||
- **Задача о размене монет**: при некоторых комбинациях монет жадный алгоритм всегда может получить оптимальное решение.
|
||
|
||
- **Задача о расписании интервалов**: пусть у вас есть несколько задач, каждая из которых выполняется в течение определенного времени, и ваша цель -- выполнить как можно больше задач. Если каждый раз вы- бирать задачу с наименьшим временем окончания, то жадный алгоритм может дать оптимальное решение.
|
||
|
||
- **Задача о дробном рюкзаке**: дана группа предметов и вместимость. Ваша цель -- выбрать группу предметов так, чтобы общая масса не превышала вместимость, а общая стоимость была максимальной. Если каждый раз выбирать предмет с наивысшим соотношением стоимости к массе, то жадный алгоритм в некоторых случаях может дать оптимальное решение.
|
||
|
||
- **Задача о покупке и продаже акций**: дана группа акций с историей цены, можно совершать многократные покупки и продажи, но если ак- ции уже куплены, то перед следующей покупкой их необходимо продать. Цель -- получить максимальную прибыль.
|
||
|
||
- **Код Хаффмана** -- это жадный алгоритм, используемый для сжатия дан- ных без потерь. Строится дерево Хаффмана: каждый раз выбираются два узла с наименьшей частотой появления и объединяются, в резуль- тате чего получается дерево с минимальной длиной взвешенного пути (длиной кодирования).
|
||
|
||
<!-- -->
|
||
|
||
- **Алгоритм Дейкстры** -- это жадный алгоритм, решающий задачу нахож- дения кратчайшего пути от заданной исходной вершины до всех осталь- ных вершин.
|
||
|
||
#### задача о дробном рюкзаке
|
||
|
||

|
||
|
||
> **Рис. 15.3.** Пример данных для задачи о дробном рюкзаке
|
||
>
|
||
> Задача о дробном рюкзаке и задача о рюкзаке 0-1 в целом очень похожи: состояние включает текущий предмет *i* и вместимость *c*, цель -- найти макси- мальную стоимость при ограниченной вместимости рюкзака.
|
||
>
|
||
> Отличие в том, что в данной задаче допускается выбирать часть предмета. **Можно произвольно разделять предметы и рассчитывать соответствую- щую стоимость пропорционально массе**, как показано на рис. 15.4.
|
||
|
||
1. Для предмета *i* его стоимость на единицу массы равна *val*\[*i* -- 1\]/*wgt*\[*i* -- 1\], сокращенно -- удельная стоимость.
|
||
|
||
2. Предположим, что в рюкзак помещена часть предмета *i* массой *w*, тогда увеличение стоимость рюкзака составит *w* × *val*\[*i* -- 1\]/*wgt*\[*i* -- 1\].
|
||
|
||
2. Задача о дробном рюкзаке ❖ **467**
|
||
|
||

|
||
|
||
> **Рис. 15.4.** Стоимость предметов на единицу массы
|
||
|
||
##### Определение жадной стратегии
|
||
|
||
> Максимизация общей стоимости предметов в рюкзаке, по сути, является мак- симизацией стоимости предметов на единицу массы. Из этого можно вывести жадную стратегию, изображенную на рис. 15.5.
|
||
|
||
1. Отсортировать предметы по убыванию стоимости на единицу массы.
|
||
|
||
2. Перебирать все предметы и **жадно выбирать на каждом этапе пред- мет с наивысшей стоимостью на единицу массы**.
|
||
|
||
3. Если оставшейся вместимости рюкзака недостаточно, использовать часть текущего предмета для заполнения рюкзака.
|
||
|
||
Номер Масса Стои-
|
||
|
||
мость
|
||
|
||
> Стоимость на ед.
|
||
>
|
||
> массы Сортировка по
|
||
>
|
||
> убыванию стоимости на ед. массы
|
||
>
|
||
> **Жадная стратегия:**
|
||
>
|
||
> В первую очередь выбирать предметы с более высокой стоимостью на ед. массы
|
||
>
|
||
> **Рис. 15.5.** Жадная стратегия для задачи о дробном рюкзаке
|
||
|
||
##### Код реализации
|
||
|
||
> Создадим класс предметов Item, чтобы можно было сортировать предметы по удельной стоимости. Будем циклически выполнять жадный выбор, если рюк- зак заполнен, выход из цикла и возврат решения.
|
||
>
|
||
> \# === File: fractional_knapsack.py ===
|
||
>
|
||
> class Item:
|
||
>
|
||
> \"\"\" Предмет.\"\"\"
|
||
>
|
||
> def init (self, w: int, v: int): self.w = w \# Масса предмета. self.v = v \# Стоимость предмета.
|
||
>
|
||
> def fractional_knapsack(wgt: list\[int\], val: list\[int\], cap: int) -\> int: \"\"\" Дробный рюкзак: жадный алгоритм.\"\"\"
|
||
>
|
||
> \# Создание списка предметов, содержащего два свойства: массу, стоимость. items = \[Item(w, v) for w, v in zip(wgt, val)\]
|
||
>
|
||
> \# Сортировка по убыванию стоимости за единицу массы item.v / item.w. items.sort(key=lambda item: item.v / item.w, reverse=True)
|
||
>
|
||
> \# Циклический жадный выбор. res = 0
|
||
>
|
||
> for item in items:
|
||
>
|
||
> if item.w \<= cap:
|
||
>
|
||
> \# Если оставшейся вместимости достаточно, текущий предмет полностью \# помещается в рюкзак.
|
||
>
|
||
> res += item.v cap -= item.w
|
||
>
|
||
> else:
|
||
>
|
||
> \# Если оставшейся вместимости недостаточно, в рюкзак помещается \# -часть текущего предмета.
|
||
>
|
||
> res += (item.v / item.w) \* cap
|
||
>
|
||
> \# Вместимость исчерпана, выход из цикла. break
|
||
>
|
||
> return res
|
||
>
|
||
> Помимо сортировки, необходимо в худшем случае пройти весь список пред- метов, поэтому **временная сложность составляет** *O*(*n*), где *n* -- количество предметов.
|
||
>
|
||
> Мы инициализируем список объектов Item, поэтому **пространственная сложность составляет** *O*(*n*).
|
||
|
||
##### Доказательство корректности
|
||
|
||
> Используем метод доказательства от противного. Предположим, что предмет *x* -- это предмет с наивысшей удельной стоимостью, и некоторый алгоритм на- шел максимальную ценность res, но это решение не включает предмет *x*.
|
||
>
|
||
> Извлечем из рюкзака любой предмет с единичной массой и заменим его на предмет *x* с той же массой. Поскольку предмет *x* обладает наибольшей удельной стоимостью, общая стоимость после замены будет больше, чем res. **Это противоречит тому**, **что** res **является оптимальным решением**, **сле- довательно**, **в оптимальном решении обязательно должен присутство- вать предмет** *x*.
|
||
>
|
||
> Для других предметов в этом решении также можно построить аналогичное противоречие. В итоге **предметы с большей удельной стоимостью всегда являются более предпочтительным выбором**, что подтверждает эффектив- ность жадной стратегии.
|
||
>
|
||
> Если рассматривать массу предметов и их удельную стоимость как оси двухмерной диаграммы, то задачу о дробном рюкзаке можно преобразо- вать в нахождение максимальной площади, ограниченной конечным ин- тервалом по горизонтальной оси, как показано на рис. 15.6. Это сравнение помогает понять эффективность жадной стратегии с геометрической точ- ки зрения.
|
||
|
||

|
||
|
||
> **Рис. 15.6.** Геометрическое представление задачи о дробном рюкзаке
|
||
|
||
#### задача о максимальной вместимости
|
||
|
||
> 
|
||
>
|
||
> **Рис. 15.7.** Пример данных для задачи о максимальной вместимости
|
||
>
|
||
> Контейнер образуется любыми двумя перегородками, **поэтому состоя- ние задачи определяется индексами двух перегородок, обозначим ее как** \[*i*, *j*\].
|
||
>
|
||
> Согласно условию вместимость равна произведению высоты на ширину, где высота определяется более короткой перегородкой, а ширина -- разницей ин- дексов двух перегородок в массиве. Обозначим вместимость как *cap*\[*i*, *j*\], тогда формула для расчета будет следующей:
|
||
>
|
||
> 𝑐𝑎𝑝\[𝑖, 𝑗\] = min(ℎ𝑡\[𝑖\] , ℎ𝑡\[𝑗\] ) × (𝑗 − 𝑖).
|
||
|
||
n
|
||
|
||
##### Определение жадной стратегии
|
||
|
||
> Для этой задачи существует более эффективное решение. Выберем состояние \[*i*, *j*\], которое удовлетворяет условиям *i* \< *j* и *ht*\[*i*\] \< *ht*\[*j*\], т. е. *i* является короткой перегородкой, а *j* -- длинной, как показано на рис. 15.8.
|
||
>
|
||
> **Если в этот момент переместить длинную перегородку** *j* **ближе к ко- роткой** *i*, **вместимость обязательно уменьшится**, как показано на рис. 15.9.
|
||
>
|
||
> Это происходит потому, что после перемещения длинной перегородки *j* ширина *j* -- *i* обязательно уменьшится. Высота же определяется короткой перегородкой, поэтому высота может остаться прежней (*i* остается короткой перегородкой) либо уменьшиться (перемещенная *j* становится короткой пе- регородкой).
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 15.8.** Начальное состояние
|
||
|
||

|
||
|
||
> **Рис. 15.9.** Состояние после перемещения длинной перегородки внутрь
|
||
>
|
||
> Обратное рассуждение: **увеличить вместимость можно только переме- щая короткую перегородку** *i* **внутрь**. Хотя ширина обязательно уменьшится, **высота может увеличиться** (перемещенная короткая перегородка *i* может стать длиннее). Например, на рис. 15.10 после перемещения короткой пере- городки площадь увеличивается.
|
||
>
|
||
> Таким образом, можно сформулировать жадную стратегию для этой задачи: инициализировать два указателя, расположив их по краям контейнера, и на каждом шаге перемещать указатель, соответствующий короткой перегородке, внутрь, пока указатели не встретятся.
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 15.10.** Состояние после перемещения короткой перегородки внутрь
|
||
>
|
||
> На рис. 15.11 демонстрируется этот процесс выполнения жадной стратегии.
|
||
|
||
1. В начальном состоянии указатели *i* и *j* расположены по краям массива.
|
||
|
||
2. Вычисление вместимости текущего состояния *cap*\[*i*, *j*\] и обновление мак- симальной вместимости.
|
||
|
||
3. Сравнение высот перегородок *i* и *j* и перемещение короткой перегород- ки на одну позицию внутрь.
|
||
|
||
4. Повторение шагов 2 и 3 до тех пор, пока *i* и *j* не встретятся.
|
||
|
||

|
||
|
||
> **Рис. 15.11.** Жадный алгоритм для задачи о максимальной вместимости. Шаг 1
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 15.11.** *Продолжение*. Шаг 2--3
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 15.11.** *Продолжение*. Шаг 4--5
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 15.11.** *Продолжение*. Шаг 6--7
|
||
>
|
||
> 
|
||
|
||

|
||
|
||
> **Рис. 15.11.** *Окончание*. Шаг 8--9
|
||
|
||
##### Код реализации
|
||
|
||
> Цикл выполняется не более *n* раз, поэтому **временная сложность состав- ляет** *O*(*n*).
|
||
>
|
||
> Переменные *i*, *j*, *res* используют дополнительное пространство постоянного размера, поэтому **пространственная сложность равна** *O*(1).
|
||
>
|
||
> \# === File: max_capacity.py ===
|
||
>
|
||
> def max_capacity(ht: list\[int\]) -\> int:
|
||
>
|
||
> \"\"\" Максимальная вместимость: жадный алгоритм.\"\"\"
|
||
>
|
||
> \# Инициализация i, j с расположением по краям массива. i, j = 0, len(ht) - 1
|
||
>
|
||
> \# Начальная максимальная вместимость равна 0. res = 0
|
||
>
|
||
> \# Цикл жадного выбора, пока две перегородки не встретятся. while i \< j:
|
||
>
|
||
> \# Обновление максимальной вместимости. cap = min(ht\[i\], ht\[j\]) \* (j - i)
|
||
>
|
||
> res = max(res, cap)
|
||
>
|
||
> \# Перемещение короткой перегородки внутрь. if ht\[i\] \< ht\[j\]:
|
||
>
|
||
> i += 1
|
||
>
|
||
> else:
|
||
>
|
||
> j -= 1
|
||
>
|
||
> return res
|
||
|
||
##### Доказательство корректности
|
||
|
||
> Жадный алгоритм быстрее перебора, потому что каждое жадное решение про- пускает некоторые состояния.
|
||
>
|
||
> Например, имеется состояние *cap*\[*i*, *j*\], в котором *i* является короткой пере- городкой, а *j* -- длинной. Если жадно переместить короткую доску *i* на одну по- зицию внутрь, это приведет к тому, что состояние, показанное на рис. 15.12, будет пропущено. Это означает, что впоследствии **невозможно будет прове- рить размеры емкости всех этих состояний**:
|
||
>
|
||
> *cap*\[*i*, *i* + 1\], *cap*\[*i*, *i* + 2\], \..., *cap*\[*i*, *j* -- 2\], *cap*\[*i*, *j* -- 1\].
|
||
>
|
||
> Наблюдение показывает, что **эти пропущенные состояния на самом деле являются всеми состояниями**, **при которых длинная доска** *j* **перемещает- ся внутрь**. Ранее было доказано, что перемещение длинной доски внутрь обя- зательно приведет к уменьшению емкости. Это означает, что пропущенные состояния не могут быть оптимальным решением, и **их пропуск не приведет к упущению оптимального решения**.
|
||
>
|
||
> Этот анализ показывает, что операция перемещения короткой перегородки является безопасной, и жадная стратегия эффективна.
|
||
>
|
||
> 
|
||
>
|
||
> **Рис. 15.12.** Перемещение короткой перегородки приводит к пропущенным состояниям
|
||
|
||
#### задача о максимальном произведении разбиения
|
||
|
||

|
||
|
||
> **Рис. 15.13.** Определение задачи о максимальном произведении разбиения
|
||
>
|
||
> Предположим, что мы разложили *n* на *m* целых множителей, где *i*-й множи- тель обозначен как *ni*, т. е.:
|
||
|
||
***m***
|
||
|
||
> ***n*** = Ι*ni* .
|
||
>
|
||
> ***i*** =1
|
||
>
|
||
> Цель данной задачи -- найти максимальное произведение всех целых мно- жителей, т. е.:
|
||
>
|
||
> max= ( ∉***m** n* 1.
|
||
>
|
||
> ***i***
|
||
>
|
||
> ***i*** 1
|
||
>
|
||
> Необходимо решить вопрос: насколько велико должно быть количество раз- биений *m* и каковы должны быть значения каждого *ni*?
|
||
|
||
##### Определение жадной стратегии
|
||
|
||
> Эмпирический факт заключается в том, произведение двух чисел часто больше их суммы. Предположим, что из *n* выделяется множитель 2, тогда итоговое про- изведение равно 2(*n* − 2). Сравним это произведение с *n*:
|
||
>
|
||
> 2(*n* − 2) ≥ *n*
|
||
>
|
||
> 2 *n* − *n* − 4 ≥ 0
|
||
>
|
||
> *n* ≥ 4.
|
||
>
|
||
> Когда *n* ≥ 4, выделение множителя 2 увеличивает произведение, как пока- зано на рис. 15.14. Это означает, что **целые числа, равные или большие** 4, **необходимо раскладывать на несколько множителей**.
|
||
>
|
||
> **Жадная стратегия 1**: если в схеме разбиения присутствует множитель ≥ 4, то его следует продолжать раскладывать. В окончательной схеме разбиения должны присутствовать только множители 1, 2, 3.
|
||
|
||

|
||
|
||
> **Рис. 15.14.** Разбиение увеличивает произведение
|
||
>
|
||
> Далее следует обдумать, какой множитель является оптимальным. Сре- ди множителей 1, 2, 3 очевидно, что 1 -- наихудший, поскольку неравенство 1 × (*n* − 1) \< *n* всегда верно, т. е. выделение 1 приведет к уменьшению произ- ведения.
|
||
>
|
||
> Если *n* = 6, 3 × 3 \> 2 × 2 × 2, **значит разбиение на тройки предпочтительнее разбиения на двойки**, см рис. 15.15.
|
||
>
|
||
> **Жадная стратегия 2**: в схеме разбиения должно быть не более двух зна- чений 2, поскольку три 2 всегда можно заменить двумя 3 и получить большее произведение.
|
||
|
||

|
||
|
||
> **Рис. 15.15.** Оптимальные множители разбиения
|
||
>
|
||
> Таким образом, можно вывести общую жадную стратегию.
|
||
|
||
1. Задать целое число *n* и выделять из него множитель 3 до тех пор, пока остаток не станет 0, 1 или 2.
|
||
|
||
2. Если остаток равен 0, значит *n* кратно 3, и дальнейшие действия не тре- буются.
|
||
|
||
3. Если остаток равен 2, не продолжать разбиение, оставить как есть.
|
||
|
||
4. Если остаток равен 1, то, поскольку 2 × 2 \> 1 × 3, следует заменить послед- ний множитель 3 на 2.
|
||
|
||
##### Код реализации
|
||
|
||
> Из рис. 15.16 видно, что для разбиения числа нет необходимости использовать цикл. Можно воспользоваться операцией целочисленного деления вниз для получения количества троек *a*, а также операцией взятия остатка для получе- ния остатка *b*, в этом случае:
|
||
>
|
||
> *n* = 3*a* + *b*.
|
||
>
|
||
> Обратите внимание, что для граничных случаев, когда *n* ≤ 3, необходимо вы- делить множитель 1, произведение будет равно 1 × (*n* − 1).
|
||
>
|
||
> \# === File: max_product_cutting.py ===
|
||
>
|
||
> def max_product_cutting(n: int) -\> int:
|
||
>
|
||
> \"\"\" Максимальное произведение разбиения: жадный алгоритм.\"\"\" \# Когда n \<= 3, необходимо выделить 1.
|
||
>
|
||
> if n \<= 3:
|
||
>
|
||
> return 1 \* (n - 1)
|
||
>
|
||
> \# Жадно выделять 3, a -- количество троек, b -- остаток. a, b = n // 3, n % 3
|
||
>
|
||
> if b == 1:
|
||
>
|
||
> \# Если остаток равен 1, преобразовать пару 1 \* 3 в 2 \* 2.
|
||
>
|
||
> return int(math.pow(3, a - 1)) \* 2 \* 2 if b == 2:
|
||
>
|
||
> \# Если остаток равен 2, ничего не предпринимать. return int(math.pow(3, a)) \* 2
|
||
>
|
||
> \# Если остаток равен 0, ничего не предпринимать. return int(math.pow(3, a))
|
||
|
||

|
||
|
||
> **Рис. 15.16.** Метод вычисления максимального произведения разбиения
|
||
>
|
||
> **Временная сложность зависит от метода реализации операции возве- дения в степень в языке программирования**. Для Python обычно использу- ются три функции для вычисления степени.
|
||
|
||
- Оператор \*\* и функция pow() имеют временную сложность *O*(log *a*).
|
||
|
||
- Функция math.pow() вызывает функцию pow() из библиотеки C, выполняющую возведение в степень с плавающей точкой c временной сложностью O(1).
|
||
|
||
> Переменные *a* и *b* используют дополнительное пространство постоянного размера, поэтому **пространственная сложность составляет** *O*(1).
|
||
|
||
##### Доказательство корректности
|
||
|
||
> Используем метод от противного и проанализируем только случай *n* ≥ 3.
|
||
|
||
1. **Все множители** ≤ 3: предположим, что в оптимальной схеме разбиения существует множитель ≥ 4, тогда его можно разложить на 2(*x* − 2) и полу- чить большее произведение. Это противоречит предположению.
|
||
|
||
2. **Схема разбиения не содержит** 1: предположим, что в оптимальной схеме разбиения существует множитель 1, тогда его можно объединить
|
||
|
||
> с другим множителем и получить большее произведение. Это противо- речит предположению.
|
||
|
||
3. **Максимальное количество двоек в разбиении равно** 2: предполо- жим, что в оптимальном разбиении содержатся три двойки, тогда их можно заменить на две тройки и получить большее произведение. Это противоречит предположению.
|
||
|
||
#### резюме
|
||
|
||
- Жадные алгоритмы обычно применяются для решения задач оптимиза- ции. Их принцип заключается в том, чтобы на каждом этапе принятия решения делать локально оптимальный выбор с целью получения гло- бально оптимального решения.
|
||
|
||
- В жадных алгоритмах циклически выполняются жадные выборы, каж- дый раз превращая задачу в меньшую подзадачу, пока задача не будет решена.
|
||
|
||
- Жадные алгоритмы не только просты в реализации, но и обладают высо- кой эффективностью решения. По сравнению с динамическим програм- мированием временная сложность жадных алгоритмов обычно ниже.
|
||
|
||
- В задаче о размене монет для некоторых комбинаций монет жадный ал- горитм может гарантировать нахождение оптимального решения. Но для других комбинаций жадный алгоритм может найти очень плохое решение.
|
||
|
||
- Задачи, подходящие для решения жадными алгоритмами, обладают двумя основными свойствами: свойство жадного выбора и оптимальная подструктура. Свойство жадного выбора свидетельствует об эффектив- ности жадной стратегии.
|
||
|
||
- Для некоторых сложных задач доказательство свойства жадного выбо- ра является сложной задачей. Относительно проще найти контрпример и опровергнуть это свойство, например в задаче о размене монет.
|
||
|
||
- Решение жадных задач обычно включает три этапа: анализ задачи, опре- деление жадной стратегии, доказательство корректности. Среди них ключевым этапом является определение жадной стратегии, а доказа- тельство корректности часто представляет собой сложную задачу.
|
||
|
||
- Задача о дробном рюкзаке, в отличие от задачи о рюкзаке 0-1, позволяет выбирать часть предметов, поэтому ее можно решить с помощью жадно- го алгоритма. Корректность жадной стратегии можно доказать методом от противного.
|
||
|
||
- Задачу о максимальной вместимости можно решить методом перебо- ра, временная сложность которого составляет *O*(*n*2). Разработав жадную стратегию, в которой на каждом шаге граница перемещается внутрь, временную сложность можно оптимизировать до *O*(*n*).
|
||
|
||
- В задаче о максимальном произведении разбиения мы последователь- но формулируем две жадные стратегии. Во-первых, для целых чисел ≥ 4 нужно продолжать разбиение. Во-вторых, оптимальным множителем разбиения является 3. В коде содержатся операции возведения в сте- пень, временная сложность которых зависит от метода их реализации и обычно составляет *O*(1) или *O*(log *n*).
|
||
|
||
> Глава 16
|