VB, MS Access, VC++, Delphi, Builder C++ принципы(технология), алгоритмы программирования
На рис. 11.9 показана хеш-таблица, которая может содержать 10 ячеек. В
таблице находятся элементы 2, 12, 22 и 32, которые все изначально
отображаются в позицию 2. Если попытаться вставить в таблицу элемент 42, то
нужно будет выполнить длительную тестовую последовательность, которая
обойдет все эти элементы, прежде чем найдет свободную ячейку.
Псевдослучайная проверка
Степень кластеризации растет, если в кластер добавляются элементы, которые
отображаются на уже занятые кластером ячейки. Вторичная кластеризация
возникает, когда для элементов, которые первоначально должны занимать одну
и ту же ячейку, выполняется одна и та же тестовая последовательность, и
образуется вторичный кластер, распределенный по хеш-таблице. Можно
устранить оба эти эффекта, если сделать так, чтобы для разных элементов
выполнялись различные тестовые последовательности, даже если элементы
первоначально и должны были занимать одну и ту же ячейку.
Один из способов сделать это заключается в использовании в тестовой
последовательности генератора псевдослучайных чисел. Для вычисления
тестовой последовательности для элемента, его значение используется для
инициализации генератора случайных чисел. Затем для построения тестовой
последовательности используются последовательные случайные числа,
получаемые на выходе генератора. Это называется псевдослучайной проверкой
(pseudo-random probing).
Когда позднее требуется найти элемент в хеш-таблице, генератор случайных
чисел снова инициализируется значением элемента, при этом на выходе
генератора мы получим ту же самую последовательность чисел, которая
использовалась для вставки элемента в таблицу. Используя эти числа, можно
воссоздать исходную тестовую последовательность и найти элемент.
@Рис. 11.9. Вторичная кластеризация
==========305
Если используется качественный генератор случайных чисел, то разные
значения элементов будут давать различные случайные числа и соответственно
разные тестовые последовательности. Даже если два значения изначально
отображаются на одну и ту же ячейку, то следующие позиции в тестовой
последовательности будут уже различными. В этом случае в хеш-таблице не
будет возникать первичная или вторичная кластеризация.
Можно проинициализировать генератор случайных чисел Visual Basic, используя
начальное число, при помощи двух строчек кода:
Rnd -1
Randomize seed_value
Оператор Rnd дает одну и ту же последовательность чисел после инициализации
одним и тем же начальным числом. Следующий кода показывает, как можно
выполнять поиск элемента с использованием псевдослучайной проверки:
Public Function LocateItem(Value As Long, pos As Integer, _
probes As Integer) As Integer
Dim new_value As Long
' Проинициализировать генератор случайных чисел.
Rnd -1
Randomize Value
probes = 1
pos = Int(Rnd * m_NumEntries)
Do
new_value = m_HashTable(pos)
' Элемент найден.
If new_value = Value Then
LocateItem = HASH_FOUND
Exit Function
End If
' Элемента нет в таблице.
If new_value = UNUSED Or probes > NumEntries Then
LocateItem = HASH_NOT_FOUND
pos = -1
Exit Function
End If
pos = Int(Rnd * m_NumEntries)
probes = probes + 1
Loop
End Function
=======306
Программа Rand демонстрирует открытую адресацию с псевдослучайной
проверкой. Она аналогична программам Linear и Quad, но использует
псевдослучайную, а не линейную или квадратичную проверку.
В табл. 11.4 приведена примерная средняя длина тестовой последовательности,
полученной в программах Quad или Rand для хеш-таблицы со 100 ячейками и
элементами, значения которых находятся в диапазоне от 1 до 999. Обычно
псевдослучайная проверка дает наилучшие результаты, хотя разница между
псевдослучайной и квадратичной проверками не так велика, как между линейной
и квадратичной.
Псевдослучайная проверка также имеет свои недостатки. Так как тестовая
последовательность выбирается псевдослучайно, нельзя точно предсказать,
насколько быстро алгоритм обойдет все элементы в таблице. Если таблица
меньше, чем число возможных псевдослучайных значений, то существует
вероятность того, что тестовая последовательность обратится к одному
значению несколько раз до того, как она выберет другие значения в таблице.
Возможно также, что тестовая последовательность будет пропускать какую-либо
ячейку в таблице и не сможет вставить новый элемент, даже если таблица не
заполнена до конца.
Так же, как и в случае квадратичной проверки, эти эффекты могут вызвать
затруднения, только если таблица почти заполнена. В этом случае увеличение
таблицы дает гораздо больший прирост производительности, чем поиск
неиспользуемых ячеек таблицы.
@Рис. 11.4. Длина поиска при использовании квадратичной и псевдослучайной
проверки
=======307
Удаление элементов
Удаление элементов из хеш-таблицы, в которой используется открытая
адресация, выполняется не так просто, как удаление их из таблицы,
использующей связные списки или блоки. Просто удалить элемент из таблицы
нельзя, так как он может находиться в тестовой последовательности другого
элемента.
Предположим, что элемент A находится в тестовой последовательности элемента
B. Если удалить из таблицы элемент A, найти элемент B будет невозможно. Во
время поиска элемента B встретится пустая ячейка, которая осталась после
удаления элемента A, поэтому будет сделан неправильный вывод о том, что
элемент B отсутствует в таблице.
Вместо удаления элемента из хеш-таблицы можно просто пометить его как
удаленный. Можно использовать эту ячейку позднее, если она встретится во
время выполнения вставки нового элемента в таблицу. Если помеченный элемент
встречается во время поиска другого элемента, он просто игнорируется и
тестовая последовательность продолжится.
После того, как большое число элементов будет помечено как удаленные, в хеш-
таблице может оказаться множество неиспользуемых ячеек, и при поиске
элементов достаточно много времени будет уходить на пропуск удаленных
элементов. В конце концов, может потребоваться рехеширование таблицы для
освобождения неиспользуемой памяти.
Рехеширование
Чтобы освободить удаленные элементы из хеш-таблицы, можно выполнить ее
рехеширование (rehashing) на месте. Чтобы этот алгоритм мог работать, нужно
иметь какой-то способ для определения, было ли выполнено рехеширование
элемента. Простейший способ сделать это — определить элементы в виде
структур данных, содержащих поле Rehashed.
Type ItemType
Value As Long
Rehashed As Boolean
End Type
Вначале присвоим полю Rehashed значение false. Затем выполним проход по
таблице в поиске ячеек, которые не помечены как удаленные, и для которых
еще не было выполнено рехеширование.
Если такой элемент встретится, то выполняется его удаление из таблицы и
повторное хеширование, при этом выполняется обычная тестовая
последовательность для элемента. Если встречается свободная или помеченная
как удаленная ячейка, элемент размещается в ней, помечается как
рехешированный, и продолжается проверка остальных элементов, для которых
еще не было выполнено рехеширование.
Если при выполнении рехеширования найдется элемент, который уже был помечен
как рехешированный, то тестовая последовательность продолжается. Если затем
встретится элемент, для которого еще не было выполнено рехеширование, то
элементы меняются местами, текущая ячейка помечается как рехешированная и
процесс начинается снова.
======308
Изменение размера хеш-таблиц
Если хеш-таблица становится почти заполненной, производительность
значительно падает. В этом случае может понадобиться увеличение размера
таблицы, чтобы в ней было больше места для элементов. И наоборот, если в
таблице слишком мало ячеек, может потребоваться уменьшить ее, чтобы
освободить занимаемую память. Используя методы, похожие на те, которые
использовались при рехешировании таблицы на месте, можно увеличивать и
уменьшать размер хеш-таблицы.
Чтобы увеличить хеш-таблицу, вначале размер массива, в котором она
находится, увеличивается при помощи оператора Dim Preserve. Затем
выполняется рехеширование таблицы, при этом элементы могут занимать ячейки
в созданной свободной области в конце таблицы. После завершения
рехеширования таблица будет готова к использованию.
Чтобы уменьшить размер таблицы, вначале определим, сколько элементов должно
содержаться в массиве таблицы после уменьшения. Затем выполняем
рехеширование таблицы, причем элементы помещаются только в уменьшенную
часть таблицы. После завершения рехеширования всех элементов, размер
массива уменьшается при помощи оператора ReDim Preserve.
Следующий код демонстрирует рехеширование таблицы с использованием линейной
проверки. Код для рехеширования таблицы с использованием квадратичной или
псевдослучайной проверки выглядит почти так же:
Public Sub Rehash()
Dim i As Integer
Dim pos As Integer
Dim probes As Integer
Dim Value As Long
Dim new_value As Long
' Пометить все элементы как нерехешированные.
For i = 0 To NumEntries - 1
m_HashTable(i).Rehashed = False
Next i
' Поиск нерехешированных элементов.
For i = 0 To NumEntries - 1
If Not m_HashTable(i).Rehashed Then
Value = m_HashTable(i).Value
m_HashTable(i).Value = UNUSED
If Value <> DELETED And Value <> UNUSED Then
' Выполнить тестовую последовательность
' для этого элемента, пока не найдется свободная,
' удаленная или нерехешированная ячейка.
probes = 0
Do
pos = (Value + probes) Mod NumEntries
new_value = m_HashTable(pos).Value
' Если ячейка свободна или помечена как
' удаленная, поместить элемент в нее.
If new_value = UNUSED Or _
new_value = DELETED _
Then
m_HashTable(pos).Value = Value
m_HashTable(pos).Rehashed = True
Exit Do
End If
' Если ячейка не помечена как рехешированная,
' поменять их местами и продолжить.
If Not m_HashTable(pos).Rehashed Then
m_HashTable(pos).Value = Value
m_HashTable(pos).Rehashed = True
Value = new_value
probes = 0
Else
probes = probes + 1
End If
Loop
End If
End If
Next i
End Sub
Программа Rehash использует открытую адресацию с линейной проверкой. Она
аналогична программе Linear, но позволяет также помечать объекты как
удаленные и выполнять рехеширование таблицы.
Резюме
Различные типы хеш-таблиц, описанные в этой главе, имеют свои преимущества
и недостатки.
Для хеш-таблиц, которые используют связные списки или блоки можно легко
изменять размер таблицы и удалять из нее элементы. Использование блоков
также позволяет легко работать с таблицами на диске, позволяя считать за
одно обращение к диску сразу множество элементов данных. Тем не менее, оба
эти метода являются более медленными, чем открытая адресация.
Линейная проверка проста и позволяет достаточно быстро вставлять и удалять
элементы из таблицы. Применение упорядоченной линейной проверки позволяет
быстрее, чем в случае неупорядоченной линейной проверки, установить, что
элемент отсутствует в таблице. С другой стороны, вставку элементов в
таблицу при этом выполнить сложнее.
Квадратичная проверка позволяет избежать кластеризации, которая характерна
для линейной проверки, и поэтому обеспечивает более высокую
производительность. Псевдослучайная проверка обеспечивает еще более высокую
производительность, так как при этом удается избавиться как от первичной,
так и от вторичной кластеризации.
В табл. 11.5 приведены преимущества и недостатки различных методов
хеширования.
======310
@Таблица 11.5. Преимущества и недостатки различных методов хеширования
Выбор наилучшего метода хеширования для данного приложения зависит от
данных задачи и способов их использования. При применении разных схем
достигаются различные компромиссы между занимаемой памятью, скоростью и
простотой изменений. Табл. 11.5 может помочь вам выбрать наилучший алгоритм
для вашего приложения.
=======311
Глава 12. Сетевые алгоритмы
В 6 и 7 главах обсуждались алгоритмы работы с деревьями. Данная глава
посвящена более общей теме сетей. Сети играют важную роль во многих
приложениях. Их можно использовать для моделирования таких объектов, как
сеть улиц, телефонная или электрическая сеть, водопровод, канализация,
водосток, сеть авиаперевозок или железных дорог. Менее очевидна возможность
использования сетей для решения таких задач, как разбиение на районы,
составление расписания методом критического пути, планирование коллективной
работы или распределения работы.
Определения
Как и в определении деревьев, сетью (network) или графом (graph) называется
набор узлов (nodes), соединенных ребрами (edges) или связями (links). Для
графа, в отличие от дерева, не определено понятие родительского или
дочернего узла.
С ребрами сети может быть связано соответствующее направление, тогда в этом
случае сеть называется ориентированной сетью (directed network). Для каждой
связи можно также определить ее цену (cost). Для сети дорог, например, цена
может быть равна времени, которое займет проезд по отрезку дороги,
представленному ребром сети. В телефонной сети цена может быть равна
коэффициенту электрических потерь в кабеле, представленном связью. На рис.
12.1 показана небольшая ориентированная сеть, в которой числа рядом с
ребрами соответствуют цене ребра.
Путем (path) между узлами A и B называется последовательность ребер,
которая связывает два этих узла между собой. Если между любыми двумя узлами
сети есть не больше одного ребра, то путь можно однозначно описать,
перечислив входящие в него узлы. Так как такое описание проще представить
наглядно, то пути по возможности описываются таким образом. На рис. 12.1
путь, проходящий через узлы B, E, F, G,E и D, соединяет узлы B и D.
Циклом (cycle) называется путь который связывает узел с ним самим. Путь E,
F, G, E на рис. 12.1 является циклом. Путь называется простым (simple),
если он не содержит циклов. Путь B, E, F, G, E, D не является простым, так
как он содержит цикл E, F, G, E.
Если существует какой-либо путь между двумя узлами, то должен существовать
и простой путь между ними. Этот путь можно найти, если удалить все циклы из
исходного пути. Например, если заменить цикл E, F, G, E в пути B, E, F, G,
E, D на узел E, то получится простой путь B, E, D, связывающий узлы B и D.
=======313
@Рис. 12.1. Ориентированная сеть с ценой ребер
Сеть называется связной (connected), если между любыми двумя узлами
существует хотя бы один путь. В ориентированной сети не всегда очевидно,
является ли сеть связной. На рис. 12.2 сеть слева является связной. Сеть
справа не является связной, так как не существует пути из узла E в узел C.
Представления сети
В 6 главе было описано несколько представлений деревьев. Большинство из них
применимо также и для работы с сетями. Например, представления полными
узлами, списком потомков (списком соседей для сетей) или нумерацией связей
также могут использоваться для хранения сетей. За описанием этих
представлений обратитесь к 6 главе.
@Рис. 12.2. Связная (слева) и несвязная (справа) сети
======314
Для различных приложений могут лучше подходить разные представления сети.
Представление полными узлами обеспечивает хорошие результаты, если каждый
узел в сети связан с небольшим числом ребер. Представление списком соседних
узлов обеспечивает большую гибкость, чем представление полными узлами, а
представление нумерацией связей, хотя его сложнее модифицировать,
обеспечивает более высокую производительность.
Кроме этого, некоторые варианты представления ребер могут упростить работу
с определенными типами сетей. Эти представления используют один класс для
узлов и другой — для представления связей. Применение класса для связей
облегчает работу со свойствами связей, такими, как цена связи.
Например, ориентированная сеть с ценой связей может использовать следующее
определения для класса узла:
Public Id As Integer ' Номер узла.
Public Links As Collection ' Связи, ведущие к соседним узлам.
Можно использовать следующее определение класса связей:
Public ToNode As NetworkNode ' Узел на другом конце связи.
Public Cost As Integer ' Цена связи.
Используя эти определения, программа может найти связь с наименьшей ценой,
используя следующий код:
Dim link As NetworkLink
Dim best_link As NetworkLink
Dim best_cost As Integer
best_cost = 32767
For Each link In node.Links
If link.cost < best_cost Then
Set best_link = link
best_cost = link.cost
End If
Next link
Классы node и link часто расширяются для удобства работы с конкретными
алгоритмами. Например, к классу node часто добавляется флаг Marked. Если
программа обращается к узлу, то она устанавливает значение поля Marked
равным true, чтобы знать, что узел уже был проверен.
Программа, управляющая неориентированной сетью, может использовать немного
другое представление. Класс node остается тем же, что и раньше, но класс
link включает ссылку на оба узла на концах связи.
Public Node1 As NetwokNode ' Один из узлов на конце связи.
Public Node2 As NetwokNode ' Другой узел.
Public Cost As Integer ' Цена связи.
Для неориентированной сети, предыдущее представление использовало бы два
объекта для представления каждой связи — по одному для каждого из
направлений связи. В новой версии каждая связь представлена одним объектом.
Это представление достаточно наглядно, поэтому оно используется далее в
этой главе.
=======315
Используя это представление, программа NetEdit позволяет оперировать
неориентированными сетями с ценой связей. Меню File (Файл) позволяет
загружать и сохранять сети в файлах. Команды в меню Edit (Правка) позволяют
вам вставлять и удалять узлы и связи. На рис. 12.3 показано окно программы
NetEdit.
Директория OldSrc\Ch12 содержит программы, которые используют представление
нумерацией связей. Эти программы немного сложнее понять, но они обычно
работают быстрее. Они не описаны в тексте, но использованные в них методы
похожи на те, которые применялись в программах, написанных для 4 версии
Visual Basic. Например, обе программы Src\Ch12\Paths и OldSrc\Ch12\Paths
находят кратчайший маршрут, используя описанный ниже алгоритм установки
меток. Основное отличие между ними заключается в том, что первая программа
использует коллекции и классы, а вторая — псевдоуказатели и представление
нумерацией связей.
Оперирование узлами и связями
Корень дерева — это единственный узел, не имеющий родителя. Можно найти
любой узел в сети, начав от корня и следуя по указателям на дочерние узлы.
Таким образом, узел представляет основание дерева. Если ввести переменную,
которая будет содержать указатель на корневой узел, то впоследствии можно
будет получить доступ ко всем узлам в дереве.
Сети не всегда содержат узел, который занимает такое особое положение. В
несвязной сети может не существовать способа обойти все узлы по связям,
начав с одного узла.
Поэтому программы, работающие с сетями, обычно содержат полный список всех
узлов в сети. Программа также может хранить полный список всех связей. При
помощи этих списков можно легко выполнить какие-либо действия над всеми
узлами или связями в сети. Например, если программа хранит указатели на
узлы и связи в коллекциях Nodes и Links, она может вывести сеть на экран
при помощи следующего метода:
@Рис. 12.3. Программа NetEdit
=======316
Dim node As NetworkNode
dim link As NetworkLink
For Each link in links
' Нарисовать связь.
:
Next link
For Each node in nodes
' Нарисовать узел.
:
Next node
Программа NetEdit использует коллекции Nodes и Links для вывода сетей на
экран.
Обходы сети
Обход сети выполняется аналогично обходу дерева. Можно обходить сеть,
используя либо обход в глубину, либо обход в ширину. Обход в ширину обычно
похож на прямой обход дерева, хотя для сетей можно определить также
обратный и даже симметричный обход.
Алгоритм для выполнения прямого обхода двоичного дерева, описанный в 6
главе, формулируется так:
Обратиться к узлу.
Выполнить рекурсивный прямой обход левого поддерева.
Выполнить рекурсивный прямой обход правого поддерева.
В дереве между связанными между собой узлами существует отношение родитель-
потомок. Так как алгоритм начинается с корневого узла и всегда выполняется
сверху вниз, он не обращается дважды ни к одному узлу.
В сети узлы не обязательно связаны в направлении сверху вниз. Если
попытаться применить к сети алгоритм прямого обхода, может возникнуть
бесконечный цикл.
Чтобы избежать этого, алгоритм должен помечать узел после обращения к нему,
при этом при поиске в соседних узлах, обращение происходит только к узлам,
которые еще не были помечены. После того, как алгоритм завершит работу, все
узлы в сети будут помечены (если сеть является связной). Алгоритм прямого
обхода сети формулируется так:
Пометить узел.
Обратиться к узлу.
3. Выполнить рекурсивный обход не помеченных соседних узлов.
========317
В Visual Basic можно добавить флаг Marked к классу NetworkNode.
Public Id As Long
Public Marked As Boolean
Public Links As Collection
Класс NetworkNode может включать открытую процедуру для обхода сети,
начиная с этого узла. Процедура узла PreorderPrint обращается ко всем
непомеченным узлам, которые доступны из данного узла. Если сеть является
связной, то при таком обходе произойдет обращение ко всем узлам сети.
Public Sub PreorderPrint()
Dim link As NoworkLink
Dim node As NetworkNode
' Пометить узел.
Marked = True
' Обратиться к непомеченным узлам.
For Each link In Links
' Найти соседний узел.
If link.Node1 Is Me Then
Set node = link.Node2
Else
Set node = link.Node1
End If
' Определить, требуется ли обращение к соседнему узлу.
If Not node.Marked Then node.PreorderPrint
Next link
End Sub
Так как эта процедура не обращается ни к одному узлу дважды, то коллекция
обходимых связей не содержит циклов и образует дерево.
Если сеть является связной, то дерево будет обходить все узлы сети. Так как
это дерево охватывает все узлы сети, то оно называется остовным деревом
(spanning tree). На рис. 12.4 показана небольшая сеть с остовным деревом с
корнем в узле A, которое изображено жирными линиями.
Можно использовать похожий подход с пометкой узлов для преобразования
обхода дерева в ширину в сетевой алгоритм. Алгоритм обхода дерева
начинается с помещения корневого узла в очередь. Затем первый узел
удаляется из очереди, происходит обращение к узлу, и затем в конце очереди
помещаются его дочерние узлы. Затем этот процесс повторяется до тех пор,
пока очередь не опустеет.
======318
@Рис. 12.4. Остовное дерево
В алгоритме обхода сети нужно вначале убедиться, что узел не проверялся
раньше или он уже не находится в очереди. Для этого мы помечаем каждый
узел, который помещается в очередь. Сетевая версия этого алгоритма выглядит
так:
1. Пометить первый узел (который будет корнем остовного дерева) и добавить
его в конец очереди.
2. Повторять следующие шаги до тех пор, пока очередь не опустеет:
a) Удалить из очереди первый узел и обратиться к нему.
b) Для каждого из непомеченных соседних узлов, пометить его и добавить в
конец очереди.
Следующая процедура печатает список узлов сети в порядке обхода в ширину:
Public Sub BreadthFirstPrint(root As NetworkNode)
Dim queue As New Collection
Dim node As NetworkNode
Dim neighbor As NetworkNode
Dim link As NetworkLink
' Поместить корень в очередь.
root.Marked = True
queue.Add root
' Многократно помещать верхний элемент в очередь
' пока очередь не опустеет.
Do While queue.Count > 0
' Выбрать следующий узел из очереди.
Set node = queue.Item(1)
queue.Remove 1
' Обратиться к узлу.
Print node.Id
' Добавить в очередь все непомеченные соседние узлы.
For Each link In node.Links
' Найти соседний узел.
If link.Node1 Is Me Then
Set neighbor = link.Node2
Else
Set neighbor = link.Node1
End If
' Проверить, нужно ли обращение к соседнему узлу.
If Not neighbor.Marked Then queue.Add neighbor
Next link
Loop
End Sub
Наименьшие остовные деревья
Если задана сеть с ценой связей, то наименьшим остовным деревом (minimal
spanning tree) называется остовное дерево, в котором суммарная цена всех
связей в дереве будет наименьшей. Наименьшее остовное дерево можно
использовать, чтобы связать все узлы в сети путем с наименьшей ценой.
Например, предположим, что требуется разработать телефонную сеть, которая
должна соединить шесть городов. Можно проложить магистральный кабель между
всеми парами городов, но это будет неоправданно дорого. Меньшую стоимость
будет иметь решение, при котором города будут соединены связями, которые
содержатся в наименьшем остовном дереве. На рис. 12.5 показаны шесть
городов, каждые два из которых соединены магистральным кабелем. Жирными
линиями нарисовано наименьшее остовное дерево.
Заметьте, что сеть может иметь несколько наименьших остовных деревьев. На
рис. 12.6 показаны два изображения сети с двумя различными наименьшими
остовными деревьями, которые нарисованы жирными линиями. Полная цена обоих
деревьев равна 32.
@Рис. 12.5. Магистральные телефонные кабели, связывающие шесть городов
========320
@Рис. 12.6. Два различных наименьших остовных дерева для одной сети
Существует простой алгоритм поиска наименьшего остовного дерева для сети.
Вначале поместим в остовное дерево любой узел. Затем найдем связь с
наименьшей ценой, которая соединяет узел в дереве с узлом, который еще не
помещен в дерево. Добавим эту связь и соответствующий узел в дерево. Затем
эта процедура повторяется до тех пор, пока все узлы не окажутся в дереве.
Этот алгоритм похож на эвристику восхождения на холм, описанную в 8 главе.
На каждом шаге оба алгоритма изменяют решение, пытаясь его максимально
улучшить. Алгоритм остовного дерева на каждом шаге выбирает связь с
наименьшей ценой, которая добавляет новый узел в дерево. В отличие от
эвристики восхождения на холм, которая не всегда находит наилучшее решение,
этот алгоритм гарантированно находит наименьшее остовное дерево.
Подобные алгоритмы, которые находят глобальный оптимум, при помощи серии
локально оптимальных приближений называются поглощающими алгоритмами(greedy
algorithms). Можно представлять себе поглощающие алгоритмы как алгоритмы
типа восхождения на холм, не являющиеся при этом эвристиками — они
гарантированно находят наилучшее возможное решение.
Алгоритм наименьшего остовного дерева использует коллекцию для хранения
списка связей, которые могут быть добавлены к остовному дереву. Вначале
алгоритм помещает в этот список связи корневого узла. Затем проводится
поиск связи с наименьшей ценой в этом списке. Чтобы максимально ускорить
поиск, программа может использовать приоритетную очередь типа описанной в 9
главе. Или наоборот, чтобы упростить реализацию, программа может
использовать для хранения списка возможных связей коллекцию.
Если узел на другом конце связи еще не находится в остовном дереве, то
программа добавляет его и соответствующую связь в дерево. Затем она
добавляет связи, выходящие из нового узла, в список возможных узлов.
Алгоритм использует флаг Used в классе link, чтобы определить, попадала ли
эта связь ранее в список возможных связей. Если да, то она не заносится в
этот список снова.
Может оказаться, что список возможных связей опустеет до того, как все узлы
будут добавлены в остовное дерево. В этом случае сеть является несвязной, и
не существует путь, который связывает корневой узел со всеми остальными
узлами сети.
=========321
Private Sub FindSpanningTree(root As SpanNode)
Dim candidates As New Collection
Dim to_node As SpanNode
Dim link As SpanLink
Dim i As Integer
Dim best_i As Integer
Dim best_cost As Integer
Dim best_to_node As SpanNode
If root Is Nothing Then Exit Sub
' Сбросить флаг Marked для всех узлов и флаги
' Used и InSpanningTree для всех связей.
ResetSpanningTree
' Начать с корня остовного дерева.
root.Marked = True
Set best_to_node = root
Do
' Добавить связи последнего узла в список
' возможных связей.
For Each link In best_to_node.Links
If Not link.Used Then
candidates.Add link
link.Used = True
End If
Next link
' Найти самую короткую связь в списке возможных
' связей, которая ведет к узлу, которого еще нет
' в дереве.
best_i = 0
best_cost = INFINITY
i = 1
Do While i 0
' Найти ближайший к корню узел-кандидат.
best_dist = INFINITY
For i = 1 To candidates.Count
new_dist = candidates(i).Dist
If new_dist < best_dist Then
best_i = i
best_dist = new_dist
End If
Next i
' Добавить узел к дерева кратчайшего маршрута.
Set node = candidates(best_i)
candidates.Remove best_i
node.NodeStatus = WAS_IN_LIST
' Проверить соседние узлы.
For Each link In node.Links
If node Is link.Node1 Then
Set to_node = link.Node2
Else
Set to_node = link.Node1
End If
If to_node.NodeStatus = NOT_IN_LIST Then
' Узел раньше не был в списке возможных
' узлов. Добавить его в список.
candidates.Add to_node
to_node.NodeStatus = NOW_IN_LIST
to_node.Dist = best_dist + link.Cost
Set to_node.InLink = link
ElseIf to_node.NodeStatus = NOW_IN_LIST Then
' Узел находится в списке возможных узлов.
' Обновить значения его полей Dist и inlink,
' если это необходимо.
new_dist = best_dist + link.Cost
If new_dist < to_node.Dist Then
to_node.Dist = new_dist
Set to_node.InLink = link
End If
End If
Next link
Loop
GotPathTree = True
' Пометить входящие узлы, чтобы их было проще вывести на экран.
For Each node In Nodes
If Not (node.InLink Is Nothing) Then _
node.InLink.InPathTree = True
Next node
' Перерисовать сеть.
DrawNetwork
End Sub
Важно, чтобы алгоритм обновлял поля InLink и Dist только для узлов, в
которых поле NodeStatus равно NOW_IN_LIST. Для большинства сетей нельзя
получить более короткий путь, добавляя узлы, которые не находятся в списке
возможных узлов. Тем не менее, если сеть содержит цикл, полная длина
которого отрицательна, алгоритм может обнаружить, что можно уменьшить
расстояние до некоторых узлов, которые уже находятся в дереве кратчайшего
маршрута, при этом две ветви дерева кратчайшего маршрута окажутся
связанными друг с другом, так что оно перестанет быть деревом.
На рис. 12.10 показана сеть с циклом отрицательной цены и «дерево»
кратчайшего маршрута, которое получилось бы, если бы алгоритм обновлял цену
узлов, которые уже находятся в дереве.
=======329
@Рис. 12.10. Неправильное «дерево» кратчайшего маршрута для сети с циклом
отрицательной цены
Программа PathS использует этот алгоритм установки меток для вычисления
кратчайшего маршрута. Она аналогична программам NetEdit и Span. Если вы не
вставляете или не удаляете узел или связь, то можно выбрать узел при помощи
мыши и программа при этом найдет и выведет на экран дерево кратчайшего
маршрута с корнем в этом узле. На рис. 12.11 показано окно программы PathS
с деревом кратчайшего маршрута с корнем в узле 3.
@Рис. 12.11. Дерево кратчайшего маршрута с корнем в узле 3
=======330
Варианты метода установки меток
Узкое место этого алгоритма заключается в поиске узла с наименьшим
значением поля Dist в списке возможных узлов. Некоторые варианты этого
алгоритма используют другие структуры данных для хранения списка возможных
узлов. Например, можно было бы использовать упорядоченный связный список.
При использовании этого метода потребуется только один шаг для того, чтобы
найти следующий узел, который будет добавлен к дереву кратчайшего маршрута.
Этот список будет всегда упорядоченным, поэтому узел на вершине списка
всегда будет искомым узлом.
Это облегчит поиск нужного узла в списке, но усложнит добавление узла в
него. Вместо того чтобы просто помещать узел в начало списка, его придется
поместить в нужную позицию.
Иногда также требуется перемещать узлы в списке. Если в результате
добавления узла в дерево кратчайшего маршрута уменьшилось кратчайшее
расстояние до другого узла, который уже был в списке, то нужно переместить
этот элемент ближе к вершине списка.
Предыдущий алгоритм и этот его новый вариант представляют собой два крайних
случая управления списком возможных узлов. Первый алгоритм совсем не
упорядочивает список и тратит достаточно много времени на поиск узлов в
сети. Второй тратит много времени на поддержание упорядоченности списка, но
может очень быстро выбирать из него узлы. Другие варианты используют
промежуточные стратегии.
Например, можно использовать для хранения списка возможных узлов
приоритетную очередь на основе пирамид, тогда можно будет просто выбрать
следующий узел с вершины пирамиды. Вставка нового узла в пирамиду и ее
переупорядочение будет выполняться быстрее, чем аналогичные операции для
упорядоченного связного списка. Другие стратегии используют сложные схемы
организации блоков для того, чтобы упростить поиск возможных узлов.
Некоторые из этих вариантов достаточно сложны. Из-за этой их сложности эти
Страницы: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
|