mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-26 04:56:06 +00:00
build
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 4.6 Упражнения
|
||||
|
||||
## 4.6.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Как массив и связный список находят элемент
|
||||
|
||||
В массиве и односвязном списке по порядку хранятся `[A, B, C, D, E]`. Требуется прочитать 4-й элемент `D`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какой индекс можно напрямую использовать в массиве?
|
||||
2. Через какие узлы нужно последовательно пройти по `next` в односвязном списке, начиная с головного узла `A`?
|
||||
3. Как меняется число шагов в каждой структуре по мере удаления нужного элемента от начала? Какая структура лучше подходит для многократного доступа по позиции и почему?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Если индексация начинается с 0, индекс 4-го элемента равен 3, поэтому в массиве можно напрямую обратиться к `arr[3]`.
|
||||
|
||||
2. В односвязном списке нужно начинать с головы. Путь имеет вид `A → B → C → D`, то есть по `next` нужно перейти 3 раза.
|
||||
|
||||
3. Массив позволяет напрямую определить положение элемента по адресу начала и индексу, поэтому временная сложность доступа по позиции равна $O(1)$.
|
||||
Чтобы обратиться к $k$-му узлу односвязного списка, нужно начать с головного узла и перейти по `next` $k-1$ раз,
|
||||
что в худшем случае требует $O(n)$ времени.
|
||||
|
||||
Здесь сравнивается только доступ по позиции; это не означает, что связный список медленнее при всех операциях.
|
||||
|
||||
### 2. Как вставить элемент в массив и связный список
|
||||
|
||||
В массиве и односвязном списке хранятся `A, B, C, D`. Требуется вставить `X` после `B`.
|
||||
|
||||
- емкость массива равна 5, его текущее состояние — `[A, B, C, D, _]`;
|
||||
- связный список имеет вид `A → B → C → D`, и ссылка на узел `B` уже получена.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какие элементы массива нужно переместить? Запишите массив после вставки.
|
||||
2. В каком порядке следует изменить `X.next` и `B.next`? Запишите связный список после вставки.
|
||||
3. Почему при сравнении эффективности вставки важно уточнить, что «ссылка на узел B уже получена»?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. В массиве сначала нужно сдвинуть `D` на одну позицию вправо, затем так же сдвинуть `C` и, наконец, поместить `X` по индексу 2.
|
||||
Получится `[A, B, X, C, D]`.
|
||||
|
||||
2. Изначально `B.next` указывает на `C`. Сначала следует выполнить `X.next = B.next`, чтобы `X` указывал на `C`,
|
||||
а затем `B.next = X`. Получится `A → B → X → C → D`.
|
||||
Если сначала перезаписать `B.next`, не сохранив прежнюю связь, узел `C` можно потерять.
|
||||
|
||||
3. Если положение `B` известно, для вставки в связный список достаточно изменить две связи, что занимает $O(1)$ времени.
|
||||
Если же `B` еще нужно искать от головы списка, сам поиск может потребовать $O(n)$ времени.
|
||||
|
||||
### 3. Как растет емкость списка
|
||||
|
||||
Список на основе массива сейчас содержит `[A, B, C]`, его длина `size = 3`, а емкость `capacity = 4`.
|
||||
При нехватке места емкость нового массива увеличивается в 2 раза.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Чему будут равны длина и емкость списка после добавления `D`? Потребуется ли увеличить емкость?
|
||||
2. Затем добавляют `E`. Какой станет емкость и сколько прежних элементов потребуется скопировать?
|
||||
3. Длина базового массива неизменна. Почему тогда кажется, что емкость списка может расти?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. `D` можно поместить в последнюю свободную ячейку. Теперь содержимое равно `[A, B, C, D]`,
|
||||
`size = 4`, `capacity = 4`, и увеличивать емкость не требуется.
|
||||
|
||||
2. Для `E` свободного места уже нет, поэтому нужно создать новый массив емкостью 8,
|
||||
скопировать в него 4 прежних элемента `A, B, C, D`, а затем добавить `E`.
|
||||
После этого `size = 5`, `capacity = 8`.
|
||||
|
||||
3. Исходный массив сам по себе не удлиняется. Список создает новый массив большего размера, копирует прежние элементы
|
||||
и затем использует новый массив как базовое хранилище. Поэтому для пользователя емкость выглядит увеличившейся.
|
||||
|
||||
## 4.6.2 Задачи по программированию
|
||||
|
||||
### 1. Прибавление единицы к большому числу в массиве
|
||||
|
||||
Массив `digits` хранит слева направо цифры неотрицательного целого числа. Например, `[3, 0, 8]` обозначает 308.
|
||||
Число 0 представлено как `[0]`; у всех остальных входных данных первая цифра не равна 0.
|
||||
|
||||
Смоделируйте сложение столбиком в десятичной системе: увеличьте это число на 1 и верните результат в том же формате массива.
|
||||
Массив `digits` можно изменять непосредственно. Если перед первой цифрой появляется новый перенос, можно вернуть более длинный массив.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Как при сложении столбиком, начните с последней цифры массива
|
||||
2. Если текущая цифра меньше 9, увеличьте ее на один и сразу верните результат
|
||||
3. Если текущая цифра равна 9, замените ее на 0; если все цифры равны 9, добавьте 1 в начало
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/plus-one/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. Разворот односвязного списка
|
||||
|
||||
Дан головной узел `head` односвязного списка. Каждый узел содержит значение и поле `next`, указывающее на следующий узел.
|
||||
|
||||
Итеративно разверните все связи между узлами и верните новый головной узел.
|
||||
Не создавайте новые узлы связного списка.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Нарисуйте на бумаге три связанных узла и два указателя prev и cur
|
||||
2. Прежде чем изменить cur.next, сохраните исходный следующий узел в nxt
|
||||
3. Развернув cur.next, выполните prev = cur, затем cur = nxt и продолжайте с очередным узлом исходного списка
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/reverse-linked-list/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/view-list-outline
|
||||
- [4.3 Список](list.md)
|
||||
- [4.4 Оперативная память и кэш *](ram_and_cache.md)
|
||||
- [4.5 Резюме](summary.md)
|
||||
- [4.6 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 13.6 Упражнения
|
||||
|
||||
## 13.6.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Пропустит ли этот алгоритм некоторые перестановки
|
||||
|
||||
Алгоритм поиска с возвратом пытается построить все перестановки, перебирая числа в порядке `1, 2, 3`. Выбрав число `x`, он:
|
||||
|
||||
1. добавляет `x` в конец текущего пути;
|
||||
2. помечает `x` как «использованное»;
|
||||
3. рекурсивно заполняет следующую позицию.
|
||||
|
||||
После возврата из рекурсии ученик удалил `x` только из конца пути и перешел к следующему числу.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какую перестановку алгоритм получит первой? Сможет ли он получить все 6 перестановок?
|
||||
2. Достаточно ли перед возвратом на предыдущий уровень удалить число из конца пути? Если нет, что еще нужно сделать и почему?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Первой получится перестановка `[1, 2, 3]`, но алгоритм не сможет получить все перестановки. Хотя после возврата путь становится короче,
|
||||
числа 1, 2 и 3 по-прежнему помечены как «использованные», поэтому в последующих ветвях не останется доступных чисел.
|
||||
|
||||
2. Нет. После удаления `x` из конца пути нужно также снова пометить `x` как «неиспользованное».
|
||||
Текущий путь и отметки «использовано» вместе описывают состояние поиска. При выборе изменяются обе части состояния, поэтому при возврате нужно восстановить обе,
|
||||
чтобы другие ветви могли снова выбрать `x`.
|
||||
|
||||
### 2. Важен ли порядок выбора чисел
|
||||
|
||||
Дан упорядоченный массив `[2, 3, 5]` и целевое значение 5. Каждое число разрешено выбирать повторно.
|
||||
Алгоритм требует, чтобы числа в каждом пути поиска располагались только в неубывающем порядке.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какие различные комбинации можно получить?
|
||||
2. Почему один и тот же набор чисел не нужно повторно перебирать в разном порядке? Какую роль играет ограничение «по неубыванию»?
|
||||
3. Текущий путь равен `[3]`, до цели не хватает 2, а следующий возможный элемент равен 3. Почему теперь можно прекратить проверку всех следующих элементов на этом уровне?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Различные комбинации: `[2, 3]` и `[5]`.
|
||||
|
||||
2. В этой задаче `[2, 3]` и `[3, 2]` считаются одной комбинацией: порядок выбора чисел не влияет на ответ.
|
||||
Требование располагать числа в пути по неубыванию позволяет сразу исключить из поиска повторные комбинации наподобие `[3, 2]`.
|
||||
|
||||
3. До цели осталось 2, а возможный элемент 3 уже больше 2. Поскольку массив упорядочен,
|
||||
все последующие элементы будут еще больше и также не смогут войти в текущую комбинацию. Поэтому проверку этого уровня можно сразу завершить.
|
||||
|
||||
### 3. Куда можно поставить следующего ферзя
|
||||
|
||||
Ферзи расставляются по строкам на доске `4 × 4`; индексы строк и столбцов начинаются с 0.
|
||||
Ферзи уже стоят на позициях `(0, 1)` и `(1, 3)`. Следующего ферзя нужно поставить в строке 2.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какие столбцы исключаются из-за того, что в них уже есть ферзь?
|
||||
2. Какие из оставшихся позиций исключаются из-за расположения на одной диагонали с другим ферзем?
|
||||
3. Какие позиции в строке 2 еще можно попробовать?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. В столбцах 1 и 3 уже стоят ферзи, поэтому позиции `(2, 1)` и `(2, 3)` исключаются.
|
||||
|
||||
2. Среди оставшихся позиций `(2, 2)` находится на одной диагонали с `(1, 3)`, поэтому ее также нужно исключить.
|
||||
Позиция `(2, 0)` не совпадает по столбцу ни с одним из уже поставленных ферзей и не лежит с ними на одной диагонали.
|
||||
|
||||
3. В строке 2 можно попробовать только позицию `(2, 0)`.
|
||||
|
||||
Этот шаг показывает лишь, что текущее расположение допустимо. Если завершить доску затем не удастся, потребуется вернуться и попробовать другие, более ранние варианты.
|
||||
|
||||
## 13.6.2 Задачи по программированию
|
||||
|
||||
### 1. Все перестановки различных элементов
|
||||
|
||||
Массив целых чисел `nums` содержит хотя бы один элемент, и все его элементы попарно различны.
|
||||
Перечислите все возможные порядки, в которых каждый элемент используется ровно один раз, и верните каждый порядок как отдельный массив.
|
||||
Порядок самих перестановок в результате не имеет значения.
|
||||
Используйте поиск с возвратом и булев массив, отмечающий, выбран ли элемент из каждой позиции входного массива в текущую перестановку.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Глубина рекурсии показывает, какую по счету позицию перестановки нужно заполнить
|
||||
2. На каждом уровне пробуйте только еще не использованные элементы
|
||||
3. Когда длина пути станет равна длине `nums`, добавьте в ответ копию пути
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/permutations/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/map-marker-path
|
||||
- [13.3 Задача о сумме подмножеств](subset_sum_problem.md)
|
||||
- [13.4 Задача о n ферзях](n_queens_problem.md)
|
||||
- [13.5 Резюме](summary.md)
|
||||
- [13.6 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 2.6 Упражнения
|
||||
|
||||
## 2.6.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Временная и пространственная сложность итерации и рекурсии
|
||||
|
||||
Обе функции ниже вычисляют $1 + 2 + \dots + n$ (считайте, что $n \ge 1$). Присвойте `n` значение 4,
|
||||
ответьте на вопросы в порядке фактического выполнения программы, а затем сравните эффективность двух вариантов.
|
||||
|
||||
```python
|
||||
def sum_iter(n):
|
||||
s = 0
|
||||
for i in range(1, n + 1):
|
||||
s += i
|
||||
return s
|
||||
|
||||
def sum_recur(n):
|
||||
if n == 1:
|
||||
return 1
|
||||
return n + sum_recur(n - 1)
|
||||
```
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какие значения принимает переменная `s` после каждой итерации при выполнении `sum_iter(4)`?
|
||||
2. Какие функции поочередно вызываются при выполнении `sum_recur(4)`? Как формируется результат при возврате, начиная с самого глубокого вызова?
|
||||
3. Каковы временная и пространственная сложности каждого варианта? Обоснуйте ответ, опираясь на процессы выполнения из вопросов 1 и 2.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Переменная цикла `i` поочередно принимает значения `1, 2, 3, 4`, а `s` после каждой итерации становится равной
|
||||
`1, 3, 6, 10`. Поэтому `sum_iter(4)` возвращает 10.
|
||||
|
||||
2. Функции вызываются в следующем порядке:
|
||||
`sum_recur(4) → sum_recur(3) → sum_recur(2) → sum_recur(1)`.
|
||||
`sum_recur(1)` возвращает 1, после чего следующие уровни поочередно получают `2 + 1 = 3`, `3 + 3 = 6` и `4 + 6 = 10`.
|
||||
В самой глубокой точке все 4 вызова функции еще не завершены.
|
||||
|
||||
3. В обеих функциях число итераций или вызовов пропорционально $n$, поэтому их временная сложность равна $O(n)$.
|
||||
Пространственная сложность различается: итеративный вариант использует постоянное число переменных, поэтому она равна $O(1)$;
|
||||
в рекурсивном варианте предыдущие вызовы ожидают возврата результата до достижения условия завершения, поэтому в стеке вызовов одновременно хранится до $n$ вызовов.
|
||||
Его пространственная сложность равна $O(n)$.
|
||||
|
||||
При анализе пространственной сложности нужно учитывать не только переменные в коде, но и память, занимаемую рекурсивными вызовами.
|
||||
|
||||
### 2. Временная сложность трех фрагментов кода
|
||||
|
||||
Во всех трех фрагментах кода входные данные — положительное целое число $n$. Расположите фрагменты в порядке возрастания временной сложности и укажите сложность каждого из них.
|
||||
|
||||
```python
|
||||
# Фрагмент 1
|
||||
s = 0
|
||||
for i in range(n):
|
||||
s += i
|
||||
|
||||
# Фрагмент 2
|
||||
s = 0
|
||||
for i in range(n):
|
||||
for j in range(i, n):
|
||||
s += j
|
||||
|
||||
# Фрагмент 3
|
||||
while n > 1:
|
||||
n = n // 2
|
||||
```
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
Порядок от меньшей сложности к большей: фрагмент 3 — $O(\log n)$, фрагмент 1 — $O(n)$, фрагмент 2 — $O(n^2)$.
|
||||
В фрагменте 3 на каждой итерации $n$ уменьшается вдвое, поэтому цикл выполняется примерно $\log_2 n$ раз.
|
||||
Цикл во фрагменте 1 выполняется ровно $n$ раз. Число итераций внутреннего цикла во фрагменте 2 последовательно равно
|
||||
$n,n-1,\dots,1$, а их сумма — $n(n+1)/2$, поэтому сложность квадратичная.
|
||||
|
||||
### 3. Какой способ разворота экономнее по памяти
|
||||
|
||||
Все элементы массива `nums` можно расположить в обратном порядке двумя способами:
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. создать новый массив `res` той же длины, скопировать в него элементы в обратном порядке и вернуть его;
|
||||
2. задать два индекса `i` и `j`, перемещать их от начала и конца массива к середине и попарно менять местами `nums[i]` и `nums[j]`.
|
||||
|
||||
Какова пространственная сложность каждого способа? Какой из них выполняет операцию «на месте»?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Требуется вспомогательный массив той же длины, что и входной, поэтому пространственная сложность равна $O(n)$.
|
||||
|
||||
2. Используются только две индексные переменные,
|
||||
поэтому пространственная сложность равна $O(1)$, а операция выполняется на месте.
|
||||
|
||||
Обратите внимание: разворот на месте изменяет входной массив,
|
||||
поэтому его следует предпочитать только тогда, когда менять входные данные разрешено. Если исходный массив нужно сохранить, затрат на копирование в способе 1 не избежать.
|
||||
|
||||
## 2.6.2 Задачи по программированию
|
||||
|
||||
### 1. Число Фибоначчи
|
||||
|
||||
Последовательность Фибоначчи задается так: $F(0)=0$, $F(1)=1$, а при $n\ge2$
|
||||
выполняется $F(n)=F(n-1)+F(n-2)$.
|
||||
|
||||
Дано неотрицательное целое число `n`. Вычислите и верните $F(n)$ с помощью цикла, не используя рекурсию.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Сначала отдельно обработайте случаи, когда n равно 0 или 1
|
||||
2. Для вычисления следующего члена нужны только два предыдущих, поэтому хранить всю последовательность не требуется
|
||||
3. Обновляя две переменные, не перезапишите слишком рано старое значение, которое еще понадобится
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/fibonacci-number/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/timer-sand
|
||||
- [2.3 Временная сложность](time_complexity.md)
|
||||
- [2.4 Пространственная сложность](space_complexity.md)
|
||||
- [2.5 Резюме](summary.md)
|
||||
- [2.6 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 3.6 Упражнения
|
||||
|
||||
## 3.6.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Связи между данными в повседневных ситуациях
|
||||
|
||||
Для каждого из трех примеров выберите по характеру связей между данными один вариант: «линейная структура», «древовидная структура» или «сетевая структура». Объясните свой выбор.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Ученики стоят в ряд, и каждого интересуют только стоящие непосредственно перед ним и за ним;
|
||||
2. школа организована по уровням «школа → параллель → класс»;
|
||||
3. городские дороги соединяют множество перекрестков: из одного перекрестка можно попасть в несколько других, а дороги могут образовывать циклы.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Это линейная структура. Кроме первого и последнего в ряду, каждый ученик соседствует ровно с одним человеком впереди и одним сзади, а связи вытянуты в одну линию.
|
||||
|
||||
2. Это древовидная структура. Каждый класс относится к одной параллели, а каждая параллель — к школе; связи образуют уровни сверху вниз.
|
||||
|
||||
3. Это сетевая структура. Один перекресток может быть связан с несколькими другими, а дороги могут образовывать циклы, поэтому связи нельзя выстроить в единую последовательность или строгую иерархию.
|
||||
|
||||
Определяя структуру, сначала рассматривайте связи между элементами, а не объем памяти, который они занимают.
|
||||
|
||||
### 2. Как сохранить логический порядок в памяти
|
||||
|
||||
Логический порядок `A → B → C` можно сохранить в памяти двумя упрощенными способами:
|
||||
|
||||
- вариант 1: `A, B, C` размещены в ячейках памяти с номерами `20, 21, 22` соответственно;
|
||||
- вариант 2: `A, B, C` размещены в ячейках с номерами `20, 7, 31` соответственно, причем `A` хранит расположение `B`, а `B` — расположение `C`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какой вариант использует хранение в непрерывном, а какой — в разрозненном пространстве памяти?
|
||||
2. Какой вариант ближе к массиву, а какой — к связному списку?
|
||||
3. Почему вариант 2 по-прежнему задает логический порядок `A → B → C`, хотя номера ячеек памяти не упорядочены по величине?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Вариант 1 использует соседние ячейки памяти, поэтому хранение происходит в непрерывном пространстве. Узлы в варианте 2 находятся в разных местах, поэтому они хранятся в разрозненном пространстве.
|
||||
|
||||
2. Вариант 1 ближе к массиву, а вариант 2 — к связному списку.
|
||||
|
||||
3. Логический порядок задается связями, записанными между узлами, а не величиной номеров ячеек памяти.
|
||||
По адресу, который хранит `A`, можно найти `B`, а затем по адресу из `B` найти `C`, поэтому элементы по-прежнему доступны в порядке `A, B, C`.
|
||||
|
||||
Это также показывает, что логическая и физическая структуры — два разных взгляда на одни и те же данные.
|
||||
|
||||
### 3. Типы данных и структура записей о домашних заданиях
|
||||
|
||||
Учебная группа записала в порядке рассадки, сдали ли домашнее задание 4 ученика:
|
||||
|
||||
`[true, false, true, true]`
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какой базовый тип данных подходит для каждого элемента?
|
||||
2. Какую логическую структуру образуют эти 4 элемента, расположенные в ряд по порядку рассадки?
|
||||
3. Если затем записывать оценки за задание каждого ученика как `[90, 0, 85, 100]`, изменится «тип содержимого» или «способ организации» данных? Объясните ответ.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Каждый элемент выражает только «да» или «нет», поэтому подходит логический тип `bool`.
|
||||
|
||||
2. Элементы расположены по порядку рассадки и образуют линейную структуру, которую можно хранить в массиве.
|
||||
|
||||
3. Изменится тип содержимого: логические значения будут заменены целыми числами. Способ организации не изменится:
|
||||
данные по-прежнему расположены в ряд по порядку рассадки и могут храниться в массиве как линейная структура.
|
||||
|
||||
Базовый тип данных описывает, «что хранится», а структура данных — «как организованы данные».
|
||||
|
||||
## 3.6.2 Задачи по программированию
|
||||
|
||||
### 1. Число единиц в двоичной записи
|
||||
|
||||
Дано неотрицательное целое число `n`. Подсчитайте количество единиц в его двоичной записи.
|
||||
|
||||
Используйте побитовые операции. Не преобразовывайте двоичную запись в строку и не применяйте встроенную функцию, которая напрямую подсчитывает единицы.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Выражение n & 1 извлекает крайний правый бит n и позволяет проверить, равен ли он 1
|
||||
2. Сдвиг вправо на один разряд отбрасывает текущий крайний правый бит; в большинстве языков для этого используется оператор >>
|
||||
3. Реализовав проверку каждого бита со сдвигом вправо, обратите внимание: n & (n - 1) превращает в 0 крайнюю правую единицу в n
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/number-of-1-bits/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/shape-outline
|
||||
- [3.3 Кодирование чисел *](number_encoding.md)
|
||||
- [3.4 Кодирование символов *](character_encoding.md)
|
||||
- [3.5 Резюме](summary.md)
|
||||
- [3.6 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 12.6 Упражнения
|
||||
|
||||
## 12.6.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Какие задачи подходят для метода «разделяй и властвуй»
|
||||
|
||||
Ученик хочет решить следующие задачи способом «разделить на две половины, решить каждую отдельно, затем объединить результаты».
|
||||
Для каждой задачи выберите одно из утверждений: «подходит для метода “разделяй и властвуй”», «разделение возможно, но не уменьшает общий объем работы» или «половины нельзя решить независимо». Объясните свой выбор.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Отсортировать неупорядоченный массив;
|
||||
2. найти максимальный элемент массива;
|
||||
3. последовательно выполнить набор операций стека `push(x)` и `pop()`, выводя элемент, полученный при каждой операции `pop()`.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Подходит: массив делится пополам, обе половины независимо сортируются, а затем сливаются за $O(n)$ — именно так работает сортировка слиянием.
|
||||
2. Разделение возможно, но оно не уменьшит общий объем работы: в обеих половинах в сумме все равно нужно проверить все $n$ элементов,
|
||||
поэтому, как и при прямом обходе, потребуется $O(n)$ времени.
|
||||
3. Половины нельзя решить независимо: содержимое стека в начале второй половины операций зависит от результата первой,
|
||||
поэтому решить их независимо, не зная результатов друг друга, невозможно.
|
||||
|
||||
### 2. Как быстрое возведение в степень уменьшает число вычислений
|
||||
|
||||
Рекурсивная функция ниже вычисляет $x^n$ методом «разделяй и властвуй»:
|
||||
|
||||
```python
|
||||
def fast_pow(x, n):
|
||||
if n == 0:
|
||||
return 1
|
||||
half = fast_pow(x, n // 2)
|
||||
if n % 2 == 0:
|
||||
return half * half
|
||||
return half * half * x
|
||||
```
|
||||
|
||||
Функция вызывается как `fast_pow(3, 5)`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какие значения последовательно принимает параметр `n` при рекурсивных вызовах?
|
||||
2. Какие значения поочередно возвращаются на каждом уровне, начиная с самого глубокого?
|
||||
3. Почему результат нужно сначала сохранить в `half`, а не дважды записывать `fast_pow(x, n // 2)`?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Параметр последовательно принимает значения `5 → 2 → 1 → 0`: на каждом шаге показатель степени уменьшается вдвое до достижения условия завершения.
|
||||
|
||||
2. При `n = 0` возвращается 1; при `n = 1` возвращается $1×1×3=3$;
|
||||
при `n = 2` — $3×3=9$; при `n = 5` — $9×9×3=243$.
|
||||
|
||||
3. Если записать `fast_pow(x, n // 2)` по обе стороны умножения, два рекурсивных вызова будут решать одну и ту же подзадачу.
|
||||
Когда результат сохраняется в `half`, на каждом уровне выполняется только один рекурсивный вызов, а глубина рекурсии составляет примерно $\log n$;
|
||||
два вызова привели бы к множеству повторных вычислений.
|
||||
|
||||
### 3. Разделение левого и правого поддеревьев по последовательностям обхода
|
||||
|
||||
Двоичное дерево без повторяющихся узлов имеет следующие последовательности прямого и симметричного обходов:
|
||||
|
||||
- прямой обход: `[A, B, D, E, C]`
|
||||
- симметричный обход: `[D, B, E, A, C]`
|
||||
|
||||
Выполните только разделение на уровне корневого узла: продолжать рекурсию и рисовать все дерево не требуется.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какой узел является корневым?
|
||||
2. Какие участки симметричного обхода соответствуют левому и правому поддеревьям?
|
||||
3. Какие участки прямого обхода соответствуют левому и правому поддеревьям? Какие непосредственные дочерние узлы имеет корень?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Первый узел прямого обхода является корневым, поэтому корень — `A`.
|
||||
|
||||
2. Узел `A` делит симметричный обход на две части: левому поддереву соответствует `[D, B, E]`, правому — `[C]`.
|
||||
|
||||
3. Левое поддерево содержит 3 узла, поэтому следующие после корня `A` 3 элемента прямого обхода относятся к левому поддереву:
|
||||
`[B, D, E]`. Оставшийся участок `[C]` относится к правому поддереву.
|
||||
Следовательно, левый дочерний узел корня — `B`, а правый — `C`.
|
||||
|
||||
## 12.6.2 Задачи по программированию
|
||||
|
||||
### 1. Быстрое возведение в степень
|
||||
|
||||
Даны вещественное число `x` и целое число `n`. Вычислите $x^n$, не используя встроенную функцию возведения в степень.
|
||||
Примените рекурсивный метод «разделяй и властвуй»: каждый раз уменьшайте показатель степени вдвое и повторно используйте уже вычисленный результат подзадачи.
|
||||
В этой задаче $x^0=1$, в том числе при `x = 0`; если `n < 0`, гарантируется, что `x != 0`, а ответ можно преобразовать к $(1/x)^{-n}$.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. При n, равном 0, ответ равен 1
|
||||
2. Вычислив x в степени n // 2, сохраните результат в half и не выполняйте рекурсивный вызов второй раз
|
||||
3. Если n < 0, сначала замените x на 1 / x, а n — на -n; в C++ или Java предварительно преобразуйте n в 64-битное целое число, чтобы избежать переполнения при смене знака у минимального 32-битного целого
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/powx-n/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -20,3 +20,4 @@ icon: material/set-split
|
||||
- [12.3 Задача построения двоичного дерева](build_binary_tree_problem.md)
|
||||
- [12.4 Задача о Ханойской башне](hanota_problem.md)
|
||||
- [12.5 Резюме](summary.md)
|
||||
- [12.6 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 14.8 Упражнения
|
||||
|
||||
## 14.8.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Когда подходит динамическое программирование
|
||||
|
||||
Ученик утверждает: «Если можно записать рекуррентное соотношение, нужно использовать динамическое программирование».
|
||||
Для каждой из трех задач выберите наиболее подходящий из трех вариантов: динамическое программирование; поиск с возвратом; цикл или математическая формула без таблицы `dp`. Укажите одну основную причину.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Набрать сумму 6 с помощью монет номиналов `[1, 3, 4]`, используя каждый номинал любое число раз, и найти минимальное число монет;
|
||||
2. вывести все перестановки `[1, 2, 3]`;
|
||||
3. вычислить $1 + 2 + \dots + n$.
|
||||
|
||||
Для задачи, которой подходит динамическое программирование, также объясните значение `dp[i]`.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Подходит динамическое программирование. Пусть `dp[i]` — минимальное число монет, необходимое для получения суммы `i`.
|
||||
Для каждой монеты `c`, не превосходящей `i`, значение `dp[i-c] + 1` можно рассматривать как возможный ответ,
|
||||
а затем выбрать минимум среди всех вариантов. Разные последовательности выбора многократно приводят к одной и той же сумме, а оптимальное решение для большей суммы можно составить из оптимальных решений для меньших сумм.
|
||||
Для суммы 6 ответ равен 2: `3 + 3`.
|
||||
|
||||
2. Следует использовать поиск с возвратом. Задача требует поочередно получить все 6 перестановок; поиск с возвратом позволяет систематически сделать выбор, продолжить поиск,
|
||||
затем отменить выбор и перейти к другой ветви. Каким бы ни был метод, при фактическом выводе всех перестановок пропустить их перебор нельзя.
|
||||
|
||||
3. Достаточно цикла или формулы суммы арифметической прогрессии. Хотя можно записать `S(i) = S(i-1) + i`, при вычислении `S(i)` требуется только одно меньшее значение `S(i-1)`,
|
||||
а каждая частичная сумма вычисляется один раз. Повторяющихся подзадач нет, поэтому таблица `dp` не нужна. Возможность записать рекуррентное соотношение не означает необходимости динамического программирования.
|
||||
|
||||
### 2. Как вычислить одну ячейку таблицы рюкзака
|
||||
|
||||
Задача о рюкзаке 0-1: веса предметов `wgt = [1, 2, 3]`, их стоимости `val = [5, 11, 15]`, вместимость рюкзака равна 4.
|
||||
`dp[i][c]` — максимальная стоимость, которую можно получить, рассматривая только первые $i$ предметов при предельной вместимости рюкзака $c$;
|
||||
заполнять рюкзак точно до предела не требуется.
|
||||
|
||||
Вычислите только состояние `dp[3][4]`. Известно, что `dp[2][4] = 16` и `dp[2][1] = 5`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какова возможная стоимость, если не брать 3-й предмет?
|
||||
2. Какая вместимость останется, если взять 3-й предмет, и чему равна возможная стоимость?
|
||||
3. Какое значение нужно присвоить `dp[3][4]`? Какие предметы ему соответствуют?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Если не брать 3-й предмет, сохраняется результат для первых двух предметов, поэтому возможная стоимость равна `dp[2][4] = 16`.
|
||||
|
||||
2. Вес 3-го предмета равен 3, после его добавления остается вместимость $4-3=1$. Возможная стоимость равна
|
||||
`dp[2][1] + 15 = 5 + 15 = 20`.
|
||||
|
||||
3. Сравнив 16 и 20, получаем `dp[3][4] = 20`. Это соответствует выбору 1-го и 3-го предметов:
|
||||
их общий вес равен $1+3=4$, а общая стоимость — $5+15=20$.
|
||||
|
||||
Вычисление этого состояния показывает один выбор в задаче о рюкзаке 0-1: «взять или не взять».
|
||||
|
||||
### 3. В каком порядке обновлять вместимость рюкзака
|
||||
|
||||
В задаче о рюкзаке 0-1 есть только один предмет весом 2 и стоимостью 5; вместимость рюкзака равна 4.
|
||||
Каждый предмет разрешено выбрать не более одного раза, а исходный одномерный массив имеет вид `dp = [0, 0, 0, 0, 0]`.
|
||||
|
||||
Обрабатывая предмет, ученик обновляет вместимость от 2 до 4:
|
||||
|
||||
- после обновления `dp[2]` получается 5;
|
||||
- после обновления `dp[3]` также получается 5;
|
||||
- при обновлении `dp[4]` снова используется только что полученное `dp[2]`, поэтому выходит `dp[4] = 10`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Правилен ли результат `dp[4] = 10`? Почему?
|
||||
2. Чему должно быть равно `dp[4]`, если каждый предмет можно выбрать не более одного раза?
|
||||
3. При обработке каждого предмета вместимость нужно перебирать от большей к меньшей или от меньшей к большей? Какую проблему предотвращает такой порядок?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Результат неверен. Стоимость 10 означает, что предмет стоимостью 5 положили в рюкзак дважды,
|
||||
нарушив условие «каждый предмет можно выбрать не более одного раза».
|
||||
|
||||
2. В рюкзак можно положить не более одного такого предмета, поэтому правильное значение `dp[4]` равно 5.
|
||||
|
||||
3. Вместимость нужно обновлять от большей к меньшей, то есть в порядке 4, 3, 2.
|
||||
Тогда при вычислении `dp[c]` читаемое значение `dp[c-2]` все еще относится к состоянию до обработки текущего предмета,
|
||||
и этот предмет нельзя повторно использовать на той же итерации.
|
||||
|
||||
## 14.8.2 Задачи по программированию
|
||||
|
||||
### 1. Число способов подняться по лестнице
|
||||
|
||||
Лестница состоит из `n` ступеней. За один шаг можно подняться только на 1 или 2 ступени, причем нужно попасть точно на ступень `n`.
|
||||
Вычислите число различных способов подъема. Считайте, что `n >= 1`; способы различаются только последовательностью шагов на 1 и 2 ступени.
|
||||
Используйте одномерный массив динамического программирования; пока не применяйте оптимизацию памяти, сохраняющую лишь два состояния.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Последний шаг на ступень i мог быть только на 1 или на 2 ступени
|
||||
2. Поэтому dp[i] = dp[i-1] + dp[i-2]
|
||||
3. Сначала обработайте случаи n, равного 1 и 2, а затем заполняйте таблицу начиная с 3-й ступени
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/climbing-stairs/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. Рюкзак 0-1
|
||||
|
||||
Даны массивы одинаковой длины `wgt` и `val`: вес предмета `i` — положительное целое число `wgt[i]`, а его стоимость — неотрицательное целое число `val[i]`.
|
||||
Вместимость рюкзака `cap` — неотрицательное целое число. Каждый предмет можно выбрать не более одного раза. Найдите максимальную суммарную стоимость предметов,
|
||||
общий вес которых не превышает `cap`. Используйте одномерное динамическое программирование.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Создайте массив dp длины cap + 1, где dp[c] — максимальная стоимость при предельной вместимости c
|
||||
2. Обрабатывая предмет i, сравните dp[c] для варианта «не брать предмет» и dp[c-wgt[i]] + val[i] для варианта «взять предмет»
|
||||
3. Вместимость нужно обновлять от большей к меньшей, чтобы не выбрать текущий предмет повторно на одной итерации
|
||||
@@ -22,3 +22,4 @@ icon: material/table-pivot
|
||||
- [14.5 Задача о полном рюкзаке](unbounded_knapsack_problem.md)
|
||||
- [14.6 Задача о расстоянии редактирования](edit_distance_problem.md)
|
||||
- [14.7 Резюме](summary.md)
|
||||
- [14.8 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 9.5 Упражнения
|
||||
|
||||
## 9.5.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Два представления одного графа
|
||||
|
||||
Неориентированный граф содержит 4 вершины `A, B, C, D` и ребра
|
||||
`A-B, A-C, B-C, C-D`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Запишите его список смежности.
|
||||
2. Заполните матрицу смежности, содержащую только 0 и 1.
|
||||
3. Какое представление графа позволяет определить, соединены ли напрямую `A` и `D`, просмотрев лишь один элемент хранилища?
|
||||
4. Какое представление обычно экономнее по памяти, если в графе много вершин, но мало ребер?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Список смежности:
|
||||
|
||||
```text
|
||||
A: B, C
|
||||
B: A, C
|
||||
C: A, B, D
|
||||
D: C
|
||||
```
|
||||
|
||||
2. Матрица смежности:
|
||||
|
||||
| | A | B | C | D |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| A | 0 | 1 | 1 | 0 |
|
||||
| B | 1 | 0 | 1 | 0 |
|
||||
| C | 1 | 1 | 0 | 1 |
|
||||
| D | 0 | 0 | 1 | 0 |
|
||||
|
||||
3. В матрице смежности достаточно посмотреть ячейку на пересечении строки `A` и столбца `D`, поэтому она удобна для проверки прямой связи любой пары вершин.
|
||||
|
||||
4. Если вершин много, а ребер мало, список смежности хранит только существующие ребра и обычно занимает меньше памяти, чем матрица, в которой место отводится каждой паре вершин.
|
||||
|
||||
### 2. Порядок обхода в ширину и в глубину
|
||||
|
||||
Неориентированный граф содержит вершины `A, B, C, D, E` и ребра
|
||||
`A-B, A-C, B-D, C-D, D-E`.
|
||||
|
||||
Начните с A и при наличии нескольких непосещенных смежных вершин выбирайте их в алфавитном порядке.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Запишите порядок обхода в ширину (BFS).
|
||||
2. Запишите порядок рекурсивного обхода в глубину (DFS).
|
||||
3. Почему при обоих обходах необходимо отмечать уже посещенные вершины?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Порядок BFS: `A, B, C, D, E`. Сначала посещаются B и C, отстоящие от A на одно ребро,
|
||||
а затем более далекие D и E.
|
||||
|
||||
2. Порядок DFS: `A, B, D, C, E`. Обход последовательно переходит в непосещенную смежную вершину текущей вершины,
|
||||
поэтому сначала проходит путь `A → B → D → C`. Когда у C не остается новых смежных вершин, обход возвращается к D и посещает E.
|
||||
|
||||
3. В графе есть цикл, например `A-B-D-C-A`. Если не отмечать посещенные вершины,
|
||||
обход может снова и снова проходить по циклу через одни и те же вершины и не завершиться.
|
||||
|
||||
### 3. Можно ли посетить весь граф за один BFS
|
||||
|
||||
Неориентированный граф содержит вершины `A, B, C, D, E, F` и только ребра
|
||||
`A-B, B-C, D-E`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какие вершины можно посетить за один BFS, начав с A?
|
||||
2. Посещены ли после этого все вершины графа? Почему?
|
||||
3. Если перебирать все вершины в алфавитном порядке и запускать новый BFS при каждой непосещенной вершине,
|
||||
какие вершины станут начальными для отдельных запусков? На сколько несвязанных частей (компонент связности) разделен граф?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Из A можно посетить только `A, B, C`.
|
||||
|
||||
2. Не все вершины посещены. `D, E` образуют другую связную часть, а F — отдельная вершина;
|
||||
путей от A до них нет, поэтому из A они недостижимы.
|
||||
|
||||
3. Начальными вершинами трех запусков BFS будут последовательно `A, D, F`; они посетят
|
||||
`{A, B, C}`, `{D, E}` и `{F}` соответственно. Следовательно, граф содержит 3 компоненты связности.
|
||||
|
||||
## 9.5.2 Задачи по программированию
|
||||
|
||||
### 1. Проверка существования пути в неориентированном графе
|
||||
|
||||
Дан неориентированный граф из $n$ вершин, пронумерованных от $0$ до $n-1$. Каждый элемент `[u, v]` массива `edges` обозначает неориентированное ребро между вершинами `u` и `v`.
|
||||
|
||||
Также даны начальная вершина `source` и конечная вершина `destination`. Сначала постройте по `edges` список смежности, а затем с помощью BFS или DFS
|
||||
определите, существует ли путь от `source` до `destination`: если существует, верните `true`, иначе — `false`.
|
||||
Граф может содержать циклы и быть несвязным.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Каждое неориентированное ребро нужно добавить в обоих направлениях
|
||||
2. Граф может содержать циклы, поэтому обязательно отмечайте уже посещенные вершины
|
||||
3. Начните с source: встретив destination, верните true; если обход завершился без нее, верните false
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/find-if-path-exists-in-graph/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -19,3 +19,4 @@ icon: material/graphql
|
||||
- [9.2 Базовые операции графа](graph_operations.md)
|
||||
- [9.3 Обход графа](graph_traversal.md)
|
||||
- [9.4 Краткие итоги](summary.md)
|
||||
- [9.5 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 15.6 Упражнения
|
||||
|
||||
## 15.6.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Всегда ли лучше брать самую крупную монету
|
||||
|
||||
Даны монеты номиналов `[1, 7, 10]` и целевая сумма 14.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Запишите монеты, выбранные по правилу «каждый раз брать наибольший номинал, не превышающий оставшуюся сумму».
|
||||
2. Существует ли способ использовать меньше монет? Если да, приведите один пример; если нет, объясните почему.
|
||||
3. Доказывает ли результат, что эта жадная стратегия верна для любых номиналов монет?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Жадный алгоритм последовательно выбирает `10 + 1 + 1 + 1 + 1`, всего 5 монет.
|
||||
|
||||
2. Способ с меньшим числом монет существует: `7 + 7`, всего 2 монеты.
|
||||
|
||||
3. Нет. Этот контрпример показывает, что для произвольных номиналов выбор наибольшей допустимой в данный момент монеты не всегда дает минимальное число монет;
|
||||
лучший на текущем шаге выбор может помешать получить более выгодную комбинацию в дальнейшем.
|
||||
|
||||
### 2. Какой предмет первым положить в рюкзак
|
||||
|
||||
В рюкзак вместимостью 4 килограмма можно положить следующие предметы. Разрешено брать только часть предмета,
|
||||
а полученная ценность пропорциональна взятому весу.
|
||||
|
||||
- предмет A: вес 4 килограмма, ценность 20;
|
||||
- предмет B: вес 3 килограмма, ценность 18.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какова ценность одного килограмма каждого предмета? Какой предмет нужно брать первым?
|
||||
2. Заполните рюкзак по жадной стратегии для дробного рюкзака. Какова итоговая ценность?
|
||||
3. Если предметы можно делить, а рюкзак ограничен по общему весу, что следует сравнивать при выборе: общую ценность или ценность килограмма? Почему?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Ценность килограмма A равна `20 ÷ 4 = 5`, а килограмма B — `18 ÷ 3 = 6`,
|
||||
поэтому первым нужно взять B с большей удельной ценностью.
|
||||
|
||||
2. Сначала берется весь предмет B: он занимает 3 килограмма и дает ценность 18. В рюкзаке остается место для 1 килограмма,
|
||||
поэтому затем берется 1 килограмм предмета A ценностью 5. Итоговая ценность равна `18 + 5 = 23`.
|
||||
|
||||
3. Рюкзак ограничен по общему весу, а предметы можно делить, поэтому нужно сравнивать ценность единицы веса.
|
||||
Хотя общая ценность A выше, ценность его килограмма ниже, чем у B. Если сначала полностью положить A, получится только ценность 20.
|
||||
|
||||
### 3. Как переместить два указателя на следующем шаге
|
||||
|
||||
Высоты вертикальных перегородок заданы массивом `[1, 8, 6, 2, 5]`. Для поиска наибольшей вместимости используются два указателя на концах массива.
|
||||
Вместимость равна произведению высоты более короткой из двух перегородок на расстояние между их индексами.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Сначала левый указатель находится по индексу 0, а правый — по индексу 4. Чему равна текущая вместимость? Какой указатель нужно переместить следующим?
|
||||
2. После одного перемещения, выбранного в вопросе 1, по каким индексам находятся указатели? Чему теперь равна вместимость? Какой указатель нужно переместить следующим?
|
||||
3. Для текущей пары перегородок можно переместить указатель как более короткой, так и более высокой перегородки. Какое перемещение еще может привести к большей вместимости и почему?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Текущая вместимость равна `min(1, 5) × (4 - 0) = 4`. Левая перегородка короче, поэтому нужно переместить левый указатель.
|
||||
|
||||
2. После перемещения левый и правый указатели находятся по индексам 1 и 4. Текущая вместимость равна
|
||||
`min(8, 5) × (4 - 1) = 15`. Теперь короче правая перегородка, поэтому следующим нужно переместить правый указатель.
|
||||
|
||||
3. Большую вместимость еще может дать перемещение указателя более короткой перегородки. При перемещении более высокой перегородки расстояние между ними обязательно уменьшается,
|
||||
а высота контейнера по-прежнему ограничена оставшейся короткой перегородкой, поэтому вместимость может только остаться прежней или уменьшиться.
|
||||
Только перемещение короткой перегородки дает возможность встретить более высокую.
|
||||
|
||||
## 15.6.2 Задачи по программированию
|
||||
|
||||
### 1. Дробный рюкзак
|
||||
|
||||
Даны массивы одинаковой длины `wgt` и `val`, где `wgt[i] > 0`, `val[i] >= 0`, а вместимость рюкзака `cap >= 0`.
|
||||
Каждый предмет имеется в одном экземпляре, но разрешено положить в рюкзак только его часть;
|
||||
полученная ценность пропорциональна доле веса предмета, которую положили в рюкзак. Используйте жадный алгоритм
|
||||
и верните максимальную суммарную ценность как вещественное число.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Сначала вычислите ценность единицы веса каждого предмета как val[i] / wgt[i]; результат деления должен сохранять дробную часть
|
||||
2. Чем выше ценность единицы веса предмета, тем раньше его нужно класть в рюкзак
|
||||
3. Если оставшаяся вместимость меньше веса текущего предмета, возьмите такую его часть, которая точно заполнит рюкзак, и завершите работу
|
||||
@@ -20,3 +20,4 @@ icon: material/head-heart-outline
|
||||
- [15.3 Задача о максимальной вместимости](max_capacity_problem.md)
|
||||
- [15.4 Задача о максимальном произведении разбиения](max_product_cutting_problem.md)
|
||||
- [15.5 Резюме](summary.md)
|
||||
- [15.6 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 6.5 Упражнения
|
||||
|
||||
## 6.5.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Как искать после хеш-коллизии
|
||||
|
||||
Хеш-таблица содержит 5 бакетов и использует хеш-функцию $h(x)=x \bmod 5$. При коллизии элементы последовательно добавляются в список соответствующего бакета.
|
||||
В таблицу по порядку вставляют `[1, 6, 11, 7]`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Запишите содержимое бакетов с номерами 0–4.
|
||||
2. В какой бакет сначала попадает поиск числа 6 и какие элементы проверяются по порядку?
|
||||
3. Исходя из содержимого бакетов в вопросе 1, перезаписали ли позднее вставленные элементы более ранние? Объясните ответ с учетом этого способа разрешения коллизий.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Так как $1\bmod5=6\bmod5=11\bmod5=1$, а $7\bmod5=2$, бакеты имеют вид:
|
||||
|
||||
```text
|
||||
0: []
|
||||
1: [1, 6, 11]
|
||||
2: [7]
|
||||
3: []
|
||||
4: []
|
||||
```
|
||||
|
||||
2. При поиске числа 6 сначала выбирается бакет 1, затем сравниваются элементы 1 и 6. Искомый элемент находится при втором сравнении.
|
||||
|
||||
3. Одинаковые хеш-значения означают лишь попадание элементов в один бакет, но не равенство самих элементов. При цепной адресации все столкнувшиеся элементы сохраняются в бакете,
|
||||
а при поиске сравниваются по очереди. Поэтому 1, 6 и 11 не перезаписывают друг друга.
|
||||
|
||||
### 2. Куда перемещаются элементы при расширении хеш-таблицы
|
||||
|
||||
Хеш-таблица с цепной адресацией первоначально содержит 5 бакетов и использует хеш-функцию $h(x)=x\bmod5$.
|
||||
Ключи `[1, 6, 11]` находятся в бакете 1.
|
||||
|
||||
Теперь таблицу расширяют до 7 бакетов, и хеш-функция принимает вид $h(x)=x\bmod7$.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Вычислите новые номера бакетов для 1, 6 и 11.
|
||||
2. Какие бакеты будут непустыми после расширения?
|
||||
3. Можно ли при расширении скопировать список из прежнего бакета 1 без изменений в новый бакет 1? Обоснуйте ответ результатами вопросов 1 и 2.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Новые номера бакетов:
|
||||
|
||||
- $1\bmod7=1$;
|
||||
- $6\bmod7=6$;
|
||||
- $11\bmod7=4$.
|
||||
|
||||
2. В бакете 1 хранится 1, в бакете 4 — 11, а в бакете 6 — 6. Три ключа больше не находятся в одном бакете.
|
||||
|
||||
3. Копировать список без изменений нельзя. Номер бакета вычисляется как остаток от деления ключа на число бакетов. После замены 5 на 7 номер бакета для того же ключа может измениться,
|
||||
поэтому положение каждого ключа необходимо вычислить заново. Если просто скопировать прежний бакет 1, то последующий поиск по новой формуле направится за 6 в бакет 6, а за 11 — в бакет 4
|
||||
и не найдет эти ключи.
|
||||
|
||||
### 3. Можно ли найти 11 после удаления 6
|
||||
|
||||
Хеш-таблица содержит 5 позиций с индексами `0–4` и использует хеш-функцию $h(x)=x\bmod5$.
|
||||
При коллизии поиск первого свободного места начинается с вычисленного хеш-функцией индекса и продолжается вправо.
|
||||
|
||||
В таблицу по порядку вставляют `[1, 6, 11]`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. По какому индексу в итоге находится каждое из трех чисел?
|
||||
2. Какие индексы проверяются по порядку при поиске 11?
|
||||
3. Допустим, при удалении 6 его позицию просто освобождают, как если бы она никогда не использовалась, а поиск прекращается при встрече с пустой позицией.
|
||||
Что произойдет при следующем поиске 11? Будет ли результат правильным? Если нет, как избежать ошибки?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Число 1 находится по индексу 1. Число 6 также отображается в индекс 1, но после коллизии помещается по индексу 2.
|
||||
Число 11 тоже начинает поиск с индекса 1, пропускает занятые индексы 1 и 2 и в итоге помещается по индексу 3.
|
||||
|
||||
2. При поиске 11 последовательно проверяются индексы `1, 2, 3`, и число находится по индексу 3.
|
||||
|
||||
3. Если пометить индекс 2 как «никогда не использовавшийся», то поиск 11 остановится на индексе 2 после проверки индекса 1
|
||||
и ошибочно решит, что числа 11 нет. При удалении следует оставлять отметку «удалено»:
|
||||
встретив ее, поиск продолжает проверять следующий индекс (после индекса 4 возвращаясь к индексу 0), а при последующей вставке эту позицию можно использовать снова.
|
||||
|
||||
## 6.5.2 Задачи по программированию
|
||||
|
||||
### 1. Сравнение состава символов в двух строках
|
||||
|
||||
Даны две строки `s` и `t`, состоящие только из строчных английских букв.
|
||||
Символы в `s` можно переставлять произвольным образом, но нельзя добавлять, удалять или заменять.
|
||||
|
||||
Определите, можно ли после перестановки получить `t`. Если можно, верните `true`, иначе — `false`.
|
||||
Используйте хеш-таблицу для подсчета числа вхождений каждой буквы и не сортируйте символы в строках.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Если строки имеют разную длину, их состав символов не может совпадать
|
||||
2. Сохраняйте в хеш-таблице количество каждой буквы; при обходе s увеличивайте соответствующий счетчик на 1
|
||||
3. При обходе t уменьшайте соответствующий счетчик на 1; состав символов совпадает, только если все счетчики в конце равны 0
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/valid-anagram/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -19,3 +19,4 @@ icon: material/table-search
|
||||
- [6.2 Хеш-коллизии](hash_collision.md)
|
||||
- [6.3 Алгоритмы хеширования](hash_algorithm.md)
|
||||
- [6.4 Резюме](summary.md)
|
||||
- [6.5 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 8.5 Упражнения
|
||||
|
||||
## 8.5.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Как перестраивается куча после добавления числа 10
|
||||
|
||||
Массив `[9, 7, 8, 3, 5]` представляет максимальную кучу. Теперь в нее добавляют число 10.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Сначала число 10 добавляется в конец массива. Чему равно значение его родительского узла?
|
||||
2. Выполните упорядочивание кучи снизу вверх, начиная с нового узла, и запишите массив после каждого обмена.
|
||||
3. Какой элемент в итоге окажется на вершине кучи? Сколько всего обменов выполнено?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. После добавления 10 его индекс равен 5, а индекс родительского узла равен
|
||||
$\lfloor(5-1)/2\rfloor=2$. Значение родительского узла — 8.
|
||||
|
||||
2. Число 10 больше 8, поэтому после первого обмена получается `[9, 7, 10, 3, 5, 8]`;
|
||||
затем 10 больше родительского узла 9, и после второго обмена получается `[10, 7, 9, 3, 5, 8]`.
|
||||
Число 10 достигло корневого узла, поэтому упорядочивание завершено.
|
||||
|
||||
3. В итоге на вершине кучи находится 10. Всего выполнено 2 обмена.
|
||||
|
||||
### 2. Проверка отношений между родительскими и дочерними узлами минимальной кучи
|
||||
|
||||
Массив `[1, 4, 3, 7, 6, 2]` представляет полное двоичное дерево. В минимальной куче значение каждого родительского узла не превышает значения его дочерних узлов.
|
||||
Для индекса $i$ индексы левого и правого дочерних узлов равны $2i+1$ и $2i+2$ соответственно.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Укажите индексы и значения дочерних узлов для индекса 2.
|
||||
2. В узле с индексом 2 находится значение 3. Нарушается ли правило минимальной кучи при сравнении этого узла с его дочерним узлом? Если да, какие элементы нужно поменять местами?
|
||||
3. С учетом ответа на вопрос 2: если правило нарушено, запишите массив после обмена; если не нарушено, объясните, почему обмен не требуется. Затем проверьте остальные отношения между родительскими и дочерними узлами.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Левый дочерний узел индекса 2 имеет индекс 5 и значение 2. Индекс правого дочернего узла равен 6, но длина массива равна 6, поэтому такого узла нет.
|
||||
|
||||
2. Значение родительского узла 3 больше значения дочернего узла 2, что нарушает правило минимальной кучи. Нужно поменять местами элементы с индексами 2 и 5.
|
||||
|
||||
3. После обмена получается `[1, 4, 2, 7, 6, 3]`. Проверим все отношения:
|
||||
`1 ≤ 4`, `1 ≤ 2`; `4 ≤ 7`, `4 ≤ 6`; `2 ≤ 3`.
|
||||
Каждый родительский узел теперь не больше своих дочерних узлов, поэтому правило минимальной кучи выполняется.
|
||||
|
||||
### 3. Сохранение трех наибольших чисел в минимальной куче
|
||||
|
||||
Чтобы сохранять 3 наибольших числа из потока данных `[4, 1, 7, 3, 8]`, можно поддерживать минимальную кучу размером не более 3.
|
||||
|
||||
Сначала последовательно добавьте в минимальную кучу первые 3 числа. Когда куча заполнена, для каждого нового числа:
|
||||
если оно больше вершины кучи, удалите вершину и добавьте новое число; иначе оставьте кучу без изменений.
|
||||
|
||||
Запишите после чтения каждого числа набор чисел, хранящихся в куче, и ее вершину.
|
||||
Достаточно записывать сохраненные числа как множество; порядок их расположения в массиве кучи указывать не нужно.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
Результаты после чтения каждого числа:
|
||||
|
||||
| Прочитанное число | Сохраненные числа | Вершина кучи |
|
||||
| --- | --- | --- |
|
||||
| 4 | `{4}` | 4 |
|
||||
| 1 | `{1, 4}` | 1 |
|
||||
| 7 | `{1, 4, 7}` | 1 |
|
||||
| 3 | `{3, 4, 7}` | 3 |
|
||||
| 8 | `{4, 7, 8}` | 4 |
|
||||
|
||||
После заполнения кучи на ее вершине находится наименьшее из сохраненных чисел. Новое число заменяет вершину,
|
||||
только если оно больше нее. Итоговое множество `{4, 7, 8}` действительно содержит 3 наибольших числа.
|
||||
|
||||
## 8.5.2 Задачи по программированию
|
||||
|
||||
### 1. k-й наибольший элемент массива
|
||||
|
||||
Даны массив целых чисел `nums` и целое число $k$, где $1 \le k \le n$, а $n$ — длина массива. Если расположить массив по убыванию, верните элемент на позиции $k$.
|
||||
|
||||
Повторяющиеся элементы учитываются по отдельности. Например, 2-й наибольший элемент массива `[5, 5, 2]` по-прежнему равен 5. Используйте минимальную кучу размером не более $k$.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. k-й наибольший элемент — наименьший среди k наибольших чисел
|
||||
2. Добавляйте каждое число в минимальную кучу, а когда ее размер превышает k, извлекайте наименьшее значение
|
||||
3. После обхода в куче останутся k наибольших чисел, а на ее вершине будет находиться ответ
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/kth-largest-element-in-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -19,3 +19,4 @@ icon: material/family-tree
|
||||
- [8.2 Построение кучи](build_heap.md)
|
||||
- [8.3 Задача Top-k](top_k.md)
|
||||
- [8.4 Резюме](summary.md)
|
||||
- [8.5 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 10.7 Упражнения
|
||||
|
||||
## 10.7.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Как двоичный поиск сужает область поиска
|
||||
|
||||
Найдите число 16 в упорядоченном массиве `[2, 5, 8, 12, 16, 23, 38]`.
|
||||
Используйте двойной замкнутый интервал `[i, j]` и середину
|
||||
$m=i+(j-i)/2$ с округлением вниз.
|
||||
|
||||
Для каждой итерации запишите `(i, j, m)`, средний элемент и способ дальнейшего сужения интервала,
|
||||
пока цель не будет найдена.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
Процесс поиска по итерациям:
|
||||
|
||||
| Итерация | `(i, j, m)` | Средний элемент | Следующий шаг |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `(0, 6, 3)` | 12 | `12 < 16`, присвоить `i = 4` |
|
||||
| 2 | `(4, 6, 5)` | 23 | `23 > 16`, присвоить `j = 4` |
|
||||
| 3 | `(4, 4, 4)` | 16 | Цель найдена, вернуть индекс 4 |
|
||||
|
||||
Массив упорядочен, поэтому если значение в середине меньше цели, можно исключить средний элемент и все элементы слева от него;
|
||||
если оно больше цели — исключить средний элемент и все элементы справа.
|
||||
|
||||
### 2. Левая и правая границы повторяющегося элемента
|
||||
|
||||
В массиве `[1, 2, 2, 2, 4, 6]` ищут число 2.
|
||||
Ученик выполнил двоичный поиск, нашел цель по индексу 2, сразу вернул ответ и сказал:
|
||||
«Индекс 2 — левая граница числа 2».
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Верно ли утверждение ученика? Чему равны левая и правая границы числа 2? Объясните ответ.
|
||||
2. В какой части массива нужно продолжить поиск левой границы, если средний элемент равен цели?
|
||||
3. В какой части нужно продолжить поиск правой границы? Достаточно указать направление, полный процесс поиска записывать не нужно.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Утверждение неверно. Немедленный возврат после обнаружения числа 2 гарантирует лишь, что найден один из таких элементов, но не самый левый или самый правый.
|
||||
Левая граница в этом массиве имеет индекс 1, а правая — индекс 3.
|
||||
|
||||
2. При поиске левой границы, даже если средний элемент равен 2, нужно продолжить поиск слева,
|
||||
например, при двойном замкнутом интервале присвоить `j = m - 1`.
|
||||
|
||||
3. При поиске правой границы после совпадения среднего элемента с 2 нужно продолжить поиск справа,
|
||||
например, присвоить `i = m + 1`.
|
||||
|
||||
### 3. Как выбрать способ поиска для разных данных
|
||||
|
||||
Для каждой ситуации выберите подходящий вариант из «линейный поиск, двоичный поиск, хеш-таблица» и объясните свой выбор.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Многократно выполнять поиск среди $10^7$ уже упорядоченных и больше не изменяющихся целых чисел, не создавая дополнительных структур данных;
|
||||
2. многократно проверять наличие ключа в наборе с частыми вставками и удалениями, не поддерживая порядок и не выполняя поиск по диапазону;
|
||||
3. один раз найти некоторое значение в неупорядоченном массиве.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Двоичный поиск: данные упорядочены и не меняются, сложность равна $O(\log n)$, дополнительная память не нужна.
|
||||
2. Хеш-таблица: если хеш-функция достаточно равномерно распределяет ключи по бакетам, средняя временная сложность вставки, удаления и поиска по ключу равна $O(1)$.
|
||||
3. Прямой обход от начала до конца: ради одного поиска сортировка или построение хеш-таблицы все равно потребует предварительно обработать весь массив
|
||||
и не уменьшит общий объем работы для этой единственной задачи.
|
||||
|
||||
Выбор зависит от того, упорядочены ли данные, разрешено ли создавать дополнительную структуру, сколько будет запросов и какие операции нужно выполнять.
|
||||
|
||||
## 10.7.2 Задачи по программированию
|
||||
|
||||
### 1. Двоичный поиск в упорядоченном массиве
|
||||
|
||||
Дан массив целых чисел `nums`, упорядоченный строго по возрастанию, и целевое значение `target`. Найдите `target` с помощью двоичного поиска: если значение присутствует, верните его индекс в массиве; иначе верните -1.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Начальный интервал задается как left = 0, right = n - 1; он непуст, пока left <= right
|
||||
2. Вычисляйте середину как mid = left + (right - left) // 2
|
||||
3. Если `nums[mid]` меньше `target`, перенесите левую границу в `mid + 1`; если `nums[mid]` больше `target`, перенесите правую границу в `mid - 1`; при равенстве сразу верните ответ
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/binary-search/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. Позиция вставки в упорядоченный массив
|
||||
|
||||
Дан массив целых чисел `nums`, упорядоченный строго по возрастанию, и целевое значение `target`.
|
||||
|
||||
Если `target` уже находится в массиве, верните его индекс. Иначе верните позицию, в которую нужно вставить `target`, чтобы массив остался строго возрастающим. Ответ может быть равен 0 или длине массива. Используйте двоичный поиск.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Ответ может быть равен 0 или длине массива n
|
||||
2. При двойном замкнутом интервале, если `nums[mid]` больше или равно `target`, присвойте `right = mid - 1` и продолжите проверять позиции левее; иначе присвойте `left = mid + 1`
|
||||
3. После завершения цикла left будет позицией вставки
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/search-insert-position/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -21,3 +21,4 @@ icon: material/text-search
|
||||
- [10.4 Стратегии оптимизации хеширования](replace_linear_by_hashing.md)
|
||||
- [10.5 Переосмысление алгоритмов поиска](searching_algorithm_revisited.md)
|
||||
- [10.6 Резюме](summary.md)
|
||||
- [10.7 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 11.12 Упражнения
|
||||
|
||||
## 11.12.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Первые итерации сортировки выбором и пузырьком
|
||||
|
||||
Дан массив `[4, 2, 5, 1, 3]`. В обоих случаях элементы сортируются по возрастанию.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Выполните первые две итерации сортировки выбором, запишите массив после каждой из них и укажите, какие позиции уже определены.
|
||||
2. Выполните первую итерацию сортировки пузырьком, запишите состояние массива и число обменов, а также укажите, какая позиция уже определена.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Первые две итерации:
|
||||
|
||||
| Итерация | Состояние массива | Пояснение |
|
||||
| --- | --- | --- |
|
||||
| 1 | `[1, 2, 5, 4, 3]` | Наименьший элемент 1 меняется местами с первым элементом |
|
||||
| 2 | `[1, 2, 5, 4, 3]` | Значение 2 уже находится по индексу 1, обмен не нужен |
|
||||
|
||||
Теперь первые две позиции определены, а дальнейший выбор наименьшего элемента выполняется только в `[5, 4, 3]`.
|
||||
|
||||
2. Последовательно сравниваются соседние элементы: 4 и 2 меняются местами; 4 и 5 остаются на месте; 5 и 1 меняются местами; 5 и 3 меняются местами.
|
||||
Получается `[2, 4, 1, 3, 5]`, всего 3 обмена. Наибольший элемент 5 переместился в конец массива, поэтому последняя позиция определена.
|
||||
|
||||
### 2. Меняется ли взаимный порядок равных элементов
|
||||
|
||||
В массиве $[2_a, 2_b, 1]$ значения $2_a$ и $2_b$ равны, но нижние индексы отмечают их исходный порядок.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Запишите массив после первой итерации сортировки выбором. Изменился ли взаимный порядок $2_a$ и $2_b$?
|
||||
2. Запишите массив после первой итерации сортировки пузырьком. Изменился ли взаимный порядок $2_a$ и $2_b$?
|
||||
3. На основании первых двух ответов объясните, чем различаются эти сортировки в сохранении исходного порядка равных элементов.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. На первой итерации сортировка выбором находит наименьший элемент 1 и меняет его местами с первым элементом $2_a$, поэтому получается
|
||||
$[1, 2_b, 2_a]$. Элемент $2_a$ переместился за $2_b$, и их взаимный порядок изменился.
|
||||
|
||||
2. Сортировка пузырьком сначала сравнивает $2_a$ и $2_b$. Они равны, поэтому обмена нет. Затем $2_b$ сравнивается с 1 и меняется с ним местами.
|
||||
После первой итерации получается $[2_a, 1, 2_b]$. Элемент $2_a$ по-прежнему находится перед $2_b$, их взаимный порядок не изменился.
|
||||
|
||||
3. В этом примере сортировка выбором меняет исходный порядок равных элементов. Сортировка пузырьком обменивает соседние элементы, только когда левый больше правого.
|
||||
Равные элементы не меняются местами, поэтому их исходный взаимный порядок сохраняется.
|
||||
|
||||
### 3. Сравнение сортировки подсчетом и поразрядной сортировки
|
||||
|
||||
Школе нужно упорядочить большое количество номеров учеников, каждый из которых состоит ровно из 8 цифр. Ответьте на вопросы.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Сколько итераций потребуется поразрядной сортировке, начинающей с младшего разряда?
|
||||
2. Почему сортировка подсчетом, использующая весь номер как целое число, подготовит множество неиспользуемых позиций для счетчиков?
|
||||
3. Какой из двух способов вы выберете для большого количества 8-значных номеров — сортировку подсчетом или поразрядную сортировку? Обоснуйте выбор с учетом первых двух ответов.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Номер состоит из 8 разрядов, поэтому от младшего до старшего потребуется 8 итераций; на каждой из них элементы распределяются только по цифрам 0–9.
|
||||
|
||||
2. Для прямого подсчета пришлось бы выделить позиции для всех возможных 8-значных значений, хотя в действительности используется лишь небольшая их часть.
|
||||
Большинство счетчиков всегда оставалось бы равным 0.
|
||||
|
||||
3. Следует выбрать поразрядную сортировку. Она использует свойства «фиксированное число разрядов и только 10 возможных значений каждого разряда» и требует лишь 8 повторений стабильной группировки.
|
||||
Если же использовать весь 8-значный номер как индекс при сортировке подсчетом, пришлось бы выделить счетчики для множества значений, которые никогда не встретятся.
|
||||
|
||||
## 11.12.2 Задачи по программированию
|
||||
|
||||
### 1. Сортировка массива слиянием
|
||||
|
||||
Дан массив целых чисел `nums`. Самостоятельно реализуйте сортировку слиянием, расположите элементы в неубывающем порядке и верните массив. Не используйте встроенную функцию сортировки языка.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Интервал длины не более 1 уже упорядочен
|
||||
2. Разделите интервал в середине на две половины и рекурсивно отсортируйте каждую
|
||||
3. Слейте два упорядоченных полуинтервала с помощью двух указателей, а затем запишите результат обратно в исходный массив
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/sort-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. Сортировка массива целых чисел подсчетом
|
||||
|
||||
Даны массив целых чисел `nums` и неотрицательное целое число $K$. Каждый элемент массива находится в диапазоне от $0$ до $K$.
|
||||
|
||||
Реализуйте сортировку подсчетом, запишите результат в `nums` в неубывающем порядке и верните `nums`.
|
||||
Не определяйте порядок элементов с помощью сравнений между ними и не используйте встроенную функцию сортировки языка.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Поскольку каждый элемент находится в диапазоне от 0 до K, значение элемента можно напрямую использовать как индекс массива счетчиков
|
||||
2. При первом обходе nums увеличивайте счетчик в соответствующей позиции на 1
|
||||
3. Затем переберите массив счетчиков от 0 до K: если значение x встретилось несколько раз, запишите x в nums подряд столько же раз
|
||||
@@ -26,3 +26,4 @@ icon: material/sort-ascending
|
||||
- [11.9 Сортировка подсчетом](counting_sort.md)
|
||||
- [11.10 Поразрядная сортировка](radix_sort.md)
|
||||
- [11.11 Резюме](summary.md)
|
||||
- [11.12 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 5.5 Упражнения
|
||||
|
||||
## 5.5.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Что первым извлекается из стека и очереди
|
||||
|
||||
Подготовьте пустой стек `S` и пустую очередь `Q`, а затем выполните с каждым из них один и тот же набор операций:
|
||||
|
||||
шаг 1: добавить `A`;
|
||||
шаг 2: добавить `B`;
|
||||
шаг 3: удалить один элемент и записать его;
|
||||
шаг 4: добавить `C`;
|
||||
шаг 5: удалять и записывать элементы, пока контейнер не опустеет.
|
||||
|
||||
Запишите порядок удаления элементов из `S` и `Q`. Объясните различие принципами «последним пришел — первым вышел» и «первым пришел — первым вышел».
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
Порядок удаления из стека `S`: `B, C, A`. После добавления `A, B` первым извлекается последний добавленный элемент `B`; затем добавляется `C`,
|
||||
после чего извлекаются `C, A`. Это принцип «последним пришел — первым вышел».
|
||||
|
||||
Порядок удаления из очереди `Q`: `A, B, C`. После добавления `A, B` первым удаляется самый ранний элемент `A`;
|
||||
затем добавляется `C`, после чего удаляются `B, C`. Это принцип «первым пришел — первым вышел».
|
||||
|
||||
### 2. Что происходит, когда хвост очереди проходит конец массива
|
||||
|
||||
Очередь реализована с помощью кольцевого массива длины 5 с индексами `0–4`.
|
||||
Сейчас `front = 3`, `size = 2`, а элементы очереди `A, B` находятся по индексам 3 и 4 соответственно.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. По какому индексу нужно поместить `C` при операции «добавить `C` в очередь»? Чему после этого равно `size`?
|
||||
2. Затем из очереди удаляется один элемент. Какой именно? Чему равны новые значения `front` и `size`?
|
||||
3. Каков теперь логический порядок элементов от головы к хвосту очереди? Нужно ли при удалении сдвигать остальные элементы массива и почему?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Позиция нового элемента равна
|
||||
`(front + size) % 5 = (3 + 2) % 5 = 0`,
|
||||
поэтому `C` помещается по индексу 0. После добавления `size = 3`.
|
||||
|
||||
2. Из очереди удаляется текущий первый элемент `A`. Новый индекс головы равен
|
||||
`(3 + 1) % 5 = 4`, поэтому `front = 4`, `size = 2`.
|
||||
|
||||
3. Логический порядок находящихся в очереди элементов — `B, C`: `B` находится по индексу 4, а `C` — по индексу 0.
|
||||
При удалении достаточно изменить `front` и `size`. Благодаря остатку от деления индекс в кольцевом массиве возвращается к началу,
|
||||
поэтому сдвигать вперед все остальные элементы не нужно.
|
||||
|
||||
### 3. Операции с обоих концов двусторонней очереди
|
||||
|
||||
Здесь `push_first` означает добавление в голову, `push_last` — добавление в хвост,
|
||||
`pop_first` — извлечение из головы, а `pop_last` — извлечение из хвоста.
|
||||
|
||||
С пустой двусторонней очередью `deq` последовательно выполняются операции:
|
||||
|
||||
1. `push_last(A)`
|
||||
2. `push_last(B)`
|
||||
3. `push_first(C)`
|
||||
4. `pop_last()`
|
||||
5. `push_last(D)`
|
||||
6. `pop_first()`
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какие элементы возвращают две операции извлечения?
|
||||
2. Какие элементы остаются после всех операций в порядке от головы к хвосту?
|
||||
3. Рассмотрите все 6 шагов. Можно ли выполнить их с обычной очередью, в которую разрешено добавлять только с хвоста, а удалять только с головы? Если нет, укажите недопустимые операции. Объясните также, может ли выполнить все шаги двусторонняя очередь и почему.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
После первых трех шагов двусторонняя очередь от головы к хвосту имеет вид `[C, A, B]`.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. `pop_last()` возвращает `B`. После добавления `D` очередь имеет вид `[C, A, D]`,
|
||||
а `pop_first()` возвращает `C`.
|
||||
|
||||
2. В конце остаются `[A, D]`.
|
||||
|
||||
3. Обычная очередь, допускающая добавление с хвоста и удаление с головы, не может выполнить все операции:
|
||||
шаг 3 `push_first(C)` требует добавить элемент с головы, а шаг 4 `pop_last()` — удалить элемент с хвоста. Обе операции выходят за рамки обычной очереди.
|
||||
Двусторонняя очередь позволяет добавлять и удалять элементы с обоих концов, поэтому может выполнить все 6 шагов.
|
||||
|
||||
## 5.5.2 Задачи по программированию
|
||||
|
||||
### 1. Проверка последовательности скобок
|
||||
|
||||
Дана строка `s`, содержащая только скобки трех видов: `()`, `[]`, `{}`. С помощью стека определите, является ли последовательность правильной.
|
||||
|
||||
Правильная последовательность должна удовлетворять двум условиям: каждая закрывающая скобка соответствует по виду ближайшей еще не сопоставленной открывающей скобке,
|
||||
а после обхода не остается несопоставленных открывающих скобок. Верните логическое значение результата проверки.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Можно создать отображение из закрывающей скобки в соответствующую открывающую
|
||||
2. Встретив закрывающую скобку, сначала проверьте, не пуст ли стек, а затем — совпадает ли его вершина
|
||||
3. После обхода стек также должен быть пуст
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/valid-parentheses/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -19,3 +19,4 @@ icon: material/stack-overflow
|
||||
- [5.2 Очередь](queue.md)
|
||||
- [5.3 Двусторонняя очередь](deque.md)
|
||||
- [5.4 Резюме](summary.md)
|
||||
- [5.5 Упражнения](exercises.md)
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
<!-- Автоматически создано utils/exercises/publish_exercises.py; не редактируйте напрямую. -->
|
||||
|
||||
# 7.7 Упражнения
|
||||
|
||||
## 7.7.1 Вопросы для самопроверки
|
||||
|
||||
### 1. Полные, строгие и идеальные двоичные деревья
|
||||
|
||||
Два массива ниже представляют двоичные деревья в порядке обхода по уровням; `None` обозначает пустую позицию.
|
||||
|
||||
- дерево A: `[1, 2, 3, 4, 5, 6]`
|
||||
- дерево B: `[1, 2, 3, None, None, 6, 7]`
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Какое из деревьев является полным?
|
||||
2. Какое из деревьев является строгим, то есть каждый его нелистовой узел имеет два дочерних узла?
|
||||
3. Есть ли среди них идеальное двоичное дерево? Объясните ответ для каждого дерева.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Дерево A полное. В нем не заполнен только последний уровень, причем узлы на нем расположены непрерывно слева направо.
|
||||
Дерево B не является полным: на последнем уровне слева уже есть пустые позиции, но справа от них еще находятся узлы.
|
||||
|
||||
2. Дерево B строгое: узлы 1 и 3 имеют по два дочерних узла, а все остальные узлы являются листьями.
|
||||
Дерево A не является строгим, потому что у узла 3 есть только левый дочерний узел 6.
|
||||
|
||||
3. Ни одно дерево не является идеальным, поскольку последний уровень каждого из них заполнен не полностью.
|
||||
|
||||
### 2. Три порядка обхода одного дерева
|
||||
|
||||
Поместите массив `[1, 2, 3, 4, 5, 6, 7]` по уровням в полное двоичное дерево.
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Нарисуйте это дерево.
|
||||
2. Запишите последовательности прямого, симметричного и обратного обходов.
|
||||
3. Каким частям дерева соответствуют участки последовательности симметричного обхода слева и справа от корневого узла 1?
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. Получится дерево:
|
||||
|
||||
```text
|
||||
1
|
||||
/ \
|
||||
2 3
|
||||
/ \ / \
|
||||
4 5 6 7
|
||||
```
|
||||
|
||||
2. Прямой обход: `1, 2, 4, 5, 3, 6, 7`;
|
||||
симметричный обход: `4, 2, 5, 1, 6, 3, 7`;
|
||||
обратный обход: `4, 5, 2, 6, 7, 3, 1`.
|
||||
|
||||
3. Последовательность `4, 2, 5` слева от корневого узла 1 — симметричный обход левого поддерева,
|
||||
а `6, 3, 7` справа — симметричный обход правого поддерева.
|
||||
|
||||
### 3. Сравнение двух двоичных деревьев поиска
|
||||
|
||||
Последовательно, слева направо, вставьте каждую из двух последовательностей в пустое двоичное дерево поиска:
|
||||
|
||||
- последовательность A: `[4, 2, 6, 1, 3, 5, 7]`
|
||||
- последовательность B: `[1, 2, 3, 4, 5, 6, 7]`
|
||||
|
||||
<!-- numbered-subquestions -->
|
||||
|
||||
1. Запишите узлы, через которые проходит поиск числа 7 в каждом дереве.
|
||||
2. Если высота равна числу ребер на пути от корневого узла до самого удаленного листового узла, чему равна высота каждого дерева?
|
||||
3. Одинакова ли эффективность поиска числа 7 в двух деревьях? Объясните ответ, учитывая форму деревьев и пути поиска.
|
||||
|
||||
??? success "Ответ"
|
||||
|
||||
1. В дереве, построенном из последовательности A, путь поиска равен `4 → 6 → 7`;
|
||||
в дереве из последовательности B — `1 → 2 → 3 → 4 → 5 → 6 → 7`.
|
||||
|
||||
2. Каждый уровень первого дерева заполнен, его высота равна 2. Второе дерево состоит только из правых дочерних узлов, его высота равна 6.
|
||||
|
||||
3. Эффективность поиска 7 различается. Порядок вставки изменил форму и высоту двоичного дерева поиска. В первом дереве поиск 7 проходит только через 3 узла,
|
||||
а во втором — через все 7. Чем выше дерево, тем больше узлов в худшем случае приходится сравнивать вдоль пути.
|
||||
|
||||
## 7.7.2 Задачи по программированию
|
||||
|
||||
### 1. Максимальная глубина двоичного дерева
|
||||
|
||||
Дан корневой узел `root` двоичного дерева. Каждый узел содержит целое значение и ссылки на левый и правый дочерние узлы.
|
||||
|
||||
Максимальной глубиной двоичного дерева назовем **число узлов** на пути от корневого узла до самого удаленного листового узла. Верните максимальную глубину дерева; глубина пустого дерева равна 0.
|
||||
Используйте рекурсию.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. В этой задаче глубина измеряется числом узлов: если в дереве есть только корневой узел, максимальная глубина равна 1
|
||||
2. Пусть рекурсивная функция возвращает максимальную глубину поддерева с корнем в текущем узле
|
||||
3. Для пустого узла верните 0, для непустого — max(depth(left), depth(right)) + 1
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/maximum-depth-of-binary-tree/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 2. Обход двоичного дерева по уровням
|
||||
|
||||
Дан корневой узел `root` двоичного дерева. С помощью очереди посетите все узлы уровень за уровнем сверху вниз, а внутри каждого уровня — слева направо.
|
||||
|
||||
Верните двумерный массив: первый вложенный массив хранит значения узлов на уровне корня, второй — значения следующего уровня и так далее.
|
||||
Для пустого дерева верните пустой массив.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. При обходе по уровням узлы нужно посещать в порядке их добавления, поэтому используйте очередь
|
||||
2. В начале каждой итерации в очереди находятся ровно узлы одного уровня
|
||||
3. Сначала запомните длину очереди, затем извлеките столько узлов и добавьте их дочерние узлы
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/binary-tree-level-order-traversal/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
|
||||
### 3. k-й наименьший элемент двоичного дерева поиска
|
||||
|
||||
Двоичное дерево поиска содержит `n` узлов с попарно различными значениями.
|
||||
Если расположить все значения по возрастанию, их позиции нумеруются начиная с 1.
|
||||
|
||||
Даны корневой узел `root` и целое число `k`, удовлетворяющее `1 <= k <= n`. Верните значение узла на позиции `k`.
|
||||
Найдите ответ непосредственно во время симметричного обхода, не собирая предварительно значения всех узлов.
|
||||
|
||||
??? tip "Подсказки"
|
||||
|
||||
1. Симметричный обход двоичного дерева поиска посещает значения узлов в порядке возрастания
|
||||
2. При симметричном обходе последовательно обрабатываются левое поддерево, текущий узел и правое поддерево; при посещении текущего узла увеличьте счетчик на 1
|
||||
3. Когда счетчик впервые станет равен k, значение текущего узла будет ответом и продолжать обход не потребуется
|
||||
|
||||
[LeetCode](https://leetcode.com/problems/kth-smallest-element-in-a-bst/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
|
||||
@@ -21,3 +21,4 @@ icon: material/graph-outline
|
||||
- [7.4 Двоичное дерево поиска](binary_search_tree.md)
|
||||
- [7.5 AVL-дерево *](avl_tree.md)
|
||||
- [7.6 Краткие итоги](summary.md)
|
||||
- [7.7 Упражнения](exercises.md)
|
||||
|
||||
Reference in New Issue
Block a user