Add ru version (#1865)
* Add Russian docs site baseline * Add Russian localized codebase * Polish Russian code wording * Update ru code translation. * Update code translation and chapter covers. * Fix pythontutor extraction. * Add README and landing page. * placeholder of profiles * Use figures of English version * Remove chapter paperbook
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,52 @@
|
||||
# Задача о дробном рюкзаке
|
||||
|
||||
!!! question
|
||||
|
||||
Дано $n$ предметов. Вес предмета $i$ равен $wgt[i-1]$, ценность равна $val[i-1]$, также дан рюкзак вместимостью $cap$. Каждый предмет можно выбрать только один раз, **но разрешается взять лишь часть предмета, а ценность вычисляется пропорционально взятому весу**. Требуется найти максимальную ценность предметов в рюкзаке при ограниченной вместимости. Пример показан на рисунке ниже.
|
||||
|
||||

|
||||
|
||||
Задача о дробном рюкзаке в целом очень похожа на задачу о рюкзаке 0-1: состояние включает текущий предмет $i$ и вместимость $c$, а цель состоит в нахождении максимальной ценности при заданной вместимости рюкзака.
|
||||
|
||||
Отличие в том, что здесь разрешено брать только часть предмета. Как показано на рисунке ниже, **мы можем произвольно делить предмет и вычислять соответствующую ценность пропорционально весу**.
|
||||
|
||||
1. Для предмета $i$ его ценность на единицу веса равна $val[i-1] / wgt[i-1]$, сокращенно - удельная ценность.
|
||||
2. Если взять часть предмета $i$ весом $w$, то ценность рюкзака увеличится на $w \times val[i-1] / wgt[i-1]$.
|
||||
|
||||

|
||||
|
||||
### Определение жадной стратегии
|
||||
|
||||
Максимизация общей ценности предметов в рюкзаке **по сути равносильна максимизации ценности на единицу веса**. Отсюда естественно выводится следующая жадная стратегия.
|
||||
|
||||
1. Отсортировать предметы по убыванию удельной ценности.
|
||||
2. Перебирать все предметы и **на каждом шаге жадно выбирать предмет с наибольшей удельной ценностью**.
|
||||
3. Если оставшейся вместимости рюкзака недостаточно, взять часть текущего предмета, чтобы заполнить рюкзак.
|
||||
|
||||

|
||||
|
||||
### Код реализации
|
||||
|
||||
Мы вводим класс `Item`, чтобы можно было сортировать предметы по удельной ценности. Далее циклически выполняем жадный выбор и, когда рюкзак заполнен, выходим и возвращаем ответ:
|
||||
|
||||
```src
|
||||
[file]{fractional_knapsack}-[class]{}-[func]{fractional_knapsack}
|
||||
```
|
||||
|
||||
Встроенный алгоритм сортировки обычно имеет временную сложность $O(\log n)$, а пространственная сложность обычно равна $O(\log n)$ или $O(n)$, в зависимости от конкретной реализации в языке программирования.
|
||||
|
||||
Помимо сортировки, в худшем случае потребуется пройти весь список предметов, **поэтому временная сложность равна $O(n)$**, где $n$ - число предметов.
|
||||
|
||||
Поскольку инициализируется список объектов `Item`, **пространственная сложность равна $O(n)$**.
|
||||
|
||||
### Доказательство корректности
|
||||
|
||||
Используем доказательство от противного. Предположим, что предмет $x$ имеет наибольшую удельную ценность, некоторый алгоритм получил максимальную ценность `res`, но в найденном решении предмет $x$ отсутствует.
|
||||
|
||||
Теперь вынем из рюкзака произвольный предмет единичного веса и заменим его на предмет $x$ того же веса. Поскольку предмет $x$ имеет наибольшую удельную ценность, общая ценность после замены обязательно станет больше `res`. **Это противоречит тому, что `res` является оптимальным решением, а значит оптимальное решение обязательно содержит предмет $x$**.
|
||||
|
||||
Для других предметов в этом решении можно построить аналогичное противоречие. Иными словами, **предметы с большей удельной ценностью всегда являются более выгодным выбором**, а значит жадная стратегия корректна.
|
||||
|
||||
Как показано на рисунке ниже, если рассматривать вес предметов и их удельную ценность как горизонтальную и вертикальную оси двумерной диаграммы, то задачу о дробном рюкзаке можно интерпретировать как «поиск максимальной площади, ограниченной конечным отрезком по горизонтали». Эта аналогия помогает понять корректность жадной стратегии с геометрической точки зрения.
|
||||
|
||||

|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,94 @@
|
||||
# Жадный алгоритм
|
||||
|
||||
<u>Жадный алгоритм (greedy algorithm)</u> - это распространенный подход к решению задач оптимизации. Его основная идея состоит в том, чтобы на каждом этапе принятия решения выбирать вариант, который выглядит наилучшим прямо сейчас, то есть жадно принимать локально оптимальные решения в надежде получить глобально оптимальный результат. Жадные алгоритмы лаконичны и эффективны, поэтому широко применяются во многих практических задачах.
|
||||
|
||||
Жадные алгоритмы и динамическое программирование часто используются для решения задач оптимизации. У них есть некоторое сходство, например оба опираются на свойство оптимальной подструктуры, но принципы работы различаются.
|
||||
|
||||
- Динамическое программирование учитывает все решения предыдущих этапов при выборе текущего решения и использует ответы для прошлых подзадач, чтобы построить ответ для текущей подзадачи.
|
||||
- Жадный алгоритм не учитывает прошлые решения, а просто движется вперед, каждый раз делая жадный выбор, постепенно сужая область задачи, пока она не будет решена.
|
||||
|
||||
Сначала разберем принцип работы жадного алгоритма на примере задачи «размен монет». Эта задача уже встречалась в разделе «задача о полном рюкзаке», поэтому она наверняка вам знакома.
|
||||
|
||||
!!! question
|
||||
|
||||
Дано $n$ видов монет. Номинал монеты $i$ равен $coins[i - 1]$, целевая сумма равна $amt$, причем каждую монету можно брать неограниченное число раз. Требуется найти минимальное число монет, которыми можно набрать целевую сумму. Если набрать сумму невозможно, верните $-1$.
|
||||
|
||||
Жадная стратегия для этой задачи показана на рисунке ниже. Для заданной целевой суммы **мы жадно выбираем монету, которая не превышает ее и находится к ней ближе всего**, и повторяем этот шаг, пока не получим нужную сумму.
|
||||
|
||||

|
||||
|
||||
Код реализации выглядит следующим образом:
|
||||
|
||||
```src
|
||||
[file]{coin_change_greedy}-[class]{}-[func]{coin_change_greedy}
|
||||
```
|
||||
|
||||
У вас может невольно вырваться: So clean! Жадный алгоритм решает задачу размена монет всего примерно десятью строками кода.
|
||||
|
||||
## Преимущества и ограничения жадного алгоритма
|
||||
|
||||
**Жадный алгоритм не только прост в действиях и реализации, но и обычно очень эффективен**. В приведенном выше коде обозначим минимальный номинал монеты через $\min(coins)$, тогда жадный выбор выполняется не более чем $amt / \min(coins)$ раз, а временная сложность равна $O(amt / \min(coins))$. Это на порядок меньше, чем временная сложность решения через динамическое программирование $O(n \times amt)$.
|
||||
|
||||
Однако **для некоторых наборов номиналов монет жадный алгоритм не может найти оптимальный ответ**. Ниже показаны два примера.
|
||||
|
||||
- **Положительный пример $coins = [1, 5, 10, 20, 50, 100]$**: для такого набора монет при любом $amt$ жадный алгоритм находит оптимальное решение.
|
||||
- **Отрицательный пример $coins = [1, 20, 50]$**: пусть $amt = 60$. Жадный алгоритм найдет только комбинацию $50 + 1 \times 10$, то есть всего $11$ монет, тогда как динамическое программирование находит оптимум $20 + 20 + 20$, где требуется лишь $3$ монеты.
|
||||
- **Отрицательный пример $coins = [1, 49, 50]$**: пусть $amt = 98$. Жадный алгоритм найдет только комбинацию $50 + 1 \times 48$, то есть всего $49$ монет, тогда как динамическое программирование находит оптимум $49 + 49$, где требуется лишь $2$ монеты.
|
||||
|
||||

|
||||
|
||||
Иными словами, в задаче о размене монет жадный алгоритм не гарантирует нахождение глобально оптимального решения и иногда может приводить к очень плохому ответу. Для этой задачи больше подходит динамическое программирование.
|
||||
|
||||
В общем случае жадный алгоритм применим в двух следующих ситуациях.
|
||||
|
||||
1. **Можно гарантировать нахождение оптимального решения**: в таком случае жадный алгоритм часто является лучшим выбором, поскольку обычно он эффективнее, чем поиск с возвратом и динамическое программирование.
|
||||
2. **Можно найти приближенно оптимальное решение**: в таком случае жадный алгоритм тоже полезен. Для многих сложных задач поиск глобального оптимума очень труден, и возможность быстро найти субоптимальный ответ уже весьма ценна.
|
||||
|
||||
## Свойства жадного алгоритма
|
||||
|
||||
Тогда возникает вопрос: какие задачи подходят для решения жадным алгоритмом? Или, другими словами, в каких случаях жадный алгоритм может гарантировать оптимальный ответ?
|
||||
|
||||
По сравнению с динамическим программированием условия применения жадного алгоритма строже. В основном нас интересуют два свойства задачи.
|
||||
|
||||
- **Свойство жадного выбора**: только когда локально оптимальный выбор всегда может привести к глобально оптимальному решению, жадный алгоритм способен гарантировать оптимум.
|
||||
- **Оптимальная подструктура**: оптимальное решение исходной задачи содержит оптимальные решения подзадач.
|
||||
|
||||
Оптимальная подструктура уже обсуждалась в главе «Динамическое программирование», поэтому здесь не будем повторяться. Стоит отметить, что у некоторых задач оптимальная подструктура не столь очевидна, но их все равно можно решать жадным алгоритмом.
|
||||
|
||||
Основное внимание мы уделяем тому, как определить свойство жадного выбора. Хотя формулировка выглядит довольно простой, **на практике для многих задач доказать свойство жадного выбора совсем не легко**.
|
||||
|
||||
Например, в задаче о размене монет легко привести контрпример и опровергнуть свойство жадного выбора, но вот доказать его истинность намного сложнее. Если спросить: **для каких наборов монет можно использовать жадный алгоритм**? - обычно удается дать лишь интуитивный или примерный ответ, а не строгое математическое доказательство.
|
||||
|
||||
!!! quote
|
||||
|
||||
Существует статья, в которой приводится алгоритм со временной сложностью $O(n^3)$ для определения того, можно ли с помощью жадного алгоритма находить оптимальный размен для любой суммы в заданной системе монет.
|
||||
|
||||
Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.
|
||||
|
||||
## Этапы решения задач жадным алгоритмом
|
||||
|
||||
В общем виде процесс решения жадной задачи можно разбить на три шага.
|
||||
|
||||
1. **Анализ задачи**: разобраться в свойствах задачи, включая определение состояний, целевой функции и ограничений. Этот этап присутствует и в поиске с возвратом, и в динамическом программировании.
|
||||
2. **Определение жадной стратегии**: определить, какой жадный выбор следует делать на каждом шаге. Эта стратегия должна уменьшать размер задачи на каждом этапе и в итоге привести к решению всей задачи.
|
||||
3. **Доказательство корректности**: обычно требуется доказать, что задача обладает свойством жадного выбора и оптимальной подструктурой. На этом этапе может понадобиться математическое доказательство, например индукция или доказательство от противного.
|
||||
|
||||
Определение жадной стратегии - это ключевой этап решения, но на практике он часто оказывается непростым по следующим причинам.
|
||||
|
||||
- **Жадные стратегии для разных задач сильно различаются**. Для многих задач стратегия довольно очевидна, и до нее можно дойти за счет общих рассуждений и нескольких проб. Но в более сложных задачах жадная стратегия может быть очень скрытой, и тут уже многое зависит от опыта решения задач и алгоритмической подготовки.
|
||||
- **Некоторые жадные стратегии выглядят убедительно, но оказываются обманчивыми**. Бывает, что мы с уверенностью придумали жадную стратегию, написали код и отправили его на проверку, а часть тестов не проходит. Причина в том, что спроектированная стратегия лишь «частично верна», и описанная выше задача о размене монет - типичный пример.
|
||||
|
||||
Чтобы гарантировать корректность, нужно дать строгое математическое доказательство жадной стратегии, **обычно с использованием доказательства от противного или математической индукции**.
|
||||
|
||||
Однако и доказательство корректности может оказаться непростой задачей. Если идей нет, мы обычно начинаем отлаживать код на тестовых примерах, постепенно меняя и проверяя жадную стратегию.
|
||||
|
||||
## Типичные задачи для жадного алгоритма
|
||||
|
||||
Жадные алгоритмы часто применяются в задачах оптимизации, которые обладают свойством жадного выбора и оптимальной подструктурой. Ниже приведены некоторые типичные задачи, решаемые жадным подходом.
|
||||
|
||||
- **Задача о размене монет**: при некоторых системах монет жадный алгоритм всегда дает оптимальный ответ.
|
||||
- **Задача о расписании интервалов**: пусть есть несколько задач, каждая выполняется в некотором временном интервале, и требуется завершить как можно больше задач. Если каждый раз выбирать задачу с самым ранним временем окончания, то жадный алгоритм дает оптимальный ответ.
|
||||
- **Задача о дробном рюкзаке**: дана группа предметов и грузоподъемность. Требуется выбрать предметы так, чтобы их общий вес не превышал ограничение, а общая ценность была максимальной. Если каждый раз выбирать предмет с наилучшим отношением стоимости к весу, то в некоторых случаях жадный алгоритм дает оптимальный ответ.
|
||||
- **Задача о покупке и продаже акций**: дана история цен акции. Можно совершать несколько сделок, но если акция уже куплена, то до продажи покупать снова нельзя. Цель - получить максимальную прибыль.
|
||||
- **Код Хаффмана**: это жадный алгоритм для сжатия данных без потерь. Построив дерево Хаффмана и каждый раз объединяя два узла с наименьшей частотой, мы получаем дерево с минимальной взвешенной длиной пути, то есть минимальной длиной кодирования.
|
||||
- **Алгоритм Дейкстры**: это жадный алгоритм решения задачи о кратчайших путях от заданной исходной вершины до всех остальных вершин.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Жадность
|
||||
|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
Подсолнух поворачивается к солнцу, постоянно стремясь к наилучшим условиям для роста.
|
||||
|
||||
Жадная стратегия через цепочку простых выборов постепенно приводит к наилучшему ответу.
|
||||
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,99 @@
|
||||
# Задача о максимальной вместимости
|
||||
|
||||
!!! question
|
||||
|
||||
Дан массив $ht$, где каждый элемент обозначает высоту вертикальной перегородки. Любые две перегородки в массиве вместе с пространством между ними образуют контейнер.
|
||||
|
||||
Вместимость контейнера равна произведению высоты и ширины (площади), где высота определяется более короткой перегородкой, а ширина - разностью индексов двух перегородок в массиве.
|
||||
|
||||
Требуется выбрать две перегородки так, чтобы образованный ими контейнер имел максимальную вместимость. Пример показан на рисунке ниже.
|
||||
|
||||

|
||||
|
||||
Контейнер образуется произвольными двумя перегородками, **поэтому состоянием задачи служит пара индексов этих перегородок, обозначим ее как $[i, j]$**.
|
||||
|
||||
Согласно условию, вместимость равна произведению высоты на ширину, где высота определяется короткой перегородкой, а ширина - разностью индексов двух перегородок. Обозначим вместимость через $cap[i, j]$, тогда формула принимает вид:
|
||||
|
||||
$$
|
||||
cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
|
||||
$$
|
||||
|
||||
Пусть длина массива равна $n$. Тогда число пар перегородок, то есть общее число состояний, равно $C_n^2 = \frac{n(n - 1)}{2}$. Самый прямолинейный подход - **перебрать все состояния**, после чего найти максимальную вместимость. Его временная сложность равна $O(n^2)$.
|
||||
|
||||
### Определение жадной стратегии
|
||||
|
||||
У этой задачи есть и более эффективное решение. Как показано на рисунке ниже, рассмотрим состояние $[i, j]$, где индексы удовлетворяют $i < j$, а высоты - условию $ht[i] < ht[j]$, то есть $i$ - короткая перегородка, а $j$ - длинная.
|
||||
|
||||

|
||||
|
||||
Как показано на рисунке ниже, **если в этот момент сдвинуть длинную перегородку $j$ ближе к короткой перегородке $i$, то вместимость обязательно уменьшится**.
|
||||
|
||||
Причина в том, что после смещения длинной перегородки $j$ ширина $j-i$ обязательно станет меньше, а высота определяется короткой перегородкой, поэтому высота либо останется прежней (если $i$ останется короткой перегородкой), либо уменьшится (если сдвинутая $j$ станет короткой перегородкой).
|
||||
|
||||

|
||||
|
||||
Рассуждая в обратную сторону, **только сдвигая короткую перегородку $i$ внутрь, мы можем получить шанс увеличить вместимость**. Хотя ширина при этом обязательно уменьшится, **высота может возрасти** (если после перемещения короткая перегородка $i$ станет выше). Например, на рисунке ниже после перемещения короткой перегородки площадь увеличивается.
|
||||
|
||||

|
||||
|
||||
Отсюда и выводится жадная стратегия для этой задачи: инициализировать два указателя по краям контейнера и на каждом шаге сдвигать внутрь указатель, соответствующий короткой перегородке, пока указатели не встретятся.
|
||||
|
||||
На рисунках ниже показан процесс выполнения этой жадной стратегии.
|
||||
|
||||
1. В начальном состоянии указатели $i$ и $j$ стоят на двух концах массива.
|
||||
2. Вычислить вместимость текущего состояния $cap[i, j]$ и обновить максимальную вместимость.
|
||||
3. Сравнить высоты перегородок $i$ и $j$, после чего сдвинуть короткую перегородку на одну позицию внутрь.
|
||||
4. Повторять шаги `2.` и `3.` до тех пор, пока $i$ и $j$ не встретятся.
|
||||
|
||||
=== "<1>"
|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
=== "<9>"
|
||||

|
||||
|
||||
### Код реализации
|
||||
|
||||
Цикл в коде выполняется не более $n$ раз, **поэтому временная сложность равна $O(n)$**.
|
||||
|
||||
Переменные $i$, $j$, $res$ используют дополнительную память постоянного размера, **поэтому пространственная сложность равна $O(1)$**.
|
||||
|
||||
```src
|
||||
[file]{max_capacity}-[class]{}-[func]{max_capacity}
|
||||
```
|
||||
|
||||
### Доказательство корректности
|
||||
|
||||
Жадный алгоритм быстрее полного перебора именно потому, что каждый жадный шаг «пропускает» часть состояний.
|
||||
|
||||
Например, в состоянии $cap[i, j]$ перегородка $i$ является короткой, а $j$ - длинной. Если жадно сдвинуть короткую перегородку $i$ на одну позицию внутрь, то состояния, показанные на рисунке ниже, будут «пропущены». **Это означает, что позже мы уже не сможем проверить вместимость этих состояний**.
|
||||
|
||||
$$
|
||||
cap[i, i+1], cap[i, i+2], \dots, cap[i, j-2], cap[i, j-1]
|
||||
$$
|
||||
|
||||

|
||||
|
||||
Нетрудно заметить, что **эти пропущенные состояния на самом деле и есть все состояния, в которых длинная перегородка $j$ сдвигается внутрь**. Ранее мы уже доказали, что перемещение длинной перегородки внутрь обязательно уменьшает вместимость. Иными словами, пропущенные состояния не могут быть оптимальным решением, **поэтому их пропуск не приводит к потере оптимума**.
|
||||
|
||||
Приведенный анализ показывает, что операция перемещения короткой перегородки является «безопасной», а жадная стратегия действительно эффективна.
|
||||
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1,85 @@
|
||||
# Задача о максимальном произведении разбиения
|
||||
|
||||
!!! question
|
||||
|
||||
Дан положительный целый $n$. Требуется разложить его в сумму как минимум двух положительных целых чисел и найти максимально возможное произведение всех полученных чисел, как показано на рисунке ниже.
|
||||
|
||||

|
||||
|
||||
Предположим, что мы разбили $n$ на $m$ целочисленных множителей, где $i$-й множитель обозначим через $n_i$, то есть
|
||||
|
||||
$$
|
||||
n = \sum_{i=1}^{m}n_i
|
||||
$$
|
||||
|
||||
Цель задачи - найти максимальное произведение всех целочисленных множителей, то есть
|
||||
|
||||
$$
|
||||
\max(\prod_{i=1}^{m}n_i)
|
||||
$$
|
||||
|
||||
Нужно понять: каким должно быть число частей $m$ и какими должны быть значения каждого $n_i$?
|
||||
|
||||
### Определение жадной стратегии
|
||||
|
||||
Из опыта известно, что произведение двух целых чисел часто больше их суммы. Предположим, что мы выделяем из $n$ множитель $2$, тогда произведение равно $2(n-2)$. Сравним это выражение с $n$:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
2(n-2) & \geq n \newline
|
||||
2n - n - 4 & \geq 0 \newline
|
||||
n & \geq 4
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
Как показано на рисунке ниже, когда $n \geq 4$, выделение множителя $2$ увеличивает произведение. **Это означает, что все целые числа, большие либо равные $4$, следует продолжать разбивать**.
|
||||
|
||||
**Жадная стратегия 1**: если в схеме разбиения присутствует множитель $\geq 4$, то его нужно дальше разбивать. В конечной схеме разбиения должны остаться только множители $1$, $2$, $3$.
|
||||
|
||||

|
||||
|
||||
Теперь подумаем, какой множитель является наилучшим. Среди $1$, $2$, $3$ очевидно худшим является $1$, потому что всегда выполняется $1 \times (n-1) < n$, то есть выделение $1$ уменьшает произведение.
|
||||
|
||||
Как показано на рисунке ниже, при $n = 6$ имеем $3 \times 3 > 2 \times 2 \times 2$. **Это означает, что выделять $3$ выгоднее, чем выделять $2$**.
|
||||
|
||||
**Жадная стратегия 2**: в схеме разбиения должно быть не более двух множителей $2$. Потому что три двойки всегда можно заменить двумя тройками и получить большее произведение.
|
||||
|
||||

|
||||
|
||||
Итак, получаем следующую жадную стратегию.
|
||||
|
||||
1. Для заданного целого $n$ непрерывно выделять из него множитель $3$, пока остаток не станет равным $0$, $1$ или $2$.
|
||||
2. Если остаток равен $0$, это означает, что $n$ кратно $3$, и больше ничего делать не нужно.
|
||||
3. Если остаток равен $2$, дальнейшее разбиение не требуется, его нужно сохранить.
|
||||
4. Если остаток равен $1$, то поскольку $2 \times 2 > 1 \times 3$, последний множитель $3$ следует заменить на $2$.
|
||||
|
||||
### Код реализации
|
||||
|
||||
Как показано на рисунке ниже, нам не нужен цикл, чтобы выполнять разбиение числа. Можно использовать целочисленное деление вниз, чтобы получить число троек $a$, и операцию взятия остатка, чтобы получить остаток $b$. Тогда имеем:
|
||||
|
||||
$$
|
||||
n = 3 a + b
|
||||
$$
|
||||
|
||||
Обратите внимание, что для граничного случая $n \leq 3$ необходимо выделить множитель $1$, и тогда произведение равно $1 \times (n - 1)$.
|
||||
|
||||
```src
|
||||
[file]{max_product_cutting}-[class]{}-[func]{max_product_cutting}
|
||||
```
|
||||
|
||||

|
||||
|
||||
**Временная сложность зависит от того, как в языке программирования реализовано возведение в степень**. Если взять Python, то обычно используются три распространенные функции для вычисления степени.
|
||||
|
||||
- Оператор `**` и функция `pow()` имеют временную сложность $O(\log a)$.
|
||||
- Функция `math.pow()` внутри вызывает функцию `pow()` из библиотеки C, выполняющую возведение в степень с плавающей точкой, и ее временная сложность равна $O(1)$.
|
||||
|
||||
Переменные $a$ и $b$ занимают дополнительную память постоянного размера, **поэтому пространственная сложность равна $O(1)$**.
|
||||
|
||||
### Доказательство корректности
|
||||
|
||||
Используем доказательство от противного и рассмотрим только случай $n \geq 4$.
|
||||
|
||||
1. **Все множители $\leq 3$**: предположим, что в оптимальной схеме разбиения существует множитель $x \geq 4$. Тогда его можно дальше разложить в $2(x-2)$ и получить большее или равное произведение. Это противоречит предположению.
|
||||
2. **Схема разбиения не содержит $1$**: предположим, что в оптимальной схеме присутствует множитель $1$. Тогда его можно объединить с другим множителем и получить большее произведение. Это противоречит предположению.
|
||||
3. **Схема разбиения содержит не более двух $2$**: предположим, что в оптимальной схеме присутствуют три двойки. Тогда их можно заменить двумя тройками и получить большее произведение. Это противоречит предположению.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Резюме
|
||||
|
||||
### Ключевые моменты
|
||||
|
||||
- Жадный алгоритм обычно используется для решения задач оптимизации. Его принцип состоит в том, чтобы на каждом этапе принятия решения делать локально оптимальный выбор в надежде получить глобально оптимальный ответ.
|
||||
- Жадный алгоритм итеративно делает один жадный выбор за другим, на каждом шаге превращая задачу в подзадачу меньшего размера, пока задача не будет полностью решена.
|
||||
- Жадный алгоритм не только прост в реализации, но и часто обладает высокой эффективностью. По сравнению с динамическим программированием его временная сложность обычно ниже.
|
||||
- В задаче о размене монет для некоторых наборов монет жадный алгоритм способен гарантировать оптимальный ответ, а для других наборов - нет: он может дать очень плохое решение.
|
||||
- Задачи, подходящие для жадного алгоритма, обладают двумя ключевыми свойствами: свойством жадного выбора и оптимальной подструктурой. Свойство жадного выбора отражает корректность жадной стратегии.
|
||||
- Для некоторых сложных задач доказать свойство жадного выбора непросто. Относительно легче найти контрпример и опровергнуть его, как это видно на примере задачи о размене монет.
|
||||
- Решение жадной задачи обычно состоит из трех шагов: анализ задачи, определение жадной стратегии и доказательство корректности. Из них ключевым является выбор жадной стратегии, а доказательство корректности часто оказывается самым трудным.
|
||||
- В задаче о дробном рюкзаке, в отличие от задачи о рюкзаке 0-1, разрешено брать часть предмета, поэтому ее можно решать жадным алгоритмом. Корректность жадной стратегии доказывается методом от противного.
|
||||
- Задачу о максимальной вместимости можно решать полным перебором со временной сложностью $O(n^2)$. Разработав жадную стратегию со сдвигом короткой перегородки внутрь на каждом шаге, временную сложность можно оптимизировать до $O(n)$.
|
||||
- В задаче о максимальном произведении разбиения мы последовательно выводим две жадные стратегии: все целые числа $\geq 4$ следует дальше разбивать, а оптимальным множителем разбиения является $3$. В коде присутствуют операции возведения в степень, поэтому временная сложность зависит от способа их реализации и обычно равна $O(1)$ или $O(\log n)$.
|
||||