← Все темы
LeetCode 75
Вопросов: 75
Решение задачи
Мы последовательно проходим по обеим строкам одновременно, используя цикл до минимума их длин, затем присоединяем остаток более длинной строки.
Преимущества:
- Оперативность: один проход по символам, сложность по времени O(n+m).
- Эффективность памяти: используется список для накопления символов, итоговая сложность по памяти O(n+m).
Код на Python
Пояснение:
Реализация выбрана из-за минимальной асимптотической сложности: O(n+m) по времени, так как каждый символ обрабатывается один раз, и O(n+m) по памяти для хранения результата. Использование стандартных конструкций Python обеспечивает читабельность и надёжность.
Мы последовательно проходим по обеим строкам одновременно, используя цикл до минимума их длин, затем присоединяем остаток более длинной строки.
Преимущества:
- Оперативность: один проход по символам, сложность по времени O(n+m).
- Эффективность памяти: используется список для накопления символов, итоговая сложность по памяти O(n+m).
Код на Python
def mergeAlternately(word1: str, word2: str) -> str:
merged = []
len1, len2 = len(word1), len(word2)
for i in range(min(len1, len2)):
merged.append(word1[i])
merged.append(word2[i])
if len1 > len2:
merged.append(word1[len2:])
elif len2 > len1:
merged.append(word2[len1:])
return "".join(merged)
if __name__ == "__main__":
w1 = "abc"
w2 = "pqr"
print(mergeAlternately(w1, w2))Пояснение:
Реализация выбрана из-за минимальной асимптотической сложности: O(n+m) по времени, так как каждый символ обрабатывается один раз, и O(n+m) по памяти для хранения результата. Использование стандартных конструкций Python обеспечивает читабельность и надёжность.
Пояснение:
Данное решение использует наблюдение, что если строки имеют общий делитель в виде строки X, то их конкатенация в разных порядках должна совпадать.
Алгоритм:
- Проверяем условие: если
- Если условие выполняется, находим НОД длин строк и возвращаем префикс
Асимптотическая сложность:
Время: O(n), где n – суммарная длина строк, поскольку производится их конкатенация и сравнение.
Память: O(n) для хранения склеенных строк.
Реализация на Python:
Данное решение использует наблюдение, что если строки имеют общий делитель в виде строки X, то их конкатенация в разных порядках должна совпадать.
Алгоритм:
- Проверяем условие: если
str1 + str2 не равно str2 + str1, то общий делитель отсутствует. - Если условие выполняется, находим НОД длин строк и возвращаем префикс
str1 длины gcd(len(str1), len(str2)). Асимптотическая сложность:
Время: O(n), где n – суммарная длина строк, поскольку производится их конкатенация и сравнение.
Память: O(n) для хранения склеенных строк.
Реализация на Python:
import math
def gcd_of_strings(str1, str2):
if str1 + str2 != str2 + str1:
return ""
return str1[:math.gcd(len(str1), len(str2))]
if __name__ == "__main__":
s1 = "ABCABC"
s2 = "ABC"
result = gcd_of_strings(s1, s2)
print(result)
Решение:
Данное решение сначала находит максимальное количество конфет у детей, а затем с помощью спискового включения проверяет для каждого ребёнка, сможет ли он, добавив дополнительные конфеты, достичь или превзойти найденный максимум. Такой подход проходит по массиву один раз для нахождения максимума и один раз для проверки каждого элемента, что обеспечивает минимальную асимптотическую сложность.
Объяснение:
- Время: O(n), где n — количество элементов в массиве.
- Память: O(n) для формирования результирующего массива булевых значений.
Код на Python:
Данное решение сначала находит максимальное количество конфет у детей, а затем с помощью спискового включения проверяет для каждого ребёнка, сможет ли он, добавив дополнительные конфеты, достичь или превзойти найденный максимум. Такой подход проходит по массиву один раз для нахождения максимума и один раз для проверки каждого элемента, что обеспечивает минимальную асимптотическую сложность.
Объяснение:
- Время: O(n), где n — количество элементов в массиве.
- Память: O(n) для формирования результирующего массива булевых значений.
Код на Python:
def kidsWithCandies(candies, extraCandies):
max_candies = max(candies)
return [candy + extraCandies >= max_candies for candy in candies]
if __name__ == "__main__":
candies = [2, 3, 5, 1, 3]
extraCandies = 3
print(kidsWithCandies(candies, extraCandies))
Решение задачи
Описание:
Дано число n и список, представляющий клумбу. Для каждого свободного места проверяем, свободны ли его соседние позиции (учитывая крайние случаи). Если условие выполняется, сажаем цветок, увеличиваем счётчик и при достижении n возвращаем True. Иначе – продолжаем проверку всего списка и в конце сравниваем результат с n.
Сложность:
- Время: O(n), так как проходим по списку один раз.
- Память: O(1), используем несколько дополнительных переменных независимо от размера входных данных.
Код на Python:
Пояснение:
Алгоритм оптимален по времени (O(n)) и использует минимальное дополнительное пространство (O(1)), что делает его эффективным для обработки даже больших клумб.
Описание:
Дано число n и список, представляющий клумбу. Для каждого свободного места проверяем, свободны ли его соседние позиции (учитывая крайние случаи). Если условие выполняется, сажаем цветок, увеличиваем счётчик и при достижении n возвращаем True. Иначе – продолжаем проверку всего списка и в конце сравниваем результат с n.
Сложность:
- Время: O(n), так как проходим по списку один раз.
- Память: O(1), используем несколько дополнительных переменных независимо от размера входных данных.
Код на Python:
def can_place_flowers(flowerbed, n):
count = 0
for i in range(len(flowerbed)):
if flowerbed[i] == 0 and (i == 0 or flowerbed[i-1] == 0) and (i == len(flowerbed)-1 or flowerbed[i+1] == 0):
flowerbed[i] = 1
count += 1
if count >= n:
return True
return count >= n
if __name__ == "__main__":
# Примеры тестов
print(can_place_flowers([1, 0, 0, 0, 1], 1)) # True
print(can_place_flowers([1, 0, 0, 0, 1], 2)) # False
Пояснение:
Алгоритм оптимален по времени (O(n)) и использует минимальное дополнительное пространство (O(1)), что делает его эффективным для обработки даже больших клумб.
Решение задачи «Разворот гласных в строке»
Пояснение:
Для решения используется алгоритм с двумя указателями, который проходит по строке один раз, осуществляя поиск гласных с обоих концов. Когда оба указателя указывают на гласные, их символы меняются местами. Такой алгоритм имеет время работы O(n) и использует O(n) дополнительной памяти для хранения преобразованной строки.
Код на Python:
Пояснение:
Для решения используется алгоритм с двумя указателями, который проходит по строке один раз, осуществляя поиск гласных с обоих концов. Когда оба указателя указывают на гласные, их символы меняются местами. Такой алгоритм имеет время работы O(n) и использует O(n) дополнительной памяти для хранения преобразованной строки.
Код на Python:
def reverseVowels(s: str) -> str:
vowels = set(["a", "e", "i", "o", "u", "A", "E", "I", "O", "U"])
s = list(s)
left, right = 0, len(s) - 1
while left < right:
while left < right and s[left] not in vowels:
left += 1
while left < right and s[right] not in vowels:
right -= 1
if left < right:
s[left], s[right] = s[right], s[left]
left += 1
right -= 1
return ''.join(s)
if __name__ == ''__main__'':
test_str = "Hello, World!"
print(reverseVowels(test_str))
Пояснение решения
Задача реализована посредством встроенных функций Python:
- split() разбивает строку по пробельным символам, автоматически удаляя лишние пробелы.
- reversed() разворачивает последовательность слов.
- " ".join() объединяет слова, вставляя ровно один пробел между ними.
Сложность:
- По времени: O(n), где n – длина строки, так как каждое слово обрабатывается один раз.
- По памяти: O(n), дополнительная память используется для хранения списка слов.
Реализация на Python:
Задача реализована посредством встроенных функций Python:
- split() разбивает строку по пробельным символам, автоматически удаляя лишние пробелы.
- reversed() разворачивает последовательность слов.
- " ".join() объединяет слова, вставляя ровно один пробел между ними.
Сложность:
- По времени: O(n), где n – длина строки, так как каждое слово обрабатывается один раз.
- По памяти: O(n), дополнительная память используется для хранения списка слов.
Реализация на Python:
def reverse_words(s: str) -> str:
# Trim spaces and split by whitespace
words = s.split()
return " ".join(reversed(words))
if __name__ == "__main__":
s = " Hello world! "
print(reverse_words(s))
Решение задачи
Подход заключается в том, чтобы сначала вычислить произведение всех элементов слева от каждого индекса, а затем дополнить их накопленным правым произведением. Решение реализовано в два прохода по массиву без использования операции деления.
Пояснение по сложности:
- Время: O(n) – два последовательных прохода по массиву.
- Память: O(n) – дополнительное пространство для результирующего массива (при этом вспомогательные переменные занимают O(1)).
Подход заключается в том, чтобы сначала вычислить произведение всех элементов слева от каждого индекса, а затем дополнить их накопленным правым произведением. Решение реализовано в два прохода по массиву без использования операции деления.
Пояснение по сложности:
- Время: O(n) – два последовательных прохода по массиву.
- Память: O(n) – дополнительное пространство для результирующего массива (при этом вспомогательные переменные занимают O(1)).
def product_except_self(nums):
n = len(nums)
result = [1] * n
left = 1
for i in range(n):
result[i] = left
left *= nums[i]
right = 1
for i in range(n - 1, -1, -1):
result[i] *= right
right *= nums[i]
return result
if __name__ == "__main__":
nums = [1, 2, 3, 4]
print(product_except_self(nums)) # [24, 12, 8, 6]
Решение задачи Increasing Triplet Subsequence
Пояснение:
Описание алгоритма: Мы проходим по массиву один раз, сохраняя два наименьших кандидата для первого и второго элемента потенциальной возрастающей последовательности. При нахождении элемента, большего второго кандидата, можно утверждать, что существует подпоследовательность из трёх возрастающих чисел.
- Временная сложность: O(n) (одинарный проход по массиву)
- Пространственная сложность: O(1) (используются лишь несколько переменных)
Код на Python:
Пояснение:
Описание алгоритма: Мы проходим по массиву один раз, сохраняя два наименьших кандидата для первого и второго элемента потенциальной возрастающей последовательности. При нахождении элемента, большего второго кандидата, можно утверждать, что существует подпоследовательность из трёх возрастающих чисел.
- Временная сложность: O(n) (одинарный проход по массиву)
- Пространственная сложность: O(1) (используются лишь несколько переменных)
Код на Python:
import sys
def increasingTriplet(nums: list[<int>]) -> bool:
first = second = float("inf")
for n in nums:
if n <= first:
first = n
elif n <= second:
second = n
else:
return True
return False
if __name__ == '__main__':
nums = [1, 2, 3, 4, 5]
print(increasingTriplet(nums))
Решение задачи:
Пояснение:
- Алгоритм проходит по массиву однократно, используя два указателя: один для чтения повторяющихся символов и другой для записи результата.
- Сложность по времени составляет O(n), где n — длина массива, так как каждый символ обрабатывается один раз.
- Сложность по памяти составляет O(1) дополнительной памяти, поскольку модификация происходит на месте.
def compress(chars):
read = 0
write = 0
n = len(chars)
while read < n:
curr = chars[read]
count = 0
while read < n and chars[read] == curr:
read += 1
count += 1
chars[write] = curr
write += 1
if count > 1:
for ch in str(count):
chars[write] = ch
write += 1
return write
# Пример использования
if __name__ == "__main__":
arr = ['a', 'a', 'b', 'b', 'c', 'c', 'c']
new_length = compress(arr)
print(arr[:new_length]) # Вывод: ['a', '2', 'b', '2', 'c', '3']
Пояснение:
- Алгоритм проходит по массиву однократно, используя два указателя: один для чтения повторяющихся символов и другой для записи результата.
- Сложность по времени составляет O(n), где n — длина массива, так как каждый символ обрабатывается один раз.
- Сложность по памяти составляет O(1) дополнительной памяти, поскольку модификация происходит на месте.
Описание решения:
Данная реализация использует два указателя для обработки массива на месте. Один указатель (last_nonzero) отвечает за позицию вставки следующего ненулевого элемента, а второй (i) проходит по всему массиву. Если элемент не равен нулю, происходит обмен с элементом на позиции last_nonzero, и указатель last_nonzero сдвигается на одну позицию. Такой подход сохраняет относительный порядок ненулевых элементов и перемещает нули в конец массива без использования дополнительной памяти.
Сложность:
- Время: O(n) – каждый элемент обрабатывается один раз.
- Память: O(1) – используется константное количество дополнительной памяти.
Код на Python:
Данная реализация использует два указателя для обработки массива на месте. Один указатель (last_nonzero) отвечает за позицию вставки следующего ненулевого элемента, а второй (i) проходит по всему массиву. Если элемент не равен нулю, происходит обмен с элементом на позиции last_nonzero, и указатель last_nonzero сдвигается на одну позицию. Такой подход сохраняет относительный порядок ненулевых элементов и перемещает нули в конец массива без использования дополнительной памяти.
Сложность:
- Время: O(n) – каждый элемент обрабатывается один раз.
- Память: O(1) – используется константное количество дополнительной памяти.
Код на Python:
def move_zeroes(nums):
last_nonzero = 0
for i in range(len(nums)):
if nums[i] != 0:
nums[last_nonzero], nums[i] = nums[i], nums[last_nonzero]
last_nonzero += 1
# Пример использования
if __name__ == "'__main__'":
arr = [0, 1, 0, 3, 12]
move_zeroes(arr)
print(arr) # Вывод: [1, 3, 12, 0, 0]
Решение задачи «Является ли строка подпоследовательностью»
Описание:
- Мы используем два указателя для строк s и t.
- Если текущий символ s совпадает с символом t, перемещаем указатель s.
- Если указатель s достигает конца строки s, значит, s является подпоследовательностью t.
Временная сложность: O(n + m), где n = длина s, m = длина t.
Памятная сложность: O(1) — используются лишь несколько переменных.
Код на Python:
Пояснение реализации:
Мы выбираем двухуказательную стратегию, позволяющую пройти по обеим строкам за один проход.
- Такой подход минимизирует асимптотическую сложность, делая алгоритм линейным по времени.
- Память используется оптимально, так как дополнительное использование памяти отсутствует.
Описание:
- Мы используем два указателя для строк s и t.
- Если текущий символ s совпадает с символом t, перемещаем указатель s.
- Если указатель s достигает конца строки s, значит, s является подпоследовательностью t.
Временная сложность: O(n + m), где n = длина s, m = длина t.
Памятная сложность: O(1) — используются лишь несколько переменных.
Код на Python:
def is_subsequence(s: str, t: str) -> bool:
i, j = 0, 0
while i < len(s) and j < len(t):
if s[i] == t[j]:
i += 1
j += 1
return i == len(s)
if __name__ == '__main__':
s = input().strip()
t = input().strip()
print(is_subsequence(s, t))
Пояснение реализации:
Мы выбираем двухуказательную стратегию, позволяющую пройти по обеим строкам за один проход.
- Такой подход минимизирует асимптотическую сложность, делая алгоритм линейным по времени.
- Память используется оптимально, так как дополнительное использование памяти отсутствует.
Решение задачи:
Пояснение:
- Почему такой подход? Использование двух указателей позволяет пройти массив за один проход, постоянно обновляя максимальную площадь, без необходимости проверки всех возможных пар.
- Сложность по времени: O(n), так как каждый элемент массива обрабатывается не более одного раза.
- Сложность по памяти: O(1), так как используется фиксированный набор переменных независимо от размера входного массива.
Контейнер с наибольшей площадью воды решается с помощью двух указателей, которые начинаются с краёв массива. При этом на каждой итерации мы выбираем сдвиг указателя, у которого высота меньше, поскольку именно она ограничивает возможное увеличение площади. Такой жадный подход гарантирует, что мы не упускаем ни одного потенциального оптимального случая.
def max_area(heights):
left = 0
right = len(heights) - 1
best = 0
while left < right:
width = right - left
if heights[left] < heights[right]:
best = max(best, heights[left] * width)
left += 1
else:
best = max(best, heights[right] * width)
right -= 1
return best
# Пример использования:
if __name__ == '__main__':
sample_heights = [1,8,6,2,5,4,8,3,7]
print(max_area(sample_heights))
Пояснение:
- Почему такой подход? Использование двух указателей позволяет пройти массив за один проход, постоянно обновляя максимальную площадь, без необходимости проверки всех возможных пар.
- Сложность по времени: O(n), так как каждый элемент массива обрабатывается не более одного раза.
- Сложность по памяти: O(1), так как используется фиксированный набор переменных независимо от размера входного массива.
Решение задачи
Мы используем подход с подсчётом частот элементов через collections.Counter. Для каждого числа находим комплемент (k - число) и формируем пару, если комплемент присутствует. В случае, когда число равно своему комплементу (т.е. k равен удвоенному значению), количество пар определяется как целочисленное деление частоты на 2. Такой алгоритм гарантирует однократное использование каждого элемента и имеет линейную асимптотику по времени O(n) и O(n) по памяти.
Реализация на Python:
Пояснение:
- Временная сложность: O(n), где n — длина массива, так как каждый элемент обрабатывается один раз.
- Памятная сложность: O(n) за счёт хранения счётчика частот элементов.
Мы используем подход с подсчётом частот элементов через collections.Counter. Для каждого числа находим комплемент (k - число) и формируем пару, если комплемент присутствует. В случае, когда число равно своему комплементу (т.е. k равен удвоенному значению), количество пар определяется как целочисленное деление частоты на 2. Такой алгоритм гарантирует однократное использование каждого элемента и имеет линейную асимптотику по времени O(n) и O(n) по памяти.
Реализация на Python:
import collections
def max_k_sum_pairs(nums, k):
count = collections.Counter(nums)
pairs = 0
for num in list(count.keys()):
comp = k - num
if comp in count:
if comp == num:
pairs += count[num] // 2
else:
pairs += min(count[num], count[comp])
count.pop(num, None)
count.pop(comp, None)
return pairs
if __name__ == "__main__":
nums = [1, 2, 3, 4, 3, 2, 1, 5]
k = 4
print(max_k_sum_pairs(nums, k))
Пояснение:
- Временная сложность: O(n), где n — длина массива, так как каждый элемент обрабатывается один раз.
- Памятная сложность: O(n) за счёт хранения счётчика частот элементов.
Краткое решение задачи
Мы используем метод скользящего окна для нахождения подмассива длины k с максимальной суммой. Затем делим найденную максимальную сумму на k для получения максимального среднего значения.
Обоснование решения:
- Сложность по времени: O(n), так как каждый элемент обрабатывается один раз.
- Сложность по памяти: O(1), используется фиксированное количество переменных.
Реализация на Python:
Мы используем метод скользящего окна для нахождения подмассива длины k с максимальной суммой. Затем делим найденную максимальную сумму на k для получения максимального среднего значения.
Обоснование решения:
- Сложность по времени: O(n), так как каждый элемент обрабатывается один раз.
- Сложность по памяти: O(1), используется фиксированное количество переменных.
Реализация на Python:
def findMaxAverageSubarray(nums, k):
curr_sum = sum(nums[:k])
max_sum = curr_sum
for i in range(k, len(nums)):
curr_sum += nums[i] - nums[i - k]
if curr_sum > max_sum:
max_sum = curr_sum
return max_sum / k
# Пример использования:
if __name__ == '__main__':
nums = [1, 12, -5, -6, 50, 3]
k = 4
print(findMaxAverageSubarray(nums, k))
Описание решения:
Данная задача решается с использованием метода скользящего окна. Вместо того, чтобы пересчитывать количество гласных для каждой подстроки, мы обновляем счётчик при смещении окна, добавляя символ, попавший в окно, и удаляя символ, вышедший из него. Это обеспечивает асимптотику по времени O(n), где n – длина строки, а по памяти O(1) – используются лишь несколько переменных для хранения счётчиков.
Код на Python:
Пояснение:
- Сложность по времени: O(n) – каждый символ строки обрабатывается один раз.
- Сложность по памяти: O(1) – используется фиксированное количество переменных независимо от размера входных данных.
Данная задача решается с использованием метода скользящего окна. Вместо того, чтобы пересчитывать количество гласных для каждой подстроки, мы обновляем счётчик при смещении окна, добавляя символ, попавший в окно, и удаляя символ, вышедший из него. Это обеспечивает асимптотику по времени O(n), где n – длина строки, а по памяти O(1) – используются лишь несколько переменных для хранения счётчиков.
Код на Python:
def max_vowels(s: str, k: int) -> int:
vowels = set("aeiouAEIOU")
count = 0
max_count = 0
for i in range(len(s)):
if s[i] in vowels:
count += 1
if i >= k and s[i-k] in vowels:
count -= 1
if i >= k-1:
max_count = max(max_count, count)
return max_count
if __name__ == "__main__":
s = "abciiidef"
k = 3
result = max_vowels(s, k)
print(result)
Пояснение:
- Сложность по времени: O(n) – каждый символ строки обрабатывается один раз.
- Сложность по памяти: O(1) – используется фиксированное количество переменных независимо от размера входных данных.
Решение задачи:
Мы используем скользящее окно (sliding window), чтобы найти максимальную длину подмассива, в котором можно заменить не более k нулей на единицы. Данный подход имеет временную сложность O(n) и дополнительную память O(1).
Пояснение:
При проходе по массиву мы расширяем правую границу окна и увеличиваем счётчик нулей. Когда количество нулей превышает k, сдвигаем левую границу до тех пор, пока условие не выполнится. Таким образом, каждое значение обрабатывается ровно один раз, что обеспечивает асимптотическую сложность O(n). Дополнительная память используется константно (O(1)), так как задействуются только несколько переменных.
Мы используем скользящее окно (sliding window), чтобы найти максимальную длину подмассива, в котором можно заменить не более k нулей на единицы. Данный подход имеет временную сложность O(n) и дополнительную память O(1).
def max_consecutive_ones(nums: list[int], k: int) -> int:
left = 0
zero_count = 0
max_length = 0
for right in range(len(nums)):
if nums[right] == 0:
zero_count += 1
while zero_count > k:
if nums[left] == 0:
zero_count -= 1
left += 1
max_length = max(max_length, right - left + 1)
return max_length
# Пример использования:
nums = [1, 0, 1, 1, 0, 1]
k = 1
print(max_consecutive_ones(nums, k))
Пояснение:
При проходе по массиву мы расширяем правую границу окна и увеличиваем счётчик нулей. Когда количество нулей превышает k, сдвигаем левую границу до тех пор, пока условие не выполнится. Таким образом, каждое значение обрабатывается ровно один раз, что обеспечивает асимптотическую сложность O(n). Дополнительная память используется константно (O(1)), так как задействуются только несколько переменных.
Описание решения:
В данном решении используется метод скользящего окна, позволяющий за один проход найти максимальное расстояние между двумя индексами, где в окне содержится не более одного нуля. Если в окне нулей нет, то согласно условию задачи, удаление обязано убрать единицу, поэтому итоговая длина равна длине окна минус единица. Это решение имеет временную сложность O(n) и использует постоянное количество дополнительной памяти O(1).
Код на modern python:
Пояснение:
- Скользящее окно эффективно расширяется по элементам массива, контролируя число нулей.
- Если в текущем окне больше одного нуля, начинаем сдвигать левую границу до восстановления допустимого состояния.
- В случае, если окно не содержит нулей, мы вынуждены удалить одну единицу согласно условию, поэтому итоговая длина окна уменьшается на один.
- Решение проходит по массиву один раз, что даёт линейную временную сложность O(n), а дополнительная память используется в константном объёме O(1).
В данном решении используется метод скользящего окна, позволяющий за один проход найти максимальное расстояние между двумя индексами, где в окне содержится не более одного нуля. Если в окне нулей нет, то согласно условию задачи, удаление обязано убрать единицу, поэтому итоговая длина равна длине окна минус единица. Это решение имеет временную сложность O(n) и использует постоянное количество дополнительной памяти O(1).
Код на modern python:
def longest_subarray(nums):
n = len(nums)
left = 0
zeros = 0
max_len = 0
for right in range(n):
if nums[right] == 0:
zeros += 1
while zeros > 1:
if nums[left] == 0:
zeros -= 1
left += 1
# Если нулей нет, то удаляем единицу, поэтому длина окна уменьшается на 1
curr_len = (right - left) if zeros == 0 else (right - left)
max_len = max(max_len, curr_len)
return max_len
# Пример использования:
if __name__ == '__main__':
nums = [1, 1, 0, 1]
print(longest_subarray(nums)) # Выведет 3
Пояснение:
- Скользящее окно эффективно расширяется по элементам массива, контролируя число нулей.
- Если в текущем окне больше одного нуля, начинаем сдвигать левую границу до восстановления допустимого состояния.
- В случае, если окно не содержит нулей, мы вынуждены удалить одну единицу согласно условию, поэтому итоговая длина окна уменьшается на один.
- Решение проходит по массиву один раз, что даёт линейную временную сложность O(n), а дополнительная память используется в константном объёме O(1).
Решение задачи
Подсчитываем текущую высоту суммированием элементов массива gain. После каждого изменения обновляем максимум, если текущая высота превысила предыдущий максимум.
Код на Python
Пояснение
- Используется один проход по массиву, что обеспечивает асимптотическую сложность по времени
- Дополнительная память используется только для хранения нескольких переменных, что даёт сложность по памяти
Подсчитываем текущую высоту суммированием элементов массива gain. После каждого изменения обновляем максимум, если текущая высота превысила предыдущий максимум.
Код на Python
def highest_altitude(gain):
altitude = 0
max_altitude = 0
for g in gain:
altitude += g
if altitude > max_altitude:
max_altitude = altitude
return max_altitude
if __name__ == "__main__":
gain = [ -5, 1, 5, 0, -7 ]
print(highest_altitude(gain))
Пояснение
- Используется один проход по массиву, что обеспечивает асимптотическую сложность по времени
O(n). - Дополнительная память используется только для хранения нескольких переменных, что даёт сложность по памяти
O(1).
Решение задачи «Find Pivot Index»
Подход: Решение использует один проход по массиву. Сначала вычисляем сумму всего массива, затем для каждого элемента проверяем, равна ли сумма слева сумме элементов справа. Если условие выполняется, сразу возвращаем текущий индекс. Если обход завершён без нахождения опорного индекса – возвращаем -1.
Сложность:
- Время: O(n), один проход по массиву.
- Память: O(1), используется постоянное количество переменных.
Код на Python:
Подход: Решение использует один проход по массиву. Сначала вычисляем сумму всего массива, затем для каждого элемента проверяем, равна ли сумма слева сумме элементов справа. Если условие выполняется, сразу возвращаем текущий индекс. Если обход завершён без нахождения опорного индекса – возвращаем -1.
Сложность:
- Время: O(n), один проход по массиву.
- Память: O(1), используется постоянное количество переменных.
Код на Python:
def pivot_index(nums):
total_sum = sum(nums)
left_sum = 0
for i, num in enumerate(nums):
if left_sum == total_sum - left_sum - num:
return i
left_sum += num
return -1
# Пример использования
if __name__ == '__main__':
nums = [1, 7, 3, 6, 5, 6]
print(pivot_index(nums)) # Ожидаемый вывод: 3
Пояснение задачи
Задача требует вернуть два массива, в первом – элементы, присутствующие в первом массиве, но отсутствующие во втором, во втором – наоборот. Решение использует list comprehension с предварительным преобразованием второго массива в множество для быстрого поиска, что обеспечивает асимптотическую сложность O(n+m) по времени и O(n+m) по памяти.
Код на python
Сложность решения
- Время: O(n + m), так как перебор элементов осуществляется один раз для каждого массива.
- Память: O(n + m) для хранения множеств и результирующих списков.
Задача требует вернуть два массива, в первом – элементы, присутствующие в первом массиве, но отсутствующие во втором, во втором – наоборот. Решение использует list comprehension с предварительным преобразованием второго массива в множество для быстрого поиска, что обеспечивает асимптотическую сложность O(n+m) по времени и O(n+m) по памяти.
Код на python
def array_difference(arr1, arr2):
# Возвращает два массива:
# - элементы, присутствующие в arr1, но отсутствующие в arr2
# - элементы, присутствующие в arr2, но отсутствующие в arr1
set2 = set(arr2)
diff1 = [x for x in arr1 if x not in set2]
set1 = set(arr1)
diff2 = [x for x in arr2 if x not in set1]
return [diff1, diff2]
# Пример использования:
if __name__ == ''__main__'':
arr1 = [1, 2, 3, 4, 5]
arr2 = [4, 5, 6, 7]
result = array_difference(arr1, arr2)
print(result) # Вывод: [[1, 2, 3], [6, 7]]
Сложность решения
- Время: O(n + m), так как перебор элементов осуществляется один раз для каждого массива.
- Память: O(n + m) для хранения множеств и результирующих списков.
Код решения задачи
Пояснение
Решение основано на использовании модуля collections.Counter, который позволяет за один проход по массиву подсчитать количество вхождений каждого элемента. Далее сравниваются длина списка значений и длина множества этих значений. Если числа вхождений уникальны, множества совпадают по размеру с исходным списком, что приводит к возвращению true, иначе false.
Сложность
- Время: O(n), так как осуществляется один проход по массиву.
- Память: O(n), для хранения счетчиков в Counter.
from collections import Counter
def unique_occurrences(arr):
# Подсчитываем количество вхождений каждого числа
counts = Counter(arr)
# Проверяем, что количества вхождений уникальны
return len(counts.values()) == len(set(counts.values()))
if __name__ == '__main__':
arr = [1, 2, 2, 1, 1, 3]
print(unique_occurrences(arr))
Пояснение
Решение основано на использовании модуля collections.Counter, который позволяет за один проход по массиву подсчитать количество вхождений каждого элемента. Далее сравниваются длина списка значений и длина множества этих значений. Если числа вхождений уникальны, множества совпадают по размеру с исходным списком, что приводит к возвращению true, иначе false.
Сложность
- Время: O(n), так как осуществляется один проход по массиву.
- Память: O(n), для хранения счетчиков в Counter.
Решение задачи
Идея решения:
- Сначала проверяем, равны ли длины строк.
- Затем с помощью
- Если множества символов не совпадают, возвращаем
- Наконец, сравниваем отсортированные списки количеств символов – они должны совпадать, так как разрешена перестановка количеств.
Ассимптотическая сложность:
- По времени: O(n) (проход по строкам).
- По памяти: O(n) в худшем случае.
Код на Python:
Данный код использует минимальное количество операций и стандартную библиотеку Python, что обеспечивает оптимальную асимптотическую сложность по времени (O(n)) и памяти.
Идея решения:
- Сначала проверяем, равны ли длины строк.
- Затем с помощью
collections.Counter получаем частотные словари. - Если множества символов не совпадают, возвращаем
False. - Наконец, сравниваем отсортированные списки количеств символов – они должны совпадать, так как разрешена перестановка количеств.
Ассимптотическая сложность:
- По времени: O(n) (проход по строкам).
- По памяти: O(n) в худшем случае.
Код на Python:
def are_close(word1, word2):
if len(word1) != len(word2):
return False
from collections import Counter
count1 = Counter(word1)
count2 = Counter(word2)
if set(word1) != set(word2):
return False
return sorted(count1.values()) == sorted(count2.values())
if __name__ == __'main'__:
print(are_close("abc", "bca")) # True
print(are_close("a", "aa")) # False
Данный код использует минимальное количество операций и стандартную библиотеку Python, что обеспечивает оптимальную асимптотическую сложность по времени (O(n)) и памяти.
Решение задачи:
Для нахождения количества пар, где строка совпадает со столбцом, мы используем два счётчика (Counter) из стандартной библиотеки collections. Сначала преобразуем каждую строку матрицы в кортеж и подсчитаем их вхождения. Затем аналогичным образом формируем кортежи для каждого столбца. После этого для каждого кортежа строки умножаем число её вхождений на число вхождений идентичного кортежа среди столбцов. Такое решение имеет асимптотическую сложность по времени O(n²) и по памяти O(n²) в худшем случае, что оптимально для данной задачи.
Для нахождения количества пар, где строка совпадает со столбцом, мы используем два счётчика (Counter) из стандартной библиотеки collections. Сначала преобразуем каждую строку матрицы в кортеж и подсчитаем их вхождения. Затем аналогичным образом формируем кортежи для каждого столбца. После этого для каждого кортежа строки умножаем число её вхождений на число вхождений идентичного кортежа среди столбцов. Такое решение имеет асимптотическую сложность по времени O(n²) и по памяти O(n²) в худшем случае, что оптимально для данной задачи.
def equal_pairs(matrix):
from collections import Counter
n = len(matrix)
row_count = Counter(tuple(row) for row in matrix)
col_count = Counter(tuple(matrix[i][j] for i in range(n)) for j in range(n))
result = 0
for key in row_count:
result += row_count[key] * col_count.get(key, 0)
return result
if __name__ == "__main__":
arr = [[1, 2, 1],
[2, 2, 2],
[1, 2, 1]]
print(equal_pairs(arr)) # Expected output: 3
Код решения на Python
Пояснение
- Реализация: Использована структура данных «stack» для накопления символов. При встрече символа '*' из стека удаляется последний добавленный символ, что соответствует условию задачи.
- Сложность по времени: O(n), где n — длина строки, так как проход по строке выполняется один раз.
- Сложность по памяти: O(n), в худшем случае весь текст хранится в стеке.
def removeStars(s: str) -> str:
stack = []
for char in s:
if char == '*':
if stack:
stack.pop()
else:
stack.append(char)
return "".join(stack)
if __name__ == "__main__":
test_str = "leet**cod*e"
result = removeStars(test_str)
print(result) # Expected output: "lecoe"
Пояснение
- Реализация: Использована структура данных «stack» для накопления символов. При встрече символа '*' из стека удаляется последний добавленный символ, что соответствует условию задачи.
- Сложность по времени: O(n), где n — длина строки, так как проход по строке выполняется один раз.
- Сложность по памяти: O(n), в худшем случае весь текст хранится в стеке.
Решение задачи
Описание: Мы используем стек для моделирования столкновений. Итеративно проходим по каждому астероиду и, если возникает столкновение (текущий астероид движется влево, а верхний элемент стека – вправо), сравниваем их размеры. Меньший разрушается, при равенстве – оба уничтожаются. Если столкновений не происходит, добавляем астероид в стек.
Код:
Пояснение:
- Выбор стека: позволяет за один проход по массиву моделировать столкновения, сравнивая текущий астероид с последним в стеке, что соответствует реальному процессу.
- Асимптотическая сложность: Время: O(n), так как каждый элемент обрабатывается не более одного раза. Память: O(n) для стека, в худшем случае все астероиды остаются.
Описание: Мы используем стек для моделирования столкновений. Итеративно проходим по каждому астероиду и, если возникает столкновение (текущий астероид движется влево, а верхний элемент стека – вправо), сравниваем их размеры. Меньший разрушается, при равенстве – оба уничтожаются. Если столкновений не происходит, добавляем астероид в стек.
Код:
def asteroidCollision(asteroids: list[int]) -> list[int]:
stack = []
for a in asteroids:
collision = False
# Пока есть возможность столкновения: справа движущийся астероид в стеке и левый текущий астероид
while stack and a < 0 and stack[-1] > 0:
if abs(stack[-1]) < abs(a):
# Верхний астероид меньше - уничтожается
stack.pop()
continue
elif abs(stack[-1]) == abs(a):
# Оба равны по размеру - уничтожаются
stack.pop()
collision = True
break
if not collision:
stack.append(a)
return stack
if __name__ == "__main__":
asteroids = [5,10,-5]
print(asteroidCollision(asteroids))
Пояснение:
- Выбор стека: позволяет за один проход по массиву моделировать столкновения, сравнивая текущий астероид с последним в стеке, что соответствует реальному процессу.
- Асимптотическая сложность: Время: O(n), так как каждый элемент обрабатывается не более одного раза. Память: O(n) для стека, в худшем случае все астероиды остаются.
Описание решения:
Мы реализуем декодирование строки с помощью стека. Каждый раз, когда встречается число или символ "[", текущая строка и множитель сохраняются в стеке. При встрече "]" извлекается последний элемент стека, и текущая строка повторяется необходимое число раз, затем объединяется с ранее сохранённой строкой. Такой подход обеспечивает однократный проход по символам исходной строки, что приводит к временной сложности O(n) и пространственной сложности O(n) в худшем случае.
Код на Python:
Пояснение:
Данное решение реализовано с использованием стандартных структур Python и обеспечивает минимальную асимптотику.
Временная сложность:
Память:
Мы реализуем декодирование строки с помощью стека. Каждый раз, когда встречается число или символ "[", текущая строка и множитель сохраняются в стеке. При встрече "]" извлекается последний элемент стека, и текущая строка повторяется необходимое число раз, затем объединяется с ранее сохранённой строкой. Такой подход обеспечивает однократный проход по символам исходной строки, что приводит к временной сложности O(n) и пространственной сложности O(n) в худшем случае.
Код на Python:
def decodeString(s: str) -> str:
stack = []
current = ""
num = 0
for c in s:
if c.isdigit():
num = num * 10 + int(c)
elif c == "[":
stack.append((current, num))
current = ""
num = 0
elif c == "]":
prev_str, repeat = stack.pop()
current = prev_str + current * repeat
else:
current += c
return current
# Пример использования
if __name__ == "__main__":
input_str = "3[a2[c]]"
print(decodeString(input_str)) #!-- Oжидаемый вывод: accaccacc
Пояснение:
Данное решение реализовано с использованием стандартных структур Python и обеспечивает минимальную асимптотику.
Временная сложность:
O(n), так как каждый символ строки обрабатывается один раз. Память:
O(n) за счёт стека в худшем случае.
Реализация задачи:
Пояснение:
Решение использует структуру данных deque из стандартной библиотеки collections, что позволяет эффективно добавлять элементы в конец и удалять их с начала очереди.
Каждый вызов метода ping(t) добавляет новое значение времени и удаляет все вызовы, которые произошли до момента t-3000. Это дает асимптотическую сложность O(1) в среднем для каждого запроса, так как каждый элемент добавляется и удаляется из deque единожды. По памяти сложность равна O(n) в худшем случае, где n – количество запросов, находящихся в интервале 3000 миллисекунд.
from collections import deque
class RecentCounter:
def __init__(self):
self.q = deque()
def ping(self, t: int) -> int:
self.q.append(t)
while self.q and self.q[0] < t - 3000:
self.q.popleft()
return len(self.q)
if __name__ == "__main__":
counter = RecentCounter()
print(counter.ping(1)) # -> 1
print(counter.ping(100)) # -> 2
print(counter.ping(3001)) # -> 3
print(counter.ping(3002)) # -> 3
Пояснение:
Решение использует структуру данных deque из стандартной библиотеки collections, что позволяет эффективно добавлять элементы в конец и удалять их с начала очереди.
Каждый вызов метода ping(t) добавляет новое значение времени и удаляет все вызовы, которые произошли до момента t-3000. Это дает асимптотическую сложность O(1) в среднем для каждого запроса, так как каждый элемент добавляется и удаляется из deque единожды. По памяти сложность равна O(n) в худшем случае, где n – количество запросов, находящихся в интервале 3000 миллисекунд.
Решение задачи
Мы используем два очереди для хранения индексов депутатов каждой фракции. Каждый раунд сравниваются первые элементы обеих очередей: депутат с меньшим индексом аннулирует голос противника и добавляется в очередь с новым индексом (исходный индекс + длина строки) для следующего раунда. Такой подход гарантирует обработку каждого депутата ровно один раз, что обеспечивает оптимальную асимптотику.
Временная сложность: O(n)
Память: O(n)
Мы используем два очереди для хранения индексов депутатов каждой фракции. Каждый раунд сравниваются первые элементы обеих очередей: депутат с меньшим индексом аннулирует голос противника и добавляется в очередь с новым индексом (исходный индекс + длина строки) для следующего раунда. Такой подход гарантирует обработку каждого депутата ровно один раз, что обеспечивает оптимальную асимптотику.
Временная сложность: O(n)
Память: O(n)
import collections
def predictPartyVictory(senate: str) -> str:
n = len(senate)
qR = collections.deque()
qD = collections.deque()
for i, s in enumerate(senate):
if s == 'R':
qR.append(i)
else:
qD.append(i)
while qR and qD:
iR = qR.popleft()
iD = qD.popleft()
if iR < iD:
qR.append(iR + n)
else:
qD.append(iD + n)
return 'R' if qR else 'D'
if __name__ == '__main__':
senate = 'RDD'
print(predictPartyVictory(senate))
Описание решения:
Мы используем алгоритм с двумя указателями: быстрым и медленным.
- Быстрый указатель перемещается на 2 узла за шаг, а медленный – на 1 узел.
- Когда быстрый указатель достигает конца списка, медленный указывает на средний узел.
- Для списка с чётным количеством узлов медленный указывает на правый из двух центральных, что соответствует условию задачи.
- Удаление осуществляется обновлением ссылки предыдущего узла на следующий после медленного.
Сложность решения:
- По времени: O(n)
- По памяти: O(1)
Код на Python:
Мы используем алгоритм с двумя указателями: быстрым и медленным.
- Быстрый указатель перемещается на 2 узла за шаг, а медленный – на 1 узел.
- Когда быстрый указатель достигает конца списка, медленный указывает на средний узел.
- Для списка с чётным количеством узлов медленный указывает на правый из двух центральных, что соответствует условию задачи.
- Удаление осуществляется обновлением ссылки предыдущего узла на следующий после медленного.
Сложность решения:
- По времени: O(n)
- По памяти: O(1)
Код на Python:
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def deleteMiddle(head: ListNode) -> ListNode:
if not head or not head.next:
return None
slow = head
fast = head
prev = None
while fast and fast.next:
fast = fast.next.next
prev = slow
slow = slow.next
if prev:
prev.next = slow.next
return head
# Пример использования:
def printList(head: ListNode):
while head:
print(head.val, end=" ")
head = head.next
print()
if __name__ == "__main__":
# Создаем список: 1->2->3->4->5
node5 = ListNode(5)
node4 = ListNode(4, node5)
node3 = ListNode(3, node4)
node2 = ListNode(2, node3)
head = ListNode(1, node2)
head = deleteMiddle(head)
printList(head)
Решение задачи
Подход:
- Используем два указателя:
- Сохраняем голову четного списка в переменную
- За один проход O(n) проходим по списку, переставляя узлы.
- Дополнительная память O(1), так как манипуляции проводятся in-place.
Код на Python:
Пояснение:
Данный алгоритм реализован таким образом, поскольку требует лишь одного прохода по списку (O(n) по времени) и использует постоянное количество дополнительной памяти (O(1) по памяти). Это оптимальное решение для перестановки узлов в односвязном списке с сохранением порядка.
Подход:
- Используем два указателя:
odd для списка узлов на нечетных позициях и even для четных. - Сохраняем голову четного списка в переменную
evenHead для последующего объединения. - За один проход O(n) проходим по списку, переставляя узлы.
- Дополнительная память O(1), так как манипуляции проводятся in-place.
Код на Python:
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def oddEvenList(head: ListNode) -> ListNode:
if not head or not head.next:
return head
odd = head
even = head.next
evenHead = even
while even and even.next:
odd.next = even.next
odd = odd.next
even.next = odd.next
even = even.next
odd.next = evenHead
return head
def print_list(head: ListNode):
while head:
print(head.val, end=" - ")
head = head.next
print()
# Пример использования:
nodes = [ListNode(i) for i in range(1, 8)]
for i in range(len(nodes)-1):
nodes[i].next = nodes[i+1]
head = nodes[0]
head = oddEvenList(head)
print_list(head)
Пояснение:
Данный алгоритм реализован таким образом, поскольку требует лишь одного прохода по списку (O(n) по времени) и использует постоянное количество дополнительной памяти (O(1) по памяти). Это оптимальное решение для перестановки узлов в односвязном списке с сохранением порядка.
Решение задачи "Reverse Linked List"
Описание:
- Мы используем итеративный подход для изменения ссылок в односвязном списке.
- С помощью трёх указателей (
- Такой алгоритм обладает оптимальной асимптотикой:
- По времени: O(n) – каждый узел обрабатывается один раз.
- По памяти: O(1) – используется фиксированное количество указателей.
Пояснение:
Реализация выбрана итеративная, так как она позволяет перевернуть список за один проход с использованием константного количества дополнительной памяти.
Сложность по времени: O(n), так как каждый узел обрабатывается один раз.
Сложность по памяти: O(1), т.к. используется фиксированное количество указателей вне списка.
Описание:
- Мы используем итеративный подход для изменения ссылок в односвязном списке.
- С помощью трёх указателей (
prev, current, nxt) мы последовательно переворачиваем ссылки, не теряя связи между узлами. - Такой алгоритм обладает оптимальной асимптотикой:
- По времени: O(n) – каждый узел обрабатывается один раз.
- По памяти: O(1) – используется фиксированное количество указателей.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def reverseList(head):
prev = None
current = head
while current:
nxt = current.next
current.next = prev
prev = current
current = nxt
return prev
if __name__ == "__main__":
# Создаем односвязный список: 1 -> 2 -> 3 -> 4 -> None
node4 = ListNode(4)
node3 = ListNode(3, node4)
node2 = ListNode(2, node3)
head = ListNode(1, node2)
# Разворачиваем список
new_head = reverseList(head)
# Выводим развернутый список
current = new_head
while current:
print(current.val, end=" ")
current = current.next
print()
Пояснение:
Реализация выбрана итеративная, так как она позволяет перевернуть список за один проход с использованием константного количества дополнительной памяти.
Сложность по времени: O(n), так как каждый узел обрабатывается один раз.
Сложность по памяти: O(1), т.к. используется фиксированное количество указателей вне списка.
Решение задачи
Ниже представлена реализация на языке python. Решение использует идею нахождения середины списка с помощью двух указателей, последующего разворота второй половины и одновременного прохождения обеих частей для вычисления суммы пар. Такой подход имеет временную сложность O(n) и использует дополнительные ресурсы O(1) по памяти.
Пояснение:
- Нахождение середины списка выполняется с помощью двух указателей (медленного и быстрого), что занимает O(n) времени.
- После нахождения середины происходит разворот второй половины списка in-place, что требует O(n) времени и O(1) дополнительной памяти.
- Финальный проход для вычисления суммы пар также имеет сложность O(n).
- Общая асимптотическая сложность алгоритма: O(n) по времени и O(1) по памяти.
Ниже представлена реализация на языке python. Решение использует идею нахождения середины списка с помощью двух указателей, последующего разворота второй половины и одновременного прохождения обеих частей для вычисления суммы пар. Такой подход имеет временную сложность O(n) и использует дополнительные ресурсы O(1) по памяти.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def pair_sum(head: ListNode) -> int:
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
prev = None
current = slow
while current:
nxt = current.next
current.next = prev
prev = current
current = nxt
max_sum = 0
first, second = head, prev
while second:
sum_pair = first.val + second.val
if sum_pair > max_sum:
max_sum = sum_pair
first = first.next
second = second.next
return max_sum
# Пример использования:
if __name__ == '__main__':
# Создаем связный список: 5 -> 4 -> 2 -> 1
n4 = ListNode(1)
n3 = ListNode(2, n4)
n2 = ListNode(4, n3)
n1 = ListNode(5, n2)
print(pair_sum(n1)) # Выведет 6, так как пары (5,1) и (4,2); максимум 5+1 = 6
Пояснение:
- Нахождение середины списка выполняется с помощью двух указателей (медленного и быстрого), что занимает O(n) времени.
- После нахождения середины происходит разворот второй половины списка in-place, что требует O(n) времени и O(1) дополнительной памяти.
- Финальный проход для вычисления суммы пар также имеет сложность O(n).
- Общая асимптотическая сложность алгоритма: O(n) по времени и O(1) по памяти.
Пояснение:
Решение задачи основано на рекурсивном обходе бинарного дерева. Для каждого узла вычисляется максимальная глубина его левого и правого поддеревьев, после чего берётся максимум из двух значений и прибавляется единица для текущего узла. Такой подход гарантирует обход каждого узла ровно один раз, поэтому асимптотическая сложность по времени равна O(n), а по памяти используется O(h) для стека вызовов, где h – высота дерева.
Код на Python:
Решение задачи основано на рекурсивном обходе бинарного дерева. Для каждого узла вычисляется максимальная глубина его левого и правого поддеревьев, после чего берётся максимум из двух значений и прибавляется единица для текущего узла. Такой подход гарантирует обход каждого узла ровно один раз, поэтому асимптотическая сложность по времени равна O(n), а по памяти используется O(h) для стека вызовов, где h – высота дерева.
Код на Python:
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def maxDepth(root):
if root is None:
return 0
return 1 + max(maxDepth(root.left), maxDepth(root.right))
# Пример использования:
if __name__ == '__main__':
# Создадим дерево:
# 1
# / \
# 2 3
# / \
# 4 5
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.right.left = TreeNode(4)
root.right.right = TreeNode(5)
print(maxDepth(root))
Решение задачи:
Мы используем обход в глубину (DFS) для сбора листовых узлов каждого дерева, затем сравниваем полученные последовательности. Данный подход проходит по каждому узлу ровно один раз, что обеспечивает асимптотическую сложность по времени O(n) и использует O(n) памяти для хранения листьев.
Код на Python:
Пояснение:
Выбран обход DFS для генерации последовательностей листьев, так как он прост в реализации и эффективен. Каждое дерево обходится за O(n) времени, где n – количество узлов, а память используется для хранения стека вызовов и списка листьев – в худшем случае O(n). Сравнение списков также выполняется за O(n). Таким образом, общее время – O(n), а по памяти – O(n).
Мы используем обход в глубину (DFS) для сбора листовых узлов каждого дерева, затем сравниваем полученные последовательности. Данный подход проходит по каждому узлу ровно один раз, что обеспечивает асимптотическую сложность по времени O(n) и использует O(n) памяти для хранения листьев.
Код на Python:
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def leaf_similar(root1, root2):
def dfs(root):
if root:
if not root.left and not root.right:
yield root.val
yield from dfs(root.left)
yield from dfs(root.right)
return list(dfs(root1)) == list(dfs(root2))
# 'Пример использования:'
if __name__ == "__main":
# 'Дерево 1:'
# 3
# / \
# 5 1
# / \ \
# 6 2 9
# / \
# 7 4
tree1 = TreeNode(3)
tree1.left = TreeNode(5, TreeNode(6), TreeNode(2, TreeNode(7), TreeNode(4)))
tree1.right = TreeNode(1, None, TreeNode(9))
# 'Дерево 2:'
# 3
# / \
# 5 1
# \ \
# 2 4
# / \
# 7 9
tree2 = TreeNode(3)
tree2.left = TreeNode(5, None, TreeNode(2, TreeNode(7), TreeNode(9)))
tree2.right = TreeNode(1, None, TreeNode(4))
# 'Проверка:'
print("Leaf-similar trees:", leaf_similar(tree1, tree2))
Пояснение:
Выбран обход DFS для генерации последовательностей листьев, так как он прост в реализации и эффективен. Каждое дерево обходится за O(n) времени, где n – количество узлов, а память используется для хранения стека вызовов и списка листьев – в худшем случае O(n). Сравнение списков также выполняется за O(n). Таким образом, общее время – O(n), а по памяти – O(n).
Код решения задачи:
Пояснение:
- Рекурсивный обход DFS используется для прохода по всем узлам, передавая текущий максимум на пути.
- Если значение узла не меньше максимума, узел считается «хорошим».
- Решение имеет асимптотическую сложность O(n) по времени (каждый узел посещается ровно один раз) и O(h) по памяти (глубина рекурсии, в худшем случае O(n) для несбалансированного дерева).
Почему так:
- Использование DFS позволяет обойти дерево за минимальное время без использования дополнительных структур данных.
- Решение оптимально как по времени, так и по памяти, с минимальной асимптотической сложностью.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def countGoodNodes(root):
def dfs(node, max_val):
if not node:
return 0
# Если значение узла не меньше максимального на пути, то узел «хороший»
count = 1 if node.val >= max_val else 0
max_val = max(max_val, node.val)
count += dfs(node.left, max_val)
count += dfs(node.right, max_val)
return count
return dfs(root, root.val) if root else 0
# Пример использования:
if __name__ == "__main__":
# Создание бинарного дерева:
# 3
# / \
# 1 4
# / / \
# 3 1 5
root = TreeNode(3)
root.left = TreeNode(1)
root.right = TreeNode(4)
root.left.left = TreeNode(3)
root.right.left = TreeNode(1)
root.right.right = TreeNode(5)
print(countGoodNodes(root))
Пояснение:
- Рекурсивный обход DFS используется для прохода по всем узлам, передавая текущий максимум на пути.
- Если значение узла не меньше максимума, узел считается «хорошим».
- Решение имеет асимптотическую сложность O(n) по времени (каждый узел посещается ровно один раз) и O(h) по памяти (глубина рекурсии, в худшем случае O(n) для несбалансированного дерева).
Почему так:
- Использование DFS позволяет обойти дерево за минимальное время без использования дополнительных структур данных.
- Решение оптимально как по времени, так и по памяти, с минимальной асимптотической сложностью.
Решение задачи
В данном решении используется обход дерева в глубину с подсчётом префиксных сумм. Мы сохраняем в словаре количество встреченных префиксных сумм, что позволяет за O(1) получать количество путей с нужной суммой. Это обеспечивает линейную асимптотику по времени – O(n) при обходе всех узлов, а по памяти – O(n) в худшем случае.
Пояснение:
- Использование префиксных сумм позволяет за константное время определять, существует ли ранее встреченный путь, дополняющий текущую сумму до targetSum.
- Обход выполняется один раз для каждого узла, что гарантирует асимптотику по времени O(n), где n – число узлов.
- В худшем случае словарь может хранить до O(n) элементов, что соответствует асимптотической сложности по памяти O(n).
В данном решении используется обход дерева в глубину с подсчётом префиксных сумм. Мы сохраняем в словаре количество встреченных префиксных сумм, что позволяет за O(1) получать количество путей с нужной суммой. Это обеспечивает линейную асимптотику по времени – O(n) при обходе всех узлов, а по памяти – O(n) в худшем случае.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def pathSum(root: TreeNode, targetSum: int) -> int:
prefix = {0: 1}
def dfs(node, curr_sum):
if not node:
return 0
curr_sum += node.val
count = prefix.get(curr_sum - targetSum, 0)
prefix[curr_sum] = prefix.get(curr_sum, 0) + 1
count += dfs(node.left, curr_sum)
count += dfs(node.right, curr_sum)
prefix[curr_sum] -= 1
return count
return dfs(root, 0)
Пояснение:
- Использование префиксных сумм позволяет за константное время определять, существует ли ранее встреченный путь, дополняющий текущую сумму до targetSum.
- Обход выполняется один раз для каждого узла, что гарантирует асимптотику по времени O(n), где n – число узлов.
- В худшем случае словарь может хранить до O(n) элементов, что соответствует асимптотической сложности по памяти O(n).
Решение задачи:
Мы используем рекурсивный обход дерева (DFS) с передачей текущего направления и длины зигзаг-пути. При переходе в обратном направлении увеличиваем длину, а при повторении направления – начинаем новый подсчёт. Таким образом обход каждое ребро производится один раз, что обеспечивает асимптотическую сложность по времени O(n) и по памяти O(n) в худшем случае (глубина рекурсии при несбалансированном дереве).
Код на Python:
Пояснение:
- Рекурсия проходит по каждому узлу, обновляя глобальный максимум.
- При переходе в противоположное направление увеличивается длина зигзага, иначе начинается новый путь с длины 1.
- Сложность по времени O(n), так как каждый узел посещается один раз, а по памяти — O(n) из-за стека вызовов.
Мы используем рекурсивный обход дерева (DFS) с передачей текущего направления и длины зигзаг-пути. При переходе в обратном направлении увеличиваем длину, а при повторении направления – начинаем новый подсчёт. Таким образом обход каждое ребро производится один раз, что обеспечивает асимптотическую сложность по времени O(n) и по памяти O(n) в худшем случае (глубина рекурсии при несбалансированном дереве).
Код на Python:
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def longestZigZag(self, root):
self.ans = 0
def dfs(node, is_left, length):
if node is None:
return
self.ans = max(self.ans, length)
if is_left:
dfs(node.left, False, length + 1)
dfs(node.right, True, 1)
else:
dfs(node.right, True, length + 1)
dfs(node.left, False, 1)
dfs(root, True, 0)
dfs(root, False, 0)
return self.ans
# Пример использования:
if __name__ == "__main__":
# Формирование дерева
n4 = TreeNode(4)
n5 = TreeNode(5)
n2 = TreeNode(2, n4, n5)
n3 = TreeNode(3)
root = TreeNode(1, n2, n3)
sol = Solution()
print(sol.longestZigZag(root))
Пояснение:
- Рекурсия проходит по каждому узлу, обновляя глобальный максимум.
- При переходе в противоположное направление увеличивается длина зигзага, иначе начинается новый путь с длины 1.
- Сложность по времени O(n), так как каждый узел посещается один раз, а по памяти — O(n) из-за стека вызовов.
Решение задачи
Пояснение к реализации
- Выбор подхода: Решение основано на рекурсивном обходе дерева. Если текущий узел равен одному из искомых, он возвращается как потенциальный предок. Рекурсия возвращает найденные предки из левого и правого поддерева. Если оба поддерева возвращают ненулевые значения, значит текущий узел является наименьшим общим предком.
- Сложность по времени: O(n) – каждый узел дерева посещается единожды.
- Сложность по памяти: O(h) – используется память для стека рекурсии, где h – высота дерева (в худшем случае O(n)).
class TreeNode:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
def lowestCommonAncestor(root, p, q):
if root is None:
return None
if root == p or root == q:
return root
left = lowestCommonAncestor(root.left, p, q)
right = lowestCommonAncestor(root.right, p, q)
if left is not None and right is not None:
return root
return left if left is not None else right
if __name__ == "__main__":
# Пример построения дерева
root = TreeNode(3)
root.left = TreeNode(5)
root.right = TreeNode(1)
root.left.left = TreeNode(6)
root.left.right = TreeNode(2)
root.right.left = TreeNode(0)
root.right.right = TreeNode(8)
lca = lowestCommonAncestor(root, root.left, root.right)
print(lca.val if lca else "None")
Пояснение к реализации
- Выбор подхода: Решение основано на рекурсивном обходе дерева. Если текущий узел равен одному из искомых, он возвращается как потенциальный предок. Рекурсия возвращает найденные предки из левого и правого поддерева. Если оба поддерева возвращают ненулевые значения, значит текущий узел является наименьшим общим предком.
- Сложность по времени: O(n) – каждый узел дерева посещается единожды.
- Сложность по памяти: O(h) – используется память для стека рекурсии, где h – высота дерева (в худшем случае O(n)).
Решение задачи "Binary Tree Right Side View"
Описание:
Мы решаем задачу обходом в ширину (BFS). На каждом уровне дерева выбирается последний по очереди узел, который и является видимым с правой стороны.
Алгоритм:
- Проверяем, что дерево не пустое.
- Используем очередь для обхода каждого уровня дерева.
- На каждом уровне сохраняем последнее значение узла.
Сложность:
- По времени: O(n) – каждый узел посещается один раз.
- По памяти: O(n) – в худшем случае очередь может содержать порядка n/2 узлов.
Пояснение:
Мы используем обход в ширину, потому что он позволяет разделить дерево на уровни и легко выбрать последний узел каждого уровня. Такой подход эффективен при обработке бинарных деревьев, обеспечивая посещение каждого узла ровно один раз и минимальную дополнительную память для хранения очереди.
Описание:
Мы решаем задачу обходом в ширину (BFS). На каждом уровне дерева выбирается последний по очереди узел, который и является видимым с правой стороны.
Алгоритм:
- Проверяем, что дерево не пустое.
- Используем очередь для обхода каждого уровня дерева.
- На каждом уровне сохраняем последнее значение узла.
Сложность:
- По времени: O(n) – каждый узел посещается один раз.
- По памяти: O(n) – в худшем случае очередь может содержать порядка n/2 узлов.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def rightSideView(root):
if not root:
return []
result = []
from collections import deque
queue = deque([root])
while queue:
level_length = len(queue)
for i in range(level_length):
node = queue.popleft()
if i == level_length - 1:
result.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return result
# Пример использования:
if __name__ == "__main__":
# Построим примерное бинарное дерево:
# 1
# / \
# 2 3
# \ \
# 5 4
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.right = TreeNode(5)
root.right.right = TreeNode(4)
print(rightSideView(root)) # Ожидаемый вывод: [1, 3, 4]
Пояснение:
Мы используем обход в ширину, потому что он позволяет разделить дерево на уровни и легко выбрать последний узел каждого уровня. Такой подход эффективен при обработке бинарных деревьев, обеспечивая посещение каждого узла ровно один раз и минимальную дополнительную память для хранения очереди.
Решение задачи:
Мы используем обход в ширину (BFS) для последовательного обхода уровней дерева, что позволяет за один проход вычислять сумму узлов на каждом уровне. При нахождении уровня с большей суммой обновляем результат. Выбранный подход имеет временную сложность O(n) и пространственную сложность O(n) в худшем случае.
Код на Python:
Пояснение:
- Обход дерева реализован через очередь – каждый узел посещается ровно один раз, что гарантирует временную сложность O(n).
- Дополнительная память используется для хранения очереди, в худшем случае O(n) (например, при полном бинарном дереве на последнем уровне).
- Решение реализовано с минимальными накладными расходами и максимально эффективным алгоритмом.
Мы используем обход в ширину (BFS) для последовательного обхода уровней дерева, что позволяет за один проход вычислять сумму узлов на каждом уровне. При нахождении уровня с большей суммой обновляем результат. Выбранный подход имеет временную сложность O(n) и пространственную сложность O(n) в худшем случае.
Код на Python:
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def maxLevelSum(root):
from collections import deque
if not root:
return 0
max_sum = -float('inf')
level = 1
queue = deque([root])
best_sum = -float('inf')
while queue:
cur_sum = 0
size = len(queue)
for _ in range(size):
node = queue.popleft()
cur_sum += node.val
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
if cur_sum > best_sum:
best_sum = cur_sum
level += 1
return best_sum
# Пример использования:
# Построение бинарного дерева:
# 1
# / \
# 7 0
# / \
# 7 -8
if __name__ == "__main__":
root = TreeNode(1)
root.left = TreeNode(7)
root.right = TreeNode(0)
root.left.left = TreeNode(7)
root.left.right = TreeNode(-8)
print(maxLevelSum(root)) # Вывод: 7Пояснение:
- Обход дерева реализован через очередь – каждый узел посещается ровно один раз, что гарантирует временную сложность O(n).
- Дополнительная память используется для хранения очереди, в худшем случае O(n) (например, при полном бинарном дереве на последнем уровне).
- Решение реализовано с минимальными накладными расходами и максимально эффективным алгоритмом.
Решение задачи
Алгоритм использует итеративный подход, что минимизирует использование памяти, поскольку не происходит рекурсивных вызовов.
Пояснение
- Итеративное решение позволяет проходить по дереву, сравнивая значение узла с искомым значением и переходя в левое или правое поддерево, что соответствует свойствам BST.
- Время работы: O(h), где h – высота дерева.
- Память: O(1), так как не используются дополнительные структуры данных или рекурсия.
Алгоритм использует итеративный подход, что минимизирует использование памяти, поскольку не происходит рекурсивных вызовов.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def searchBST(root: TreeNode, val: int) -> TreeNode:
while root:
if root.val == val:
return root
elif root.val < val:
root = root.right
else:
root = root.left
return None
# Пример использования
if __name__ == "__main__":
# Создание BST:
# 4
# / \
# 2 7
# / \
# 1 3
root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(7)
root.left.left = TreeNode(1)
root.left.right = TreeNode(3)
result = searchBST(root, 2)
if result:
print(result.val) # Вывод: 2
else:
print("Не найдено")
Пояснение
- Итеративное решение позволяет проходить по дереву, сравнивая значение узла с искомым значением и переходя в левое или правое поддерево, что соответствует свойствам BST.
- Время работы: O(h), где h – высота дерева.
- Память: O(1), так как не используются дополнительные структуры данных или рекурсия.
Решение задачи
Использован рекурсивный подход для удаления узла в BST.
Основные шаги алгоритма:
- Если дерево пустое, возвращается None.
- Ищем узел с ключом, двигаясь в левое или правое поддерево.
- При нахождении узла с нужным значением:
- Если отсутствует левый или правый ребёнок, возвращается непустой ребёнок (или None).
- Если оба ребёнка есть, находим минимальный узел в правом поддереве (inorder successor), копируем его значение в текущий узел и рекурсивно удаляем этот минимальный узел.
Пояснение выбора реализации:
Алгоритм реализован рекурсивно, что делает решение максимально лаконичным. При каждом вызове функция переходит в одно из поддеревьев, что обеспечивает асимптотику по времени O(h), где h – высота дерева (в худшем случае O(n)). Дополнительная память используется для стека рекурсии, также O(h). Это оптимальное решение с точки зрения временной и пространственной сложности для данной задачи.
Использован рекурсивный подход для удаления узла в BST.
Основные шаги алгоритма:
- Если дерево пустое, возвращается None.
- Ищем узел с ключом, двигаясь в левое или правое поддерево.
- При нахождении узла с нужным значением:
- Если отсутствует левый или правый ребёнок, возвращается непустой ребёнок (или None).
- Если оба ребёнка есть, находим минимальный узел в правом поддереве (inorder successor), копируем его значение в текущий узел и рекурсивно удаляем этот минимальный узел.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def deleteNode(root, key):
if not root:
return None
if key < root.val:
root.left = deleteNode(root.left, key)
elif key > root.val:
root.right = deleteNode(root.right, key)
else:
if not root.left:
return root.right
if not root.right:
return root.left
curr = root.right
while curr.left:
curr = curr.left
root.val = curr.val
root.right = deleteNode(root.right, curr.val)
return root
Пояснение выбора реализации:
Алгоритм реализован рекурсивно, что делает решение максимально лаконичным. При каждом вызове функция переходит в одно из поддеревьев, что обеспечивает асимптотику по времени O(h), где h – высота дерева (в худшем случае O(n)). Дополнительная память используется для стека рекурсии, также O(h). Это оптимальное решение с точки зрения временной и пространственной сложности для данной задачи.
Решение задачи
Мы рассматриваем комнаты как узлы графа, где каждый ключ – ребро, открывающее какую-либо комнату. Используя обход в глубину (DFS) с итеративным стеком, мы посещаем все доступные комнаты, начиная с комнаты 0, и затем проверяем, были ли посещены все комнаты.
Код на Python
Пояснение
- Почему так: Решение использует итеративный DFS, что позволяет избежать проблем с ограничением рекурсии и эффективно обходить граф.
- Временная сложность: O(n + m), где n – количество комнат, m – суммарное количество ключей.
- Памятная сложность: O(n) для хранения посещённых комнат и стека обхода.
Мы рассматриваем комнаты как узлы графа, где каждый ключ – ребро, открывающее какую-либо комнату. Используя обход в глубину (DFS) с итеративным стеком, мы посещаем все доступные комнаты, начиная с комнаты 0, и затем проверяем, были ли посещены все комнаты.
Код на Python
def canVisitAllRooms(rooms):
visited = set()
stack = [0]
while stack:
room = stack.pop()
if room not in visited:
visited.add(room)
for key in rooms[room]:
if key not in visited:
stack.append(key)
return len(visited) == len(rooms)
if __name__ == "__main__":
# Пример использования:
rooms = [[1], [2], [3], []]
print(canVisitAllRooms(rooms)) # Expected output: True
Пояснение
- Почему так: Решение использует итеративный DFS, что позволяет избежать проблем с ограничением рекурсии и эффективно обходить граф.
- Временная сложность: O(n + m), где n – количество комнат, m – суммарное количество ключей.
- Памятная сложность: O(n) для хранения посещённых комнат и стека обхода.
Решение задачи:
Для решения используется обход графа в глубину (DFS).
- Алгоритм: Обход каждой вершины, если она не посещена, запускается DFS, который посещает все связанные города (вершины), что формирует одну провинцию.
- Временная сложность: O(n^2) — проверяются все элементы матрицы смежности.
- Памятная сложность: O(n) — используется массив для отметки посещённых вершин.
Пояснение:
Реализация основана на DFS, которая обеспечивает минимальную асимптотическую сложность для данной задачи - O(n^2) по времени и O(n) по памяти.
Для решения используется обход графа в глубину (DFS).
- Алгоритм: Обход каждой вершины, если она не посещена, запускается DFS, который посещает все связанные города (вершины), что формирует одну провинцию.
- Временная сложность: O(n^2) — проверяются все элементы матрицы смежности.
- Памятная сложность: O(n) — используется массив для отметки посещённых вершин.
def findCircleNum(isConnected):
n = len(isConnected)
visited = [False] * n
def dfs(i):
for j in range(n):
if isConnected[i][j] == 1 and not visited[j]:
visited[j] = True
dfs(j)
count = 0
for i in range(n):
if not visited[i]:
visited[i] = True
dfs(i)
count += 1
return count
# Пример использования:
if __name__ == '__main__':
isConnected = [
[1, 1, 0],
[1, 1, 0],
[0, 0, 1]
]
print(findCircleNum(isConnected))
Пояснение:
Реализация основана на DFS, которая обеспечивает минимальную асимптотическую сложность для данной задачи - O(n^2) по времени и O(n) по памяти.
Код решения на Python:
Пояснение:
- Алгоритм строит неориентированный граф с аннотацией направления: если ребро идёт от текущего узла к соседу (метка 1), значит оно требует переориентации, чтобы путь шел к городу 0.
- Начинаем обход в ширину/глубину из города 0 (DFS). При переходе по ребру с меткой 1 увеличиваем счётчик, так как данное ребро необходимо переориентировать; ребра с меткой 0 переориентировать не нужно.
- Временная сложность: O(n), так как каждый узел и каждое ребро обрабатываются один раз.
- Памятная сложность: O(n) для хранения графа и множества посещённых вершин.
from collections import defaultdict
def minReorder(n, roads):
# Построение графа, где для каждого ребра (u, v) добавляем:
# - из u в v с меткой 1 (ребро направлено от u к v, требуется переориентация)
# - из v в u с меткой 0 (ребро уже идёт в сторону 0, если использовать эту связь)
graph = defaultdict(list)
for u, v in roads:
graph[u].append((v, 1))
graph[v].append((u, 0))
res = 0
visited = set()
def dfs(node):
nonlocal res
visited.add(node)
for neighbor, cost in graph[node]:
if neighbor not in visited:
res += cost
dfs(neighbor)
dfs(0)
return res
# Пример использования:
n = 6
roads = [[0, 1], [1, 3], [2, 3], [4, 0], [4, 5]]
print(minReorder(n, roads)) # Ожидаемый результат: 3
Пояснение:
- Алгоритм строит неориентированный граф с аннотацией направления: если ребро идёт от текущего узла к соседу (метка 1), значит оно требует переориентации, чтобы путь шел к городу 0.
- Начинаем обход в ширину/глубину из города 0 (DFS). При переходе по ребру с меткой 1 увеличиваем счётчик, так как данное ребро необходимо переориентировать; ребра с меткой 0 переориентировать не нужно.
- Временная сложность: O(n), так как каждый узел и каждое ребро обрабатываются один раз.
- Памятная сложность: O(n) для хранения графа и множества посещённых вершин.
Реализация задачи
Для решения используется графовый подход с поиском в глубину (DFS). Каждое уравнение представлено ребром с весом, а обратное ребро имеет обратное значение. В поиске в глубину перемножаются коэффициенты переходов.
Пояснение
- Реализация использует DFS, проходя по смежным вершинам графа, что позволяет найти по цепочке коэффициенты деления.
- Временная сложность: O(N) для каждого запроса, где N — количество переменных (вершин) в худшем случае.
- Память: O(N + E), где E — количество уравнений (ребер).
Замечание: Решение оптимально по времени для заданной задачи и использует только стандартные библиотеки Python.
Для решения используется графовый подход с поиском в глубину (DFS). Каждое уравнение представлено ребром с весом, а обратное ребро имеет обратное значение. В поиске в глубину перемножаются коэффициенты переходов.
def calcEquation(equations, values, queries):
graph = {}
for (a, b), k in zip(equations, values):
if a not in graph:
graph[a] = []
if b not in graph:
graph[b] = []
graph[a].append((b, k))
graph[b].append((a, 1/k))
def dfs(src, target, visited):
if src == target:
return 1.0
visited.add(src)
for neighbour, value in graph[src]:
if neighbour in visited:
continue
product = dfs(neighbour, target, visited)
if product != -1:
return value * product
return -1
res = []
for x, y in queries:
if x not in graph or y not in graph:
res.append(-1.0)
elif x == y:
res.append(1.0)
else:
res.append(dfs(x, y, set()))
return res
if __name__ == "'__main__'":
equations = [["a", "b"], ["b", "c"]]
values = [2.0, 3.0]
queries = [["a", "c"], ["b", "a"], ["a", "e"], ["a", "a"], ["x", "x"]]
print(calcEquation(equations, values, queries))
Пояснение
- Реализация использует DFS, проходя по смежным вершинам графа, что позволяет найти по цепочке коэффициенты деления.
- Временная сложность: O(N) для каждого запроса, где N — количество переменных (вершин) в худшем случае.
- Память: O(N + E), где E — количество уравнений (ребер).
Замечание: Решение оптимально по времени для заданной задачи и использует только стандартные библиотеки Python.
Решение задачи:
Для поиска кратчайшего пути используется алгоритм обхода в ширину (BFS), который гарантирует нахождение кратчайшего пути в невзвешенном графе.
Пояснение:
- Алгоритм использует очередь для обхода клеток лабиринта.
- Каждая клетка посещается максимум один раз, что обеспечивает асимптотическую сложность по времени O(N*M) и по памяти O(N*M), где N и M – размеры матрицы лабиринта.
Для поиска кратчайшего пути используется алгоритм обхода в ширину (BFS), который гарантирует нахождение кратчайшего пути в невзвешенном графе.
Пояснение:
- Алгоритм использует очередь для обхода клеток лабиринта.
- Каждая клетка посещается максимум один раз, что обеспечивает асимптотическую сложность по времени O(N*M) и по памяти O(N*M), где N и M – размеры матрицы лабиринта.
import collections
def nearest_exit(maze, entrance):
rows = len(maze)
cols = len(maze[0])
ex, ey = entrance
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
visited = {(ex, ey)}
queue = collections.deque([(ex, ey, 0)])
while queue:
x, y, steps = queue.popleft()
if (x, y) != (ex, ey) and (x == 0 or x == rows - 1 or y == 0 or y == cols - 1):
return steps
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < rows and 0 <= ny < cols and maze[nx][ny] == '.' and (nx, ny) not in visited:
visited.add((nx, ny))
queue.append((nx, ny, steps + 1))
return -1
# Пример использования:
maze = [
['+', '+', '.', '+'],
['.', '.', '.', '+'],
['+', '+', '+', '.']
]
entrance = (1, 0)
print(nearest_exit(maze, entrance))
Решение задачи
Данное решение использует алгоритм поиска в ширину (BFS) с несколькими источниками (гнилые апельсины). Это позволяет за один проход по матрице определить время, за которое все свежие апельсины окажутся заражёнными. Алгоритм проходит по каждому элементу матрицы один раз, поэтому асимптотическая сложность по времени составляет O(n·m), где n и m – размеры матрицы. Использование очереди и хранения матрицы приводит к пространственной сложности O(n·m).
Код на Python
Пояснение
- Использование BFS позволяет эффективно «распространить заражение» от всех гнилых апельсинов одновременно.
- Каждая клетка посещается единожды, что гарантирует оптимальную асимптотику по времени O(n·m) и аналогичную по памяти.
Данное решение использует алгоритм поиска в ширину (BFS) с несколькими источниками (гнилые апельсины). Это позволяет за один проход по матрице определить время, за которое все свежие апельсины окажутся заражёнными. Алгоритм проходит по каждому элементу матрицы один раз, поэтому асимптотическая сложность по времени составляет O(n·m), где n и m – размеры матрицы. Использование очереди и хранения матрицы приводит к пространственной сложности O(n·m).
Код на Python
def orangesRotting(grid):
from collections import deque
if not grid:
return -1
rows, cols = len(grid), len(grid[0])
queue = deque()
fresh = 0
for i in range(rows):
for j in range(cols):
if grid[i][j] == 2:
queue.append((i, j, 0)) # i, j, minute
elif grid[i][j] == 1:
fresh += 1
minutes = 0
directions = [(1,0),(-1,0),(0,1),(0,-1)]
while queue:
i, j, minutes = queue.popleft()
for di, dj in directions:
ni, nj = i + di, j + dj
if 0 <= ni < rows and 0 <= nj < cols and grid[ni][nj] == 1:
grid[ni][nj] = 2
fresh -= 1
queue.append((ni, nj, minutes + 1))
return minutes if fresh == 0 else -1
# Пример использования:
if __name__ == ""__main__"":
grid = [
[2,1,1],
[1,1,0],
[0,1,1]
]
result = orangesRotting(grid)
print(result)
Пояснение
- Использование BFS позволяет эффективно «распространить заражение» от всех гнилых апельсинов одновременно.
- Каждая клетка посещается единожды, что гарантирует оптимальную асимптотику по времени O(n·m) и аналогичную по памяти.
Решение задачи:
Использована реализация алгоритма Quickselect, который в среднем имеет линейную сложность по времени O(n) и требует O(1) дополнительной памяти (вызовы рекурсии – O(n) в худшем случае, но в среднем глубина рекурсии мала).
Ключевые моменты решения:
- Преобразование задачи: k-е по величине число в массиве соответствует числу с индексом len(nums) - k при сортировке по возрастанию.
- Алгоритм Quickselect: выбирается случайный опорный элемент, происходит его позиционирование – все элементы меньше опорного сдвигаются влево, затем решение сводится к одной из частей массива.
Сложность:
- В среднем: O(n) по времени, O(1) по дополнительной памяти,
- В худшем случае: O(n²) по времени, однако такой сценарий встречается крайне редко благодаря случайному выбору опорного элемента.
Код на Python:
Использована реализация алгоритма Quickselect, который в среднем имеет линейную сложность по времени O(n) и требует O(1) дополнительной памяти (вызовы рекурсии – O(n) в худшем случае, но в среднем глубина рекурсии мала).
Ключевые моменты решения:
- Преобразование задачи: k-е по величине число в массиве соответствует числу с индексом len(nums) - k при сортировке по возрастанию.
- Алгоритм Quickselect: выбирается случайный опорный элемент, происходит его позиционирование – все элементы меньше опорного сдвигаются влево, затем решение сводится к одной из частей массива.
Сложность:
- В среднем: O(n) по времени, O(1) по дополнительной памяти,
- В худшем случае: O(n²) по времени, однако такой сценарий встречается крайне редко благодаря случайному выбору опорного элемента.
Код на Python:
import random
def kth_largest(nums, k):
# k-е по величине число соответствует элементу с индексом len(nums) - k
return quickselect(nums, 0, len(nums) - 1, len(nums) - k)
def quickselect(nums, left, right, index):
if left == right:
return nums[left]
pivot_index = random.randint(left, right)
pivot_index = partition(nums, left, right, pivot_index)
if index == pivot_index:
return nums[pivot_index]
elif index < pivot_index:
return quickselect(nums, left, pivot_index - 1, index)
else:
return quickselect(nums, pivot_index + 1, right, index)
def partition(nums, left, right, pivot_index):
pivot_value = nums[pivot_index]
nums[pivot_index], nums[right] = nums[right], nums[pivot_index]
store_index = left
for i in range(left, right):
if nums[i] < pivot_value:
nums[store_index], nums[i] = nums[i], nums[store_index]
store_index += 1
nums[right], nums[store_index] = nums[store_index], nums[right]
return store_index
if __name__ == '__main__':
arr = [3, 2, 1, 5, 6, 4]
k = 2
print(kth_largest(arr, k)) # Expected output: 5
Решение задачи
Класс SmallestInfiniteSet использует два основных компонента:
- Счётчик (
- Мини-кучу (
При вызове
Метод
Сложность:
- Временная –
- По памяти –
Класс SmallestInfiniteSet использует два основных компонента:
- Счётчик (
curr) для отслеживания следующего нового числа. - Мини-кучу (
min_heap) для хранения возвращённых чисел методом addBack. При вызове
popSmallest() минимальное доступное число возвращается: если в куче есть добавленные числа, берётся минимум из них, иначе используется curr, который инкрементируется. Метод
addBack(num) добавляет число в кучу, если оно меньше текущего числа и ещё не было добавлено. Сложность:
- Временная –
O(log n) для операций с кучей, где n – количество элементов в куче. - По памяти –
O(n) для хранения элементов в куче и множестве.
import heapq
class SmallestInfiniteSet:
def __init__(self):
self.min_heap = []
self.added = set()
self.curr = 1
def popSmallest(self):
if self.min_heap:
smallest = heapq.heappop(self.min_heap)
self.added.remove(smallest)
return smallest
else:
smallest = self.curr
self.curr += 1
return smallest
def addBack(self, num):
if num < self.curr and num not in self.added:
heapq.heappush(self.min_heap, num)
self.added.add(num)
# Пример использования:
if __name__ == '__main__':
s = SmallestInfiniteSet()
print(s.popSmallest()) # вывод: 1
print(s.popSmallest()) # вывод: 2
s.addBack(1)
print(s.popSmallest()) # вывод: 1
print(s.popSmallest()) # вывод: 3
Код решения:
Пояснение:
Для решения задачи используется жадный алгоритм:
- Сортировка пар по значениям второго массива по убыванию гарантирует, что рассматриваемое значение b является минимальным для выбранной подпоследовательности.
- С использованием кучи (min-heap) выбираются k наибольших элементов из первого массива для максимизации суммы.
- При итерации вычисляется оценка как произведение текущей суммы и текущего b.
Сложность: По времени O(n log k), по памяти O(k). Эта реализация оптимальна для заданной задачи с использованием стандартных библиотек python.
import heapq
def max_subsequence_score(nums1, nums2, k):
# Создаем список пар и сортируем по значениям второго массива по убыванию
pairs = list(zip(nums1, nums2))
pairs.sort(key=lambda pair: pair[1], reverse=True)
current_sum = 0
min_heap = []
best_score = 0
# Проходим по парам, используя значение из второго массива как потенциальный минимум
for a, b in pairs:
heapq.heappush(min_heap, a)
current_sum += a
# Если размер кучи превышает k, удаляем наименьшее значение, чтобы сохранить сумму максимальной
if len(min_heap) > k:
current_sum -= heapq.heappop(min_heap)
# Когда размер кучи равен k, обновляем лучший результат
if len(min_heap) == k:
best_score = max(best_score, current_sum * b)
return best_score
# Пример использования:
if __name__ == "__main__":
nums1 = [1, 3, 3, 2]
nums2 = [2, 1, 3, 4]
k = 3
print(max_subsequence_score(nums1, nums2, k))
Пояснение:
Для решения задачи используется жадный алгоритм:
- Сортировка пар по значениям второго массива по убыванию гарантирует, что рассматриваемое значение b является минимальным для выбранной подпоследовательности.
- С использованием кучи (min-heap) выбираются k наибольших элементов из первого массива для максимизации суммы.
- При итерации вычисляется оценка как произведение текущей суммы и текущего b.
Сложность: По времени O(n log k), по памяти O(k). Эта реализация оптимальна для заданной задачи с использованием стандартных библиотек python.
Пояснение:
Задача решается перебором всех вариантов:
- выбираем i работников с начала очереди
- и оставшиеся k-i с конца очереди
Общий выбранный набор определяется суммой первых i элементов и последних k-i элементов массива затрат.
При этом гарантируется, что порядок выбора допускает только два непрерывных отрезка – слева и справа – что позволяет свести задачу к поиску минимума суммы для всех допустимых i от 0 до k.
Сложность решения:
- Время: O(k), так как перебор возможных вариантов занимает O(k) шагов.
- Память: O(k) для хранения префиксных и суффиксных сумм.
Реализация на Python:
Задача решается перебором всех вариантов:
- выбираем i работников с начала очереди
- и оставшиеся k-i с конца очереди
Общий выбранный набор определяется суммой первых i элементов и последних k-i элементов массива затрат.
При этом гарантируется, что порядок выбора допускает только два непрерывных отрезка – слева и справа – что позволяет свести задачу к поиску минимума суммы для всех допустимых i от 0 до k.
Сложность решения:
- Время: O(k), так как перебор возможных вариантов занимает O(k) шагов.
- Память: O(k) для хранения префиксных и суффиксных сумм.
Реализация на Python:
def total_cost(costs, k):
n = len(costs)
# Если k больше количества кандидатов, используем минимальное значение
k = min(k, n)
# Вычисление префиксных сумм для первых k элементов
prefix = [0] * (k + 1)
for i in range(1, k + 1):
prefix[i] = prefix[i - 1] + costs[i - 1]
# Вычисление суффиксных сумм для последних k элементов
suffix = [0] * (k + 1)
for j in range(1, k + 1):
suffix[j] = suffix[j - 1] + costs[n - j]
res = float("inf")
# Перебор вариантов: i работников слева и k-i с конца
for i in range(0, k + 1):
cost_sum = prefix[i] + suffix[k - i]
if cost_sum < res:
res = cost_sum
return res
if __name__ == "__main__":
# Пример использования:
costs = [10, 3, 3, 6, 2]
k = 3
print(total_cost(costs, k))
Описание решения
Данное решение использует бинарный поиск для минимизации числа вызовов функции guess. Алгоритм сравнивает середину текущего диапазона с загаданным числом и корректирует границы, в зависимости от результата вызова функции. Это обеспечивает асимптотическую сложность по времени O(log n) и по памяти O(1).
Реализация на Python
Пояснение
- Использование бинарного поиска гарантирует минимальное количество вызовов функции guess.
- Сложность по времени составляет O(log n), так как каждый шаг делит диапазон пополам.
- Сложность по памяти равна O(1), так как используются только несколько переменных.
Данное решение использует бинарный поиск для минимизации числа вызовов функции guess. Алгоритм сравнивает середину текущего диапазона с загаданным числом и корректирует границы, в зависимости от результата вызова функции. Это обеспечивает асимптотическую сложность по времени O(log n) и по памяти O(1).
Реализация на Python
def guess(num):
# Placeholder implementation; normally provided by the system.
# For example, suppose the target number is 42.
target = 42
if num == target:
return 0
elif num > target:
return -1
else:
return 1
def guessNumber(n):
left = 1
right = n
while left <= right:
mid = (left + right) // 2
res = guess(mid)
if res == 0:
return mid
elif res < 0:
right = mid - 1
else:
left = mid + 1
return -1 # Number not found
if __name__ == "__main__":
n = 100
number = guessNumber(n)
print("Guessed number is:", number)
Пояснение
- Использование бинарного поиска гарантирует минимальное количество вызовов функции guess.
- Сложность по времени составляет O(log n), так как каждый шаг делит диапазон пополам.
- Сложность по памяти равна O(1), так как используются только несколько переменных.
Решение:
Подход использует сортировку массива potions и бинарный поиск для каждого элемента массива spells.
- Для каждого заклинания вычисляем минимальное значение зелия: target = (success + spell - 1) // spell
- Выполняем бинарный поиск в отсортированном potions, чтобы найти первый элемент, не меньше target
- Количество подходящих зелий равно разности общего числа зелий и найденного индекса
Сложность:
- Время: O(n log m), где n – размер spells, m – размер potions (сортировка O(m log m) + для каждого элемента бинарный поиск O(log m))
- Память: O(n + m)
Подход использует сортировку массива potions и бинарный поиск для каждого элемента массива spells.
- Для каждого заклинания вычисляем минимальное значение зелия: target = (success + spell - 1) // spell
- Выполняем бинарный поиск в отсортированном potions, чтобы найти первый элемент, не меньше target
- Количество подходящих зелий равно разности общего числа зелий и найденного индекса
Сложность:
- Время: O(n log m), где n – размер spells, m – размер potions (сортировка O(m log m) + для каждого элемента бинарный поиск O(log m))
- Память: O(n + m)
def successfulPairs(spells, potions, success):
potions.sort()
n = len(potions)
res = []
import bisect
for spell in spells:
target = (success + spell - 1) // spell
index = bisect.bisect_left(potions, target)
res.append(n - index)
return res
if __name__ == "__main__":
spells = [5, 1, 3]
potions = [1, 2, 3, 4, 5]
success = 7
print(successfulPairs(spells, potions, success))
Решение задачи "Find Peak Element"
Описание: Дана последовательность чисел, необходимо найти индекс пикового элемента, удовлетворяющего условию: элемент больше своих соседей. Гарантируется, что такой элемент существует.
Подход: Используется бинарный поиск для нахождения пика, что обеспечивает асимптотику по времени O(log n) и использование O(1) дополнительной памяти. Алгоритм сравнивает элемент с его правым соседом и выбирает ту половину, где потенциально находится пик.
Код на Python:
Пояснение:
- Почему так: Бинарный поиск позволяет эффективно сузить диапазон поиска пика за счет сравнения соседних элементов.
- Сложность по времени: O(log n) — каждая итерация делит диапазон пополам.
- Сложность по памяти: O(1) — используются только несколько переменных для индексов.
Это решение оптимально для данной задачи.
Описание: Дана последовательность чисел, необходимо найти индекс пикового элемента, удовлетворяющего условию: элемент больше своих соседей. Гарантируется, что такой элемент существует.
Подход: Используется бинарный поиск для нахождения пика, что обеспечивает асимптотику по времени O(log n) и использование O(1) дополнительной памяти. Алгоритм сравнивает элемент с его правым соседом и выбирает ту половину, где потенциально находится пик.
Код на Python:
def findPeakElement(nums):
left = 0
right = len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] > nums[mid + 1]:
right = mid
else:
left = mid + 1
return left
if __name__ == "__main__":
nums = [1, 2, 3, 1]
print(findPeakElement(nums)) # Вывод: 2
Пояснение:
- Почему так: Бинарный поиск позволяет эффективно сузить диапазон поиска пика за счет сравнения соседних элементов.
- Сложность по времени: O(log n) — каждая итерация делит диапазон пополам.
- Сложность по памяти: O(1) — используются только несколько переменных для индексов.
Это решение оптимально для данной задачи.
Решение задачи
Для нахождения минимальной скорости съедания бананов используется бинарный поиск. Идея состоит в том, чтобы перебрать возможные скорости от 1 до максимального числа бананов в кучке. Для каждой кандидатной скорости вычисляется общее количество часов, за которое Коко сможет съесть все кучки, при этом для каждой кучки вычисляется количество часов округлением вверх, что реализуется формулой '(pile + speed - 1) // speed'. Если общее количество часов не превышает заданное значение h, то скорость может быть снижена, иначе – увеличивается.
Сложность:
- Время: O(n % log(max(piles))), где n = количество кучек.
- Память: O(1) дополнительной памяти.
Для нахождения минимальной скорости съедания бананов используется бинарный поиск. Идея состоит в том, чтобы перебрать возможные скорости от 1 до максимального числа бананов в кучке. Для каждой кандидатной скорости вычисляется общее количество часов, за которое Коко сможет съесть все кучки, при этом для каждой кучки вычисляется количество часов округлением вверх, что реализуется формулой '(pile + speed - 1) // speed'. Если общее количество часов не превышает заданное значение h, то скорость может быть снижена, иначе – увеличивается.
Сложность:
- Время: O(n % log(max(piles))), где n = количество кучек.
- Память: O(1) дополнительной памяти.
def minEatingSpeed(piles, h):
left, right = 1, max(piles)
while left < right:
mid = (left + right) // 2
hours = sum((pile + mid - 1) // mid for pile in piles)
if hours <= h:
right = mid
else:
left = mid + 1
return left
if __name__ == '__main__':
# Пример использования
piles = [30, 11, 23, 4, 20]
h = 6
print(minEatingSpeed(piles, h))
Решение задачи с использованием Python:
Пояснение реализации:
- Использование рекурсии и backtracking. Каждый вызов функции
- Минимальная асимптотическая сложность. По времени решение имеет сложность O(4^n), где n – количество цифр (максимум 4 буквы на цифру).
- Потребление памяти. O(n) для стека рекурсии, n – длина строки.
Вывод: Решение корректно и эффективно генерирует все комбинации букв для входной строки цифр согласно стандартной раскладке телефона.
def letter_combinations(digits):
if not digits:
return []
mapping = {
'2': 'abc',
'3': 'def',
'4': 'ghi',
'5': 'jkl',
'6': 'mno',
'7': 'pqrs',
'8': 'tuv',
'9': 'wxyz'
}
result = []
def backtrack(index, path):
if index == len(digits):
result.append(path)
return
for letter in mapping[digits[index]]:
backtrack(index + 1, path + letter)
backtrack(0, '')
return result
# Пример использования:
if __name__ == '__main__':
digits = '23'
combinations = letter_combinations(digits)
print(combinations)
Пояснение реализации:
- Использование рекурсии и backtracking. Каждый вызов функции
backtrack добавляет одну букву для соответствующей цифры.- Минимальная асимптотическая сложность. По времени решение имеет сложность O(4^n), где n – количество цифр (максимум 4 буквы на цифру).
- Потребление памяти. O(n) для стека рекурсии, n – длина строки.
Вывод: Решение корректно и эффективно генерирует все комбинации букв для входной строки цифр согласно стандартной раскладке телефона.
Решение задачи с использованием метода перебора (backtracking):
Мы рекурсивно строим комбинации чисел от 1 до 9, учитывая, что число можно использовать не более одного раза. Если длина комбинации равна k и сумма равна n, сохраняем комбинацию в результат. Отсечение по сумме и длине позволяет минимизировать количество рекурсивных вызовов.
Сложность по времени: O(2⁹) в худшем случае (фиксированный диапазон от 1 до 9, что эквивалентно O(1)),
Сложность по памяти: O(k) для рекурсивного стека.
Код на python:
Пояснение реализации:
Рекурсивная функция backtrack строит все возможные комбинации, начиная с текущего числа (start). Если комбинация достигает длины k и сумма равна n, она добавляется в результирующий список. Ограничения по длине и сумме позволяют эффективно отсеивать не подходящие варианты, что минимизирует асимптотическую сложность.
Мы рекурсивно строим комбинации чисел от 1 до 9, учитывая, что число можно использовать не более одного раза. Если длина комбинации равна k и сумма равна n, сохраняем комбинацию в результат. Отсечение по сумме и длине позволяет минимизировать количество рекурсивных вызовов.
Сложность по времени: O(2⁹) в худшем случае (фиксированный диапазон от 1 до 9, что эквивалентно O(1)),
Сложность по памяти: O(k) для рекурсивного стека.
Код на python:
def combinationSum3(k, n):
result = []
def backtrack(start, path, total):
if len(path) == k and total == n:
result.append(path[:])
return
if len(path) > k or total > n:
return
for i in range(start, 10):
path.append(i)
backtrack(i + 1, path, total + i)
path.pop()
backtrack(1, [], 0)
return result
if __name__ == "__main__":
k = 3
n = 7
print(combinationSum3(k, n))
Пояснение реализации:
Рекурсивная функция backtrack строит все возможные комбинации, начиная с текущего числа (start). Если комбинация достигает длины k и сумма равна n, она добавляется в результирующий список. Ограничения по длине и сумме позволяют эффективно отсеивать не подходящие варианты, что минимизирует асимптотическую сложность.
Решение задачи
Ниже представлен современный код на Python, который вычисляет N-е число Трибоначчи с использованием итеративного подхода, что обеспечивает асимптотическую сложность по времени O(n) и по памяти O(1).
Пояснение
- Итеративный алгоритм позволяет вычислять число Трибоначчи за один проход, используя три переменные, что оптимально по памяти.
- Итерация проходит с 3 до n, поэтому асимптотическая сложность по времени равна O(n).
- Использование трёх переменных обеспечивает константную сложность по памяти O(1).
Ниже представлен современный код на Python, который вычисляет N-е число Трибоначчи с использованием итеративного подхода, что обеспечивает асимптотическую сложность по времени O(n) и по памяти O(1).
def tribonacci(n):
if n == 0:
return 0
if n <= 2:
return 1
a, b, c = 0, 1, 1
for _ in range(3, n+1):
a, b, c = b, c, a + b + c
return c
if __name__ == "'__main__":
n = int(input().strip())
print(tribonacci(n))
Пояснение
- Итеративный алгоритм позволяет вычислять число Трибоначчи за один проход, используя три переменные, что оптимально по памяти.
- Итерация проходит с 3 до n, поэтому асимптотическая сложность по времени равна O(n).
- Использование трёх переменных обеспечивает константную сложность по памяти O(1).
Решение задачи
Используем оптимизированный динамический подход с O(n) по времени и O(1) по памяти.
Ключевая идея:
- На каждом шаге вычисляем минимальную стоимость подъёма до текущей позиции, используя две переменные для хранения предыдущих результатов.
- Итоговое решение — минимум из двух последних накопленных сумм.
Пояснение:
- Временная сложность: O(n), так как мы проходим один раз по списку cost.
- Памятная сложность: O(1), поскольку используем фиксированное число переменных, независимо от размера входа.
- Решение реализовано таким образом для обеспечения минимальной асимптотической сложности и эффективного использования ресурсов.
Используем оптимизированный динамический подход с O(n) по времени и O(1) по памяти.
Ключевая идея:
- На каждом шаге вычисляем минимальную стоимость подъёма до текущей позиции, используя две переменные для хранения предыдущих результатов.
- Итоговое решение — минимум из двух последних накопленных сумм.
def minCostClimbingStairs(cost):
a, b = 0, 0
for x in cost:
a, b = b, x + min(a, b)
return min(a, b)
if __name__ == "'__main__'":
cost = [10, 15, 20]   # Пример ввода
print(minCostClimbingStairs(cost))
Пояснение:
- Временная сложность: O(n), так как мы проходим один раз по списку cost.
- Памятная сложность: O(1), поскольку используем фиксированное число переменных, независимо от размера входа.
- Решение реализовано таким образом для обеспечения минимальной асимптотической сложности и эффективного использования ресурсов.
Решение задачи House Robber
Описание:
- Дано целочисленный массив nums, где каждый элемент представляет сумму денег в доме.
- Ограничение: нельзя грабить два соседних дома.
- Цель: найти максимальную сумму денег, которую можно собрать.
Реализация:
Решение использует динамическое программирование с оптимизацией по памяти. Вместо массива для хранения промежуточных значений, используются две переменные: prev и curr.
На каждом шаге выбирается максимум между неограбленным текущим домом (curr) и грабежом текущего дома с добавлением суммы prev.
Сложность:
- Время: O(n)
- Память: O(1)
Код на Python:
Пояснение:
- Решение реализовано итеративно, что позволяет проходить по массиву за один проход (линейное время).
- Использование двух переменных сокращает затраты памяти до константного уровня.
- Выбор в пользу динамического программирования гарантирует оптимальное решение задачи с точки зрения асимптотической сложности.
Описание:
- Дано целочисленный массив nums, где каждый элемент представляет сумму денег в доме.
- Ограничение: нельзя грабить два соседних дома.
- Цель: найти максимальную сумму денег, которую можно собрать.
Реализация:
Решение использует динамическое программирование с оптимизацией по памяти. Вместо массива для хранения промежуточных значений, используются две переменные: prev и curr.
На каждом шаге выбирается максимум между неограбленным текущим домом (curr) и грабежом текущего дома с добавлением суммы prev.
Сложность:
- Время: O(n)
- Память: O(1)
Код на Python:
def rob(nums):
prev, curr = 0, 0
for num in nums:
prev, curr = curr, max(curr, prev + num)
return curr
if __name__ == "__main__":
test_nums = [1, 2, 3, 1]
print(rob(test_nums))
Пояснение:
- Решение реализовано итеративно, что позволяет проходить по массиву за один проход (линейное время).
- Использование двух переменных сокращает затраты памяти до константного уровня.
- Выбор в пользу динамического программирования гарантирует оптимальное решение задачи с точки зрения асимптотической сложности.
Код на Python
Пояснение:
- Реализация решает задачу с помощью динамического программирования, используя рекуррентную зависимость
- Базовые случаи выбраны как
- Итеративное решение использует O(1) дополнительной памяти (три переменные) и имеет временную сложность O(n).
def domino_tromino_tiling(n: int) -> int:
if n == 0:
return 1
if n == 1:
return 1
if n == 2:
return 2
# Используем оптимизированную версию: dp[n] = 2*dp[n-1] + dp[n-3]
dp0, dp1, dp2 = 1, 1, 2
for i in range(3, n + 1):
dp = 2 * dp2 + dp0
dp0, dp1, dp2 = dp1, dp2, dp
return dp2
if __name__ == "'__main__'":
n = 10
print(domino_tromino_tiling(n))
Пояснение:
- Реализация решает задачу с помощью динамического программирования, используя рекуррентную зависимость
dp[n] = 2 * dp[n-1] + dp[n-3].- Базовые случаи выбраны как
dp[0]=1, dp[1]=1 и dp[2]=2, что соответствует минимальным размерам доски.- Итеративное решение использует O(1) дополнительной памяти (три переменные) и имеет временную сложность O(n).
Решение задачи Unique Paths
Реализация основана на комбинаторном подходе: количество уникальных путей равно числу сочетаний из (m+n-2) по (m-1). Использование функции
Временная сложность: O(min(m, n))
Памятная сложность: O(1)
Реализация основана на комбинаторном подходе: количество уникальных путей равно числу сочетаний из (m+n-2) по (m-1). Использование функции
math.comb позволяет вычислить это значение напрямую с оптимальной асимптотической сложностью.Временная сложность: O(min(m, n))
Памятная сложность: O(1)
import math
def unique_paths(m, n):
return math.comb(m+n-2, m-1)
if __name__ == "__main__":
m = 3
n = 7
print(unique_paths(m, n))
Описание решения задачи
Для нахождения длины наибольшей общей подпоследовательности (LCS) используются методы динамического программирования.
Основная идея:
- Используется двумерный массив
- Если текущие символы строк совпадают,
Сложность:
- Время: O(m*n)
- Память: O(m*n)
Реализация на Python
Пояснение
Реализация реализована таким образом, поскольку динамическое программирование позволяет эффективно решать задачу и гарантировать минимальную асимптотическую сложность по времени (O(m*n)) и памяти (O(m*n)).
Для нахождения длины наибольшей общей подпоследовательности (LCS) используются методы динамического программирования.
Основная идея:
- Используется двумерный массив
dp размером (m+1) x (n+1), где m и n – длины строк text1 и text2 соответственно. - Если текущие символы строк совпадают,
dp[i][j] = dp[i-1][j-1] + 1. В противном случае – выбирается максимум между dp[i-1][j] и dp[i][j-1]. Сложность:
- Время: O(m*n)
- Память: O(m*n)
Реализация на Python
def longest_common_subsequence(text1: str, text2: str) -> int:
m, n = len(text1), len(text2)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
# Пример использования:
if __name__ == "__main__":
text1 = "abcde"
text2 = "ace"
print(longest_common_subsequence(text1, text2)) # Выведет 3
Пояснение
Реализация реализована таким образом, поскольку динамическое программирование позволяет эффективно решать задачу и гарантировать минимальную асимптотическую сложность по времени (O(m*n)) и памяти (O(m*n)).
Решение задачи
Для решения задачи используется динамическое программирование с поддержанием двух состояний: cash — максимальная прибыль на данный момент без владения акциями, и hold — максимальная прибыль, если мы держим акцию (при покупке цена вычитается). При каждой итерации обновляются оба состояния с учётом комиссии fee, которая вычитается при продаже. Такой подход позволяет пройтись по массиву один раз, что обеспечивает оптимальную асимптотику по времени и постоянное использование памяти.
Сложность решения:
- По времени: O(n) (один проход по массиву)
- По памяти: O(1) (используются лишь несколько переменных)
Для решения задачи используется динамическое программирование с поддержанием двух состояний: cash — максимальная прибыль на данный момент без владения акциями, и hold — максимальная прибыль, если мы держим акцию (при покупке цена вычитается). При каждой итерации обновляются оба состояния с учётом комиссии fee, которая вычитается при продаже. Такой подход позволяет пройтись по массиву один раз, что обеспечивает оптимальную асимптотику по времени и постоянное использование памяти.
Сложность решения:
- По времени: O(n) (один проход по массиву)
- По памяти: O(1) (используются лишь несколько переменных)
def max_profit(prices: list<int>, fee: int) -> int:
cash = 0
hold = -prices[0]
for price in prices[1:]:
cash = max(cash, hold + price - fee)
hold = max(hold, cash - price)
return cash
# 'Пример использования'
if __name__ == '__main__':
prices = [1, 3, 2, 8, 4, 9]
fee = 2
print(max_profit(prices, fee)) # Вывод: 8
Описание решения
Для задачи «Edit Distance» используется динамическое программирование, позволяющее эффективно находить минимальное количество операций преобразования одной строки в другую.
Пояснение реализации:
- Для решения используется таблица
-
- Заполнение таблицы происходит в два этапа: инициализация краевых значений и основное заполнение на основе предшествующих значений.
- Выбор минимальной операции (вставка, удаление, замена) осуществляется на каждом шаге.
Сложность:
- Время: O(m×n)
- Память: O(m×n)
Блок кода на современном Python
Заключение
Данное решение является оптимальным с точки зрения асимптотической сложности. Оно прекрасно подходит для задач, где размеры строк не превышают разумные пределы.
Для задачи «Edit Distance» используется динамическое программирование, позволяющее эффективно находить минимальное количество операций преобразования одной строки в другую.
Пояснение реализации:
- Для решения используется таблица
dp размера (m+1)×(n+1), где m и n — длины строк word1 и word2 соответственно. -
dp[i][j] хранит минимальное количество операций, необходимых для преобразования первых i символов из word1 в первые j символов из word2. - Заполнение таблицы происходит в два этапа: инициализация краевых значений и основное заполнение на основе предшествующих значений.
- Выбор минимальной операции (вставка, удаление, замена) осуществляется на каждом шаге.
Сложность:
- Время: O(m×n)
- Память: O(m×n)
Блок кода на современном Python
def minDistance(word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
return dp[m][n]
if __name__ == "__main__":
word1 = input().strip()
word2 = input().strip()
print(minDistance(word1, word2))
Заключение
Данное решение является оптимальным с точки зрения асимптотической сложности. Оно прекрасно подходит для задач, где размеры строк не превышают разумные пределы.
Описание решения:
Используется динамическое программирование, где для каждого числа i количество единиц в его двоичном представлении вычисляется по формуле:
Это позволяет получать результат за один проход по числам от 0 до n.
Сложность:
- Время: O(n) – каждый элемент массива вычисляется за O(1).
- Память: O(n) – требуется массив размера n+1.
Реализация на Python:
Используется динамическое программирование, где для каждого числа i количество единиц в его двоичном представлении вычисляется по формуле:
dp[i] = dp[i >> 1] + (i & 1) Это позволяет получать результат за один проход по числам от 0 до n.
Сложность:
- Время: O(n) – каждый элемент массива вычисляется за O(1).
- Память: O(n) – требуется массив размера n+1.
Реализация на Python:
def count_bits(n):
dp = [0] * (n + 1)
for i in range(1, n + 1):
dp[i] = dp[i >> 1] + (i & 1)
return dp
# 'Пример использования'
if __name__ == '__main__':
n = 10
print(count_bits(n))
Описание решения:
В данной задаче использован побитовый оператор XOR, который обладает свойством: a ^ a = 0 и a ^ 0 = a. Применяя XOR ко всем элементам массива, пары одинаковых чисел взаимно обнуляются, оставляя единственное число, которое встречается один раз.
Сложность:
Время: O(n) — поскольку каждый элемент обрабатывается один раз; Память: O(1) — используется фиксированное количество переменных.
Код на Python:
В данной задаче использован побитовый оператор XOR, который обладает свойством: a ^ a = 0 и a ^ 0 = a. Применяя XOR ко всем элементам массива, пары одинаковых чисел взаимно обнуляются, оставляя единственное число, которое встречается один раз.
Сложность:
Время: O(n) — поскольку каждый элемент обрабатывается один раз; Память: O(1) — используется фиксированное количество переменных.
Код на Python:
def single_number(nums):
result = 0
for num in nums:
result ^= num
return result
# Пример использования:
if __name__ == "__main__":
nums = [4, 1, 2, 1, 2]
print(single_number(nums))
Решение задачи:
Мы перебираем биты чисел a, b и c, сравнивая соответствующие биты:
- Если бит c равен 0, то оба соответствующих бита a и b должны быть 0, иначе их нужно перевернуть.
- Если бит c равен 1, то хотя бы один из битов a или b должен быть 1, если оба равны 0 – производится один переворот.
Сложность:
- Время: O(n), где n – количество бит (обычно n=32, что практически константно).
- Память: O(1), используются только переменные для подсчёта и работы с битами.
Пояснение реализации:
Решение использует простой перебор бит за битом с применением побитовых операций, что позволяет эффективно решать задачу.
Ассимптотическая сложность по времени: O(n) – перебор каждого бита (n обычно фиксировано: 32).
Использование памяти: O(1) – используются лишь несколько вспомогательных переменных для подсчёта переворотов.
Мы перебираем биты чисел a, b и c, сравнивая соответствующие биты:
- Если бит c равен 0, то оба соответствующих бита a и b должны быть 0, иначе их нужно перевернуть.
- Если бит c равен 1, то хотя бы один из битов a или b должен быть 1, если оба равны 0 – производится один переворот.
Сложность:
- Время: O(n), где n – количество бит (обычно n=32, что практически константно).
- Память: O(1), используются только переменные для подсчёта и работы с битами.
def min_flips(a: int, b: int, c: int) -> int:
flips = 0
# Пока хотя бы одно число не обнулится
while a or b or c:
a_bit = a & 1
b_bit = b & 1
c_bit = c & 1
# Если бит c равен 0, оба бита a и b должны быть 0
if c_bit == 0:
if a_bit == 1:
flips += 1
if b_bit == 1:
flips += 1
# Если бит c равен 1, хотя бы один из битов a и b должен быть 1
else:
if (a_bit | b_bit) == 0:
flips += 1
a //= 2
b //= 2
c //= 2
return flips
if __name__ == "__main__":
# Пример использования:
a_val = 2
b_val = 6
c_val = 5
print(f"Minimum flips: {min_flips(a_val, b_val, c_val)}")
Пояснение реализации:
Решение использует простой перебор бит за битом с применением побитовых операций, что позволяет эффективно решать задачу.
Ассимптотическая сложность по времени: O(n) – перебор каждого бита (n обычно фиксировано: 32).
Использование памяти: O(1) – используются лишь несколько вспомогательных переменных для подсчёта переворотов.
Реализация Trie на Python
Пояснение
- Выбор структуры данных Trie обеспечивает вставку и поиск за O(L), где L – длина слова или префикса.
- Использование словаря в каждом узле позволяет динамически добавлять символы, что оптимально по памяти при умеренной плотности данных.
- Операции
Вывод: Реализация оптимальна по времени и памяти, используя стандартную библиотеку Python без лишних зависимостей.
class TrieNode:
def __init__(self):
self.children = { }
self.end_of_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str) -> None:
node = self.root
for ch in word:
if ch not in node.children:
node.children[ch] = TrieNode()
node = node.children[ch]
node.end_of_word = True
def search(self, word: str) -> bool:
node = self.root
for ch in word:
if ch not in node.children:
return False
node = node.children[ch]
return node.end_of_word
def startsWith(self, prefix: str) -> bool:
node = self.root
for ch in prefix:
if ch not in node.children:
return False
node = node.children[ch]
return True
if __name__ == '__main__':
trie = Trie()
trie.insert("apple")
print(trie.search("apple")) -#> True
print(trie.search("app")) -#> False
print(trie.startsWith("app")) -#> True
Пояснение
- Выбор структуры данных Trie обеспечивает вставку и поиск за O(L), где L – длина слова или префикса.
- Использование словаря в каждом узле позволяет динамически добавлять символы, что оптимально по памяти при умеренной плотности данных.
- Операции
insert, search и startsWith работают за O(L) по времени и требуют памяти, пропорциональной суммарной длине всех вставленных слов (O(N)).Вывод: Реализация оптимальна по времени и памяти, используя стандартную библиотеку Python без лишних зависимостей.
Решение
Для данной задачи оптимально сначала отсортировать массив products, а затем для каждого префикса searchWord с помощью бинарного поиска найти индекс первого потенциального совпадения. После чего достаточно проверить до 3 элементов, удовлетворяющих условию, что позволяет получить решение с асимптотической сложностью O(n log n + m log n), где n – количество продуктов, m – длина строки поиска.
Пояснение
Сначала сортировка массива осуществляется за O(n log n). Для каждого префикса выполняется бинарный поиск за O(log n), и проверка до 3 элементов – константная операция. Итоговая временная сложность составляет O(n log n + m log n), а по памяти используется O(n + m) для хранения сортированного массива и результата.
Для данной задачи оптимально сначала отсортировать массив products, а затем для каждого префикса searchWord с помощью бинарного поиска найти индекс первого потенциального совпадения. После чего достаточно проверить до 3 элементов, удовлетворяющих условию, что позволяет получить решение с асимптотической сложностью O(n log n + m log n), где n – количество продуктов, m – длина строки поиска.
import bisect
def suggestedProducts(products, searchWord):
products.sort()
ans = []
prefix = ""
for ch in searchWord:
prefix += ch
idx = bisect.bisect_left(products, prefix)
suggestion = []
for i in range(idx, min(idx+3, len(products))):
if products[i].startswith(prefix):
suggestion.append(products[i])
else:
break
ans.append(suggestion)
return ans
# Пример использования функции
if __name__ == "'__main__'":
products = ['mobile', 'mouse', 'moneypot', 'monitor', 'mousepad']
searchWord = 'mouse'
print(suggestedProducts(products, searchWord))
Пояснение
Сначала сортировка массива осуществляется за O(n log n). Для каждого префикса выполняется бинарный поиск за O(log n), и проверка до 3 элементов – константная операция. Итоговая временная сложность составляет O(n log n + m log n), а по памяти используется O(n + m) для хранения сортированного массива и результата.
Описание решения
Алгоритм:
- Сортируем интервалы по значению их конца.
- Проходим по интервалам и выбираем интервалы, не пересекающиеся с предыдущим выбранным.
- Количество удалённых интервалов – разность между общим количеством интервалов и количеством выбранных.
Обоснование выбора алгоритма:
Такой жадный алгоритм гарантирует минимальное число удалений, потому что выбор интервала с минимальным концом оставляет максимальное пространство для последующих интервалов.
Сложность:
- Время: O(n log n) из-за сортировки, O(n) для итерации по интервалам, суммарно O(n log n).
- Память: O(n) для хранения списка интервалов (в случае сортировки in-place — O(1) дополнительной памяти).
Код на Python
Пояснение:
Этот код реализует жадный алгоритм для задачи поиска минимального количества удалений, необходимых для получения набора нескрывающихся интервалов. Выбор интервала, который заканчивается раньше, позволяет максимально увеличить число последующих не пересекающихся интервалов.
Алгоритм:
- Сортируем интервалы по значению их конца.
- Проходим по интервалам и выбираем интервалы, не пересекающиеся с предыдущим выбранным.
- Количество удалённых интервалов – разность между общим количеством интервалов и количеством выбранных.
Обоснование выбора алгоритма:
Такой жадный алгоритм гарантирует минимальное число удалений, потому что выбор интервала с минимальным концом оставляет максимальное пространство для последующих интервалов.
Сложность:
- Время: O(n log n) из-за сортировки, O(n) для итерации по интервалам, суммарно O(n log n).
- Память: O(n) для хранения списка интервалов (в случае сортировки in-place — O(1) дополнительной памяти).
Код на Python
def eraseOverlapIntervals(intervals):
# Сортируем интервалы по значению конца
intervals.sort(key=lambda x: x[1])
count = 0
end = float('-inf')
for interval in intervals:
if interval[0] >= end:
end = interval[1]
else:
count += 1
return count
if __name__ == "__main__":
# Пример использования:
intervals = [[1,2], [2,3], [3,4], [1,3]]
result = eraseOverlapIntervals(intervals)
print(result) # Ожидаемый вывод: 1
Пояснение:
Этот код реализует жадный алгоритм для задачи поиска минимального количества удалений, необходимых для получения набора нескрывающихся интервалов. Выбор интервала, который заканчивается раньше, позволяет максимально увеличить число последующих не пересекающихся интервалов.
Описание решения:
Решение реализовано с использованием жадного алгоритма.
- Сначала сортируем интервалы шаров по их конечной координате.
- Затем проходим по отсортированным интервалам, выбирая стрелу в позиции конца первого интервала.
- Если следующий интервал не пересекается с позицией выбранной стрелы, увеличиваем счётчик стрел и обновляем позицию стрелы на конец текущего интервала.
Временная сложность: O(n log n) из-за сортировки.
Памяти: O(n) для хранения входного массива (при условии, что сортировка происходит in-place – дополнительная память O(1)).
Реализация на Python:
Пояснение:
Мы сортируем интервалы по концу. Это позволяет выбрать минимальное число стрел, устанавливая стрелу на конец первого интервала и затем пропуская все пересекающиеся интервалы. Если новый интервал начинается после текущей стрелы, увеличиваем счётчик стрел и обновляем позицию. Такой подход обеспечивает оптимальную максимальную эффективность.
Решение реализовано с использованием жадного алгоритма.
- Сначала сортируем интервалы шаров по их конечной координате.
- Затем проходим по отсортированным интервалам, выбирая стрелу в позиции конца первого интервала.
- Если следующий интервал не пересекается с позицией выбранной стрелы, увеличиваем счётчик стрел и обновляем позицию стрелы на конец текущего интервала.
Временная сложность: O(n log n) из-за сортировки.
Памяти: O(n) для хранения входного массива (при условии, что сортировка происходит in-place – дополнительная память O(1)).
Реализация на Python:
def findMinArrowShots(points):
if not points:
return 0
# Сортировка по конечной точке интервала
points.sort(key=lambda x: x[1])
arrows = 1
arrow_pos = points[0][1]
# Проход по оставшимся интервалам
for start, end in points[1:]:
if start > arrow_pos:
arrows += 1
arrow_pos = end
return arrows
# Пример использования:
if __name__ == "__main__":
intervals = [[10, 16], [2, 8], [1, 6], [7, 12]]
print(findMinArrowShots(intervals))
Пояснение:
Мы сортируем интервалы по концу. Это позволяет выбрать минимальное число стрел, устанавливая стрелу на конец первого интервала и затем пропуская все пересекающиеся интервалы. Если новый интервал начинается после текущей стрелы, увеличиваем счётчик стрел и обновляем позицию. Такой подход обеспечивает оптимальную максимальную эффективность.
Решение задачи
Используем стек для отслеживания индексов температур.
Пояснение:
- Мы проходим по массиву температур один раз, для каждого дня сравнивая его с предыдущими значениями, хранящимися в стеке.
- Если текущая температура выше, чем температура дня из стека, то для этого дня вычисляется разница индексов.
- Стек гарантирует, что каждый элемент будет обработан единожды, что обеспечивает асимптотическую сложность по времени O(n) и по памяти O(n).
Используем стек для отслеживания индексов температур.
def dailyTemperatures(temperatures: list[int]) -> list[int]:
n = len(temperatures)
answer = [0] * n
stack = [] # 'stack' хранит индексы
for i, temp in enumerate(temperatures):
while stack and temperatures[stack[-1]] < temp:
idx = stack.pop()
answer[idx] = i - idx
stack.append(i)
return answer
if __name__ == "__main__":
temperatures = [73, 74, 75, 71, 69, 72, 76, 73]
print(dailyTemperatures(temperatures))
Пояснение:
- Мы проходим по массиву температур один раз, для каждого дня сравнивая его с предыдущими значениями, хранящимися в стеке.
- Если текущая температура выше, чем температура дня из стека, то для этого дня вычисляется разница индексов.
- Стек гарантирует, что каждый элемент будет обработан единожды, что обеспечивает асимптотическую сложность по времени O(n) и по памяти O(n).
Реализация задачи
Используем стек, в котором каждый элемент – пара (
Описание алгоритма:
- Для каждого нового значения
- Пока стек не пуст и цена на вершине стека меньше или равна текущей цене, суммируем их
- Добавляем в стек пару (
Пояснение:
Реализация использует стек для эффективного вычисления спана. Каждый элемент стека хранит цену и сумму спанов для дней с ценой не выше текущей. Операция
Используем стек, в котором каждый элемент – пара (
price, span). Описание алгоритма:
- Для каждого нового значения
price начинаем с span = 1. - Пока стек не пуст и цена на вершине стека меньше или равна текущей цене, суммируем их
span и удаляем элемент. - Добавляем в стек пару (
price, span) и возвращаем span.
class StockSpanner:
def __init__(self):
self.stack = []
def next(self, price: int) -> int:
span = 1
while self.stack and self.stack[-1][0] <= price:
span += self.stack.pop()[1]
self.stack.append((price, span))
return span
# Пример использования:
if __name__ == "__main__":
stockSpanner = StockSpanner()
# Пример входных данных
prices = [100, 80, 60, 70, 60, 75, 85]
# Ожидаемые span: [1, 1, 1, 2, 1, 4, 6]
results = [stockSpanner.next(price) for price in prices]
print(results)
Пояснение:
Реализация использует стек для эффективного вычисления спана. Каждый элемент стека хранит цену и сумму спанов для дней с ценой не выше текущей. Операция
next проходит по стеку до тех пор, пока условие удовлетворяется, что в сумме даёт амортизированное время O(1) на запрос. В худшем случае один вызов может занять O(n), но суммарное время на последовательность вызовов – O(n). Память занимает O(n), где n – количество запросов.