12.6 Упражнения¶
12.6.1 Вопросы для самопроверки¶
1. Какие задачи подходят для метода «разделяй и властвуй»¶
Ученик хочет решить следующие задачи способом «разделить на две половины, решить каждую отдельно, затем объединить результаты». Для каждой задачи выберите одно из утверждений: «подходит для метода “разделяй и властвуй”», «разделение возможно, но не уменьшает общий объем работы» или «половины нельзя решить независимо». Объясните свой выбор.
- Отсортировать неупорядоченный массив;
- найти максимальный элемент массива;
- последовательно выполнить набор операций стека
push(x)иpop(), выводя элемент, полученный при каждой операцииpop().
Ответ
- Подходит: массив делится пополам, обе половины независимо сортируются, а затем сливаются за \(O(n)\) — именно так работает сортировка слиянием.
- Разделение возможно, но оно не уменьшит общий объем работы: в обеих половинах в сумме все равно нужно проверить все \(n\) элементов, поэтому, как и при прямом обходе, потребуется \(O(n)\) времени.
- Половины нельзя решить независимо: содержимое стека в начале второй половины операций зависит от результата первой, поэтому решить их независимо, не зная результатов друг друга, невозможно.
2. Как быстрое возведение в степень уменьшает число вычислений¶
Рекурсивная функция ниже вычисляет \(x^n\) методом «разделяй и властвуй»:
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).
- Какие значения последовательно принимает параметр
nпри рекурсивных вызовах? - Какие значения поочередно возвращаются на каждом уровне, начиная с самого глубокого?
- Почему результат нужно сначала сохранить в
half, а не дважды записыватьfast_pow(x, n // 2)?
Ответ
-
Параметр последовательно принимает значения
5 → 2 → 1 → 0: на каждом шаге показатель степени уменьшается вдвое до достижения условия завершения. -
При
n = 0возвращается 1; приn = 1возвращается \(1×1×3=3\); приn = 2— \(3×3=9\); приn = 5— \(9×9×3=243\). -
Если записать
fast_pow(x, n // 2)по обе стороны умножения, два рекурсивных вызова будут решать одну и ту же подзадачу. Когда результат сохраняется вhalf, на каждом уровне выполняется только один рекурсивный вызов, а глубина рекурсии составляет примерно \(\log n\); два вызова привели бы к множеству повторных вычислений.
3. Разделение левого и правого поддеревьев по последовательностям обхода¶
Двоичное дерево без повторяющихся узлов имеет следующие последовательности прямого и симметричного обходов:
- прямой обход:
[A, B, D, E, C] - симметричный обход:
[D, B, E, A, C]
Выполните только разделение на уровне корневого узла: продолжать рекурсию и рисовать все дерево не требуется.
- Какой узел является корневым?
- Какие участки симметричного обхода соответствуют левому и правому поддеревьям?
- Какие участки прямого обхода соответствуют левому и правому поддеревьям? Какие непосредственные дочерние узлы имеет корень?
Ответ
-
Первый узел прямого обхода является корневым, поэтому корень —
A. -
Узел
Aделит симметричный обход на две части: левому поддереву соответствует[D, B, E], правому —[C]. -
Левое поддерево содержит 3 узла, поэтому следующие после корня
A3 элемента прямого обхода относятся к левому поддереву:[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}\).
Подсказки
- При n, равном 0, ответ равен 1
- Вычислив x в степени n // 2, сохраните результат в half и не выполняйте рекурсивный вызов второй раз
- Если n < 0, сначала замените x на 1 / x, а n — на -n; в C++ или Java предварительно преобразуйте n в 64-битное целое число, чтобы избежать переполнения при смене знака у минимального 32-битного целого