Представим себе n чиновников, каждый из которых выдает справки определенного вида. Мы хотим получить все эти справки, соблюдая установленные ограничения: у каждого чиновника есть список справок, которые нужно собрать перед обращением к нему. Дело безнадежно, если схема зависимостей имеет цикл (справку A нельзя получить без B , B без C ,..., Y без Z и Z без A ). Предполагая, что такого цикла нет, требуется составить план, указывающий один из возможных порядков получения справок.
Изображая чиновников точками, а зависимости -- стрелками, приходим к такой формулировке. Имеется n точек, пронумерованных от 1 до n . Из каждой точки ведет несколько (возможно, ) стрелок в другие точки. (Такая картинка называется ориентированным графом.) Циклов нет. Требуется расположить вершины графа (точки) в таком порядке, чтобы конец любой стрелки предшествовал ее началу. Эта задача называется топологической сортировкой.
7.4.1. Доказать, что это всегда возможно.
7.4.2. Предположим, что ориентированный граф без циклов хранится в
такой форме: для каждого i от 1 до n в num[i]
хранится число выходящих из i стрелок, в , , -- номера вершин, куда эти стрелки ведут.
Составить (рекурсивный) алгоритм, который производит
топологическую сортировку не более чем за действий, где m -- число ребер графа
(стрелок).
Решение. Наша программа будет печатать номера вершин. В массиве
printed: array[1..n] of booleanмы будем хранить сведения о том, какие вершины напечатаны (и корректировать их одновременно с печатью вершины). Будем говорить, что напечатанная последовательность вершин корректна, если никакая вершина не напечатана дважды и для любого номера i, входящего в эту последовательность, все вершины, в которые ведут стрелки из i, напечатаны, и притом до i.
procedure add (i: 1..n); | {дано: напечатанное корректно;} | {надо: напечатанное корректно и включает вершину i} begin | if printed [i] then begin {вершина i уже напечатана} | | {ничего делать не надо} | end else begin | | {напечатанное корректно} | | for j:=1 to num[i] do begin | | | add(adr[i][j]); | | end; | | {напечатанное корректно, все вершины, в которые из | | i ведут стрелки, уже напечатаны - так что можно | | печатать i, не нарушая корректности} | | if not printed[i] then begin | | | writeln(i); printed [i]:= TRUE; | | end; | end; end;Основная программа:
for i:=1 to n do begin | printed[i]:= FALSE; end; for i:=1 to n do begin | add(i) end;К оценке времени работы мы вскоре вернемся.
7.4.3. В приведенной программе можно выбросить проверку,
заменив
if not printed[i] then begin | writeln(i); printed [i]:= TRUE; end;на
writeln(i); printed [i]:= TRUE;Почему? Как изменится спецификация процедуры? Решение. Спецификацию можно выбрать такой:
дано: напечатанное корректно надо: напечатанное корректно и включает вершину i; все вновь напечатанные вершины доступны из i.`
7.4.4. Где использован тот факт, что граф не имеет циклов?
Вернемся к оценке времени работы. Сколько вызовов add(i) возможно для какого-то фиксированного i? Прежде всего ясно, что первый из них закрасит i, остальные сведутся к проверке того, что i уже закрашено. Ясно также, что вызовы add(i) индуцируются << закрашивающими>> вызовами add(j) для тех j, из которых в i ведет ребро. Следовательно, число вызовов add(i) равно числу входящих в i ребер (стрелок). При этом все вызовы, кроме первого, требуют O(1) операций, а первый требует времени, пропорционального числу исходящих из i стрелок. (Не считая времени, уходящего на выполнение add(j) для концов j выходящих ребер.) Отсюда видно, что общее время пропорционально числу ребер (плюс число вершин).