Цикл с условием внутри — это первая алгоритмическая конструкция, которую участник изучает. И первая же, в которой можно споткнуться: перепутать границы, потерять одно значение на краю, или — самая злая ошибка — позволить промежуточному выражению переполниться.
Эта задача — лабораторный стенд для всего перечисленного. Условие совсем простое: дано кубическое уравнение, найти его целые корни. Гарантия, что они лежат в окне , превращает поиск в прямой перебор — мы просто пробуем каждое целое из окна и проверяем, обращается ли многочлен в ноль.
Звучит как «два цикла назад в учебнике». Но именно на ней удобно проговорить: какой цикл правильный, как корректно организовать условие, и почему int в C++ здесь — мина замедленного действия.
Что дано
Дано кубическое уравнение
где — целые коэффициенты, . Гарантируется, что все целые корни уравнения лежат в диапазоне . У уравнения могут быть и нецелые корни — их искать не нужно.
Требуется вывести все целые корни в порядке возрастания.
Ограничения
- Все целые корни уравнения находятся в .
Формат ввода
Четыре целых числа , , , в одной строке (или раздельно — стандартный split по пробелам).
Формат вывода
Целые корни уравнения в порядке возрастания, через пробел.
Пример 1
Вход:
1 -3 0 0
Выход:
0 3
Уравнение: . Корни: (двойной) и . Целые различные значения корней — и .
Пример 2
Вход:
3 -15 18 0
Выход:
0 2 3
Уравнение: . Корни: .
Пример 3
Вход:
1 -7 -33 135
Выход:
-5 3 9
Уравнение: . Корни: .
Разберём первый пример руками
. Многочлен — .
Пробежим от до и подставим. Для краткости — только окрестность нуля:
| корень? | ||
|---|---|---|
| нет | ||
| нет | ||
| да | ||
| нет | ||
| нет | ||
| да | ||
| нет |
Все остальные из окна — (кубический рост за пределами малых значений). Двойной корень в нуле даёт одно значение в выводе: . Дублей нет, потому что мы перебираем один раз.
Отсюда ровно та идея, к которой задача нас и подталкивает.
Идея решения
Алгоритм в одну фразу.
Прямой перебор: для каждого вычисляем и собираем те , где .
Почему это работает
Корни — целые, по условию лежат в окне . Окно содержит значение. Один проход — итераций при , на каждой — арифметика за . Итого — операций умножения и сложения. Уложится в любой лимит времени с гигантским запасом.
Перебор по убыванию или возрастанию выбирается так, чтобы итог уже был в требуемом порядке вывода — здесь это возрастание, значит идёт от до . Сортировать ничего не нужно: естественный порядок цикла совпадает с порядком вывода.
Дубли (двойные/тройные корни) автоматически отсекаются — мы перебираем каждое один раз. Если имеет корнем кратности 2, мы добавим в ответ ровно один раз.
Альтернативы и почему они здесь не нужны
- Решение по формуле Кардано — даёт все три комплексных корня, но из них надо ещё вытаскивать целые с проверкой точности. Лишняя возня.
- Поиск делителей — теорема о рациональных корнях говорит, что любой целый корень делит свободный член . Это даёт перебор по кандидатам (количество делителей ), что обычно сильно меньше 201. Но: код становится длиннее (ищем все делители , плюс пробуем , плюс знаки), а выигрыш по времени для практически нулевой. Прямой перебор окна проще и надёжнее.
В олимпиадах такого уровня простое решение, гарантированно укладывающееся в лимит, бьёт оптимизированное — меньше кода, меньше ошибок, меньше пограничных случаев.
Решение: псевдокод
прочитать A, B, C, D
roots ← пустой список
для x от -100 до 100 (включая обе границы):
значение ← A * x^3 + B * x^2 + C * x + D
если значение = 0:
добавить x в roots
вывести элементы roots через пробел
Сложность: по времени, где . По памяти — , где (кубическое уравнение имеет не более 3 различных корней; запас — на всякий случай).
Код решения
Переключи вкладку, чтобы посмотреть второй язык. Главная разница: в Python int бесконечной точности, переполнения нет. В C++ обязателен long long для всего — иначе промежуточное произведение A * x * x * x при , даёт , что выходит за int32 (); после переполнения значение может стать любым, иногда даже случайно нулём — получим ложный «корень».
Комментарии по реализации.
- Границы цикла. В Python
range(-100, 101)— правая граница исключающая, поэтому101, чтобы тоже попал. В C++for (int x = -100; x <= 100; ++x)—<=, чтобы не потерять . Типичная ловушка с обеих сторон. - Чтение в
long long(C++). Чтениеlong long A, B, C, Dсразу снимает проблему переполнения: все промежуточные произведения автоматически в 64-битном типе. Альтернатива — читать вintи писать(long long)A * x * x * x— работает, но один пропущенный кастинг и снова получим WA. - Возведение в степень. Python —
x ** 3, целочисленное возведение, читается как формула. C++ —x * x * xнапрямую (или , переиспользуя ). - Чтение ввода. Python —
sys.stdin.read().split()парсит весь ввод сразу; работает и когда через пробел, и когда каждое на своей строке. C++ —cin >> A >> B >> C >> Dсsync_with_stdio(false). - Мелкая оптимизация (C++).
xx = (long long)x * x— считаем один раз и переиспользуем для и . Для выигрыш эстетический, но привычка окупается на больших объёмах. - Вывод. Через пробел, в одну строку, без trailing-пробела. Финальный
'\n'для аккуратности.
Проверим на всех примерах из условия
Пример 1: → ожидание 0 3
Многочлен . Перебираем .
- → добавили
- → добавили
- — для многочлен растёт (), нулей не будет.
- — для всегда , , произведение , нулей не будет.
Собранные корни: . Ответ: 0 3 ✓.
Пример 2: → ожидание 0 2 3
Многочлен .
- → добавили
- → добавили
- → добавили
- — для многочлен растёт.
Собранные корни: . Ответ: 0 2 3 ✓.
Пример 3: → ожидание -5 3 9
Многочлен .
- → добавили
- → добавили
- → добавили
- Между функция действительно меняет знак на каждом корне (как и должно быть для кубики с тремя различными вещественными корнями).
Собранные корни: . Ответ: -5 3 9 ✓.
Все три примера дают эталонные ответы. Код корректен.
Крайние случаи, на которых можно споткнуться
1. Граница окна: и должны попасть в перебор
В Python: range(-100, 101) — правая граница исключающая. Если написать range(-100, 100), потеряем .
В C++: for (int x = -100; x <= 100; ++x) — <=, не <. Если написать <, потеряем .
Цена ошибки — потеря одного из крайних корней. Тесты обычно содержат пример с граничным корнем, поэтому ошибка ловится сразу — но только если тест включает или .
2. Переполнение int в C++
При и :
а максимум int32 — . Превышение в 15 раз. После переполнения значение может стать любым — например, случайно 0, и алгоритм добавит ложный корень.
Решение: читать в long long, тогда все промежуточные произведения автоматически в 64-битной арифметике. Запас — long long выдерживает до , что в раз больше нашего максимума.
В Python такой проблемы нет — int без границ.
3. Кратные корни
имеет тройной корень . Наш цикл проверяет один раз и добавляет в ответ один раз. Это совпадает с форматом вывода («целые корни в порядке возрастания» — каждый раз указанный одним числом).
Если бы задача требовала «корни с кратностями» — пришлось бы дополнительно искать кратность каждого найденного корня (например, делить многочлен на и снова искать корни). Здесь этого не нужно.
4. Нет целых корней в окне
Условие гарантирует, что если целые корни есть, они в . Но не гарантирует, что они вообще существуют. Например, : единственный вещественный корень — не целый, целых корней нет вообще.
В таком случае roots остаётся пустым, и мы выводим пустую строку (или сразу \n). Это допустимый ответ для acmp; формат «корней нет — пустой вывод» естественно вытекает из формата «корни через пробел» при пустом списке.
5.
Уравнение становится , где всегда корень. Наш цикл найдёт автоматически, плюс возможные корни квадратного множителя — без специальной обработки.
6. Линейное / квадратное вырождение?
Условие гарантирует , поэтому уравнение всегда строго кубическое. Случаи (квадратное) или (линейное) исключены — обрабатывать их не нужно.
Типичные ошибки
- Off-by-one на правой границе цикла.
range(-100, 100)илиfor (x = -100; x < 100; ++x)— теряет корень . На тестах с граничным корнем — мгновенный WA. Запоминать как мускульную память: «правая граница —+1вrange,<=в C++». intвместоlong longв C++. Переполнение даёт случайные значения и ложные корни. Самая частая ошибка на этой задаче — даже у опытных участников; ловится анализом «какой максимум промежуточного произведения?». На каждой задаче с произведениями нескольких чисел этот анализ — обязательный шаг.- Сортировка
roots.sort()после цикла. Не вредит, но избыточно: цикл уже идёт в возрастающем порядке. Лишняя строка кода — лишний шанс что-то сломать (например, случайно отсортировать по модулю). - Поиск нецелых корней. Если попытаться вместо целочисленного перебора применить, например, метод Ньютона — найдутся вещественные корни, и придётся их дополнительно фильтровать, проверяя «целое или нет» с заданной точностью. Здесь это лишняя работа: задача требует только целые, и они гарантированно в окне.
- Чтение через
scanf("%d")илиcin >> int_varс последующим хранением вint, а потом(long long)каст внутри произведения. Технически работает, если каст не забыт ни в одном месте. Практически — рано или поздно один каст забывается. Безопаснее объявить переменные сразу какlong long. - Печать
rootsчерез перевод строки вместо пробела. Формат вывода — через пробел в одной строке. На acmp проверка обычно строгая по формату.print(*roots)в Python даёт правильный формат через пробел;print('\n'.join(map(str, roots)))— нет. - Деление вместо равенства. Иногда участники проверяют корень условием
A * x**3 + B * x**2 + C * x + D == 0через деление многочлена на и проверку остатка. Для прямого перебора это лишнее — достаточно сравнить значение с нулём напрямую.
Анализ сложности
- Время: , где . На каждой итерации — арифметика. Итог — около 200 умножений и сложений, время ничтожное.
- Память: , где — количество найденных корней, . Можно вообще обойтись без массива и печатать корни по ходу цикла, но накопление в
rootsчище — отделяет вычисление от форматирования вывода.
Что ещё полезно потренировать
Задачи на простой перебор кандидатов с проверкой за . Все — с возраст-нейтральных платформ.
- Codeforces Div.4 / Div.3 уровень 800–1100, тег
brute force+implementation— типичные задачи «перебери все из небольшого окна, проверь условие». На начальном этапе — десятки одинаковых паттернов, отличающихся только формой условия. - acmp.ru, раздел «Простые задачи» — «Делители числа», «Простые числа в диапазоне», «Совершенные числа» — все построены на той же конструкции: цикл по окну + проверка свойства внутри.
- Яндекс Контест: тренировки по основам алгоритмов — открытые подборки начального уровня содержат целый кластер «перебор + проверка», полезный для отработки техники до автоматизма.
Следующий шаг — задачи, где прямой перебор уже не помещается в лимит, и на смену приходит более экономичный обход (бинпоиск по ответу, два указателя, префиксные суммы). Этим темам посвящены следующие посты в серии «Алгоритмические основы» — они закрывают переход «перебор → структура данных».
Итого
- Идея: прямой перебор , проверка за .
- Сложность: по времени, . По памяти — , .
- Главная ловушка: переполнение
intв C++ при . Решение —long longдля всех коэффициентов и промежуточных произведений. - Вторая ловушка: off-by-one на правой границе цикла.
range(-100, 101)в Python,x <= 100в C++. - Корни выводятся в естественном порядке возрастания, потому что цикл идёт от к — сортировка не нужна.