Понимание разницы во временной сложности двух способов создания подмножеств

Недавно я проводил анализ сложности двух отдельных методов генерации подмножеств данного набора. Для этого вопроса я удаляю все несущественные компоненты кода, а это означает, что обе эти функции являются не более чем «фиктивными» примерами, не имеющими реальной цели. Предположим, что lst представляет список размером n, а curr представляет текущее подмножество и что каждый метод изначально будет вызываться с i, установленным в 0.

Метод 1:

def meth_1(i):
   if i == len(lst):
      return
   
   curr.append(lst[i])
   meth_1(i + 1)
   curr.pop()
   
   meth_1(i + 1)

Метод 2:

def meth_2(i):
   if i == len(lst):
      return
   
   for j in range(i, len(lst)):
      curr.append(lst[j])
      meth_2(j + 1)
      curr.pop()

Хотя обе эти реализации, похоже, дают один и тот же результат, мне сложно тщательно проанализировать их временную сложность в наихудшем случае. В качестве общего подхода к анализу временной сложности рекурсии я суммирую общее количество работы на всех уровнях дерева рекурсии; Я знаю, что в более сложных примерах каждый рекурсивный вызов может выполнять разный объем работы, хотя есть простые способы обойти это.

Применять этот подход к meth_1 — обычное дело. Построение древовидной диаграммы, пожалуй, самый интуитивный способ сделать это:

На диаграмме выше есть три столбца для каждого уровня дерева рекурсии. Узлы представляют общее количество узлов на уровне, уровень представляет уровень дерева (с нулевым индексом), а общая работа представляет собой общую работу, выполненную на этом уровне. Здесь мы предполагаем, что нерекурсивная работа данного вызова является константой c. Мы знаем, что наше рекурсивное дерево будет иметь n + 1 всего уровней, поскольку каждый уровень будет увеличивать i на 1, пока не будет достигнуто значение n. Также стоит отметить, что общий объем работ, выполненных на каждом уровне, можно смоделировать как $\sum_{i=0}^{n+1} 2^ic = \frac{c(1-2^{n+1}) }{-1} = O(2^n)$.

Однако мне трудно проанализировать временную сложность meth_2. В этом случае нарисовать рекурсивную диаграмму гораздо сложнее по нескольким причинам. Во-первых, каждый вызов функции будет выполнять переменное количество вызовов рекурсии, в отличие от meth_1, где каждый вызов выполняет два последовательных рекурсивных вызова. Аналогично, каждый из рекурсивных вызовов meth_2 будет находиться на другом «уровне»; это контрастирует с meth_1, где каждый последующий рекурсивный вызов на уровне i находился на уровне i + 1. Я пишу все это, чтобы спросить: как мне проанализировать временную сложность meth_2?

Какой «результат» они генерируют? При каждом вызове ваших meths append присваивается значение curr, а затем pop оно отключается.

Scott Hunter 01.07.2024 21:04

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

trincot 01.07.2024 21:14

@ScottHunter это игрушечный пример. Я урезал сам код, чтобы изолировать проблему временной сложности.

LateGameLank 01.07.2024 22:51

@trincot Я пропустил это, когда урезал код, но это не влияет на вопрос временной сложности.

LateGameLank 01.07.2024 22:52
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
4
61
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Мы можем определить временную сложность meth_2, используя индуктивные рассуждения.

Предположим, что длина lst равна n. Обозначим временную сложность meth_2(i) как f_i.

Поскольку meth_2(n) останавливается на первом условии, количество операций, которые он выполняет, равно 1.

Будем считать, что f_i = 2^(n-i) для каждого k < i <= n. Вы можете это видеть f_k=1+f_(k+1)+f_(k+2)...+f_k(n) = 1+2^(n-k-1)+2^(n-k-2)+2^(n-n) = 1 + 2^0 + ... + 2^(n-k-1) = 1 + 2^(n-k) - 1 = 2^(n-k).

Потому что f_n = 1 = 2^(n-n), если мы установим k=n-1, это соотношение сохраняется для каждого k < i <= n (только i=n), поэтому оно также верно для i=n-1, и мы можем применять это до i=0. Поскольку мы запускаем meth_2 с i = 0, временная сложность функции равна f_0 = 2^(n-0) = O(2^n).

Индуктивные рассуждения — один из важнейших инструментов анализа сложности рекурсивных функций. Для многих функций вы можете предсказать порядок величины их сложности, подсчитав количество вызовов функций для небольших значений n, а затем подтвердив этот прогноз, применив индуктивные рассуждения.

Другие вопросы по теме