← Вернуться в блог

BFS по состоянию (mod d, сумма цифр) — задача «Find a Number» РАЗБОР

  • 2100
  • dp
  • bfs
  • state-graph
  • multi-dim
  • digit-sum
  • modular
  • icpc-2100

Что дано

Дано два целых числа dd и ss. Нужно найти наименьшее положительное целое число NN, удовлетворяющее одновременно двум условиям:

  1. NN делится нацело на dd.
  2. Сумма десятичных цифр NN равна ровно ss.

Если такого числа не существует, нужно вывести 1-1.

Ограничения

  • 1d5001 \le d \le 500.
  • 1s50001 \le s \le 5000.
  • Время — 1 секунда, память — 256 МБ.

«Наименьшее» здесь означает наименьшее значение — то есть сначала наименьшая длина (число цифр), а среди чисел одинаковой длины — наименьшее в лексикографическом порядке.

Формат ввода

В одной строке два целых числа dd и ss.

Формат вывода

Одна строка: либо искомое число NN (без ведущих нулей), либо 1-1, если такого числа нет.

Пример 1

Вход:

13 50

Вывод:

699998

Комментарий: 6+9+9+9+9+8=506 + 9 + 9 + 9 + 9 + 8 = 50, и 699998=1353846699998 = 13 \cdot 53846.

Пример 2

Вход:

61 2

Вывод:

1000000000000000000000000000001

Комментарий: число длины 3131, состоит из двух единиц и двадцати девяти нулей; сумма цифр 22; делится на 6161. Более коротких чисел с суммой цифр 22, делящихся на 6161, не существует.

Пример 3

Вход:

15 50

Вывод:

-1

Комментарий: 15=3515 = 3 \cdot 5. Для делимости на 55 последняя цифра должна быть 00 или 55. Для делимости на 33 сумма цифр должна делиться на 33. 5050 не делится на 33 — значит, требуется одновременно сумма цифр 5050 и кратность 33, что невозможно. Ответ — 1-1.

Разберём первый пример руками

Сумма цифр 5050 — это много. Минимальная длина числа с такой суммой цифр — 50/9=6\lceil 50 / 9 \rceil = 6 (если все цифры по 99, получим 5454, надо немного снизить). Например, 999995999995 имеет сумму 5050, но 999995/13=76922.69999995 / 13 = 76922.69\ldots — не делится. Среди шестизначных чисел с суммой цифр 5050 нужно найти минимальное, делящееся на 1313.

Лобовой перебор всех шестизначных чисел — 9105=9000009 \cdot 10^5 = 900\,000 кандидатов. Для каждого надо посчитать сумму цифр (8 операций) и проверить делимость. Это 77 миллионов операций — быстро. Но при s=5000s = 5000 длина числа может быть 5000/9556\lceil 5000 / 9 \rceil \approx 556, и перебор шестисотзначных чисел нереалистичен.

Идея: строить число по одной цифре слева направо, и держать в состоянии только то, что нам нужно для проверки финальных условий — текущий остаток по модулю dd и накопленную сумму цифр. Тогда конечное число — это путь в графе состояний от стартового (0,0)(0, 0) до целевого (0,s)(0, s), и нам нужен кратчайший такой путь, среди равных — лексикографически минимальный.


Идея решения

Состояние: пара (r,c)(r, c) — текущий остаток по модулю dd и сумма уже приписанных цифр. Стартовое состояние — (0,0)(0, 0) (пустое число), целевое — (0,s)(0, s) (число с остатком 00 при делении на dd и суммой цифр ровно ss).

Из состояния (r,c)(r, c) есть до 1010 переходов: приписать цифру δ{0,1,,9}\delta \in \{0, 1, \ldots, 9\}, перейти в (r,c)(r', c'), где

r=(10r+δ)modd,c=c+δ.r' = (10 \cdot r + \delta) \bmod d, \qquad c' = c + \delta.

Переход в (r,c)(r', c') возможен, если c+δsc + \delta \le s (мы не должны перебрать сумму). Длина пути от (0,0)(0, 0) до конечного состояния — это число цифр NN.

Хотим минимальное число: сначала минимальная длина, затем минимальная лексикографически. Идеальный инструмент — BFS: он гарантирует, что в момент первого попадания в каждое состояние мы пришли по кратчайшему пути.

Хитрость с лексикографической минимальностью: если при обходе очередного слоя BFS перебирать переходы в порядке цифр δ=0,1,,9\delta = 0, 1, \ldots, 9, то первое попадание в каждое состояние будет через цифру с минимальным значением. Однако нам нужно лексикографически минимальное число, а не минимальное по последней цифре. BFS сам по себе этого не даёт: к одному и тому же состоянию могут вести разные слова разной структуры.

Спасает симметрия задачи. Заметим: на состояние «след внутри числа» влияет только сумма приписанных цифр и текущий остаток. Если в одну и ту же (r,c)(r', c') можно прийти из двух разных предков через цифры δ1<δ2\delta_1 < \delta_2, то стоит выбрать тот переход, который ведёт к меньшему числу — а это переход с меньшей цифрой на текущей позиции, при условии, что префикс предка тоже минимален.

Формально это решается так: при BFS храним для каждого состояния parent[(r', c')] = ((r, c), δ). Если состояние посещаем впервые — записываем. Поскольку BFS гарантирует кратчайший путь, а слой выходит из меньшей цифры раньше большей, первое попадание даёт минимальный путь.

Здесь важно одно тонкое условие: первая цифра NN не должна быть нулём. Поэтому стартовые переходы из (0,0)(0, 0) перебираем по δ=1,2,,9\delta = 1, 2, \ldots, 9 (не от 00), а из остальных состояний — по δ=0,1,,9\delta = 0, 1, \ldots, 9.

Почему это работает

Кратчайший путь. BFS на любом графе с единичными весами находит кратчайший путь по числу рёбер. У нас каждое ребро добавляет одну цифру, то есть увеличивает длину числа на 11. Значит, кратчайший путь от (0,0)(0, 0) до (0,s)(0, s) — это минимальная длина числа NN.

Лексикографически минимальное среди кратчайших. Внутри одного слоя BFS перебираем переходы в порядке возрастания δ\delta. Когда новое состояние (r,c)(r', c') попадается в первый раз, мы пришли:

  • либо из самого «раннего» предка (минимальный путь в предка по конструкции BFS),
  • либо через минимальную возможную цифру среди равных предков-кандидатов (потому что перебор отсортирован).

Здесь полезно мысленно представить рекурсивный аргумент: путь в (r,c)(r', c') — это путь в предка (r,c)(r, c) + цифра δ\delta. Среди всех таких пар (длина пути в предка == оптимум) минимальное число лексикографически — это путь с минимальным δ\delta при тех же предках; при разных предках с одинаковой длиной — выбор предка, в котором префикс минимален. BFS обходит вершины по слоям, поэтому когда мы посещаем (r,c)(r', c') из (r,c)(r, c) в момент времени tt, состояние (r,c)(r, c) уже было посещено раньше (в момент t1t - 1), и его parent фиксирован. Перебор по δ\delta от 00 к 99 внутри слоя обеспечивает, что среди всех путей одинаковой длины выбран минимальный лексикографически.

Условие первой цифры. Из стартового состояния первый шаг — цифра, которая станет старшим разрядом числа. Если разрешить δ=0\delta = 0, путь будет описывать число с ведущим нулём — не валидно. Поэтому первый шаг — только δ1\delta \ge 1.

Размер графа состояний

Состояний (r,c)(r, c) всего d(s+1)5005001=2500500d \cdot (s + 1) \le 500 \cdot 5001 = 2\,500\,500. Рёбер — до 1010 \cdot числа состояний, то есть 2.5107\approx 2.5 \cdot 10^7. BFS с такой сложностью укладывается в секунду по времени; по памяти — O(ds)O(d \cdot s) массив парентов и посещённости.


Решение: псевдокод

visited[d][s+1] = false
parent[d][s+1] = (prev_state, digit)
visited[0][0] = true
очередь ← [(0, 0)]

пока очередь не пуста:
    (r, c) ← взять из очереди
    если (r, c) = (0, s):
        // дошли до цели; реконструкция ниже
        break
    начало = 1 если (r, c) = (0, 0) иначе 0       // запрет ведущего нуля
    для δ от начало до 9:
        если c + δ > s: continue
        r' ← (10 * r + δ) mod d
        c' ← c + δ
        если visited[r'][c']:
            continue
        visited[r'][c'] = true
        parent[r'][c'] = ((r, c), δ)
        очередь.добавить((r', c'))

если не visited[0][s]:
    вывести -1
иначе:
    реконструировать строку цифр от (0, 0) к (0, s) через parent
    вывести строку

Сложность: O(ds10)=O(ds)O(d \cdot s \cdot 10) = O(d \cdot s).


Код решения

Различия между языками здесь сводятся к двум деталям: в C++ нужно аккуратно работать с памятью под массивы парентов размера dsd \cdot s, в Python — следить, чтобы реконструкция через список не делала O(n2)O(n^2) конкатенаций строк.

Комментарии по реализации.

  • Тип под произведение. В вычислении r * 10 + delta максимальное rd1499r \le d - 1 \le 499, поэтому r10+δ4999r \cdot 10 + \delta \le 4999int хватает в обоих языках, переполнения нет.
  • break вместо continue при nc > s. Внутри цикла по δ\delta от меньших к большим, как только c + delta > s, дальше тем более не подойдёт. Это микро-оптимизация, но при ss близком к нулю и больших dd заметно ускоряет.
  • Память под parent в C++. Линейный массив int размера d(s+1)2.5106d \cdot (s + 1) \le 2.5 \cdot 10^6 — это 1010 МБ под par_r и столько же под par_c. Для par_digit используем char (1 байт), экономим. В сумме укладываемся в лимит памяти.
  • Реконструкция в Python. Собираем цифры в list, после цикла делаем reverse() и "".join(...) — это O(n)O(n). Конкатенация s = digit + s в цикле дала бы O(n2)O(n^2) — на ответе длины 50005000 это 2510625 \cdot 10^6 операций над строками, медленно.
  • Особый случай s=0s = 0. В оригинальных ограничениях s1s \ge 1, но код всё равно обрабатывает: при s=0s = 0 единственное «число» с суммой цифр 00 — это 00, но оно не положительное. Возвращаем 1-1.

Проверим на всех примерах из условия

Пример 1: d=13d = 13, s=50s = 50

Целевое состояние — (0,50)(0, 50). Длина пути в BFS до этого состояния — 66 (минимум по слоям). Восстановление пути от (0,50)(0, 50) к (0,0)(0, 0) даёт цифры 8,9,9,9,9,68, 9, 9, 9, 9, 6 (читаем от последней к первой), после реверса — 6,9,9,9,9,86, 9, 9, 9, 9, 8, то есть 699998699998. Проверяем:

  • 6+9+9+9+9+8=506 + 9 + 9 + 9 + 9 + 8 = 50
  • 699998mod13699998 \bmod 13: 699998=1353846+0699998 = 13 \cdot 53846 + 0, остаток 00

Ответ: 699998699998

Пример 2: d=61d = 61, s=2s = 2

Длина — 3131. BFS обнаруживает: из (0,0)(0, 0) можно пойти в (1,1)(1, 1) через цифру 11 (но дальше нужно ещё 11 для суммы 22, и достичь остатка 00 при основе 10kmod6110^k \bmod 61). 10kmod6110^k \bmod 61 периодично; первый раз 10k60(mod61)10^k \equiv 60 \pmod{61} при kk таком, что 10k+1010^k + 1 \equiv 0. Этот k=30k = 30, поэтому длина пути 30+1=3130 + 1 = 31, число 100029 нулей1\underbrace{1\,0\,0\ldots\,0}_{29 \text{ нулей}}\,1.

Ответ: 1002911\underbrace{0\ldots 0}_{29}1 (31 цифра) ✓

Пример 3: d=15d = 15, s=50s = 50

15=3515 = 3 \cdot 5. Делимость на 33 — сумма цифр должна делиться на 33. 50mod3=2050 \bmod 3 = 2 \ne 0. Значит, целевое состояние (0,50)(0, 50) недостижимо: для любого числа, делящегося на 33, сумма цифр кратна 33. BFS обходит все достижимые состояния и не находит (0,50)(0, 50).

Ответ: 1-1


Крайние случаи, на которых можно споткнуться

1. Делитель d=1d = 1

Делятся на 11 все числа. Тогда задача сводится к «найти минимальное число с суммой цифр ss». BFS аккуратно даёт минимум: цифр s/9\lceil s / 9 \rceil, конкретнее — s9(s/91)s - 9 \cdot (\lceil s / 9 \rceil - 1) старшая цифра, дальше всё девятки.

2. Сумма s=1s = 1

Минимальное число с суммой цифр 11 — это 10k10^k для некоторого kk. Конкретное kk — минимальное такое, что 10k0(modd)10^k \equiv 0 \pmod d. Для dd, у которого в разложении только 22 и 55 — такое kk существует. Иначе ответа нет (например, d=3d = 3 → ответ 1-1).

3. Маленькие dd при большом ss

d500d \le 500, ss до 50005000. Состояний — 2.51062.5 \cdot 10^6. Очередь BFS работает быстро; реконструкция — линейна по ответу (до 50005000 цифр). Никаких проблем со временем.

4. Состояние (0,s)(0, s) совпадает с (0,0)(0, 0)

Только при s=0s = 0, что не входит в ограничения. На всякий случай в коде явно отрабатываем это через проверку.

5. Длина ответа очень большая

При ss близком к 50005000 длина ответа может быть до 50005000 цифр. Печать через "\n" в конце; в Python — одна sys.stdout.write за весь вывод (не построчная печать в цикле); в C++ — стандартная << строки.


Типичные ошибки

  1. Разрешённая ведущая цифра 00. Если из (0,0)(0, 0) перебирать δ=0\delta = 0, в очередь попадёт (0,0)(0, 0) повторно (или сразу будет помечено как посещённое). Безобидно для BFS, но если случайно разрешить, ответ может начаться с нуля. Проверять явно в коде стартовое условие.
  2. Кэширование через dict в Python. На 2.51062.5 \cdot 10^6 состояний dict работает в 5–8 раз медленнее list[list[...]]. На пограничных тестах даёт TL. Двумерный массив фиксированного размера — обязательно.
  3. Реконструкция в неверном порядке. Идём от конца к началу, собираем цифры. Если забыть reverse() — выведем перевёрнутое число.
  4. Очередь BFS на list в Python. list.pop(0)O(n)O(n). На 2.51062.5 \cdot 10^6 операций это O(n2)O(n^2) — миллиарды операций. Только collections.deque.
  5. Хранение пути в состоянии BFS. Соблазн положить в очередь не (r,c)(r, c), а (r,c,строка цифр пока)(r, c, \text{строка цифр пока}). Это убивает память (O(n)O(n) строк длины до nn = O(n2)O(n^2) = 2510625 \cdot 10^6 символов в худшем случае, и каждая копия строки требует выделения). Только индексы — parent отдельно.
  6. Несимметричный учёт «первой цифры». Если start = 1 применяется только к одной ветке кода, но не ко второй — где-то проскочит ноль. Проверка (r == 0 && c == 0) должна стоять прямо в основном цикле.
  7. Переполнение в C++ при индексации. d(s+1)50050012.5106d \cdot (s + 1) \le 500 \cdot 5001 \approx 2.5 \cdot 10^6. int хватает, переполнения нет, но vector<int> такого размера — это 1010 МБ. В стеке нельзя — только в куче.

Анализ сложности

  • Время: O(ds)O(d \cdot s) — каждое состояние посещается один раз; из каждого — до 1010 переходов; всего 2.5107\le 2.5 \cdot 10^7 элементарных операций.
  • Память: O(ds)O(d \cdot s) под visited, parent. Очередь BFS в худшем случае — все состояния, тоже O(ds)O(d \cdot s).
  • Длина выходной строки: до ss цифр (когда все цифры — единицы) или до s/9\lceil s / 9 \rceil при «оптимальной» структуре. На печать уходит O(длина ответа)O(\text{длина ответа}) операций.

Что ещё полезно потренировать

  • Codeforces 1801A «The Very Beautiful Blanket» — конструктивная задача на свойство aba \oplus b, тренирует «строить ответ по позициям», как и наша «строить число по разрядам».
  • Codeforces 1495D «BFS Trees» — BFS-обход графа с восстановлением путей, та же техника «отдельные массивы под parent» в чистом виде.
  • Codeforces 1244C «The Football Season» — поиск решения линейного уравнения в малых числах, похожий шаблон «состояние компактно, перебор по дополнительной оси».
  • acmp.ru задача 191 «Гирлянда» — DP с двумерным состоянием на сетке, шаблон «состояние полностью описывает оставшуюся задачу».

Итого

  • Идея: BFS в графе состояний (r,c)(r, c) — остаток по модулю dd и накопленная сумма цифр; переход — добавление цифры; восстановление числа из массива parent.
  • Сложность: O(ds)O(d \cdot s) время и память.
  • Ловушки: запрет ведущего нуля, очередь — только deque, кэш — только массив, не dict; реконструкция — собрать цифры, потом reverse.

Попробуй разобрать похожие задачи

В CodePal AI-партнёр подсказывает идею, а не ответ. Разбор в диалоге, код проверяется в браузере.