Оршанский Сергей Александрович, Шалыто Анатолий Абрамович
ПРИМЕНЕНИЕ ДИНАМИЧЕСКОГО ПРОГРАММИРОВАНИЯ ПРИ РЕШЕНИИ ЗАДАЧ НА КОНЕЧНЫХ АВТОМАТАХ
ВВЕДЕНИЕ
В последнее время в программировании все чаще используются конечные автоматы [1-7]. Поэтому задача исследования их свойств остается актуальной.
Эти исследования осуществляются с применением различных математических методов [1]. При этом представляется интересным использование для этой цели динамического программирования [8-10].
Цель настоящей работы - продемонстрировать эффективность применения динамического программирования для решения одной задачи на конечных автоматах, которая называется «Непоглощающий конечный автомат» [11].
1. КОНЕЧНЫЕ АВТОМАТЫ
Конечный автомат состоит из множества состояний и «управления»,
"КоЯегЯмй а&ЛошаЛ- со&йои&
которое пере^о^мй аб^омл^ одного сос&о&Лия
которое переводит автомат из одного состояния в другое, в зависимости от получаемых извне «входных данных». Автоматы разделяются на два класса, в зависимости от типа управления. Оно может быть детерминированным — автомат в каждый момент времени находится только в одном состоянии, и недетерминированным - автомат может одновременно находиться в нескольких состояниях.
Приведем классическое определение детерминированного конечного автомата (ДКА) [1]. ДКА - это упорядоченный набор <2,и,э,т,ф>, где 2 - конечное множество, называемое входным алфавитом, и -конечное множество состояний, э е и -начальное состояние, т - множество терминальных состояний т с и, и, наконец, ф: и X 2 ® и - функция переходов.
Входом автомата является строка а над алфавитом 2. Первоначально автомат находится в состоянии э. На очередном шаге он переходит из текущего состояния и в состояние ф (и, с), где с - первый символ входной строки. После этого первый символ входной строки удаляется и шаг повторяется. Если к моменту исчерпания входной строки автомат находится в терминальном состоянии, то говорят, что он допускает ис-
—" ^ .__ ■— _ .1
■ другое..
ходную строку а, в противном случае - отвергает ее.
2. ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
Динамическое программирование позволяет решать задачи, разбивая каждую из них на подзадачи, аналогичные исходной задаче, и объединяя в дальнейшем решения этих подзадач. Подзадачи, в свою очередь, разбиваются на «подподзадачи» и т. д. Основным условием для применения динамического программирования является наличие перекрывающихся подзадач. Суммарное число всех встречающихся подзадач должно быть относительно невелико - например, полиномиально зависеть от размера входных данных.
Алгоритмы, основанные на динамическом программировании, используют перекрытие подзадач следующим образом: каждая подзадача решается один раз, и ответ заносится в специальную таблицу или запоминается иным способом. Когда эта подзадача встречается снова, программа не будет тратить время на ее повторное решение, а использует готовый ответ. Поэтому алгоритмы, основанные на динамическом программировании, оказываются существенно эффективнее алгоритмов, основанных на методе «Разделяй и властвуй» [12].
Обычно задача, решаемая с помощью динамического программирования, представляется как вычисление некоторой функции. В этом случае подзадачей, как правило, является поиск значений той же функции с меньшими значениями аргументов.
Как строится алгоритм, основанный на динамическом программировании? Его построение начинается с нахождения рекуррентных соотношений, связывающих значение функции для задачи со значениями функции для различных подзадач. Затем, зная ответ для базовых случаев, можно вычислить ответ для задачи, предварительно найдя ответы для подзадач.
Примеры классических задач, эффективно решаемых на основе динамического программирования: перемножение матриц с
минимальным количеством умножений, оптимальная триангуляция выпуклого многоугольника, наибольшая возрастающая подпоследовательность.
Систематическое изучение динамического программирования было начато Р. Бел-лманом в 1955 г. [8], хотя некоторые приемы такого рода были известны и ранее. О динамическом программировании много написано в работах [9, 10].
3. ЗАДАЧА
Задача «Непоглощающий детерминированный конечный автомат» (Non Absorbing Deterministic Finite Automaton (DFA)).
Автор задачи: Андрей Станкевич.
Источник: летние сборы команд-участниц чемпионата мира ACM по программированию. Петрозаводск, 2003.
Расположение: задача № 201 в архиве олимпиадных задач Саратовского государственного университета на сайте http:// acm.sgu.ru.
Условие задачи сформулировано на английском языке. Перевод на русский выполнен авторами настоящей работы. Аббревиатура «DFA» переводится как «ДКА» -детерминированный конечный автомат.
Ограничения и требования:
- ограничение по времени: 2с;
- ограничение по памяти: 64 Мб;
прлграммл ... ucnaœvpfeiû го&а&мй Ofü&etü...
- входные данные: стандартный ввод;
- выходные данные: стандартный вывод.
В теории компиляторов и языков широко используются детерминированные конечные автоматы, определение которых приведено в разделе 1.
Иногда удобно расширять это определение понятием непоглощающих ребер. Для этого в дополнение к функции переходов j также вводится функция поглощения %: U х S ® {0,1}. Тогда при совершении перехода из состояния u по символу c первый символ входной строки удаляется, если % (u,c) = 0. Если же % (u,c) = 1, то входная строка остается без изменений, и следующий переход производится из нового состояния, но по тому же символу c. В первом случае говорят, что произошел переход по поглощающему ребру, а во втором - по непоглощающему.
По определению, такой автомат допускает строку а, если после некоторого числа шагов строка оказывается пустой, а автомат при этом находится в терминальном состоянии.
Задача: определить число строк данной длины N, допускаемых заданным ДКА с непоглощающими ребрами.
Формат входных данных
Первая строка входного файла содержит S - подмножество английского алфавита (несколько маленьких латинских букв).
Вторая строка содержит K = |U| - количество состояний автомата (1 < K < 1000). Состояния нумеруются от 1 до K.
Третья строка содержит S (1 < S < K) -номер начального состояния и L = |T| -количество терминальных состояний, а затем L различных целых чисел от 1 до K каждое - номера терминальных состояний.
Следующие K строк содержат по ISI целых чисел каждая и определяют функцию j.
Следующие за ними K строк определяют функцию % тем же способом. Последняя строка входного файла содержит N (1 < N < 60).
Формат выходных данных
Единственное число - количество строк длины N над алфавитом S, допускаемых данным ДКА.
Пример исходных данных для рассматриваемой задачи приведен в таблице:
Пример входных данных Пример выходных данных
ab 2
2
1 1 2
2 1
1 2
0 1
0 0
3
Описанный в примере автомат допускает две строки длины три: «aaa» и «abb».
4. РЕШЕНИЕ 4.1. АНАЛИЗ ЗАДАЧИ
Имеется ДКА с непоглощающими ребрами - ребрами, при совершении перехода по которым не удаляется первый символ входной строки. В этом случае следующий переход происходит из нового состояния, но по тому же символу. Это будет продолжаться до тех пор, пока не произойдет переход по поглощающему ребру, либо пока одно и то же состояние не повторится дважды и процесс не зациклится. Важно, анализируя условие задачи, не упустить из виду возможность существования циклов из не-поглощающих ребер.
ДКА с непоглощающими ребрами может быть сведен к ДКА без них. Рассмотрим произвольное непоглощающее ребро -пару (u,c), для которой функция поглощения %(u,c) = 1. В зависимости от текущего состояния u и первого символа входной строки c автомат (ДКА) либо рано или поздно пройдет по поглощающему ребру, либо войдет в цикл. Перед этим автомат, возможно, пройдет по цепочке непоглоща-ющих ребер. Этого можно избежать, изменив функцию переходов так, чтобы ребро (u,c) стало поглощающим, но ДКА допускал бы те же строки, что и до изменения. Заметим, что после попадания в цикл из непоглощающих ребер очередной символ входной строки никогда не будет удален, и
автомат по определению не допускает исходную строку. Поэтому, для того чтобы учесть возможность вхождения в цикл, добавим фиктивное состояние «Недопуск», которое не является терминальным. Будем считать, что все ребра из состояния «Недопуск» ведут в него. Положим, что j(u,c) = «Недопуск». При этом, вместо перехода по непоглощающему ребру, приводящему в цикл, автомат совершит переход по поглощающему ребру в состояние «Недопуск», и исходная строка не будет допущена.
Обратимся к входным данным из примера. ДКА, описанный в нем, имеет два состояния. Первое состояние - начальное, а второе - терминальное. Необходимо определить число строк из трех символов, допускаемых автоматом.
На рисунке 1 изображен ДКА из примера. В нем непоглощающее ребро выделено пунктиром.
Преобразуем этот автомат, для того чтобы избавиться от непоглощающих ребер. На рисунке 2 изображен преобразованный ДКА, эквивалентный исходному. Состояние «Недопуск» помечено буквой «Н».
Определим число строк длины три, допускаемых рассматриваемым автоматом. Заметим, что в первом (начальном) состоянии на вход не должен подаваться символ «Ь», поскольку это приведет к зацикливанию. Поэтому первым символом должен быть «а», по которому автомат переходит в состояние 2. Осталось подобрать оставшиеся два входных символа так, чтобы автомат через два шага оказался в единственном терминальном состоянии 2. Перебрав все варианты, выясним, что автомат допускает две строки: «ааа» и «abb». Действительно, ответ - два.
фассмо&^им. пЪои^^ол&Ног. Аеаогмощшщге ребрл...
4.2. ОБЩАЯ СХЕМА РЕШЕНИЯ
Выше было показано, что ДКА с непог-лощающими ребрами можно преобразовать в ДКА без них. Однако пока неясно, насколько эффективно выполняется это преобразование. Допустим, что удастся разработать эффективный алгоритм удаления не-поглощающих ребер. Тогда необходимо, используя преобразованный ДКА, получить ответ - число допускаемых строк. Ясно, что получать ответ перебором и проверкой всех строк заданной длины нельзя, так как их в общем случае может оказаться очень много.
Как можно посчитать число строк, удовлетворяющих каким-то условиям? Первый подход - использование хитрых комбинаторных соотношений и непосредственное их применение в программе. Второй (если система слишком сложна): найти некую рекуррентную формулу и выразить число строк данной длины с какими-то свойствами через число строк меньшей длины с теми
Рисунок 1. ДКА из примера с непоглощающими ребрами.
Рисунок 2. ДКА из примера после удаления непоглощающих ребер.
"Шлкое fvtufrHoe чимл Не комес&ш&с& сЛлН^л^Л-Ний Лип,,,
же или другими свойствами, а затем применить динамическое программирование для последовательного вычисления всех этих значений. Опыт подсказывает, что для подсчета числа строк, допускаемых данным ДКА, следует использовать именно динамическое программирование.
Оценим практический масштаб задачи. Алфавит может состоять из 26 символов (|A| = 26), а строка может иметь максимальную длину в 60 символов. Рассмотрим конечный автомат, допускающий любую поданную на вход строку, например, автомат, состоящий из одного состояния, которое одновременно является начальным и терминальным. При этом ответ будет 2660 строк, что очень много. Насколько много? При наличии компьютера или хорошего калькулятора, на этот вопрос ответить легко. Определим log10 (2660) = 60log10(26) = = 60(ln(2 6) / ln(10)) » 60*1.415 » 84.9. Следовательно, в ответе может быть 85 цифр в десятичном представлении. Такое длинное число не поместится в стандартный тип (Integer). Поэтому при написании решения на языке Pascal придется вручную реализовать арифметику повышенной точности.
Выделим подзадачи и составим рекуррентные соотношения. Ключевая идея: достоинство модели конечного автомата состоит в том, что вся история выражается одним числом - номером состояния, а число этих состояний конечно. Для ответа на вопрос, допускается ли конкретная строка, имеет значение не только состояние автомата, но и число поглощенных (удаленных) символов строки. Будем рассматривать ДКА без не-
поглощающих ребер. Тогда число поглощенных символов совпадает с числом прошедших шагов - тактов работы автомата.
Рассмотрим пару (state,k) - текущее состояние, которое описывается состоянием автомата state и числом символов k, поглощенных автоматом к текущему моменту. Рассмотрим все строки длины k, обладающие следующим свойством: получив на вход такую строку, ДКА попадает из начального состояния в состояние state. Обозначим число таких строк через f(state,k) . Каждой паре (state,k) соответствует подзадача - вычисление f(state,k) . После решения этих подзадач останется только просуммировать значения f(t,N) по всем терминальным состояниям t е T. Здесь N - длина строк, число которых требуется найти. Как отмечалось выше, для применения динамического программирования необходимы рекуррентные соотношения, которые в данном случае вытекают из определения ДКА:
f( t t 0) - Jl' state — initial.
[0, state Ф initial'
f( state, k) = ^ f(st,k -1).
steU ,ceS j(st,c)=state
где initial - начальное состояние ДКА. Здесь для вычисления функции f (state, k) перебираются все пары вида (st,c), где st - предыдущее состояние, а c - символ, по которому произошел последний переход. Соответственно, j(st,c) = state.
Сформулируем схему решения.
1. Превращаем ДКА с поглощающими ребрами в ДКА без них. Наличие решения с помощью динамического программирования для ДКА без непоглощающих ребер укрепляет уверенность в том, что эффективное устранение непоглощающих ребер также возможно.
2. Применяем динамическое программирование, используя вышеприведенные рекуррентные соотношения. Целесообразно, хотя и не обязательно, использовать динамическое программирование «Снизу вверх» [9].
Для нахождения ответа необходимо про-
суммировать^f(t,N) по всем терминаль-
teT
ным состояниям t.
4.3. УДАЛЕНИЕ ПОГЛОЩАЮЩИХ РЕБЕР
Предположим, что ДКА находится в некотором состоянии, и последовательно переберем все символы из входного алфавита. Выполним переход по рассматриваемому символу, как будто он является очередным символом входной строки. Если переход произошел по непоглощающему ребру, то выполним следующий переход по тому же символу. Если переход снова произошел по непоглощающему ребру, то повторим тот же «маневр» и т. д. В результате либо символ будет поглощен, либо автомат войдет в цикл.
Модифицируем функцию переходов. В первом случае изменим переход из исходного состояния, из которого автомат прошел по цепочке непоглощающих ребер, сразу вводя переход в конечное состояние. Во втором - введем ребро в фиктивное состояние - «Недопуск», которое не является терминальным. Оно замкнуто на себя при переходе по всем символам. Попадание в это состояние будет соответствовать входу в цикл из непоглощающих ребер в исходном автомате. Обработав таким образом все состояния автомата, получим конечный автомат без непоглощающих ребер, эквивалентный исходному.
Оценим эффективность программы, реализующей предложенный подход. Как определить, что произошло попадание в цикл? В худшем случае при обработке одного состояния придется сделать порядка | и| шагов. Поэтому можно сделать | и | шагов, и если ДКА все еще не перейдет по непогло-щающему ребру, то, следовательно, он вошел в цикл. Верхняя оценка времени работы: |U|*|U|* |S| - порядка 26*106. Много это или мало? По меркам 2003 г., в котором была предложена задача, за секунду можно было выполнить 107 простых операций на языке высокого уровня, таком, как, например, Pascal или C. Следовательно, такой способ устранения непоглощающих ребер требует не более одной секунды, что в первом приближении допустимо, так как по условию задачи время на выполнение одного теста не должно превышать двух секунд.
Приведем псевдокод алгоритма удаления непоглощающих ребер, в котором множество всех состояний обозначено через State (листинг 1).
Отметим, что существует решение, которое устраняет непоглощающие ребра за О ( | и | * | S | ) с помощью поиска в глубину. Однако не следует искать его на этом этапе -
Листинг 1
for c in Alpha do // По всем символам алфавита
for i in State do // По всем состояниям автомата begin
cur := i // Текущее состояние
z := n // Количество состояний
// Пока ребро-переход из состояния k по символу j -// поглощающее, и еще сделано не очень много переходов while (%[cur,c] = 1) and (z > 0) do begin
cur := j[cur][c] // Перейти z := z - 1 // Уменьшить счетчик
end
// Если все еще очередное ребро - непоглощающее if (%[cur][c] = 1) then
j[i][c] := 0 // Следовательно - цикл
else
j[i][c] := j[cur][c] // Иначе переставляем ребро // Теперь снимаем пометку "непоглощающее ребро" С [i][j] := 0 end
ни в практическом программировании, ни в олимпиадном [13]. Необходимо рассмотреть решение второй части, а потом решать, требуется ли более эффективно устранять непоглощающие ребра или предложенный вариант приемлем.
4.4. ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ И ПОЛУЧЕНИЕ ОТВЕТА
Перейдем к рассмотрению второго и третьего этапов схемы решения (разд. 4.2). Теперь будем рассматривать только ДКА без непоглощающих ребер. Строим решение.
Динамическое программирование бывает двух видов: «Снизу вверх» и «Сверху вниз» [9]. Динамическое программирование «Снизу вверх», в свою очередь, удобно разделять на два вида. Будем называть первый из них, который больше похож на динамическое программирование «Сверху вниз», методом «Сзади сюда». В этом случае для каждой пары (state, k) значение функции f(state,k) находится через уже вычисленные значения функции для других пар аргументов. Второй метод («Отсюда вперед») состоит в следующем. Каждый раз выбирается пара (state, k), для которой значение функции уже известно, и в таблице учитывается его вклад в значение функции для других пар аргумен-
тов, для которых верное значение функции еще не найдено.
В динамическом программировании «Сверху вниз» в каждом состоянии (state, k) придется найти пары, из которых был переход в рассматриваемое состояние. Следовательно, придется перебирать прошлые состояния автомата и символы, по которым мог произойти переход, но суммирование производить лишь иногда. Аналогичная ситуация возникает при использовании динамического программирования «Снизу вверх», «Сзади сюда». Для варианта «Отсюда вперед» все проще: из данной пары переходим только в те состояния, в которые необходимо (рассматривая все символы), а прибавление будет происходить на каждой итерации цикла. Тем самым, полученное решение будет более эффективным. Итак, выбираем решение с помощью динамического программирования вида: «Снизу вверх», «Отсюда вперед».
Рассмотрим некоторые детали реализации. Заведем для значений функции f одноименный массив f [state, k].
Используем в псевдокоде рекуррентные соотношения, приведенные в разд. 4.2. Заметим, что цикл по всем состояниям включает фиктивное состояние «Недопуск» под номером ноль, добавленное при удалении непоглощающих ребер. Поэтому вместо множества состояний State будет исполь-
Листинг 2
// Один способ оказаться в начальном состоянии f[init,0] := 1
// И ноль - во всех остальных состояниях for i in State0 do
if (i <> init) then f[i,0] := 0
// Считаем число допускаемых n-символьных строк for k := 1 to n do
for st in State0 do // По всем состояниям for c in Alpha do // По всем символам алфавита f[j[st,c],k] := f[j[st,c],k] + f[st,k-1]
ans := 0
// Суммируем по всем терминальным состояниям for st in TerminalState do ans := ans + f[st,n]
зоваться множество состояний State0, включающее еще и состояние «Недопуск» (листинг 2).
Отметим, что в псевдокоде для простоты опущено обнуление всего массива f.
Зачем заводить двумерный массив? Ведь в каждый момент требуются далеко не все ранее вычисленные значения функции. Достаточно одномерного массива sum[state] -число способов оказаться в данном состоянии на текущем шаге.
Изменим псевдокод, чтобы вместо двумерного массива использовался одномерный (листинг 3).
Оценим эффективность этого решения. Длинные числа складываются N *|U|*| S | раз, что в худшем случае может достигать величины 60*1000*26 » 1.5 миллионов раз. Второй секунды на это достаточно. И на копирования временного массива sum2 в sum - тем более. Скорее всего, для первой части не потребуется искать более эффективное решение.
Как было отмечено выше, ответ может иметь много разрядов. Поэтому должна применяться арифметика повышенной точности («длинная арифметика»). Для написания
"ОиНамигеское п^огромжфо&аНие б^&геЛ- f&yx и
решения на языке Pascal (Borland Delphi) длинную арифметику придется реализовы-вать самостоятельно, используя при этом книгу Д. Кнута [14], в которой этому посвящена четвертая глава. В данной задаче для повышения эффективности разумно реализовать длинную арифметику по основанию 109 - хранить по девять десятичных цифр в одной ячейке массива, содержащего длинное число.
Исходный текст решения приведен в приложении на диске.
Листинг 3
// Один способ оказаться в начальном состоянии sum[init] := 1
// И ноль - во всех остальных состояниях for i in State0 do if (i <> init) then sum[i] := 0
// Считаем число допускаемых n-символьных строк
for k := 1 to n do
begin
sum2 := sum
for i in State0 do sum[i] := 0 for i in State do for c in Alpha do
sum[j[i][c]] := sum[j[i][c]] + sum2[i]
end
ans := 0
// Суммируем по всем терминальным состояниям for st in TerminalState do ans := ans + sum[st]
по&миеяяой ЛогЯосЛи ("длинная а^ир^еЛика»),
ЗАКЛЮЧЕНИЕ
Применение динамического программирования позволило получить эффективное решение рассмотренной задачи, удовлетворяющее ограничениям по времени и памяти, поставленным в условии задачи.
Кроме рассмотренной, с применением динамического программирования могут быть решены также многие другие задачи на автоматах.
Перечислим некоторые из них:
- найти лексикографически минимальную строку, допускаемую данным автоматом;
- определить кратчайшую по числу символов строку, допускаемую данным автоматом;
- определить в каких состояниях может оказаться автомат после получения на вход строки данной длины;
- определить вероятность оказаться в каждом из состояний после получения случайной строки фиксированной длины, различные символы которой независимы в совокупности. При этом могут быть заданы вероятности появления всех символов алфавита.
Перечислим еще несколько задач на автоматах:
- существует ли входная последовательность длины, не превышающей заданную, на которой автомат формирует данный выход;
- существует ли входная последовательность, длина которой не превышает заданную, на которой автомат формирует выход, допускаемый вторым автоматом (это дина-
мическое программирование на произведении автоматов).
В этих задачах фразу «существует ли входная последовательность» можно заменить на фразу «найти лексикографически минимальную входную последовательность».
Аналогично можно определить число строк заданной длины, на которых автомат формирует заданный выход и т. д.
Как было показано в настоящей работе, применение динамического программирования на конечных автоматах, как и вообще на графах, является весьма эффективным. Перекрывающиеся подзадачи автоматически выделяются: необходимо только отслеживать целевую функцию во всех состояниях автомата. В качестве примера можно привести задачу, при решении которой применяется та же техника, что и при решении задачи, рассмотренной в настоящей работе: задача «Currency Exchange» («Обмен валюты»). Автор: Николай Дуров. Источник: четвертьфинал NEERC-2001, северный подрегион. Задача размещена на сайте http://acm.timus.ru под № 1162. Фактически математической моделью этой задачи является конечный автомат, в котором состояниями являются валюты, а переходами -обменные пункты.
Следует указать еще одну известную задачу: «Censored!» («Цензура!»). Автор: Николай Дуров. Источник: четвертьфинал NEERC-2001, северный подрегион. Задача размещена на сайте http://acm.timus.ru/ под № 1158. Ее решение состоит из двух этапов: построение конечного автомата, распознающего набор строк (на основе алгоритма Ахо - Корасика), и использование динамического программирования на полученном конечном автомате.
В заключение работы отметим, что исследования по теории автоматов проводятся на кафедре «Интеллектуальные системы» мехмата Московского государственного университета (http://intsys.msu.ru/), а по применению автоматов в программировании -на кафедре «Технологии программирования» Санкт-Петербургского государственного университета информационных технологий, механики и оптики (СПбГУ ИТМО) (http://is.ifmo.ru/).
Литература
1. Хопкрофт Д., Мотвани Р., Ульман Д. Введение в теорию автоматов, языков и вычислений. М.: Вильямс, 2002.
2. Непейвода H.H. Стили и методы программирования. М.: Интернет-университет информационных технологий. 2005.
3. Карпов Ю.Г. Теория автоматов. СПб.: Питер, 2002.
4. Шалыто A.A. Технология автоматного программирования // Мир ПК. 2003. № 10. С.74-78. http://is.ifmo.ru/works/tech aut prog/
5. Казаков М.А., Шалыто A.A. Использование автоматного программирования для реализации визуализаторов // Компьютерные инструменты в образовании. 2004. № 2. С. 19-33.
http ://is. ifmo.ru/works/art vis.pdf
6. Беляев A.B., Суясов Д.И., Шалыто A.A. Компьютерная игра «Космонавт». Проектирование и реализация // Компьютерные инструменты в образовании. 2004. № 4. С. 75-84. http://is.ifmo.ru/works/ cosmo article.pdf
7. Мазин M.A., Парфенов В.Г., Шалыто A.A. Анимация. FLASH технология. Автоматы // Компьютерные инструменты в образовании. 2003. № 4. С. 39-47. http ://is.ifmo .ru/proj ects/flash/
8. Беллман Р. Динамическое программирование. М.: Изд-во иностр. лит., 1960.
9. Кормен Т., Лейзерсон Ч., Ривест Р. Алгоритмы. Построение и анализ. М.: МЦНМО, 1999.
10. Скиена С., Ревилла М. Олимпиадные задачи по программированию. Руководство по подготовке к соревнованиям. М.: Кудиц-Образ, 2005.
11. Станкевич A. С. Непоглощающий детерминированный конечный автомат / Архив олимпиад -ных задач Саратовского государственного университета. Задача № 201. http://acm.sgu.ru
12. Бобак И. Алгоритмы: «возврат назад» и «разделяй и властвуй» // Программист. 2002. № 3. С.29-32.
13. Оршанский СЛ. О решении олимпиадных задач по программированию формата ACM ICPC // Информатика. 2006. № 1. С. 21-26.
14. Кнут Д.Э. Искусство программирования. Том 2. Получисленные алгоритмы. М.: Вильямс, 2004.
Оршанский Сергей Александрович, бакалавр прикладной математики и информатики СПбГУ ИТМО, чемпион мира по программированию ACM ICPC 2004, III место на чемпионате мира по программированию ACM ICPC 2005,
Шалы1то Анатолий Абрамович, доктор технических наук, профессор, заведующий кафедрой «Технологии Программирования» СПбГУ ИТМО.
© Наши авторы. 2006 Our authors, 2006.