Решение различных задач на Python (2 часть)
Задача 21: Проверка на совершенное число
Принцип решения: Найти все делители числа (кроме самого числа) и проверить их сумму
Что используется: Цикл, математические операции, проверка делителей
def is_perfect_number(n: int) -> bool:
"""Проверяет, является ли число совершенным."""
if n <= 1:
return False
divisors_sum = 1 # 1 всегда делитель
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
divisors_sum += i
if i != n // i: # Добавляем парный делитель, если он не равен i
divisors_sum += n // i
return divisors_sum == n
# Пояснение: Совершенное число равно сумме своих собственных делителей.
# Пример: 6 = 1 + 2 + 3, 28 = 1 + 2 + 4 + 7 + 14
# Проверяем делители до √n, находим парные делители.
Задача 22: Вычисление числа π методом Монте-Карло
Принцип решения: Генерация случайных точек в квадрате и подсчет точек внутри четверти круга
Что используется: Библиотека random, математические операции, статистика
import random
def estimate_pi(num_points: int) -> float:
"""Оценивает число π методом Монте-Карло."""
points_inside = 0
for _ in range(num_points):
x = random.random() # [0, 1)
y = random.random() # [0, 1)
# Проверяем, попадает ли точка в четверть круга радиусом 1
if x**2 + y**2 <= 1:
points_inside += 1
# Площадь четверти круга / площадь квадрата = π/4
return 4 * points_inside / num_points
# Пояснение: Отношение точек внутри четверти круга к общему числу точек ≈ π/4
# Чем больше точек, тем точнее оценка. Нужно ≈ 1 млн точек для точности 0.001
Задача 23: Решето Эратосфена для поиска простых чисел
Принцип решения: Последовательное вычеркивание составных чисел
Что используется: Список булевых значений, вложенные циклы
def sieve_of_eratosthenes(n: int) -> list:
"""Находит все простые числа до n с помощью решета Эратосфена."""
if n < 2:
return []
is_prime = [True] * (n + 1)
is_prime[0] = is_prime[1] = False
for i in range(2, int(n**0.5) + 1):
if is_prime[i]:
# Вычеркиваем все кратные i, начиная с i²
for j in range(i * i, n + 1, i):
is_prime[j] = False
return [i for i in range(n + 1) if is_prime[i]]
# Пояснение: Алгоритм последовательно помечает кратные каждого простого числа как составные.
# Начинаем с i=2, вычеркиваем 4,6,8... затем i=3, вычеркиваем 9,12,15...
# Оптимизация: начинаем вычеркивать с i², так как меньшие кратные уже вычеркнуты.
Задача 24: Числа Армстронга (нарциссические числа)
Принцип решения: Сумма цифр в степени количества цифр равна самому числу
Что используется: Преобразование числа в строку, возведение в степень
def is_armstrong_number(n: int) -> bool:
"""Проверяет, является ли число числом Армстронга."""
if n < 0:
return False
digits = str(n)
power = len(digits)
total = sum(int(digit)**power for digit in digits)
return total == n
# Пояснение: Число Армстронга: сумма его цифр, возведенных в степень количества цифр, равна самому числу.
# Пример: 153 = 1³ + 5³ + 3³ = 1 + 125 + 27 = 153
# 9474 = 9⁴ + 4⁴ + 7⁴ + 4⁴ = 6561 + 256 + 2401 + 256 = 9474
Задача 25: Конвертация числа в римскую систему счисления
Принцип решения: Последовательное вычитание максимальных возможных римских значений
Что используется: Словарь соответствий, цикл while
def int_to_roman(num: int) -> str:
"""Конвертирует целое число в римскую запись."""
roman_numerals = [
(1000, 'M'), (900, 'CM'), (500, 'D'), (400, 'CD'),
(100, 'C'), (90, 'XC'), (50, 'L'), (40, 'XL'),
(10, 'X'), (9, 'IX'), (5, 'V'), (4, 'IV'), (1, 'I')
]
result = []
for value, symbol in roman_numerals:
while num >= value:
result.append(symbol)
num -= value
return ''.join(result)
# Пояснение: Римская система использует вычитательную нотацию (IV = 4, IX = 9).
# Проходим от больших значений к меньшим, вычитая максимально возможные.
# Пример: 1994 → 1000(M) + 900(CM) + 90(XC) + 4(IV) = MCMXCIV
Задача 26: Конвертация римского числа в арабское
Принцип решения: Обработка символов слева направо с учетом вычитательной нотации
Что используется: Словарь значений, проверка следующего символа
def roman_to_int(s: str) -> int:
"""Конвертирует римскую запись в целое число."""
roman_values = {
'I': 1, 'V': 5, 'X': 10, 'L': 50,
'C': 100, 'D': 500, 'M': 1000
}
total = 0
i = 0
while i < len(s):
# Если текущий символ меньше следующего, это вычитательная нотация
if i + 1 < len(s) and roman_values[s[i]] < roman_values[s[i + 1]]:
total += roman_values[s[i + 1]] - roman_values[s[i]]
i += 2
else:
total += roman_values[s[i]]
i += 1
return total
# Пояснение: IV = 4 (5 - 1), IX = 9 (10 - 1), XL = 40 (50 - 10) и т.д.
# Если меньшая цифра стоит перед большей, вычитаем ее значение.
# Иначе складываем значения символов.
Задача 27: Проверка валидности email-адреса
Принцип решения: Проверка структуры с использованием регулярных выражений
Что используется: Модуль re (регулярные выражения)
import re
def is_valid_email(email: str) -> bool:
"""Проверяет валидность email-адреса."""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))
# Пояснение: Регулярное выражение проверяет:
# 1. Локальная часть: буквы, цифры, ., _, %, +, -
# 2. Символ @
# 3. Доменное имя: буквы, цифры, ., -
# 4. Точка и домен верхнего уровня (минимум 2 буквы)
# Пример валидных: user@example.com, name.last@sub.domain.org
Задача 28: Генерация случайного пароля
Принцип решения: Выбор случайных символов из разных наборов
Что используется: Библиотеки random и string
import random
import string
def generate_password(length: int = 12) -> str:
"""Генерирует случайный пароль заданной длины."""
if length < 4:
raise ValueError("Пароль должен быть минимум 4 символа")
# Наборы символов
lowercase = string.ascii_lowercase
uppercase = string.ascii_uppercase
digits = string.digits
special = "!@#$%^&*"
# Гарантируем минимум по одному символу из каждой категории
password = [
random.choice(lowercase),
random.choice(uppercase),
random.choice(digits),
random.choice(special)
]
# Добавляем остальные символы случайным образом
all_chars = lowercase + uppercase + digits + special
password.extend(random.choice(all_chars) for _ in range(length - 4))
# Перемешиваем для случайного порядка
random.shuffle(password)
return ''.join(password)
# Пояснение: Создаем пароль, гарантируя наличие разных типов символов.
# Перемешиваем результат для большей случайности.
# Длина по умолчанию 12 символов считается безопасной.
Задача 29: ROT13 шифрование
Принцип решения: Сдвиг букв на 13 позиций в алфавите
Что используется: Строковые операции, арифметика символов
def rot13(text: str) -> str:
"""Применяет шифр ROT13 к тексту."""
result = []
for char in text:
if 'a' <= char <= 'z':
# Сдвиг для строчных букв
result.append(chr((ord(char) - ord('a') + 13) % 26 + ord('a')))
elif 'A' <= char <= 'Z':
# Сдвиг для заглавных букв
result.append(chr((ord(char) - ord('A') + 13) % 26 + ord('A')))
else:
# Оставляем другие символы без изменений
result.append(char)
return ''.join(result)
# Пояснение: ROT13 - частный случай шифра Цезаря со сдвигом 13.
# Особенность: ROT13 обратим сам к себе (двойное применение дает исходный текст).
# Пример: rot13("Hello") = "Uryyb", rot13("Uryyb") = "Hello"
Задача 30: Проверка на счастливый билет
Принцип решения: Сравнение суммы первых трех цифр с суммой последних трех
Что используется: Преобразование числа в строку, сумма цифр
def is_lucky_ticket(ticket: int) -> bool:
"""Проверяет, является ли билет счастливым (суммы половин равны)."""
# Форматируем с ведущими нулями
ticket_str = f"{ticket:06d}"
if len(ticket_str) != 6:
return False
first_half = sum(int(digit) for digit in ticket_str[:3])
second_half = sum(int(digit) for digit in ticket_str[3:])
return first_half == second_half
# Пояснение: Счастливый билет - когда сумма первых трех цифр равна сумме последних трех.
# Пример: 123456 → 1+2+3 = 6, 4+5+6 = 15 → не счастливый
# 123321 → 1+2+3 = 6, 3+2+1 = 6 → счастливый
Задача 31: Упаковка и распаковка последовательных элементов (RLE)
Принцип решения: Кодирование последовательных одинаковых символов
Что используется: Цикл по строке, подсчет последовательностей
def rle_encode(s: str) -> str:
"""Кодирует строку с помощью RLE (Run-Length Encoding)."""
if not s:
return ""
encoded = []
count = 1
current_char = s[0]
for char in s[1:]:
if char == current_char:
count += 1
else:
encoded.append(f"{count}{current_char}")
current_char = char
count = 1
encoded.append(f"{count}{current_char}")
return ''.join(encoded)
def rle_decode(encoded: str) -> str:
"""Декодирует строку, закодированную RLE."""
decoded = []
i = 0
while i < len(encoded):
# Собираем число (может быть многозначным)
count_str = ""
while i < len(encoded) and encoded[i].isdigit():
count_str += encoded[i]
i += 1
if i < len(encoded):
char = encoded[i]
decoded.append(char * int(count_str))
i += 1
return ''.join(decoded)
# Пояснение: RLE заменяет последовательности одинаковых символов на "количество символ".
# Пример: "AAABBBCCDAA" → "3A3B2C1D2A"
# Эффективно для данных с большими последовательностями одинаковых значений.
Задача 32: Проверка на треугольник и определение его типа
Принцип решения: Проверка неравенства треугольника и сравнение сторон
Что используется: Условные операторы, математические проверки
def triangle_type(a: float, b: float, c: float) -> str:
"""Определяет тип треугольника по длинам сторон."""
# Проверка на треугольник
if a <= 0 or b <= 0 or c <= 0:
return "Не треугольник"
sides = sorted([a, b, c])
if sides[0] + sides[1] <= sides[2]:
return "Не треугольник"
# Определение типа
if a == b == c:
return "Равносторонний"
elif a == b or b == c or a == c:
if abs(a**2 + b**2 - c**2) < 0.0001 or \
abs(a**2 + c**2 - b**2) < 0.0001 or \
abs(b**2 + c**2 - a**2) < 0.0001:
return "Равнобедренный прямоугольный"
return "Равнобедренный"
elif abs(a**2 + b**2 - c**2) < 0.0001 or \
abs(a**2 + c**2 - b**2) < 0.0001 or \
abs(b**2 + c**2 - a**2) < 0.0001:
return "Прямоугольный"
else:
return "Разносторонний"
# Пояснение: Проверяем неравенство треугольника: сумма любых двух сторон > третьей.
# Типы: равносторонний (все равны), равнобедренный (две равны),
# прямоугольный (a² + b² = c²), разносторонный (все разные).
Задача 33: Вычисление определителя матрицы 2x2
Принцип решения: Применение формулы для определителя матрицы 2x2
Что используется: Списки списков, математическая формула
def determinant_2x2(matrix: list) -> float:
"""Вычисляет определитель матрицы 2x2."""
if len(matrix) != 2 or len(matrix[0]) != 2 or len(matrix[1]) != 2:
raise ValueError("Матрица должна быть 2x2")
# Формула: ad - bc
return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0]
# Пояснение: Для матрицы [[a, b], [c, d]] определитель = a*d - b*c
# Определитель показывает, обратима ли матрица (≠0) и масштаб преобразования.
# Пример: [[1, 2], [3, 4]] → 1*4 - 2*3 = 4 - 6 = -2
Задача 34: Проверка на магический квадрат
Принцип решения: Проверка сумм строк, столбцов и диагоналей
Что используется: Вложенные циклы, множества для проверки уникальности
def is_magic_square(matrix: list) -> bool:
"""Проверяет, является ли матрица магическим квадратом."""
n = len(matrix)
# Проверяем, что все строки имеют одинаковую длину
for row in matrix:
if len(row) != n:
return False
# Проверяем, что все числа от 1 до n² используются ровно один раз
numbers = set()
for row in matrix:
for num in row:
numbers.add(num)
if numbers != set(range(1, n*n + 1)):
return False
# Вычисляем магическую сумму
magic_sum = n * (n*n + 1) // 2
# Проверяем суммы строк
for row in matrix:
if sum(row) != magic_sum:
return False
# Проверяем суммы столбцов
for j in range(n):
col_sum = sum(matrix[i][j] for i in range(n))
if col_sum != magic_sum:
return False
# Проверяем главную диагональ
diag1 = sum(matrix[i][i] for i in range(n))
if diag1 != magic_sum:
return False
# Проверяем побочную диагональ
diag2 = sum(matrix[i][n-1-i] for i in range(n))
return diag2 == magic_sum
# Пояснение: Магический квадрат n×n содержит числа от 1 до n²,
# и суммы всех строк, столбцов и диагоналей равны.
# Магическая сумма = n(n² + 1)/2
Задача 35: Калькулятор выражений с учетом приоритета операций
Принцип решения: Преобразование в обратную польскую нотацию (RPN)
Что используется: Стеки, словарь приоритетов операций
def calculate_expression(expression: str) -> float:
"""Вычисляет арифметическое выражение с учетом приоритетов."""
def apply_operator(operators, values):
operator = operators.pop()
right = values.pop()
left = values.pop()
if operator == '+':
values.append(left + right)
elif operator == '-':
values.append(left - right)
elif operator == '*':
values.append(left * right)
elif operator == '/':
values.append(left / right)
elif operator == '^':
values.append(left ** right)
# Удаляем пробелы
expression = expression.replace(" ", "")
values = []
operators = []
i = 0
precedence = {'+': 1, '-': 1, '*': 2, '/': 2, '^': 3}
while i < len(expression):
if expression[i].isdigit() or expression[i] == '.':
# Считываем число
j = i
while j < len(expression) and (expression[j].isdigit() or expression[j] == '.'):
j += 1
values.append(float(expression[i:j]))
i = j
elif expression[i] == '(':
operators.append('(')
i += 1
elif expression[i] == ')':
while operators and operators[-1] != '(':
apply_operator(operators, values)
operators.pop() # Удаляем '('
i += 1
else:
# Оператор
while (operators and operators[-1] != '(' and
precedence.get(operators[-1], 0) >= precedence.get(expression[i], 0)):
apply_operator(operators, values)
operators.append(expression[i])
i += 1
# Применяем оставшиеся операторы
while operators:
apply_operator(operators, values)
return values[0]
# Пояснение: Используем алгоритм сортировочной станции (Shunting-yard) Дейкстры.
# Преобразуем инфиксную запись в обратную польскую нотацию и вычисляем.
# Поддерживает скобки и приоритеты операций.
Задача 36: Генерация всех перестановок строки
Принцип решения: Рекурсивная генерация перестановок
Что используется: Рекурсия, множества для исключения дубликатов
def permutations(s: str) -> list:
"""Генерирует все уникальные перестановки строки."""
def backtrack(path, used, result):
if len(path) == len(s):
result.append(''.join(path))
return
for i in range(len(s)):
if used[i] or (i > 0 and s[i] == s[i-1] and not used[i-1]):
continue
used[i] = True
path.append(s[i])
backtrack(path, used, result)
path.pop()
used[i] = False
# Сортируем для обработки дубликатов
s = sorted(s)
result = []
backtrack([], [False] * len(s), result)
return result
# Пояснение: Используем backtracking для генерации всех перестановок.
# Отслеживаем использованные символы и пропускаем дубликаты при одинаковых символах.
# Количество перестановок для n уникальных символов = n!
# Для "aab": ["aab", "aba", "baa"]
Задача 37: Поиск самого частого слова в тексте
Принцип решения: Подсчет частоты слов с очисткой от знаков препинания
Что используется: Словарь Counter, регулярные выражения, обработка текста
import re
from collections import Counter
def most_common_word(text: str) -> tuple:
"""Находит самое частое слово в тексте."""
# Убираем знаки препинания и приводим к нижнему регистру
cleaned_text = re.sub(r'[^\w\s]', '', text.lower())
# Разбиваем на слова
words = cleaned_text.split()
# Подсчитываем частоту
word_counts = Counter(words)
# Находим самое частое
if word_counts:
most_common = word_counts.most_common(1)[0]
return most_common[0], most_common[1]
return None, 0
# Пояснение: Очищаем текст от знаков препинания, приводим к нижнему регистру,
# разбиваем на слова и подсчитываем частоту с помощью Counter.
# most_common(1) возвращает список из одного элемента (слово, частота).
Задача 38: Проверка на изоморфные строки
Принцип решения: Построение взаимно-однозначного соответствия между символами
Что используется: Два словаря для двустороннего отображения
def are_isomorphic(s1: str, s2: str) -> bool:
"""Проверяет, являются ли строки изоморфными."""
if len(s1) != len(s2):
return False
mapping_s1_to_s2 = {}
mapping_s2_to_s1 = {}
for c1, c2 in zip(s1, s2):
# Проверяем отображение из s1 в s2
if c1 in mapping_s1_to_s2:
if mapping_s1_to_s2[c1] != c2:
return False
else:
mapping_s1_to_s2[c1] = c2
# Проверяем отображение из s2 в s1
if c2 in mapping_s2_to_s1:
if mapping_s2_to_s1[c2] != c1:
return False
else:
mapping_s2_to_s1[c2] = c1
return True
# Пояснение: Строки изоморфны, если существует взаимно-однозначное отображение символов.
# Пример: "egg" и "add" изоморфны: e→a, g→d
# "foo" и "bar" не изоморфны: f→b, o→a, o→r (o отображается и в a, и в r)
Задача 39: Кодирование Хаффмана (базовый вариант)
Принцип решения: Построение префиксного кода на основе частоты символов
Что используется: Очередь с приоритетом (heapq), двоичное дерево
import heapq
from collections import Counter
class HuffmanNode:
def __init__(self, char, freq):
self.char = char
self.freq = freq
self.left = None
self.right = None
def __lt__(self, other):
return self.freq < other.freq
def huffman_encode(text: str) -> dict:
"""Строит коды Хаффмана для символов текста."""
if not text:
return {}
# Подсчет частот
freq = Counter(text)
# Создаем узлы и помещаем в кучу
heap = [HuffmanNode(char, freq) for char, freq in freq.items()]
heapq.heapify(heap)
# Строим дерево Хаффмана
while len(heap) > 1:
left = heapq.heappop(heap)
right = heapq.heappop(heap)
merged = HuffmanNode(None, left.freq + right.freq)
merged.left = left
merged.right = right
heapq.heappush(heap, merged)
# Обходим дерево для получения кодов
codes = {}
def traverse(node, code):
if node.char is not None:
codes[node.char] = code
return
traverse(node.left, code + "0")
traverse(node.right, code + "1")
traverse(heap[0], "")
return codes
# Пояснение: Алгоритм Хаффмана строит оптимальный префиксный код.
# Частые символы получают короткие коды, редкие - длинные.
# Коды префиксные: ни один код не является началом другого.
Задача 40: Алгоритм Кадана для поиска максимального подмассива
Принцип решения: Динамическое программирование - нахождение лучшей подпоследовательности
Что используется: Динамическое программирование, один проход по массиву
def max_subarray_sum(arr: list) -> tuple:
"""Находит максимальную сумму подмассива и его границы."""
if not arr:
return 0, -1, -1
max_so_far = arr[0]
max_ending_here = arr[0]
start = 0
end = 0
temp_start = 0
for i in range(1, len(arr)):
if arr[i] > max_ending_here + arr[i]:
max_ending_here = arr[i]
temp_start = i
else:
max_ending_here += arr[i]
if max_ending_here > max_so_far:
max_so_far = max_ending_here
start = temp_start
end = i
return max_so_far, start, end
# Пояснение: Алгоритм Кадана находит подмассив с максимальной суммой за O(n).
# Идея: на каждом шаге выбираем - начать новый подмассив или продолжить текущий.
# Пример: [-2, 1, -3, 4, -1, 2, 1, -5, 4] → сумма 6, подмассив [4, -1, 2, 1]
Каждая задача решена с использованием типичных для Python подходов и содержит подробные комментарии о принципах работы алгоритмов. Эти задачи охватывают различные аспекты программирования: математические вычисления, обработку строк, алгоритмы сжатия, динамическое программирование и структуры данных.