Краткое содержание
В этом уроке нет кода, и не по недосмотру: алгоритм предшествует программе, и тот, кто не умеет точно описать, что нужно сделать, не сумеет сказать этого и на C++. Сначала мы учимся специфицировать задачу, то есть объявлять, какие данные она получает, какой результат должна выдать и какое отношение их связывает, и видим, что одна и та же формулировка допускает разные спецификации. Затем алгоритм определяется как конечная, определённая и массовая процедура, и в пронумерованных шагах записывается алгоритм деления последовательным вычитанием. Мы учимся выполнять алгоритм вручную с таблицей трассировки, которая на протяжении всего курса будет первым инструментом, чтобы понимать и чтобы отлаживать. Вводятся три способа соединять шаги (следование, ветвление и повторение), которыми записывается любой алгоритм, и мы учимся обосновывать, что повторение завершается, с помощью величины, которая уменьшается на каждой итерации и не может убывать бесконечно.
Цели обучения
- Специфицировать простую задачу, указывая её входные данные, выходные данные и условия, которым они должны удовлетворять.
- Описывать алгоритм пронумерованными шагами, без двусмысленности, с явными ветвлениями и повторениями.
- Выполнять алгоритм вручную с таблицей трассировки и находить его результат для данного входа.
- Различать в данной процедуре следование, ветвление и повторение и обосновывать, что повторение завершается.
Что такое вычислительная задача
Вычислительная задача есть отношение между двумя видами данных: теми, что получены, которые называются входом, и теми, что должны быть выданы, которые называются выходом. Решить её значит не получить ответ для одного случая, а располагать методом, который для каждого допустимого входа выдаёт выход, удовлетворяющий отношению. «Сколько будет 17, делённое на 5?» есть вопрос; «даны два целых числа, найти их частное и остаток» есть задача, потому что она говорит обо всех парах целых чисел сразу.
Однако обыденный язык неточен, и одна и та же формулировка может пониматься по-разному. Поэтому прежде чем решать, задачу специфицируют: записывают, что поступает на вход, что получается на выходе и каким условиям должно удовлетворять каждое из них. Спецификация состоит из трёх частей. Вход перечисляет данные с их видом (целые числа, десятичные дроби, слова) и условия, которые к ним предъявляются; эти условия называются предусловием: тот, кто пользуется решением, обязуется их соблюдать. Выход перечисляет результаты. Отношение, или постусловие, говорит, чему должен удовлетворять выход по отношению ко входу: тот, кто пишет решение, обязуется гарантировать его всякий раз, когда предусловие соблюдено. Отсюда следует, что спецификация является контрактом между тем, кто ставит задачу, и тем, кто её решает, и что вне предусловия решение ничего не обещает.
Обыденная формулировка: «найти наибольшее из трёх чисел». Спецификация:
- Вход: три числа , , (целые или десятичные, без других условий).
- Выход: число .
- Отношение: есть одно из трёх данных, и ни одно из них не больше ; то есть , и .
Отношение не говорит «наибольшее из трёх», ведь именно это и требовалось уточнить, а даёт два проверяемых условия. Первое требует, чтобы было одним из данных (без него число 1000 удовлетворяло бы второму почти для любого входа); второе требует, чтобы ни одно из данных его не превосходило. И спецификация попутно снимает сомнение, которое формулировка оставляла открытым: если два данных вместе достигают максимума, как в , выход равен , и никакой ошибки в этом нет.
Одна и та же формулировка допускает разные спецификации, и выбор между ними есть решение, а не недосмотр. Самый поучительный случай связан с целочисленным делением. Для целых и все согласны: частное и остаток суть единственные целые числа, удовлетворяющие условиям
Так, для и получаем и , потому что . Но формулировка «разделить два целых числа» не говорит, что делать, если отрицательно, и вопрос не праздный: для и есть два разумных ответа.
Первая спецификация: остаток никогда не бывает отрицательным, . Тогда , и выход таков: , . Именно её изучает математика, в уроке «Делимость и деление с остатком» курса «Алгебра и геометрия I».
Вторая спецификация: частное есть результат деления с десятичными знаками, у которого отброшена дробная часть, то есть округлённый к нулю; остаток есть то, чего недостаёт, и он имеет знак делимого. Тогда усекается до , и : выход таков: , .
Обе удовлетворяют равенству ; они различаются условием, наложенным на остаток. Ни одна из них не ошибочна, но программа, которая пользуется одной, полагая, что пользуется другой, будет давать неверные результаты для отрицательных чисел. Забежим вперёд: C++ принимает вторую, как будет видно в уроке Арифметические операции; кто ждёт первую, будет неприятно удивлён.
Есть и третий вариант, самый частый на практике: исключить этот случай предусловием. Если спецификация требует , проблема отрицательных чисел исчезает, не потому что она решена, а потому что тот, кто пользуется решением, обязался её не ставить. Чего нельзя делать, так это оставлять вопрос нерешённым. И по той же причине спецификация требует : при не существует и , удовлетворяющих отношению, и контракт должен это оговаривать.
Частая ошибка состоит в том, чтобы специфицировать примером: «выход для 17 и 5 равен 3 и 2». Пример иллюстрирует, но не специфицирует, потому что не говорит, что происходит с остальными входами; бесконечно много разных отношений совпадают в одном случае. Спецификация записывается как общее условие, а примеры служат затем для того, чтобы её проверить.
Алгоритмы
Когда задача специфицирована, ищут метод её решения. Алгоритм есть последовательность инструкций, которая, будучи применена к любому входу, удовлетворяющему предусловию, за конечное число шагов выдаёт выход, удовлетворяющий постусловию. Определение содержит три требования.
- Конечность. Алгоритм записывается конечным числом инструкций и для каждого допустимого входа завершается, выполнив конечное число из них. Процедура, которая для какого-то входа продолжается бесконечно, не является алгоритмом для этого входа.
- Определённость. Каждая инструкция имеет единственный смысл, и тот, кто её выполняет, в любой момент знает, что делать и какая инструкция следующая. Ни одно решение не оставлено на усмотрение исполнителя.
- Массовость. Алгоритм решает задачу для всех допустимых входов, а не только для одного случая.
С некоторой вольностью алгоритмом называют и процедуру, которая определённа и завершается, но для какого-то допустимого входа не удовлетворяет постусловию: строго говоря, это алгоритм для другой задачи, и мы будем говорить, что он неправилен для специфицированной задачи. Так мы будем говорить в задачах этого урока, которые опровергают неправильные алгоритмы контрпримером.
Кулинарный рецепт похож на алгоритм, и поэтому сравнение с ним привычно; но почти ни один рецепт алгоритмом не является. «Добавить щепотку соли» неточно, потому что щепотка не есть количество; «запекать до золотистого цвета» зависит от суждения, которое два человека вынесли бы по-разному; «взбить несколько раз» не говорит, сколько именно. Повар восполняет эти пробелы своим опытом; у компьютера опыта нет. Отсюда следует требование, которое управляет всем курсом: алгоритм должен уметь выполнить тот, кто не понимает задачи, ничего не угадывая. Если для выполнения шага нужно знать, для чего он служит, шаг записан плохо.
Алгоритмы этого курса записываются сначала пронумерованными шагами, по-русски, с некоторыми соглашениями. Данные, которые меняются во время работы алгоритма, обозначаются буквами или словами, как или , и называются переменными. Пишут « принимает значение », чтобы сказать, что вычисляется при текущем значении и результат становится новым значением ; прежнее значение теряется. В сокращённой записи, которая используется в заметках, та же инструкция записывается стрелкой влево, которая на клавиатуре набирается как r <- r - b. Инструкции выполняются в порядке их номеров, если только одна из них не предписывает иное.
Задача та, что специфицирована в предыдущем параграфе: вход, два целых числа и ; выход, два целых числа и , для которых и . Идея в том, чтобы раздавать: из отнимают группы по , пока это возможно, считают отнятые группы, и то, что осталось, есть остаток.
- принимает значение , и принимает значение .
- Пока выполняется , повторять шаги 3 и 4; когда это перестанет выполняться, перейти к шагу 5.
- принимает значение .
- принимает значение ; вернуться к шагу 2.
- Выдать и и завершить работу.
Шаг 2 есть вопрос, который задаётся каждый раз: перед первой итерацией и после каждой. Шаги 3 и 4 образуют тело повторения, то, что повторяется. Ничто не оставлено на усмотрение исполнителя: в каждый момент есть ровно одна следующая инструкция.
Алгоритм медленный (чтобы разделить миллион на три, он делает более трёхсот тысяч итераций), и школьный способ деления намного быстрее; но он правилен, и его простота делает его идеальным, чтобы учиться рассуждать об алгоритмах. Курс вернётся к различию между правильным и эффективным алгоритмом в уроке Сложность алгоритма.
Наконец, стоит отделить алгоритм от его записи. Алгоритм деления вычитанием один и тот же, записан ли он по-русски, в виде схемы, на C++ или на любом другом языке программирования: его определяет последовательность операций, а не слова. Программа есть запись алгоритма на языке, который может исполнить машина, и урок Программы, компиляторы и C++ объяснит, как происходит это исполнение. Если это так, программа может быть неверной двумя совершенно разными способами: потому что алгоритм неправилен, и тогда никакой язык его не спасёт, или потому что алгоритм правилен, но записан неверно. Различать эти два случая значит сделать половину работы по отладке, и поэтому курс всегда сначала записывает алгоритм.
Выполнение алгоритма вручную
Прочитать алгоритм недостаточно, чтобы знать, что он делает; его нужно выполнить. Выполнять его вручную, как это делала бы машина, не пропуская шагов и не угадывая результатов, есть самое полезное умение программиста: с его помощью алгоритм проверяют до того, как его записать, предсказывают выход программы до её запуска и находят ошибку в программе, которая работает неверно.
Инструмент для этого есть таблица трассировки: столбец для номера выполняемого шага, по столбцу на каждую переменную и, если удобно, столбец для проверяемых условий; и по строке на каждый выполненный шаг. В каждой строке записывают новое значение переменной, изменившейся на этом шаге, а то, что не изменилось, оставляют пустым, так что текущее значение переменной есть последнее записанное в её столбце. Условия записывают вместе с их результатом.
Выполним деление последовательным вычитанием при и . Стрелка на рисунке указывает шаг, который только что выполнен; пунктирная линия напоминает, что шаг 4 возвращает к шагу 2.
Доказательство
Шаг 1: задаются начальные значения. Вход удовлетворяет предусловию, потому что и .
Первая итерация. На шаге 2 проверяется, что , поэтому выполняется тело: шаг 3 отнимает группу из пяти, а шаг 4 её считает.
Вторая итерация: , и тело выполняется снова.
Третья итерация: , и тело выполняется снова.
На шаге 2 условие не выполняется, потому что : алгоритм переходит к шагу 5 и выдаёт и . Выход удовлетворяет постусловию.
Полная таблица имеет двенадцать строк: шаг 1, три итерации по три строки (шаги 2, 3 и 4), вычисление условия, давшее ложь, и выдача результата.
Два замечания о таблице. Первое: число итераций равно частному, , а шаг 2 выполнился на один раз больше, чем тело, раза; так бывает всегда в повторении такого вида, потому что именно при последнем вычислении условие не выполняется. Второе: трассировка показывает то, чего текст алгоритма не говорит с первого взгляда; если , например и , условие не выполняется с первого же раза, тело не выполняется ни разу, и выход таков: , , что правильно, потому что . Забыть случай, когда повторение не выполняется, есть одна из самых частых ошибок при проектировании алгоритмов, и трассировка на маленьких входах её обнаруживает.
Трассировка одного случая не доказывает, что алгоритм правилен: она проверяет один случай. Но хорошо выбранная трассировка (обычный случай, граничный случай, случай, когда повторение не выполняется) обнаруживает большую часть ошибок, а та, что не проходит, показывает, где они. Урок Проектирование правильных циклов научит переходить от случаев к доказательству.
Следование, ветвление и повторение
Если присмотреться, алгоритмы строятся из очень немногих деталей. Есть элементарные инструкции (присвоить значение переменной, выдать результат) и три способа их соединять.
- Следование: одна инструкция за другой, по порядку. Шаги 3 и 4 деления образуют следование: сначала вычитают, потом считают.
- Ветвление: в зависимости от того, выполняется ли условие, выполняется одна часть или другая (или одна часть или ничего). «Если , то принимает значение » есть ветвление.
- Повторение: пока выполняется условие, некоторая часть выполняется снова и снова. Шаг 2 деления вместе со своим телом есть повторение.
Каждая форма имеет один вход и один выход, и её внутренность в свою очередь может быть любой из трёх: тело повторения может содержать ветвления, ветвь ветвления может содержать повторение, и так далее. Если это так, тремя формами строятся алгоритмы любой сложности, подобно тому как из немногих деталей, подходящих друг к другу, строится любое здание. То, что их достаточно для выражения любого алгоритма, есть известный результат теории программирования (его доказали Коррадо Бём и Джузеппе Якопини в 1966 году); здесь он не доказывается, но весь курс его подтверждает: программы, которые будут в нём написаны, сколь бы длинными они ни были, не будут использовать ничего, кроме этих трёх форм, записанных на C++ как if для ветвления и while или for для повторения, в уроках Ветвления и Циклы.
Удобная нотация, чтобы видеть форму алгоритма, есть блок-схема: каждая инструкция изображается прямоугольником, каждое условие ромбом с двумя выходами (выполнено, что на рисунке помечено , и не выполнено, ), а стрелки указывают, какой шаг следующий. Это нотация, а не цель: она помогает охватить взглядом небольшой алгоритм, но не масштабируется, и никто не пишет большую программу, рисуя её.
Рисунок изображает три формы с обобщёнными инструкциями и и условием , а затем соединяет их в блок-схему деления последовательным вычитанием.
Доказательство
Следование: выполняется , а затем ; есть один вход и один выход.
Ветвление: если выполнено, выполняется ; иначе . Обе ветви сходятся в один выход.
Повторение: вычисляется ; если оно выполнено, выполняется и снова вычисляется ; иначе происходит выход. Условие вычисляется на один раз больше, чем тело.
Композиция: деление последовательным вычитанием есть следование (начальные значения, повторение, выдача результата), у которого повторение имеет телом другое следование (вычесть, сосчитать).
Различить три формы в процедуре, записанной обыденным языком, значит сделать первый шаг к её переводу. Признак ветвления есть «если», «в случае, если» или «в зависимости от»; признак повторения, «пока», «до тех пор, пока не», «для каждого» или «столько раз, сколько». Процедура без единого такого слова есть следование, и если она должна обрабатывать входы переменного размера (список чисел, текст), в ней не хватает повторения.
Завершается ли?
Конечность входит в определение алгоритма, и повторение есть единственное место, где её можно потерять: следование из пяти шагов выполняет пять шагов, ветвление выполняет одну из своих ветвей; но повторение выполняет своё тело столько раз, сколько выполняется условие, и ничто заранее не гарантирует, что оно перестанет выполняться.
Чтобы обосновать, что повторение завершается, ищут величину, вычисляемую по переменным, которая удовлетворяет двум условиям: она есть натуральное число (целое и неотрицательное), пока повторение продолжается, и строго убывает на каждой итерации. Если это так, повторение завершается: строго убывающая последовательность натуральных чисел не может быть бесконечной, потому что каждая итерация уменьшает её хотя бы на единицу, а от натурального до нуля не больше ступенек. Такую величину называют вариантом цикла или ограничивающей функцией, и урок Проектирование правильных циклов будет использовать её систематически.
В делении последовательным вычитанием вариантом цикла служит . Пока повторение продолжается, , так что есть натуральное число. На каждой итерации принимает значение , и поскольку (предусловие), новое значение строго меньше. Следовательно, повторение завершается, и происходит это не более чем через итераций (на самом деле ровно через итераций, по частному).
Обратите внимание, где использовано предусловие . Если выполнить алгоритм при , шаг 3 оставляет неизменным, условие выполняется всегда, и повторение не завершается никогда: алгоритм не ошибочен, просто ему дали вход, который контракт исключал. А если бы было отрицательным, возрастало бы на каждой итерации. Предусловие не есть украшение: это ровно то, что нужно, чтобы рассуждение работало.
Оба условия варианта необходимы, и ошибиться легко в любом из них. Рассмотрим процедуру, которая должна решать, чётно ли натуральное : «пока , принимает значение ; если дошли до нуля, было чётным». Величина убывает на каждой итерации, и всё же процедура не завершается, если нечётно: при она проходит через , , , , и условие никогда не перестаёт выполняться. Не выполняется другое условие: перестаёт быть натуральным, потому что условие повторения не мешает ему опуститься ниже нуля. Исправление состоит в том, чтобы повторять, пока , и в конце решать по оставшемуся значению, или ; теперь натурально на каждой итерации, потому что вычитание происходит только при , и убывает: процедура завершается.
Решить, завершается ли повторение, не всегда легко. Знаменитый пример: начиная с натурального , пока , если чётно, его делят на два, а если нечётно, заменяют на . При последовательность такова: , и она завершается. Никто не нашёл , для которого она не завершалась бы, и это проверено на компьютерах для огромного количества значений; но никому не удалось доказать, что она завершается всегда: это гипотеза Коллатца, открытая проблема математики. Ни одна известная величина не убывает на каждой итерации. Мораль для программиста скромна и тверда: алгоритм, завершение которого не удаётся обосновать, не закончен.
Частые ошибки
- Двусмысленные шаги. «Повторить несколько раз», «пока не станет хорошо», «взять подходящее число». Каждый шаг должен быть выполним без понимания задачи; иначе он записан плохо.
- Забытый случай, когда повторение не выполняется. Любое повторение с условием в начале может выполнить своё тело ноль раз. Всегда нужно спрашивать себя, что выдаёт алгоритм в этом случае, и трассировка на минимальном входе на это отвечает.
- Спецификация примером. «Для 17 и 5 даёт 3 и 2» не есть спецификация; отношение записывается как общее условие, а пример его иллюстрирует.
- Смешение алгоритма с его программой. В программе, которая работает неверно, может быть ошибочен алгоритм или ошибочна его запись. Кто не записал алгоритм заранее, не знает, что из двух искать.
- Завершение, принятое на веру. Недостаточно, чтобы величина убывала: она должна, кроме того, оставаться натуральной, пока продолжается повторение.
Решённые задачи
Написать алгоритм, удовлетворяющий спецификации максимума трёх чисел из первого параграфа, и выполнить его вручную для входов и .
Решение
Идея: просматривать вход, храня наибольшее из уже просмотренных в переменной . Первый шаг: принимает значение . После него есть максимум просмотренных данных, а это только .
Второй шаг: если , то принимает значение . Третий шаг: если , то принимает значение . Четвёртый шаг: выдать . Это три инструкции в следовании, две из них ветвления без альтернативной ветви.
Трассировка для : после первого шага ; на втором ложно, и не меняется; на третьем ложно, и не меняется. Выдаётся .
Трассировка для : после первого шага ; на втором , и становится равным ; на третьем ложно, и не меняется. Выдаётся , и это правильно: равенство не требует особой обработки.
Обоснование: после каждого шага есть одно из просмотренных данных, и ни одно из них его не превосходит. Это верно после первого шага, и каждое ветвление это сохраняет: если новое данное больше, оно становится ; если нет, оно не превосходит . В конце просмотрены все три, и удовлетворяет постусловию.
Если бы было написано вместо , результат был бы тем же, потому что при равенстве присваивается равное значение. Выбор между и был бы важен, если бы требовалась ещё и позиция максимума: с выдаётся первая из равных позиций, а с последняя, которую формула обозначает .
В григорианском календаре год високосный, если он делится на , за исключением делящихся на , которые високосны, только если делятся ещё и на . Студент предлагает алгоритм «если год делится на , выдать да; иначе выдать нет». Найти год, на котором он ошибается, и написать правильный алгоритм с его спецификацией.
Решение
Спецификация. Вход: целое число (григорианский календарь действует с октября 1582 года). Выход: да или нет. Отношение: выход равен да ровно тогда, когда делится на и не делится на или делится на .
Предложенный алгоритм прав для большинства лет, и поэтому кажется правильным. Чтобы его опровергнуть, достаточно года, делящегося на и не делящегося на : делится на , и алгоритм говорит да; но не был високосным.
Исправленный алгоритм из трёх ветвлений, следующих цепочкой. Шаг 1: если делится на , выдать да и завершить работу. Шаг 2: если делится на , выдать нет и завершить работу. Шаг 3: если делится на , выдать да; иначе выдать нет.
Порядок вопросов существен: они идут от самого частного правила к самому общему. Если сначала спросить о числе , год получит да, не дойдя до исключения. Трассировка для четырёх лет, по одному на каждый случай: (шаг 1: да), (шаг 2: нет), (шаг 3: да), (шаг 3: нет).
Урок здесь не только о календарях: алгоритм опровергается одним-единственным контрпримером, и контрпримеры ищут на границах правил, а не в обычных случаях. Испытания на , и ничего бы не обнаружили.
Выполнить вручную деление последовательным вычитанием при и , а также при и . Затем обосновать, что для любого допустимого входа алгоритм завершается ровно через итераций и что его выход удовлетворяет постусловию.
Решение
Трассировка при , : проходит значения , , , , , а значения , , , , . При пятом вычислении условие не выполняется. Выход: , . Здесь граничный случай есть деление нацело: четвёртая итерация выполняется, потому что , и поэтому условие должно быть , а не .
Трассировка при , : при первом же вычислении условие не выполняется; тело не выполняется ни разу. Выход: , , и это правильно.
Отношение, которое сохраняется. Перед первой итерацией . На каждой итерации уменьшается на , а увеличивается на , так что величина не меняется. Следовательно, равенство верно после каждой итерации.
Завершение. Пока продолжается повторение, , и есть натуральное число; на каждой итерации оно уменьшается на . Строго убывающая последовательность натуральных чисел конечна: повторение завершается.
По завершении условие не выполнилось: . Кроме того, , потому что вычитается только при . Вместе с сохраняющимся отношением это даёт, что выход удовлетворяет постусловию; а поскольку считает итерации, число итераций равно частному.
Упражнения онлайн
Выполнить вручную следующий алгоритм и записать в поле значение, которое он выдаёт, только число.
- принимает значение , и принимает значение .
- Пока выполняется , повторять шаги 3 и 4; когда это перестанет выполняться, перейти к шагу 5.
- Если чётно, принимает значение ; если нечётно, принимает значение .
- принимает значение ; вернуться к шагу 2.
- Выдать .
Браузер сразу проверяет твой ответ, не оценивая его.
Подсказка
Составь таблицу: на первой итерации переходит от к . Считай одну итерацию на каждое изменение , пока не станет равным .
Показать решение
7Следующий алгоритм, записанный в сокращённой форме, должен вычислять частное и остаток делением последовательным вычитанием, но в нём не хватает условия повторения. Заменить строку из знаков подчёркивания правильным условием, ничего больше не меняя.
Браузер сразу проверяет твой ответ, не оценивая его.
Подсказка
Выполни алгоритм вручную при и : вторая итерация должна произойти, хотя остаток равен ровно .
Показать решение
Вход: a (целое, a >= 0), b (целое, b > 0)1. q <- 02. r <- a3. пока r >= b повторять: r <- r - b q <- q + 14. выдать q, rЗадачи для самостоятельного решения
К каждой задаче есть подсказка, которую лучше открывать только после собственной попытки, и ответ, который появляется после того, как открыта подсказка.
Написать спецификацию задачи «упорядочить три числа» (вход, выход и отношение) и указать, какие решения она заставляет принять, которых формулировка не принимала. Затем проверить, что спецификация отвергает для входа два таких выхода: и , и допускает .
Подсказка
«Упорядоченный» выход может не иметь ничего общего со входом. Какое условие связывает числа выхода с числами входа, включая повторяющиеся? По возрастанию или по убыванию? Строго или нет?
Ответ
Вход: три числа , , . Выход: три числа , , . Отношение: , и есть перестановка , то есть содержит те же значения с теми же повторениями. Решения, которых формулировка не принимала: направление (здесь от меньшего к большему); то, что порядок нестрогий (, а не , потому что при повторяющихся значениях строгий порядок невозможен); и то, что выход должен быть перестановкой. Выход упорядочен, но содержит число , которого не было; в четыре значения; удовлетворяет обоим условиям. Без условия перестановки было бы правильным выходом для любого входа.
Написать алгоритм пронумерованными шагами, который получает натуральное и выдаёт его цифры от последней к первой (для выдаёт , , , ), используя только частное и остаток от деления на . Выполнить его вручную для и для и обосновать, что он завершается для любого .
Подсказка
Остаток от деления на есть последняя цифра, а частное есть число без неё. Реши, задаётся ли условие до или после выдачи цифры, и посмотри, что происходит при , у которого одна цифра.
Ответ
Правильный алгоритм: (1) выдать остаток от деления на ; (2) принимает значение частного от деления на ; (3) если , вернуться к шагу 1; иначе завершить работу. Вопрос стоит после тела, так что тело выполняется хотя бы один раз: при выдаётся , его единственная цифра. Если бы вопрос задавался в начале («пока »), при ничего не было бы выдано; это ошибка забытого случая, когда повторение не выполняется. Трассировка для : выдаются , , , , а проходит значения , , , . Завершение: вариант цикла есть , натуральное число; если происходит возврат к шагу 1, значит, , а при частное от деления на строго меньше , так что строго убывает на каждой итерации.
Имеются монеты в , и единиц, в неограниченном количестве. Спецификация: вход, натуральное ; выход, три натуральных числа , , , для которых и наименьшее возможное количество монет, . Некто предлагает следующий алгоритм. Найти вход, на котором он ошибается, объяснить каждый дефект и исправить его.
- , и принимают значение .
- Если , то принимает значение , и принимает значение .
- Пока : принимает значение , и принимает значение .
- Пока : принимает значение , и принимает значение .
- Выдать , и .
Подсказка
Сумма монет всегда равна ; ошибиться может их количество. Попробуй точное кратное и сумму больше и спроси себя, какой способ соединения шагов использует шаг 2 и какой он должен был бы использовать.
Ответ
Дефектов два. Во-первых, шаг 3 использует : при он не выдаёт ни одной монеты в и выдаёт десять монет в вместо одной. Во-вторых, шаг 2 есть ветвление там, где нужно повторение: при алгоритм в том виде, как он записан, выдаёт одну монету в , шесть в и десять в (семнадцать монет), и даже с уже исправленным шагом 3 он выдал бы одну в и семь в (восемь) вместо двух в и двух в (четырёх). Вход показывает оба дефекта: алгоритм выдаёт одну монету в , четыре в и десять в , пятнадцать монет, вместо двух. Исправление: шаг 2, «пока , …»; шаг 3, «пока , …». После такого исправления алгоритм всегда берёт самую крупную монету, которая помещается, и с этими тремя монетами это даёт наименьшее количество: цифру единиц числа можно оплатить только монетами в , а то, что остаётся, выгодно оплачивать монетами в насколько возможно, потому что пять монет в стоят столько же, сколько одна в . С другими монетами, например в , и при , правило всегда брать самую крупную уже не даёт минимума ( против ).