1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7

# теория · шаг 4 из 7

Теорема Акры-Бацци: подзадачи разного размера

Коротко

Главная теорема берёт только равные подзадачи. Для T(n) = a₁·T(b₁·n) + ... + a_k·T(b_k·n) + f(n) с долями 0 < bᵢ < 1 есть теорема Акры-Бацци. Сначала ищут показатель p из уравнения a₁·b₁^p + ... + a_k·b_k^p = 1: это степень, при которой n^p при делении не теряется и не множится. Потом ответ: T(n) = Θ(n^p·(1 + ∫₁ⁿ f(u)/u^(p+1) du)). Интеграл складывает работу по полосам размеров подзадач, и по нему видно, кто решает - листья, все масштабы поровну или корень.

Если умеешь найти p из уравнения Акры-Бацци и посчитать интеграл для обычных f, листай до «Теперь сам».

Как думаешь, что решают эти три рекуррентности

В прошлом уроке T(n) = T(n/3) + T(2n/3) + n пришлось отложить: подзадачи разного размера, общего b нет. Деревом выходило Θ(n·log n). Прежде чем читать дальше, ответь себе, каков порядок у двух соседок:

T(n) = T(n/2) + T(n/4) + n
T(n) = T(n/2) + T(n/4) + 1

Подзадачи вместе меньше входа: 1/2 + 1/4 = 3/4. Растёт ли T быстрее n во втором случае, где работа одного вызова вне рекурсии постоянна? И какой это будет показатель - целый?

На что это похоже

Представь, что режешь брусок сыра. Длина при разрезе сохраняется: кусок длиной n дал куски n/3 и 2n/3, их длины в сумме те же n. А если после разреза половину выбрасывают, сумма длин падает, и на следующем разрезе упадёт снова в ту же долю. Тогда хочется найти другую меру куска - не длину, а «длину в степени p», - которая при разрезе сохраняется точно. Найдёшь такую меру - узнаешь, сколько «сыра» дойдёт до самых мелких крошек.

Механизм: показатель p и интеграл по масштабам

Показатель. Пусть работы вне рекурсии нет, f = 0. Подставим в рекуррентность T(n) = n^p: справа выйдет a₁·(b₁n)^p + ... = n^p·(a₁·b₁^p + ... + a_k·b_k^p). Это равно n^p ровно тогда, когда сумма в скобках равна единице. Значит n^p - мера, которая при делении на подзадачи не теряется: сумма (размер)^p сохраняется при замене одного ещё не остановившегося узла его детьми. Здесь речь о границе дерева: листья, достигшие базового случая, остаются на ней, а не исчезают при следующем шаге. Для листьев размера 1 эта сумма и есть их число - отсюда главная роль n^p. Каждая доля bᵢ меньше единицы, поэтому bᵢ^p убывает с ростом p, и корень уравнения единственный: его находят делением отрезка пополам. Для равных подзадач уравнение a·(1/b)^p = 1 даёт p = log_b a - водораздел главной теоремы.

Интеграл. При неравных долях группируем вызовы по размеру, а не по глубине: на одной глубине размеры уже могут различаться. Мысленный разрез дерева останавливает каждую ветвь при достижении масштаба около m. В модели без округления сохраняемая мера даёт около n^p/m^p подзадач этого масштаба, а их работа сравнима с n^p·f(m)/m^p. Условие регулярности ниже позволяет сравнивать работу внутри полосы размеров с работой на её границе.

Осталось сложить по масштабам. На каждой полосе [2^j, 2^(j+1)] площадь под 1/u одинакова: ln 2^(j+1) - ln 2^j = ln 2. Поэтому сумма f(u)/u^p по таким полосам соответствует интегралу ∫ f(u)/u^(p+1) du. Полоса размеров не является уровнем дерева. Единица в формуле учитывает базовую работу листьев.

Это объяснение дерева предполагает целое число вызовов и положительный порог остановки. В коде ниже аргументы округляются вниз, и лист размера 0 тоже стоит единицу: считать его вклад как 0^p нельзя. Версия теоремы допускает такие ограниченные смещения аргументов при указанных условиях; картинка меры объясняет порядок роста, а не точное число листьев в коде.

Правило

Теорема Акры-Бацци (Akra, Bazzi, 1998). Для T(n) = Σ aᵢ·T(bᵢ·n) + f(n), где все aᵢ > 0, 0 < bᵢ < 1, базовые значения положительны и ограничены константами, а f положительна, не убывает и удовлетворяет условию регулярности: для каждого фиксированного c > 1 существует постоянная d > 0, такая что при достаточно больших n выполняется f(n/c) ≥ d·f(n). Рассматриваем суммы коэффициентов не меньше единицы; показатель может быть нулём. Это достаточная версия условий для показанных здесь функций; одна лишь оценка сверху многочленом регулярность не заменяет. Найди p из Σ aᵢ·bᵢ^p = 1. Тогда T(n) = Θ(n^p·(1 + ∫₁ⁿ f(u)/u^(p+1) du)).

Разбор: треть и две трети

T(n) = T(n/3) + T(2n/3) + n. Уравнение (1/3)^p + (2/3)^p = 1 угадывается: при p = 1 выходит 1/3 + 2/3 = 1. Интеграл: ∫₁ⁿ u/u² du = ∫₁ⁿ du/u = ln n. Ответ Θ(n·(1 + ln n)) = Θ(n·log n) - то, что дерево давало в прошлом уроке, но без рисования.

Проверим на главной теореме. T(n) = 4·T(n/2) + n: 4·(1/2)^p = 1 даёт p = 2; интеграл ∫₁ⁿ u/u³ du = ∫₁ⁿ u^(-2) du = 1 - 1/n, не больше единицы. Ответ Θ(n²) - случай 1. Все три случая видны по интегралу при f = n^k: под знаком стоит u^(k-p-1). При k < p площадь ограничена (решают листья), при k = p это 1/u и выходит ln n (все уровни поровну), при k > p площадь растёт как n^(k-p), и ответ n^k (решает корень).

Разбор: дробный показатель и ответ, который колеблется

Для T(n/2) + T(n/4) уравнение (1/2)^p + (1/4)^p = 1 в целых числах не решается. Программа находит p делением пополам и сверяет предсказание с точным T(n), посчитанным рекурсией с округлением вниз:

Запись @lru_cache(maxsize=None) перед функцией - декоратор: он оборачивает её механизмом запоминания ответов. При повторном вызове с тем же n Python возвращает сохранённое значение. maxsize=None снимает ограничение числа запомненных аргументов. Здесь это ускоряет вычисление таблицы значений T(n); оно не меняет ни рекуррентность, ни число операций алгоритма, которое она описывает.

akra_bacci.py
import math
from functools import lru_cache


def najti_p(chasti):            # пары (aᵢ, bᵢ), bᵢ - доля входа
    lo, hi = 0.0, 1.0           # здесь сумма aᵢ >= 1, значит p >= 0
    while sum(a * b ** hi for a, b in chasti) > 1:
        hi *= 2                # сначала заключаем корень в отрезок
    for _ in range(100):        # сумма aᵢ·bᵢ^p убывает по p
        p = (lo + hi) / 2
        if sum(a * b ** p for a, b in chasti) > 1:
            lo = p
        else:
            hi = p
    return p


@lru_cache(maxsize=None)
def t_a(n):                     # T(n/3) + T(2n/3) + n
    return 1 if n < 2 else t_a(n // 3) + t_a(2 * n // 3) + n


@lru_cache(maxsize=None)
def t_b(n):                     # T(n/2) + T(n/4) + 1
    return 1 if n < 2 else t_b(n // 2) + t_b(n // 4) + 1


@lru_cache(maxsize=None)
def t_c(n):                     # T(n/2) + T(n/4) + n
    return 1 if n < 2 else t_c(n // 2) + t_c(n // 4) + n


pa = najti_p([(1, 1 / 3), (1, 2 / 3)])
pb = najti_p([(1, 1 / 2), (1, 1 / 4)])
print(f"p для 1/3 + 2/3: {pa:.4f}   p для 1/2 + 1/4: {pb:.4f}")
print("       n   A/(n ln n)   B/n^p    C/n")
for k in range(3, 8):
    n = 10 ** k
    print(f"{n:8}   {t_a(n) / (n * math.log(n)):9.3f}"
          f"   {t_b(n) / n ** pb:6.3f}   {t_c(n) / n:5.3f}")
↳  выводpython3 akra_bacci.py
p для 1/3 + 2/3: 1.0000   p для 1/2 + 1/4: 0.6942
       n   A/(n ln n)   B/n^p    C/n
    1000       1.524    1.463   3.466
   10000       1.544    2.037   3.786
  100000       1.553    1.746   3.893
 1000000       1.539    1.496   3.937
10000000       1.544    2.073   3.975

Точное значение p = log₂((1+√5)/2) ≈ 0,69424. Для B (работа 1) интеграл ∫ u^(-p-1) du ограничен, поскольку p > 0, и ответ Θ(n^p): решают листья. Округлять показатель внутри Θ нельзя: даже малая постоянная разница показателей меняет асимптотический класс. Для C (работа n) интеграл ∫ u^(-p) du растёт как n^(1-p), ведь 0 < p < 1. Ответ n^p·n^(1-p) = Θ(n); решает корень. Отношение подходит к 4: работа по глубинам здесь n·(1 + 3/4 + 9/16 + ...) = 4n.

Граница наивного чтения - столбец B: в показанных пяти замерах отношение колеблется между 1,46 и 2,07. По конечной таблице нельзя доказать ни существование, ни отсутствие предела. Теорема этого и не утверждает: Θ обещает зажать T(n) между двумя постоянными кратными n^p. Округления сдвигают границы базовых случаев, поэтому постоянный множитель в этой таблице заметно меняется, оставаясь совместимым с оценкой.

Что ломается без этого

Первая ошибка - заменить разные подзадачи большей: T(n/3) + T(2n/3) превращают в 2·T(2n/3), главная теорема даёт n^(log₁,₅ 2) ≈ n^1,71, и честный n·log n теряется под завышенной оценкой. Вторая - сложить доли и решить, что раз 1/2 + 1/4 < 1, то ответ линейный при любой f: у B он n^p, а не константа, - сумма долей меньше единицы ещё не значит «решает корень». Третья - применить теорему к f, которая растёт быстрее многочлена: при f(n) = 2ⁿ условие нарушено, вход вдвое меньше роняет f не в постоянное число раз.

Зачем это в работе

Выбор k-го элемента медианой медианРазбирается в разделе 6, глава «Выбор k-го элемента»: Медиана медиан: линейное время в худшем случае даёт T(n) ≤ T(n/5) + T(7n/10) + n. Прочитать его главной теоремой нельзя: подзадачи разные. Сумма долей 1/5 + 7/10 = 0,9, поэтому p < 1, под интегралом u^(-p), площадь растёт как n^(1-p), и равенство для мажорирующей рекуррентности даёт Θ(n). Для исходного неравенства это доказывает верхнюю границу O(n); линейная работа самого шага дополнительно даёт Ω(n). Поэтому полный алгоритм работает за Θ(n). Так же читается слияние, которое режет вход не пополам, а в отношении 1:3: T(n/4) + T(3n/4) + n даёт p = 1 и тот же Θ(n·log n).

Теперь сам

Реши теоремой Акры-Бацци T(n) = 2·T(n/2) + n·log n - тот самый зазор, где главная теорема молчала.

Ответ. 2·(1/2)^p = 1 даёт p = 1. Интеграл ∫₁ⁿ u·ln u/u² du = ∫₁ⁿ ln u/u du. По полосам удвоения: на полосе j значение ln u около j·ln 2, вес полосы ln 2, сумма по j до log₂ n даёт около (ln 2)²·(log₂ n)²/2 = (ln n)²/2. Ответ Θ(n·log² n) - ровно то, что дерево дало в прошлом уроке, и на log n больше, чем обещал бы наивный случай 3.

Главное

Неравные подзадачи сводятся к одному числу - показателю p, при котором сумма (размер подзадачи)^p сохраняется при замене узла его детьми на границе дерева. Уже остановившиеся листья остаются на границе. Дальше всё решает интеграл I(n)=∫₁ⁿ f(u)/u^(p+1) du: ограничен - ответ Θ(n^p); растёт неограниченно - ответ Θ(n^p·I(n)). Множитель n^p остаётся в обоих случаях.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.