Введение Theory is when you think you know something but it doesn't work. Practice is when something works but you don't know why. Usually we combine theory and practice: nothing works and we don't know why1. Engineering Jokes (http://www.hitequest.com/Articles/humor2.htm) Современные языки программирования, предоставляя мощные и дружественные инструменты для разработки приложений в различных предметных областях, невольно создают иллюзию легкости решения любых задач. Однако любой профессиональный программист почти наверняка хранит воспоминания о проекте, по ходу выполнения которого первоначальный оптимизм постепенно сменялся нервозностью, усталостью и, наконец, полным разочарованием. Причина этого чаще всего кроется в недооценке прикладной задачи, которая в самый неподходящий момент неожиданно демонстрировала свою сложность. Сложность прикладных задач многолика: она может проявиться не только в объеме кода, но и в структуре используемых данных, и в потребностях разработанного алгоритма в вычислительных ресурсах, а иногда – и во всех этих аспектах одновременно. В этой ситуации бессильны великолепные возможности современных инструментов программирования. В конце концов это только инструменты, тогда как суть проблемы – в самих задачах. Целью данного учебного пособия является развитие навыков формализации и объективной оценки сложности прикладных задач, а также разработки эффективных алгоритмов их решения. Материал пособия систематически преследует поставленную цель в предположении, что читатель уже владеет знаниями основ дискретной математики и навыками программирования (успешно освоил, по крайней мере, один объектно-ориентированный язык). Вопросы формализации прикладных задач и связанные с ними методы управления сложностью данных рассмотрены в разделе «Абстрактные типы данных». Методы оценки потребностей создаваемого алгоритма в вычислительных ресурсах рассматриваются в разделе «Теория сложности и оценка вычислительной эффективности алгоритмов». Теория – это когда Вы думаете, будто что-то знаете, но оно не работает. Практика – это когда что-то работает, но Вы не знаете, почему. Обычно мы сочетаем теорию и практику: ничего не работает и неизвестно, почему. 1 3 Иногда оказывается, что получение решения исходной задачи с помощью предложенного алгоритма влечет за собой неприемлемые вычислительные затраты, например решение задачи, потребность в которой возникает ежедневно, занимает несколько дней. В этой ситуации критически важно классифицировать задачу с точки зрения ее вычислительной сложности и найти рациональный вариант решения. Для этого можно либо корректно ограничить число рассматриваемых вариантов, либо разработать приближенный, но эффективный в вычислительном отношении алгоритм решения, либо переформулировать задачу таким образом, чтобы привести ее к одной или нескольким задачам, для которых уже разработаны эффективные алгоритмы. Эта логика и объединяет остальные разделы учебного пособия: «Исчерпывающий поиск оптимальных решений», «Приближенные методы оптимизации», «Методы оптимизации на графах и сетях». Не претендуя на полноту рассмотрения всех этих вопросов, автор пособия тем не менее стремился иллюстрировать их взаимосвязь и практическую ценность полученных результатов примерами программной реализации, которые представляют профессиональный интерес для инженеровсистемотехников. 1. Абстрактные типы данных - Ты когда-нибудь видела, как рисуют множество? - Множество чего? – спросила Алиса. - Ничего, - ответила Соня. - Просто множество. Л. Кэрролл. Приключения Алисы в стране чудес Согласно энциклопедическому словарю Кирилла и Мефодия, «абстракция – форма познания, основанная на мысленном выделении существенных свойств и связей предмета и отвлечении от других, частных его свойств и связей». Абстрагирование широко применяется при разработке программ как прием, который позволяет увеличить сложность разрабатываемых программных систем за счет использования готовых компонентов, существенным аспектом которых является выполняемая функция или присущее компоненту свойство, а второстепенным – способ реализации этой функции или свойства. Объектами абстрагирования в программировании являются код и данные. Абстракция кода – это подпрограмма, адресуемый фрагмент кода, выполняющий определенную функцию. Абстракция данных предполагает такую их организацию, при которой свойства данных доступны для просмотра или изменения посредством оговоренного набора операций, а внутренняя реализация этих свойств несущественна. 4 Таким образом, абстрактный тип данных (АТД) – это тип, определённый в терминах применимых к его экземплярам операций доступа. Абстракцию данных давно используют все языки программирования. Например, тип целых чисел есть абстракция, которая устанавливает, что экземпляры данных целого типа можно складывать, умножать и т.п. При этом несущественные детали, скажем, то, что отрицательные значения целых на самом деле представлены в двоичном дополнительном коде, скрыты от программиста. В данном разделе описан такой механизм реализации АТД, который позволяет самому программисту определять и использовать их в соответствии с потребностями решаемых задач. Прямую технологическую поддержку АТД обеспечивают только объектно-ориентированные языки. Инкапсуляция позволяет определять операции в контексте элементов данных, а полиморфное наследование позволяет использовать готовые АТД для определения новых. Понятие объекта соединяет в себе абстракции кода и данных и является их логическим развитием (рис. 1). Поэтому в дальнейшем изложении речь пойдет об объектной реализации АТД. Абстракция кода Подпрограмма Объекты Абстракция данных АТД Рис. 1. Использование абстракций в информатике 1.1.Термины и определения Как уже упоминалось, абстракция предполагает отделение существенных качеств предмета от несущественных. Множество существенных качеств АТД объединяется понятием интерфейса, множество несущественных – понятием реализации. Под интерфейсом АТД понимается набор операций, используемых для внешнего доступа к значениям его свойств. Широко распространена следующая классификация операций интерфейса: конструкторы создают экземпляр АТД в некотором предопределённом состоянии; деструкторы уничтожают экземпляр АТД; 5 селекторы операций обеспечивают доступ к свойствам АТД для чтения; модификаторы обеспечивают доступ к свойствам АТД для изменения их значений. Под реализацией АТД понимается набор свойств и операций, отражающих его внутреннюю организацию и не предназначенных для внешнего доступа. Компоненты реализации доступны только интерфейсным операциям. Принцип абстракции предполагает независимость интерфейса от реализации. Это позволяет выбирать способ реализации АТД в пределах неизменного интерфейса, руководствуясь при этом соображениями простоты и/или вычислительной эффективности. Яркий пример эффективного абстрагирования – современная бытовая техника: компьютеры, мобильные телефоны, телевизоры, видеомагнитофоны, стиральные машины, микроволновые печи и т.п. Сложность их реализации удачно экранируется простым интерфейсом, который к тому же достаточно унифицирован. В результате смена модели мобильного телефона (смена реализации) не создает стресса для его хозяина. 1.2. Линейные и нелинейные структуры данных Важный признак структуры данных – характер упорядоченности ее элементов. По этому признаку структуры подразделяются на линейные и нелинейные. Отличительный признак линейных структур – линейная упорядоченность доступа к их элементам. Для каждого элемента линейной структуры можно однозначно указать предшествующий и последующий. Нелинейные структуры таким свойством не обладают. К линейным относятся строковые структуры: – стек (рис. 2,а) - структура данных с дисциплиной доступа «последний вошел, первый вышел», или LIFO, следуя английской аббревиатуре, основное использование – поддержка рекурсии; – очередь (рис. 2,б) - структура данных с дисциплиной доступа «первый вошел, первый вышел», или FIFO, основное использование – поддержка стратегии поиска «в ширину», моделирование систем массового обслуживания; – дек (рис. 2,в) – двусторонняя очередь (double-ended queue, DEQ), или структура, позволяющая добавлять и извлекать элементы как в начале, так и в конце последовательности данных, основное использование – моделирование систем массового обслуживания с потерями. 6 а) б) в) Рис. 2. Строковые линейные структуры: а – стек; б – очередь; в – дек Кроме строковых структур, к классу линейных относятся также прямоугольные структуры (таблицы). К нелинейным структурам относятся графы, деревья и сети. Примеры реализации линейных и нелинейных структур будут рассмотрены далее. 1.3. Примеры реализации и использования АТД Линейные структуры данных: АТД «стек» Стек – это набор данных с дисциплиной обслуживания «последний пришел – первым обслужен». Это значит, что элемент данных, попавший в стек последним, извлекается из него первым. Интерфейс стека 2 должен включать операции, поддерживающие описанную дисциплину доступа, плюс операции создания и уничтожения экземпляров АТД: конструктор; модификаторы (положить элемент на вершину стека; взять элемент с вершины стека); селекторы состояния (пустой стек, полный стек); деструктор. Реализация должна определять внутренние детали, необходимые для обеспечения доступа: структуру данных, представляющую стек; указатель на вершину стека. В рамках данного интерфейса возможны следующие варианты внутренней реализации стека: АТД других строковых структур (очередь, дек) имеют похожие интерфейсы. Семантика операций доступа, естественно, иная. 2 7 массив3 (более простой в реализации); линейный список (обеспечивает динамический размер стека).Фрагменты списковой реализации АТД «стек» – декларация объекта и реализация метода Push (добавить элемент в стек) –приведены ниже. Обратите внимание на использование статуса видимости деклараций: public – для деклараций интерфейса, private – для деклараций реализации. В качестве самостоятельного упражнения рекомендуется завершить этот пример, выполнить альтернативную реализацию стека в виде массива и убедиться в тождественности интерфейсов для этих двух реализаций. Type NodePtr = ^Node; Node = Record Data : whatever; Next: NodePtr End; tStack = object private {вершина стека – это голова списка} Top : NodePtr; Public Constructor Init; … procedure Push(D: whatever); … end; procedure tStack.Push; var t : NodePtr; begin new(t); t^.Data:=D; t^.Next:=Top; Top:=t end; Наиболее типичное использование стека – сохранение контекста операций при организации рекурсии. Рассмотрим в качестве примера задачу построения и интерпретации польской инверсной записи арифметического выражения. Запись выражений, использующих бинарные операции, возможна в следующих формах: – инфиксной – знак операции стоит между операндами; – префиксной – знак операции стоит перед операндами; Динамический массив в Object Pascal также обеспечивает переменный размер стека. В Turbo Pascal возможно размещение массива в динамической памяти и ее перевыделение при необходимости 3 8 – постфиксной – знак операции стоит после операндов (польская инверсная запись). Преимущество двух последних форм записи заключается в том, что они не требуют скобок. Это существенно упрощает алгоритм вычисления выражений. Пусть a/b^c-d*e-a*c – традиционная инфиксная запись. Построение польской записи «вручную» может быть выполнено при помощи следующего алгоритма: 1. В исходном выражении расставить все скобки: {[(a/(b^c))–(d*e)]– a*c)} . 2. Вынести знак операции за соответствующую ей правую скобку: {[(a/(b^c) ) -(d*e) ] –(a*c) } 3. Убрать все скобки. 4. В результате получена польская инверсная запись abc^/de*-ac*-. Рассмотрим теперь алгоритм перевода инфиксной формы в польскую инверсную запись с использованием АТД «стек». Для этого определим вспомогательные таблицы приоритетов. а) Инфиксный приоритет (приоритет операций во входной строке). Операнд + - * / ^ ( ) Ограничитель Приоритет 1 1 2 2 3 4 0 0 Значения приоритета в этой таблице показывают, в какой последовательности будут выполняться операции. Нулевой приоритет закрывающей скобки отражает ее вспомогательный статус (она балансирует открывающую скобку). Нулевой приоритет ограничителя (символа, завершающего запись выражения, например, символа конца строки) показывает, что этот символ, хотя и встречается в позиции оператора4, но таковым не является. б) Стековый приоритет (приоритет операций, размещенных в стеке). Операнд + * / ^ ( ) Ограничитель Приоритет 1 1 2 2 3 0 0 Обозначения: RPN – выходная строка польской инверсной записи; 4 Предполагается синтаксически правильная запись выражения. 9 Lex – лексема (минимальный семантически значимый элемент входной строки: операция, операнд); Stk – стек операций. Псевдокод алгоритма {входной поток данных содержит инфиксную запись} RPN: = пустая строка; {инициализация стека} {положить «ограничитель» в стек} Repeat Читать Lex; Case Lex of Операнд: RPN: =RPN + Lex; ‘)’: операции из Stk до ‘(‘ перенести в RPN; Ограничитель: все операции из Stk добавить к RPN; Операция: взять из Stk все операции, стековый приоритет которых больше или равен инфиксному приоритету Lex, и добавить их к RPN; положить Lex в Stk End {of case} Until Lex=Ограничитель. Рекомендуем реализовать алгоритм перевода в польскую инверсную запись на любом языке программирования самостоятельно. Интерпретация польской инверсной записи также может быть выполнена с использованием АТД «стек». На этот раз во входном потоке находится польская запись арифметического выражения, а в выходном – его вычисленное значение. Алгоритм интерпретации прост: если во входном потоке распознан операнд, он помещается в стек вещественных значений, если операция – из стека извлекаются два операнда, попавшие туда последними, и к ним применяется эта операция. Необходимо только помнить, что первый операнд извлекается из стека последним, и соответственно интерпретировать операцию. Последовательные состояния стека в ходе интерпретации тестового выражения представлены на рис. 3 10 c b a 1 b^c a 2 e d a/(b^c) 3 d*c a/(b^c) 4 Рис. 3. Этапы интерпретации польской записи: 1 – прочитаны операнды a, b, с и операция «^»; 2 – интерпретирована операция «^»; 3 – прочитаны операнды d, e и операция «*»; 4 – интерпретирована операция «*» Ниже приведен псевдокод интерпретации. Он использует распространенные имена операций модификации стека: Push (положить) и Pop (взять). Repeat Чтение Lex; If Lex=операнд then Stk.Push(Lex) Else Begin Op2:=Stk.Pop; Op1:=Stk.Pop; Применить Lex к Op1 и Op2 end Until Lex = ограничитель; {Взять результат с вершины стека} Result:=Stk.Pop. В качестве примечания к рассмотренному примеру заметим, что алгоритм преобразования использует стек операций, то есть стек символов, а алгоритм интерпретации – стек операндов, то есть стек вещественных чисел. Несмотря на различие типов базовых элементов стека, интерфейсные операции остаются теми же самыми, поэтому для определения таких ресурсов мог бы быть полезен механизм шаблонов, имеющийся в языке С++ [4], и реализованная с его помощью библиотека стандартных шаблонов для АТД, в том числе и для стека. Нелинейные структуры данных: АТД «бинарное дерево» Дерево – это связный граф, количество ребер которого на единицу меньше количества вершин. У ориентированных деревьев различают корень (вершину с нулевой полустепенью захода), листья (вершины с нулевой степенью исхода) и внутренние (все прочие) вершины. Вершину a, связанную исходящей дугой с вершиной b, называют ро11 дительской по отношению к b, а b – дочерней по отношению к a. Особый интерес для алгоритмической практики представляют бинарные деревья, у которых каждая родительская вершина может иметь не более двух дочерних. Интерфейс АТД «бинарное дерево» на базовом уровне включает конструктор, деструктор, создание и уничтожение узла. Семантика остальных операций, например операции добавления узла, зависит от приложений, использующих бинарные деревья. Такие операции целесообразно определять на уровне объектов-потомков. Рассмотрим в качестве примера использование АТД «бинарное дерево» в задаче оптимального префиксного кодирования. Задача заключается в построении такой системы кодов переменной длины для слов входного потока, которая бы обеспечила минимальную стоимость хранения кодов в выходном потоке. Стоимость хранения определяется функционалом Q n fl i 1 i i , где n - количество различных слов во входном потоке; fi – частота i-го слова; li – длина кода i- го слова в выходном потоке. Очевидно, что минимум Q достигается в том случае, если большим частотам слов соответствуют меньшие длины кода. Алгоритм сжатия, основанный на построении оптимальных префиксных кодов, носит название алгоритма Хаффмана. Процедура построения вектора оптимальных длин кодов в рамках алгоритма Хаффмана, с которым желающие могут ознакомиться в [7, 8], приведена в приложении. Далее будем считать, что в результате анализа входного потока оптимальный вектор длин уже получен. АТД «бинарное дерево» используется на следующем этапе при построении самих префиксных кодов. Каждому из этих кодов однозначно соответствует путь из корня дерева в один из листьев, причем в код включается 0, если в путь включена дуга, связывающая родительскую вершину с левой дочерней, 1 – в противном случае. Код, построенный таким образом, является префиксным, то есть ни один из кодов не является префиксом другого. Построение дерева обеспечивает рекурсивная операция добавления пути, аргументы которой – адрес родительского узла t, длина кода L и кодируемое слово W. Операция обеспечивает вставку узла, соответствующего кодируемому слову, на глубину, равную длине кода. С этой целью алгоритм вставки руководствуется следующими приоритетами: сначала выполняется попытка построить путь в направлении левых дочерних вершин; если таковые не существуют, они предварительно создаются. Если эта попытка не удается, путь на заданную глубину строится в направлении правых дочерних вершин. Кодируемые слова сопоставляются с листьями бинарного дерева, следовательно, ни один 12 код не является префиксом другого. Кроме того, так как при построении дерева использовались оптимальные длины кодов, в результате построений имеем дерево оптимального префиксного кодирования. Ниже приведен фрагмент декларации интерфейса АТД «бинарное дерево» и рекурсивная реализация операции вставки пути. Операцию decode, выполняющую декодирование слова (в формате байта) путем идентификации пути из корня в соответствующий лист по правилу: если из кодированного потока считан «0», то спуск влево, «1» – спуск вправо, – предлагается реализовать самостоятельно в качества упражнения. Type pNode = ^tNode; {указатель на вершину дерева} tNode= record w:word; Left,Right:pNode; end; opt= object(tree) public root:pNode; private … function createnode:pNode; function inspath(var t: pNode; L:byte;W:word): boolean; function decode:byte; end; Function opt.inspath(var t: pNode; L:byte;w:word):boolean; var result:boolean; begin if L>0 then begin if t^.left=nil then begin t^.left:=createnode; inspath:=inspath(t^.left,L-1,w) end else begin result:=inspath(t^.left,L-1,w); if not result then if t^.right=nil then begin t^.right:=createnode; inspath:=inspath(t^.right,L-1,w) end else inspath:=inspath(t^.right,L-1,w) end end else if (t^.left=nil)and(t^.right=nil)and(t^.w=0) then begin t^.w:=w; 13 inspath:=true end else inspath:=false end; Задания и упражнения для самоконтроля 1. Дайте определение абстрактного типа данных. 2. Назовите основное преимущество абстракции данных. 3. Дайте определение интерфейса и реализации абстрактного типа данных. 4. Почему при разработке абстрактных типов данных важно соблюдать принцип независимости интерфейса от реализации? 5. В чем преимущества объектной реализации абстрактных типов данных? 6. Приведите классификацию операций интерфейса абстрактных типов данных, проиллюстрируйте ее примерами. 7. Объясните, в чем заключается преимущество способа доступа к свойствам абстрактного типа данных через операции интерфейса. Какие возможности языка Object Pascal поддерживают этот способ? 8. Какие преимущества с точки зрения абстракции данных дает использование шаблонных классов языка С++? 9. В чем отличие линейных и нелинейных структур данных? Приведите примеры. 10. Назовите основные области применения стека, очереди, дека, бинарного дерева. 11. Определите набор интерфейсных операций для абстрактного типа «таблица». 14 2. Теория сложности и оценка вычислительной эффективности алгоритмов Раньше, чем разрывать навозную кучу, надо оценить, сколько на это уйдет времени и какова вероятность того, что там есть жемчужина А.Б. Мигдал Теория вычислительной сложности (ТВС) изучает: - эффективность алгоритмов; - сложность, присущую задачам, которые имеют практическое или теоретическое значение. Знание основ ТВС позволяет избежать непроизводительных затрат на разработку алгоритмов точного решения задач, сложность которых доказана методами этой теории, и сосредоточиться на поиске альтернативных вариантов их решения (приближенное решение исходной задачи или изменение ее формулировки, в результате которого становится возможным эффективное точное решение). 2.1. Анализ вычислительной эффективности алгоритмов Анализ вычислительной эффективности алгоритма базируется на следующих основных принципах. 1. Оценка не должна быть жестко привязана к конкретной программно-аппаратной среде. Абсолютное время выполнения алгоритма не является адекватной оценкой его эффективности. Аппаратно независимой единицей оценки эффективности является количество выполняемых алгоритмом операций, например сравнений и перестановок в процессе сортировки. 2. Оценка должна строиться для худшего случая, в предположении, что будут выполнены все возможные операции алгоритма. Например, при построении оценки эффективности алгоритмов сортировки предполагается, что каждое сравнение пары ключей ведет за собой перестановку. Использование «верхней границы» – типично инженерный подход, благодаря которому погрешность оценки образует «запас прочности» алгоритма при решении реальных задач. Ориентация на худший случай (сколько элементов данных – столько операций) позволяет строить оценку эффективности как аналитическую функцию от числа элементов данных, обрабатываемых алгоритмом. 15 3. Оценка должна быть ориентирована на ситуацию обработки достаточно большого количества элементов данных. С одной стороны, именно такие задачи представляют прикладную ценность, с другой – эффективность алгоритмов слабо проявляется на задачах малой размерности. Это требование позволяет упростить оценку за счет отбрасывания второстепенных составляющих, что создает предпосылки для построения порядковых оценок, классифицирующих алгоритмы в соответствии с их сложностью. Практическая реализация эти принципов приводит к следующей методике анализа кода программы в целях построения оценки эффективности. 1. Исключаются из рассмотрения линейные фрагменты кода. 2. Исключаются циклы, количество повторения которых не зависит от числа элементов данных. 3. Цикл, выполняющий операции последовательно над всеми n элементами данных, имеет линейную оценку эффективности n. Цикл, при каждом выполнении которого количество рассматриваемых элементов n уменьшается в m раз, имеет оценку эффективности logmn. 4. Оценки эффективности последовательных циклов складываются. Оценки эффективности вложенных циклов перемножаются. 5. Отбрасываются младшие члены полученной аналитической зависимости и безразмерный коэффициент при оставшемся старшем члене. В результате получается порядковая оценка эффективности О(n). В целях иллюстрации методики рассмотрим примеры ее применения для построения порядковой оценки эффективности различных алгоритмов. Задача коммивояжера. Имеется n городов, расстояние между каждой парой которых известно. Найти цикл обхода городов, такой, что каждый город будет посещен ровно один раз и при этом суммарное расстояние будет минимальным. Решение задачи представляет собой последовательность из n чисел С = с1, c2,…,сn , где ci – «номер» города в цикле обхода. Необходимо найти такую перестановку на С, которая обеспечит минимальное суммарное расстояние. Псевдокод решения задачи имеет вид: Ввод(n); Min:=достаточно большое число; Для всех циклических перестановок на С {Найти длину маршрута L; Если L<Min то {Min:=L; запомнить маршрут}} Вывести решение. 16 Очевидно, что единственная составляющая оценки в данном случае соответствует циклу, количество выполнений которого равно числу перестановок - (n-1)!. Таким образом, порядковая оценка эффективности имеет вид O(n)=n!. Это значит, что если решение для 10 городов займет 1 секунду, то для двадцати оно составит 20!/10! 20 лет. Сортировка. Упорядочить n элементов массива в порядке возрастания значений. Фрагмент кода, который соответствует доминирующей составляющей оценки (циклы ввода и вывода последовательности записей имеют линейную эффективность) приведен ниже: For i:=1 to n-1 do For j:=i+1 to n do If A[j]>A[i] then swap(A[i], A[j]); {обмен} Порядковая оценка эффективности данного алгоритма O (n) = n2, так как количество повторений вложенного цикла равно n*(n-1)/2. Это означает, что при возрастании длины массива в 10 3 раз время сортировки возрастет в 106 раз. Бинарный поиск. Найти элемент с заданным значением ключа key в упорядоченной последовательности ключей длины n. i: =1; j: =n; k:=(i+j) div 2; While (A[k]<>key) or (i j) do Begin if A[k]> key then j:=k-1 else i: =k+1; k:=(i+j) div 2; end; Так как количество элементов данных, которые анализируются в цикле, уменьшается вдвое при каждом его повторном выполнении, оценка эффективности алгоритма бинарного поиска имеет вид O (n) =log2n. Это означает, что при увеличении длины последовательности в 2m раз время поиска увеличится только в m раз. Рассмотренные примеры позволяют выделить полиномиальные и субполиномиальные (например, с логарифмической эффективностью) алгоритмы, сложность которых приемлема для практической реализации, и экспоненциальные алгоритмы (включая алгоритмы с эффектом комбинаторного взрыва), реализация которых не представляет практического интереса. Расчеты, подтверждающие этот тезис для различных классов алгоритмов, сведены в табл. 1. 17 Таблица 1. Сравнительный анализ эффективности алгоритмов Порядковая оценка эффективности n n2 n3 2n Размерность задачи, которая может быть решена за одну единицу времени Компьютер базовой производительности Р N1 N2 N3 N4 Компьютер с производительностью 100Р 100N1 10N2 4.64N3 N4+6.64 Компьютер с производительностью 1000Р 1000N1 31.6N2 10N3 N4+9.97 На основании данных табл. 1 можно сделать следующий вывод. Если для полиномиальных алгоритмов повышение производительности компьютера имеет мультипликативный эффект, то для экспоненциальных – только аддитивный. Например, для алгоритма квадратичной эффективности прирост размерности задачи, вызванный 100кратным увеличением производительности, определится из решения уравнения 100n2=x2, где n и x – размерность задачи на компьютере старой и новой конфигураций соответственно, откуда x = 10*n. Для экспоненциального алгоритма в той же ситуации имеем: 100*2 n = 2x. В результате логарифмирования получим x = n + log2100 = n+6.64. Это означает, что увеличение производительности компьютера в 100 раз позволяет при прежних временных затратах увеличить количество элементов данных лишь на 7 единиц. 2.2. Оптимизационные и распознавательные задачи Как правило, на практике исходная постановка задачи является оптимизационной. Оптимизация предполагает нахождение решения, наилучшего в смысле заданного критерия. Однако теория вычислительной сложности рассматривает распознавательный вариант постановки задач. Распознавательная постановка задачи предполагает ответ на вопрос о существовании решения, удовлетворяющего заданному критерию. Таким образом, выходными данными распознавательной задачи является ответ «да/нет». Возможность обобщения результатов анализа задач в распознавательной постановке определяется следующим: – для каждой оптимизационной задачи существует распознавательный вариант; 18 – результат анализа сложности вычислительной задачи в распознавательной постановке справедлив и для оптимизационной задачи, то есть исходная задача не может быть намного сложнее распознавательной. Для подтверждения этих положений рассмотрим связь между оптимизационной и распознавательной постановками задачи коммивояжера. Предположим, что существует алгоритм А решения задачи в распознавательной постановке: существует ли цикл, длина которого короче заданной. Тогда псевдокод решения оптимизационной задачи может выглядеть следующим образом. Найти длину оптимального цикла Lmin; Для i:=1 до n-1 Для j:=1 до n { буфер:= Cij; Cij:= большое число; Если А(С, Lmin)<> «да» то Cij:=буфер } В результате выполнения алгоритма подмножество ребер, веса Сij которых остались неизменными, и образует оптимальный цикл. Циклы вызова подпрограммы А имеют полиномиальную сложность (n2), так что, если бы существовал полиномиальный алгоритм для А, оптимизационный алгоритм также остался бы в классе полиномиальных. 2.3. Недетерминированные алгоритмы Понятие «недетерминированный алгоритм» (НдА) является абстракцией. НдА отличается от обычного алгоритма наличием гипотетической инструкции безусловного одновременного перехода к меткам А и В: goto both М1, М2. Эта инструкция разделяет поток вычислений на два параллельных процесса, которые выполняются на одном и том же процессоре. Рассмотрим в качестве примера следующую задачу. Даны: целочисленные матрица А размером mn и вектор b длины n. Найти значение элементов двоичного вектора х длины n, такого, что Ах=b. Ниже приведен НдА ее решения: begin for i:=1 to n do goto both M1, M2 M1: xi:=0; goto M3; M2: xi:=1 M3: next i 19 If Ax=B then // распознавательная задача Res:=”Yes” Else Res:=“No”; end Схема распараллеливания процессов формирования элементов двоичного вектора х показана на рис. 4. Как видно из анализа кода и рисунка, наличие команды goto both М1, М2 позволяет решить данную задачу за полиномиальное время. x1=0 Шаг 1 x1=1 Шаг 2 x2=0 x2=1 x2=0 x2=1 Рис. 4. Фрагмент потока вычислений 2.4. P и NP - классы сложности распознавательных задач Теперь мы можем формально определить два класса распознавательных задач – P и NP. Практическая ценность этой классификации состоит в том, что для NP-полных задач полиномиального алгоритма точного решения не существует. Констатация NP – полноты задачи позволяет сосредоточиться на поиске других вариантов формулировки задачи или эффективных алгоритмов ее приближенного решения. Класс P (полиномиальный) включает распознавательные задачи, для которых существуют алгоритмы с полиномиальной эффективностью. Класс NP (недетерминировано-полиномиальный) включает распознавательные задачи, для которых существуют недетерминированные алгоритмы с полиномиальной эффективностью. Альтернативное определение: если вход NP-задачи имеет ответ «да», то существует полиномиальный алгоритм, доказывающий этот факт5. Например, ответ «да» в распознавательной постановке задачи коммивояжера предполагает, что нам известен порядок с1, с2, с3, … сn, с1 обхода вершин, такой, что сумма весов ребер (с1, с2), (с2, с3),…( сn, Чтобы показать принадлежность задачи к классу NP, нет необходимости определять способ получения правильного ответа. Достаточно показать, что сложность его проверки полиномиальна. 5 20 с1) меньше или равна наперед заданному L. Ясно, что проверка этого факта имеет полиномиальную сложность. Вместе с тем данная задача не принадлежит классу P, так как для ее решения до сих пор не найден полиномиальный алгоритм. Можно предположить, что между классами P и NP имеет место соотношение PNP (рис. 5). Действительно, пусть для задачи PA существует полиномиальный алгоритм А. Тогда распознавание положительного ответа будет выполнено за полиномиальное число шагов. Важный вопрос, однако, заключается в том, является ли P строгим подмножеством NP: PNP. Хотя формальное доказательство этого факта до сих пор не получено, существуют серьезные аргументы в его пользу. NP P Рис. 5. Соотношение между классами P и NP 2.5. Полиномиальная сводимость и трансформация Основными приемами, позволяющими установить относительную сложность различных задач, являются полиномиальная сводимость и трансформация Задача А полиномиально сводится к В при следующих условиях: 1. Существует алгоритм для А, который вызывает алгоритм для В как подпрограмму; 2. Каждый вызов В рассматривается как отдельный шаг; 3. Алгоритм А выполняется за полиномиальное время. Следствие: если А полиномиально сводится к В и существует полиномиальный алгоритм для В, то А также имеет полиномиальную эффективность. Полиномиальная трансформация – частный случай полиномиальной сводимости, когда алгоритм А конструирует вход для алгоритма В, выполняет его однократный вызов и интерпретацию результатов. Отношение полиномиальной трансформации транзитивно, что 21 легко можно показать, так как композиция двух полиномов также является полиномом. 2.6. Класс NP - полных задач Задача принадлежит к классу NP - полных при следующих условиях: 1. Она принадлежит к классу NP; 2. Все другие задачи из NP полиномиально трансформируются в нее. NP-полнота имеет важное практическое свойство. Если удается доказать существование полиномиального решения хотя бы для одной NP полной задачи, это автоматически доказывает полиномиальную разрешимость других. Таким образом, чтобы показать NP-полноту задачи из NP, достаточно показать, что некоторые другие задачи, NP-полнота которых известна, полиномиально трансформируется в нее. Задания и упражнения для самоконтроля 1. В чем заключается практическое значение теории вычислительной сложности? 2. Перечислите основные принципы построения порядковой оценки эффективности алгоритма. 3. Почему оценка эффективности алгоритма в терминах времени его выполнения некорректна? 4. В чем заключается смысл оценки эффективности по ее верхней границе? Приведите другие подобные примеры в инженерных расчетах. 5. Какое практическое следствие имеет расчет оценки эффективности по верхней границе? 6. В чем смысл требования «достаточно большого n»? Как оно влияет на упрощение оценки? 7. Опишите методику построения порядковой оценки эффективности алгоритма. Дайте пояснения, исходя из принципов формирования порядковой оценки. 8. Почему эта методика не учитывает вклада линейных участков алгоритма в оценку? 9. Почему циклы, количество повторений которых не зависит от числа элементов данных, также не участвуют в оценке? 10. Дайте определение оптимизационных и распознавательных задач. Почему теория вычислительной сложности распространяет результаты анализа распознавательных задач на оптимизационные? 11. Что такое недетерминированный алгоритм? В чем практическая ценность этого понятия? 22 12. Дайте определение P и NP – классам сложности распознавательных задач. 13. Объясните, почему любая задача класса P принадлежит также и к классу NP. 14. Дайте определение понятий полиномиальной сводимости и трансформации. В чем заключается их практическая ценность? 15. Что такое NP -полные задачи? Какие практические выводы следует сделать, если удалось показать NP-полноту Вашей задачи? 23 3. Исчерпывающий поиск оптимальных решений 3.1. Стратегии полного перебора …Остап уже принял решение. Он перебрал в голове все четыреста честных способов отъема денег… ни один из них не подходил к данной ситуации. И Остап придумал четыреста первый способ. И.Ильф, Е.Петров. «Золотой теленок» Если количество вариантов решения оптимизационной задачи конечно (пример – задачи оптимизации на графах), лучший вариант может быть гарантированно найден методом полного перебора. Полный перебор предполагает систематическое порождение и проверку на оптимальность всех вариантов решения. В процессе систематического порождения возможно использование стратегий поиска «в глубину» и «в ширину». Отличие этих стратегий заключается в том, что если при поиске в глубину в качестве очередного шага построения варианта решения рассматривается (а при порождении следующего варианта пересматривается) первый из возможных, то при поиске в ширину отыскиваются все возможные шаги продолжения, которые затем рассматриваются в порядке очереди. Проиллюстрируем различие этих стратегий примером нахождения вариантов пути между двумя заданными вершинами графа на рис. 6 (приоритет выбора продолжения определяется номером вершины). 2 0/0 3 3 1 8/2 4 1/1 1 4 6 9/6 1 2/3 6 5 3/4 5 4/7 2 6/8 5/11 6 4 7/5 6 10/9 5 11/12 2 13/13 12/14 6 14/10 6 6 Рис. 6. Иллюстрация стратегий полного перебора: а – граф поиска путей; б – дерево полного перебора и последовательность его построения поиском в «глубину/ширину» 24 Пусть требуется найти все пути между вершинами 3 и 6. Стратегия поиска в глубину будет порождать варианты в последовательности: (3-1-5-2-6); (3-1-5-6); (3-1-6); (3-4-1-5-2-6) и т.д. Можно заметить, что продолжение, выбранное при порождении текущего варианта решения последним, первым пересматривается при построении следующего. Так, вслед за продолжением 5-2 рассматривается продолжение 5-6. Эта особенность делает возможной организацию поиска в глубину на базе АТД «стек». Стратегия поиска в ширину на первом шаге приводит к формированию очереди претендентов, включающей вершины 1 и 4, из которой затем выбирается вершина 1. Так как смежная 1 вершина 6 есть вершина назначения, вариант (3-1-6) выбирается в качестве первого решения. Особенностью стратегии поиска в ширину является то, что первое решение содержит минимальное количество узлов. Далее очередь пополняется вершинами 4,5, и в соответствии с дисциплиной FIFO рассматриваются все продолжения пути из вершины 4 (вновь добавляется в очередь вершина 1), вершины 2 (5,6); таким образом, очередное решение имеет вид (3,1,5,2,6). Стратегия поиска в ширину может быть реализована с применением АТД «очередь». Рассмотрим эту реализацию в качестве примера. Интерфейс АТД «очередь» (объект Queue) представлен в приведенном ниже фрагменте операциями Put – «поставить» (в очередь) и Get – «взять» (из очереди) и селектором Empty – пустая очередь. Исходная, заключительная и промежуточная вершина пути Way обозначена переменными Source, Dest и Current. Finished обозначает флаг окончания поиска, Visited – функция, проверяющая принадлежность текущей вершины построенной части пути, M – матрица смежности вершин графа. Queue.Put(Source); WHILE not Queue.Empty DO BEGIN Current:=Queue.Get; Finished:= Current=Dest; j:=1; WHILE j<=N DO BEGIN IF (М[Current,j]<>0) AND NOT Visited(j, Way) THEN BEGIN Way[j]:=Current; Finished:=j=Dest; IF not Finished then Queue.Put(j) ELSE BEGIN i:=Dest; WHILE i<>Source DO 25 BEGIN Write(Way[i]:3);i:=Way[i] END; writeln END END; Inc(j) END END; Алгоритмы полного перебора обладают экспоненциальной эффективностью. Однако следует иметь в виду, что это не распространяется на используемые для реализации полного перебора стратегии поиска. Они вполне могут быть эффективными, если речь идет о нахождении ограниченного числа решений (чаще всего первого решения). 3.2. Метод ветвей и границ Описание метода Суть метода ветвей и границ заключается в ограничении объема полного перебора вариантов решения за счет разбиения множества возможных вариантов на подмножества (ветвление) и исключения из дальнейшего рассмотрения подмножеств, заведомо не содержащих оптимального решения (ограничение). То обстоятельство, что из рассмотрения сразу исключаются подмножества решений без дальнейшего анализа входящих в них вариантов, позволяет надеяться, что это усовершенствование полного перебора может способствовать повышению его вычислительной эффективности. Выбор способа разбиения на подмножества и оценки, обеспечивающей отсечение подмножеств, которые не содержат оптимального решения, составляет основную проблему метода. Пусть M={m1,…,mr} – конечное множество вариантов решения оптимизационной задачи, f:M – вещественнозначная функция на нем. Требуется найти минимум этой функции и вариант решения, на котором этот минимум достигается. Метод ветвей и границ применим только тогда, когда выполняются специфические дополнительные условия на множество M и минимизируемую на нем функцию. А именно, предположим, что имеется вещественнозначная оценочная функция φ на множестве подмножеств M со следующими двумя свойствами: 1. i : ({mi }) f (mi ) – значение φ на одноэлементном подмножестве {mi} равно значению критерия оптимальности для mi; 26 2. (U , V M ) & (U V ) (U ) (V ) – если подмножество V вариантов решения включает U, значение φ(U) не меньше φ(V). В этих условиях можно организовать перебор элементов множества M в целях минимизации функции φ следующим образом. Разобьем множество M на части и выберем ту из его частей Ω 1, на которой функция φ минимальна; затем разобьем на несколько частей множество Ω1 и выберем ту из его частей Ω2, на которой минимальна функция φ; затем разобьем Ω2 на несколько частей и выберем ту из них, где минимальна φ, и так далее, пока не придем к какому-либо одноэлементному множеству {mi}. Это одноэлементное множество называется рекордом. Очевидно, что рекорд не обязан доставлять минимум функции f. Однако возникает возможность сократить перебор при благоприятных обстоятельствах. Описанный выше процесс построения рекорда состоял из последовательных этапов, на каждом из которых фиксировалось несколько множеств и затем выбиралось одно из них. Пусть M1,…Ms – подмножества M, возникшие на предпоследнем этапе построения рекорда, и пусть множество M1 оказалось выбранным с помощью оценочной функции. При разбиении M1 возник рекорд {m1}. Согласно сказанному выше, ( M 1 ) ( M i ), i 1,..., s . Кроме того, по определе- (M1 ) ({m1}) f (m1 ) . Пусть f (m1 ) (M 2 ) . Тогда для любого mM2 будут верны неравенства f (m1 ) (M 2 ) f (m) ; это значит, что при полном нию оценочной функции переборе элементов из M элементы из M2 можно не рассматривать. Если же f (m1 ) (M 2 ) , то все элементы из M2 надо последовательно сравнить с найденным рекордом, и как только отыщется элемент, дающий меньшее значение оптимизируемой функции, надо им заменить рекорд и продолжить перебор. Последнее действие называется улучшением рекорда. Пример: задача о коммивояжере Рассмотрим применение метода ветвей и границ для решения задачи о коммивояжере (см. раздел 2). Этой задаче метод и обязан своим появлением. Описываемая реализация носит название «алгоритм Литтла» [1]. Формализуем задачу в терминах теории графов. Города будут вершинами графа, а дороги между городами – ориентированными ре27 брами (дугами) графа, на каждом из которых задана весовая функция: вес дуги – это длина соответствующей дороги. Путь, который требуется найти, – это легчайший гамильтонов цикл. Вес цикла – это сумма весов его дуг. Так как из любого города можно попасть в любой другой, орграф является полным. Очевидно, в полном орграфе циклы указанного выше типа есть. Если данный орграф не является полным, то его можно дополнить недостающими дугами с весом , считая, что – это «компьютерная бесконечность», максимально представимое число. Если в построенном полном орграфе в легчайший гамильтонов цикл входят дуги с весом , можно говорить, что в исходном графе «цикла Гамильтона» нет. Если же в полном орграфе легчайший гамильтонов цикл окажется конечным по весу, то он и будет искомым циклом в исходном графе. Отсюда следует, что задачу о коммивояжере достаточно решить для полных орграфов. Итак, пусть [ A, B ] – полный орграф, где A {a1 ,..., a p } – множество вершин; B – множество ребер; : B – весовая функция; C (cij ), i, j 1,..., p – весовая мат- рица графа, cij ( ai , a j ) , cii . Требуется найти цикл Гамильто- на минимального веса. Введем необходимые определения. Пусть имеется некоторая числовая матрица (рис. 7, а). 1 2 3 4 5 6 1 6 4 8 7 14 2 6 7 11 7 10 3 4 7 4 3 10 а) 4 8 11 4 5 11 5 7 7 3 5 7 6 14 10 10 11 7 - 1 2 3 4 5 6 1 0 1 4 4 7 2 2 4 7 4 3 3 0 1 0 0 3 4 4 5 1 2 4 5 3 1 0 1 0 6 10 4 7 7 4 - 4 6 3 4 3 7 1 2 3 4 5 6 1 01 1 4 4 7 2 01 2 5 2 1 2 б) 3 0 1 01 0 3 4 3 4 01 1 3 1 в) Рис. 7. К определению понятий приведения: а – исходная матрица; б – матрица (а) после приведения строк; в – матрица (б) после приведения столбцов. Привести строку (столбец) этой матрицы означает выделить в строке (столбце) минимальный элемент (его называют константой приведения) и вычесть его из всех элементов этой строки (столбца). Значение минимального элемента в этом случае называют константой приведения строки (столбца). 28 5 3 1 0 1 01 6 6 0 3 3 0 4 Очевидно, в результате в этой строке (столбце) на месте минимального элемента окажется нуль, а все остальные элементы будут неотрицательными. Привести матрицу по строкам (столбцам) означает привести все строки (столбцы) матрицы (рис. 7, б,в). Привести матрицу означает привести ее по строкам, а затем – по столбцам (рис. 7, в). Можно рассматривать содержимое строки i (столбца j) матрицы смежности как плату, взымаемую за выезд из города i (за въезд в город j). Так как операции приведения уменьшают эту плату одинаково для всех циклов, проходящих через данный город, и каждый цикл включает все города, они не нарушают упорядоченности маршрутов по длине. Поэтому минимальный маршрут в исходной матрице остается минимальным и в приведенной, однако его длина в приведенном варианте будет меньше, чем в исходном, на сумму констант приведения. На первом шаге решения задачи о коммивояжере с применением метода ветвей и границ фиксируем множество всех обходов коммивояжера. Поскольку граф полный, это множество заведомо не пусто. Сопоставим ему число, которое будет играть роль значения оценочной функции на этом множестве. Так как «физический» смысл константы приведения столбца (строки) – это минимальная плата за въезд (выезд) в город, и в каждый город необходимо въехать и выехать ровно один раз, сумму констант приведения данной матрицы можно рассматривать как нижнюю границу оценки длины цикла Гамильтона. Это означает, что кратчайший цикл не может иметь длину, меньшую этой суммы. В нашем примере сумма констант по строкам равна 27, по столбцам – 7, общая – 34. Если множество всех обходов коммивояжера обозначить через Г , то сумму констант приведения матрицы весов обозначим через φ(Г)=34. Приведенную матрицу следует запомнить; обозначим ее через M1. На втором шаге выберем в матрице M1 самый «тяжелый» нуль. Весом нуля матрицы называют сумму минимального элемента строки и минимального элемента столбца, содержащего нуль. Самый тяжелый нуль в матрице – нуль с максимальным весом. Оценка нулей состоит в следующем. Пусть нуль стоит в клетке (i,j); это значит, что из города i в город j мы едем бесплатно. Если мы отказываемся от использования дуги (i,j), то все равно должны уехать из i и приехать в j с минимальными затратами. Этим затратам соответствуют минимальные элементы строки i и столбца j. Веса нулей, вычисленные таким образом, показаны на рис. 7,в верхним индек29 сом (веса нулей, равные нулю, не проставлены). Выберем самый «тяжелый»; так как в примере все веса равны 1, возьмем первый из них – нуль (1,2). Фиксируем дугу графа (i,j) и разделим множество Г на две части: на часть Г{( i , j )} , состоящую из обходов, которые проходят че- Г{(i , j )} , состоящую из обходов, которые не рез дугу (i,j), и на часть проходят через дугу (i,j); ее нижняя граница – 34+1=35. Сопоставим множество Г{( i , j )} и матрицу M1,1, полученную из матрицы M1 заменой на число в клетке (j,i). Это запрет на перемещение по дуге (j,i). Так как выбрана дуга (i,j), (j,i) преждевременно замыкает цикл. Затем в полученной матрице вычеркнем строку i и столбец j, причем у оставшихся строк и столбцов сохраним их исходные номера. Наконец, приведем эту последнюю матрицу и запомним сумму констант приведения. Полученная приведенная матрица и будет матрицей M1,1 (рис. 8). Только что запомненную сумму констант приведения (в примере возможно приведение только по первому столбцу на 1) прибавим к φ(Г) и результат, обозначаемый в дальнейшем через ( Г{( i , j )} ) , сопоставим множеству Г{( i , j )} . Таким образом, ( Г{(1, 2)} ) 34 1 35 . Сопоставим множество ем оценки, обозначенное через что значение ( Г{(1,2)} ) Множество ( Г{(i , j )} ) . Г{(i , j )} со значени- Ранее было установлено, также равно 35. Г{(i , j )} тоже сопоставим с некоей матрицей M1,2. Для этого в матрице M1 заменим на число в клетке (i,j) и полученную в результате матрицу приведем. Сумму констант приведения запомним, а полученную матрицу обозначим через M1,2. 2 3 4 5 6 1 01 1 4 4 7 3 1 01 0 3 4 4 01 1 3 а) 5 1 0 1 01 6 0 3 3 0 - 2 3 4 5 6 1 01 03 3 3 6 3 1 01 0 3 4 4 01 1 3 б) 5 1 0 1 01 Рис. 8. Матрица М1,1: a – до приведения; б – после приведения 30 6 0 3 3 0 - Текущее состояние задачи можно представить в виде бинарного дерева, родительской вершине которого сопоставлено полное множество решений, а дочерним – его подмножества, одно из которых содержит (i,j), а другое нет. Следует выбрать между множествами Г{( i , j )} и Г{(i , j )} то, на котором минимальна функция φ. В примере оценки одинаковы, поэтому для определенности выберем Г{( i , j )} (в примере Г{(1, 2 )} ), тем самым зафиксировав данную дугу в маршруте, и продолжим описанные выше действия из соответствующей Г{(1, 2 )} левой дочерней вершины. Все дальнейшие рассуждения относятся к рассматриваемому примеру. Максимальный вес нуля в матрице М 1,1 на рис. 8,б (позиция (3,1)) равен 3. Поэтому оценка для правой нижней вершины, которой соответствует множество решений, не содержащих дугу (3,1), равна 35+3=38. Для оценки левой нижней вершины нужно вычеркнуть из матрицы на рис. 8,б строку 3 и столбец 1. Полученная в результате этих действий матрица показана на рис. 9,а. В эту матрицу нужно поставить запрет на дугу (2,3), так как оно преждевременно замыкает маршрут 3-1-2. Матрица также приводится по столбцу 4 на 1 (рис. 9,б). Таким образом, каждый маршрут, содержащий дуги (1,2) и (3,1), «весит» не менее 36 единиц. Самый тяжелый нуль в приведенной матрице рис. 9,б находится в позиции (6,5), следовательно, множество решений, которое не содержит эту дугу, имеет оценку нижней границы 36+3=39. Для получения оценки альтернативного варианта убираем строку 6 и столбец 5, ставим запрет на дугу (5,6). Эта матрица (рис. 9,в) неприводима, поэтому оценка нижней границы не увеличивается. Оценивая нули этой матрицы, получаем ветвление по включению дуги (2,6). Отрицательный вариант получает оценку 36+3=39, а для оценки положительного варианта вычеркиваем строку 2 и столбец 6 и добавляем запрет на дугу (5,3), замыкающую маршрут 3-1-2-6-5 . Таким образом, получена неприводимая матрица 2*2 (рис. 9,г), из которой в маршрут добавляются дуги (4,3) и (5,4). Цикл Гамильтона приобретает вид: 1-26-5-4-3-1 с оценкой 36 снизу, которая равна его точной стоимости. Он выделен на рис. 7,а жирным шрифтом. Дерево, иллюстрирующее процесс выбора этого варианта решения, показано на рис. 10. Далее, так как все подмножества с оценкой 36 и выше лучшего решения не содержат, соответствующие вершины вычеркиваются из дерева. Также вычеркиваются и родительские вершины, у которых вычеркнуты оба потомка. Подобное отсечение существенно сокращает полный перебор. 31 Выполненная аналогичным путем проверка подмножества Г{(i , j )} убеждает, что и эта ветвь не дает лучших решений. Рассмотренный на примере задачи коммивояжера метод ветвей и границ, хотя и приводит в частных случаях к существенному росту эффективности (на практике были решены задачи, где количество вершин достигало ста), тем не менее, если иметь в виду худший случай, не переводит задачу в класс полиномиально эффективных. 2 4 5 6 3 1 01 0 3 4 4 1 3 5 1 1 01 6 0 3 0 - 2 4 5 6 3 1 01 0 3 а) 4 5 3 1 1 02 2 03 б) 6 0 3 0 - 2 4 5 3 1 03 0 4 3 03 в) 6 03 3 0 Рис. 9. Продолжение решения задачи коммивояжера 34 35 38 3,1 1,2 39 6,5 36 36 все 36 3,1 36 2,6 35 1,2 36 6,5 39 2,6 4,3 5,4 Рис. 10. Дерево метода ветвей и границ 32 4 5 3 0 0 4 0 г) Задания и упражнения для самоконтроля 1. Дайте определения стратегий поиска «в глубину» и «в ширину». 2. Обоснуйте целесообразность использования абстрактных типов данных «стек» и «очередь» для организации поиска «в ширину» и «в глубину», соответственно. 3. Какова эффективность полного перебора? Приведите примеры формирования порядковой оценки. 4. В чем заключается основная идея метода ветвей и границ, обеспечивающая ограничение полного перебора? 5. Назовите требования, которым должна удовлетворять оценочная функция. 6. Какова роль оценочной функции в реализации метода ветвей и границ? 7. Что такое «рекорд»? В каком случае возможно его улучшение? 8. Докажите, что приведение матрицы не нарушает упорядоченности множества циклов Гамильтона по значению критерия. 9. Что такое константа приведения? Почему сумму этих констант можно использовать в качестве нижней границы значения оценочной функции? 10. В чем заключается правило разбиения множества циклов на два подмножества в алгоритме Литтла? 11. В чем содержательный смысл понятия «нуль максимального веса»? 12. Как соотносятся между собой значения оценочной функции для множества вариантов и для подмножеств этого множества? 13. Какие преобразования матрицы графа выполняются при разбиении множества вариантов на два подмножества? 14. Сформулируйте правила пересчета значений оценочной функции для дочерних вершин дерева поиска вариантов. 15. Сформулируйте правило отсечения подмножеств бесперспективных решений в алгоритме Литтла. 16. Опираясь на принципы построения порядковой оценки эффективности и правило отсечения в методе ветвей и границ, покажите, что применение этого метода не делает решение задачи полного перебора полиномиально эффективным. 33 4. Приближенные методы оптимизации: генетические алгоритмы Должен ли я отказаться от хорошего обеда лишь потому, что не понимаю процесса пищеварения? О. Хэвисайд В тех случаях, когда невозможно получить точное решение оптимизационной задачи (задача относится к классу труднорешаемых), а изменение ее постановки недопустимо, следует сосредоточиться на разработке алгоритмов приближенного решения. Одним из множества возможных вариантов приближенного решения являются генетические алгоритмы. Генетические алгоритмы – это типовая схема приближенного решения задач, основанная на моделировании процессов дарвиновской эволюции по принципу выживания сильнейшего. Объектом эволюции в генетических алгоритмах является популяция (множество вариантов решения оптимизационной задачи). Каждый из вариантов представлен в популяции цепочкой признаков (генов) – хромосомой. Набор закодированных в хромосоме признаков решения должен быть достаточным для его однозначной реконструкции. На множестве вариантов решения определена оценочная функция, значение которой для каждого из вариантов определяет степень его «жизнеспособности». Процесс эволюции моделируется при помощи так называемых генетических операторов. Оператор селекции обеспечивает случайный выбор вариантов решений из текущей популяции для последующего скрещивания. Оператор скрещивания (кроссовера) случайным образом комбинирует из генетического материала родительских решений, отобранных оператором селекции в текущей популяции, вариантыпотомки, которые войдут в новую популяцию. Оператор мутации вносит случайные изменения в генетический материал потомков. В условия ограниченного разнообразия генетического материала популяции (по соображениям эффективности алгоритма количество хромосом в популяции должно быть существенно меньше количества возможных вариантов решения) оператор мутации не позволяет генетическому алгоритму «застрять» в локальном экстремуме оценочной функции. Следует заметить, что все генетические операторы имеют вероятностую природу. А именно, случайным образом отбираются решения для скрещивания (хотя в вероятностном смысле предпочтение отдается лучшим), случайно выбирается объем порций генетического материала, заимствуемого у каждого из родителей при скрещивании, 34 мутационные изменения затрагивают случайный набор признаков у случайно выбранных решений. Подобная модель организации вычислений называется «мягкими вычислениями». Именно мягкие вычисления позволяют моделировать эволюционный процесс, обеспечивая при рациональном выборе генетических операторов вероятностное преимущество каждого последующего поколения перед предыдущим. В результате применения генетических операторов к решениям текущей популяции формируется новая популяция (новое поколение) решений. Итерационный процесс смены поколений и улучшения решений под действием окружающей среды, которую моделирует оценочная функция, завершается при выполнении некоторого заданного условия сходимости. В качестве такого условия может быть использован модуль разности последовательных значений оценочной функции (усредненных или лучших у двух смежных поколений), предельное число порождаемых поколений или время работы алгоритма. Возможность прекратить генетическое моделирование в любой момент и при этом тем не менее получить законченные решения (которые могут быть хуже или лучше в зависимости от объема затраченных ресурсов) – привлекательная черта генетических алгоритмов. Это позволяет получить решение настолько оптимальное, насколько большой объем вычислительных ресурсов был затрачен. Ниже приведен обобщенный псевдокод генетического алгоритма. Инициализация популяции; Repeat {цикл эволюции} Ранжировать популяцию по значению оценочной функции Repeat {формирование новой популяции} Селекция; Скрещивание; Замена худших особей новыми Until (выполнено нужное количество замен) Мутация Until (выполнено условие сходимости) Этот псевдокод следует рассматривать как настраиваемый шаблон, в котором параметрами настройки на конкретную задачу, отражающими ее специфику, являются: структура хромосомы, определяющая способ однозначного эффективного декодирования решения; размер популяции, определяющий оптимальное разнообразие генетического материала; оценочная функция, определяющая оптимальность решения; способ реализации и статистические характеристики генетических операторов, определяющие содержание и динамику эволюции; 35 условия сходимости эволюции. Рассмотрим возможности этой настройки на примере решения уже знакомой задачи коммивояжера. Так как решение однозначно определяется последовательностью посещения городов, в качестве генов хромосомы можно использовать имена (номера) этих городов (рис. 11). Размер популяции для 6 городов может быть выбран равным 20, но в любом случае не больше 100–200. Оценочная функция, очевидно, имеет вид суммы весов Wi ребер маршрута: ОФ N W . i 1 i C B А AC D E B F A D F E а) б) Рис. 11. Кодирование решения в задаче коммивояжера: а – возможный маршрут; б – код маршрута (хромосома) Оператор селекции должен реализовывать принцип: чем больше значение ОФ, тем больше шансов у решения быть выбранным для скрещивания. Этого эффекта можно достигнуть при помощи метода рулетки: – упорядочить решения в популяции по возрастанию значения оценочной функции; – сопоставить каждому решению сумму оценочных функций нарастающим итогом ΣОФi в упорядоченной популяции; – вычислить сумму значений оценочных функций всех решений в популяции ΣОФ; – генерировать случайное число rand{0.. ΣОФ -1}; – найти первое решение j, для которого ΣОФi > rand. Например: Номер особи в популяции 1 2 3 4 5 6 7 8 Пусть rand =25, ОФ 1 3 3 5 7 8 11 15 тогда j = 6. ΣОФi 1 4 7 12 19 27 38 53 Метод рулетки предполагает максимизацию критерия качества решения, что требует модификации оценочной функции: 36 ОФi:=max(ОФ)-Офi, где max(ОФ) – максимально возможное значение ОФ. К операторам скрещивания и мутации предъявляется следующее требование: они не должны порождать бессмысленных решений. Применительно к задаче коммивояжера это означает, что результатом применения любой из этих операций должен быть цикл Гамильтона. Отвечающий этому требованию оператор мутации реализует процедуру перемешивания подсписка, левая и правая граница которого определяется случайным образом. Например: Левая граница мутации>> <<Правая граница мутации До мутации A E D F C B A После мутации A E F C D B A Ниже приведен код процедуры мутации, которая реализует случайное перемешивание участка генов в хромосоме S. procedure Mutation(var S: tSolution); var i,j,L,H: 2..Order; Buf:tSolution; used:set of 2..Order-1; begin {определить подверженный мутации участок} L:=random(order-1)+2; {нижняя граница} H:=random(order-L)+L; {верхняя граница} Buf:=S; used:=[]; for i:=L to H do {выполнить перемешивание генов на участке} begin repeat j:=L+random(H-L+1); until not(j in used); used:=used+[j]; Buf[i]:=S[j]; end; S:=Buf end; Оператор скрещивания в задаче коммивояжера реализует следующий алгоритм комбинирования генетического материала родителей: 1. Генерировать случайную двоичную строку, длина которой на 2 меньше длины хромосомы (маску). 37 2. Поместить в дочернюю хромосому гены первого родителя, соответствующие единичным битам маски. 3. Составить список генов первого родителя, соответствующих нулевым битам маски. 4. Упорядочить список в соответствии с порядком этих генов у второго родителя. 5. Заполнить оставшиеся гены дочерней хромосомы элементами списка. Исходные данные и результат работы этого алгоритма иллюстрирует рис. 12. Хромосома первого родителя: A E D F C B A Хромосома второго родителя: A E F C D B A Маска скрещивания: 0 0 0 1 1 Участок скрещивания Дочерняя хромосома: A E F D C B A Рис. 12. Пример применения оператора скрещивания Ниже приведен код процедуры скрещивания: procedure CrossOver(Parent1,Parent2: tSolution;var Child: tSolution); var r: Buf: i,j: found: Word; tSolution; 1..order+1; Boolean; begin Child:=Parent1; {сформировать двоичную маску кроссовера} R:=random(1 shl order); for i:=2 to order do {выделить пункты маршрута первого родителя,} {соответствующие установленным битам маски} begin if R mod 2 = 0 then begin Buf[i]:=Child[i]; Child[i]:=0; end else Buf[i]:=0; R:=R shr 1 end; {выделить пункты маршрута второго родителя,} {соответствующие установленным битам маски} for i:=2 to order do begin 38 found:=false; j:=2; while not found and (j<=order) do begin found:=Parent2[i]=Buf[j]; {найден пункт маршрута 2-го родителя} {не вошедший в дочернее решение} inc(j); end; if not found then Parent2[i]:=0 end; j:=2; for i:=2 to order do if Child[i]=0 then begin while (j<=order)and(Parent2[j]=0) do inc(j); child[i]:=parent2[j]; Inc(j) end; end; Условием сходимости для задачи небольшой размерности можно выбрать величину скорости изменения оценочной функции. Для задач большой размерности лучше ограничивать число поколений или абсолютное время счета. Генетические алгоритмы получили достаточно широкое распространение благодаря универсальности этой схемы и относительно невысоким требованиям к вычислительным ресурсам. Однако их эффективность полностью определяется правильностью выбора настраиваемых параметров алгоритма. Кроме того, генетические алгоритмы по качеству решений, как правило, уступают специализированным эвристикам (например, алгоритм Литтла дает лучшие результаты для задачи коммивояжера). Задания и упражнения для самоконтроля 1. В чем отличие генетических алгоритмов от других методов оптимизации? 2. Что такое хромосома и какие требования к ней предъявляются? 3. Какова роль оценочной функции и какие требования к ней предъявляются? 4. Перечислите операторы, используемые генетическим алгоритмом. В чем их главная особенность? Какие требования к ним предъявляются? 5. В чем смысл операции «мутация»? 6. Опишите шаблонную схему применения генетических алгоритмов. 39 7. Перечислите настраиваемые параметры генетического алгоритма. 8. Какими соображениями следует руководствоваться при выборе размеров популяции? 9. Как можно сформулировать условие сходимости генетического алгоритма? 10. В чем Вы видите сильные и слабые стороны генетических алгоритмов? 40 5.Эффективные алгоритмы оптимизации на графах и сетях Ковыляющий по прямой дороге опередит бегущего, который сбился с пути. Ф.Бэкон Bis dat qui cito dat6. Латинская пословица Никакой другой математический формализм не имеет такого количества очевидных прикладных интерпретаций, как графы. Вероятно, объяснение этому следует искать в присущем языку графов сочетании простоты и изобразительной силы. Интерес к этим математическим объектам возник еще во времена Леонарда Эйлера и сохраняется до сих пор. В результате сформулировано и доказано множество теорем, разработана масса эффективных методов и алгоритмов решения задач на графах, основанных на этих теоремах – от задачи о кенигсбергских мостах до последних опытов применения теории графов в информатике, схемотехнике и других высокотехнологичных областях. Количество известных алгоритмов решения оптимизационных задач на графах не позволяет в рамках одного пособия дать сколько-нибудь полное представление о каждом из них, поэтому выбор рассматриваемых далее методов определяют два аспекта: их прикладная ценность для инженера-системотехника и возможность практического использования описанной ранее методики расчета вычислительной эффективности для анализа этих алгоритмов. В целях более глубокого знакомства с методами оптимизации на графах можно рекомендовать [3-8]. 5.1. Оптимальные деревья бинарного поиска Исхитрись-ка мне добыть То-Чаво-Не-Может-Быть! Запиши себе названье, Чтобы в спешке не забыть! Л.Филатов. Сказ про Федота-стрельца Задача поиска справочной информации по ключу весьма популярна в информатике. Поэтому эффективность алгоритмов, реализующих поиск, имеет важное практическое значение. Распространенным типом структур данных, которые используются для организации 6 Вдвойне дает тот, кто дает скоро. 41 поиска, являются бинарные деревья, построенные по следующим правилам: между ключами поиска и вершинами бинарного дерева существует взаимно-однозначное соответствие. значение ключа в корне больше значения любого ключа в левом поддереве и меньше любого ключа в правом поддереве. Пусть S – полное множество ключей, АS – упорядоченное по возрастанию множество ключей, присутствующих в словаре, а Р – вектор частот А: значение pi соответствует количеству обращений к ключу ai. Далее, пусть В = S\A –множество ключей, отсутствующих в словаре. Оно разбито на n+1 подмножеств. С множеством В связан вектор Q частот обращений к ключам подмножеств из В. Между ключами подмножеств из В и ключами из А имеется следующее соотношение: x ai 1 | i 0 x bi : ai x ai 1 | 0 i n . x ai | i n Графическая иллюстрация этого соотношения показана на рис. 13. b0 b1 a1 b2 bn-1 a2 an-1 bn an Рис. 13. Упорядочение ключей в В-дереве Определим расширенное дерево бинарного поиска (РДБП) таким образом, что ключам из А соответствуют внутренние вершины и корень, а подмножествам ключей из В – листья бинарного дерева (рис. 14). 12 7 5 b0 15 9 b1 13 b2 b3 b4 b5 16 b6 b7 Рис. 14. Пример расширенного дерева бинарного поиска 42 Отметим следующую особенность РДБП: степени всех его внутренних вершин равны двум. Очевидно, что успешный поиск завершается во внутренних вершинах РДБП, а количество сравнений при этом на 1 больше глубины вершины7. Неудачный поиск заканчивается в листьях РДБП, а количество сравнений при этом равно глубине листа. Пусть d(ai) – глубина вершины ai, d(bi) – глубина вершины bi. Тогда стоимость РДБП определится функционалом C pi d (ai ) 1 qi * d (bi ) . m m i 1 i 0 Здесь составляющая (1) pi d (ai ) 1 соответствует удачному m i 1 завершению поиска, а m qi d (bi ) – неудачному завершению. Например, i 0 для РБДП на рис. 15, а и заданных для него таблиц частот ( рис. 15, б) С=20*2+20*1+…+ 10*2+10*2 +5*3+…=240. i p q a2 a1 b0 a3 b1 b2 a4 b3 0 10 1 20 10 2 20 5 3 10 5 4 10 10 b4 а) б) Рис. 15. Пример расширенного дерева бинарного поиска: а – РДБП; б – векторы частот Итак, задача заключается в том, чтобы для заданных множеств A, B и векторов P и Q построить РДБП минимальной стоимости. Алгоритм ее решения, рассматриваемый далее, носит название алгоритма Глубина вершины – количество ребер на пути из корня в соответствующую вершину. 7 43 Гилберта-Мура [8] и основан на следующем свойстве оптимального РДБП (ОДБП). Если Тon является ОДБП для весов q0, p1,q2, …pn,qn, причём корень этого дерева имеет индекс k, то левое поддерево есть ОДБП для весов q0, p1,…pk-1, qk-1 , а правое – ОДБП для весов qk, pk+1,…pn,qn. Пусть Tij (0≤i<j≤n) – ОДБП для весов {qi, pi+1,qi+1, …pj,qj}, rij – корень Tij, cij – стоимость Tij. Определим вес дерева как сумму весов его узлов wij qi pi 1 ... p j q j . Тогда для Tii, состоящего из единственного узла, wii = qi, а cii=0. С учетом оптимальности поддеревьев ОДБП, а также принимая во внимание, что глубина вершины в Tij на единицу больше ее глубины в поддеревьях Ti,k-1, Tkj, запишем выражение для стоимости ОДБП через стоимость и веса его поддеревьев: ci ,i (ci , k 1 wi , k 1 ) (ckj wkj ) pk (ci , k 1 ckj ) wij . (2) Здесь (ci , k 1 wi , k 1 ) и (ckj wkj ) – стоимости левого и правого поддеревьев, приведенные к глубине корня, p k – вес корня. Слагаемые wi , k 1 , wkj отражают тот факт, что глубина любой вершины в Tij на единицу больше глубины этой вершины в поддереве. Полученная в результате перегруппировки составляющая стоимости (ci , k 1 ckj ) допускает минимизацию за счет выбора k, а wij – постоянная составляющая стоимости, она определяется суммой весов и не зависит от конфигурации дерева. Алгоритм Гилберта-Мура позволяет рассчитать оптимальные (минимизирующие переменную составляющую) значения k в порядке возрастания разности (j-i). for i:=0 to n do //выполнить инициализацию Begin W[i,i]:=Q[i]; C[i,i]:=0; r[i,i]:=B[i]; End; for l:=1 to n do Begin for i:=0 to n-l do Begin //определить верхнюю границу индексов j:=i+l; //определить постоянную составляющую стоимости W[i,j]:=W[i,j-1]+P[j]+Q[j]; //найти перебором k (i<k<=j), минимизирующее сумму //стоимостей левого и правого поддеревьев (ci,k-1+ckj) //вычислить стоимость дерева 44 C[i,j]:=W[i,j]+C[i,k-1]+C[k,j]; //присвоить значение корня r[i,j]:=A[k]; End; End; Рассмотрим пример работы алгоритма Гилберта-Мура. Пусть A = {a1, a2, a3, a4}; P = {2, 1, 3, 1}; B = {b0, b1, b2 ,b3, b4}; Q = {1, 2, 3, 2, 1}. Результаты заполнения матриц весов, стоимостей и корней показаны на рис.16. Детализация расчетов для дерева T02 приведена ниже. Эти расчеты показывают, что оптимальным корнем для T02 будет вершина k=2, так как она доставляет минимум функционалу C02. W02 W01 p2 q2 5 1 3 9; C02 W02 C00 C12 9 0 6 15; C02 W02 C01 C22 9 5 0 14. i l=j-i 4 3 3 3 Рис. 16. Результат работы алгоритма Гилберта-Мура Вычислив rij, можно построить дерево Т0n, используя следующую процедуру: r0n – корень дерева T0n. Если r0n = ak, то ak имеет в качестве левой дочерней вершины r0,k-1, а в качестве правой – rkn. 45 Рассмотрим внутреннюю вершину am. Если rij = am, то am имеет в качестве левой дочерней вершины ri,m-1, а в качестве правой – rmj. ОДБП, полученное в результате применения этой процедуры для контрольного примера, показано на рис.16 штриховой линией. Анализ структуры алгоритма построения таблицы на рис.16 показывает, что порядковая оценка его эффективности – O(n3). Квадратичную составляющую определяют два явно определенных в псевдокоде вложенных цикла, а линейный множитель дает переборный поиск оптимального значения k. Алгоритм построения ОДБП по таблице сводится к линейному просмотру списка длины n. таким образом, в соответствии с рассмотренной в разделе 2 методикой общая оценка эффективности остается O(n3). Задания и упражнения для самоконтроля 1. Оцените диапазон оценок эффективности поиска с применением бинарного дерева. Какой характеристикой бинарного дерева он определяется? 2. Какие характеристики учитываются в оценке стоимости, представленной формулой (1)? 3. Какая из двух составляющих оценки стоимости в формуле (2) допускает минимизацию? За счет чего эта минимизация достигается? 4. Покажите на псевдокоде алгоритма Гилберта-Мура циклы, определяющие оценку его эффективности. Обоснуйте эту оценку. 5.2. Алгоритм Дейкстры поиска кратчайшего пути Кто знает, что открыл бы Колумб, не попадись ему на пути Америка! Станислав Ежи Лец Практическая значимость задачи построения кратчайшего пути очевидна и не нуждается в глубоких обоснованиях. Достаточно вспомнить, что издержки транспортных перевозок напрямую связаны с расстоянием. Графовая интерпретация этой задачи также абсолютно естественна. Пусть задан граф (V,E), вершины которого сопоставлены с пунктами маршрута, а дуги – со связывающими их дорогами. Каждой дуге (x,y) поставим в соответствие число w(x,y), которое назовем весом (длиной) ребра. Если какое-либо ребро в графе отсутствует, будем считать, что w(x,y)=. В таком случае граф можно считать полносвязным. Определим вес пути между парой заданных вершин s и t как сумму весов входящих в него ребер. Путь, имеющий минимальный вес, назовем кратчайшим. Рассмотрим алгоритм Дейкстры [6], разрабо46 танный для решения данной задачи в 1959 г. и до сих пор считающийся одним из самых эффективных. Этот алгоритм использует понятие окрашивания вершин и ребер графа: вершина или ребро графа называются окрашенными, если они включены в перспективные варианты построения пути. Окрашивание вершины (ребра) происходит однократно по ходу построения кратчайшего пути. Рассмотрим описание алгоритма Дейкстры. Шаг 1: инициализация. Все вершины и ребра не окрашены, длина кратчайшего пути из s в x d(s)=0, для любого xV\{s} d(x)=. Окрасим вершину s. Положим y=s, где y – последняя из окрашенных вершин на каждом шаге. Шаг 2. Для каждой неокрашенной вершины x d(x) пересчитывается следующим образом: d(x)=min{d(x); d(y)+w(y,x)}. Если для всех неокрашенных х d(x)=, то пути из s в t нет. Иначе окрашивается вершина, дла которой значение d(x) минимально, и ведущее в нее ребро, y полагается равным х. Шаг 3. Если у=t , то кратчайший путь из s в t найден. Его образует цепочка окрашенных ребер из s в t. Иначе перейти к выполнению Шага 2. Отметим, что так как в каждую окрашенную вершину входит ровно одно ребро, а окрашиваться может только дуга, у которой окрашена только одна инцидентная ей вершина, окрашенный подграф представляет собой дерево с вершиной в s (дерево кратчайших путей). Когда в процессе наращивания этого дерева (шаг 2) будет окрашена вершина t, процедура останавливается. Чтобы найти кратчайшие пути из s в любую другую вершину, необходимо окрасить все вершины графа. Рассмотрим пример. Пусть требуется найти кратчайший путь из s в t на графе, показанном на рис. 17,а. Опуская очевидные действия инициализации, рассмотрим итерационный процесс, образованный шагами 2–3. На каждом шаге окрашивается только одна вершина; эти вершины выделены на рис. 17,б затенением и в последующих итерациях не участвуют. За исключением третьей итерации, ребро, соединяющее построенную часть дерева кратчайших путей и вновь окрашиваемую вершину, определяется однозначно. На третьей итерации возможно окрашивание любого из ребер: (c,d) или (a,d), но не обоих сразу. Результирующее дерево кратчайших путей выделено на рис. 17,а жирными линиями. Итерации 3 b а 2 1 2 3 4 5 4 7 d(a) 4 4 2 s t 3 с 3 d 2 47 d(b) d(c) d(d) d(t) а) 7 3 7 6 7 6 7 8 8 б) Рис. 17. Пример работы алгоритм Дейкстры Эффективность алгоритма Дейкстры определяется из следующих соображений. Итерационный цикл, образованный шагами 2–3, выполняется порядка n раз, где n – количество вершин графа; неявно присутствующий на шаге 2 цикл вычисления d(x) для окраски очередной вершины и ребра также выполняется порядка n раз. Поскольку второй цикл вложен в первый, общая оценка эффективности определяется как произведение частных оценок. Таким образом, алгоритм Дейкстры имеет эффективность порядка O(n2). Задания и упражнения для самоконтроля 1. Докажите, что алгоритм окрашивания формирует дерево с корнем в исходной вершине. 2. Какие изменения следует внести в алгоритм Дейкстры, чтобы обеспечить поиск путей между заданной вершиной и всеми остальными вершинами графа? 3. Как эти изменения повлияют на оценку эффективности? 5.3. Задача об остове Стягивающее дерево, или остов графа G=(V,E) – это дерево, покрывающее все его вершины. Ребра графа, вошедшие в остов, называются ветвями, а не вошедшие в него – хордами. Отсюда следует, что количество ребер остова на единицу меньше количества его вершин: Е1=V-1, где |.| – мощность множества. На рис. 18 показан пример графа (а) и вариантов его остова (б,в). Как видно, в общем случае остов может быть построен неединственным способом. Задача построения остова имеет важное прикладное значение, например, для автоматизации построения контурных уравнений электрических цепей. Каждая хорда замыкает независимый контур; таким образом, количество независимых контуров равно количеству хорд. 48 Рассмотрим алгоритмы решения этой задачи, основанные на различных стратегиях поиска [4]. а) б) в) Рис. 18. Граф (а) и варианты его остова (б,в) Построение остова методом поиска в глубину выполняет приведенная ниже рекурсивная процедура. Ее формальным параметром является вершина V, через которую проходит остов. В определении алгоритма участвуют также переменные Visited (множество покрытых остовом вершин), М (матрица смежности вершин графа), U (вершина, смежная V по ребру) и процедура AddBranch, которая добавляет в остов ребро (V,U). PROCEDURE DepthFirstFrame(V: tVertice); BEGIN Visited:=Visited+[V]; FOR U:=1 TO n DO IF (M[V,U]<>0) AND NOT(U IN Visited) THEN BEGIN AddBranch(V,U);DepthFirstFrame(U) END END; Процедура построения остова методом поиска в глубину начинается с произвольной вершины графа, которая в этом случае окажется корнем дерева. Следствием применения этой процедуры является то, что путь между любыми двумя дочерними вершинами обязательно проходит через корень. Это свойство будет далее использовано при обсуждении алгоритма декомпозиции графа на блоки. Алгоритм построения остова методом поиска в ширину использует объект Queue (очередь) с интерфейсными операциями Put (поставить в очередь), Get (взять из очереди), Empty (пустая очередь) и переменную r (произвольная вершина графа). Visited:=[]; Queue.Put(r); Visited:=Visited+[r]; WHILE NOT Queue.Empty DO BEGIN V:=Queue.Get; FOR U:=1 TO n DO 49 IF (M[V,U]<>0) AND NOT(U IN Visited) THEN BEGIN AddBranch(V,U); Queue.Put(U);Visited:=Visited+[U] END END Различие стратегий поиска в ширину и в глубину иллюстрируют результаты, показанные на рис. 19. 8 9 8 9 5 6 7 5 6 7 2 3 4 2 3 4 1 а) 1 б) Рис. 19. Варианты остова, построенного различными стратегиями: а – остов, построенный поиском «в глубину»; б – остов, построенный поиском «в ширину» Заметим, что оба приведенных алгоритма построения остова имеют эффективность O(n+m), где n, m – количество вершин и ребер графа. 5.4. Задача о минимальном остове Не все йогурты одинаково полезны… Реклама Если на множестве ребер исходного графа определена весовая функция W: E, имеет смысл задача построения остова минимального веса, или минимального остова. Минимальный остов графа G – это остов, сумма весов ребер которого минимальна. Задача о минимальном остове имеет ясную прикладную интерпретацию. Очевидное приложение – проектирование схемы коммуникаций, например линий электропередачи между населенными пунктами. Рассмотрим несколько алгоритмов ее решения. Как и для любой комбинаторной задачи, здесь может быть применен метод полного перебора поиском «в глубину», однако его эффективность – порядка O(|E|!) – практически неприемлема. Тем не 50 менее, можно модифицировать полный перебор, определив приоритет отбора ребер для включения в остов по возрастанию значения функции весов («легкие» ребра включаются первыми). Тогда первое же полученное перебором решение окажется оптимальным. Стратегия решения многошаговой задачи, которая рассматривает оптимальное решение всей задачи как совокупность оптимальных решений на каждом шаге, называется жадной. «Жадная» стратегия подходит не для всех задач – вопросы ее применимости рассматривает теория матроидов [4]. Однако в данном случае ее применение приводит к хорошему результату. Ниже приведен код рекурсивной функции, выполняющей построение минимального остова «жадным» методом. Функция заканчивает работу, если количество включенных в остов ребер nE на единицу меньше числа вершин графа. В противном случае добавляется очередное ребро в виде пары инцидентных вершин (S, T) из списка IncMatrix, упорядоченного по возрастанию весов, при условии, что оно не образует цикла с уже построенной частью остова (по крайней мере, одна из вершин, инцидентных ребру, не должна принадлежать остову). Это условие проверяет функция way. Рекурсивный вызов функции в контексте измененного состояния остова замыкает логику ее работы. function MinOstov(Ostov:tEdgeSet; nE:byte):boolean; var Found:boolean; var i:1..n; begin if nE=m-1 then MinOstov:=true else begin Found:=false; for i:=1 to n do with IncMatrix[i] do if not ((i in Ostov) or way(Ostov,S,T)) then begin Ost[nE+1]:=IncMatrix[i]; Found:=MinOstov(Ostov+[i],nE+1); if Found then break end; MinOstov:=Found end end; Порядковая сложность данного алгоритма- O(E V ), так как количество рекурсивно добавляемых ребер на единицу меньше числа вершин графа и при добавлении каждого ребра возможно включение любого из них. 51 Более мощный, по сравнению с «жадным», алгоритм Прима [2] основан на том, что каждый раз в остов добавляется ребро, одна из вершин которого уже покрыта остовом, а другая еще нет. При этом выбирается ближайшая вершина, т.е. вершина, находящаяся на кратчайшем расстоянии от построенной части остова. Введем необходимые обозначения. Каждой вершине х сопоставим пару чисел (х) и (х). (х) – кратчайшее расстояние от х до построенной части остова. (х) – вторая концевая вершина ребра, связывающая х и остов кратчайшим путем. Например, начальной фазе построения остова из вершины a (рис. 20) соответствуют следующие значения функций и : (b)=7; (b)=a; (h)=5; (h)=a; для остальных вершин ()=; ()=nil. Также через N(x) обозначим окрестность вершины х – множество вершин, смежных х; VT, ET – множества вершин и ребер, покрытых строящимся остовом. Ниже приведен псевдокод алгоритма, а результат его работы показан на рис. 20. Толщиной выделены ребра, включенные в минимальный остов. Жирными цифрами показана последовательность включения ребер в остов, а обычным шрифтом – веса ребер. // Инициализация ЕТ:=; VT:={a}; ДЛЯ ВСЕХ x ВЫПОЛНЯТЬ ЕСЛИ x Na ТО {(x):=w(x,a); (x):=a} ИНАЧЕ {(х):=; (x):=nil}; // Итерационный цикл построения Остова. ПОКА VT< V //Пока Остов не покрыл все вершины графа { // Отыскание ближайшей вершины, не покрытой остовом Найти вершину *, такую, что *V\VT, (*)=min(()),V\VT; // Добавление вершины и кратчайшего ребра в Остов. VT:=VT{*};ET:=ET((*),*); // Пересчет кратчайших расстояний для N* V\VT; ДЛЯ ВСЕХ N* V\VT ВЫПОЛНЯТЬ ЕСЛИ ()>(*,) ТО { ():=(*,); ():=*} }. 52 4 c 9 7 1 d 1 6 15 13 a 5 20 b 3 4 10 i 5 12 20 h e 8 6 7 f 2 2 g 22 Рис. 20. Пример построения остова алгоритмом Прима Оценим эффективность алгоритма. Цикл поиска ближайшей вершины имеет эффективность порядка |V| и повторяется |V|-1 раз. Таким образом, имеем эффективность О(|V|2), так как количество ребер обычно превышает количество вершин графа. Это лучший, по сравнению с «жадным» алгоритмом, результат. Тот же принцип расширения остова использует другой, еще более эффективный алгоритм Крускала [2]. Его отличие в том, что расширение начинается не из одной произвольно выбранной вершины, а одновременно из всех вершин графа (остовного леса из одновершинных компонентов). Каждая итерация этого алгоритма состоит в следующем. Для каждой пары компонентов остовного леса определяется кратчайшее расстояние между ними. Эти пары связываются между собой по соответствующему ребру. Новая итерация имеет дело с остовным лесом, содержащим меньшее число компонентов. Алгоритм заканчивает работу, когда количество компонент станет равным 1. Рассмотрим алгоритм метода. Введем необходимые для этого обозначения. № Обозначения Комментарии 1 Е Список ребер; Еi=(x,y) – пара концевых вершин ребра 2 W Список весов ребер 3 ET Множество ребер текущего состояния леса 4 P Количество компонентов текущего леса 5 Е1 Множество минимальных по весу ребер для те53 6 НМР 7 8 ВМР КОМР кущего состояния леса НМРi – номер минимального ребра для компоненты i ВМРi – вес минимального ребра для компоненты i КОМРj – номер компоненты остовного леса, содержащей ребро j, j=1,n (n – количество вершин) Ниже приведен псевдокод алгоритма: // Инициализация ЕТ:=; ДЛЯ i:=1 ДО n ВЫПОЛНЯТЬ {КОМП[i]:=i;BMP[i]:=} p:=n; //Число компонентов равно числу вершин графа Цикл построения Остова. ПОКА p<>1 ВЫПОЛНЯТЬ { ДЛЯ k:=1 ДО E ВЫПОЛНЯТЬ { //анализ компонентной принадлежности концевых вершин} i:= КОМП[E[k]. x]; j:= КОМП[E[k]. y]; ЕСЛИ j<>i ТО // вершины принадлежат разным компонентам { // минимизация веса связи для компоненты ЕСЛИ W[k]<BMP[i] ТО {BMP[i]:=W[k]; HMP[i]:=k}; ЕСЛИ W[k]<BMP[j] ТО {BMP[j]:=W[k]; HMP[j]:=k}; } }; E1:=; // формирование множества ребер, добавляемых на шаге ДЛЯ i:=1 ДО p ВЫПОЛНЯТЬ E1:=E1{HMP[i]; ET:=ETE1; Пересчет р }. Пересчет количества компонентов реализуется путем помечивания вершин остовного леса методом поиска в глубину. Поиск выполняется для каждой вершины, которая еще не получила метку. Метка, помещенная в нее, распространяется по ребрам, вошедшим в остовный лес. Количество построенных таким образом деревьев равно количеству компонентов. Контрольный пример и результат его выполнения данным алгоритмом показан на рис. 21. Для ребер, вошедших в остов, вслед за весами в скобках указан номер шага, на котором ребро было включено в остов. 54 Вычислительные затраты на каждую итерацию имеют порядок числа ребер графа E. Количество итераций определяется тем, что на каждой из них число компонентов уменьшается как минимум вдвое, а так как первоначально число компонентов равно числу вершин V, в итоге имеем оценку О(|E|log2(|V|)). 2(1) 1 1(1) 2 3 6 7 6 4(2) 3(1) 4 9 8 7 9 2(1) 7 9 3(1) 10 6(3) 8 5 4(1) 5(2) 8 Рис. 21. Пример построения остова алгоритмом Крускала Задания и упражнения для самоконтроля 1. Как определить число независимых контуров связного графа, если известно количество его вершин и ребер? 2. Почему можно утверждать, что при построении остова методом поиска «в глубину» любой путь между дочерними вершинами проходит через корень остовного дерева? 3. Обоснуйте оценку эффективности алгоритмов построения остова поиском «в ширину» и «в глубину». 4. Как убедиться, что при построении минимального остова «жадным» алгоритмом вновь добавляемое ребро не образует контура с построенной частью остова? 5. Почему проблема проверки контура отсутствует в других алгоритмах построения остова минимального веса? 6. Обоснуйте оценку эффективности для каждого из трех алгоритов построения остова минимального веса. 7. Почему оценка эффективности O(|V|2) лучше, чем O(|V|*|E|)? 8. В чем заключается идея алгоритма, использующего понятие остовного леса? Как объяснить наличие логарифмической составляющей в оценке? 55 5.5.Метод динамического программирования Видеть легко. Трудно предвидеть. Б.Франклин В процессе обсуждения методов построения минимального остова утверждалось, что применение «жадных» стратегий для решения многошаговых задач не всегда приводит к успеху. В качестве примера несостоятельности «жадной» стратегии рассмотрим следующую задачу [2]. Требуется проложить трассу минимальной стоимости из пункта А в пункт В (рис. 22). Каждый участок трассы имеет вес Wi. На каждом шаге можно выбирать направление движения – вверх или вправо. Трасса, полученная применением «жадного» алгоритма, показана на рис. 22 штриховыми утолщенными линиями со стрелкой. Суммирование весов вдоль этой трассы дает критериальную оценку 138. Сравним эту оценку с другой, полученной методом динамического программирования. Суть метода динамического программирования заключается в выполнении пошаговой оптимизации с учетом влияния управления, выбранного на текущем шаге, на эффективность всех последующих шагов, т.е. так, чтобы сумма выигрышей (потерь) на данном шаге и всех оставшихся шагах была максимальной (минимальной). Поэтому выбор управлений в методе динамического программирования начинается с последнего шага, который не влияет на дальнейшее управление. В примере (рис. 22) это выбор управлений в узлах, смежных В. Мы не задаемся вопросом о том, как мы попали в эти узлы. Вместо этого ставится вопрос о выборе оптимального управления при условии, что мы уже находимся в этих узлах. Выбранное таким образом управление будет условно-оптимальным (показано стрелками на участках трассы). Его стоимость показана числом внутри вершины. Далее можно рассмотреть тот же вопрос, но уже относительно узлов, цена условно-оптимального управления для которых известна. Например, если на предыдущем шаге были определены стоимости условно-оптимального управления 10 и 14, а выбор управления в текущей вершине может добавить к сумме потерь 13 или 14, то условно-оптимальным управлением из этой вершины будет «вверх». «Срезы» шагов выбора условнооптимального управления показаны на рис. 22 диагональными штриховыми линиями. Систематический выбор условно-оптимальных управлений на последовательных шагах от В к А приводит решение в узел А. Стоимость управления, соответствующего этой вершине, и будет оптимальной. После этого следует только составить трассу из 56 участков, помеченных стрелками условно-оптимального управления, в направлении из А в В. Эта трасса (показана на рис. 22 утолщенной линией со стрелками) и будет оптимальной со стоимостью прокладки 118 единиц. Сравнение двух значений критерия (118 против 138, полученных «жадным» алгоритмом) показывает, что «жадность», проявленная на начальных этапах, дорого обошлась впоследствии. Например, стремление проложить начальные участки трассы по равнине привело к необходимости прорубать тоннель в горном хребте. 5.6. Задача о максимальном потоке Рассмотрим следующую практическую ситуацию. Пусть имеются поставщик и потребитель какого-либо ресурса и связывающая их сеть магистралей, каждый из участков которых имеет определенную пропускную способность. Типичные вопросы, которые могут возникнуть в данной ситуации – каково максимальное количество ресурса, которое может быть пропущено через сеть за единицу времени, и какие участки магистрали мешают дальнейшему увеличению потока. В более формальной постановке эта актуальная задача носит название задачи о максимальном потоке. Сформулируем ее, предварительно введя необходимые определения. Транспортная сеть – направленный граф (V,E) со следующими особенностями: не имеет петель; имеет единственную вершину с нулевой полустепенью захода – исток S; имеет единственную вершину с нулевой полустепенью исхода – сток T; на множестве дуг Е определены целочисленные неотрицательные функции пропускной способности C(e) и распределения потока f(e), причем 0≤f(e)≤C(e); в случае строгого равенства функций для какой-либо дуги говорят о насыщении дуги потоком; для вершинV\{S,T}, справедлив закон сохранения: сумма потоков, входящих в вершину, равна сумме потоков выходящих из нее. Граф, соответствующий перечисленным требованиям, показан на рис. 23. Пары чисел в скобках соответствуют значениям C(e) и f(e). Задача о максимальном потоке состоит в поиске такой функции потокораспределения f, которая максимизирует суммарный поток из истока (суммарный поток в сток). 57 Рис. 22. Прокладка трассы методом динамического программирования 58 Рис. 23. Пример транспортной сети Идея рассматриваемого далее алгоритма решения этой задачи (алгоритма Форда-Фалкерсона [8,6]) состоит в итерационном увеличении потоков в сети от некоторого начального (обычно нулевого) значения. Приращение потока в рамках данного алгоритма выполняется вдоль так называемого f-добавляющего, или увеличивающего пути. Путь P в транспортной сети называется увеличивающим при следующих условиях: он ведет из истока в сток; дуги могут входить в путь в прямом и обратном направлениях; для каждой дуги ei пути P определена функция c(ei ) f (ei ) | ei прямая; f (ei ) | ei обратная; i ( P) функция ( P) min ( i ( P)) i положительна. Пример, иллюстрирующий понятия прямой и обратной дуги пути из a в b, показан на рис. 24. Здесь (a,x1) – прямая дуга, (x1x2) – обратная дуга. В целях максимизации общего потока поток через прямую дугу можно увеличивать до насыщения, а поток в обратной дуге уменьшать до нуля. Рис. 24. К пояснению понятий прямой и обратной дуг 59 Распределение потока вдоль увеличивающего пути может быть пересчитано: f (e) ( P) | прямое; f ( e) f(e)- ( P) | обратное. Очевидно, что пока в транспортной сети существует хотя бы один увеличивающий путь, распределение потока не максимально. Следовательно, если алгоритмы поиска увеличивающего пути и пересчета потоков вдоль этого пути известны, псевдокод решения задачи о максимальном потоке может иметь следующий вид: ПОВТОРЯТЬ Поиск увеличивающего пути; ЕСЛИ увеличивающий путь найден ТО пересчет потоков вдоль пути; ПОКА НЕ (Нет увеличивающих путей) Рассмотрим теперь алгоритмы решения частных задач, входящих в итерационный цикл. Алгоритм поиска увеличивающего пути (алгоритм помечивания) использует понятие метки вершины. Меткой вершины называется пара (dv, v), где указывается на смежную вершину u, из которой v получила метку, и направление передачи метки (прямое или обратное помечивание), а v в зависимости от направления помечивания определяется соотношением min( u , C (e) f (e)) | e (u, v), C (e) f (e); . v min( , f ( e ) | e ( v , u )), f ( e ) 0 . u Процесс помечивания стартует из вершины-истока. Метка может быть передана в любую из смежных вершин в том случае, если вершина допускает помечивание (не была помечена раньше). Дополнительным условием прямой передачи метки является ненасыщенность дуги потоком (f(e)<C(e)), для обратной передачи необходимо, чтобы выполнялось условие f(e)>0. Процесс передачи метки заканчивается, если соблюдены следующие условия: – метку получила вершина T (увеличивающий путь построен, поток вдоль этого пути может быть увеличен на T); – T не получила метку и нет других вершин, способных ее принять (это означает, что распределение потока в транспортной сети максимально). Для поиска увеличивающих путей можно использовать любую из стратегий поиска («в глубину» или «в ширину»), примеры реа60 лизации которых были рассмотрены ранее. Поэтому ниже без дополнительных комментариев приведен псевдокод алгоритма помечивания. //Инициализация Пометить вершину S Очередь. Поставить:S //Цикл поиска пути ПОКА НЕ ((Очередь.Пуста) И (T не помечено)) ВЫПОЛНЯТЬ { V:=Очередь.Взять; ПОКА НЕ ((Все дуги просмотрены) И (T не помечено)) ВЫПОЛНЯТЬ ЕСЛИ (вершина U не помечена) И (возможно прямое помечение) ТО { Очередь. Поставить(U); Пометить U; }; ИНАЧЕ ЕСЛИ (U не помечено) И (Возможно обратное помечивание) ТО { Очередь. Поставить(U); Пометить U; }; }. Псевдокод алгоритма пересчета потоков использует метки вершин для пересчета потоков в обратном порядке от T к S: V: =T; ПОКА V<>S { ЕСЛИ dv f (u, ИНАЧЕ f (u, V: =U; }. ВЫПОЛНЯТЬ =u+ ТО v): = f (u, v) + Δт v): = f (u, v) – Δт Результаты поэтапной интерпретации этого псевдкода приведены в Error! Reference source not found.. Итак, максимальный поток определен. Второй вопрос – какие дуги графа являются «узким местом» и мешают дальнейшему увеличению потока – это вопрос о так называемом минимальном сечении. Минимальным сечением транспортной сети называется подмножество ее ребер, насыщенных потоком, таких, что их удаление приводит к разделению графа на два связных подграфа, в одном из которых находится вершина-исток, а в другом – вершина-сток. В рассмотренном выше примере в минимальное сечение входят дуги (S, a) и (S, c). Следовательно, для увеличения максимального 61 потока в сети необходимо увеличить пропускную способность этих дуг. Оценка эффективности алгоритма Форда-Фалкерсона основана на следующих рассуждениях. Во-первых, на каждой итерации добавления увеличивающего пути насыщается как минимум одна дуга графа. Во-вторых, алгоритм поиска увеличивающего пути однократно помечивает вершины графа, рассматривая при этом все возможности передачи метки из текущей вершины. В-третьих, алгоритм пересчета потока имеет оценку эффективности порядка числа вершин графа. В итоге имеем оценку вида O(|E|(|V|2)+|V|) или, следуя правилам упрощения из раздела 2, O(|E||V|2). Таблица 2. Результаты поэтапной интерпретации псевдокода Распределение потоков Состояние очереди S= 1 2 4 3 5 6 =T S= 1 2 4 5 3 6 =T S= 1 4 3 5 6 =T Нет увеличивающих путей. Дуги (1,2) и (1,4) входят в минимальное сечение 62 Задания и упражнения для самоконтроля 1. Дайте определение транспортной сети. 2. Для каких вершин транспортной сети не выполняется закон сохранения потока? 3. Дайте определение увеличивающего пути. 4. Докажите, что итерации метода сходятся. 5. Обоснуйте оценку эффективности алгоритма Форда-Фалкерсона. 5.7. Транспортная задача Пусть имеется m поставщиков и n потребителей однородной продукции. Каждый поставщик характеризуется определенной производительностью, каждый потребитель – объемом спроса. При этом суммарное производство должно быть равно суммарному потреблению. Каждая пара поставщик-потребитель связана транспортным маршрутом, для которого известна стоимость доставки единицы продукции (тариф). Определить схему поставок, при которой транспортные расходы будут минимальны. Математическая модель этой задачи, которая получила название транспортной, представлена двумя матрицами – матрицей поставок S, элемент i, j которой определяет объем поставок i-го производителя j–му потребителю, и матрицей тарифов T, элемент i, j которой определяет стоимость доставки единицы продукции от i-го производителя j–му потребителю ( рис. 25). Матрица поставок определяет допустимый (соответствующий ограничениям) вариант распределения продукции. Такой вариант может быть получен методом «северо-западного угла». Суть метода в том, что каждый очередной (в порядке возрастания индекса) поставщик пытается удовлетворить спрос очередного потребителя до исчерпания своих возможностей. Результат применения этого метода показан на рис. 25,а. Обратите внимание, что сумма значений по строкам соответствует объемам производства, сумма значений по столбцам – объемам потребления, а суммы итоговых сумм по стокам и столбцам равны между собой. Очевидно, что суммарная стоимость транспортных расходов C при заданном распределении поставок определится m как сумма произведений элементов матриц S и T: n C sij t ij . i 1 j 1 Является ли допустимый вариант оптимальным? Ответить на этот вопрос можно, используя понятие цикла отрицательной стоимости. 63 1 2 3 1 5 0 0 2 2 6 0 5 3 0 3 4 8 7 4 0 0 14 14 7 9 18 34 1 2 3 1 19 70 10 2 30 30 8 б) 3 50 40 70 4 10 60 20 а) Рис. 25. Математическая модель транспортной задачи: а – матрица поставок; б – матрица тарифов Чтобы определить это понятие, выполним следующие построения. Найдем в текущем варианте матрицы поставок нулевой элемент. Нуль в координатах (i,j) означает отсутствие поставок i потребителю j. Очевидно, что в этом случае возможно увеличение объема поставок. Однако, учитывая ограничение мощности производителя, это возможно только за счет уменьшения поставок другому потребителю этого же производителя, то есть за счет уменьшения какого-либо ненулевого элемента той же строки, например k. Его уменьшение нарушает баланс потребителя k и требует увеличения какого-либо из элементов соответствующего столбца. Процесс перераспределения поставок продолжается до тех пор, пока номер текущего столбца не совпадет с исходным. Это означает возможность предполагаемого перераспределения поставок. Его целесообразность определяется путем знакопеременного суммирования элементов матрицы тарифов, соответствующих элементам цикла в матрице поставок. Содержательный смысл этой операции в том, что увеличение объема поставок (i,j) на единицу увеличивает общую стоимость транспортировки, в то время как уменьшение объема поставок (i,k) на единицу уменьшает ее на величину соответствующих элементов матрицы тарифов. Если знакопеременная сумма 64 отрицательна, это означает, что перераспределение поставок привело к уменьшению транспортных расходов. В таком случае найденный цикл – это цикл отрицательной стоимости. На рис. 25, а найденный цикл отрицательной стоимости (8-70+4030 = -52) выделен затенением. Поэтому имеет смысл максимально возможное перераспределение поставок вдоль найденного цикла. Для этого нужно найти минимальный четный элемент цикла в матрице поставок (в примере максимальный четный элемент равен 4) и на величину этого элемента увеличить нечетные элементы и уменьшить четные. Очевидно, что сокращение транспортных расходов в примере в этом случае составит 524=208 единиц. Так как наличие цикла отрицательной стоимости свидетельствует о неоптимальности текущей схемы поставок, процесс поиска таких циклов и пересчета схемы поставок следует продолжать, пока циклы отрицательной стоимости существуют. Эти рассуждения приводят к следующему псевдокоду: ПОВТОРЯТЬ { Найти цикл отрицательной стоимости, который максимально сокращает транспортные расходы; ЕСЛИ цикл существует ТО Перераспределить поставки } ПОКА НЕ (Нет циклов отрицательной стоимости). Самым сложным элементом данного алгоритма является поиск цикла отрицательной стоимости. Рассмотрим его реализацию с применением стратегии поиска «в ширину» при поддержке абстрактного типа данных «очередь». Очередь.Поставить(начальная строка); Цикл.Добавить(начальная строка); ПОКА НЕ ((Очередь.Пустая) ИЛИ (Найден цикл)) ВЫПОЛНЯТЬ { Текущий элемент: =Очередь.Взять. ЕСЛИ Движение по строке ТО { ДЛЯ ВСЕХ СТОЛБЦОВ ЕСЛИ (Поставка ненулевая)И(элемент не помечен) ТО ЕСЛИ Текущий столбец начальный ТО цикл найден ИНАЧЕ { Пометить элемент матрицы поставок; Очередь. Поставить (текущий столбец); Цикл. Добавить (текущий столбец); }; }; ИНАЧЕ 65 ДЛЯ ВСЕХ СТРОК ЕСЛИ (Поставка ненулевая)И(элемент не помечен) ТО { Пометить элемент матрицы поставок; Цикл. Добавить (текущая строка); Очередь. Поставить (текущая строка); }; }; Что касается эффективности рассмотренного алгоритма, то количество итераций внешнего цикла не может превышать количества элементов матрицы поставок – m*n; количество операций, выполняемых при поиске цикла отрицательной стоимости, (m*n)2; количество операций проверки цикла на отрицательность и пересчета имеет порядок m*n. Учитывая взаимное расположение этих циклов, определим общую оценку эффективности алгоритма решения транспортной задачи – (m*n)3. Задания и упражнения для самоконтроля 1. Сформулируйте транспортную задачу. 2. В чем заключается метод «северо-западного угла»? 3. Как вычислить значение оценки для полученного варианта распределения поставок? 4. Как доказать, что полученное распределение является (не является) оптимальным? 5. Дайте определение цикла отрицательной стоимости. 6. Как построить цикл отрицательной стоимости? 7. Какой содержательный смысл вкладывается в понятие стоимости цикла? 8. Как определить максимальную величину перераспределения потоков вдоль цикла отрицательной стоимости? 9. Чему равно уменьшение стоимости транспортировки в результате такого перераспределения? 10. Дайте оценку эффективности алгоритма решения транспортной задачи. 5.8. Задача о максимальном паросочетании в двудольном графе Рассмотрим следующую практическую ситуацию: пусть требуется составить расписание учебных занятий, если известна занятость каждого преподавателя, то есть множество классов, в которых он должен вести занятия. Формально занятость преподавателей может быть представлена в виде двудольного графа. 66 Напомним, что граф G=(V,E), где V – множество вершин, E – множество ребер, называется двудольным, если множество его вершин разбито на два непересекающихся подмножества: V=V1UV2; V1∩V2=Ø, и любое ребро графа инцидентно вершинам из двух разных подмножеств. Рис. 26. Пример двудольного графа Если элементам множества V1 противопоставить преподавателей, а элементам множества V2 – классы, то ребро, инцидентное вершинам из V1 и V2, можно трактовать как занятие, проводимое конкретным преподавателем в конкретном классе. Для составления расписания важно знать, какие занятия можно проводить одновременно. Очевидно, что преподаватель не может одновременно проводить занятия более чем в одном классе, так же как и класс – присутствовать на занятиях нескольких преподавателей. Формально это ограничение описывается при помощи понятия паросочетания. Паросочетанием называется множество попарно несмежных ребер графа (т.е. ребер, не имеющих общих вершин). Вершины, инцидентные ребрам, которые вошли в паросочетание, называются насыщенными в паросочетании. Множество ребер, составляющих паросочетание, показано на рис. 26 утолщенными линиями. Так как множество учебных занятий, как правило, требуется провести на ограниченном временном интервале (четверть, семестр), расписание должно быть «плотным». В связи с этим практический интерес приобретает анализ возможностей максимизации паросочетания. Максимальное паросочетание – это паросочетание с максимально возможным количеством вошедших в него ребер. 67 Содержательно максимальное паросочетание можно интерпретировать как максимальное множество занятий, которые могут быть проведены одновременно. Введем понятия, составляющие основу алгоритма максимизации паросочетаний. Чередующаяся цепь в графе G при паросочетании M – это цепь, ребра которой поочередно входят в M и E\M. Добавляющий путь – это чередующаяся цепь между двумя ненасыщенными в паросочетании вершинами. Пример чередующейся цепи, которая одновременно является и добавляющим путем, показан на рис. 26 пунктирной линией. Из анализ этого примера следует, что если добавить в паросочетание ребра пути, которые не входят в данное паросочетание, и удалить ребра, принадлежащие паросочетанию, их количество в паросочетании увеличится на единицу. Это наблюдение формализовано теоремой Бержа: Паросочетание М максимально, если не существует добавляющего пути Р по отношению к этому паросочетанию. Процедура наращивания паросочетания при помощи добавляющего пути реализуется операцией вычисления кольцевой суммы. Кольцевой суммой двух графов (обозначается ) называется граф, в который входят ребра, принадлежащие либо одному слагаемому, либо другому, но не обоим слагаемым одновременно (рис. 27). Рис. 27. К понятию кольцевой суммы Эффективность алгоритма построения максимального паросочетания путем итераций, включающих поиск добавляющего пути и пе68 ресчет паросочетания, имеет порядковую оценку количества итераций O(n3), где n =|V|. Однако Хопкрофту и Карпу удалось показать, что если для расширения паросочетаний использовать кратчайшие (с минимальным числом ребер) добавляющие пути, эта оценка может быть 52 улучшена до O(n ) . Рассмотрим далее алгоритм построения максимального паросочетания, который известен как алгоритм ХопкрофтаКарпа [4, 8]. Пусть имеем двудольный граф G=(V,E), на котором требуется найти максимальное паросочетание M. Расположим граф G так, как показано на рис. 28. Ориентируем ребра G, не вошедшие в паросочетание, «сверху-вниз» (показаны на рис. 28 обычными линиями), а те, которые принадлежат паросочетанию, – «снизу-вверх» (показаны на рис. 28 утолщенными линиями). В результате добавляющие пути становятся ориентированными по отношению к М. Далее, пусть L0 – множество ненасыщенных в паросочетании «верхних» вершин графа. Выберем их в качестве «отправных точек» для построения множества добавляющих путей, один из которых окажется кратчайшим, если это множество не пустое. Спустимся из этих вершин вниз по ребрам, не вошедшим в паросочетание. Множество «нижних» вершин, инцидентных этим ребрам, обозначим L1. Если в множестве присутствуют вершины, не насыщенные в паросочетании, значит, найдено множество кратчайших добавляющих путей. Иначе поднимемся из вершин L1 по ребрам, вошедшим в паросочетание. Множество «верхних» вершин, инцидентных этим ребрам, вновь обозначим L0. Описанный процесс спуска – подъема нужно повторять до тех пор, пока не будет выполнено одно из условий: – найдена ненасыщенная паросочетанием нижняя вершина; – не осталось вершин, которые еще не были использованы в процессе описанных выше построений. В результате выполнения этой процедуры формируется несвязный граф G*, каждый связный компонент которого представляет собой дерево. Каждый путь нечетной длины из корня этого дерева в лист, сопоставленный ненасыщенной «нижней» вершине, однозначно соответствует кратчайшему добавляющему пути в графе G. Вид этого дерева для графа на рис. 28 показан на рис. 29. 69 Рис. 28. Ориентация ребер двудольного графа 0 5 1 4 1 5 2 8 7 9 3 6 6 8 9 11 Рис. 29. Множество добавляющих путей Граф G* определяет два кратчайших добавляющих пути: (6,5,9,9) и (6,5,9,11). Однако они не могут быть одновременно использованы для расширения текущего паросочетания, так как пересекаются по вершинам. В этой процедуре могут участвовать лишь по одному (любому) пути из каждой пары «конкурентов». Обсуждение алгоритма Хопкрофта-Карпа завершим псевдокодом построения максимального паросочетания путем построения 70 и последующего кольцевого суммирования множества кратчайших добавляющих путей. Алгоритм использует следующие обозначения. Идентификатор Содержательный смысл М Текущее паросочетание ТУ Текущий уровень вершин G* МИНВ Множество нижних вершин G, уже использованных в процессе построения кратчайших добавляющих путей на текущей итерации МКДП Множество кратчайших добавляющих путей, не пересекающихся по вершинам Ниже приведен сам псевдокод. // цикл построения максимального паросочетания ПОВТОРЯТЬ ТУ:= 0; МИНВ:= ; Сформировать множество L0 Включить вершины из L0 в граф G*; // цикл построения МКДП ПОВТОРЯТЬ Сформировать множество L1, L1МИНВ=; Включить эти вершины в граф G*; ЕСЛИ (L1) И (нет свободных нижних вершин) ТО { МИНВ:=МИНВL1; ТУ:=ТУ+1; Сформировать новое множество верхних вершин L0, смежных добавленным по ребрам, вошедшим в паросочетание; Включить вершины из L0 в граф G*; ТУ:=ТУ+1; }; ПОКА НЕ ((найдена свободная нижняя вершина) ИЛИ (невозможно добавить нижнюю вершину)); ЕСЛИ найдены свободные нижние вершины ТО М:=М МКДП ПОКА НЕ ((нет вариантов продолжения из верхних вершин) ИЛИ (нет свободных нижних вершин)) 5.9. Задача о плотном расписании занятий Алгоритм построения максимального паросочетания в двудольном графе создает основу для решения более сложной задачи – задачи о составлении «плотного» расписания занятий на основании данных об учебной нагрузке. Формально модель нагрузки представлена двудольным мультиграфом G ( X , Y , E ) , где X , Y – множества вершин, соответ- 71 ствующих преподавателям и классам, E – рёбра, кратность которых определяет количество занятий, проводимых преподавателем x в классе y. Основная идея рассматриваемого ниже метода составления плотного расписания состоит в первоочередном включении в него занятий для наиболее загруженных преподавателей и классов. Загруженность (количество занятий) – это степень вершины, которая соответствует преподавателю (классу). Пусть x – максимальная степень вершины из множества X X1 X – подмножество X вершин из , имеющих максимальную степень (наиболее загруженных преподавателей); Y1 – подмножество вершин из Y , смежных вершинам из X 1 ; G ( X1, Y1, E) – подграф, который содержит вершины X 1 и Y1 и инцидентные им рёбра; M 1 – это паросочетание в G , которое насыщает все вершины из X 1 . Аналогично пусть y –максимальная степень вершины из Y (самый загруженный класс); Y2 Y – подмножество классов с максимальной загрузкой; Х2 – множество вершин из X , смежных вершинам из Y2 ; G ( X 2 , Y2 , E) – подграф, образованный вершинами из X 2 и Y2 и инцидентными им рёбрами; M 2 – максимальное паросочетание, насыщающее все вершины из Y2 . В содержательном смысле M 1 и M 2 – это множество заня(максимальная загрузка преподавателя); тий, которые могут одновременно иметь место у самых загруженных преподавателей и у самых загруженных классов соответственно. Основу излагаемого далее алгоритма построения плотного расписания составляет следующая теорема (Мендельсон и Далмедж) [8, с.160]: M * , построенное на основе M1 и M 2 , насыщающее все вершины из X1 и Y2 . Существует паросочетание G X1 X 2 , Y1 Y2 , M1 M 2 – граф, объединяющий G и G . Кроме того, обозначим через P множество добавля ющих путей. Построение M , удовлетворяющего условиям теоремы, Пусть обеспечивает следующий псевдокод: 72 P:=; ДЛЯ ВСЕХ YY2\Y1 // Все эти вершины имеют единичную степень { Найти путь PY из Y в XX2\X1 или в ZY1\Y2 // Степени этих вершин также равны единице; P:=P{PY} – добавить этот путь; }; M:=M1P Результаты работы этого алгоритма иллюстрируют примеры, показанные на рис. 30. X Y X1 Y1 X2 Y2 X Y Y1 Х1 Y2 Х2 а) б) Рис. 30. Построение паросочетания, насыщающего X1 и Y2: а –паросочетания М1 и М2; б – паросочетание М* Таким образом, из паросочетаний, насыщающих X1 или Y2, получено паросочетание, которое насыщает X1 и Y2 одновременно. Его можно интерпретировать как подмножество занятий, одновременно проводимых самыми занятыми преподавателями в самых занятых классах. Для построения плотного расписания следует вычесть это подмножество занятий из полной нагрузки, вновь определить подмножества X1 и Y2 и паросочетание М*. Эти действия повторяются цикли73 чески до тех пор, пока граф нагрузки не окажется пустым (не содержащим ребер). ПОКА множество рёбер графа G не пусто ПОВТОРЯТЬ { Определить множество X1 Построить подграф G Найти максимальное паросочетание M1 Определить множество Y2 Построить граф G Найти паросочетание M 2 } Построить граф G * Найти паросочетание M для G Из множества рёбер графа G вычесть паросочетание M* Задания и упражнения для самоконтроля 1. Сформулируйте теорему Бержа. 2. Что такое кольцевая сумма? Приведите пример суммирования паросочетания с добавляющим путем. 3. Почему итерации, основанные на применении теоремы Бержа, обязательно сходятся? 4. Почему добавляющие пути, построенные по алгоритму ХопкрофтаКарпа, являются кратчайшими? 5. Докажите, что степени вершин YY2\Y1, XX2\X1, ZY1\Y2 равны единице. 5.10. Задача декомпозиции графа на блоки Divide et empera Н. Макиавелли Декомпозиция – распространенный принцип борьбы со сложностью задач. Применение принципа «разделяй и властвуй» дает реальный выигрыш в эффективности. Например, декомпозиция задачи, имеющей квадратичную оценку эффективности и выполняющей обработку n элементов данных, на две задачи, которые выполняют обработку k и l элементов данных соответственно, где k+l=n, позволяет уменьшить количество операций на 2kl. Для задач, имеющих графовую интерпретацию, объектом декомпозиции является граф. Многие практически важные задачи, такие, как нахождение всех элементарных циклов для расчета передаточных функций систем автоматического управления или установление факта планарности графа в задачах конструкторского проектирования печатных плат, сводятся к аналогич74 ным задачам для особым образом выделенных подблоков исходного графа. В связи с этим рассмотрим алгоритм декомпозиции графа на блоки [4], введя предварительно необходимые определения. Граф называется связным, если любые две его несовпадающие вершины соединены маршрутом. Максимальный связный подграф полного графа называется связной компонентой, или просто компонентой. Точкой сочленения называется такая вершина неориентированного графа, удаление которой вместе с инцидентными ей рёбрами приводит к увеличению числа компонент связности в графе8. Неориентированный граф называется двусвязным, если он связный и не содержит точек сочленения. Максимальный двусвязный подграф полного графа называется компонентой двусвязности или блоком9. Примеры, иллюстрирующие некоторые из определяемых понятий, показаны на рис. 31. Свойство двусвязности – очень желательный признак для некоторых приложений. Например, если сеть узлов связи представлена двусвязым графом, выход из строя одного узла связи не приведет к потере соединения между любой парой других. Излагаемый ниже алгоритм декомпозиции графа на блоки [4] основан на следующей теореме. Пусть D=(V,T) – остов графа G=(V,E), построенный методом поиска в глубину, а r – корень остова. Вершина vV является точкой сочленения тогда и только тогда, когда либо v = r и r имеет, по крайней мере, две дочерние вершины10 в остове, либо v r и существует ее дочерняя вершина w, такая, что ни w, ни какой-либо ее потомок не связаны ребром ни с одним предком v. Равнозначное утверждение состоит в том, что если в графе существуют такие вершины u и v, что каждый путь из u в v проходит через а, то а является точкой сочленения графа. 9 Отсюда следует, что подмножества вершин двух блоков либо не пересекаются, либо их пересечение есть точка сочленения. 10 Имеются в виду отношения между вершинами остова в процессе его построения методом поиска «в глубину». 8 75 7 1 4 2 5 3 8 9 6 11 13 10 12 3 1 4 7 12 2 5 8 11 13 5 9 8 13 13 14 6 10 9 14 а) б) Рис. 31. Точки сочленения и блоки графа: а – граф с точками сочленения; б – блоки графа Действительно, при рассмотрении алгоритма построения остова методом поиска «в глубину» уже упоминалось, что любой путь между любой парой дочерних вершин остова проходит через корень. Удаление корня, таким образом, приведет к увеличению числа компонент связности. Следовательно, в этом случае корень является точкой сочленения. Аналогично, если после удаления вершины v r существует путь от какого-либо из ее потомков до корня, значит, он содержит ребро, соединяющее вершину-потомок с одним из предков v, и в этом случае не является точкой сочленения. Таким образом, теорема доказана. Основываясь на этой теореме, для выполнения процедуры декомпозиции с каждой вершиной v можно связать два параметра. Первый Nv – это порядковый номер включения вершины в остов в процессе его построения методом поиска «в глубину». Второй параметр Lv определяет наименьшее значение Nv на момент окончания обхода поддерева с корнем в v. Если к этому моменту Lu< Nv, то v не является точкой сочленения. Если обозначить A = min{L[w]: w – дочерняя вершина v}, B = min{N[u]: (u,v)E\T}, то L[v] = min{N[v], A, B}. Ниже приведен использующий это соотношение рекурсивный алгоритм. PROCEDURE BiLink(V, P:byte); {P - “родитель” V} VAR U:byte;Edge:tEdge; BEGIN Inc(Num);N[V]:=Num;L[V]:=N[V]; FOR U:=1 TO Deg DO IF G[V,U]=1 THEN BEGIN IF N[U]=0 THEN {вершина U не посещалась} BEGIN 76 Edge.a:=V;Edge.b:=U;Stk.Push(Edge); BiLink(U,V); IF L[V]>L[U] THEN L[V]:=L[U]; IF L[U]>=N[V] THEN {V есть корень или точка сочленения} BEGIN REPEAT Edge:=Stk.Pop; write(Edge.a:3; Edge.b:3) UNTIL(Edge.a=V)AND(Edge.b=U); Writeln; END END {вершина U посещалась, она не является родительской} {вершиной V и (V,U) является хордой} ELSE IF (U<>P)AND (N[U]<N[V]) THEN BEGIN Edge.a:=V;Edge.b:=U;Stk.Push(Edge); IF L[V]>N[U] THEN L[V]:=N[U] END END END; Эта подпрограмма вызывается последовательно для всех компонент графа (в худшем случае – для всех вершин). Каждый вызов для компоненты связности требует выполнения ni+mi операций, где ni, mi количество вершин и ребер в i–й компоненте. Кроме того, каждое ребро графа попадает в стек и удаляется из него в точности один раз, что дает еще m операций. Таким образом, общая оценка эффективности алгоритма – O(n+m). Задания и упражнения для самоконтроля 1. Как декомпозиция задачи влияет на эффективность алгоритмов ее решения? 2. Дайте определение блока графа. 3. Докажите, что удаление корня остовного дерева приведет к увеличению числа компонент связности графа. 4. Подпрограмма определения блоков графа вызывается в цикле по вершинам графа. Как определить, требуется ли для данной вершины вызов подпрограммы? 77 Заключение Решение задач, которые выдвигает перед нами повседневная практика, не является привилегией какой-то одной научной дисциплины, они междисциплинарны. Поэтому, по словам известного американского специалиста в области принятия решений Р.Акоффа, «нужно перестать поступать так, словно природа делится на дисциплины, как в университетах». Это заявление, однако, не следует понимать как призыв отказаться от попыток спроецировать реальную задачу на аппарат той или иной дисциплины. Так, например, модели человеческой деятельности, состоящей в принятии решений (формализация изучаемого процесса, описание постановки задачи и цели ее решения, решение возникающей оптимизационной задачи) изучает научная дисциплина «Исследование операций». Однако исследование операций не занимается анализом структурной и алгоритмической сложности, присущей оптимизационным задачам. Решение этих вопросов требует изучения методов построения и оценки эффективности структур и алгоритмов обработки данных, используемых при формализации задач данного класса. Методы управления сложностью данных и объективной оценки эффективности алгоритмов их обработки, в том числе и на примерах задач исследования операций, и составили содержание настоящего пособия. И если автору удалась попытка продемонстрировать ценность, красоту и сложность представленных задач, а также показать читателю основные приемы управления этой сложностью, то цель учебного пособия можно считать достигнутой. В заключение автор хотел бы выразить благодарность студентам кафедры программного обеспечения компьютерных систем за их деятельное участие в разработке и отладке программ, реализующих некоторые из описанных алгоритмов (тексты этих программ приведены в приложении), а также за помощь в подготовке иллюстративного материала. 78 Библиографический список 1. An algorithm for the traveling salesman problem/J. D.C. Little, K.G. Murty, D.W. Sweeney, C. Karel // Operations Research. – 1963. – № 11. – Р. 972–989. 2. Вентцель Е.С. Исследование операций. – М.: Советское радио, 1972. 3. Лекции по теории графов / В.А.Емеличев, О.И.Мельников, В.И.Сарванов, Р.И.Тышкевич. – М.: Наука, ГРФМЛ, 1990. – 384 с. 4. Страуструп Б. Язык программирования С++: Пер.с англ. – М.: Радио и связь, 1991. – 352 с. 5. Липский В. Комбинаторика для программистов: Пер.с польск. – М.: Мир, 1988. – 218 с. 6. Майника Э. Алгоритмы оптимизации на сетях и графах: Пер.с англ. – М.: Мир, 1981. – 323 с. 7. Пантелеев Е.Р., Фомин П.А. Структуры данных и алгоритмы сжатия информации без потерь: Метод. указания/Иван.гос.энерг.ун-т. – Иваново, 2001. – 28 с. 8. Свами М., Тхуласираман К. Графы, сети и алгоритмы: Пер.с англ. – М.: Мир, 1984. – 455 с. 79 Приложение Тексты программ Алгоритм Хаффмана const N=7; {Размер векторов частот и длин } type tArray = ARRAY[0..N-1] of byte; pNode = ^tNode; tNode = record w:word; Left,Right:pNode; end; {построение вектора длин L по вектору частот W; Pos – позиция суммы частот в упорядоченном массиве} procedure LenVect(W:tArray;VAR L:tArray;N:byte;var Pos:byte); VAR Last,Pos1,i,j,L1 : byte; procedure Sort(VAR W:tArray); var i,j,T:byte; begin for i:=0 to N-2 do for j:=i+1 to N-1 do if W[j]>W[i] then begin t:=W[j]; W[j]:=W[i];W[i]:=t end end; function find(W:tArray;N,Last:byte):byte; var i:byte; begin i:=n-1; while W[i]<>Last do dec(i); find:=i end; procedure TypeArr(A:tArray); var i:byte; begin FOR i:=0 TO N-1 DO write(A[i]:4); writeln end; begin Last:=W[N-1]; Sort(W); TypeArr(W); Pos:=find(W,N,Last); if N>2 then begin inc(W[N-2],W[N-1]); LenVect(W,L,N-1,Pos1); L1:=L[Pos1]; j:=0; for i:=0 to N-3 do begin if j=Pos1 then inc(j); L[i]:=L[j]; inc(j) 80 end; L[N-2]:=L1+1;L[N-1]:=L1+1; TypeArr(L); end else begin L[0]:=1;L[1]:=1 end end; function CreateNode:pNode; var ptr:pNode; begin new(ptr); ptr^.w:=0; ptr^.Left:=nil; ptr^.right:=nil; CreateNode:=ptr end; {Вставка кода в дерево Хаффмана} function insert(var t:pNode; L:byte;W:word):boolean; var result:boolean; begin if L>0 then begin if t^.left=nil then begin t^.left:=createnode; insert:=insert(t^.left,L-1,w) end else begin result:=insert(t^.left,L-1,w); if not result then if t^.right=nil then begin t^.right:=createnode; insert:=insert(t^.right,L-1,w) end else insert:=insert(t^.right,L-1,w) end end else if (t^.left=nil)and(t^.right=nil)and(t^.w=0) then begin t^.w:=w; insert:=true end else insert:=false end; procedure scantree(t:pnode;D:byte;code:byte); begin if t<>nil then begin 81 scantree(t^.left,D+1,code); if t^.w<>0 then writeln(t^.w:5,D:5,code:5); scantree(t^.right,D+1,code or 1 shl d); end end; CONST W:tArray = (10,8,7,5,5,4,2); {вектор частот} VAR L:tArray; Pos:byte; i:0..n-1; t:pnode; begin LenVect(W,L,N,Pos);{построить вектор длин} T:=createnode; (создать корень дерева кодирования) for i:=n-1 downto 0 do insert(T,L[i],w[i]); {построить коды} scantree(t,0,0) {посмотреть результат} end. Алгоритм Гилберта-Мура unit UMain; interface uses Windows, Messages, SysUtils, Controls, Forms, Dialogs, StdCtrls, Menus; Variants, const maxCountElement = 100; BIG_DIGIT = 1000000; { n = 4; P: array [1..n] of Integer = (2,1,3,1); Q: array [0..n] of Integer = (1,2,3,2,1); A: array [1..n] of string[10] = ('a1','a2','a3','a4'); 82 Classes, Graphics, B: array [0..n] of string[10] = ('b0','b1','b2','b3','b4'); } type TfrmMain = class(TForm) MainMenu1: TMainMenu; N1: TMenuItem; procedure Button1Click(Sender: TObject); procedure N1Click(Sender: TObject); procedure FormPaint(Sender: TObject); procedure FormCreate(Sender: TObject); private { Private declarations } public n: Integer; P: array [1..maxCountElement-1] of Integer; Q: array [0..maxCountElement-1] of Integer; A: array [1..maxCountElement-1] of string[10]; B: array [0..maxCountElement-1] of string[10]; procedure BuildMatrix; procedure DrawTree(i,number,count,typea: string); function GetLevel(iter: Integer): Integer; //procedure { Public declarations } end; var frmMain: TfrmMain; implementation uses UTree, UStack, UAbout; {$R *.dfm} { TfrmMain } procedure TfrmMain.BuildMatrix; var i,j,l,k,x: Integer; 83 Integer;atext: Cij: Integer; currX,currY: Integer; tempTree,root: TItemTree; W: array [0..maxCountElement-1,0..maxCountElement-1] of Integer; C: array [0..maxCountElement-1,0..maxCountElement-1] of Integer; r: array [0..maxCountElement-1,0..maxCountElement-1] of string[10]; ka: array [0..maxCountElement-1,0..maxCountElement-1 of Integer; Order1,Order2: tempItem,Item: iter: level: number: TOrder; TItem; Integer; Integer; Integer; x_count: Integer; isLast: Boolean; begin // ******************************** FillChar(W,SizeOf(W),0); FillChar(C,SizeOf(C),0); for i:=0 to n do //инициализация begin W[i,i]:=Q[i]; C[i,i]:=0; r[i,i]:=B[i]; // *********************** ka[i,i]:=i; // *********************** end; for l:=1 to n do begin for i:=0 to n-l do begin j:=i+l; //определяем верхнюю гарницу индексов W[i,j]:=W[i,j-1]+P[j]+Q[j]; //определяем постоянную составляющую стоимости - вес // ******************************** Cij:=BIG_DIGIT; for k:=i+1 to j do begin if Cij>(W[i,j]+C[i,k-1]+C[k,j]) then begin Cij:=W[i,j]+C[i,k-1]+C[k,j]; x:=k; 84 end; end; //определяем минимизирующую составляющую // ******************************** C[i,j]:=Cij; //вычисляем стоимость дерева r[i,j]:=A[x]; //вычисляем корень ka[i,j]:=x; //запоминаем к end; end; // *************************************** // рисование дерева Order1:=TOrder.Init; root:=TItemTree.Init; root.Value:=r[0,n]; // *************** данные для стека Item.i:=0; Item.j:=n; Item.ItemTree:=root; Order1.Push(Item); // ******************************** iter:=0; level:=0; number:=0; while not Order1.isEmpty do //строим и рисуем дерево пока очередь не из 667 begin // *************************** Inc(iter); if (EXP(level*ln(2))>(number)) then begin Inc(number); end else begin number:=1; end; level:=GetLevel(iter); // *************************** Item:=Order1.Pop; x_count:=ROUND(EXP(level*ln(2))); if (Item.i=Item.j) or ((Item.i=667)and(Item.j=667)) // если лист или заглушка //ставим в очередь "заглушки" для баланса дерева begin tempTree:=TItemTree.Init; tempItem.i:=667; tempItem.j:=667; Order1.Push(tempItem); Order1.Push(tempItem); if item.i<>667 then 85 then DrawTree(level,number,x_count,1,r[Item.i,Item.j]); end else begin DrawTree(level,number,x_count,0,r[Item.i,Item.j]); k:=ka[Item.i,Item.j]; // левая ветвь tempTree:=TItemTree.Init; tempTree.Value:=r[Item.i,k-1]; Item.ItemTree.AddItemTree(tempTree,1); tempItem.i:=Item.i; tempItem.j:=k-1; tempItem.ItemTree:=tempTree; Order1.Push(tempItem); // правая ветвь tempTree:=TItemTree.Init; tempTree.Value:=r[k,Item.j]; Item.ItemTree.AddItemTree(tempTree,2); tempItem.i:=k; tempItem.j:=Item.j; tempItem.ItemTree:=tempTree; Order1.Push(tempItem); end; end; Order1.Free; end; procedure TfrmMain.Button1Click(Sender: TObject); begin BuildMatrix; end; procedure TfrmMain.DrawTree (i,number,count,typea: Integer;atext: string); const x_width = 10; RADIUS = 5; var x_center,y_center: Integer; x,y,curr_x,curr_y: Integer; self1,x_height: Integer; begin self1:=self.Width; with Canvas do begin Brush.Color:=clBlue; Brush.Style:=bsSolid; Pen.Color:=clBlue; 86 Pen.Style:=psSolid; end; x_height:=self.Height div n; x_center:=ROUND(self1/2); self1:=self.Width-20; y_center:=20; x:=ROUND(self1/(count*2)+self1/(count)*(number-1)); y:=Y_center+x_height*i; if (typea=0) then begin with Canvas do begin Ellipse(x-RADIUS,y-RADIUS,x+RADIUS,y+RADIUS); MoveTo(x,y); LineTo(x-ROUND(self1/(count*4)),y+x_height); MoveTo(x,y); LineTo(x+ROUND(self1/(count*4)),y+x_height); Brush.Style:=bsClear; TextOut(x-20,y-20,atext); end; end else begin with Canvas do begin Ellipse(x-RADIUS,y-RADIUS,x+RADIUS,y+RADIUS); Brush.Style:=bsClear; TextOut(x-20,y-20,atext); end; end; end; function TfrmMain.GetLevel(iter: Integer): Integer; begin Result:=Trunc(Ln(iter)/Ln(2)); end; procedure TfrmMain.N1Click(Sender: TObject); begin Form1.ShowModal; end; procedure TfrmMain.FormPaint(Sender: TObject); begin BuildMatrix; end; procedure TfrmMain.FormCreate(Sender: TObject); var filetext: TextFile; s,aa: string; 87 i,k,f: Integer; begin AssignFile(filetext,'data.txt'); Reset(filetext); ReadLN(filetext,s); n:=StrToInt(s); ReadLN(filetext,s); aa:=''; k:=0; for i:=1 to Length(s) do begin if s[i]<>' ' then begin aa:=aa+s[i]; end else begin Inc(k); P[k]:=StrToInt(aa); aa:=''; end; end; Inc(k); P[k]:=StrToInt(aa); ReadLN(filetext,s); aa:=''; k:=-1; for i:=1 to Length(s) do begin if s[i]<>' ' then begin aa:=aa+s[i]; end else begin Inc(k); Q[k]:=StrToInt(aa); aa:=''; end; end; Inc(k); Q[k]:=StrToInt(aa); for i:=1 to n do A[i]:='a'+IntToStr(i); for i:=0 to n do B[i]:='b'+IntToStr(i); CloseFile(filetext); end; end. 88 Алгоритм Форда-Фалкерсона program Project1; {$APPTYPE CONSOLE} uses SysUtils, Unit1 in 'Unit1.pas', Math; var Graph: TGraph; i,j,n,de: integer; Stack: TStack; PathNotFound: boolean; Verts: TVerts; v1,v2: integer; begin {Задание графа} for i:=1 to MaxN do for j:=1 to MaxN do Graph[i,j].c:=-1; for i:=1 to 10 do begin Write('Vertex 1: '); Readln(v1); Write('Vertrx 2: '); Readln(v2); Write('c(',v1,',',v2,') = '); Readln(Graph[v1,v2].c); Write('f(',v1,',',v2,') = '); Readln(Graph[v1,v2].f); WriteLn; end; Stack:=TStack.Create; {Построение максимального потока} repeat PathNotFound:=true; Stack.Clear; for i:=1 to MaxN do Verts[i].From:=0; Stack.Push(1); Verts[1].From:=MaxN+1; Verts[1].de:=High(Integer); {Построение пути} while (not Stack.Empty) and (Verts[MaxN].From=0) do begin n:=Stack.Pop; for i:=1 to MaxN do if (i<>n) and (Verts[i].From=0) then if (Graph[n,i].c>0) and (Graph[n,i].c<>Graph[n,i].f) then begin 89 Stack.Push(i); Verts[i].From:=n; Verts[i].de:= min(Verts[n].de,Graph[n,i].c-Graph[n,i].f); end else if (Graph[i,n].c>0) and (Graph[n,i].f<>0) then begin Stack.Push(i); Verts[i].From:=-n; Verts[i].de:=min(Verts[n].de,Graph[i,n].f); end; end; {пересчет потоков вдоль пути} if Verts[MaxN].From<>0 then {если сток получил метку} begin PathNotFound:=False; de:=Verts[MaxN].de; n:=MaxN; Write('Pyt(de=',de,'): ',n,' '); while n<>1 do if Verts[n].From>0 then begin inc(Graph[Verts[n].From,n].f,de); n:=Verts[n].From; end else begin dec(Graph[-Verts[n].From,n].f,de); n:=-Verts[n].From; end; end; until PathNotFound; {Вывод результатов} for i:=1 to MaxN do for j:=1 to MaxN do if Graph[i,j].c>0 then Writeln('f(',i,',',j,') = ',Graph[i,j].f); Stack.Free; Readln; end. Транспортная задача uses SysUtils, Math, Stack in 'Stack.pas'; type TMetka = record x1,x2: integer; 90 end; var Post,Potr: array [1..10] of integer; Cena, Matr: array [1..10,1..10] of integer; Metka: array [1..10,1..10] of TMetka; Way: array[1..100] of TMetka; WayLen, Sum: integer; m,n,i,j,ii,k, x1,x2,d: integer; NegativeLoopNotFound, Found: boolean; St: TStackInt; MinD, t1, t2: integer; begin Randomize; Write('количество поставщиков: '); ReadLn(m); Write('количество потребителей: '); ReadLn(n); for i:= 1 to m do begin Write('производительность поставщика №'+IntToStr(i)+'1: '); ReadLn(Post[i]); end; for i:=1 to n do begin Write('спрос потребителя №'+IntToStr(i)+'1: '); ReadLn(Potr[i]); end; {заполнение матрицы тарифов} for i:= 1 to m do for j:= 1 to n do Cena[i,j]:=Random(10)+1; for i:= 1 to m do begin for j:=1 to n do Write(Cena[i,j]:5); Writeln; end; {формирование допустимого плана поставок} for j:=1 to n do for i:=1 to m do begin k:=min(Post[i],Potr[j]); Matr[i,j]:=k; Post[i]:=Post[i]-k; Potr[j]:=Potr[j]-k; end; Sum:=0; WriteLn('Потоки: '); for i:= 1 to m do begin for j:=1 to n do 91 begin Sum:=Sum+Matr[i,j]*Cena[i,j]; Write(Matr[i,j]:3); end; Writeln; end; WriteLn('Сумма = ',Sum); {оптимизация плана поставок} St:=TStackInt.Create; repeat NegativeLoopNotFound:=true; for i:=1 to m do for j:=1 to n do if Matr[i,j]=0 then begin FillChar(Metka,SizeOf(Metka),0); St.Push(i,j,0); Found:=false; {поиск цикла} while not (St.IsEmpty or Found) do begin st.Pop(x1,x2,d); {движение по строке} if d=0 then begin for ii:=1 to m do if (ii<>x1) and (Matr[ii,x2]>0) and (Metka[ii,x2].x1=0) then begin St.Push(ii,x2,1); Metka[ii,x2].x1:=x1; Metka[ii,x2].x2:=x2; end end {движение по столбцу} else begin for ii:=1 to n do if (ii<>x2) and (Matr[x1,ii]>0) and (Metka[x1,ii].x1=0) then begin St.Push(x1,ii,0); Metka[x1,ii].x1:=x1; Metka[x1,ii].x2:=x2; End else {цикл найден} if (x1=i) and (ii=j) then Found:=true; end; end; 92 if Found then begin {расчет стоимости цикла} WayLen:=0; inc(WayLen); Way[WayLen].x1:=x1; Way[WayLen].x2:=x2; repeat t1:=x1; t2:=x2; x1:=Metka[t1,t2].x1; x2:=Metka[t1,t2].x2; inc(WayLen); Way[WayLen].x1:=x1; Way[WayLen].x2:=x2; until (x1=i) and (x2=j); Sum:=0; for ii:=1 to WayLen do begin if ii mod 2 = 0 then inc(sum,Cena[Way[ii].x1,Way[ii].x2]) else dec(sum,Cena[Way[ii].x1,Way[ii].x2]); end; if Sum<0 then {если стоимость отрицательна} begin NegativeLoopNotFound:=false; MinD:=High(Integer); ii:=1; {определение минимального четного элемента} while ii<WayLen do begin if Matr[Way[ii].x1,Way[ii].x2]<MinD then MinD:=Matr[Way[ii].x1,Way[ii].x2]; ii:=ii+2; end; {пересчет потоков вдоль цикла} for ii:=1 to WayLen do begin if ii mod 2 = 0 then inc(Matr[Way[ii].x1,Way[ii].x2],MinD) else dec(Matr[Way[ii].x1,Way[ii].x2],MinD) end; end; end; end; until NegativeLoopNotFound; St.Free; Sum:=0; WriteLn('Оптимальная стоимость поставок: '); for i:= 1 to m do begin 93 for j:=1 to n do begin Sum:=Sum+Matr[i,j]*Cena[i,j]; Write(Matr[i,j]:3); end; Writeln; end; WriteLn('Сумма = ',Sum); Readln; end. 94 Оглавление ВВЕДЕНИЕ .................................................................................. 3 1. АБСТРАКТНЫЕ ТИПЫ ДАННЫХ ......................................... 4 Термины и определения ........................................................ 5 Линейные и нелинейные структуры данных ...................... 6 Примеры реализации и использования АТД ........................ 7 Задания и упражнения для самоконтроля ....................... 14 2. ТЕОРИЯ СЛОЖНОСТИ И ОЦЕНКА ВЫЧИСЛИТЕЛЬНОЙ ЭФФЕКТИВНОСТИ АЛГОРИТМОВ ............................................................. 15 Анализ вычислительной эффективности алгоритмов ... 15 Оптимизационные и распознавательные задачи ............ 18 Недетерминированные алгоритмы. ................................. 19 P и NP - классы сложности распознавательных задач .. 20 Полиномиальная сводимость и трансформация ............ 21 Класс NP - полных задач .................................................... 22 Задания и упражнения для самоконтроля ....................... 22 3. ИСЧЕРПЫВАЮЩИЙ ПОИСК ОПТИМАЛЬНЫХ РЕШЕНИЙ... 24 Стратегии полного перебора ............................................ 24 Метод ветвей и границ ...................................................... 26 Задания и упражнения для самоконтроля ....................... 33 4. ПРИБЛИЖЕННЫЕ МЕТОДЫ ОПТИМИЗАЦИИ: ГЕНЕТИЧЕСКИЕ АЛГОРИТМЫ .................................................................. 34 Задания и упражнения для самоконтроля ....................... 39 5. ЭФФЕКТИВНЫЕ АЛГОРИТМЫ ОПТИМИЗАЦИИ НА ГРАФАХ И СЕТЯХ ........................................................................................ 41 Оптимальные деревья бинарного поиска ......................... 41 Задания и упражнения для самоконтроля ....................... 46 Алгоритм Дейкстры поиска кратчайшего пути ............ 46 Задания и упражнения для самоконтроля ....................... 48 Задача об остове ................................................................ 48 Задача о минимальном остове .......................................... 50 Задания и упражнения для самоконтроля ....................... 55 Метод динамического программирования ....................... 56 Задача о максимальном потоке ........................................ 57 Задания и упражнения для самоконтроля ....................... 63 Транспортная задача ......................................................... 63 Задания и упражнения для самоконтроля ....................... 66 Задача о максимальном паросочетании в двудольном графе .................................................................................................. 66 Задача о плотном расписании занятий ........................... 71 95 Задания и упражнения для самоконтроля ....................... 74 Задача декомпозиции графа на блоки ............................... 74 Задания и упражнения для самоконтроля ....................... 77 ЗАКЛЮЧЕНИЕ ........................................................................... 78 БИБЛИОГРАФИЧЕСКИЙ СПИСОК ............................................... 79 ПРИЛОЖЕНИЕ: ТЕКСТЫ ПРОГРАММ .......................................... 80 Алгоритм Хаффмана ......................................................... 80 Алгоритм Гилберта-Мура ................................................. 82 Алгоритм Форда-Фалкерсона ........................................... 89 Транспортная задача ......................................................... 90 96