Aлгорітми на графах h2>
Елементи теорії графів. h2>
Граф
- Сукупність точок і ліній, в якій кожна лінія з'єднує дві точки. Точки
називаються вершинами, або вузлами, графа, лінії - ребрами графа. Якщо ребро
з'єднають дві вершини, то говорять, що воно їм інцідентно; вершини, з'єднані
ребром називаються суміжними. Дві вершини, з'єднані ребром, можуть збігатися;
таке ребро називається петлею. Число ребер, інцідентних вершині, називається
ступенем вершини. Якщо два ребра інцідентни одній і тій же парі вершин, вони
називаються кратними; граф, що містить кратні ребра, називається мультіграфом. p>
Ребро,
що з'єднує дві вершини, може мати направлення від однієї вершини до іншої; в
цьому випадку воно називається спрямованим, або орієнтованим, і зображується
стрілкою. Граф, у якому всі ребра орієнтовані, називається орієнтованим
графом (Орграф); ребра Орграф часто називають дугами. Дуги іменуються
кратними, якщо вони не тільки мають спільні вершини, але й збігаються за напрямком.
Іноді потрібно розглядати не весь граф, а його частина (частина вершин і частина
ребер). Частина вершин і всі інцідентние їм ребра називаються подграфом; все
вершини і частина інцідентних їм ребер називаються суграфом. Циклом називається
замкнута ланцюг вершин. Деревом називається граф без циклів. Остовне деревом
називається пов'язаний суграф графа, що не має циклів. p>
Граф
однозначно заданий, якщо задані безліч його вершин, безліч ребер і зазначені
всі інцідентності (тобто вказано, які вершини якими ребрами з'єднані).
Найбільш наочно граф задається малюнком, а проте не всі деталі малюнка однаково
важливі; зокрема, несуттєві геометричні властивості ребер (довжина,
кривизна і т.д.) і взаємне розташування вершин на площині. p>
Для
неорієнтованого ребра порядок, в якому вказані що з'єднуються з ним вершини, не
важливий. Для орієнтованого ребра важливо: перше вказується вершина, з якої
виходить ребро. p>
Маршрут,
або шлях - це послідовність ребер в неорієнтовані графі, в якому
кінець кожного ребра співпадає з початком наступного ребра. Число ребер маршруту
називається його довжиною. p>
Графи
широко використовуються як у самій математиці, так і в її додатках. Вони
застосовуються при побудові різних математичних моделей: ліній
електропередачі, мереж автодоріг, ліній повітряних сполучень і пр. p>
Завдання
полягає в тому, знайти шлях з вершини A в вершину B. Будемо ставити граф
матрицею суміжності, тобто квадратною таблицею NxN, в якій на перетині i-й
рядка і j-го стовпця значення TRUE, якщо i і j з'єднані ребром, і FALSE в
Інакше. p>
Пошук в ширину. h2>
Подібно
до того як, згідно з принципом Гюйгенса, кожна точка хвильового фронту є
джерелом вторинної хвилі, ми, відправляючись із заданої вершини A, відвідуємо всі
суміжні з нею вершини (тобто вершини, до яких ведуть стрілки з A). Кожна
Відвідане вершина стає джерелом нової хвилі і т.д. При цьому необхідно
подбати про те, щоб не повернуться в ту вершину, в якій вже були. p>
Для
реалізації алгоритму знадобляться: p>
матриця
m [1 .. n, 1 .. n] - матриця суміжності графа; p>
допоміжний
масив queue [1 .. n], в якому буде формуватися чергу, тобто тип даних
першим увійшов - першим вийшов (FIFO). Розмір його достатній, тому що ми не
відвідуємо вершини двічі. З масивом queue пов'язані дві змінні - head і tail.
У змінної head перебуватиме номер поточної вершини, з якої йде
хвиля, а за допомогою змінної tail нові вершини поміщаються в
"хвіст" черги queue; p>
допоміжний
масив visited [1 .. n], який потрібен для того, щоб відзначати вже пройдені вершини
(visited [i] = TRUE <=> вершина i пройдена); p>
допоміжний
масив prev [1 .. n] для зберігання пройдених вершин. У цьому масиві і буде
сформований шуканий шлях; p>
мінлива
f, яка прийме значення TRUE, коли шлях буде знайдений. p>
Крім
того, ми введемо декілька допоміжних змінних, які знадобляться при
організації циклів. p>
Program breadth_first_search; p>
Const n = 9; p>
m: array [1 .. n, 1 .. n] of boolean = p>
( p>
(False, True, True, False, False,
False, False, False, False), p>
(True, False, True, False, False,
False, True, True, False), p>
(True, True, False, True, True,
False, False, False, False), p>
(False, False, True, False, True,
False, False, False, False), p>
(False, False, True, True, False,
True, False, True, False), p>
(False, False, False, False, True,
False, True, True, True), p>
(False, True, False, False, False,
True, False, True, True), p>
(False, True, False, False, True,
True, True, False, False), p>
(False, False, False, False, False,
True, True, False, False) p>
); p>
Var A, B: integer; p>
Procedure A_to_B (A, B: integer); p>
Var p>
Visited: array [1 .. n] of boolean; p>
Prev: array [1 .. n] of integer; p>
c: array [1 .. n] of integer; p>
head, tail: integer; p>
f: boolean; p>
i, v, k: integer; p>
Begin p>
head: = 1; p>
tail: = 1; p>
f: = False; p>
For i: = 1 to n do p>
Begin p>
Visited [i]: = False; p>
Prev [i]: = 0 p>
End; p>
C [tail]: = A; p>
Visited [A]: = True; p>
While (head <= tail) and not f do p>
Begin p>
v: = C [head]; p>
head: = head 1; p>
For k: = 1 to n do p>
if m [v, k] and not Visited [k] then p>
Begin p>
tail: = tail 1; p>
C [tail]: = k; p>
Visited [k]: = True; p>
Prev [k]: = v; p>
if k = B then p>
Begin p>
f: = true; p>
break p>
End p>
End p>
End; p>
if f then p>
Begin p>
k: = B; p>
Write (B); p>
While Prev [k] <> 0 do p>
Begin p>
Write ('<-', Prev [k ]); p>
k: = Prev [k] p>
end p>
End p>
else p>
Write ( 'Шляхи
з ', A,' в ', B,' немає ') p>
end; p>
Begin p>
Write ( 'A ='); readln (A); p>
Write ( 'B ='); readln (B); p>
A_to_B (A, B) p>
End. p>
Пошук b> b> в b> b> глибину b> . b>
Ідея
пошуку в глибину проста: відправляючись від поточної вершини, ми знаходимо нову (ще
не пройдений) суміжну з нею вершину, яку помічаємо як пройдену і
оголошуємо поточної. Після цього процес відновлюється. Якщо нової суміжній
вершини немає (тупик), повертаємося до тієї вершини, з якої потрапили в поточну, і
робимо наступну спробу. Якщо потрапимо в вершину B, друкуємо шлях. Якщо все
вершини вичерпані - такого шляху немає. p>
Зауважимо,
що побудований таким чином алгоритм здатний знаходити всі шляхи з A в B, але
перший знайдений необов'язково повинен бути найкоротшим. p>
Як
звичайно, алгоритм з поверненнями найлегше оформити за допомогою рекурсивної
процедури. Для її реалізації нам знадобляться: p>
матриця
m [1 .. n, 1 .. n] - матриця суміжності графа; p>
допоміжний
масив visited [1 .. n], який ми будемо для того, щоб відзначати вже пройдені
вершини (visited [i] = TRUE <=> вершина i пройдена); p>
мінлива
f, яка прийме значення TRUE, коли шлях буде знайдений. p>
Program depth_first_search; p>
Const n = 9; p>
m: array [1 .. n, 1 .. n] of boolean = p>
( p>
(False, True, True, False, False,
False, False, False, False), p>
(True, False, True, False, False,
False, True, True, False), p>
(True, True, False, True, True,
False, False, False, False), p>
(False, False, True, False, True,
False, False, False, False), p>
(False, False, True, True, False,
True, False, True, False), p>
(False, False, False, False, True,
False, True, True, True), p>
(False, True, False, False, False,
True, False, True, True), p>
(False, True, False, False, True,
True, True, False, False), p>
(False, False, False, False, False,
True, True, False, False) p>
); p>
Var A, B: integer; p>
Procedure A_to_b (A, B: integer); p>
Var p>
Visited: array [1 .. n] of boolean; p>
f: boolean; p>
i: integer; p>
Procedure Depth (p: integer); p>
var k: integer; p>
Begin p>
Visited [p]: = True; p>
For k: = 1 to n do p>
If not f then p>
If m [p, k] and not Visited [k] then p>
If k = B then p>
Begin p>
f: = True; p>
Write (B); p>
Break p>
End p>
else Depth (k); p>
If f then write ('<=', p); p>
End; p>
Begin p>
For i: = 1 to n do Visited [i]: = False; p>
f: = false; p>
Depth (A); p>
If not f then write ( 'Шляхи з', A, 'в', B,
'Немає') p>
End; p>
Begin p>
write ( 'A ='); readln (A); p>
write ( 'B ='); readln (B); p>
A_to_B (A, B) p>
End. p>
Ейлерови b> b> цикли b> . b> p>
Потрібно
знайти цикл, що проходить по кожній дузі рівно один раз. Це завдання вперше
поставив і вирішив Леонард Ейлер, чим і заклав основи теорії графів, а
відповідні цикли тепер називаються ейлеровимі. p>
Завдання
виникла із запропонованої Ейлера головоломки, що одержала назву "проблема
кенігсберзькими мостів ". Річка Прегель, протікають ющая через Калінінград (перш
місто називалося Кенігсбергом), омиває два острови. Береги річки пов'язані з
островами так, як це показано на малюнку. У головоломці потрібно було знайти
маршрут, що проходить по всіх ділянках суші таким чином, щоб кожен з мостів
був пройдений рівно один раз, а початковий і кінцевий пункти маршруту збігалися. p>
Виберемо
в якості вершин графа берега річки, а в якості ребер - мости та їх
з'єднують. Після цього завдання стає очевидною: вимога нездійсненно
- Щоб його виконати, число дуг, що приходять до кожної вершини, має бути
парних. Справді, оскільки по одному мосту не можна проходити двічі, кожному
входу на берег повинен відповідати вихід. p>
Що
необхідно, щоб у графі існував Ейлером цикл? По-перше, граф повинен бути
пов'язаним: для будь-яких двох вершин повинен існувати шлях, що з'єднує їх.
По-друге, для неорієнтовані графів число ребер в кожній вершині має
бути парним. Насправді цього виявляється достатньо. P>
Теорема.
Щоб у зв'язаному графі існував Ейлером цикл, необхідно і достатньо,
щоб число ребер в кожній вершині було парних. p>
Доказ.
Необхідність доведена вище. Доказ достатності конструктивно - в
результаті буде отримано потрібний алгоритм. p>
Доказ
ведеться індукцією за кількістю ребер графа. Нехай твердження доведено для всіх
m
Зауважимо,
що, поки ми не повернулися в А, ми завжди можемо продовжити шлях з поточного вузла
X принаймні на одне ребро. Справді, кожен прохід через X залишає
парних число ребер у цій вершині. Тому, якщо ми входимо в X, залишається
непарне число невичеркнутих ребер, з неї виходять (принаймні одне
ребро). Нехай ми повернулися в A. Тоді, якщо всі ребра викреслені, теорема
доведена. В іншому випадку залишилися ребра розпадаються на пов'язані
подграфи, кожен з яких містить хоча б одну вершину з вже побудованого
циклу (оскільки початковий граф пов'язаний) і містить менше n ребер.
Нехай, ..., - вершини зазначених подграфов, через які проходить побудований
шлях. p>
За
припущенням індукції в кожному з них існує Ейлером цикл. Будуємо тепер
новий шлях з A наступним чином: p>
--
йдемо по старому шляху до; p>
--
включаємо в нього новий шлях; p>
--
йдемо по старому шляху від до; p>
--
повторюємо процес для вершини і т.д. p>
Для
закінчення докази (і побудови алгоритму) зауважимо, що база індукції
очевидна: якщо одне ребро, то воно - петля. p>
Хоча
доказ проведено для неорієнтовані графів, воно відразу переноситься на
орієнтовані, тільки вимога парності замінюється тепер на таке: число
що входять в кожну вершину ребер має дорівнювати числу що виходять. p>
Залишилось
помітити, що запропонований в доказі алгоритм лине, тобто число
дій прямо пропорційно числу ребер. p>
Program Euler; p>
const n = 9; p>
m: array [1 .. n, 1 .. n] of boolean = p>
( p>
(False, True, True, False, False,
False, False, False, False), p>
(True, False, True, False, False,
False, True, True, False), p>
(True, True, False, True, True,
False, False, False, False), p>
(False, False, True, False, True,
False, False, False, False), p>
(False, False, True, True, False,
True, False, True, False), p>
(False, False, False, False, True,
False, True, True, True), p>
(False, True, False, False, False,
True, False, True, True), p>
(False, True, False, False, True,
True, True, False, False), p>
(False, False, False, False, False,
True, True, False, False) p>
); p>
Type p>
list = ^ node; p>
node = record p>
i: integer; p>
next: list p>
end; p>
Var stack1, stack2: list; p>
v, u, x, i: integer; p>
Procedure Push (x: integer; var
stack: list); p>
Var temp: list; p>
Begin p>
New (temp); p>
temp ^. i: = x; p>
temp ^. next: = stack; p>
stack: = temp p>
End; p>
Procedure Pop (var x: integer; var
stack: list); p>
Begin p>
x: = stack ^. i; p>
stack: = stack ^. next p>
End; p>
Function Peek (stack: list): integer; p>
Begin p>
Peek: = stack ^. i p>
End; p>
Procedure PrintList (l: list); p>
Begin p>
Writeln; p>
If l = nil then writeln ( 'NIL'); p>
While l <> nil do p>
Begin p>
Write (l ^. i: 3); p>
l: = l ^. next p>
End p>
End; p>
Begin p>
stack1: = nil; p>
stack2: = nil; p>
Write ( 'Початкова
вершина: '); readln (v); p>
Push (v, stack1); p>
While stack1 <> NIL do p>
Begin p>
v: = peek (stack1); p>
i: = 1; p>
While (i <= n) and not m [v, i] do
inc (i); p>
If i <= n then p>
Begin p>
u: = i; p>
Push (u, stack1); p>
m [v, u]: = False; p>
m [u, v]: = False; p>
End p>
else p>
Begin p>
pop (x, stack1); p>
push (x, stack2) p>
End p>
End; p>
PrintList (stack2) p>
End. p>
Завдання Прима-Краскала. h2>
Дана
плоска країна і в ній n міст. Потрібно з'єднати всі міста телефонним зв'язком
так, щоб загальна довжина телефонних ліній була мінімальною. p>
Або
в термінах теорії графів: p>
Дан
граф з n вершинами; довжини ребер задані матрицею. Знайти остовне дерево
мінімальної довжини. p>
Уявімо
собі, що зимівники на деякий запас продуктів, і його завданням є
складання смачного меню на всю зиму. Якщо зимівник почне з того, що спершу
буде їсти саму смачну їжу (наприклад, шоколад), потім - другу за смакоту
(наприклад, м'ясо), то він ризикує залишити на останній місяць тільки сіль і
маргарин. Подібним чином, якщо оптимальний (для визначеності, мінімальний)
об'єкт будується як-то по кроках, то не можна на першому кроці вибирати що-небудь
найменше, на другому кроці - що залишився щонайменше і т.д. За таку політику
зазвичай доводиться розплачуватися на останні кроки. Такий алгоритм називається
жадібним. p>
Дивно,
але в задачі Прима-Краскала, яка не здається особливо простий, жадібний
алгоритм дає точне оптимальне рішення. p>
Як
відомо (це легко довести по індукції), дерево з n вершинами має n-1
ребер. Виявляється, кожне ребро потрібно вибирати жадібно (лише б не виникали
цикли). Тобто n-1 раз вибирати найкоротший ребро з ще не вибране ребро
за умови, що воно не утворює циклу з вже обраними. p>
А
як стежити, щоб нове ребро не утворювали циклу зі старими? Зробити це
просто. До побудови дерева забарвленням кожну вершину i у відмінний від інших колір
i. При виборі чергового ребра, наприклад (i, j), де i і j мають різні кольори,
вершина j і все, пофарбовані в її колір (тобто раніше з нею з'єднані)
перефарбовуються в колір i. Таким чином, вибір вершин різного кольору
забезпечує відсутність циклів. Після вибору n-1 ребер всі вершини отримують
один колір. p>
Доведемо,
що описаний алгоритм отримує в точності мінімальне рішення. Для
докази нам знадобиться дуже просте твердження: p>
Якщо
до дерева додати ребро, то у лісі з'явиться цикл, що містить це ребро. p>
Дійсно,
нехай додано ребро (u, v) - "додано" означає, що ребро - нове, що
раніше його у лісі не було. Оскільки дерево є пов'язаною графом, то
існує ланцюг C (u, ..., v) з декількох ребер, що з'єднує вершини u і v.
Додавання ребра (u, v) замикає ланцюг, перетворюючи її в цикл. P>
Теорема.
Алгоритм Прима-Краскала отримує мінімальну остовне дерево. P>
Доказ.
Результатом роботи алгоритму є набір з n-1 ребер. Вони не утворюють
циклу, бо на кожному з n-1 кроків з'єднувалися вершини різного кольору, тобто раніше
не пов'язані. Цей граф зв'язний, тому що після проведення 1-го ребра
залишилося n-1 різних кольорів, ..., після проведення (n-1)-го ребра залишився один
колір, тобто одна компонента зв'язності. Отже, отриманий набір ребер утворює
зв'язний граф без циклів, що містить n-1 ребер і n вершин. Отже, граф
є остовне дерево. p>
Для
реалізації алгоритму знадобляться: p>
Matrix
- Матриця відстаней, значення перетині i-го рядка і j-го стовпця одно
відстані між i-ий і j-ий вершинами. Якщо такого ребра немає то значення дорівнює
Infinity - просто великого числа (машинна нескінченність); p>
Color
- Масив квітів вершин; p>
Ribs
- В цьому масиві запам'ятовуються знайдені ребра; p>
a,
b - вершини, що з'єднуються з черговим мінімальним ребром p>
len
- Довжина дерева. P>
Матрицю
відстаней будемо зберігати в текстовому файлі INPUT.MTR, де число на перше
рядку - кількість вершин n, а інші n рядків по n чисел в кожній - матриця
відстаней. Якщо відстань дорівнює 1000 (Infinity), то такого ребра немає. P>
Program
Algorithm_PrimaKrascala; p>
Uses Crt; p>
Const MaxSize = 100; p>
Infinity = 1000; p>
Var Matrix: array [1 .. MaxSize,
1 .. MaxSize] of integer; p>
Color: array [1 .. MaxSize] of integer; p>
Ribs: array [1 .. MaxSize] of record p>
a, b: integer; p>
end; p>
n, a, b, k, col, i, len: integer; p>
Procedure Init; p>
Var f: text; p>
i, j: integer; p>
Begin p>
Assign (f, 'INPUT.MTR'); p>
Reset (f); p>
Readln (f, n); p>
For i: = 1 to n do p>
Begin p>
For j: = 1 to n do read (f, matrix [i,
j ]); p>
Readln (f) p>
End; p>
For i: = 1 to n do color [i]: = i; p>
len: = 0 p>
End; p>
Procedure Findmin (var a, b:
integer); p>
Var min, i, j: integer; p>
Begin p>
min: = infinity; p>
For i: = 1 to n-1 do p>
For j: = i +1 to n do p>
If (Matrix [i, j] color [j]) then p>
Begin p>
min: = Matrix [i, j]; p>
a: = i; p>
b: = j p>
End; p>
len: = len + min p>
end; p>
Begin p>
Clrscr; p>
Init; p>
For k: = 1 to n-1 do p>
Begin p>
Findmin (a, b); p>
Ribs [k]. a: = a; p>
Ribs [k]. b: = b; p>
col: = Color [b]; p>
For i: = 1 to n do p>
If color [i] = col then
color [i]: = color [a]; p>
End; p>
For i: = 1 to n-1 do p>
Writeln (ribs [i]. a, '-', ribs [i]. b); p>
Writeln ( 'Length =', len); p>
Readkey p>
End. p>
Для
такого вхідного файлу p>
8 p>
0
23 12 1000 1000 1000 1000 1000 p>
23
0 25 1000 22 1000 1000 35 p>
12
25 0 18 1000 1000 1000 1000 p>
1000
1000 18 0 1000 20 1000 1000 p>
1000
22 1000 1000 0 23 14 1000 p>
1000
1000 1000 20 23 0 24 1000 p>
1000
1000 1000 1000 14 24 0 16 p>
1000
35 1000 1000 1000 1000 16 0 p>
програма
надрукує: p>
1-3 p>
5-7 p>
7-8 p>
3-4 p>
4-6 p>
2-5 p>
1-2 p>
Length =
125. P>
Алгоритм
Дейкстри. P>
Дана
дорожня мережа, де міста і розвилки будуть вершинами, а дороги - ребрами.
Потрібно знайти найкоротший шлях між двома вершинами. P>
Можна
запропонувати багато процедур вирішення цього завдання, наприклад, фізична
моделювання такого роду: на плоскій дошці малюється карта місцевості, в міста
і в р.азвілкі забиваються цвяхи, на кожен цвях надягає кільце, дороги
укладаються мотузками, які прив'язуються до відповідних кілець. Щоб
знайти найкоротша відстань між кільцем i і кільцем k, потрібно взяти i в одну
руку, взяти k в іншу і розтягнути. Ті мотузки, які вимушено і не дадуть
розводити ширше, і утворюють найкоротший шлях між i та k. Однак, математична
процедура, яка промоделірует цю фізичну, виглядає дуже складно.
Відомі алгоритми простіше, наприклад, запропонований Дейкстра ще в 1959 р. Цей
алгоритм вирішує більш загальну задачу: p>
В
орієнтованої, неорієнтовані або змішаної мережі знайти найкоротший шлях із
заданої вершини в усі інші вершини. p>
Алгоритм
використовує три масиву з n чисел кожен. Перший масив Visited містить мітки
з двома значеннями: False (вершина ще не розглянута) та True (вершина вже
розглянута); другий масив Len містить відстані від - поточні найкоротші
відстані від початкової до відповідної вершини; третій масив C містить
номери вершин - k-ий елемент C є номер передостанній вершини на поточному
найкоротший шлях з початкової вершини в k-ю. Використовується також Matrix - матриця
відстаней. p>
Опишемо
алгоритм Дейкстри: p>
1
(ініціалізація). У циклі від 1 до n заповнити значенням False масив Visited;
заповнити числом i масив C (i - номер стартовою вершини); перенести i-й рядок
матриці Matrix в масив Len; p>
Visited [i]: = True; C [i]: = 0; p>
2
(загальний крок). Знайти мінімум серед невідміченим (тобто до тих, для яких
Visitid [k] = False); нехай мінімум досягається на індексі j, тобто Len [j]