Files
hello-algo/ru/chapters/chapter_15.md
2026-03-25 16:54:42 +08:00

494 lines
45 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Жадность
![](ru/docs/assets/media/image1028.jpeg){width="3.71875in" height="4.8125in"}
#### жадные алгоритмы
> *Жадный алгоритм* -- это распространенный метод решения задач оптими- зации. Его основная идея заключается в том, чтобы на каждом этапе приня- тия решения выбирать наиболее оптимальный на данный момент вариант, т. е. с жадностью принимать локально оптимальные решения в надежде по- лучить глобально оптимальное решение. Жадные алгоритмы просты и эф- фективны, и они находят широкое применение в решении многих практи- ческих задач.
>
> Жадные алгоритмы и динамическое программирование часто использу- ются для решения задач оптимизации. Между ними есть некоторые сходства, например оба метода зависят от свойств оптимальной подструктуры, но их принципы работы различны.
- Динамическое программирование для получения текущего решения учитывает все предыдущие решения и использует решения предыдущих подзадач для построения решения текущей подзадачи.
- Жадный алгоритм не учитывает предыдущие решения, а просто движет- ся вперед, делая жадные выборы и постепенно сокращая область задачи, пока она не будет решена.
> Чтобы лучше понять принцип работы жадного алгоритма, рассмотрим его применение к задаче о размене монет. Она уже была рассмотрена в разделе
>
> «Задача о полном рюкзаке», и, вероятно, вы с ней уже знакомы.
>
> Жадная стратегия, применяемая в этой задаче, показана на рис. 15.1. Для заданной целевой суммы **мы жадно выбираем монету, которая не превы- шает и наиболее близка к этой сумме**, и повторяем этот шаг, пока не будет достигнута целевая сумма.
>
> ![](ru/docs/assets/media/image1030.jpeg)
>
> **Рис. 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 монеты.
> ![](ru/docs/assets/media/image1032.jpeg)**Комбинация монет Целевая сумма Оптимальное решение**
>
> **жадного алгоритма (локальный оптимум)**
>
> **Оптимальное решение динамического программирования (глобальный оптимум)**
>
> **Рис. 15.2.** Примеры, когда жадный алгоритм не может найти оптимальное решение
>
> Таким образом, для задачи размена монет жадный алгоритм не гарантиру- ет нахождение глобально оптимального решения и может привести к очень плохому решению. Для решения этой задачи лучше подходит динамическое программирование.
>
> В общем случае жадные алгоритмы применимы в следующих двух ситуациях:
1) **можно гарантировать нахождение оптимального решения**: в этом случае жадный алгоритм часто является лучшим выбором, так как он обычно более эффективен, чем методы обратного поиска и динамиче- ского программирования;
2) **можно найти приближенное оптимальное решение**: в этом случае жадный алгоритм также применим. Для многих сложных задач поиск глобально оптимального решения очень затруднителен, и возможность найти субоптимальное решение с высокой эффективностью является весьма хорошим результатом.
### Свойства жадных алгоритмов
> Итак, возникает вопрос: какие задачи подходят для решения с помощью жад- ного алгоритма? Или, иначе говоря, в каких случаях жадный алгоритм может гарантировать нахождение оптимального решения?
>
> По сравнению с динамическим программированием условия применения жадного алгоритма более строгие, и они в основном сосредоточены на двух свойствах задачи.
1. **Свойство жадного выбора**: жадный алгоритм может гарантировать получение оптимального решения только в случае, если локально опти- мальный выбор всегда приводит к глобально оптимальному решению.
2. **Оптимальная подструктура**: оптимальное решение исходной задачи содержит оптимальное решение подзадачи.
> Оптимальная подструктура уже была рассмотрена в главе «Динамическое программирование», поэтому здесь не будем повторяться. Стоит отметить, что оптимальная подструктура некоторых задач не всегда очевидна, но их все же можно решить с помощью жадного алгоритма.
>
> Основное внимание уделяется методам определения свойства жадного вы- бора. Хотя его описание кажется простым, **на практике доказательство это- го свойства для многих задач является сложной задачей**.
>
> Например, в задаче о размене монет мы можем легко привести контрпри- мер для опровержения свойства жадного выбора. Однако доказательство его истинности значительно сложнее. На вопрос «**При каких условиях можно использовать жадный алгоритм для решения задачи размена монет**?» обычно мы можем дать лишь интуитивный или примерный ответ, но не мо- жем предоставить строгое математическое доказательство.
### Этапы решения задач жадным алгоритмом
> Процесс решения жадных задач можно разделить на следующие три этапа:
1) **анализ задачи**: изучение и понимание характеристик задачи, включая определение состояния, цели оптимизации и ограничения. Этот этап также присутствует в методах поиска с возвратом и динамического про- граммирования;
2) **определение жадной стратегии**: определение того, как делать жадный выбор на каждом шаге. Эта стратегия позволяет уменьшать размер за- дачи на каждом шаге и в конечном итоге решить всю задачу;
3) **доказательство корректности**: обычно требуется доказать нали- чие свойства жадного выбора и оптимальной подструктуры задачи. Этот этап может потребовать использования математических доказа- тельств, таких как метод математической индукции или доказатель- ство от противного.
> Определение жадной стратегии является ключевым этапом решения зада- чи, но его реализация может быть непростой по следующим причинам.
- **Жадные стратегии для различных задач могут значительно разли- чаться**. Для многих задач жадная стратегия очевидна, и ее можно опре- делить с помощью общего размышления и эмпирических проб. Однако для некоторых сложных задач жадная стратегия может оказаться очень скрытой, что потребует значительного опыта в решении задач и навы- ков работы с алгоритмами.
- **Некоторые жадные стратегии могут быть обманчивыми**. Бывает, жадная стратегия разработана с полной уверенностью в ее правильности, код написан и отправлен на выполнение. Но оказывается, что некоторые тестовые примеры не проходят проверку на корректность. Это происхо- дит потому, что разработанная жадная стратегия является лишь частично правильной, как в случае с задачей о размене монет, описанной выше.
> Для обеспечения корректности необходимо провести строгое математиче- ское доказательство жадной стратегии, **обычно с использованием метода доказательства от противного или метода математической индукции**.
>
> Тем не менее доказательство корректности может оказаться непростой за- дачей. Если нет ясности, обычно выбирается отладка кода на основе тестовых примеров с постепенной модификацией и проверкой жадной стратегии.
### Типичные задачи для жадного алгоритма
> Жадный алгоритм часто применяется в задачах оптимизации, удовлетворяю- щих свойству жадного выбора и оптимальной подструктуре. Ниже перечисле- ны некоторые типичные задачи для жадного алгоритма.
- **Задача о размене монет**: при некоторых комбинациях монет жадный алгоритм всегда может получить оптимальное решение.
- **Задача о расписании интервалов**: пусть у вас есть несколько задач, каждая из которых выполняется в течение определенного времени, и ваша цель -- выполнить как можно больше задач. Если каждый раз вы- бирать задачу с наименьшим временем окончания, то жадный алгоритм может дать оптимальное решение.
- **Задача о дробном рюкзаке**: дана группа предметов и вместимость. Ваша цель -- выбрать группу предметов так, чтобы общая масса не превышала вместимость, а общая стоимость была максимальной. Если каждый раз выбирать предмет с наивысшим соотношением стоимости к массе, то жадный алгоритм в некоторых случаях может дать оптимальное решение.
- **Задача о покупке и продаже акций**: дана группа акций с историей цены, можно совершать многократные покупки и продажи, но если ак- ции уже куплены, то перед следующей покупкой их необходимо продать. Цель -- получить максимальную прибыль.
- **Код Хаффмана** -- это жадный алгоритм, используемый для сжатия дан- ных без потерь. Строится дерево Хаффмана: каждый раз выбираются два узла с наименьшей частотой появления и объединяются, в резуль- тате чего получается дерево с минимальной длиной взвешенного пути (длиной кодирования).
<!-- -->
- **Алгоритм Дейкстры** -- это жадный алгоритм, решающий задачу нахож- дения кратчайшего пути от заданной исходной вершины до всех осталь- ных вершин.
#### задача о дробном рюкзаке
![](ru/docs/assets/media/image1036.jpeg)
> **Рис. 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**
![](ru/docs/assets/media/image1040.jpeg)
> **Рис. 15.4.** Стоимость предметов на единицу массы
##### Определение жадной стратегии
> Максимизация общей стоимости предметов в рюкзаке, по сути, является мак- симизацией стоимости предметов на единицу массы. Из этого можно вывести жадную стратегию, изображенную на рис. 15.5.
1. Отсортировать предметы по убыванию стоимости на единицу массы.
2. Перебирать все предметы и **жадно выбирать на каждом этапе пред- мет с наивысшей стоимостью на единицу массы**.
3. Если оставшейся вместимости рюкзака недостаточно, использовать часть текущего предмета для заполнения рюкзака.
Номер Масса Стои-
![](ru/docs/assets/media/image1042.jpeg)мость
> Стоимость на ед.
>
> массы Сортировка по
>
> убыванию стоимости на ед. массы
>
> **Жадная стратегия:**
>
> В первую очередь выбирать предметы с более высокой стоимостью на ед. массы
>
> **Рис. 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. Это сравнение помогает понять эффективность жадной стратегии с геометрической точ- ки зрения.
![](ru/docs/assets/media/image1044.jpeg)
> **Рис. 15.6.** Геометрическое представление задачи о дробном рюкзаке
#### задача о максимальной вместимости
> ![](ru/docs/assets/media/image1046.jpeg)
>
> **Рис. 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* становится короткой пе- регородкой).
>
> ![](ru/docs/assets/media/image1048.jpeg)
>
> **Рис. 15.8.** Начальное состояние
![](ru/docs/assets/media/image1050.jpeg)
> **Рис. 15.9.** Состояние после перемещения длинной перегородки внутрь
>
> Обратное рассуждение: **увеличить вместимость можно только переме- щая короткую перегородку** *i* **внутрь**. Хотя ширина обязательно уменьшится, **высота может увеличиться** (перемещенная короткая перегородка *i* может стать длиннее). Например, на рис. 15.10 после перемещения короткой пере- городки площадь увеличивается.
>
> Таким образом, можно сформулировать жадную стратегию для этой задачи: инициализировать два указателя, расположив их по краям контейнера, и на каждом шаге перемещать указатель, соответствующий короткой перегородке, внутрь, пока указатели не встретятся.
>
> ![](ru/docs/assets/media/image1052.jpeg)
>
> **Рис. 15.10.** Состояние после перемещения короткой перегородки внутрь
>
> На рис. 15.11 демонстрируется этот процесс выполнения жадной стратегии.
1. В начальном состоянии указатели *i* и *j* расположены по краям массива.
2. Вычисление вместимости текущего состояния *cap*\[*i*, *j*\] и обновление мак- симальной вместимости.
3. Сравнение высот перегородок *i* и *j* и перемещение короткой перегород- ки на одну позицию внутрь.
4. Повторение шагов 2 и 3 до тех пор, пока *i* и *j* не встретятся.
![](ru/docs/assets/media/image1054.jpeg)
> **Рис. 15.11.** Жадный алгоритм для задачи о максимальной вместимости. Шаг 1
>
> ![](ru/docs/assets/media/image1056.jpeg)
![](ru/docs/assets/media/image1058.jpeg)
> **Рис. 15.11.** *Продолжение*. Шаг 2--3
>
> ![](ru/docs/assets/media/image1060.jpeg)
![](ru/docs/assets/media/image1062.jpeg)
> **Рис. 15.11.** *Продолжение*. Шаг 4--5
>
> ![](ru/docs/assets/media/image1064.jpeg)
![](ru/docs/assets/media/image1066.jpeg)
> **Рис. 15.11.** *Продолжение*. Шаг 6--7
>
> ![](ru/docs/assets/media/image1068.jpeg)
![](ru/docs/assets/media/image1070.jpeg)
> **Рис. 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* **перемещает- ся внутрь**. Ранее было доказано, что перемещение длинной доски внутрь обя- зательно приведет к уменьшению емкости. Это означает, что пропущенные состояния не могут быть оптимальным решением, и **их пропуск не приведет к упущению оптимального решения**.
>
> Этот анализ показывает, что операция перемещения короткой перегородки является безопасной, и жадная стратегия эффективна.
>
> ![](ru/docs/assets/media/image1072.jpeg)
>
> **Рис. 15.12.** Перемещение короткой перегородки приводит к пропущенным состояниям
#### задача о максимальном произведении разбиения
![](ru/docs/assets/media/image1074.jpeg)
> **Рис. 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.
![](ru/docs/assets/media/image1076.jpeg)
> **Рис. 15.14.** Разбиение увеличивает произведение
>
> Далее следует обдумать, какой множитель является оптимальным. Сре- ди множителей 1, 2, 3 очевидно, что 1 -- наихудший, поскольку неравенство 1 × (*n* 1) \< *n* всегда верно, т. е. выделение 1 приведет к уменьшению произ- ведения.
>
> Если *n* = 6, 3 × 3 \> 2 × 2 × 2, **значит разбиение на тройки предпочтительнее разбиения на двойки**, см рис. 15.15.
>
> **Жадная стратегия 2**: в схеме разбиения должно быть не более двух зна- чений 2, поскольку три 2 всегда можно заменить двумя 3 и получить большее произведение.
![](ru/docs/assets/media/image1078.jpeg)
> **Рис. 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))
![](ru/docs/assets/media/image1080.png)
> **Рис. 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