Чтобы попытаться проверить, является ли алгоритм для какой-либо проблемы правильным, обычная отправная точка состоит в том, чтобы попытаться запустить алгоритм вручную на нескольких простых тестовых примерах - попробуйте на нескольких примерах проблемных примеров, включая несколько простых «угловых случаев ». Это отличная эвристика: это отличный способ быстро отсеять множество неверных попыток алгоритма и понять, почему алгоритм не работает.
Тем не менее, при изучении алгоритмов у некоторых студентов возникает искушение остановиться на этом: если их алгоритм работает правильно на нескольких примерах, в том числе во всех угловых случаях, которые они могут попробовать, то они приходят к выводу, что алгоритм должен быть правильным. Всегда есть студент, который спрашивает: «Почему мне нужно доказать, что мой алгоритм правильный, если я могу просто попробовать его на нескольких тестовых примерах?»
Так как же обмануть эвристику "попробуй кучу тестов"? Я ищу несколько хороших примеров, чтобы показать, что этой эвристики недостаточно. Другими словами, я ищу один или несколько примеров алгоритма, который выглядит поверхностно, как будто он может быть правильным, и который выводит правильный ответ на все мелкие входные данные, которые могут возникнуть у любого, но где алгоритм на самом деле не работает Может быть, алгоритм просто работает правильно на всех маленьких входах и терпит неудачу только для больших входов, или терпит неудачу только для входов с необычным шаблоном.
В частности, я ищу:
Алгоритм Недостаток должен быть на алгоритмическом уровне. Я не ищу ошибки реализации. (Например, как минимум, пример должен быть независимым от языка, а недостаток должен касаться алгоритмических проблем, а не разработки программного обеспечения или реализации.)
Алгоритм, который кто-то может правдоподобно придумать. Псевдокод должен выглядеть как минимум правдоподобно правильным (например, код, который запутан или явно сомнителен, не является хорошим примером). Бонусные баллы, если это алгоритм, который на самом деле придумал какой-то студент, пытаясь решить домашнюю работу или экзаменационную задачу.
Алгоритм, который прошел бы разумную ручную стратегию тестирования с высокой вероятностью. Тот, кто пробует несколько небольших тестовых примеров вручную, вряд ли обнаружит недостаток. Например, «симуляция QuickCheck вручную на дюжине небольших тестовых случаев» вряд ли покажет, что алгоритм неверен.
Желательно, детерминированный алгоритм. Я видел, что многие студенты думают, что «пробовать некоторые тестовые примеры вручную» - это разумный способ проверить правильность детерминированного алгоритма, но я подозреваю, что большинство студентов не будут считать, что использование нескольких тестовых примеров является хорошим способом проверки вероятностных алгоритмы. Для вероятностных алгоритмов часто нет способа определить, верен ли какой-либо конкретный результат; и вы не можете провернуть достаточное количество примеров, чтобы провести какой-либо полезный статистический тест на выходное распределение. Поэтому я бы предпочел сосредоточиться на детерминистических алгоритмах, поскольку они более понятны сердцу заблуждений учащихся.
Я хотел бы рассказать о важности доказательства правильности вашего алгоритма, и я надеюсь использовать несколько подобных примеров, чтобы помочь мотивировать доказательства правильности. Я предпочел бы примеры, которые являются относительно простыми и доступными для студентов; примеры, которые требуют тяжелой техники или тонны математического / алгоритмического фона, менее полезны. Кроме того, я не хочу, чтобы алгоритмы были «неестественными»; в то время как может быть легко создать какой-то странный искусственный алгоритм, чтобы обмануть эвристику, если он выглядит крайне неестественным или имеет очевидный черный ход, созданный просто для того, чтобы обмануть эту эвристику, он, вероятно, не будет убедительным для студентов. Есть хорошие примеры?
Ответы:
Я думаю, что распространенной ошибкой является использование жадных алгоритмов, что не всегда является правильным подходом, но может работать в большинстве тестовых случаев.
Пример: номиналы монет, и число n , выражают n как сумму d i : s с как можно меньшим количеством монет.d1,…,dk n n di
Наивный подход состоит в том, чтобы сначала использовать как можно большую монету и жадно получить такую сумму.
Например, монеты со значениями , 5 и 1 будут давать правильные ответы с жадностью для всех чисел от 1 до 14, кроме числа 10 = 6 + 1 + 1 + 1 + 1 = 5 + 5 .6 5 1 1 14 10=6+1+1+1+1=5+5
источник
Я сразу вспомнил пример Р. Бэкхауса (это могло быть в одной из его книг). По-видимому, он назначил задание по программированию, где студенты должны были написать программу на Паскале, чтобы проверить равенство двух строк. Одна из программ, предложенных студентом, была следующей:
Теперь мы можем протестировать программу со следующими входами:
"университет" "университет" Верно; Хорошо⇒
«курс» «курс» Верно; Хорошо⇒
"" "" Верно; Хорошо⇒
"университет" "курс" Неверно; Хорошо⇒
«лекция» «курс» Ложь; Хорошо⇒
«точность» «точность» Неверно, ОК⇒
Все это кажется очень многообещающим: возможно, программа действительно работает. Но более тщательное тестирование, скажем, «чистый» и «истинный» выявляет ошибочный вывод. Фактически, программа говорит «True», если строки имеют одинаковую длину и одинаковый последний символ!
Тем не менее, тестирование было довольно тщательным: у нас были строки разной длины, строки одинаковой длины, но разного содержимого и даже одинаковые строки. Кроме того, студент даже проверил и выполнил каждое отделение. Вы не можете утверждать, что тестирование здесь было небрежным - учитывая, что программа действительно очень проста, может быть трудно найти мотивацию и энергию для ее тщательного тестирования.
Еще один милый пример - бинарный поиск. В TAOCP Кнут говорит, что «хотя основная идея бинарного поиска сравнительно проста, детали могут быть на удивление хитрыми». По-видимому, ошибка в реализации Java для бинарного поиска оставалась незамеченной в течение десятилетия. Это была ошибка целочисленного переполнения, которая проявлялась только при достаточно большом входном сигнале. Сложные подробности реализации бинарного поиска также освещены Бентли в книге « Программирование жемчужин» .
Итог: может быть на удивление трудно убедиться, что алгоритм двоичного поиска верен, просто протестировав его.
источник
Лучший пример, с которым я когда-либо сталкивался, - это тестирование простоты:
Это работает (почти) для каждого числа, за исключением очень небольшого числа контрпримеров, и на самом деле нужен компьютер, чтобы найти контрпример в реалистичный период времени. Первый контрпример составляет 341, и плотность контрпримеров фактически уменьшается с увеличением p, хотя это примерно логарифмически.
Вместо того, чтобы просто использовать 2 в качестве основы степени, можно улучшить алгоритм, также используя дополнительные, увеличивая маленькие простые числа в качестве основы в случае, если предыдущее простое число вернуло 1. И все же, есть контрпример к этой схеме, а именно числа Кармайкла, довольно редко, хотя
источник
Вот тот, который был брошен на меня представителями Google на съезде, на котором я был. Он был написан на C, но работает на других языках, которые используют ссылки. Извините за то, что написал код на [cs.se], но это только для иллюстрации.
Этот алгоритм будет работать для любых значений, заданных для x и y, даже если они имеют одинаковое значение. Однако он не будет работать, если он называется swap (x, x). В этой ситуации x заканчивается как 0. Теперь это может вас не устраивать, поскольку вы можете каким-то образом доказать, что эта операция математически корректна, но все же забыть об этом крайнем случае.
источник
Существует целый класс алгоритмов, которые по своей природе сложно протестировать: генераторы псевдослучайных чисел . Вы не можете проверить один выход, но должны исследовать (много) ряды выходов с помощью статистики. В зависимости от того, что и как вы тестируете, вы можете пропустить неслучайные характеристики.
Один известный случай, когда все пошло не так, как надо - это RANDU . Он прошел проверку, доступную в то время - которая не учитывала поведение кортежей последующих выходных данных. Уже тройки показывают много структуры:
По сути, тесты не охватывали все варианты использования: хотя одномерное использование RANDU было (вероятно, в основном) нормальным, оно не поддерживало его использование для выборки трехмерных точек (таким образом).
Правильная псевдослучайная выборка - сложное дело. К счастью, в наши дни существуют мощные тестовые наборы, например dieharder, которые специализируются на выдаче всей известной нам статистики в предлагаемом генераторе. Это достаточно?
Если честно, я понятия не имею, что вы можете реально доказать для PRNG.
источник
2D локальный максимум
тогда каждая ячейка с полужирным шрифтом является локальным максимумом. Каждый непустой массив имеет хотя бы один локальный максимум.
Таким образом, мы доказали следующую теорему:
Или мы?
источник
Это примеры первичности, потому что они распространены.
(1) Первичность в SymPy. Выпуск 1789 . На известном веб-сайте был поставлен неверный тест, который не удался до 10 ^ 14. Хотя исправление было правильным, оно просто исправляло дыры, а не переосмысливало проблему.
(2) Первичность в Perl 6. Perl6 добавил is-prime, который использует ряд тестов MR с фиксированными основаниями. Существуют известные контрпримеры, но они довольно велики, поскольку количество тестов по умолчанию огромно (в основном скрывая реальную проблему, снижая производительность). Это будет решено в ближайшее время.
(3) Первичность в FLINT. n_isprime () возвращает true для композитов , поскольку исправлено. В основном та же проблема, что и SymPy. Используя базу данных Feitsma / Galway псевдопраймов SPRP-2 до 2 ^ 64, мы можем теперь проверить их.
(4) Математика Perl :: Первичность. is_aks_prime сломан . Эта последовательность, похоже, похожа на множество реализаций AKS - много кода, который либо работал случайно (например, потерян на шаге 1 и в итоге делал все это при пробном разделении), либо не работал для больших примеров. К сожалению, AKS настолько медленный, что его сложно проверить.
(5) Пари до 2.2 is_prime. Математика :: Пари билет . Он использовал 10 случайных баз для тестов MR (с фиксированным начальным числом при запуске, а не с фиксированным начальным значением GMP при каждом вызове). Он скажет вам, что 9 простое число примерно 1 из каждых 1M звонков. Если вы выберете правильное число, вы можете заставить его ошибаться относительно часто, но числа становятся более разреженными, поэтому на практике это не так уж много. С тех пор они изменили алгоритм и API.
Это не так, но это классика вероятностных тестов: сколько раундов вы даете, скажем, mpz_probab_prime_p? Если мы дадим ему 5 раундов, то это, похоже, будет работать хорошо - числа должны пройти тест Ферма с базой 210, а затем 5 предварительно выбранных тестов Миллера-Рабина. Вы не найдете контрпример до 3892757297131 (с GMP 5.0.1 или 6.0.0a), поэтому вам придется много тестировать, чтобы найти его. Но есть тысячи контрпримеров под 2 ^ 64. Таким образом, вы продолжаете поднимать число. Как далеко? Есть ли противник? Насколько важен правильный ответ? Вы путаете случайные базы с фиксированными? Знаете ли вы, какие входные размеры вам дадут?
Это довольно сложно проверить правильно. Моя стратегия включает в себя очевидные модульные тесты, плюс крайние случаи, а также примеры сбоев, замеченных ранее или в других пакетах, тестирование по сравнению с известными базами данных, где это возможно (например, если вы делаете один тест MR с базовым уровнем 2, то вы уменьшаете невозможность вычисления). задача проверки 2 ^ 64 чисел (около 32 миллионов чисел) и, наконец, множество рандомизированных тестов, использующих другой пакет в качестве стандарта. Последний пункт работает для таких функций, как первичность, где есть довольно простой ввод и известный вывод, но довольно много задач, как это. Я использовал это, чтобы найти дефекты как в моем собственном коде разработки, так и случайные проблемы в пакетах сравнения. Но учитывая бесконечное пространство ввода, мы не можем проверить все.
Что касается доказательства правильности, вот еще один пример первичности. Методы BLS75 и ECPP имеют концепцию сертификата первичности. По сути, после того, как они работают в поисках значений, которые подходят для их доказательств, они могут выводить их в известном формате. Затем можно написать верификатор или попросить кого-нибудь написать его. Они выполняются очень быстро по сравнению с созданием, и теперь либо (1) оба фрагмента кода неверны (следовательно, почему вы бы предпочли других программистов для верификаторов), либо (2) неверна математика, лежащая в основе идеи доказательства. # 2 всегда возможен, но они, как правило, публикуются и проверяются несколькими людьми (и в некоторых случаях вам достаточно легко пройти через себя).
Для сравнения, такие методы, как AKS, APR-CL, пробное деление или детерминистический тест Рабина, не дают ничего, кроме «простого» или «составного». В последнем случае у нас может быть фактор, следовательно, мы можем проверить, но в первом случае у нас не осталось ничего, кроме этого одного бита вывода. Программа работала правильно? Не знаю.
Важно протестировать программное обеспечение не только на нескольких игрушечных примерах, а также на нескольких примерах на каждом шаге алгоритма и сказать: «Учитывая этот вход, имеет ли смысл, что я нахожусь здесь с этим состоянием?»
источник
Алгоритм перетасовки Фишера-Йейтса-Кнута является (практическим) примером, который прокомментировал один из авторов этого сайта .
Алгоритм генерирует случайную перестановку заданного массива в виде:
«Наивный» алгоритм может быть:
Где в цикле заменяемый элемент выбирается из всех доступных элементов. Однако это приводит к смещенной выборке перестановок (некоторые перепредставлены и т. Д.)
На самом деле можно придумать перетасовку Фишера-Йейтса-Кнута, используя простой (или наивный) счетный анализ .
Основная проблема с проверкой правильности алгоритма тасования ( смещения или нет ) состоит в том, что из-за статистики требуется большое количество выборок. В статье codinghorror, на которую я ссылаюсь выше, объясняется именно это (и с реальными тестами).
источник
Лучший пример (читай: что мне больше всего неприятно ), который я когда-либо видел, имеет отношение к гипотезе Коллатца, Я участвовал в соревновании по программированию (с призом в 500 долларов на линии за первое место), в котором одной из проблем было найти минимальное количество шагов, необходимых для того, чтобы два числа достигли одного и того же числа. Решение, конечно, состоит в том, чтобы поочередно шагать каждый, пока они оба не достигнут чего-то, что было замечено ранее. Нам дали диапазон чисел (я думаю, что это было между 1 и 1000000) и сказали, что гипотеза Коллатца была проверена до 2 ^ 64, так что все числа, которые нам дали, в конечном итоге сходятся на 1. Я использовал 32-битный целые числа, чтобы сделать шаги с однако. Оказывается, что существует одно неясное число от 1 до 1000000 (что-то 170 тысяч), которое в свое время приведет к переполнению 32-разрядного целого числа. На самом деле эти цифры чрезвычайно редки ниже 2 ^ 31. Мы проверили нашу систему на ОГРОМНЫЕ числа, намного превышающие 1000000, чтобы «убедиться», что переполнения не произошло. Получается, что гораздо меньшее число, которое мы просто не тестировали, вызывало переполнение. Поскольку я использовал «int» вместо «long», я получил только приз в 300 долларов, а не приз в 500 долларов.
источник
Проблема рюкзака 0/1 - это проблема, которую почти все студенты считают разрешимой с помощью жадного алгоритма. Это случается чаще, если вы ранее показывали некоторые жадные решения в качестве версии проблемы рюкзака, где работает жадный алгоритм .
Для этих задач в классе я должен показать доказательство для рюкзака 0/1 ( динамическое программирование ) для устранения любых сомнений и для жадной версии задачи. На самом деле, оба доказательства не тривиальны, и студенты, вероятно, считают их очень полезными. Кроме того, есть комментарий по этому поводу в CLRS 3ed , глава 16, стр. 425-427 .
Проблема: вор грабит магазин и может нести максимальный вес W в свой рюкзак. Есть n предметов, и каждый из них весит wi и стоит vi долларов. Какие вещи должен взять вор? максимизировать свою выгоду ?
Проблема ранца 0/1 : установка такая же, но предметы не могут быть разбиты на более мелкие части , поэтому вор может решить либо взять предмет, либо оставить его (двоичный выбор), но не может взять часть предмета ,
И вы можете получить от студентов некоторые идеи или алгоритмы, которые следуют той же идее, что и проблема жадных версий, а именно:
Это полезно для вас? на самом деле, мы знаем, что проблема с монетами - это проблема с рюкзаком. Но в лесу проблем с рюкзаком есть и другие примеры, например, как насчет рюкзака 2D (это действительно полезно, когда вы хотите резать древесину для изготовления мебели , я видел это в местном городе из моего города), очень часто думают, что жадные работы здесь тоже, но нет.
источник
Распространенной ошибкой является неправильная реализация алгоритмов тасования. Смотрите обсуждение в Википедии .
источник
Питоны PEP450, которые ввели статистические функции в стандартную библиотеку, могут представлять интерес. В качестве оправдания наличия функции, которая вычисляет дисперсию в стандартной библиотеке python, автор Стивен Д'Апрано пишет:
Вопрос о числах и о том, как теряется точность. Если вам нужна максимальная точность, вам нужно определенным образом упорядочить свои операции. Наивная реализация приводит к неверным результатам, потому что неточность слишком велика. Это было одной из проблем, о которых говорил мой числовой курс в университете.
источник
Хотя это, вероятно, не совсем то, что вам нужно, это, конечно, легко понять, и тестирование некоторых небольших случаев без какого-либо другого мышления приведет к неверному алгоритму.
Предлагаемое решение :
Этот подход «попробуйте несколько небольших случаев и выведите алгоритм из результата» часто встречается (хотя и не так сильно, как здесь) в соревнованиях по программированию, где нужно придумать алгоритм, который (а) можно быстро реализовать и (б) ) имеет быстрое время выполнения.
источник