390 likes | 1.18k Views
ГРАФИ основни дефиниции АЛГОРИТМИ ВЪРХУ ГРАФИ. 2009 / 2010. Графи - примери. Примери за графи.
E N D
ГРАФИосновни дефиницииАЛГОРИТМИ ВЪРХУ ГРАФИ 2009 / 2010
Примери за графи • Няколко града могат да бъдат представени като върхове на граф, а преките пътища между тях — като ребра. Теглата на ребрата ще бъдат дължините на преките пътища. Илюстрация на примера е транспортната карта на Република България • Компютърна мрежа може да бъде представена чрез граф, като компютрите са върховете на графа, а всяко ребро между два върха показва, че съответните компютри са пряко свързани в мрежата • Множество от Internet страници и връзките по между им могат да бъдат представени като граф. • Няколко химични съединения могат да бъдат представени като върхове на граф. Всяко ребро от графа ще показва дали съответните химични съединения могат да си взаимодействат. • Процесите в изработването на едно изделие могат да се представят с върховете на граф, а с ребрата — кой процес след кой трябва да следва в изработката.
Дефиниции в теорията на графите • Ориентиран граф - фиг 1 • Ориентиран претеглен граф- фиг 2 • Неориентиран граф – фиг 3
Полустепен ДефиницияДаден е ориентиран граф G(V,E). Полустепен на изхода на връх iV се нарича броят на всички ребра (i, j),jV. Аналогично, броят на всички ребра (j, i),jV се нарича полустепен на входа на i.Сборът от полустепентта на входа и полустепентта на изхода се нарича степен на върха i. Изолиран се нарича връх от графа, чиято степен е 0. При неориентиран граф степен на връх i се нарича броя на всички ребра (i,j) инцидентни с него.
Съседство Два върха iи j (i, jV) се наричатсъседни, когато поне едно от ребрата (i, j)и(j, i) принадлежат на E. В такъв случай още казваме, че i и j са краищаза реброто (i, j).За всяко ребро (i, j) върхът iсе нарича предшественик на j, а jнаследникна i.Всеки от върховетеiиjсе нарича инцидентен с реброто (i, j). Казваме, че две ребра са инцидентни помежду си, когато са инцидентни с един и същ връх.
Път в граф Път в граф G(V, E) се нарича последователност от върхове v1, v2,..., vk, такава че за всяко i=1, 2, …, k–1 е изпълнено (vi, vi+1)E. Върховете v1и vkсе наричат краищана пътя. Ако v1= vk,то пътят се нарича цикъл. Ако за всяко ij следва vivj , то пътят се нарича прост. Респективно, ако в последователността от върхове v1=vk,и всички останали върхове от нея са различни, цикълът се нарича прост. Когато граф съдържа поне един цикъл в себе си, той се нарича цикличен, в противен случай се нарича ацикличен.
Цикъл в граф Цикъл е път в граф, при който началният и крайният връх съвпадат
Пълен / Празен Граф Ако даден граф не съдържащ нито едно ребро, се нарича празен(фиг.5.1.д). Ако за даден граф е изпълнено (i, j)E за всяко i, jV, графът се нарича пълен
Свързаност в граф Ориентиран граф се нарича слабо свързан, ако всеки два върха iи j от графа са краища на поне един път (т.е. съществува поне един път от i до j или от j до i). Когато в ориентиран граф за всеки два върха i, jсъществува път от i до j и от j до i то графа се нарича силно свързан. Неориентиран граф се нарича свързан, ако съществува път между всяка двойка негови върхове i, j. Диаметър в граф се нарича максималното число k, такова, че за всяко i,jV да съществува прост път между i и j с дължина поне k.
Клика Клика (английски clique) ще наричаме граф или подграф, в който има ребро между всеки два върха. По начало, задачите за търсене на клики в граф са NP-пълни, примери са "Търсене на максимална клика в граф по брой на върховете в нея", "Търсене на клика с дължина К" и др. Примери за клика с 3 върха е произволен триъгълник, ето пример за клика с 6 върха.
Построяване и прости операции с графи Основните операции, свързани с построявяне и модифициране на граф, са следните:- създаване на празен граф.- добавяне/премахване на връх.- добавяне/премахване на ребро.По-нататък следват операциите:- проверка за съществуване на връх- проверка за съществуване на ребро- намиране на наследниците на даден връхРеализация за граф, представен с матрица на съседство (матрица на теглата).
Задачи • търсене на оптимален път • минимална дължина • максималния път • Критерии • критерий за оценката на един маршрут (например — като сума от теглата участващите в него ребра) • критерий за оптималност(екстремалност) на маршрута (например желаем да минимизираме дефинираната в предната точка сума)
ПРЕДСТАВЯНЕ НА ГРАФИ Матрица на съседство, матрица на теглата {0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, //1{1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, //2{0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0}, //3{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0}, //4{0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0}, //5{0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0}, //6{0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0}, //7{0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0}, //8{0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0}, //9{0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0}, //10{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0}, //11{0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0}, //12{0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}, //13{0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0} //14
Списък на наследниците връх наследници 1 - 2, 3 2 - 4, 5 3 - 2, 5 4 5
Обхождане на граф в дълбочина Обхождането в дълбочина на граф (съкратено DFS - Depth-First-Search) е един от фундаменталните методи за решаване на изчерпващи задачи - търсене с връщане назад (на англ. backtracking). Това обхождане се стреми да “се спусне” колкото се може “по-надълбоко” при обхождането. Описва се най-лесно рекурсивно: започваме от избрания начален връхi εV, маркираме го като посетен продължаваме рекурсивно обхождането в дълбочина за всеки негов непосетен наследник, т.е. функцията за обхождане в дълбочина от връхi (DFS(i)) изглежда по следния начин:1) Разглежаме i.2) Маркирамеi като обходен.3) За всяко инцидентно с i ребро (i, j) εE такова, че j е необходен връх от графа, изпълняваме рекурсивно DFS(j). В случаите, когато повече от едно ребро е инцидентно с даден връх (стъпка 2), ще продължаваме последователно от върховете с по-малък към върховете с по-голям номер. Така резултатът от DFS(1) ще бъде: 1, 2, 3, 4, 5, 6, 7, 5, 10, 11, 12, 4, а от DFS(3): 3, 2, 1, 4, 12, 6, 7, 5, 10, 11. Програмата, извършваща обхождането, ще реализираме директно по описания алгоритъм (чрез рекурсивната функцияDFS(i)). В булев масив used[] ще маркираме обходените върхове: в началото го инициализираме с нули, а при посещаване на върха i извършваме присвояването used[i] = 1.
Има два фундаментални алгоритъма за обхождане на графи: търсене в дълбочина (Depth First Search, dfs) и търсене в ширина (Breadth First Search, bfs). Алгоритъм за търсене в дълбочина void dfs(int u) { int v; visited[u]=1; Previsit(u); for(v in Adj(u))) if (visited[v]==0) dfs(v); Postvisit(u); } for(u=1; u<=n; u++) visited[u]=0; for(u=1; u<=n; u++) if (visited[u]==0) dfs(u); Процедурата за обхождане на граф в дълбочина дефинира по естествен начин дърво: всеки път, когато dfs открие нов непосетен съсед v на върха u, добавяме реброто (u,v) към дървото. Останалите ребра на графа могат да бъдат класифицирани в следните категории: ребро напред (forward edge) - от връх към непряк наследник в dfs-дървото; ребро назад (back edge) - от връх към предшественик в dfs-дървото; напречно ребро(cross edge) - между върхове, от които никой не е наследник на другия в dfs-дървото.
Посредством подходящи процедури Previsit и Postvisit можем да използваме dfs за решаване на редица важни проблеми. Естествен начин за използване на процедурите Previsit и Postvisit е да се поддържа брояч, който се увеличава всеки път при изпълнението на тези процедури. Това съответства на понятието време. Така с всеки връх се свързват две числа (времена) - моментите на започване и завършване на процедурата dfs за този връх. Да разгледаме интервалите [preorder[u],postorder[u]] и [preorder[v],postorder[v]]. Те или не се застъпват или единият се съдържа в другия. В сила са следните свойства на dfs-номерирането: Твърдение 3.1. Реброто (u,v) е обратно ребро, тогава и само тогава, когато postorder[u] < postorder[v]. Твърдение 3.2. Графът G(V,E) има цикъл, тогава и само тогава, когато при търсенето в дълбочина в G има обратно ребро. DB – обратно ребро AD – ребро напред EC – напречно ребро
Обхождане в дълбочина от връх 5 void DFS(int i){int k; used[i] = 1;cout << i+1 << " ";for(k = 0; k < n; k++)if(A[i][k] && !used[k]) DFS(k);} int main(){int k; for(k = 1;k < n; k++) used[k] = 0;cout << "Obhojdane v dylbochina ot vryh " << v << endl;DFS(v-1);cout << endl; system("pause");return 0;} Резултатът от изпълнението на програмата е: Обхождане в дълбочина от връх 5:5 2 1 3 6 7 10 11 12 4
Обхождане на граф в ширина Под обхождане на неориентиран (ориентиран) граф се разбира последователно посещаване (разглеждане) на всеки връх от графа точно веднъж. Стратегията на обхождане определя реда, в който ще се разглеждат върховете. Обхождането в ширина от даден връх i се нарича следната стратегия за обхождане на граф: започва се от върха i, разглеждат се всички негови непосредствени съседи, и едва след това се преминава към по-нататъшно обхождане (обхождане в ширина от всеки един от съседите му). По този начин се постига последователно обхождане “по нива”, започвайки от стартовия връх, докато се обходят всички върхове на графа, достижими от i. Обхождане в ширина на граф се нарича преминаването през всички негови върхове по описания начин - т.е. последователно избиране на (произволен) страртов връх, докато всички върхове на графа не бъдат обходени. Използва се съкращението BFS(i) (от англ. Breath-First-Search), за да означаваме функцията за обхождане в ширина от връх i.Ще приложим BFS върху следния граф: Ако за стартов връх изберем изберем 1, резултатът от обхождането ще изглежда по следния начин:BFS(1):ниво 1:1ниво 2:2ниво 3:3, 4, 5ниво 4:7, 6, 12ниво 5:10ниво 6:11 Ако вместо връх 1 изберем връх 3 за начален при BFS(3) се получва:BFS(3):ниво 1:3ниво 2:2,6ниво 3:1, 4, 5, 7, 12ниво 4:10ниво 5:11
Реализация на обхождане в ширина от даден връх Графа се представя с матрица на съседство. Това представяне е достатъчно ефективно при повечето задачи.В процеса на обхождане в ширина се налага да намираме наследниците на даден връх i. Ще поддържаме опашка, в която първоначлно се намира естествено стартовия връх. След това, докато в опашката има поне един връх, извършваме следното: изваждаме върха, намиращ се в началото на опашката, разглеждаме го и добавяме в опашката всички негови непосетение до момента наследници. Върховете ще маркираме като посетени в момента, в който ги добавяме в опашката: BFS(i){Създаваме празна опашка Queue;Добавяме към опашката върха i;for(k = 1,2,…,n) used[k] = 0;while(Опашката не е празна){p = Извличаме елемент от началото на опашката;Анализираме върха p;for(за всеки наследник j на p)if(used[j] == 0){/* ако върхът j не е обходен */Добавяме към опашката върхът j;used[j] = 1; /* маркираме j като обходен */}}}
BFS void BFS(int i){int k, j, p, queue[MAXN], currentVert, levelVertex, queueEnd; for(k = 0; k < n; k++) queue[k] = 0;for(k = 0; k < n; k++) used[k] = 0;queue[0] = i; used[i] = 1;currentVert = 0; levelVertex = 1; queueEnd = 1;while(currentVert < queueEnd) /* dokato opashkata ne e prazna */{for(p = currentVert; p < levelVertex; p++)/* p - vzimame poredniq element ot opashkata */{cout << queue[p] + 1 << ” “;currentVert++; /* za vseki neobhoden naslednik j na queue[p] */for(j = 0; j < n; j++)if(A[queue[p]][j] && !used[j]){queue[queueEnd++] = j;used[j] = 1;}//if}//for cout << endl;levelVertex = queueEnd;}//while} int main(){cout << “Obhojdane v shirina na graf ot vryh ” << v << “\n”;BFS(v-1); system(”pause”);return 0;} Резултат от изпълнението на програмата:Обхождане в ширина от връх 5:52 71 3 4 6 1012 11
Търсенето в ширина (Breadth First Search, bfs) е начин за систематично обхождане на върховете и ребрата на граф. Докато при dfs неявно се използва стек, то при bfs явно се използва опашка. Алгоритъм за търсене в ширина void bfs(int s) { queue q; int dist[n]; dist[s]=0; push(q,s); while(q is not empty) { pop(q,u); visited[u]=1; Previsit(u); for(v in Adj(u))) if (visited[v]==0) { push(q,v); dist[v]=dist[u]+1; } } } Въпреки, че bfs няма тънките свойства на dfs, и този алгоритъм дава полезна информация. Процедурата bfs посещава върховете по нарастващи разстояния от върха s. Фактически dist[u] е разстоянието от s до u по брой ребра.
СИЛНО СВЪРЗАНИ КОМПОНЕНТИСИЛНО СВЪРЗАНИ КОМПОНЕНТИ Два върха u и v в ориентиран граф G(V,E) наричаме свързани, ако има път както от u до v, така и от v до u. Да разгледаме множеството от върхове, свързани с даден връх u. Това множество представлява силно свързана компонента на G. Има ефективен алгоритъм за намиране на силно свързаните компоненти основаващ се на търсенето в дълбочина. Свойство 4.1. Ако търсенето в дълбочина започва от връх u, то процедурата dfs за върха u ще завърши едва след като бъдат посетени всички върхове, както от силно свързаната компонента на u, така и от всички други силно свързани компоненти, които са достижими от връх u. Следователно, ако dfs бъде стартирана от връх на такава компонента, от която няма достъп до други компоненти, то процедурата ще завърши с посещаването на върховете само от една силно свързана компонента.
АЛГОРИТЪМ НА ДЕЙКСТРА Нека s е връх в граф с неотрицателни дължини на дъгите и се интересуваме от дължините на най-късите пътища от s до всички останали върхове. Едно решение ни предлага търсенето в ширина: всяко ребро с дължина L разделяме на L ребра с дължина 1 и прилагаме bfs. Тази идея работи само теоретично – дължините на ребрата могат да бъдат хиляди или милиони и алгоритъмът ще работи преобладаващо с изкуствени върхове. Ще усъвършенстваме bfs, като вместо обикновена опашка ще използваме приоритетна опашка. За реализацията на приоритетна опашка може да се използва пирамида. Пирамидата е структура от данни, която съхранява множество от елементи, имащи ключове за сравнение. Операциите с пирамидата h, които ще използваме са: insert(h,x,y) – добавяне в пирамидата hна нов елемент x с ключ y; (ако в пирамидата вече има елемент x, евентуално само се променя неговият ключ; ако стойността на y е по-малка от текущия ключ на x, то y се записва като нов ключ на x); deletemin(h,x) – изключване от пирамидата h на елемента с минимален ключ и записването му в x. heap h; int dist[n]; prev[n]; for (u=1; u<=n; u++) { dist[u]=infinity; prev[u]=0; } dist[s]:=0; insert(h,s,0); while (h is not empty) { deletemin(h,u); for(v in Adj(u))) if (dist[v] > dist[u]+length[u][v]) { dist[v] = dist[u]+length[u][v]; prev[v] = u; insert(h,v,dist[v]); } }
НАЙ-КЪСИ ПЪТИЩА В ГРАФИ С ПРОИЗВОЛНИ ДЪЛЖИНИ НА ДЪГИТЕ Алгоритъм на Форд-Белман heap h; int dist[n]; prev[n]; for (u=1; u<=n; u++) { dist[u]=infinity; prev[u]=0; } dist[s]=0; for (i=1; i<n; i++) for ((u,v) in E) if (dist[v] > dist[u]+length[u][v]) { dist[v]=dist[u]+length[u][v]; prev[v]=u; } С алгоритъма на Форд-Белман се намират дължините на най-късите пътища от фиксиран начален връх s до всеки от останалите върхове. Сложността на алгоритъма е от порядъка на n³. При следващия алгоритъм, отново със сложност n³, се намират дължините на най-късите пътища между всички двойки върхове.
Алгоритъм на Флойд int dist[n][n]; for (u=1; u<=n; u++) for (v=1; v<=n; v++) dist[u][v]=length[u][v]; for (u=1; u<=n; u++) dist[u][u]=0; for (x=1; x<=n; x++) for (u=1; u<=n; u++) for (v=1; v<=n; v++) if (dist[u][v] > dist[u][x] + dist[x][v]) dist[u][v] = dist[u][x] + dist[x][v];
МИНИМАЛНИ ПОКРИВАЩИ ДЪРВЕТА Неориентиран ацикличен свързан граф ще наричаме дърво. Теорема 8.1. Нека G(V,E) е неориентиран граф с n върха и m ребра. Следните пет твърдения са еквивалентни: а) G е дърво б) G е свързан, но при отстраняването на произволно ребро се получава граф, който не е свързан в) Ако u и v са различни върхове, то от u до v има точно един прост път г) G е ацикличен и m = n -1 д) G е свързан и m = n -1.
Алгоритъм за построяване на минимално покриващо дърво X = { }; while (|X| < n-1) { Избери множество от върхове S, такова, че ребрата от X не свързват връх от S с връх от V-S. Намери най-лекото ребро e, свързващо връх от S с връх от V-S. Добави реброто e към X. }