Решаем криптарифмы с помощью алгебры и python / Хабр
Если вы увлекались математикой в возрасте до 12 лет, то, наверное, встречались с криптарифмами — числовыми ребусами.
Числовым ребусом называется корректное арифметическое выражение (обычно — равенство), часть цифр в котором заменена на буквы и звездочки. Правила просты: одинаковые буквы заменяются на одинаковые цифры, разные — на разные.
Задача — восстановить исходные цифры, получив верное равенство.
Числовые ребусы хороши для тренировки у младшеклассников навыков логического мышления и счета в столбик. Однако и взрослым программистам может быть интересно поискать ответ на общий вопрос — а как, всё таки, алгоритмизировать процесс решения ребуса?
Формулировка задачи
На вход программы подается арифметический ребус. Он представляет из себя строку и состоит из букв, цифр, знаков арифметических действий , знака и круглых скобок.
Если ребус корректно преобразуется в арифметическое сравнение, программа должна вернуть список его решений. Например, на КОЗА*2 = СТАДО
программа вернет что-то вроде [8653*2 = 17306, 7693*2 = 15386]
, что решит нашу задачу.
Часть первая. Наивный алгоритм.
Искусство программирования учит нас: что бы решить задачу, представьте, что она уже решена. В данном случае, определим функцию, решающую ребусы:
def solve(rebus: str) -> list[str]: # Решаем ребус return solutions def test_rebus_solver(): solutions = solve("КОЗА+КОЗА = СТАДО") solutions.sort() assert solutions == ["7693+7693 = 15386", "8653+8653 = 17306"]
Предобработка ребуса
Прежде всего, стоит упростить задачу, преобразовав текущее выражение. А именно:
Уберем знак . Для этого обернем выражение справа и слева в скобки и вычтем одно из другого.
КОЗА*2 = СТАДО => (КОЗА*2)-(СТАДО)
Составим множество уникальных букв в выражении. Для упрощения восприятия, я буду записывать в своем псевдокоде список в виде последовательности строк, разделенных запятыми:
[К,О,З,A,С,Т,Д]
Разобьем выражение на токены.
(КОЗА*2)-(СТАДО) => [(,КОЗА,*,2,),-,(,СТАДО,)]
Уберем из списка токенов скобки. Для этого преобразуем выражение внутри него в обратную польскую запись (Reverse Polish Notation, PPN)
[(,КОЗА,*,2,),-,(,СТАДО,)] => [КОЗА,2,*,СТАДО,-]
Для переписывания выражения в RPN используем, например, алгоритм Shunting Yard.
Операции 1-4 можно назвать предобработкой исходного ребуса. Объединим их в функцию:
def rebus_preprocessing(rebus: str) -> list[str], set[str]: # 1) Убираем знак = # 2) Составляем список букв под замену # 3) Токенезируем выражение # 4) Переводим выражение в RPN return rpn_rebus, letters def test_rebus_preprocessing(): rpn_rebus, letters = rebus_preprocessing("КОЗА*2 = СТАДО") assert rpn_rebus == ["КОЗА","2",'*',"СТАДО",'-'] assert letters == {'К','О','З','A','С','Т','Д'}
Поиск решений
Первая — простая — идея состоит в том, что бы перебрать все возможные замены букв на цифры и проверить полученное после каждой замены арифметическое выражение на равенство нулю.
Напишем функцию под это:
def naive_rebus_solver(rpn_rebus: list[str], letters: set[str]) -> list[dict[str,str]] # Перебираем все возможные подстановки. Возвращаем список корректных подстановок return substitutions def test_naive_rebus_solver(): rpn_rebus, letters = rebus_preprocessing("КОЗА*2 = СТАДО") substitutions = naive_rebus_solver(rpn_rebus, letters) substitutions_pairs = sorted([ tuple(sorted([tuple(item) for item in s.items()])) for s in substitutions ]) assert substitutions_pairs == [ (('А', '3'), ('Д', '0'), ('З', '5'), ('К', '8'), ('О', '6'), ('С', '1'), ('Т', '7')), (('А', '3'), ('Д', '8'), ('З', '9'), ('К', '7'), ('О', '6'), ('С', '1'), ('Т', '5')) ]
Как же мы будем делать перебор?
Арифметические выражения, записанные с помощью RPN, замечательно удобно вычислять. Например, представим себе, что мы хотим вычислить 8653*2 — 17306 . В обратной польской записи это выражение перепишется как:
Затем наш вычислитель идет по списку токенов, и по очереди кладет их в стек, пока не обнаружит операцию. Обнаружив её, он извлекает из стека два последний числа, применяет к ним операцию и снова кладет в стек. Если изначальное арифметическое выражение корректно, когда мы дойдем до конца списка, в стеке будет лежать единственное число — результат вычислений.
Остается перебрать все подстановки. На этот случай python есть удобнейший itertools.permutations
:
from itertools import permutations def naive_rebus_solver(rpn_rebus, letters): substitutions = [] substitution = {l:'' for l in letters} for permutation in permutations('0123456789', len(letters)): for i, s in enumerate(letters): substitution[s] = permutation[i] # ...
Нужно отметить, что pythonic way здесь в том, что бы использовать таблицы подстановки. substitution_table = str.maketrans(substitution)
. Это немного ускоряет процесс подстановки букв в токены. Однако ускорение не столь существенно, и ради ясности подхода, было решено пренебречь этой возможностью языка.
Подставляем, вычисляем, сравниваем с нулем каждую подстановку. Готово!
Часть вторая. Не такой наивный алгоритм.
Поигравшись с наивным алгоритмом, представленным выше, обнаруживаем в нем один недостаток. Он медленный.
Легко видеть, что в худшем случае нам придется перебрать подстановок. Не так уж много — однако, тест ТРАВА+КОРОВА+ДОЯРКА = МОЛОКО
, содержащий как раз 10 букв, исполняется на моей машине 18 секунд.
Пути решения проблемы:
Всё это — хорошие пути решения, но можно поступить проще.
Задумаемся над тем, как эту задачу бы решал ребенок. Врят ли он стал бы механически перебирать миллионы вариантов подстановки. Покумекав слегка, умный математический школьник изобретет примерно следующее:
Рассмотрим последнюю букву в каждом из слагаемых.
Переберем несколько вариантов этой буквы, затем добавим в рассмотрение предпоследнюю.
Будем делать так, пока не решим весь ребус.
Когда матшкольнику случится подрасти и поступить в универ, он может встретить концепцию кольца — множества элементов, замкнутого относительно сложения, вычитания и умножения.
Самые простые для понимания кольца — кольца остатков от деления. Действительно, для любого a,b,p верно:(a%p + b%p)%p == (a+b)%p (a%p - b%p)%p == (a-b)%p (a%p * b%p)%p == (a*b)%p
К сожалению, вообще говоря, это неверно для деления. Система чисел, замкнутая относительно операции деления, помимо первых трех, зовется у математиков полем — и если мы рассматривали простое p, то могли бы утверждать, что у множества остатков от деления на p есть свойства поля. В данном случае, однако, мы будем последовательно рассматривать в роли p числа 10, 100, 1000… то есть, по сути, брать несколько последних цифр от каждого значения в нашем выражении.
Мы получаем цепочку ребусов такого вида:
А+А = О (mod 10) ЗА+ЗА = ДО (mod 100) ОЗА+ОЗА = АДО (mod 1000) КОЗА+КОЗА = ТАДО (mod 10000) КОЗА+КОЗА = СТАДО (mod 100000)
Мы должны последовательно решить каждый из них с учетом решений предыдущих. А именно, первый ребус дает варианты:
2+2 = 4, 3+3 = 6, 4+4 = 8, 5+5 = 0, 6+6 = 2, 7+7 = 4, 8+8 = 6, 9+9 = 8
Для каждого из этих вариантов мы пытаемся решить второй ребус, подставив в него предварительно буквы А
и О
в соответствии с вариантом. Допустим, {А:2, О:4}
. В таком случае, второй ребус имеет следующие варианты решения:
32+32 = 64, 52+52 = 04, 82+82 = 64, 92+92 = 84
Для каждого из эти вариантов мы решаем третий ребус…
В конце концов, мы получим все варианты, каждый из которых решает пятый ребус. Это множество вариантов гарантированно содержит все ответы на наш ребус, и может содержать некоторое количество ложных ответов. К примеру, ребус А+А=В
решенный таким образом имел бы в качестве кандидатов на решения сложения двух равных цифр, перечисленные выше, но верными были бы только 2+2 = 4, 3+3 = 6, 4+4 = 8
. Нам не составит никакого труда отфильтровать итоговый список вариантов, проверив каждый из них на оригинальном ребусе уже без всяких алгебраических колец.
Посчитаем выигрыш в скорости!
Тест
КОЗА+КОЗА = СТАДО
(7 букв) дает на моей машине ровно 3 секунды исполнения наивным алгоритмом, и 0.0037 секунд умным. Более точным будет указать, что методcalc
, производящий в моей программе, собственно, вычисление подставленного значения ребуса, при наивном методе вызывается 483840 раза. В умном — 773 раза.Тест
ТРАВА+КОРОВА+ДОЯРКА = МОЛОКО
исполняется наивным методом 18 секунд, вызываяcalc
2177280 раза. Умным — 0.038 секунд, а calc вызывается 2502 раза, включая частичные подсчеты.
Заключение
Мораль нашей истории проста. Приложив немного алгебры, можно значительно упростить себе жизнь.
Попробовать реализацию алгоритма в действии вы можете в онлайн-демо:
Полный код опубликован на github. Помимо всего вышеперечисленного там можно найти две интересные вещи, которые я решил не включать в статью: распараллеливание на python и рабочую реализацию функции на облаке Яндекса, исполняющей роль сервера для демки.
Настоящая статья написана по мотивам кружковых занятий Малого Мехмата МГУ для старших классов. Надеюсь, вам было интересно читать её не меньше, чем нам писать.
Спасибо за внимание!
Упростить выражение: определение и примеры
Основные понятия
- Объединить одинаковые члены с целыми коэффициентами
- Объединить одинаковые члены с рациональными коэффициентами
- Объединить одинаковые члены с двумя переменными
Чтобы упростить любые алгебраические выражения, следующие основные правила и шаги:
- Удалите все символы группировки, такие как квадратные и круглые скобки, путем умножения на множители.
- Используйте правило экспоненты, чтобы удалить группировку, если термины содержат экспоненты.
- Объедините одинаковые термины сложением или вычитанием.
- Объединить константы.
Пример:
Упростить 3x + 2(x – 4)
Решение:
3x + 2(x – 4)
В этом случае невозможно объединить термины, когда они еще в круглых скобках или любом знаке группировки.
Поэтому удалите круглые скобки, умножив любой фактор вне группы на все члены внутри нее.
Следовательно,
3x + 2(x – 4)
= 3x + 2x – 8
= 5x – 8
4.3.1 Объединить одинаковые члены с целыми коэффициентами 90 014Подобные термины:
В алгебре подобные термины — это термины, которые имеют одинаковые переменные и мощности. Коэффициенты не должны совпадать.
Коэффициент:Коэффициент – это число, умноженное на переменную.
Целочисленный коэффициент:В математике целочисленный многочлен (также известный как числовой многочлен) — это многочлен, значением которого является целое число для каждого целого числа n .
Каждый многочлен с целыми коэффициентами является целочисленным, но обратное неверно.
Пример 1:
Упростим выражение – 3c+5c—4-4c+6.
Решение:
Запишите выражение, сгруппировав одинаковые термины вместе.
Шаг 2: Объедините похожие термины.
(-3c-4c+5c) +(-4+6)
– 2c+2
Пример 2:
Упростите выражение 8n+12+(- 9) -(-6n).
Решение:
8n+12+(- 9) -(-6n)
Объедините числовые члены:
14n+12-9
14n+3 9 0017
Упрощенное выражение: 14n+3.
4.3.2 Объединить одинаковые члены с рациональными коэффициентамиПример 1:
Упростите выражение
2/5 м – 4/5 – 3/5 м
Решение:
2/5 м -4/5 – 3/5 м 9 0017
Пример 2: Упростите выражение 0002 4.3.3 Объедините одинаковые термины с двумя переменными Здесь b и c — две переменные. 6b и -2b — одинаковые термины. 4c и 7c — одинаковые термины. Объедините одинаковые термины =4b+11c. Пример 1: Упростите выражение 9x +3y-2x+5. Решение: 9x +3y-2x+5 +2y Пример 2: Упростите выражение 9y+3-4x-2y-3x-5 900 17 Решение: 9y+3 -4x-2y-3x-5 10. Какое выражение эквивалентно c+c+r+r+r? С отр. Примеры PowersComplex У нас есть три основных правила объединения показателей: Однако при упрощении выражений, содержащих экспоненты, не думайте, что вы должны работать только с этими правилами или непосредственно с ними. Часто проще работать непосредственно со значением показателей. Содержание продолжается ниже Упрощение выражений Правила говорят мне добавить показатели степени. Но когда я начал заниматься алгеброй, у меня были проблемы с соблюдением правил, поэтому я просто думал о том, что означают показатели степени. « a 6 » означает «шесть копий a , умноженных вместе», а « a 5 «означает «пять копий a , умноженных вместе». Если я умножу эти два выражения вместе, я получу одиннадцать копий a , умноженных вместе. То есть: a 90 076 6 × a 5 = ( a 6 )( a 5 ) = ( aaaaa а )( ааааа ) = ааааааааааа = а 11 Таким образом: a 6 × a 5 = a 11 902 53 Правила экспоненты говорят мне вычитать экспоненты. Упражнение:
-3+3v+(-4v)
–6b+9b–5b
2x+4x—5y+3
-5,8c+4,2-3,1+1,4c−5,8c+4,2−3,1+1,4c
7x + 6y + 6x
5t+7p-4p—2t
4.7+5g+4k+11.1-2g Концептуальная карта
Чему мы научились:
Простое упрощение выражений, содержащих показатели степени
Purplemath
MathHelp.com
Сколько у меня лишних шестерок и где они? У меня есть три лишних 6, и они сверху. Затем:
Если в инструкциях также не указано «вычислить», вы, вероятно, должны оставить такие задачи с числовым показателем степени в форме показателя степени. Однако, если вы не уверены, не стесняйтесь добавлять «= 216», просто на всякий случай.
- Упростите следующее выражение:
Сколько дополнительных копий т есть у меня и где они? У меня есть две дополнительные копии сверху:
Как только вы освоитесь с вопросом «сколько дополнительных у меня есть и где они?» рассуждая, вы обнаружите, что вам не нужно записывать вещи и отменять повторяющиеся факторы. Ответы начнут казаться вам довольно очевидными.
- Упростите следующее выражение:
Этот вопрос немного отличается, потому что больший показатель находится в члене в знаменателе. Но основная аргументация та же.
Сколько у меня есть дополнительных копий 5 и где они? У меня есть шесть дополнительных копий, и они внизу:
Примечание. Если вы примените правило вычитания, вы получите 5 3−9 = 5 −6 , что математически правильно, но почти наверняка не тот ответ, который они ищут.
Независимо от того, знаете ли вы об отрицательных показателях степени, когда они говорят «упростить», они имеют в виду «упростить выражение, чтобы оно не имело никаких отрицательных или нулевых степеней». Некоторые учащиеся попытаются обойти эту проблему со знаком минус, произвольно меняя знак, чтобы волшебным образом получить » 5 6 » сверху (а не под «1»), но это неверно.
Перейдем к более сложным выражениям.
- Упростим следующее выражение:
Я не должен забывать, что «5» и «3» — это просто цифры. Поскольку 3 не переходит в 5 поровну, я не могу отменить числа.
И я не должен пытаться вычитать числа, потому что 5 и 3 в дроби 5 / 3 совсем не то же самое, что 5 и 3 в рациональном выражении х 5 / х 3 . Числовая часть 5 / 3 остается прежней.
Для переменных у меня есть две дополнительные копии x сверху, поэтому ответ таков:
Любой из ответов, выделенных фиолетовым цветом, должен быть приемлемым: единственная разница заключается в форматировании; они означают одно и то же.
- Упростить (−46 x 2 y 3 z ) 0
Это достаточно просто: что угодно в нулевой степени равно 1.