--- comments: true --- # 12.6   Упражнения ## 12.6.1   Вопросы для самопроверки ### 1.   Какие задачи подходят для метода «разделяй и властвуй» Ученик хочет решить следующие задачи способом «разделить на две половины, решить каждую отдельно, затем объединить результаты». Для каждой задачи выберите одно из утверждений: «подходит для метода “разделяй и властвуй”», «разделение возможно, но не уменьшает общий объем работы» или «половины нельзя решить независимо». Объясните свой выбор. 1. Отсортировать неупорядоченный массив; 2. найти максимальный элемент массива; 3. последовательно выполнить набор операций стека `push(x)` и `pop()`, выводя элемент, полученный при каждой операции `pop()`. ??? success "Ответ" 1. Подходит: массив делится пополам, обе половины независимо сортируются, а затем сливаются за $O(n)$ — именно так работает сортировка слиянием. 2. Разделение возможно, но оно не уменьшит общий объем работы: в обеих половинах в сумме все равно нужно проверить все $n$ элементов, поэтому, как и при прямом обходе, потребуется $O(n)$ времени. 3. Половины нельзя решить независимо: содержимое стека в начале второй половины операций зависит от результата первой, поэтому решить их независимо, не зная результатов друг друга, невозможно. ### 2.   Как быстрое возведение в степень уменьшает число вычислений Рекурсивная функция ниже вычисляет $x^n$ методом «разделяй и властвуй»: === "Python" ```python title="fast_power.py" def fast_pow(x: int, n: int) -> int: """Быстрое возведение в степень""" if n == 0: return 1 half = fast_pow(x, n // 2) if n % 2 == 0: return half * half return half * half * x ``` === "C++" ```cpp title="fast_power.cpp" /* Быстрое возведение в степень */ int fastPow(int x, int n) { if (n == 0) { return 1; } int half = fastPow(x, n / 2); if (n % 2 == 0) { return half * half; } return half * half * x; } ``` === "Java" ```java title="fast_power.java" /* Быстрое возведение в степень */ int fastPow(int x, int n) { if (n == 0) { return 1; } int half = fastPow(x, n / 2); if (n % 2 == 0) { return half * half; } return half * half * x; } ``` === "C#" ```csharp title="fast_power.cs" /* Быстрое возведение в степень */ int FastPow(int x, int n) { if (n == 0) { return 1; } int half = FastPow(x, n / 2); if (n % 2 == 0) { return half * half; } return half * half * x; } ``` === "Go" ```go title="fast_power.go" /* Быстрое возведение в степень */ func fastPow(x, n int) int { if n == 0 { return 1 } half := fastPow(x, n/2) if n%2 == 0 { return half * half } return half * half * x } ``` === "Swift" ```swift title="fast_power.swift" /* Быстрое возведение в степень */ func fastPow(x: Int, n: Int) -> Int { if n == 0 { return 1 } let half = fastPow(x: x, n: n / 2) if n % 2 == 0 { return half * half } return half * half * x } ``` === "JS" ```javascript title="fast_power.js" /* Быстрое возведение в степень */ function fastPow(x, n) { if (n === 0) { return 1; } const half = fastPow(x, Math.floor(n / 2)); if (n % 2 === 0) { return half * half; } return half * half * x; } ``` === "TS" ```typescript title="fast_power.ts" /* Быстрое возведение в степень */ function fastPow(x: number, n: number): number { if (n === 0) { return 1; } const half = fastPow(x, Math.floor(n / 2)); if (n % 2 === 0) { return half * half; } return half * half * x; } ``` === "Dart" ```dart title="fast_power.dart" /* Быстрое возведение в степень */ int fastPow(int x, int n) { if (n == 0) { return 1; } int half = fastPow(x, n ~/ 2); if (n % 2 == 0) { return half * half; } return half * half * x; } ``` === "Rust" ```rust title="fast_power.rs" /* Быстрое возведение в степень */ fn fast_pow(x: i32, n: i32) -> i32 { if n == 0 { return 1; } let half = fast_pow(x, n / 2); if n % 2 == 0 { return half * half; } half * half * x } ``` === "C" ```c title="fast_power.c" /* Быстрое возведение в степень */ int fastPow(int x, int n) { if (n == 0) { return 1; } int half = fastPow(x, n / 2); if (n % 2 == 0) { return half * half; } return half * half * x; } ``` === "Kotlin" ```kotlin title="fast_power.kt" /* Быстрое возведение в степень */ fun fastPow(x: Int, n: Int): Int { if (n == 0) { return 1 } val half = fastPow(x, n / 2) if (n % 2 == 0) { return half * half } return half * half * x } ``` === "Ruby" ```ruby title="fast_power.rb" ### Быстрое возведение в степень ### def fast_pow(x, n) return 1 if n == 0 half = fast_pow(x, n / 2) return half * half if n.even? half * half * x end ``` Задайте `x = 3` и `n = 5`, затем вычислите результат с помощью этой функции. 1. Какие значения последовательно принимает параметр `n` при рекурсивных вызовах? 2. Какие значения поочередно возвращаются на каждом уровне, начиная с самого глубокого? 3. Почему результат рекурсии нужно сначала сохранить в `half`, а не вызывать одну и ту же подзадачу по одному разу с каждой стороны умножения? ??? 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. Если вызвать одну и ту же подзадачу по одному разу с каждой стороны умножения, два рекурсивных вызова выполнят одинаковые вычисления. Когда результат сохраняется в `half`, на каждом уровне выполняется только один рекурсивный вызов, а глубина рекурсии составляет примерно $\log n$; два вызова привели бы к множеству повторных вычислений. ### 3.   Разделение левого и правого поддеревьев по последовательностям обхода Двоичное дерево без повторяющихся узлов имеет следующие последовательности прямого и симметричного обходов: - прямой обход: `[A, B, D, E, C]` - симметричный обход: `[D, B, E, A, C]` Выполните только разделение на уровне корневого узла: продолжать рекурсию и рисовать все дерево не требуется. 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" }