mirror of
https://github.com/krahets/hello-algo.git
synced 2026-08-27 02:27:12 +00:00
294 lines
12 KiB
Markdown
294 lines
12 KiB
Markdown
---
|
||
comments: true
|
||
---
|
||
|
||
# 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"
|
||
|
||
```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`, затем вычислите результат с помощью этой функции.
|
||
|
||
<!-- numbered-subquestions -->
|
||
|
||
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]`
|
||
|
||
Выполните только разделение на уровне корневого узла: продолжать рекурсию и рисовать все дерево не требуется.
|
||
|
||
<!-- 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" }
|