Почему два разных подхода к решению задачи о подсчете путей преобразования чисел дают разные результаты?
Задача: Исполнитель преобразует число на экране. У исполнителя есть две команды:
- Прибавить 1
- Умножить на 2
Программа для исполнителя – это последовательность команд. Нужно найти количество программ, которые при исходном числе 1 дают результат 30, при этом траектория вычислений не проходит через число 10.
Подход 1 (код 1):
def f(st, end):
a={st: 1}
for i in range(st + 1, end + 1):
a[i]=0
if i - 1 in a:
a[i]+=a[i - 1]
if i % 2 == 0 and int(i//2) in a:
a[i]+=a[int(i//2)]
return a[end]
print(f(1,9)*f(11,30))
Подход 2 (код 2):
def f(st, end, propusk):
a={st: 1}
for i in range(st + 1, end + 1):
a[i]=0
a[propusk]=0
if i - 1 in a:
a[i]+=a[i - 1]
if i % 2 == 0 and int(i//2) in a:
a[i]+=a[int(i//2)]
return a[end]
print(f(1,30, 10))
В первом подходе я пытаюсь дойти от 1 до 9, а затем от 11 до 30, тем самым игнорируя 10. Однако это почему-то не правильно.
Во втором подходе я просто “говорю”, что в 10 не ведут пути, каждый раз когда 10 участвует в счете, оно возвращает 0 (не считаем пути из него).
Почему первый код неправильный? По сути же я так же не учитываю пути из 10.
Первый подход неправильный, потому что он предполагает, что задачу можно разбить на независимые подзадачи (от 1 до 9 и от 11 до 30), и результаты можно перемножить. Однако это неверно, потому что в действительности нет пути из 9 в 11, минуя 10 - граф не позволяет таких переходов, поэтому подход просто считает пустое множество путей.
Содержание
- Основная проблема первого подхода
- Анализ работы первого кода
- Анализ работы второго кода
- Математическое обоснование различий
- Правильное решение задачи
- Примеры путей для наглядности
- Заключение
Основная проблема первого подхода
Ключевая ошибка первого подхода заключается в неправильном предположении о разделимости задачи. Разработчик предположил, что можно просто посчитать пути от 1 до 9, затем от 11 до 30 и перемножить результаты. Однако это фундаментально неверно, потому что:
- Нет непрерывных путей: В действительности пути должны быть непрерывными последовательностями от начального до конечного числа
- Отсутствие перехода 9 → 11: Чтобы перейти из 9 в 11, необходимо пройти через 10 (9 + 1 = 10, затем 10 + 1 = 11)
- Нарушение структуры графа: Операции “прибавить 1” и “умножить на 2” создают специфическую структуру графа, где некоторые переходы невозможны
Анализ работы первого кода
Рассмотрим детально, что делает первый подход:
def f(st, end):
a={st: 1}
for i in range(st + 1, end + 1):
a[i]=0
if i - 1 in a:
a[i]+=a[i - 1]
if i % 2 == 0 and int(i//2) in a:
a[i]+=a[int(i//2)]
return a[end]
print(f(1,9)*f(11,30))
Что происходит:
f(1,9)считает количество путей от 1 до 9f(11,30)считает количество путей от 11 до 30- Результаты перемножаются
Проблема: Этот подход не учитывает, что нет путей из 9 в 11, минуя 10. В результате подход просто умножает два числа, но не учитывает, что эти пути на самом деле не могут быть объединены в единый путь от 1 до 30.
Важно: В графе операций “прибавить 1” и “умножить на 2” нет дуги из 9 в 11. Единственный способ попасть из 9 в 11 - это пройти через 10, что запрещено условием.
Анализ работы второго кода
Второй подход реализует правильную стратегию:
def f(st, end, propusk):
a={st: 1}
for i in range(st + 1, end + 1):
a[i]=0
a[propusk]=0
if i - 1 in a:
a[i]+=a[i - 1]
if i % 2 == 0 and int(i//2) in a:
a[i]+=a[int(i//2)]
return a[end]
print(f(1,30, 10))
Ключевые отличия:
- Единое вычисление: Считает все пути от 1 до 30 за один проход
- Явное исключение: На каждой итерации явно устанавливает
a[10] = 0 - Корректная обработка ограничений: Динамическое программирование корректно учитывает запрет на прохождение через 10
Как это работает:
- Алгоритм строит таблицу
a, гдеa[i]- количество путей от 1 доi - На каждом шаге явно запрещает использование числа 10
- Корректно учитывает только те пути, которые никогда не проходят через 10
Математическое обоснование различий
Пусть:
P(S, E)- множество путей из S в EP'(S, E)- множество путей из S в E, минуя 10
Первый подход вычисляет: |P(1, 9)| × |P(11, 30)|
Второй подход вычисляет: |P'(1, 30)|
Математическая ошибка:
Причина: Множество P'(1, 30) требует, чтобы путь был непрерывной последовательностью от 1 до 30. Разбиение на P(1, 9) и P(11, 30) нарушает это требование, так как нет продолжения из 9 в 11.
Правильное соотношение:
где в сумме учитываются только те пути, которые проходят через 10.
Правильное решение задачи
Правильный подход должен:
- Считать все пути от 1 до 30 без ограничений
- Вычитать пути, которые проходят через 10
- Или явно исключать 10 при динамическом программировании
Альтернативный правильный код:
def count_paths(start, end, forbidden):
# Сначала считаем все пути без ограничений
all_paths = dynamic_programming(start, end)
# Затем считаем пути, проходящие через forbidden
paths_through_forbidden = 0
for k in range(start, end + 1):
if k == forbidden:
continue
paths_through_forbidden += dynamic_programming(start, k) * dynamic_programming(k, end)
# Вычитаем запрещенные пути
return all_paths - paths_through_forbidden
def dynamic_programming(start, end):
dp = {}
dp[start] = 1
for i in range(start + 1, end + 1):
dp[i] = 0
if i - 1 in dp:
dp[i] += dp[i - 1]
if i % 2 == 0 and (i // 2) in dp:
dp[i] += dp[i // 2]
return dp[end]
Примеры путей для наглядности
Пример путей из 1 в 9:
- 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9
- 1 → 2 → 4 → 5 → 6 → 7 → 8 → 9
- 1 → 2 → 4 → 8 → 9
Пример путей из 11 в 30:
- 11 → 12 → 13 → 14 → 15 → 16 → 17 → 18 → 19 → 20 → 21 → 22 → 23 → 24 → 25 → 26 → 27 → 28 → 29 → 30
- 11 → 12 → 24 → 25 → 26 → 27 → 28 → 29 → 30
Почему нельзя объединить?
- Нет пути из 9 в 11: 9 + 1 = 10 (запрещено), 9 × 2 = 18
- Единственный вариант: 9 → 10 → 11, но 10 запрещен
Заключение
Основные выводы:
-
Неправильная декомпозиция: Первый подход ошибочно разбивает задачу на независимые подзадачи, хотя граф операций не позволяет таких разбиений.
-
Отсутствие непрерывности: Пути должны быть непрерывными последовательностями, а первый подход нарушает это требование.
-
Корректное динамическое программирование: Второй подход правильно использует динамическое программирование с явным исключением запрещенного числа.
-
Математическая ошибка: Умножение количеств путей в независимых сегментах не равно количеству путей в связной задаче с ограничениями.
Рекомендация: При решении задач на подсчет путей с ограничениями всегда используйте единое динамическое программирование с явным учетом всех ограничений, а не пытайтесь декомпозировать задачу на независимые подзадачи.