САМАРСКИЙ ГОСУДАРСТВЕННЫЙ АРХИТЕКТУРНО-СТРОИТЕЛЬНЫЙ УНИВЕРСИТЕТ ИНФОРМАЦИОННЫЕ ТЕХНОЛОГИИ.Часть 2. Курс лекций для студентов специальности 230400 – «Информационные системы и технологии» Прохорова О.В. 01.01.2012 Рассматриваются общие требования к методологии и проектированию ПО. Приводятся основные положения алгоритмизации на примерах работы с перестановками. Разбираются аспекты применения рекурсии. Изучаются основные информационные структуры ПО. Разбираются алгоритмы и программы генерирования случайных последовательностей. Приводятся примеры конструирования программ на языке С++. Оглавление Алгоритмы на перестановках ................................................................... 3 1. 1.1. Введение ............................................................................................................................................... 3 1.2. Перестановки. Произведение перестановок .................................................................................. 4 1.3. Обратные перестановки. Алгоритмы ............................................................................................ 8 1.4. Рекурсия .............................................................................................................................................. 12 Информационные структуры ................................................................. 14 2. 2.1. Последовательное и связанное распределение данных ............................................................... 14 2.2. Стеки и очереди ................................................................................................................................ 19 2.3. Организация связанного распределения на основе стеков и очередей ...................................... 21 2.4. Деревья ............................................................................................................................................... 22 2.5. Представление деревьев ................................................................................................................. 25 2.6. Прохождение деревьев, леса ............................................................................................................ 27 Алгоритмы на графах .............................................................................. 35 3. 3.1. Графы. Представления графов. Связность и расстояние ........................................................... 35 3.2. Остовные деревья............................................................................................................................. 38 3.3. Поиск по графу в глубину. Топологическая сортировка ............................................................... 40 3.4. Топологическая сортировка ............................................................................................................ 41 3.5. Транзитивное замыкание. Поиск кратчайшего пути на графе ................................................. 43 3.6. Поиск кратчайшего пути ................................................................................................................ 43 Генерирование случайных последовательностей ................................ 47 4. 4.1. Генерирование равномерно распределенных случайных чисел ................................................... 47 4.2. Основные критерии проверки случайных наблюдений ................................................................ 54 4.3. Эмпирические критерии ................................................................................................................... 58 4.4. Численные распределения ................................................................................................................ 61 4.5. Что такое случайная последовательность ................................................................................. 65 Литература ........................................................................................................ 67 2 1. Алгоритмы на перестановках 1.1. Введение Понятие алгоритма является основным для всей области компьютерного программирования. Истоки понятия «алгоритм» ассоциируются с процессом нахождения наибольшего общего делителя двух чисел [3]. Алгоритм Евклида (Е). Даны два целых положительных числа m и n. Требуется найти их наибольший делитель, т.е. наибольшее целое число, которое нацело делит оба числа m и n. Е1. [Нахождение остатка]. Разделим m на n и, пусть остаток от деления будет равен r (где 0 r < n). E2. [Сравнение с нулем]. Если r = 0, то выполнение алгоритма прекращается: n — искомое значение. Е3. [Замещение]. Присвоить m n, n r и вернуться к шагу Е1. Каждый шаг любого алгоритма начинается заключенной в квадратные скобки фразой, которая как можно более кратко выражает содержание данного шага. Обычно эта фраза отображается в сопровождающей алгоритм блок-схеме, как показано на рисунке. В алгоритмах “” обозначает операцию замещения, например, n n+1 (n+1 n противоречит стандартным соглашениям). Современное значение слова алгоритм во многом аналогично таким понятиям, как рецепт, процесс, метод, способ, процедура, программа, но всетаки слово алгоритм имеет дополнительный смысловой оттенок. 3 Алгоритм — это не просто набор конечного числа правил, задающих последовательность выполнения операций для решения задачи. Помимо этого, он имеет 5 важных особенностей: 1) конечность. Алгоритм всегда должен заканчиваться после выполнения конечного числа шагов; 2) определенность. Каждый шаг алгоритма должен быть определен; 3) ввод. Алгоритм имеет некоторое (возможно, равное нулю) число входных данных, т.е. величин, которые задаются до начала его работы или определяются динамически во время его работы; 4) вывод. У алгоритма есть одно или несколько выходных данных, т.е. величин, имеющих вполне определенную связь с входными данными. 5) эффективность. Алгоритм обычно считается эффективным, если все его операторы достаточно просты для того, чтобы их можно было точно выполнить в течение конечного промежутка времени. В дальнейшем перейдем к рассмотрению принципов, методов и алгоритмов, используемых при программировании. 1.2. Перестановки. Произведение перестановок Перестановкой n объектов называется способ последовательного расположения n различных объектов с учетом порядка [1]. Например, для трех объектов {a, b, c} существует шесть перестановок abc, acb, bac, bca, cab, cba. (1.1) Свойства перестановок играют важную роль в анализе алгоритмов. Поставим задачу сосчитать перестановки для n объектов. Существует n способов выбора крайнего объекта слева (первого); после того как этот 4 выбор сделан, существует n-1 способ выбора следующего за ним объекта. Таким образом, получаем общее число перестановок равное n (n – 1) … (1). n Обозначим через Pk количество способов выбора k объектов из n с учетом порядка. Для прикладных целей важное значение имеет процесс построения всех перестановок из n объектов методом индукции в предположении, что все перестановки из n-1 объектов уже построены. Перепишем (1.1), заменив буквы {a, b, c} цифрами {1, 2, 3}, тогда получим следующие перестановки: 123, 132, 213, 231, 312, 321. Зададим вопрос, как с использованием этого набора получить все возможные перестановки из четырех цифр {1, 2, 3, 4} и т.д. Метод. Для каждой перестановки a1 a2 … an-1 из {1, 2, … n-1} объектов построим еще n перестановок, помещая число n на всевозможные места, в результате получим: n a1 a2 … an-1 ; a1 n a2 … an-1 ; … a1 a2 … n an-1 ; a1 a2 … an-1 n Например, для перестановки 231 получим 4231 2431 2341 2314 и т.д. Очевидно, этот метод дает все возможные перестановки из n объектов, причем ни одна из них не повторяется. Отметим, что Pn = = n (n – 1) … (1) является очень важной математической величиной, ее называют n- факториалом и записывают n! = 1 2 … n = n k. k 1 Отметим важные свойства факториала 0! = 1; n! = (n – 1)! n. n В дальнейшем нам понадобится формула определения Pk . Приведем ее известный вид: 5 Pkn n! . k!(n k )! Обычно перестановку рассматривают как расположение объектов в ряд. Но возможен и другой способ. Перестановку можно рассматривать как переупорядочение или переименование объектов. При такой интерпретации обычно используют двух строчную запись, например: a b c d e f c d f b e a (1.2) С точки зрения переупорядочения это означает, что объект с переходит на место, которое ранее занимал объект а. А если это рассматривать как переименование, то можно считать, что объект а переименован в с. При использовании двух строчной записи изменение порядка столбцов в перестановке никакой роли не играет. Например, перестановку (1.2) можно записать в виде c d f b a e , f b a d c e а также еще 718 способами [1]. В связи с описанной интерпретацией часто используют циклическую запись. Перестановку (1.2) можно записать в виде (a c f) (b d) (1.3) Запись перестановки в виде цикла не является единственной. Например, записи 6 (b d) (a c f) ; (c f a)(b d) ; (d b) (f a c) эквивалентны (1.3), а перестановка (a f c) (b d) им не эквивалентна. В программировании эти понятия применяются каждый раз, когда некоторый набор из n объектов нужно расположить в другом порядке. Чтобы переупорядочить объекты, не перемещая их в какое-либо другое место, необходимо придерживаться циклической структуры. Например, чтобы переупорядочить (1.2), т.е. присвоить (a, b, c, d, e, f) (c, d, f, b, e, a), будем следовать циклической структуре (1.3) и последовательно присваивать t a, a c, c f, f t, t b, b d, d t . То есть (c f a) (d b) Замечание. Любое подобное преобразование характерно только для непересекающихся циклов. Две перестановки можно перемножить в том смысле, что их произведение означает применение одной перестановки вслед за другой, например, a c b d c f d b a c b d c f a c b a c e Замечание. f a a b e e d b d d e e f c a c e f f b Произведение b d c c d a d a f e f e e f b d e f перестановок a b не (1.4) обладает свойством коммутативности, т.е. 1 2 2 1 , где 1 и 2 — перестановки. 7 Пользуясь циклической записью, запишем равенство (1.4) следующим образом: (a c f) (b d)* (a b d) (e f) = (a c e f b) Рассмотрим следующий подход к (1.5) перемножению перестановок, представленных в виде циклов. Например, вычисляя произведение перестановок (a c f g) (b c d) (a e d) (f a d e) (b g f a e) (1.6) находим (двигаясь слева направо), что a переходит в с, d c, a d, d a и d остается без изменений. Т.о. конечным результатом первого прохода формулы (1.6) является то, что d a и мы запишем “( a d” в качестве частичного ответа. Теперь выясним, что происходит с d: b d, g b. Следовательно, имеем частичный результат “( a d g”. Рассмотрев, что происходит с g, находим, что a f e a g, т.о. первый цикл заканчивается: “(a d g)”. Теперь выберем новый элемент, который еще не встречался, например, с. Находим, что e c … и т.д. Окончательный ответ: (a d g) (c e b). 1.3. Обратные перестановки. Алгоритмы Каждая перестановка имеет себе обратную. Например, имеем перестановку a b c d e f , для нее обратной будет перестановка c d f b e a 8 c d f b e a a b c d e f . a b c d e f f d a b e c Алгоритм I. (Обратная перестановка на месте). Заменим перестановку X[1], X[2] … X[n], которая является перестановкой чисел {1, 2, …, n}, обратной перестановкой. I1. [Инициализация]. Присвоить m n, j -1. I2. [Следующий элемент]. Присвоить i X[m]. Если i < 0, перейти к шагу I5 (этот элемент уже был обработан). I3. [Обратить один элемент]. (В этот момент j < 0 и i = X[m]. Если m не является наибольшим элементом своего цикла, то первоначальная перестановка давала X[-j] = m). Присвоить X[m] j, j -m, m i, i X[m]. I4. [Конец цикла?]. Если i > 0, перейти к шагу I3 (этот цикл не закончен); иначе — присвоить i j. (В последнем случае первоначальная перестановка давала X[-j] = m, где m — наибольший элемент в его цикле.) I5. [Сохранить окончательное значение]. Присвоить X[m] -i. (Первоначально X[-i] было равно m). I6. [Цикл по m]. Уменьшить m на 1. Если m > 0, перейти к I2, в противном случае работа алгоритма заканчивается. Пример. Найти обратную перестановку для строки: 6 2 1 5 4 3. Решение. В качестве второй строки запишем порядковые номера цифр 6 2 1 5 4 3 строки, получим: 1 2 3 4 5 6 9 1 2 3 4 5 6 . Переставим Переставим строки местами, получим: 6 2 1 5 4 3 столбцы местами, сохраняя порядок элементов заданной строки, получим 6 2 1 5 4 3 . Таким образом, перестановка (3 2 6 4 5 1) является 3 2 6 4 5 1 результатом обращения исходной строки 6 2 1 5 4 3. Алгоритм J. (Обращение на месте). Этот алгоритм дает такой же результат, как и алгоритм I, но на основании другого подхода. J1. [Сделать все величины отрицательными]. Присвоить X[k] -X[k] для 1 k n. Присвоить также m n. J2. [Инициализация j]. Присвоить j m. J3. [Нахождение отрицательного элемента]. Присвоить i X[j]. Если i > 0, то присвоить j i и повторить этот шаг. J4. [Обращение]. Присвоить X[j] X[-i], X[-i] m. J5. [Цикл по m]. Уменьшить m на 1, если m > 0, вернуться к шагу J2. В противном случае работа алгоритма заканчивается. Хотя алгоритм J невероятно изящен, анализ показал, что алгоритм I намного его превосходит. На самом деле оказывается, что среднее время выполнения алгоритма J, в сущности, пропорционально n ln(n), а среднее время выполнения алгоритма I пропорционально n. Запись перестановки в циклическом виде не единственна; состоящую из шести элементов перестановку (1 6 3) (4 5) (2) можно записать как (4 5) (3 1 6) (2) и другими способами, что вносит некоторую неопределенность. Поэтому важно рассмотреть каноническую форму циклического представления, которая является единственной. 10 Для получения канонической формы перестановки выполним следующие действия: 1. Вписать в перестановку пропущенные единичные циклы. 2. Внутри каждого цикла поместить на первое место наименьшее число. 3. Расположить циклы в порядке убывания их первых элементов. Пример. Для циклической перестановки (3 1 6) (5 4) найти каноническое представление. Решение. (3 1 6) (5 4) (2) (1 6 3) (4 5) (2) (4 5) (2) (1 6 3). Важным свойством канонической формы является то, что скобки можно опустить, а затем восстановить единственным образом. Для этого необходимо вставить левую скобку непосредственно перед каждым минимумом слева направо (т.е. перед тем элементом, перед которым нет меньшего элемента). А закрывающая скобка ставится перед открывающей кроме начала прохода, а также в конце формулы перестановки. Пример. Написать каноническую форму для перестановки (6 4 3) (5 1 2) Решение. 1. (6 4 3) (5 1 2) 2. (3 6 4) (1 2 5) 3. 3 6 4 1 2 5. Обратный процесс введения скобок циклов будет следующим (3 6 4) (1 2 5). 11 1.4. Рекурсия Объект называется рекурсивным, если он содержит сам себя или определен с помощью самого себя [2]. Очевидно, что мощность рекурсии связана с тем, что она позволяет определить бесконечное множество объектов с помощью конечного высказывания. Точно так же бесконечные вычисления можно описать с помощью конечной рекурсивной программы, даже если эта программа не содержит явных циклов. Лучше всего использовать рекурсивные алгоритмы в тех случаях, когда вычисляемая функция или обрабатываемая структура данных определены с помощью рекурсии. С процедурой принято связывать некоторое множество локальных объектов, то есть переменных, констант, процедур, которые определены локально внутри рекурсивной процедуры, а вне ее не существуют или не имеют смысла. Каждый раз, когда такая процедура вызывается рекурсивно, для нее создается новое множество локальных переменных. Хотя они имеют те же имена, что и соответствующие элементы множества локальных переменных, созданного при предыдущем обращении к этой же процедуре, их значения различны. При работе с рекурсивными процедурами следует помнить правило: идентификаторы всегда ссылаются на множество переменных, созданное последним. То же правило относится к параметрам процедуры. Подобно циклическим алгоритмам, рекурсивные процедуры могут привести к бесконечным вычислениям. Поэтому необходимо решить проблему окончания работы процедур. На практике нужно обязательно убедиться, что наибольшая глубина рекурсии не только конечна, но и достаточно мала. Дело в том, что при каждом рекурсивном вызове процедуры Р выделяется память для размещения ее переменных. Кроме 12 локальных переменных нужно еще сохранять текущее состояние вычислений. Следует избегать использования рекурсии - процедуры, когда имеется очевидное итеративное решение поставленной задачи с использованием простых рекуррентных отношений. Пример. Реализовать вычисление факториала n! рекурсивно. Решение оформим программой на языке С++. // Вычисление факториала n! рекурсивно #include "stdafx.h" #include <iostream> using namespace std; #include <conio.h> int fact(int i); int _tmain(int argc, _TCHAR* argv[]) { int n; cout<<"Enter n = cin>>n; "; cout<<" \n n! = "<<fact(n)<<"\n"; getch(); return 0; } int fact(int i) { if(i<=1) return 1; return i*fact(i-1); } 13 2. Информационные структуры 2.1. Последовательное и связанное распределение данных Последовательное распределение С вычислительной точки зрения простейшим представлением [3] конечной последовательности является список ее членов, расположенных по порядку в последовательных ячейках памяти. Ячейка d S1 S2 S3 l1 l2 l3 … Sn-1 Sn ln-1 ln памяти Адреса ячеек памяти l2 = l1 + d l3 = l1 + 2d … ln = l1 + (n-1) d Таким образом, представляется последовательное распределение данных S1, S2,…, Sn, для каждого элемента которой требуется объем памяти d, li — адрес ячейки. Такое представление имеет ряд преимуществ. Во-первых, оно легко осуществимо и требует небольших расходов памяти. Во-вторых, существует простое соотношение между номером элемента i и адресом ячейки, в которой хранится Si: li = l1 + (i-1) d. Это соотношение позволяет организовать прямой доступ к любому элементу последовательности. В-третьих, оно имеет широкий диапазон и включает в себя представление многомерных массивов. 14 Последовательное распределение имеет значительный недостаток. Оно становится неудобным, если требуется изменить последовательность путем включения новых и исключения имеющихся там элементов. Включение между Si и Si+1 нового элемента требует сдвига Si+1, … Sn вправо на одну позицию, а исключение соответственно — сдвиг влево. С точки зрения времени обработки такое передвижение элементов может оказаться дорогостоящим, и в случае динамических последовательностей лучше использовать технику связанного распределения. Связанное распределение Неудобство включения и исключения элементов при последовательном распределении происходит из-за того, что порядок следования элементов задается неявно требованием, чтобы смежные элементы последовательности находились в смежных ячейках памяти. Если это требование опустить, то можно выполнять операции включения и исключения без передвижения элементов последовательности. Конечно, необходимо сохранять информацию о способе упорядочения элементов, но можно это делать явным образом. В частности, при связанном распределении данных каждому Si поставлен в соответствие указатель Рi, отмечающий ячейку, в которой записаны Si+1 и Pi+1 (т.е. переход на следующую ячейку последовательности). Существует также указатель P0, который указывает на начальную ячейку последовательности, т.е. на ячейку с содержимым S1 и P1. (INFO) (LINK) P0=l1 S1 l1 P1=l2 S2 l2 P2=l3 … Sn-1 l n-1 Pn-1=ln Sn Pn= ln Рис. 2.1. Представление последовательности в виде связанного списка. 15 Каждый элемент списка состоит из поля INFO (содержащего элемент последовательности) и поля LINK (содержащего адрес следующего элемента) Здесь каждый узел состоит из двух полей. Под узлом понимается одно или несколько последовательных слов в памяти машины, выступающих как единое целое. — пустой или нулевой указатель. Рассмотрим более употребляемый способ задания списка. P0 S1 S2 … Sn-1 Sn Рис. 2.2. Способ задания списка Связанное представление данных облегчает операции включения и исключения элемента, если ячейка Si известна. Для этого лишь необходимо, изменить значение некоторых указателей. Например, чтобы исключить элемент S2 из последовательности, изображенной на рис. 2.2, необходимо положить LINK(l1)=LINK(l2). После этой операции элемента S2 в последовательности больше не будет. P0 S1 S2 S3 … Рис. 2.3. Исключение элемента из связанного списка 16 Чтобы в последовательность (рис. 2.2) включить новый элемент S5 после S1, необходимо воспроизвести новый элемент в некоторой ячейке l5 с INFO(l5) = S5 и LINK(l5) = LINK(l1) и присвоить LINK(l1) l5. P0 S1 S2 S3 … S5 l5 Рис. 2.4. Включение элемента в связанный список Также легко осуществляется сцепление последовательностей и разбиение последовательности на части. Использование связанного распределения предполагает существование некоторого механизма, который по мере надобности собирает ячейки (мусор), когда они освобождаются. С помощью связанного распределения достигается большая гибкость, но идет потеря скорости обращения к данным. При последовательном представлении фиксированное соотношение между i и ячейкой Si позволяет, в частности, иметь быстрый и прямой доступ к любому элементу последовательности. В связанном распределении такого соотношения не существует, и доступ ко всем элементам последовательности, кроме первого, не является прямым и эффективным. Кроме того, при связанном представлении приходится тратить память на Pi. В приложениях при выборе последовательного или связанного способа представления данных разумно сначала проанализировать типы операций, которые чаще других будут выполняться над данными и в соответствии с 17 этим выбрать способ организации данных. Если операции производятся преимущественно над случайными элементами, если осуществляется поиск отдельных элементов или производится упорядочение элементов, то лучше применять последовательное распределение. Связанное распределение предпочтительнее, если в значительной степени используются операции включения и/или исключения элементов, а также сцепления и/или разбиения последовательностей. Разновидности связанных списков Если Pn указывает на S1, то такая форма списка дает возможность достигнуть любой элемент списка (хотя и не прямо) из любого другого элемента последовательности. P0 S1 S2 … Sn-1 Sn Рис. 2.5. Циклический список Включение и исключение элементов здесь осуществляется так же, как и в нециклических списках, в то время как сцепление и разбиение реализуется более сложно. Большая гибкость при работе со списками достигается, если использовать дважды связанный список, когда каждый элемент Si последовательности вместо одного имеет 2 связанных с ним указателя, они указывают на элементы Si-1 и Si+1. В таком списке для любого элемента имеется мгновенный прямой доступ к предыдущему и последующему элементам, в связи с чем облегчаются такие 18 операции, как включение нового элемента перед Si и исключение Si без предварительного знания его предшественника. Если есть необходимость, дважды связанный список можно сделать циклическим. P0 S1 … S2 Sn-1 Sn Рис. 2.6. Дважды связанный список 2.2. Стеки и очереди В комбинаторных алгоритмах особую важность представляют две структуры данных, основанные на динамических последовательностях, т.е. последовательностях, которые изменяются вследствие включения новых и исключения имеющихся элементов. В обоих случаях операции включения и исключения производятся только на концах последовательности [3]. Стек есть последовательность, у которой все включения и исключения происходят только в одном конце, называемом вершиной стека. Соответственно другой конец последовательности называется основанием. Правило работы со стеком: «Первым пришел — последним ушел». Очередь — это последовательность, в которой все включения производятся на одном конце списка (в конце очереди), в то время как все исключения производятся на другом (в начале очереди). Правило работы с очередью: «Первым пришел — первым ушел». Стеки и очереди имеют важное значение в бухгалтерском деле. Введем следующие обозначения: 19 D X Добавить X к D. Если D — стек, то X помещается в вершину, если D — очередь, то Х добавляется в ее конец. X D Присвоить переменной Х значение верхнего элемента D, если D — стек, или начального элемента, если D — очередь. Этот элемент затем из D исключается. Последовательная реализация списков Для стеков это распределение весьма удобно. Все, что нам нужно, это переменная, например t, чтобы следить за вершиной стека S. Предположим, что для S отведены ячейки S(1), S(2), …, S(m), тогда пустой стек соответствует случаю t = 0. Операции включения и исключения в стек при этом следующие: S X tt+1 if t>m then overflow {переполнение} else s(t) X X S if t=0 then underflow {нехватка} else X S(t) t t-1 Здесь overflow означает переполнение или отсутствие в стеке места для добавления X, что означает ошибку. Underflow означает, что делается попытка удалить элемент из пустого стека, как правило, этот оператор означает окончание алгоритма. Последовательное распределение очереди сложнее потому, что она растет на одном конце и убывает на другом. Если ничего не предпринимать, то очередь начнет медленно двигаться и перейдет границу отведенных для нее ячеек. Чтобы это не произошло для 20 очереди Q будем использовать m ячеек Q(0), Q(1), …, Q(m-1), связанных круговым способом, и считаем, что Q(0) следует за Q(m-1). Если использовать переменную f в качестве указателя ячейки, расположенной непосредственно перед началом очереди, а переменную r в качестве указателя ее конца, то очередь будет состоять из элементов Q(f+1), Q(f+2), …, Q(r). Согласно этому определению, пустая очередь соответствует случаю r = f. Мы имеем: Q X r r+1 {ограничено величиной m} if r > m then overflow else Q(r) X X Q if r = f then underflow else X Q(f) f f+1 2.3. Организация связанного распределения на основе стеков и очередей Связанное распределение стека такое же легкое, как и последовательное распределение. Сохраним t в качестве указателя ячейки, являющейся вершиной стека, и используем поле LINK элемента стека для указания элемента, находящегося под данным элементом в стеке. Нижний элемент стека имеет пустой указатель в поле LINK, и t = означает пустой стек. Итак, имеем: SX new(l); {заводится ячейка памяти} INFO(l) X LINK(l) t {t — вершина стека} tl 21 XS if t = then underflow else X INFO(t) t LINK(t) Для очередей связанного распределения справедливо: QX new(l); INFO(l) X LINK(l) f {организация цикла} LINK(r) l rl XQ if f = then underflow else X INFO(f) LINK(r) LINK(f) 2.4. Деревья Конечное корневое дерево Т формально определяется как непустое конечное множество упорядоченных узлов, таких, что существует один выделенный узел, называемый корнем дерева, а оставшиеся узлы разбиты на m 0 поддеревьев Т1, Т2, …, Тm. Узлы, не имеющие поддеревьев, называют листьями, остальные узлы называются внутренними узлами [3]. 22 A B C G E F H I J K D Рис. 2.7. Дерево с 11 узлами Узлы D, E, F, H, J, K — листья, А — корень, остальные — внутренние. Посредством деревьев представляются иерархические структуры, поэтому они являются наиболее важными нелинейными структурами данных. В описании соотношений между узлами дерева будем использовать терминологию генеалогических деревьев. Т.е. говорим, что в дереве (или поддереве) все узлы, являются потомками его корня, и наоборот, корень есть предок всех своих потомков. Далее корень будем именовать отцом корней его поддеревьев, которые в свою очередь будем называть сыновьями корня. Например, на рис. 2.7 узел А является отцом узлов B, G, I; J и K — сыновья I, а C, E и F — братья и т.д. Все рассматриваемые деревья упорядочены, т.е. для них важен относительный порядок поддеревьев каждого узла. Т.е. деревья 23 A A B C C B считаются различными. Определим лес как упорядоченное множество деревьев. Важной разновидностью деревьев является класс бинарных деревьев. Бинарное дерево Т либо пустое, либо состоит из выделенного узла, называемого корнем, и двух бинарных поддеревьев: левого Tl и правого Tr. Бинарные деревья, вообще говоря, A не являются подмножеством A и B множества деревьев, B они полностью отличаются своей структурой, поскольку есть разные бинарные деревья. Различие между бинарным деревом и деревом состоит в том, что не бинарное дерево не может быть пустым, и каждый узел не бинарного дерева может иметь произвольное число поддеревьев. В то же время бинарное дерево может быть пустым и каждая его вершина может иметь 0, 1 или 2 поддерева, также существует различие между левым и правым поддеревьями. 24 2.5. Представление деревьев Почти все машинные представления деревьев основаны на связанном распределении данных. Каждый узел состоит из поля INFO и полей для указателей (рис. 2.8). На рис. 2.8 показаны связи от потомков к предку. Такая операция встречается редко, чаще требуется опуститься по дереву от предков к потомкам. Представление дерева (или леса) с использованием указателей, ведущих от предков к потомкам, довольно сложно из-за узла отца, который может иметь произвольно много сыновей. Другими словами, при таком представлении узлы должны различаться по размеру, что неудобно. A B C E G F I H J K D Рис. 2.8. Дерево, представленное с помощью узлов с полем INFO и указателем FATHER Один из путей обхода этой трудности состоит в построении соответствия между деревьями и бинарными деревьям, поскольку бинарные деревья легко представить узлами фиксированного размера. Каждый узел при этом имеет три поля: LEFT - указатель местоположения корня левого поддерева, INFO информационная часть содержимого узла и RIGHT - указатель местоположения корня правого поддерева. 25 A B G C D I E J Рис. 2.9. Представление бинарного дерева с помощью узлов с тремя полями LEFT, INFO, RIGHT Следующее представление дерева (или леса) в виде бинарного дерева будем называть естественным соответствием между деревьями и бинарными деревьями. Оно касается того, что в поле LEFT указывается самый левый сын данного узла, а в поле RIGHT — указывается следующий брат данного узла. Например, лес, показанный на рис. 2.10 преобразуется в бинарное дерево, показанное на рис. 2.11 следующим образом: A B M C D E G H F I J N L O Q P S R K Рис. 2.10. Лес 26 A B M C D E G H F I J N L O Q P S R K Рис. 2.11. Представление леса в виде бинарного дерева 2.6. Прохождение деревьев, леса Во многих приложениях необходимо пройти лес, заходя в узлы (т.е. обрабатывая их) некоторым систематическим образом. Посещение узла может быть связано с простой операцией, такой как печать содержимого узла, или со сложной, такой как вычисление функции. Единственное, что мы будет предполагать — это то, что при посещении узла структура дерева не меняется. Основными способами прохождения дерева являются: в глубину (сверху вниз); в ширину (горизонтальный порядок); снизу вверх; в симметричном порядке (для бинарных деревьев). При прохождении в глубину (прохождение в прямом порядке) узлы леса проходятся в соответствии со следующей рекурсивной процедурой: 1) посетить корень первого дерева; 27 2) пройти в глубину поддеревья первого дерева (если оно есть); 3) пройти в глубину оставшиеся деревья, если они есть. Например, для леса, показанного на рис. 2.10, узлы будут проходиться в следующем порядке: A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S. Для бинарных деревьев эта процедура упрощается и выглядит следующим образом: 1) посетить корень; 2) пройти в глубину левое поддерево; 3) пройти в глубину правое поддерево. Заметим, что прохождение леса в глубину в точности такое же, как и прохождение в глубину бинарного дерева, являющегося его представлением. Именно этот факт и делает указанное выше соответствие «естественным». Прохождение снизу вверх (обратный порядок или концевой порядок) осуществляется согласно следующей рекурсивной процедуре: 1) пройти снизу вверх поддеревья первого дерева, если они есть; 2) посетить корень первого дерева; 3) пройти снизу вверх оставшиеся деревья, если они есть. При этом порядке прохождения узлы леса рис. 2.10 проходятся в следующем порядке: B, D, E, F, C, G, J, I, K, L, H, A, O, P, N, R, Q, S, M. Рекурсивная процедура прохождения снизу вверх применительно к бинарным деревьям имеет следующий вид: 1) пройти снизу вверх левое поддерево; 2) пройти снизу вверх правое поддерево; 28 3) посетить корень. F, E, D, K, J, L, I, H, G, C, B, P, O, R, S, Q, N, M, A. Симметричный порядок для бинарных деревьев определятся рекурсивно следующим образом: 1) пройти в симметричном порядке левое поддерево; 2) посетить корень; 3) пройти в симметричном порядке правое поддерево. Такой способ прохождения известен как лексикографический порядок или внутренний прядок. Прохождение дерева снизу вверх эквивалентно прохождению в симметричном порядке бинарного дерева, соответствующего ему. Последний способ прохождения дерева — горизонтальный порядок или в ширину. При таком способе узлы дерева проходятся слева направо уровень за уровнем от корня вниз. Т.о. узлы леса рис. 2.10 будут проходиться в следующем порядке: A, M, B, C, G, H, N, Q, S, D, E, F, I, L, O, P, R, J, K. 29 A B C D E G F H I Прямой порядок: A B D G C E H I F Симметричный порядок: D G B A H E I C F Обратный порядок: G D B H I E F C A A B D C E F I G J H K L Прямой порядок: A B C E I F J D G H K L Симметричный порядок: E I C F J B G D K H L A Обратный порядок: I E J F C G K L H D B A Рис. 2.12. Прохождение бинарных деревьев 30 Многие алгоритмы и процессы, использующие бинарные деревья, распадаются на 2 фазы. В первой фазе строится бинарное дерево, а во второй оно проходится. В качестве примера рассмотрим применение бинарного дерева для сортировки чисел. Входной файл содержит список чисел, нужно распечатать числа в возрастающем порядке. По мере того, как мы считываем число, оно помещается в бинарное дерево. Все совпадающие значения также помещаются в дерево. При сравнении числа с содержимым узлов дерева выбирается тот узел, у которого еще не занята правая или левая ветвь, которая в свою очередь выбирается таким образом, что слева записывается меньшее число, чем число в узле, а справа записывается большее число, чем в узле или равное ему. Таким образом, если входной список равен 14, 15, 4, 9, 7, 18, 3, 5, 16, 4, 20, 17, 9, 14, 5, то будет построено бинарное дерево, показанное на рис. 2.13. 14 4 15 3 9 7 5 4 14 9 18 16 20 17 5 Рис. 2.13. Бинарное дерево, построенное для сортировки Такое бинарное дерево обладает тем свойством, что содержимое каждого узла в левом поддереве узла n меньше, чем содержимое узла n, а содержимое 31 каждого узла в правом поддереве узла n больше или равно содержимому узла n. Таким образом, если дерево проходится в симметричном порядке (левое поддерево, корень, правое поддерево), то числа печатаются в возрастающем порядке, т.е. 3, 4, 4, 5, 5, 7, 9, 14, 14, 15, 16, 17, 18, 20. Алгебраическая формула или логическая функция также могут быть представлены бинарным деревом. Например, на рис. 2.14 показано дерево, соответствующее арифметическому выражению a – b (c/d + e/f). _ a * b + / c / d e f Рис. 2.14. Представление формулы в виде дерева Используя дерево, легко выполнить переход от инфиксной (обычной) формы записи арифметического выражения (логической функции) к постфиксной (снизу вверх) и к префиксной (сверху вниз). Например, A+B — инфиксная запись (левое поддерево, корень, правое поддерево); A B + — постфиксная запись (левое поддерево, правое поддерево, корень), + A B — префиксная запись (корень, левое поддерево, правое поддерево). Итак, рис. 2.14 позволяет записать 32 - сверху вниз –a * b + / c d / e f (префиксная запись) - снизу вверх a b c d / e f / + * – (постфиксная запись) Такие формы представления позволяют оперировать без скобок, сохраняя правильность выполнения операций. Высота дерева Деревья используются не только как способ представления структур данных, но так же как средство для анализа поведения алгоритмов. В этой связи возникает потребность в количественных измерениях различных характеристик деревьев и, в частности, бинарных деревьев. Наиболее важные количественные характеристики деревьев связаны с уровнями узлов. Уровень p определяется рекурсивно и считается равным 0, если р — корень дерева Т; в противном случае уровень р определяется как 1+ уровень (FATHER(p)). Понятие уровня дает нам возможность просто определить высоту дерева Т: h(T ) max уровня( p) . pT Высота дерева есть максимальное число ребер, образующих путь от корня к листу дерева. Рис. 2.13. Бинарное дерево с длиной внешних путей, равной 21, а внутренних равной 9, h = 4 33 Прошитые бинарные деревья Для прохождения бинарного дерева часто используют стек. Рассмотрим другой подход. Предположим, что верхний элемент стека указывает на тот узел, левое поддерево которого мы только что прошли. Предположим, что узел с правым поддеревом содержит вместо пустого указателя указатель на узел, который находится на вершине стека в данной точке алгоритма. Тогда не будет больше нужды в стеке, поскольку узел будет непосредственно указывать на своего преемника при проходе в симметричном порядке. Такой указатель называется нитью. На рис. 2.14 показано бинарное дерево со связями-нитями, заменяющими пустые указатели в узлах с пустыми правыми поддеревьями. Нити нарисованы штриховыми линиями. Такие деревья называются бинарными деревьями, симметрично прошитыми справа. A B D C E G F I J H K L Рис. 2.14. Симметрично прошитое справа бинарное дерево 34 3. Алгоритмы на графах 3.1. Графы. Представления графов. Связность и расстояние Конечный граф G = (V, E) состоит из конечного множества вершин V = {V1, V2 … Vm} и конечного множества ребер E = {e1, e2 … en}. Каждому ребру соответствует пара вершин. Если ребро определяется e = (v, w), то говорят, что оно инцидентно вершинам v и w. Графы бывают ориентированными, если ребра — дуги и не ориентированными в противном случае [4]. Ребро называется петлей, если оно начинается и кончается в одной вершине. Два ребра считаются параллельными, если они имеют одну и ту же пару концевых вершин и, если они имеют одну и ту же ориентацию, в случае ориентированного графа. Граф называется простым, если он не имеет ни петель, ни параллельных ребер. Представление графа матрицей смежности 1 2 6 7 5 4 3 0 1 0 A 0 0 0 1 0 0 0 0 1 0 0 1 1 0 1 0 0 0 1 1 0 0 0 0 0 1 0 0 0 1 0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 0 Рис. 3.1. Ориентированный граф и его матрица смежности 35 У неориентированного графа матрица смежности симметричная, и для ее представления достаточно хранить только верхний треугольник, что экономит память. Матрица весов Граф, в котором ребру (i, j) сопоставлено число wij, называется взвешенным графом, а число wij называется весом ребра (i, j). В сетях связи или транспортных сетях эти веса представляют некоторые физические величины, такие как стоимость, расстояние, эффективность, емкость, надежность и т.д. Простой взвешенный граф может быть представлен своей матрицей весов W = [wij], где wij есть все ребра, соединяющие вершины i и j. Веса несуществующих ребер обычно полагают равными или 0 в зависимости от приложений. Список ребер Иногда более эффективно представлять ребра графа парами вершин. Это представление можно реализовать двумя массивами g = (g1, g2, …, gm) и h = (h1, h2, …, hm). Каждый элемент в массиве есть метка вершины, а i-е ребро графа выходит из вершины gi и входит в вершину hi. Например, ориентированный граф (рис. 3.1) будет представляться следующим образом: g = (1, 2, 2, 2, 2, 3, 3, 4, 5, 5, 5, 7, 7) h = (6, 1, 3, 4, 6, 4, 5, 5, 3, 6, 7, 1, 6) При этом легко представимы петли и параллельные ребра. 36 Структуры смежности В ориентированном графе вершина J называется последователем другой вершины Х, если существует ребро, направленное из Х в J. Вершина Х называется предшественником J. В случае неориентированного графа две вершины называются соседями, если между ними есть ребро. Граф может быть описан структурой смежности, т.е. списком всех последователей (соседей) каждой вершины. Для каждой вершины задается список всех ее последователей (соседей). Структура ориентированного графа (рис. 3.1) такова: 1: 6 2: 1, 3, 4, 6 3: 4, 5 4: 5 5: 3, 6, 7 6: 7: 1, 6 Структуру смежности удобно реализовать массивом линейно связанных списков, где каждый список содержит последователей некоторой вершины. Поле данных содержит метку одного из последователей и поле указателя, которое указывает на следующего последователя. Хранение списков в виде структуры смежности желательно для алгоритмов, в которых в графе добавляются или удаляются вершины. Связность и расстояние Говорят, что вершины x и y в графе смежны, если существует ребро, соединяющее их. Говорят, что два ребра смежны, если они имеют общую вершину. Путь — это последовательность смежных ребер (v1, v2), (v2, v3) …, в которой все вершины v1,v2, …, vk — различны, исключая возможно, случай v1 = vk. Записывается путь из v1 в vk как (v1, v2, v3, …, vk). Число ребер в пути 37 называется длиной пути. Расстояние от вершины a до вершины b определяется как длина кратчайшего пути (т.е. пути наименьшей длины) из a в b. Цикл — это путь, в котором первая и последняя вершины совпадают. Подграф графа G = (V, E) есть граф, вершины и ребра которого принадлежат G. Неориентированный граф G связан, если существует хотя бы один путь в G между каждой парой вершин vi и vj. Ориентированный граф G связан, если неориентированный граф, получающийся из G путем удаления ориентации ребер, является связным. Сильно связан тот ориентированный граф, если для каждой пары вершин vi и vj существует по крайней мере один ориентированный путь из vi в vj и по крайней мере один из vj в vi. 3.2. Остовные деревья Связанный неориентированный ациклический граф называется деревом. В связанном неориентированном графе G существует по крайней мере один путь между каждой парой вершин. Поэтому, если G — дерево, то между каждой парой вершин существует в точности один путь. Таким образом, дерево с n вершинами содержит в точности n-1 ребер и значит деревья есть минимально связанные графы. Удаление из дерева любого ребра превращает его в несвязанный граф, разрушая единственный путь между по крайней мере одной парой вершин. Особый интерес представляют остовные деревья графов, т.е. деревья, являющиеся подграфами графа G и содержащие все его вершины. Для построения остовного дерева (леса) неориентированного графа G, нужно последовательно просматривать ребра G, оставляя те, которые не образуют циклов с уже выбранными. 38 Во взвешенном графе часто бывает нужно определить остовное дерево (лес) с минимальным общим весом ребер, т.е. дерево (лес), у которого сумма весов всех его ребер минимальна. Такое дерево называется минимумом остовных деревьев. Процедура при этом такова. На каждом шаге выбирается новое ребро с наименьшим весом (наименьшее ребро), не образующее циклов с уже выбранными ребрами; этот процесс продолжается до тех пор, пока не будет выбрано |v|–1 ребер, образующих остовное дерево Т, где v — число узлов дерева. Этот процесс известен как жадный алгоритм. Другой способ получения минимума остовных деревьев, который не требует ни сортировки ребер, ни проверки на цикличность на каждом шаге, известен как алгоритм ближайшего соседа. Прохождение графа начинается с некоторой произвольной вершины а в заданном графе. Пусть (a, b) — ребро с наименьшим весом, инцидентное a; ребро (a, b) включается в дерево. Затем среди всех ребер, инцидентных либо а, либо b, выбирается ребро с наименьшим весом, и оно включается в частично построенное дерево. В результате в дерево добавляется новая вершина, например c. Повторяя процесс, ищем наименьшее ребро, соединяющее a, b или c с некоторой другой вершиной графа. Процесс продолжается до тех пор, пока все вершины из G не будут включены в дерево, т.е. пока дерево не станет остовным. 39 Примеры. В основе — граф рис. 3.1. 1. Жадный алгоритм 1 2 0,1 6 0,4 0,3 4 0,6 0,8 6 2 4 3 5 0,1 0, 2 0, 4 0,1 0,6 0,2 7 0,5 6 — начало: 0,7 0,5 3 0,4 5 7 1 0, 6 0, 3 = 1,7 0,7 2. Алгоритм ближайшего соседа 0,1 5 (6,2) (2,4) (4,3) (3,5) (5,7) (7,1) 0 ,1 0, 2 0, 4 0 ,1 0,6 0,3 = 1,7 Рис. 3.2. Алгоритмы на графах Остовные деревья необходимы для расщепления графов с целью исключения циклов. Они используются при организации работы коммутаторов коммуникационных сетей. 3.3. Поиск по графу в глубину. Топологическая сортировка Один из способов исследования всех вершин и ребер графа основан на процедуре прохождения графа методом поиска с возвращением, т.е. на исследовании графа в глубину. Рассмотрим это на примере неориентированного графа. Когда мы посещаем вершину vV, мы идем по одному из ребер (v, w), инцидентному v. Если вершина w уже пройдена (посещалась ранее), мы возвращаемся в v и выбираем другое ребро. Если вершина w еще не пройдена, то заходим в нее и применяем процесс рекурсивно к w. Если все ребра, инцидентные вершине v, уже просмотрены, мы идем назад к ребру (u, v), по которому пришли в v, и продолжаем исследование ребер, инцидентных u. Процесс заканчивается, когда все вершины пройдены и когда мы пытаемся вернуться назад из вершины, с которой началось исследование. 40 3 d 4 c 5 a: b, c, e e b a b: a, c, d 2 c: a, b, d, e 1 произвольно выбранная d: b, c e: a, c Рис. 3.3. Граф G. Структура смежности G. Пройденный граф G Как видно на рис. 3.3 поиск в глубину превращает исходный неориентированный граф G в ориентированный граф G , индуцируя на каждом ребре направление прохождения. Сплошные линии образуют ориентированное остовное дерево. Ориентированное остовное дерево, получаемое в результате поиска в глубину на простом связанном неориентированном графе, называется DFSдеревом. Ребра называются обратными, т.к. ведут от потомка к предку. Для реализации процедуры поиска в глубину нам необходимо отличать уже пройденные вершины от еще не пройденных. Этого можно достигнуть путем постепенной нумерации вершин числами по мере того, как мы в них попадаем. 3.4. Топологическая сортировка Простейшим случаем использования техники поиска в глубину на ориентированных графах является способ пометки вершины ациклического ориентированного графа G = (V, E) числами 1, 2, …, |V| так, что если из вершины i в вершину j идет ориентированное ребро, то i < j; такой способ 41 пометки называется топологической сортировкой вершин графа G. Например, вершины графа на рис. 3.4 (б) топологически отсортированы, а на рис. 3.4 (а) нет. 6 2 3 1 1 3 4 2 5 а) 7 5 4 б) Рис. 3.4. Ациклические ориентированные графы Топологическая сортировка может рассматриваться как процесс отыскания линейного порядка, в который может быть вложен данный частичный порядок. Граф при этом должен быть ациклическим. Топологическая сортировка полезна при анализе схем действия, где большой и сложный план представляется как ориентированный граф, вершины которого соответствуют целям плана, а ребра действиям. Топологическая сортировка дает порядок, в котором могут быть достигнуты цели. Топологическая сортировка начинается с отыскания вершины графа G = (V, E), из которой не выходят ребра, и присвоения этой вершине наибольшего номера, а именно |V|. Эта вершина удаляется из графа вместе с входящими в нее ребрами. ациклический, Поскольку мы оставшийся повторяем процесс ориентированный и присваиваем граф также следующий наибольший номер |V| – 1 вершине, из которой не выходят ребра и т. д. 42 3.5. Транзитивное замыкание. Поиск кратчайшего пути на графе Пусть задан граф G = (V, E), и нас интересует, какие вершины связаны между собой, не только напрямую, но и через посредство других вершин. Чтобы ответить на этот вопрос, воспользуемся матрицей смежности. 1 2 3 5 4 1 2 3 5 0 0 A 0 0 1 0 0 A* 0 0 1 1 0 0 1 0 0 1 0 0 0 1 1 1 1 1 0 0 1 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 4 Рис. 3.5. G* — транзитивное замыкание графа G 3.6. Поиск кратчайшего пути Предполагаем, что имеем простой взвешенный ориентированный граф. Несуществующие ребра будем считать ребрами с бесконечным весом. Сумму весов ребер пути будем называть весом или длиной этого пути. Алгоритм поиска кратчайшего пути следующий. Пусть путь ищется между вершинами s (начало) и f (конец). Мы начинаем проход из вершины s и просматриваем граф в ширину, помечая вершины значениями их расстояний от s. Процесс заканчивается, когда вершина f помечена значением ее расстояния от s и эта метка далее не меняется. Таким образом, в каждый момент времени работы алгоритма 43 некоторые вершины будут иметь окончательные метки, а некоторые — временные. Если учесть, что в начале алгоритма вершине s присваивается окончательная метка 0 (нулевое расстояние до самой себя), а каждой из остальных вершин присваивается метка , то на каждом шаге алгоритма g 10 a d 2 5 7 b f 1 8 4 6 3 2 7 5 c e одной из вершин присваивается окончательная метка и поиск продолжается дальше. 12 10 6 10 2 5 7 0 4 14 1 8 3 2 6 7 2 5 7 Рис. 3.6. Простой взвешенный граф Как видно из нижнего рисунка граф G из b в g имеет три кратчайших пути, длины которых равны 12. Это путь b, c, e, d, g ; b, c, a, d, g и b, c, d,g. На каждом шаге метки меняются следующим образом: 44 1. Каждой вершине j, не имеющей окончательной метки, присваивается новая временная метка с учетом расстояния от s до j. 2. Находится наименьшая из временных меток, она и становится окончательной меткой своей вершины. В случае равенства выбирается любая из них. Процесс продолжается до тех пор, пока вершине f не присваивается окончательная метка. 45 46 4. Генерирование случайных последовательностей 4.1. Генерирование равномерно распределенных случайных чисел «Каждый, кто использует арифметические генерирования методы случайных чисел, безусловно, грешит.» Джон фон Нейман Числа, которые выбираются случайным образом, находят множество полезных применений в моделировании. Это позволяет сделать модели похожими на реальные явления; выборочном методе. Часто невозможно исследовать все варианты, но случайная выборка обеспечивает понимание того, что можно назвать «типичным поведением»; численном анализе. Для решения сложных задач численного анализа была разработана техника, использующая случайные числа; компьютерном программировании. Случайные числа являются хорошим источником данных для тестирования эффективности компьютерных алгоритмов; принятии решений; эстетике; развлечениях. Равномерным распределением на конечном множестве чисел называется такое распределение, при котором любое из возможных чисел имеет одинаковую вероятность появления. Если не задано определенное 47 распределение на конечном множестве чисел, то принято считать его равномерным [8]. Джон фон Нейман первым предложил в 1946 г. идею возвести в квадрат предыдущее случайное число и выделить средние цифры. Например, необходимо получить 10-значное число, предыдущее равнялось 5772156649. Возводим в квадрат и получаем 33317792380594909200. Значит, следующим числом будет 7923805949. Претензии к такому подходу касаются вопроса: как может быть случайной последовательность, генерируемая таким образом, если каждое число полностью определяется предыдущим? Ответ состоит в том, что эта последовательность не случайна, но кажется такой. Последовательности, генерируемые детерминистическим путем, таким как этот, называются псевдослучайными или квази- случайными. Метод середины квадратов фактически является сравнительно бедным источником случайных чисел. Опасность состоит в коротком цикле (периоде) повторяющихся элементов. Рассмотрим методы генерирования последовательности случайных дробей [8], т.е. случайных действительных чисел Un, равномерно распределенных между нулем и единицей. Будем генерировать целое число Хn между нулем и некоторым числом U, тогда дробь Un = Хn / m будет принадлежать [0,1]. Обычно m выбирают равным размеру слова компьютера. Обозначим m = be, где b — основание системы счисления, используемой ЭВМ, а e — число разрядов машины. Поэтому Хn можно по традиции рассматривать как целое число, занимающее все компьютерное 48 слово с точкой в правом конце слова, а Un — как содержание того же слова с точкой в левом конце слова. Линейный конгруэнтный метод Выберем четыре числа [5]: m — модуль, m > 0; a — множитель, 0 a < m; c — приращение, 0 с < m; Х0 — начальное значение, 0 Х0 < m. Последовательность случайных чисел {Хn} можно получить, полагая Хn+1 = (a Хn + c) mod m, n 0 Эта последовательность называется (4.1) линейной конгруэнтной последовательностью. Например, для m = 10 и Х0 = a = c = 7 получим последовательность 7, 6, 9, 0, 7, 6, 9, 0, … В примере отражен факт, что конгруэнтная последовательность всегда образует петли, бесконечное т.е. число обязательно раз. Это существует свойство цикл, является повторяющийся общим для всех последовательностей вида Xn+1 = f(Xn), где f — функция, преобразующая конечное множество само в себя. Повторяющиеся циклы называют периодами. Задача генерации случайных последовательностей состоит в использовании относительно длинного периода, что связано с выбором довольно большого m, так как период не может иметь больше m элементов. 49 Другой фактор — скорость генерирования. При выборе множителя а следует принимать во внимание выводы следующей теоремы. Теорема А. Линейная конгруэнтная последовательность, определенная числами m, a, c и X0, имеет период длиной m тогда и только тогда, когда: числа с и m взаимно простые; b = a - 1 кратно p для каждого простого p, являющегося делителем m; b кратно 4, если m кратно 4. Пример. Для m = 120 назначить а и построить последовательность случайных чисел в интервале [0,1]. Решение. Выберем с = 7, a = 5, тогда при x0 = 1 получим следующие числа: X1 = 12, X2 = 67, X3 = 102, … Далее генерируем u0 = 1/120, u1 = 12/120, u2 = 67/120, u3 = 102/120, … В связи с довольно простым получением случайных чисел возникает такая общепринятая ошибочная идея — достаточно взять хороший генератор случайных чисел и немного его модифицировать для того, чтобы получить «еще более случайную» последовательность. Часто это не так. Например, известно, что формула (4.1) позволяет получить более или менее хорошие случайные числа. Может ли последовательность, полученная из Xn+1 = ((a Xn) mod (m + 1) + c) mod m быть еще более случайной? Ответ: новая последовательность является менее случайной с большей долей вероятности, т.е. мы попадаем в область генераторов типа Xn+1 = f(Xn) с выбранной на удачу функцией f. Но известно, что эти последовательности, вероятно, ведут себя намного хуже, чем последовательности, полученные при использовании более регулярных функций (4.1). Отметим, что период линейной конгруэнтной последовательности довольно длинный; когда m приблизительно равно 50 длине слова компьютера, обычно получаются периоды порядка 109 или больше и в типичных вычислениях используется лишь маленькая часть последовательности. Рассмотрим аддитивный генератор, предложенный Дж. Ж. Митчелом (G. J. Mitchell) и Д.Ф. Муром (D.P. Moore): Xn = (Xn-24 + Xn-55) mod m n 55, (4.2) где m — четное число, числа 24 и 55 — специальные числа, выбранные для того, чтобы определить такую последовательность, младшие значащие двоичные разряды (Xn mod 2) которой имеют длину периода 255 – 1. Далее рассмотрим реализацию этой последовательности с помощью циклической таблицы [5]. Алгоритм В. (Аддитивный генератор чисел). В ячейки памяти Y[1], Y[2], …, Y[55] записано множество значений X54 , X53 , …, X0 соответственно; j вначале равно 24, k равно 55. При реализации этого алгоритма на выходе последовательно получаем числа X55, X56, … В1. [Суммирование] (Если на выходе мы оказываемся в точке Xn, то Y[j] равно Xn-24, а Y[k] равно Xn-55). Y[k] (Y[k] + Y[j]) mod 2e. В2. [Продвижение] Уменьшим j и k на 1. Если j = 0, то присвоим j 24, иначе, если k = 0, присвоить k 55. Числа 24 и 55 в выражении (4.2) называются запаздыванием, а числа Xn, определенные в (4.2) — последовательностью Фибоначчи с запаздыванием. Вместо рассмотрения исключительно аддитивных или исключительно мультипликативных последовательностей можно построить достаточно хороший генератор случайных чисел, используя всевозможные линейные комбинации Xn-1, …, Xn-k для малых k. В этом случае наилучший результат 51 получается, когда модуль m является большим простым числом; например, m может быть выбрано так, чтобы оно было наибольшим простым числом, которое можно записать одним компьютерным словом. Формула для генерации может быть выбрана такой: Xn = (a1 Xn-1 + … + ak Xn-k) mod p (4.3) с периодом pk – 1. Здесь X0 , X1, …, Xk-1 могут быть выбранны произвольно, но не равные нулю одновременно. Обратимся к процессу генерирования случайных чисел «0» и «1» по формуле (4.3). Зададим произвольно ненулевое двоичное слово (1 0 1 1), то есть X1 = 1, X2 = 0, X3 = 1, X4 = 1, k примем равным 4, а p равным 2. Зададим также произвольно коэффициенты а1, а2, а3, а4 двоичным словом (0 0 1 1), то есть а1 = 0, а2 = 0, а3 = 1, а4 = 1. Перепишем (4.3) с учетом введенных величин, получим Xn = (a1 Xn-1 + a2 Xn-2 + a3 Xn-3 + a4 Xn-4) mod 2 Xn = (0 Xn-1 + 0 Xn-2 + 1 Xn-3 + 1 Xn-4) mod 2 Xn = (Xn-3 + Xn-4) mod 2 Откуда следует, что X5 = (X2 + X1) mod 2 = 1 X6 = (X3 + X2) mod 2 = 1; X7 = (X4 + X3) mod 2 = 0; X8 = (X5 + X4) mod 2 = 0; X9 = (X6 + X5) mod 2 = 0; X10 = (X7 + X6) mod 2 = 1; X11 = (X8 + X7) mod 2 = 0; X12 = (X9 + X8) mod 2 = 0; … и т.д. То есть сгенерированные числа (коды) имеют значения: 52 1011 (начальное число); 1100; 0100; 1101; 0111; 1000; 1001 … Период, т.е. число шагов, через которое начинается повтор, равняется 24 – 1 = 15. Для генерирования случайных чисел предложено множество других схем. Наиболее интересным из альтернативных методов является обратная конгруэнтная последовательность, предложенная Эйченауэром (Eichenauer) и Лехном (Lehn) [5]: X n 1 (aX n1 c) mod p (4.4) Здесь p — простое число, Xn принимает значения из множества {0, 1, …, p1, …}, а обращение определено как 0-1 = , -1 = 0. X-1 X 1 по модулю p. Другой важный класс методов связан с комбинацией генераторов случайных чисел. Допустим, имеются последовательности X0 , X1, … и Y0 , Y1, … случайных чисел, лежащих между 0 и m–1 и предпочтительно сгенерированных двумя различными методами. Тогда можно использовать одну случайную последовательность для изменения порядка элементов другой. Рассмотрим алгоритм рандомизации перемешиванием. Алгоритм М (рандомизации перемешиванием) Если заданы методы генерирования двух последовательностей {Xn}и {Yn}, этот алгоритм будет последовательно генерировать элементы «значительно более случайной» последовательности. Воспользуемся вспомогательной таблицей V[0], V[1], …, V[k-1], где k — некоторое число, для удобства обычно выбираемое приблизительно равным 100. Вначале V-таблица заполняется первыми k значениями последовательности X. 53 М1. [Генерирование X, Y]. Положим X и Y равными следующим членам последовательности {Xn}и {Yn}соответственно. М2. [Выбор j]. Присвоим j [kY/m], где m — модуль, используемый в последовательности{Yn}, т.е. j — случайная величина, определяемая Y, 0 j < k. M3. [Замена]. Выведем V[j], а затем присвоим V[j] X. Отметим, что методы перемешивания имеют «врожденный дефект». Они изменяют порядок следования генерируемых чисел, но не сами числа. Поэтому на практике возможно применение следующего подхода к генерации последовательности случайных чисел: Zn = (Xn - Yn) mod m, (4.5) где 0 Xn < m и 0 Yn < m m. Простейшим путем улучшения случайности при с = 0 является использование только каждого j-го элемента для некоторого малого j. Но лучшим способом, возможно, еще более простым, является применение (4.1) для получения скажем массива из 500 случайных чисел и использование только первых 55 чисел. После этого таким же методом генерируются следующие 55 чисел и т.д. 4.2. Основные критерии проверки случайных наблюдений Статистические критерии [5] отвечают на вопрос: достаточно ли случайной будет последовательность. Если критерии T1 , T2, …, Tn подтверждают, что последовательность ведет себя случайным образом, это еще не означает, вообще говоря, что проверка с помощью Tn+1–го критерия будет успешной. Однако каждая успешная проверка дает все больше и больше уверенности в случайности последовательности. Обычно к 54 последовательности применяется несколько статистических критериев, и если она удовлетворяет этим критериям, то последовательность считается случайной. Различают два вида критериев: эмпирические и теоретические. Эмпирические критерии основаны на использовании определенных статистик. Теоретические критерии реализованы с помощью рекуррентных правилах, теоретико-числовых методов, базирующихся на которые используются для образования последовательности. Рассмотрим критерий «хи - квадрат» (2-критерий) [5]. Он является основным методом, используемым в сочетании с другими критериями. Прежде чем рассматривать идею в целом, проанализируем частный пример применения 2-критерия к бросанию игральной кости. Используем 2 игральные кости, каждая из которых независимо допускает выпадение значений 1, 2, …, 6 с равной вероятностью. В следующей таблице дана вероятность получения определенной суммы S при одном бросании игральных костей: Значен 2 3 4 5 6 7 8 9 10 11 12 1 36 1 18 1 12 1 9 5 36 1 6 5 36 1 9 1 12 1 18 1 36 ие S Вероят ность PS Имеем всего 36 возможных результатов бросания. Рассмотрим результаты бросания. Например, величина 4 может быть получена тремя способами: 1 + 3, 2 + 2, 3 + 1. Это составляет 3 1 P4 . Аналогично 36 12 определяются оставшиеся PS. Если бросать игральную кость n раз, то в 55 среднем мы должны получить величину S примерно n PS раз, но это не совсем так. Например, при 144 бросаниях результаты следующие: Таблица 4.1 Величина S Наблюдаемое число, YS 2 3 4 5 6 7 8 9 10 11 12 2 4 10 12 22 29 21 15 14 9 6 4 8 12 16 20 24 20 16 12 8 4 Ожидаемое число, n pS Заметим, что во всех случаях наблюдаемое число отличалось от ожидаемого. Введем в рассмотрение число : (Y2 n p2 ) 2 (Y3 n p3 ) 2 (Y n p12 ) 2 ... 12 n p2 n p3 n p12 (4.6) Эта статистика называется статистикой «хи - квадрат» наблюдаемых значений Y2 … Y12 при бросании игральных костей. Для данных выше приведенной таблицы получим (2 4) 2 (4 8) 2 (6 4) 2 7 ... 7 . 4 8 4 48 Формулу (4.6) перепишем следующим образом: (YS n pS ) 2 . n pS S 1 k 56 (YS n pS ) 2 YS 2npS YS n 2 pS , учитывая факт, что Y1 + Y2 +…+ Yk = n, 2 2 p1 + p2 +…+ pk =1, можно записать 2 1 k YS n n S 1 pS Чтобы (4.7) 2-статистикой воспользоваться проводят несколько экспериментов, затем вычисляют числа . Далее используют известные таблицы 2-распределения имеющие вид: Таблица 4.2 p= p= p= p= p= 99% 95% 75% 50% 25% 2,558 3,940 6,737 9,342 12,55 p = 5% p = 1% 18,31 23,21 … 10 … Здесь p — процентные точки 2-распределения; v = k – 1 — число степеней свободы, что на единицу меньше, чем число категорий. (Интуитивно это означает, что Y1, Y2, …Yk, Y2, …Yk не являются полностью независимыми, т.к. формула (6.1) показывает, что Yk может быть вычислено, если Y1, …Yk-1 известны.) Предположим, что были сделаны три эксперимента с генерированием случайных последовательностей и получены 1 29 59 17 7 , 2 1 , 3 7 . 120 120 48 Сравнивая эти величины со значениями таблицы 2 при 10 степенях свободы, мы видим, что 1 гораздо больше; будет больше 23.21 только в 1% случаев. В связи с этим эксперимент 1 демонстрирует значительное отклонение от 57 случайного поведения. 2 показывает не лучшие свойства, так как результаты слишком близки к ожидаемым. Наконец, значение 3 находится между 25 и 50-процентной точками. Таким образом, наблюдение является удовлетворительно случайным по отношению к этому критерию. Отметим, таблица 4.2 — это только приближенные значения 2распределения, которое является предельным распределением случайной величины формулы (4.7). Поэтому табличные значения близки к реальным только при больших n. Насколько большими должны быть n? Эмпирическое правило гласит: нужно взять n настолько большим, чтобы все значения n pS были больше или равны пяти. 4.3. Эмпирические критерии Эмпирические критерии традиционно применяются для проверки, будет ли последовательность случайной. Каждый критерий применяется к последовательности {Un} = U0, U1, U2, … действительных чисел, которые предполагаются (4.8) независимыми и равномерно распределенными в интервале (0,1). Если критерии используются для целочисленных последовательностей, то используется вспомогательная последовательность {Yn} = Y0, Y1, Y2, …, (4.9) определенная правилом: Yn = [d Un ] (4.10) 58 Это последовательность целых чисел, распределенных в интервале (0, d– 1). Число d выбирается таким образом, чтобы сделать все Yi — целыми. Обычно d выбирается достаточно большим, чтобы критерий был значимым, но не настолько большим, чтобы критерий стал практически неприменим. Критерий равномерности (критерий частот) Первое требование, предъявляемое к последовательности (4.8) состоит в том, что ее члены — числа, равномерно распределенные между 0 и 1. Существуют 2 способа проверить это: а) использовать критерий Колмогорова-Смирнова [4] с F(X) = X, для 0 X 1, б) использовать 2-критерий. Для того, чтобы применить 2-критерий используем последовательность (8.10) вместо (8.8). Для каждого r, 0 r < d, подсчитаем число случаев, когда Yj = r. Затем применим 2-критерий принимая k = d и вероятности pS = 1/d для каждой категории. d — число, равное, например, 64 или 128 . Критерий серий Более общее требование к последовательности состоит в том, чтобы пары последовательных чисел были равномерно распределены независимым образом. В критерии серий подсчитывается число случаев, когда пара (Y2j, Y2j+1) = (q, r) для 0 j < n. Такая операция осуществляется для каждой пары целых чисел q и r, таких, что 0 q< r < d. Затем применяется 2 - критерий к этим k = d2 категориям, где 1/d2 — вероятность отнесения пары чисел к каждой из 59 категорий. При этом d выбирается таким образом, чтобы n>>k, например n 5d2. Критерий интервалов Этот критерий используется для проверки длины интервалов между появлением Uj на определенном отрезке. Если и — два действительных числа, таких, что 0< 1, то рассматриваются длины последовательностей Uj, Uj+1, …, Uj+r, в которых Uj+r лежит между и , а другие Us не лежат между этими числами. Эту последовательность, состоящую из r + 1 чисел, будем называть интервалом длины r. Покер-критерий (критерий разбиений) «Классический» покер-критерий рассматривает n групп по пять последовательных целых чисел {Y5j, Y5j+1, Y5j+2, Y5j+3, Y5j+4} для 0 j < n и проверяет, какие из следующих семи комбинаций соответствуют таким пятеркам чисел (порядок не имеет значения). Все числа разные: abcde Одна пара: aabcd Две пары: aabbc Три числа одного вида: Полный набор: aaabc aaabb Четыре числа одного вида: a a a a b Пять чисел одного вида: a a a a a 2 - критерий основан на подсчете числа пятерок в каждой категории. 60 В [5] можно ознакомиться с другими известными критериями, которые традиционно применяются для проверки, будет ли последовательность случайной. Возникает вопрос: «Зачем применять такое количество критериев?» Необходимость проверки последовательности с помощью критериев позволяет сделать более правильным вывод о случайности генерируемых чисел, а значит и точности моделирования процессов с их использованием. 4.4. Численные распределения При использовании случайных чисел часто требуются помимо равномерного распределения и другие виды распределений в зависимости от приложений. Например, необходимо моделировать случайное время ожидания между появлениями независимых событий, что достигается на основе применения показательного распределения случайных чисел. Иногда в случайных числах нет необходимости, но нужны случайные перестановки (случайное размещение n объектов) или случайное сочетание (случайный выбор k объектов из совокупности, содержащей n объектов). В принципе, любая из этих случайных величин может быть получена из равномерно распределенных случайных величин U0 , U1 , U2 , …, где Ui [0, 1]. Случайный выбор из ограниченного множества В общем случае случайные целые числа X, которые лежат между 0 и k–1, можно получить, умножив U на k и положив X = k U (ближайшее целое снизу). 61 В общем случае можно получить, если необходимо, различные веса для различных целых чисел. Предположим, что значение X = x1 должно быть получено с вероятностью p1, X = x2 — с вероятностью p2, … и X = xk — с вероятностью pk. Для этого генерируется равномерное число U и полагается x1 , если 0 U p1 ; x , если p U p p ; 1 1 2 X 2 xk , если p1 p 2 ... p k 1 U 1. (Заметим, что p1 p2 ... pk 1 .) Общие методы для непрерывных распределений В общем случае распределение действительных чисел может быть выражено в терминах «функции распределения» F(X), которая точно определяет вероятность того, что случайная величина X не превысит значения x: F(X) = Pr (X x) (4.11) Эта функция всегда монотонно возрастает от 0 до 1, т.е. F(x1) F(x2), если x1 x2 ; F(-) = 0, F(+) = 1. Если F(X) непрерывна и строго возрастающая (так что F(x1) < F(x2) , когда x1 < x2), то она принимает все значения между 0 и 1 и существует обратная функция F-1(y), такая, что для 0 < y < 1 Y = F(X), тогда и только тогда, когда X = F -1 (y). В большинстве случаев, когда F(X) непрерывна и строго возрастающая, можно вычислить случайную величину X с распределением F(X), полагая X = F -1 (U), где U — равномерно распределенная случайная 62 величина. Заметим, что если х1 — случайная величина, имеющая функцию распределения F1(Х), и если х2 — независимая от х1 случайная величина с функцией распределения F2(Х), то max (х1, х2) имеет распределение F1(Х) F2(Х), min (х1, х2) имеет распределение F1(Х) + F2(Х) – F1(Х) F2(Х). Любой алгоритм, использующий случайные числа на входе, дает на выходе случайные величины с некоторым распределением. Нормальное распределение Возможно, наиболее значительным неравномерным распределением является нормальное распределение с нулевым средним значением и среднеквадратичным отклонением, равным единице: F ( x) Рассмотрим алгоритм 1 x t e 2 2 /2 dt вычисления (4.12) двух независимых нормально распределенных случайных величин: X1 и X2. Алгоритм Р. (Метод полярных координат для нормальных случайных величин). Р1. [Получение равномерно распределенных случайных величин.] Сгенерировать две независимые случайные величины U1 и U2, равномерно распределенные между 0 и 1. Присвоить V1 2U1 – 1, V2 2U2 – 1. (Здесь V1 и V2 равномерно распределены между –1 и +1.) Р2. [Вычисление S.] Присвоить S V12 + V22. Р3. [Проверить S 1?] Если S 1 возврат к п. Р1. Р4. [Вычисление X1, X2.] Присвоить X1 и X2 следующие значения: 63 X 1 V1 2 ln s s , X 2 V2 2 ln s s . Это требуемые нормально распределенные случайные величины. Показательное распределение После равномерного и нормального распределений следующим важным распределением случайной величины является показательное распределение. Такое распределение появляется в ситуации «время поступления». Например, если одна заявка в среднем поступает каждые секунд, то время между двумя последовательными поступлениями имеет показательное распределение со средним, равным . Это распределение задается формулой F ( x) 1 e x / , x 0 . Метод логарифма. x F 1 ( y ) ln( 1 y ) . Очевидно, если (4.13) y F ( x) 1 e x / , то В [6] предлагается 1 – y рассматривать как равномерное распределение 1 – U, или просто U, что позволяет записать X ln U , где X — случайная величина, имеющая экспоненциальное распределение со средним, равным . 64 4.5. Что такое случайная последовательность Приведем утверждение Д.Н. Франклина: «Последовательность (4.8) случайна, если она обладает любыми свойствами, присущими всем бесчисленным последовательностям независимых выборок случайных равномерно распределенных величин». Отметим важные определения и замечания, необходимые в понимании, что такое случайная последовательность. Последовательность (4.8) равно распределена тогда и только тогда, когда Pr(U Un < v) = v – U для всех действительных чисел U, v при 0 U < v < 1. Последовательность может быть равно распределена, даже если она не случайна. Процедура получения простейшего генератора случайных чисел В начале целой переменной X присваивается некоторое значение X0. Эта величина X используется только для генерирования случайного числа. Как только потребуется новое случайное число, нужно положить X (aX + c) mod m и использовать новое значение X в качестве случайной величины. Необходимо тщательно выбирать X0, a, c, m и разумно использовать случайные числа согласно следующим принципам: 1. Начальное число X0 выбирается произвольно. Если программа используется несколько раз, и каждый раз требуются различные источники случайных чисел, то нужно присвоить X0 последнее значение X на 65 предыдущем прогоне или присвоить X0, если это удобно, текущую дату и время. 2. Число m должно быть большим, но меньше, чем 230. Удобно его брать равным размеру компьютерного слова. Вычисление (aX + c) mod m должно быть точным без округления ошибки. 3. Если m — степень 2, выбираем a таким, чтобы a mod 8 = 5. Одновременный выбор a и c даст гарантию, что генератор случайных чисел будет вырабатывать все m различных возможных значений X прежде, чем они начнут повторяться. 4. Множитель a предпочтительнее выбирать между .01m и .099m. 5. Значение c не существенно, когда a — хороший множитель, за исключением того, что c не должно иметь общего множителя с m, когда m — размер компьютерного слова. Таким образом, можно выбирать c = 1 или c = a. 6. Младшие значащие цифры (справа) X не очень случайны, так что решения, основанные на числе X, всегда должны опираться, главным образом, на старшие значащие цифры. Обычно лучше считать X случайной дробью X/m между 0 и 1. Далее, чтобы подсчитать случайное целое число между 0 и k–1, нужно умножить его на k и округлить результат. 7. Желательно генерировать не более m/1000 чисел, иначе последующие будут вести себя подобно предыдущим. Замечание. При работе с генераторами случайных чисел, необходимо по крайней мере дважды использовать совершенно разные источники случайных чисел, прежде чем получить решения. Это будет указывать на стабильность результатов, а также оградит от опасного доверия к генераторам со скрытыми недостатками. 66 Литература 1. Кнут Дональд Э. Искусство программирования. Т. 1. Основные алгоритмы: Уч. пос. — М.: Издательский дом «Вильямс», 2000. 720 с. 2. Вирт Н. Алгоритмы + структуры данных = программы. — М.: Мир, 1985. - 406 с. 3. Ленгсам Й., Огенстайн М., Тененбаум. Структуры данных для персональных ЭВМ. — М.: Мир, 1989. — 568 с. 4. Рейнгольд Э., Нивергельт Ю., Део Н. Комбинаторные алгоритмы. Теория и практика. — М.: Мир, 1980. — 480 с. 5. Кнут Дональд Э. Искусство программирования. Т. 2. Получисленные алгоритмы: Уч. пос. - М.: Издательский дом «Вильямс», 2000.-832 с. 6. Вендров А.М. CASE - технологии. Современные методы и средства проектирования информационных систем. - http://www.citforum.ru 67