Поиск в ширину и в глубину в графе. Построение остовного дерева графа. Алгоритмы поиска кратчайших расстояний в графе. Поиск Эйлерова пути в графе Лекция 23
Систематический обход графа можно выполнить двумя стратегиями — • поиск в глубину • и поиск в ширину. • Работа алгоритма обхода состоит в последовательном посещении вершин и исследовании ребер. • Факт посещения вершины запоминается, так что с момента посещения и до конца работы алгоритма она считается просмотренной. • Вершину, которая еще не посещена, будем называть новой.
Поиск в ширину Идея поиска в ширину состоит в том, чтобы посещать вершины в порядке их удаленности от некоторой заранее выбранной или указанной стартовой вершины. Иначе говоря, сначала посещается сама вершина vа, затем все вершины, смежные с vа, и т. д. Для этого все просмотренные вершины последовательно помещаются в очередь (массив с), для которой хранится номер последнего элемента k и номер первого элемента n.
Рассмотрим задачу нахождения пути поиском в ширину из вершины va в вершину vb. Граф будем хранить виде матрицы смежности а, а вводить как последовательность ребер. Выводить путь будем в виде последовательности вершин.
Алгоритм Вначале все вершины помечаются как новые. Первой посещается вершина vа, она становится единственной просмотренной вершиной. В дальнейшем каждый очередной шаг начинается с выбора некоторой просмотренной вершины v. Эта вершина становится активной. Далее исследуются все вершины, смежные с v, которые последовательно посещаются и помещаются в очередь. Когда все ребра, инцидентные активной вершине, исследованы, она перестает быть активной. После этого выбирается новая активная вершина, и описанные действия повторяются. Процесс заканчивается, когда очередь становится пустой.
Чтобы восстановить путь от конечной вершины к начальной нам потребуется массив b, в котором мы будем хранить для каждой вершины i ее предшественницу — b[i]. Кроме этого нам нужен массив p, количество элементов в котором равно количеству вершин графа, а элементами которого является 0, если вершина новая и 1, если просмотренная.
Поиск в ширину 4 2 1 7 5 3 6 Массив с (очередь просмотренных вершин)1 2 3 4 5 6 7 Массив b (предшествующих вершин) 1 1 2 3 3 4
program poisk. Vshir; var a: array [1. . 20, 1. . 20] of integer; {Матрица смежности исходного графа} b, p, c: array [1. . 20] of integer; va, vb, i, nn, m, x, y: integer; {va —начальная вершина, vb — конечная вершина} { nn – число вершин графа, количество ребер m} f: boolean; {Признак окончания поиска}
procedure Poisk. Sh(va, vb: integer); {va —начальная вершина, vb — конечная вершина} var i, n, k, v: integer; {v-номер активной вершины} begin n: =1; {начало очереди (откуда берутся активные вершины)} k: =1; {конец очереди (куда добавляются просмотренные вершины)} f: =false; {поиск не завершен} for i: = 1 to nn do { nn – число вершин графа} begin p[i]: =0; {Все вершины непросмотренные} b[i]: =0; {Пути пока нет} end;
c[k]: =va; {Первая вершина помещается в очередь} p[va]: =1; {и помечается как просмотренная} while (n<=k) and not f do begin {пока очередь не пуста и поиск не закончен} v: =c[n]; {Очередная вершина из начала очереди становится активной} n: =n+1; {Начало очереди сдвигается} for i: =1 to nn do {Просматриваем все вершины i от 1 до последней nn} if (a[v, i]=1) and (p[i]=0) then {если существует ребро из активной вершины v в i и она еще не просмотрена}
begin k: =k+1; {добавляем в конец очереди} c[k]: =i; {эту вершину i} p[i]: =1; {она становится просмотренной} b[i]: =v; {запоминаем предшествующую ей} if i=vb then {Если добавленная вершина i совпадает с конечной vb} begin f: =true; {поиск закончен} break; end; {if (a[v, i]=1) and (p[i]=0) } end; {while (n<=k) and not f}
if f then {Если поиск закончен} begin write(vb, ' '); {Выводим последнюю вершину} while b[i]<>0 do {Пока не просмотрены все вершины из массива пути} begin write(b[i], ' '); {Выводим вершину} i: =b[i]; {Переходим к ее предшественнице} end; end else {Если все вершины оказались просмотренными но поиск не закончен} write(‘Пути не существует'); end; {конец процедуры Poisk. Sh}
{Основная программа} begin writeln(‘Введите количество вершин и ребер'); {Вводим количество вершин nn и количество ребер m} readln(nn, m); writeln(‘Введите ребра графа'); for i: = 1 to m do {Вводим последовательность ребер} begin readln(x, y); a[x, y]: =1; {Добавляем соответствующие ребра} a[y, x]: =1; {в матрицу смежности} end;
writeln(‘Введите номер начальной вершины'); {Вводим начальную вершину} readln(va); writeln(‘Введите номер конечной вершины'); {Вводим конечную вершину} readln(vb); Poisk. Sh(va, vb); {вызываем процедуру поиска} Readln; end.
Поиск в глубину Идея этого метода — идти вперед в неисследованную область, как можно дальше от исходной вершины, пока это возможно, если же вокруг все исследовано, отступить на шаг назад и искать новые возможности для продвижения вперед. Обход начинается с посещения заданной стартовой вершины t, которая становится активной. Затем выбирается инцидентное вершине t ребро (t, i) и посещается вершина i. Она становится просмотренной и активной и т. д.
Остовное дерево графа Остовное дерево состоит из всех вершин графа и некоторого подмножества его ребер, таких, что из любой вершины графа можно попасть в любую другую вершину, двигаясь по этим рёбрами, и в нём нет циклов, то есть из любой вершины нельзя попасть в саму себя, не пройдя какое-то ребро дважды. Любое остовное дерево в графе с n вершинами содержит ровно n − 1 ребро. Остовное дерево может быть построено практически любым алгоритмом обхода графа, например поиском в глубину или поиском в ширину.
Алгоритм построения остовного дерева графа поиском в глубину Граф будем хранить виде матрицы смежности, вводить как последовательность ребер. Выводить остовное дерево будем в виде матрицы смежности. Алгоритм поиска реализован в процедуре poisk. Ей передается номер вершины из которой начинается поиск. Матрица смежности исходного графа хранится в глобальном двумерном массиве a, а для нового остовного дерева в массиве b.
Алгоритм Мы помечаем вершину t как открытую и просматриваем все вершины от первой и до последней, если просматриваемая вершина новая, и если существует ребро, соединяющее ее и t, то: • Добавляем это ребро в остовное дерево (в его матрицу смежности); • Применяем процедуру poisk к этой вершине. Таким образом мы просмотрим все вершины графа, если он связный (между любой парой вершин графа существует как минимум один путь), в противном случае после завершения процедуры в массиве p (просмотренных вершин) останутся элементы равные 0, это значит, что граф не связный и построить остовное дерево в нем невозможно.
Поиск в глубину 4 2 1 7 5 3 6
program Ostov_Glub; var i, j, n, m, x, y: integer; a, b: array [1. . 20, 1. . 20] of integer; p: array [1. . 20] of integer; procedure Poisk(t: integer); {t — вершина, из которой начинается поиск} var i: integer; begin p[t]: =1; {Вершина t становится просмотренной} for i: = 1 to n do {Просматриваем все вершины}
if p[i]<>1 then {Если i-я вершина непросмотренная} if a[t, i]=1 then {и существует ребро (t, i)} begin b[t, i]: =1; {добавляем это ребро в остовное дерево} b[i, t]: =1; Poisk(i); {Продолжаем поиск из вершины i} end; {процедуры Poisk} Begin {Основная программа} Assign(input, 'input. txt'); Reset(Input); Assign(Output, 'output. txt'); Rewrite(Output); Readln(n, m); {Вводим количество вершин n и ребер m графа}
for i: = 1 to m do begin Readln(x, y); {Вводим ребра графа} a[x, y]: =1; {и добавляем их в матрицу смежности графа} a[y, x]: =1; end; for i: = 1 to n do {Пока все вершины непросмотренные} p[i]: =0; Poisk(1); {Начинаем поиск из первой вершины} for i: = 1 to n do if p[i]=0 then {Если после окончания поиска остались непросмотренные вершины} begin write(‘Невозможно построить остовное дерево'); Readln; Halt; end;
for i: = 1 to n do begin for j: = 1 to n do write(b[i, j]); {Выводим матрицу смежности построенного остовного дерева} writeln; end; Readln; end.
Поиск эйлерова пути в графе Эйлеров путь (эйлерова цепь) в графе — это путь, проходящий по всем рёбрам графа и притом только по одному разу. Эйлеров цикл — это эйлеров путь, являющийся циклом. В неориентированном графе согласно теореме, доказанной Эйлером, эйлеров цикл существует тогда и только тогда, когда граф связный и в нём отсутствуют вершины нечётной степени. Эйлеров путь в графе существует, когда граф связный и содержит или две вершины нечётной степени или вообще их не содержит.
Можно всегда свести задачу поиска эйлерова пути к задаче поиска эйлерова цикла. Предположим, что эйлерова цикла не существует, а эйлеров путь существует. Тогда в графе будет ровно 2 вершины нечётной степени. Соединим эти вершины ребром, и получим граф, в котором все вершины чётной степени, и эйлеров цикл в нём существует. Найдём в этом графе эйлеров цикл, а затем удалим из ответа несуществующее ребро.
Граф будем хранить виде матрицы смежности. А вводить как последовательность ребер. Выводить эйлеров путь будем в виде последовательности вершин (массив b). С помощью процедуры poisk, которую мы использовали для построения остовного дерева, определяем, является ли граф связным. А сам алгоритм поиска эйлерова цикла реализован в процедуре Find. Way. Параметром является номер вершины t, с которой начинается поиск. Мы просматриваем все вершины от 1 и до последней и если есть ребро, соединяющее эту вершину с t, то удаляем это ребро, и вызываем процедуру Find. Way из нее, а вершину добавляем в эйлеров цикл.
Прежде, чем применять эти процедуры необходимо проверить, существует ли эйлеров путь в данном графе. Для этого подсчитываем степени всех вершин графа. Если есть вершины с нечетными степенями, то подсчитываем их количество и запоминаем первые две из них (чтобы добавить ребро), если их больше, то эйлерова пути нет.
program Eiler; var a: array [1. . 20, 1. . 20] of integer; {матрица смежности исходного графа} b, p: array [1. . 20] of integer; i, j, x, y, s, k, n, m, a 1, a 2: integer;
procedure Find. Way(t: integer); {t — вершина, из которой начинается поиск эйлерова пути} var i: integer; begin for i: = 1 to n do {просматриваем все вершины i, смежные с t} if a[t, i]>0 then {если существует ребро (t, i)} begin dec(a[t, i]); {удаляем его} dec(a[i, t]); Find. Way(i); {продолжаем поиск из вершины i} end; inc(k); b[k]: =t; {добавляем вершину t к эйлеровому пути} end;
procedure Poisk(t: integer); {Обходит все вершины графа, начиная с вершины t поиском в глубину и формирует массив p просмотренных вершин} var i: integer; begin p[t]: =1; {вершина t становится просмотренной} for i: = 1 to n do {рассматриваем все вершины} if p[i]<>1 then {если i-я вершина непросмотренная} if a[t, i]=1 then {и существует ребро (t, i)} Poisk(i); {то продолжаем поиск из вершины i} end;
begin Assign(Input, 'input. txt'); Reset(Input); Assign(Output, 'output. txt'); Rewrite(Output); Readln(n, m); {Вводим количество вершин n и ребер m графа} for i: = 1 to m do {Вводим ребра графа} begin Readln(x, y); a[x, y]: =1; {и добавляем их в матрицу смежности} a[y, x]: =1; end; {Проверим, существует ли эйлеров путь в графе} s: =0; { число вершин с нечетными степенями }
for i: = 1 to n do {Рассматриваем каждую из вершин графа i} begin k: =0; {будем считать степень вершины } for j: = 1 to n do {просматриваем все вершины} if a[i, j]=1 then {если существует ребро} Inc(k); {увеличиваем степень вершины)} if k mod 2=1 then {если степень вершины нечетная} begin Inc(s); {увеличиваем на 1 число вершин с нечетными степенями} if s=1 then {если это первая вершина нечетной степени, то} a 1: =i {запоминаем ее номер в переменной a 1} else if s=2 then {если вторая} a 2: =i; {то в переменной a 2} end;
for i: =1 to n do p[i]: =0; {все вершины непросмотренные} Poisk(1); {формируем массив p} for i: = 1 to n do if p[i]=0 then {если остались непросмотренные вершины} s: =3; {переменной s присваиваем значение 3} if (s<>2) and (s<>0) then writeln(‘Эйлерова пути нет') else {в противном случае} begin if s<>0 then {если есть вершины нечетной степени} begin Writeln(‘Эйлерова цикла нет'); if s=2 then {и их ровно 2} begin inc(a[a 1, a 2]); {добавляем между ними ребро} inc(a[a 2, a 1]); end;
k: =0; {Количество вершин в эйлеровом пути, глобальная переменная, определяется в процедуре Find. Way } Find. Way(1); {находим эйлеров цикл из первой вершины} if s=0 then {если не было вершин нечетных степеней} for i: = 1 to k do {просто выводим последовательность вершин, } write(b[i], ' ') {образующих эйлеров цикл} else {мы должны удалить их цикла добавленное ребро (a 1, a 2)} begin i: =1; while ((b[i]<>a 1) or (b[i+1]<>a 2)) and ((b[i]<>a 2) or (b[i+1]<>a 1)) and (i<k) do Inc(i); {начнем выводить цикл, когда там встретится добавленное ребро} for j: =i+1 to k-1 do {выведем все вершины после него} write(b[j], ' '); for j: = 1 to i do { а затем до него} write(b[j], ' '); end;
end; {Else от if (s<>2) and (s<>0) (если число вершин нечетных степеней не равно 0 или 2)} Readln; end.
Алгоритмы поиска кратчайших путей в графе (алгоритм Дейкстры) Дан взвешенный ориентированный граф G(V, E) без петель и дуг отрицательного веса. Найти кратчайшие пути от некоторой начальной вершины графа G до всех остальных вершин этого графа.
Каждой вершине из V сопоставим метку (массив v)— минимальное известное расстояние от этой вершины до начальной. Алгоритм работает пошагово — на каждом шаге он «посещает» одну вершину и пытается уменьшать метки вершин, в которые из нее можно попасть (смежные). Работа алгоритма завершается, когда все вершины посещены. Сначала метка начальной вершины полагается равной 0, метки остальных вершин — бесконечности. Это отражает то, что расстояния от начальной до других вершин пока неизвестны. Все вершины графа помечаются как непросмотренные. Кроме этого нам понадобится массив b, хранящий для каждой вершины i номер предшествующей ей вершины кратчайшего пути.
Поиск начинается с некоторой вершины t. Помечаем ее как просмотренную, затем рассматриваем все непросмотренные вершины и ищем для них новую длину пути из начальной вершины через вершину t: v[t] (длина пути из начальной вершины в t)+ a[t, i] (длина пути из t в i) и если эта новая длина меньше старой, то заменяем старую и запоминаем t в качестве предыдущей вершины для i (Если пути из t в i нет (a[t, i] — бесконечность, то новый путь не будет меньше старого и ничего не меняется) Найдя значения новых путей, выбираем из непросмотренных вершин ту, для которой значение пути (v[i]) минимально и продолжаем поиск из нее. Процесс останавливаем, когда все вершины окажутся просмотренными.
Граф будем хранить виде матрицы смежности а, в которой a[i, j]=w весу ребра (i, j) и равно некоторому большому числу(∞), если ребра (i, j) нет. Вводить граф будем как последовательность ребер с их весами. Выводить будем для каждой вершины длину кратчайшего пути и сам путь в виде последовательности вершин.
2 2 1 1 5 4 1 3 4 6 1 5 вершины 1 2 3 4 5 v 0 1 ∞ 4 ∞ 3 ∞ 5 9 ∞ 5 1 1 2 1 3 4 b
program Deixtra_Matrix; var a: array [1. . 20, 1. . 20] of integer; p, v, b: array [1. . 20] of integer; i, j, n, m, x, y, w, min: integer; procedure Deix(t: Integer); {находит кратчайший путь из t во все вершины графа} var i, tt: Integer; begin p[t]: =1; {Вершина t становится просмотренной}
for i: = 1 to n do {Проходим все вершины} if p[i]<>1 then {которые еще не просмотрены} if v[i]>v[t]+a[t, i] then {если длина нового пути меньше старого} begin v[i]: =v[t]+a[t, i]; {меняем ее} b[i]: =t; {и запоминаем предшествующую вершину t} end; min: =10000; for i: = 1 to n do {ищем среди всех} if p[i]<>1 then {непросмотренных вершин} if v[i]<min then {ту , для которой длина пути минимальна} begin min: =v[i]; tt: =i; end; if min<>10000 then {если еще есть непросмотренные вершины} Deix(tt); {продолжаем поиск из выбранной вершины tt} end;
begin Assign(Input, 'input. txt'); Reset(Input); Assign(Output, 'output. txt'); Rewrite(Output); Readln(n, m); for i: = 1 to n do for j: = 1 to n do a[i, j]: =10000; {заполняем всю матрицу смежности большими значениями} for i: = 1 to m do begin Readln(x, y, w); if w<a[x, y] then {если введенное значение меньше, чем старое} a[x, y]: =w; {заменяем старое} end;
for i: = 1 to n do begin p[i]: =0; {сначала все вершины непросмотренные} v[i]: =10000; {все длины путей ∞} end; v[1]: =0; {длина пути из первой вершины в первую 0} Deix(1); {ищем кратчайшие пути из первой вершины во все остальные} for i: = 1 to n do begin {Выводим длины кратчайших путей во все вершины} write(‘Путь в ', i, '=', v[i], ': '); j: =i; while j>=1 do begin write(j, '-'); {Выводим сами пути} j: =b[j]; end;
writeln; end; {for i: = 1 to n do} Readln; end.
Это очень сложная и объемная тема и если вы еще следите за тем, что происходит на экране, то вы молодцы!
Домашнее задание 1. Составить опорный конспект лекции по теме «Поиск в ширину и в глубину в графе. Построение остовного дерева графа. Алгоритмы поиска кратчайших расстояний в графе. Поиск Эйлерова пути в графе» на основе презентации. 2. Комбинаторика для программистов. «Мир» , 1988, стр. 83 -102. Липский В. М. :