Тема 7

Алгоритмы и программы (функция, рекурсия, работа со строками, работа с файлами, сортировка, граф)

Что проверяют по спецификации:

Функции: def, параметры, returnОбласть видимости: локальные и глобальные переменныеРекурсия: базовый случай и стек вызововСтроки: индексы, срезы, переборМетоды строк, палиндромы, подсчёт символов, словари и множестваРабота с файламиСортировка и поискГрафы: представление, обходы, кратчайший путь, деревья

Суть за минуту

Эта тема — «продвинутое» программирование на Python: как разбивать программу на функции, как функция может вызывать саму себя (рекурсия), как обрабатывать строки и файлы, как упорядочивать и быстро искать данные и как описывать связи между объектами с помощью графов. На ЕНТ почти все задания здесь — «что выведет программа», «сколько раз будет вызвана функция», «сколько сравнений/обменов сделает сортировка», «сколько шагов сделает двоичный поиск» и «найдите кратчайший путь по таблице весов». Главный навык — аккуратно трассировать код вручную: выписывать значения переменных шаг за шагом и не путать индексы. Запомните несколько ключевых чисел: пузырёк на nn элементах делает n(n−1)2\frac{n(n-1)}{2} сравнений, двоичный поиск — не больше ⌈log⁡2(n+1)⌉\lceil\log_2(n+1)\rceil шагов, сумма степеней вершин графа равна удвоенному числу рёбер. Всё остальное — внимательность и пошаговая трассировка.

Объясняем просто

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

Функция = рецепт

Один раз описали рецепт (def), потом «готовим» по нему сколько угодно раз, подставляя разные продукты — параметры. return — это готовое блюдо, которое функция отдаёт обратно.

Рекурсия = матрёшка

Чтобы открыть большую матрёшку, открываем её и видим такую же, но поменьше. Самая маленькая не открывается — это базовый случай. Без него рекурсия никогда не закончится.

Строка = бусы

Каждая буса — символ со своим номером, номера начинаются с 0. Срез s[2:5] — снять бусины с номерами 2, 3, 4 (пятая не входит!).

Граф = карта дорог

Города — вершины, дороги — рёбра, длина дороги — вес. Задача «как доехать быстрее всего» — это поиск кратчайшего пути, и прямая дорога не всегда самая короткая.

Главные формулы

Cпузырёк=n(n−1)2C_{\text{пузырёк}} = \dfrac{n(n-1)}{2}
число сравнений в классической пузырьковой сортировке (и сортировке выбором) для nn элементов; для n=5n=5 это 1010.
kmax⁡=⌈log⁡2(n+1)⌉k_{\max} = \lceil \log_2(n+1) \rceil
наибольшее число шагов (сравнений с серединой) двоичного поиска в отсортированном массиве из nn элементов; для n=1000n=1000 это 1010, для n=15n=15 — 44.
∑deg⁡(v)=2∣E∣\sum \deg(v) = 2|E|
в неориентированном графе сумма степеней всех вершин равна удвоенному числу рёбер (каждое ребро «считается» у двух концов).
∣E∣полн=n(n−1)2|E|_{\text{полн}} = \dfrac{n(n-1)}{2}
число рёбер в полном неориентированном графе на nn вершинах (каждая вершина соединена с каждой).
∣E∣дерево=n−1|E|_{\text{дерево}} = n - 1
в дереве (связный граф без циклов) на nn вершинах ровно n−1n-1 ребро.
вызовов(fib(n))=2⋅Fn−1\text{вызовов}(fib(n)) = 2\cdot F_n - 1
сколько раз вызывается «наивная» рекурсивная fib(n) с базой n <= 2; например, для n=6n=6: 2⋅8−1=152\cdot8-1=15.

Понятия и термины

Функция
именованный блок кода, который вызывается в нужных местах программы по имени; в Python объявляется словом def и может возвращать значение через return.
Параметр (аргумент)
значение, которое передаётся подпрограмме из основной программы при её вызове; в заголовке def f(a, b=2) — формальные параметры, в вызове f(5) — фактические.
Процедура
вспомогательный алгоритм, выполняющий несколько действий; в Python это функция без return (она возвращает None).
Локальная и глобальная переменная
локальная создаётся внутри функции и видна только в ней; глобальная — в основной программе. Чтобы изменить глобальную внутри функции, нужен оператор global.
Рекурсия
приём, при котором функция вызывает саму себя (прямо или через другую функцию) для решения такой же задачи меньшего размера.
Базовый случай
условие выхода из рекурсии, при котором функция возвращает ответ без нового вызова самой себя (например, n <= 1 для факториала).
Строка (str)
упорядоченная неизменяемая последовательность символов; символы нумеруются с 0, доступны по индексу и срезу.
Файл
совокупность информации, хранящейся во внешней памяти компьютера под определённым именем; в Python открывается функцией open(имя, режим).
Сортировка
упорядочивание элементов набора данных по возрастанию или убыванию некоторого признака (ключа).
Граф
набор вершин (узлов) и рёбер (связей) между ними; бывает ориентированным и неориентированным, взвешенным и невзвешенным. Дерево — связный граф без циклов.

Разбор темы

Функции: def, параметры, return 10 класс

Функция — это кусок программы, которому дали имя. Объявляем один раз, вызываем сколько угодно. Схема: def имя(параметры):, затем тело с отступом 4 пробела. Слово return завершает функцию и возвращает результат туда, откуда её вызвали.

def area(a, b=2):
    return a * b

print(area(3))        # b взят по умолчанию: 6
print(area(3, 5))     # 15
print(area(b=4, a=10))  # именованные аргументы: 40

Параметр со значением по умолчанию (b=2) можно не передавать. Параметры по умолчанию пишутся после обычных: def f(a, b=2) — правильно, def f(a=1, b) — ошибка. При вызове можно указывать аргументы по имени, тогда порядок не важен.

Функция может вернуть сразу несколько значений: return a + b, a * b — получится кортеж, его удобно «распаковать»: s, p = f(2, 3). После return код в функции уже не выполняется.

Если return нет (или он пустой), функция возвращает None. Это и есть процедура: она что-то делает (например, печатает), но значения не отдаёт. Классическая ловушка ЕНТ: r = show(7) напечатает 14, а print(r) напечатает None.

Встроенные функции, которые полезно знать: len, abs, max, min, sum, sorted, int, str, round, ord (код символа), chr (символ по коду), range.

Область видимости: локальные и глобальные переменные 10 класс

Переменная, созданная внутри функции, — локальная: она живёт только во время вызова и снаружи не видна. Переменная основной программы — глобальная: функция может её читать, но присваивание внутри функции создаёт новую локальную переменную с тем же именем.

x = 10

def f():
    x = 5          # локальная x, глобальную не трогает
    print(x)

def g():
    global x       # теперь работаем с глобальной x
    x = x + 1

f()        # 5
print(x)   # 10
g()
print(x)   # 11

Правило для задач: если в функции есть присваивание x = ... без global x — это другая, локальная x. Если есть global x — функция меняет «общую» переменную.

Параметры функции — тоже локальные переменные. Числа и строки внутрь функции «копируются по смыслу»: изменить их снаружи через параметр нельзя. А вот список, переданный в функцию, можно изменить методами (a.append(...), a[0] = ...) — изменения видны снаружи, поэтому в сортировках часто делают копию a = a[:].

Рекурсия: базовый случай и стек вызовов 10–11 класс

Рекурсивная функция вызывает саму себя для задачи поменьше. Обязательно два элемента: базовый случай (когда отвечаем сразу) и рекурсивный шаг (сводим задачу к меньшей). Без базового случая будет бесконечная рекурсия и ошибка RecursionError (в Python по умолчанию предел глубины около 1000).

def fact(n):            # n! = 1·2·…·n
    if n <= 1:          # базовый случай
        return 1
    return n * fact(n - 1)

def power(a, n):        # a в степени n
    if n == 0:
        return 1
    return a * power(a, n - 1)

def digsum(n):          # сумма цифр
    if n < 10:
        return n
    return n % 10 + digsum(n // 10)

def fib(n):             # 1, 1, 2, 3, 5, 8, 13, ...
    if n <= 2:
        return 1
    return fib(n - 1) + fib(n - 2)

Как это работает: каждый вызов кладётся в стек вызовов (как тарелки в стопку) и ждёт, пока вернётся вызов поменьше. fact(4) ждёт fact(3), тот ждёт fact(2), тот — fact(1). fact(1) сразу возвращает 1, и стек «разматывается» обратно: 2⋅1=22\cdot1=2, 3⋅2=63\cdot2=6, 4⋅6=244\cdot6=24.

Если внутри функции два рекурсивных вызова (как в fib), вызовы ветвятся деревом и их число растёт очень быстро: fib(6) вызывает функцию 15 раз, fib(7) — 25 раз. Счёт вызовов удобно вести по формуле C(n)=C(n−1)+C(n−2)+1C(n)=C(n-1)+C(n-2)+1 с C(1)=C(2)=1C(1)=C(2)=1.

Порядок вывода. Если print стоит ДО рекурсивного вызова, числа печатаются «на спуске» (в прямом порядке); если ПОСЛЕ — «на подъёме» (в обратном). Пример: def g(n): print(n); if n > 1: g(n // 2); print(n) при g(10) выведет 10 5 2 1 1 2 5 10.

Любую рекурсию можно заменить циклом (итерацией) и наоборот. Цикл обычно быстрее и не тратит стек; рекурсия короче записывается для «самоподобных» задач — обхода деревьев и графов (DFS), быстрой сортировки, вычисления по рекуррентной формуле.

Строки: индексы, срезы, перебор 10 класс

Строка — последовательность символов. Нумерация с 0 слева и с −1 справа. Для s = "informatika" (11 символов): s[0] = i, s[4] = r, s[-1] = a, len(s) = 11. Индекс за пределами строки (например s[11]) даёт ошибку IndexError.

Срез s[start:stop:step] берёт символы с номера start до stop, НЕ включая stop, с шагом step. Пропущенный start — с начала, пропущенный stop — до конца. s[2:5] = for, s[:4] = info, s[7:] = tika, s[-3:] = ika, s[::2] = ifraia, s[1::3] = nrta, s[::-1] = akitamrofni (строка задом наперёд). Срез за пределами строки ошибки не даёт — просто вернёт то, что есть.

Строки неизменяемы: s[0] = "I" — ошибка. Чтобы «изменить» строку, строят новую: s = "I" + s[1:]. Операции: + склеивает, * повторяет ("ab" * 3 = ababab), in проверяет вхождение ("for" in s → True), строки сравниваются посимвольно по кодам ("apple" < "banana", но "Z" < "a", потому что заглавные буквы имеют меньшие коды).

Перебор строки: for ch in s: — по символам; for i in range(len(s)): — по индексам (нужно, когда важна позиция, например сравнить s[i] и s[i+1]). Коды символов: ord("A") = 65, ord("a") = 97, chr(66) = B.

Методы строк, палиндромы, подсчёт символов, словари и множества 10 класс

Методы не меняют строку, а возвращают новую: s.upper() / s.lower() — регистр; s.strip() — убрать пробелы по краям; s.replace(a, b) — заменить все вхождения a на b (s.replace(a, b, 1) — только первое); s.find(x) — индекс первого вхождения или −1, если нет; s.count(x) — число непересекающихся вхождений; s.split(sep) — разрезать в список; sep.join(список) — склеить список строк; s.isdigit() / s.isalpha() — состоит ли строка только из цифр / букв.

t = "Astana, Almaty, Shymkent"
parts = t.split(", ")     # ['Astana', 'Almaty', 'Shymkent']
print("-".join(parts))     # Astana-Almaty-Shymkent
print(t.find("Almaty"))    # 8
print(t.lower().count("a")) # 5
print("aaaa".count("aa"))  # 2 (не 3!)

Палиндром — строка, которая читается одинаково слева направо и справа налево: level, kazak, 12321. Проверка в одну строку: s == s[::-1]. Если регистр не важен, сначала s = s.lower(); если надо игнорировать пробелы — s.replace(" ", "").

Подсчёт символов. Цифры: if ch.isdigit(): total += int(ch). Частоты букв удобно хранить в словаре: freq[ch] = freq.get(ch, 0) + 1; для "banana" получится {'b': 1, 'a': 3, 'n': 2}. Словарь (dict) хранит пары «ключ: значение», доступ по ключу d["a"], len(d) — число ключей.

Множество (set) хранит только уникальные элементы без порядка: set("banana") = {'a', 'b', 'n'}, len(set(s)) — число различных символов. Операции: | объединение, & пересечение, - разность.

Работа с файлами 7, 10 класс

Файл открывают функцией open(имя, режим). Режимы: "r" — чтение (по умолчанию; файла нет — ошибка), "w" — запись (файл создаётся, а если был — полностью ОЧИЩАЕТСЯ), "a" — дозапись в конец (старое содержимое сохраняется). Конструкция with open(...) as f: сама закроет файл после блока — так писать правильнее, чем вручную вызывать f.close().

Чтение: f.read() — весь файл одной строкой; f.readline() — одна следующая строка вместе с символом \n в конце; f.readlines() — список всех (оставшихся) строк; for line in f: — перебор строк по одной. Запись: f.write(текст) пишет только строку и сам перенос НЕ добавляет — \n нужно писать явно; числа перед записью переводят в строку: f.write(str(x) + "\n").

with open("data.txt", "w") as f:
    f.write("5\n12\n7\n20\n3\n")

with open("data.txt") as f:
    nums = [int(line) for line in f]   # int сам отбросит \n

print(sum(nums), max(nums))   # 47 20

Типичная задача: «в файле записаны числа по одному в строке, найдите сумму / количество чётных / максимум». Шаблон: прочитать строки, превратить каждую в число (int(line) или int(line.strip())), дальше обычный цикл со счётчиком. Если числа записаны в одной строке через пробел: nums = list(map(int, f.read().split())).

Файл читается последовательно, как магнитофон: после readline() «указатель» стоит на следующей строке, и readlines() вернёт уже только оставшиеся строки. Повторное чтение с начала — заново открыть файл.

Сортировка и поиск 9–10 класс

Пузырьковая сортировка: проходим по массиву, сравниваем соседей и меняем местами, если левый больше правого. После первого прохода самый большой элемент «всплывает» в конец; после kk-го прохода на местах стоят kk последних. Для nn элементов: n−1n-1 проход, сравнений (n−1)+(n−2)+⋯+1=n(n−1)2(n-1)+(n-2)+\dots+1=\frac{n(n-1)}{2}. Число обменов равно числу «инверсий» — пар, стоящих в неправильном порядке.

Сортировка выбором: на шаге ii ищем минимум среди элементов с ii-го до конца и меняем его с ii-м. Сравнений тоже n(n−1)2\frac{n(n-1)}{2}, но обменов не больше n−1n-1. Сортировка вставками: берём очередной элемент и сдвигаем его влево, пока слева стоит больший, — как сортируют карты в руке; на почти отсортированных данных работает очень быстро.

for i in range(n - 1):              # пузырёк
    for j in range(n - 1 - i):
        if a[j] > a[j + 1]:
            a[j], a[j + 1] = a[j + 1], a[j]

Встроенные средства: sorted(a) возвращает НОВЫЙ отсортированный список, a.sort() сортирует сам список и возвращает None. Параметры: reverse=True — по убыванию; key=функция — сортировать по признаку: sorted(words, key=len) — по длине, a.sort(key=abs) — по модулю. Сортировка в Python устойчивая: равные по ключу элементы сохраняют исходный порядок.

Линейный поиск — просматриваем элементы по порядку; в худшем случае nn сравнений, работает для любого массива. Двоичный (бинарный) поиск — только для ОТСОРТИРОВАННОГО массива: сравниваем с серединой mid = (lo + hi) // 2, отбрасываем половину, где искомого быть не может; шагов не больше ⌈log⁡2(n+1)⌉\lceil\log_2(n+1)\rceil (для 1000 элементов — 10, для миллиона — 20).

Графы: представление, обходы, кратчайший путь, деревья 10 класс

Граф — вершины и рёбра. Неориентированный граф: ребро A–B можно пройти в обе стороны (дороги). Ориентированный: дуга A→B ведёт только в одну сторону (улицы с односторонним движением, подписки). Взвешенный граф: у каждого ребра есть число — вес (расстояние, время, стоимость). Путь — последовательность вершин, соединённых рёбрами; длина пути — число рёбер или сумма весов; цикл — путь, который возвращается в начальную вершину.

Степень вершины — число рёбер, выходящих из неё (петля даёт 2). Сумма степеней всех вершин равна 2∣E∣2|E|, поэтому число вершин нечётной степени всегда чётно. В ориентированном графе различают полустепени входа и выхода.

Матрица смежности — таблица n×nn\times n: в клетке [A][B] стоит 1 (или вес), если есть ребро A–B, иначе 0 или прочерк. У неориентированного графа матрица симметрична относительно главной диагонали; степень вершины = сумма её строки. Список смежности — для каждой вершины перечисляем соседей; в Python удобно словарём: g = {"A": ["B", "C"], "B": ["A", "D"], ...}.

Обход в ширину (BFS) — «волной»: сначала все соседи стартовой вершины, потом их соседи и т.д. Использует очередь; находит путь с наименьшим числом рёбер. Обход в глубину (DFS) — идём как можно дальше по одной ветке, упираемся — возвращаемся назад; удобно писать рекурсией.

Кратчайший путь во взвешенном графе (алгоритм Дейкстры, решается по таблице вручную): 1) стартовой вершине ставим 0, остальным ∞; 2) выбираем непосещённую вершину с наименьшей меткой и «фиксируем» её; 3) для каждого её соседа пробуем улучшить метку: новая = метка текущей + вес ребра, если меньше старой — заменяем; 4) повторяем до финиша. Главное правило: прямое ребро не обязательно самое короткое — путь в обход через другие вершины может быть дешевле.

Дерево — связный граф без циклов; в дереве на nn вершинах ровно n−1n-1 ребро и между любыми двумя вершинами ровно один путь. Корневое дерево: корень, родители, потомки, листья (вершины без потомков). Деревом изображают структуру папок, родословную, дерево рекурсивных вызовов. Число путей в ориентированном графе без циклов считают так: количество путей в вершину = сумма количеств путей во все вершины, из которых в неё ведут дуги.

Примеры задач с решением

Задача 1Что выведет программа?
def f(a, b=3):
    return a * b - 1

x = f(2)
y = f(2, 5)
print(x, y, f(b=1, a=y))
  1. f(2): параметр b не передан, берётся значение по умолчанию 3: 2⋅3−1=52\cdot3-1=5, значит x = 5.
  2. f(2, 5): 2⋅5−1=92\cdot5-1=9, значит y = 9.
  3. f(b=1, a=y): аргументы по имени, a = 9, b = 1: 9⋅1−1=89\cdot1-1=8.
  4. print выводит три значения через пробел.
Ответ 5 9 8
Задача 2Дана функция. Чему равно digsum(4872) и сколько раз будет вызвана функция digsum (считая первый вызов)?
def digsum(n):
    if n < 10:
        return n
    return n % 10 + digsum(n // 10)
  1. Спуск: digsum(4872) = 2 + digsum(487); digsum(487) = 7 + digsum(48); digsum(48) = 8 + digsum(4).
  2. digsum(4): 4<104<10 — базовый случай, возвращает 4 без нового вызова.
  3. Подъём: 8+4=128+4=12, 7+12=197+12=19, 2+19=212+19=21.
  4. Вызовы: с аргументами 4872, 487, 48, 4 — всего 4 (по одному на каждую цифру).
Ответ 21; функция вызвана 4 раза
Задача 3s = "informatika". Найдите s[2:5], s[1::3], s[-3:] и s[::-1].
  1. Выпишем индексы: i0 n1 f2 o3 r4 m5 a6 t7 i8 k9 a10 (всего 11 символов).
  2. s[2:5] — индексы 2, 3, 4 (5 не входит): for.
  3. s[1::3] — с индекса 1 с шагом 3: 1, 4, 7, 10 → n, r, t, a: nrta.
  4. s[-3:] — три последних символа: ika; s[::-1] — строка задом наперёд: akitamrofni.
Ответ for, nrta, ika, akitamrofni
Задача 4Массив [5, 1, 4, 2, 8] сортируется пузырьком по возрастанию. Каким будет массив после первого прохода? Сколько всего будет сравнений и обменов?
  1. Проход 1: (5,1) — обмен → [1,5,4,2,8]; (5,4) — обмен → [1,4,5,2,8]; (5,2) — обмен → [1,4,2,5,8]; (5,8) — без обмена. После прохода: [1,4,2,5,8], 4 сравнения, 3 обмена.
  2. Проход 2: (1,4) нет; (4,2) обмен → [1,2,4,5,8]; (4,5) нет — 3 сравнения, 1 обмен.
  3. Проходы 3 и 4: 2 + 1 сравнения, обменов нет.
  4. Сравнений: 4+3+2+1=10=5⋅424+3+2+1=10=\frac{5\cdot4}{2}. Обменов: 3+1=43+1=4 — это число инверсий (5>1, 5>4, 5>2, 4>2).
Ответ [1, 4, 2, 5, 8]; 10 сравнений, 4 обмена
Задача 5Двоичным поиском ищем число 46 в массиве [3, 7, 12, 18, 25, 31, 40, 46, 52, 60, 71] (индексы 0–10, mid = (lo + hi) // 2). Сколько сравнений с серединой понадобится?
  1. Шаг 1: lo=0, hi=10, mid=5, a[5]=31 < 46 → lo=6.
  2. Шаг 2: lo=6, hi=10, mid=8, a[8]=52 > 46 → hi=7.
  3. Шаг 3: lo=6, hi=7, mid=6, a[6]=40 < 46 → lo=7.
  4. Шаг 4: lo=7, hi=7, mid=7, a[7]=46 — найдено. Проверка оценкой: ⌈log⁡212⌉=4\lceil\log_2 12\rceil=4 — больше 4 шагов и не могло быть.
Ответ 4 сравнения, число найдено по индексу 7
Задача 6Дана таблица длин дорог между пунктами A–F (прочерк — дороги нет). Найдите длину кратчайшего пути из A в F и сам путь.
    A   B   C   D   E   F
A   -   4   2   -   -   -
B   4   -   1   5   -   -
C   2   1   -   8  10   -
D   -   5   8   -   2   6
E   -   -  10   2   -   3
F   -   -   -   6   3   -
  1. Старт: A=0, остальные ∞. Фиксируем A. Соседи: B=4, C=2.
  2. Наименьшая метка у C (2), фиксируем C. Через C: B=min(4, 2+1)=3, D=2+8=10, E=2+10=12.
  3. Фиксируем B (3). Через B: D=min(10, 3+5)=8.
  4. Фиксируем D (8). Через D: E=min(12, 8+2)=10, F=8+6=14.
  5. Фиксируем E (10). Через E: F=min(14, 10+3)=13. Фиксируем F=13.
  6. Восстанавливаем путь с конца: F←E←D←B←C←A. Проверка: 2+1+5+2+3=132+1+5+2+3=13. Заметьте: прямая дорога A–B (4) длиннее, чем A–C–B (3).
Ответ 13, путь A→C→B→D→E→F

Сложные задачи: шаг за шагом

Задача 1Дана рекурсивная процедура. Что выведет вызов f(3)? Сколько чисел выведет f(5) и чему равна их сумма?
def f(n):
    if n > 0:
        f(n - 1)
        print(n, end=" ")
        f(n - 1)

Идея: Запишите вывод через вывод для меньшего n: вывод f(n) = вывод f(n−1), затем n, затем снова вывод f(n−1). Из этого получаются рекуррентные формулы для количества и суммы.

  1. Маленькие значения
    f(0) ничего не выводит. f(1): f(0), печать 1, f(0) → 1.
    f(2): вывод f(1), затем 2, затем вывод f(1) → 1 2 1.
  2. Вывод f(3)
    f(3) = вывод f(2) + 3 + вывод f(2) = 1 2 1 3 1 2 1.
  3. Количество чисел
    K(n)=2K(n−1)+1K(n)=2K(n-1)+1, K(0)=0K(0)=0: K(1)=1K(1)=1, K(2)=3K(2)=3, K(3)=7K(3)=7, K(4)=15K(4)=15, K(5)=31K(5)=31, то есть K(n)=2n−1K(n)=2^n-1.
  4. Сумма чисел
    S(n)=2S(n−1)+nS(n)=2S(n-1)+n, S(0)=0S(0)=0: S(1)=1S(1)=1, S(2)=4S(2)=4, S(3)=11S(3)=11, S(4)=26S(4)=26, S(5)=57S(5)=57.
    Проверка для f(3): 1+2+1+3+1+2+1=111+2+1+3+1+2+1=11 — совпадает.
Ответ f(3) выведет 1 2 1 3 1 2 1; f(5) выведет 31 число с суммой 57
Задача 2Сколько раз будет вызвана функция fib при вычислении fib(7) (считая первый вызов)? Чему равно fib(7)?
def fib(n):
    if n <= 2:
        return 1
    return fib(n - 1) + fib(n - 2)

Идея: Число вызовов C(n) = 1 (сам вызов) + C(n−1) + C(n−2); для базы C(1)=C(2)=1. Не рисуйте всё дерево — считайте таблицей снизу вверх.

  1. Значения функции
    fib: 1, 1, 2, 3, 5, 8, 13 для n = 1…7, значит fib(7) = 13.
  2. Таблица вызовов
    C(1)=C(2)=1C(1)=C(2)=1; C(3)=1+1+1=3C(3)=1+1+1=3; C(4)=1+3+1=5C(4)=1+3+1=5; C(5)=1+5+3=9C(5)=1+5+3=9; C(6)=1+9+5=15C(6)=1+9+5=15; C(7)=1+15+9=25C(7)=1+15+9=25.
  3. Проверка формулой
    C(n)=2⋅fib(n)−1C(n)=2\cdot fib(n)-1: 2⋅13−1=252\cdot13-1=25 — совпадает. Видно, почему наивная рекурсия медленная: одно и то же fib(3) пересчитывается много раз (это лечится запоминанием результатов или циклом).
Ответ 25 вызовов; fib(7) = 13
Задача 3В ориентированном графе есть дуги: A→B, A→C, B→C, B→D, C→D, C→E, D→E, D→F, E→F. Сколько существует различных путей из A в F? Сколько из них проходят через D?

Идея: В графе без циклов число путей в вершину равно сумме чисел путей во все вершины, из которых в неё входят дуги. Считаем по порядку от A.

  1. Считаем пути до каждой вершины
    A = 1. B: входит только из A → 1. C: из A и B → 1+1 = 2. D: из B и C → 1+2 = 3. E: из C и D → 2+3 = 5. F: из D и E → 3+5 = 8.
  2. Пути через D
    Путей A→D: 3. Путей D→F: напрямую D→F и D→E→F — 2.
    Через D проходит 3⋅2=63\cdot2=6 путей (правило умножения).
  3. Проверка
    Пути, не проходящие через D, должны идти A→…→C→E→F: A→C→E→F и A→B→C→E→F — ровно 2, и 6+2=86+2=8 — сходится.
Ответ 8 путей, из них 6 через D

Частые ловушки на ЕНТ

  • Срез не включает правую границу: s[2:5] — это 3 символа (индексы 2, 3, 4), а нумерация начинается с 0. Всегда выписывайте индексы под буквами.
  • print внутри функции и return — разные вещи. Функция без return возвращает None; a.sort() тоже возвращает None, поэтому print(a.sort()) выведет None, а b = a.sort() испортит b.
  • Рекурсия без базового случая (или с базовым случаем, который никогда не достигается, например n == 0 при шаге n - 2 от нечётного n) не останавливается и падает с RecursionError.
  • Режим "w" стирает старое содержимое файла, дописывать нужно в режиме "a". readline() возвращает строку вместе с \n, а write() перенос сам не добавляет.
  • Двоичный поиск работает только на отсортированном массиве. А в задаче на кратчайший путь прямое ребро не обязательно самое короткое — проверяйте пути в обход.

Запомнить

Функция: def + return (без return → None); рекурсия = базовый случай + шаг; срез s[a:b] без b; w стирает, a дописывает; пузырёк — n(n−1)/2 сравнений; двоичный поиск — только по отсортированному, ⌈log₂(n+1)⌉ шагов; сумма степеней = 2·рёбер; Дейкстра — фиксируй минимальную метку.

Проверь себя

1. Что выведет программа: s = "informatika"; print(s[2:5])?
  1. for
  2. nfo
  3. form
  4. orm
Показать ответ

for Индексы 2, 3, 4 (правая граница 5 не входит): f, o, r.

2. Функция задана так: def f(n): return 0 if n == 0 else n % 10 + f(n // 10). Что выведет print(f(2024))?
  1. 2024
  2. 4
  3. 8
  4. 6
Показать ответ

8 Функция считает сумму цифр: 4+2+0+2=84+2+0+2=8.

3. Сколько сравнений выполнит классическая пузырьковая сортировка массива из 5 элементов?
  1. 5
  2. 10
  3. 20
  4. 25
Показать ответ

10 n(n−1)2=5⋅42=10\frac{n(n-1)}{2}=\frac{5\cdot4}{2}=10 (проходы по 4, 3, 2 и 1 сравнению).

4. В неориентированном графе 6 вершин и 8 рёбер. Чему равна сумма степеней всех вершин?
  1. 6
  2. 8
  3. 14
  4. 16
Показать ответ

16 Каждое ребро добавляет 1 к степени двух вершин: 2⋅8=162\cdot8=16.

5. Что произойдёт при открытии существующего файла командой open("data.txt", "a") и записи в него?
  1. Файл будет очищен, затем записан текст
  2. Возникнет ошибка, так как файл уже существует
  3. Текст будет дописан в конец файла
  4. Файл откроется только для чтения
Показать ответ

Текст будет дописан в конец файла Режим "a" (append) сохраняет старое содержимое и пишет в конец; очищает файл режим "w".

6. Какое наибольшее число сравнений с серединой понадобится двоичному поиску в отсортированном массиве из 1000 элементов?
  1. 500
  2. 10
  3. 100
  4. 1000
Показать ответ

10 Каждый шаг делит диапазон пополам: ⌈log⁡21001⌉=10\lceil\log_2 1001\rceil=10, так как 210=1024>10002^{10}=1024>1000.

Связанные темы

Узнай свой уровень: бесплатная диагностика ЕНТ