Задание 6: исполнители и анализ программы с циклом

Задание 6 (базовый уровень): что сделает исполнитель и сколько программ приводят к нужному результату.

Исполнитель — это абстрактное устройство с фиксированным набором команд (двигаться, умножать, рисовать). Задание 6 проверяет умение проследить выполнение программы с циклом и анализировать её результат.

Что проверяет задание

Даётся исполнитель (например, Чертёжник, перемещающийся по координатам, или числовой автомат с командами вроде «прибавь 3», «умножь на 2») и программа с циклом. Вопросы бывают двух типов: «какой результат даст программа» (прямое моделирование) и «сколько различных программ из заданных команд приводят из A в B» (перебор/подсчёт путей). Оба идеально ложатся на Python.

Тип 1: прямое моделирование цикла

Если в задании дан конкретный код исполнителя с циклом, проще всего повторить его на Python и посмотреть результат. Пусть Чертёжник стартует в точке (0, 0) и выполняет программу: «повторить 4 раза: сместиться на (2, 1)», затем «сместиться на (−3, 5)».

x, y = 0, 0
# повторить 4 раза: сдвиг на (2, 1)
for _ in range(4):
    x += 2
    y += 1
# затем один сдвиг на (-3, 5)
x += -3
y += 5
print("итоговая точка:", (x, y))

Вывод:

итоговая точка: (5, 9)

Моделирование исключает арифметические ошибки: где бы ни было сложение или вычитание, машина считает точно. Если в исполнителе есть условие внутри цикла — переносите его в код один в один.

Тип 2: подсчёт числа программ

Самая частая разновидность: «У исполнителя две команды: 1) прибавь 3; 2) умножь на 2. Сколько различных программ переводят число 2 в число 25?» Здесь нужно перебрать все последовательности команд. Удобно — поиск в ширину (BFS): из каждого числа пробуем обе команды, считаем, сколько путей доходят до цели.

from collections import deque

start, goal = 2, 25

def count_programs(start, goal):
    count = 0
    queue = deque([(start, 0)])         # (текущее число, длина программы)
    while queue:
        value, steps = queue.popleft()
        if value == goal:
            count += 1
            continue
        if value > goal or steps > 20:  # отсекаем перелёт и слишком длинные
            continue
        queue.append((value + 3, steps + 1))   # команда 1: +3
        queue.append((value * 2, steps + 1))   # команда 2: *2
    return count

print("программ, ведущих 2 -> 25:", count_programs(start, goal))

Вывод:

программ, ведущих 2 -> 25: 6

Важно отсекать ветви, где число уже превысило цель (value > goal): команды только увеличивают число, поэтому из «перелёта» вернуться нельзя, и такие пути считать бессмысленно. Без отсечения перебор был бы бесконечным.

Альтернатива: рекурсивный подсчёт

Тот же подсчёт можно записать короткой рекурсией — она часто понятнее и так же надёжна:

def count(value, goal):
    if value == goal:
        return 1            # дошли — это одна программа
    if value > goal:
        return 0            # перелёт — тупик
    # пробуем обе команды и складываем числа способов
    return count(value + 3, goal) + count(value * 2, goal)

print("программ 2 -> 25:", count(2, 25))

Вывод:

программ 2 -> 25: 6

Эта рекурсия — мостик к заданию 16: тот же приём «число способов = сумма способов из достижимых состояний» лежит в основе всех задач на подсчёт путей.

Вариант с тремя командами и «запретным» числом

Часто условие усложняют: добавляют третью команду и/или запрещают проходить через определённое число. Например: «Команды: +1, +2, ·3. Сколько программ переводят 1 в 20, не проходя через число 7?» Запрет — это просто дополнительное отсечение в рекурсии:

def count(value, goal, forbidden):
    if value == forbidden:
        return 0                # через запретное число нельзя
    if value == goal:
        return 1
    if value > goal:
        return 0
    return (count(value + 1, goal, forbidden)
            + count(value + 2, goal, forbidden)
            + count(value * 3, goal, forbidden))

print("программ 1 -> 20, минуя 7:", count(1, 20, 7))

Вывод:

программ 1 -> 20, минуя 7: 3444

Запрет «не проходить через 7» добавляется одной строкой if value == forbidden: return 0. Это типовое усложнение задания 6 — и оно же встречается в задании 23 (Часть 2), где путей бывают тысячи. Освоив приём здесь, вы готовы к более тяжёлым версиям.

Типичные ловушки

  • Отсечение перелёта. Если команды только увеличивают значение, обязательно обрывайте ветви, где значение превысило цель, — иначе перебор не остановится.
  • Порядок команд важен. «+3 потом ·2» и «·2 потом +3» — это разные программы, обе нужно учесть.
  • Считаем программы, а не числа. Вопрос обычно про количество последовательностей команд, а не про множество достижимых чисел.
  • Координаты Чертёжника. Следите за знаками смещений и за тем, сколько раз выполняется тело цикла.

Итог

  • «Что получится» — повторите программу исполнителя на Python один в один.
  • «Сколько программ A → B» — перебор (BFS или рекурсия), складывая числа способов из достижимых состояний.
  • Если команды монотонно увеличивают значение, отсекайте «перелёт» — иначе перебор не завершится.
Проверьте себя
1. Исполнитель: команды +3 и ·2, числа только растут. Почему нужно отсекать ветви value > goal?
AЧтобы программа работала быстрее на больших числах
BИначе перебор не остановится — из перелёта цель уже недостижима
CЧтобы не считать отрицательные числа
DТак требует синтаксис Python
2. «+3 потом ·2» и «·2 потом +3» — это...
AОдна и та же программа
BДве разные программы, обе считаются
CНедопустимые последовательности
DПрограммы с одинаковым результатом
3. Чертёжник из (0,0) делает 4 раза сдвиг (2,1), затем сдвиг (−3,5). Где он окажется?
A(8, 4)
B(5, 9)
C(2, 6)
D(11, 9)
Поддержать проект