# Жадность ![](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