Детальный анализ алгоритмов

На примере задачи "Продвинутый счет" с Турнира юных программистов 2016

Введение

Программировать - значит понимать. (Кристин Нюгард)

Попытаюсь показать на простейшем примере, как можно анализировать алгоритмы и писать программы. Сразу оговорюсь, что конкретно в этой задаче "Продвинутый счет" основательный анализ не нужен - алгоритм довольно простой и легко реализуется, и возможные ошибки в программе легко отловить при тестировании.

Но вообще алгоритмы очень полезно анализировать. Типичное поведение участников олимпиады - прочитал условие задачи, что-то придумал и сразу сел за компьютер писать код.

Если алгоритм, который вы хотите реализовать, простой и понятный - тогда вперед. А если нет? Тогда вы рискуете сами запутаться в своем коде, вам придется перебирать разные варианты, потом пытаться сломать программу на каких-то тестах, но все равно вы не будете уверены, что написали программу правильно. Потому что вы ее плохо понимаете.

Трудности могут возникнуть в алгоритмах с циклами типа while. Вероятно, вы сталкивались с такой ситуацией: вроде бы алгоритм придуман, а как написать условие цикла: i >= 1 или i > 1? Примеры - алгоритм QuickSort, двоичный поиск1.

Вот для таких случаев и нужен детальный анализ. Если вы полагаете, что код можно написать как попало, а потом посмотреть, что программа выдает на тестах, и исправить ошибки, то вы сильно ошибаетесь. Даже на олимпиадах бывают задачи, в которых вы не сможете узнать правильный ответ к вашим тестам. Например, задачи на геометрию, оптимизацию. Допустим, на олимпиаде вы отправите решение в тестирующую систему, и она его проверит. Но на практике такой системы нет, поэтому вы должны сами написать тесты. А если узнать правильный ответ нельзя? Тогда программу вообще не получится протестировать (кроме, может быть, простейших частных случаев). Иногда возможно протестировать отдельные части программы. Еще полезна пошаговая отладка.

Все равно, чем лучше ты понимаешь алгоритм, тем проще его реализовать на языке программирования.

Постановка задачи

Правильная постановка задачи даже важнее, чем её решение. (Альберт Эйнштейн)

Неформальная

Юный статистик записывает количество объектов в компактном виде. Сначала он рисует единиц нулевого уровня (т.е. обычные черточки). Затем он заменяет последовательных единиц 0-го уровня на одну единицу 1-го уровня. Точно так же вместо последовательных единиц 1-го уровня малец рисует одну единицу 2-го уровня. И так далее.

Например, если , то полученный код будет записью числа в системе счисления по основанию . При этом цифр будет не более , и цифра в старшем разряде может быть сколь угодно большой.

Вычислите для заданного числа код , где - количество единиц -го уровня.

Формальная

Формальность освобождает. (Популярная у инженеров поговорка)

Дано: натуральные числа , ().

Найти: целые числа такие, что

Например, если , , то

Теоретический анализ

Нет ничего практичнее, чем хорошая теория. (Людвиг Больцман)

Исходя из формулы (1), мы можем выписать формулы для нахождения чисел 2.

Положим . Нетрудно видеть, что . Обозначим . Другими словами, ~--- остаток от деления на , а ~--- неполное частное3.

Тогда формула (1) преобразуется к виду , И тут мы замечаем, что выражения (1) и (2) абсолютно аналогичны. Рекурсия налицо.

Продолжая этот процесс по формулам приходим к выражению отсюда Теперь мы можем записать общую формулу:

Итерационный алгоритм

Формулы (3)--(5) легко преобразуются в псевдокод алгоритма:

Input: ,

Output:

;

for do

   ;

   ;

endfor

;

В данном псевдокоде не используются переменные, которые бы изменяли свое значение несколько раз.

Заменим последовательность на одну переменную , но тогда ввеем инвариант цикла.

Input: ,

Output:

for do

  //

   ;

   ;

  //

endfor

;

В комментариях указано утверждение, которое выполняется в данном месте алгоритма.

Поскольку условие в конце цикла такое же, как в начале, только вместо там , то (по индукции) на следующей итерации цикла инвариант сохраняется.

Теперь, когда мы проанализировали алгоритм и записали псевдокод4, его нетрудно записать на любом языке программирования.

     //на входе: M, a[0...N-1]
     for i := 0 to N - 1 do
     begin
       k[i] := M mod a[i];
       M := M div a[i];
     end;
     k[N] := M;
     //вывести k[N], ..., k[0]

Рекурсивный алгоритм

Тот же алгоритм можно записать в рекурсивной форме. Вместо инварианта цикла~--- спецификация рекурсивной процедуры.

Процедура с входным параметром должна найти числа такие, что Рекурсивный алгоритм сводит указанную задачу к такой же задаче, в которой на единицу больше.

Input: ,

Output:

function Rec(, )

// Разложить число с коэффициентами

   if

    

   else

    

     Rec();

   endif

endfunction

Rec(, 0);

Программная реализация:

     procedure Rec(M, i: integer);
     begin
       if (i = N) then
         k[N] := M
       else
       begin
         k[i] := M mod a[i];
         Rec(M div a[i], i + 1);         
       end;         
     end;

     begin
       //на входе: M, a[0...N-1]     
       Rec(M, 0);
       //вывести k[N], ..., k[0]
     end.

Итог

Итак, мы рассмотрели методику анализа алгоритмов на простейшем примере. С одной стороны, на олимпиаде подробнейшим образом расписывать все эти формулы не нужно, тем более в такой простой задаче. С другой стороны, если вы в процессе учебы попробуете детально расписать решение одной-двух задач, то такой подход пригодится вам при построении более сложных алгоритмов, для которых тестирование может быть просто невозможным.

Выделим этапы решения задачи:

  1. Внимательно прочитать условие и попытаться его формализовать.
  2. Провести теоретический анализ задачи. Вывести математические формулы, позволяющие построить вычислительный алгоритм.
  3. Сформулировать алгоритм на удобном для вас языке (псевдокод, блок-схема, словесное описание). Проанализировать отдельные блоки алгоритма. При необходимости записать инварианты циклов5 и убедиться, что они сохраняются от итерации к итерации.
  4. Включить компьютер и спокойно написать программу.

Хочу напомнить, что программирование - это не только алгоритмы. Это также структуры и абстрактные типы данных, но самое главное - это структура ("архитектура") вашей программы, потому что на практике программы не ограничиваются парой сотней строк, как на олимпиадах. Следовательно, нужно продумывать, как будет устроена вся программная система в целом. И эту структуру тоже не мешало бы аккуратно записать на бумажке (ну или в специальном формате на компьютере), чтобы понять, что вы хотите получить в итоге. А иначе вы сядете писать код, напишете что попало и расстроитесь.

1. Если в алгоритме только циклы for и условия, то все более-менее понятно - что написано, то и выполняется.
2. Не код, а сначала формулы! Потом мы переведем их в код.
3. которое в народе называется div
4. возможно, кому-то удобнее рисовать блок-схему
5. Иногда бывает полезно записать в некоторых местах алгоритма те утверждения, которые должны выполняться в данном месте (например, x = 0). То есть как инвариант цикла, только в любом месте алгоритма.

results matching ""

    No results matching ""