Опубликован: 06.10.2011 | Доступ: свободный | Студентов: 1678 / 90 | Оценка: 4.67 / 3.67 | Длительность: 18:18:00
Лекция 11:

Проектирование и инженерия алгоритма: топологическая сортировка

Циклы в ограничениях

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

Проблема в предусловии, требующем, чтобы на вход было подано ациклическое отношение. Программа топологической сортировки получает вход в виде индивидуально упорядоченных пар, например, [Map, Louvre] или [Map, Orsey], как выше. Такой вход может готовиться человеком, а ему свойственно ошибаться, и он имеет право на ошибку, что недопустимо в программе (при рассмотрении сборки промышленных изделий могут встречаться тысячи узлов и десятки тысяч ограничений между ними, задающих порядок сборки).

В примере с терминами словаря мы не можем надеяться, что не встретятся два или более термина, ссылающихся друг на друга в своих определениях (следовательно, создающих циклы), но у нас нет способа принуждать авторов словаря. Справедливо обратное – автор может требовать от программы: "Упорядочьте в словаре термины так, чтобы определение термина встречалось раньше его использования. Но, кстати, если вы обнаружите взаимные ссылки терминов, сообщите мне о них, чтобы я мог скорректировать определения".

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

Эти рассмотрения приводят к изменениям контракта метода process, более благосклонного к своим клиентам. Прежний контракт:

require
            — "constraints описывают ациклическое отношение"
ensure
            — "sorted представляет топологическую сортировку elements,
            — согласованную с constraints"
        

заменяется новым, более реалистичным контрактом:

— (Нет предусловия)
ensure
            — "sorted представляет топологическую сортировку, согласованную
            — с constraints, для всех членов elements, не входящих в цикл"
        

Этого все же недостаточно. Класс должен предоставлять своим клиентам отчет о возникающих циклах и указывать, какие элементы в них включены. Одним из способов обеспечения такой функциональности является введение булевской функции.

has_cycle: BOOLEAN
            — Содержат ли циклы отношение, заданное elements и constraints?
    do …end
        

Говоря неформально, нужна функция, которая будет возвращать список элементов, включенных в цикл (void, если и только если has_cycle имеет значение false). Концептуально это звучит разумно, но это не лучший способ, так как вычислительно представляет дорогое решение. Нахождение циклов – задача функции has_cycle, фактически является столь же трудной по времени и памяти, как и сама задача топологической сортировки, но если мы попытаемся выполнять топологическую сортировку без предусловия, то без особых затрат можем обнаружить циклы в процессе работы.

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

  • Как следует из приведенного выше цикла, на каждом шаге разыскивается элемент, не имеющий предшественника, и это делается до тех пор, пока множество не станет пустым.
  • Теорема говорит нам, что для ацикличного отношения процесс завершится, то есть на каждом шаге непустого множества такой элемент существует.
  • Если мы не можем найти элемент, не имеющий предшественника, а множество еще не пусто, то это означает, что оставшиеся элементы составляют, по меньшей мере, один цикл. Мы можем завершить алгоритм, отчитавшись, что полная топологическая сортировка невозможна. Это благоприятная форма завершения, так как мы топологически отсортировали элементы, не создающие цикл, и имеем возможность сказать клиенту, какие именно элементы и ограничения создают проблему, – оставшиеся после сортировки.

Рассматриваемая далее программа топологической сортировки не имеет предусловия, а ее инвариант цикла также изменен.

Вместо прежнего инварианта:

—"constraints описывают ациклическое отношение на elements"
        

будем теперь использовать его ослабленную форму:

—"constraints описывают подмножество исходного отношения на elements"
        

Отсюда следует, что

—"Любой цикл в constraints присутствует в исходном отношении"
        

а также:

—"constraints описывает ациклическое отношение, если исходное отношение ациклично".
        

Это та основа, которой мы будем придерживаться. Как следствие, мы больше не будем использовать в качестве условия выхода из цикла elements.is_empty, как ранее, так как непустое множество elements не гарантирует больше, что можно корректно выполнить оператор:

"Пусть x – элемент без предшественников в ограничениях"
        

В качестве нового условия выхода будем иметь:

"Нет элементов без предшественников в ограничениях"
        

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

Полная организация класса

Мы можем теперь определить полную форму класса, который будет служить каркасом нашего решения:

class
    TOPOLOGICAL_SORTER [G –> HASHABLE]
feature {NONE} — Внутренние структуры данных
            …Смотри следующие разделы этой главы…
feature — Инициализация
    record_element (a: G)
            — Включить а в множество элементов.
    require
            not_sorted: not done
    do
            …Смотри следующие разделы…
    end
  record_constraint (a, b: G)
    — Включить [a, b] в ограничения.
    require
            not_sorted: not done
    do
            …Смотри следующие разделы…
    end
feature — Status report
  done: BOOLEAN
            — Выполнена ли топологическая сортировка?
feature — Element change
  process
            — Выполнить топологическую сортировку над всеми применимыми элементами.
            — Результаты доступны через sorted, cycle-found и cyclists.
    require
            not_sorted: not done
    do
            …Смотри следующие разделы…
    ensure
            sorted: done
    end
feature — Access
  cycle_found: BOOLEAN
            — Исходное ограничение приводит ли к циклу?
  cyclists: LIST [G]
            — Элементы, включенные в какой-либо цикл.
  sorted: LIST [G]
            — Список из всех элементов, которые допускают упорядочивание,
            — согласованное с ограничениями
feature — Status setting
    reset
            — Допустить дальнейшее обновление элементов и ограничений.
            do
                done:= False
                cycle_found:= False; cyclists:= Void; processed_count:= 0
            ensure
                fresh: not done
            end
invariant
    elements_exist: elements /= Void
    constraints_exist: constraints /= Void
    cyclists_only_if_cycle: done implies (cycle_found = (cyclists /= Void))
end
        
Листинг .
Компоненты класса были перечислены в порядке, облегчающем последовательное чтение. Порядок, рекомендуемый стандартом, будет восстановлен в окончательной версии класса.

Класс является универсальным, его родовой параметр G представляет тип элементов. Запись G -> HASHTABLE означает, что мы "ограничиваем" G (понятие, вводимое в следующей главе) типом HASHTABLE. Причина ограничения типа прояснится немного позже.

Алгоритм будет основываться на внутренних структурах данных, проектируемых в следующих разделах этой главы. Поскольку соответствующие методы создаются для внутреннего использования, они не будут доступны клиентам класса и потому объявлены как feature {NONE}.

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

  • Булевский запрос done, позволяющий клиентам выяснить, выполнена ли топологическая сортировка. При инициализации он получает значение false.
  • Список, формируемый запросом sorted, дает порядок, совместимый со всеми ограничениями, и включает все элементы, не входящие в цикл.

Булевский запрос cycle_found показывает, что есть элементы, включенные в какой-либо цикл.

  • Список всех таких элементов, включенных в цикл, представлен запросом cyclists. Предложение инварианта cyclists_only_if_cycle говорит нам, что запрос имеет смысл только при условии истинности cycle_found.

Клиент, желающий выполнить топологическую сортировку, обычно будет использовать наш класс следующим образом:

your_structure: TOPOLOGICAL_SORTER [YOUR_ELEMENT_TYPE]
…
create your_structure
…Вызовы вида your_structure.record_element(x) для записи элементов …
…и your_structure.record_constraint(x, y) для записи ограничений…
your_structure. process
— Теперь становятся доступными результаты топологической сортировки
— your_structure.sorted
if your_structure. cycle_found then
    — Теперь становятся доступными элементы, включенные в цикл
    — your_structure.cyclists …
end
        

Было бы желательно для согласованности поставлять запросы sorted, cycle_found и cyclists с предусловием done, но мы временно будем опускать предусловие.

Однако предусловие not done задано для процедур инициализации record_element и record_constraint, так же как и для процедуры топологической сортировки process, имеющей постусловие done. Как результат, попытка повторного вызова процедуры process на одном и том же экземпляре класса будет приводить к ошибке. Конечно, запросы можно вызывать сколь угодно много раз. Процедура reset добавлена на случай, когда вы хотите явно добавлять элементы и ограничения уже после вызова process для подготовки нового его вызова.

Процедура reset просто устанавливает done равным false, не производя очистки предыдущих элементов и ограничений. Мы можем добавить процедуру forget, которая вызовет reset и очистит структуры данных. Но в этом случае клиенту проще создать новый экземпляр TOPOLOGICAL_SORTER.