Grafos

Definição e Representação

Grafo

Um grafo corresponde a uma estrutura de dados G=(V,E)G = (V, E), com VV e EE sendo respetivamente os seus conjuntos de vértices e arestas. Trivialmente, uma aresta (ou arco) liga 2 vértices, e um vértice pode estar ligado a qualquer número de outros vértices. Dizemos que o número de ligações de um vértice é o seu grau, ou degree. Na imagem abaixo podemos observar o grau de cada vértice, dd:

Grafo - Grau de Vértices

Existem várias tipologias diferentes de grafos. Podemos, por exemplo, querer que as ligações dos vértices tenham uma direção associada: numa fábrica, por exemplo, as linhas de montagem têm por norma sentido único, algo semelhante ao que pretendemos representar aqui. Dizemos que estes grafos são dirigidos ou orientados.

Grafo Dirigido

Podemos, claro, pretender pesar as arestas do grafo: pensando numa estrada, caminhos diferentes podem ter custos associados distintos (pensemos, por exemplo, em custos associados a portagens e combustível que variam). Dizemos que estes grafos são pesados.

Grafo pesado

Em certas situações, podemos querer trabalhar com grafos que não contêm ciclos: isto é, grafos onde partindo de qualquer vértice não encontramos um caminho de volta ao mesmo. Podemos, por exemplo, pensar em circuitos ou redes, onde a existência de ciclos pode (ou não, claro) trazer problemas, preferindo por norma grafos sem ciclos. Dizemos que estes grafos são acíclicos.

Grafo Acíclico

No outro espectro, temos grafos onde, escolhendo qualquer par de vértices (u,v)(u, v) do grafo, podemos sempre percorrer um caminho que nos leva de uu a vv. Grafos com esta propriedade dizem-se conexos. Podem experimentar verificar esta propriedade a olho no grafo representado abaixo, e notar que a propriedade se verifica!

Grafo Conexo

Temos ainda uma variação adicional dos grafos conexos: os grafos bi-conectados, grafos conexos onde podemos remover qualquer vértice e manter o grafo conexo.

Grafo Bi-Conectado

Representação

As duas representações mais comuns para grafos (e as abordadas em IAED) são as matrizes de adjacências e as listas de adjacências, cada uma com os respetivos prós e contras. De notar que a própria representação difere entre grafos dirigidos e não dirigidos, como se pode ver na imagem abaixo:

Representação - Visão Geral

Lista de Adjacências

Nesta representação, cada índice da lista está associado a um vértice do grafo. Esta representação procura, assim, associar a cada vértice vv um conjunto de vértices SS, SVS \in V, correspondente aos vértices adjacentes a vv.

A noção de adjacência difere entre grafos dirigidos e não dirigidos: no caso dos grafos dirigidos, a adjacência é no sentido vuv \to u, ou seja, o vértice que estamos a ver, vv, é adjacente a outro vértice uu caso haja um arco dirigido de vv para uu. No caso dos grafos não dirigidos, não havendo esta noção de "sentido" associada aos arcos, temos apenas que dois vértices são adjacentes se houver um arco que os ligue.

124/2134/32/412/\begin{darray}{l|r r r r} 1 & 2 & 4 & /\\ 2 & 1 & 3 & 4 & /\\ 3 & 2 & /\\ 4 & 1 & 2 & / \end{darray}

Vantagens

  • Inicialização é proporcional a V|V|, o tamanho do conjunto VV (ser linear é definitivamente um ponto positivo);

  • Utiliza sempre espaço proporcional a V+E|V|+|E|, sendo portanto adequado para grafos esparsos (e em algoritmos que assentem na análise de arcos em grafos esparsos);

  • A adição de arcos é feita de forma eficiente: adicionar um arco de uu para vv à lista consiste apenas em fazer um pushback de vv para o final da lista de uu, feito em tempo constante.

Desvantagens

  • Verificar arcos paralelos e adjacência entre vértices requer que se pesquise as listas de adjacências, algo que pode levar um tempo proporcional a V|V|;

  • A remoção de arcos pode levar um tempo proporcional a V|V| (visto que precisamos de pesquisar o arco em si na lista para o remover);

Não é aconselhável, portanto, para grafos de grande dimensão que não podem ter arcos paralelos (já que, temos de verificar frequentemente se um arco) ou em algoritmos onde esperamos muitas remoções de arcos.

Matriz de Adjacências

Matriz onde as linhas e colunas representam os vértices do grafo. É inicialmente composta totalmente por zeros, representando a ausência de adjacência entre os vértices uu (linha) e vv (coluna). Cada vez que encontramos uma nova adjacência, colocamos um 11 na posição correspondente à adjacência na matriz.

Tal como na lista de adjacências, a matriz é preenchida de forma diferente consoante o grafo seja ou não dirigido: no caso de grafos não-dirigidos, havendo uma adjacência entre uu e vv no grafo, tanto a entrada (u,v)(u, v) como a (v,u)(v, u) ficam a 11; caso contrário, apenas alteramos o valor da entrada (u,v)(u, v).

123410101210113010041100\begin{darray}{l | r r r r} & 1 & 2 & 3 & 4\\ \hline 1 & 0 & 1 & 0 & 1\\ 2 & 1 & 0 & 1 & 1\\ 3 & 0 & 1 & 0 & 0\\ 4 & 1 & 1 & 0 & 0 \end{darray}

Vantagens

A representação é a mais adequada quando temos:

  • Bastante espaço disponível (isto é, não são impostas grandes limitações quanto à memória que podemos utilizar);
  • Estamos a trabalhar com grafos densos (com muitos arcos, portanto);
  • Algoritmos que possam requerer mais de V2|V|^2 operações.

As afirmações acima advêm da adição e remoção de arcos ser feita de forma eficiente (O(1)O(1)), de ser fácil evitar existência de arcos paralelos (também verificável em O(1)O(1), claro), e de ser igualmente fácil determinar se 2 vértices estão ou não ligados (numa direção ou noutra).

Desvantagens

  • Grafos esparsos de grande dimensão requerem espaço de memória proporcional a V2|V|^2. Num grafo esparso, grande parte dessa memória acaba por ser inutilizada, acabando por ser uma gestão de recursos pouco sensata;
  • Neste caso, apenas inicializar o grafo (tempo proporcional a V2|V|^2) pode dominar o tempo de execução global do algoritmo;
  • Para o caso de grafos muito esparsos, mas com um número muito elevado de vértices, podemos nem ter acesso a memória suficiente para armazenar a matriz!

Representações Alternativas

Temos, portanto, três maneiras "básicas" para representar grafos:

  • Vector de arcos (pouco comum, não foi abordada em detalhe nesta secção dada a sua simplicidade);
  • Matriz de adjacências;
  • Lista de adjacências.

Cada uma destas representações, como pudemos observar ao longo da secção, produzem diferentes desempenhos (tanto quanto ao aspeto temporal como espacial) ao nível das respetivas operações de manipulação. A escolha deverá, portanto, depender do problema a resolver.

DesempenhoVetor de ArcosMatriz de AdjaceˆnciasLista de AdjaceˆnciasEspac¸oO(E)O(V2)O(V+E)Inicializac¸a˜oO(1)O(V2)O(V)CoˊpiaO(E)O(V2)O(E)Destruic¸a˜oO(1)O(V)O(E)Inserir ArcoO(1)O(1)O(V)Encontrar ArcoO(E)O(1)O(V)Remover ArcoO(E)O(1)O(V)\begin{array}{ c| c| c |c } \text{Desempenho} & \text{Vetor de Arcos} & \text{Matriz de Adjacências} & \text{Lista de Adjacências}\\ \hline \text{Espaço} & O( E) & O\left( V^{2}\right) & O( V+E)\\ \text{Inicialização} & O( 1) & O\left( V^{2}\right) & O( V)\\ \text{Cópia} & O( E) & O\left( V^{2}\right) & O( E)\\ \text{Destruição} & O( 1) & O( V) & O( E)\\ \text{Inserir Arco} & O( 1) & O( 1) & O( V)\\ \text{Encontrar Arco} & O( E) & O( 1) & O( V)\\ \text{Remover Arco} & O( E) & O( 1) & O( V) \end{array}

Algoritmos de Procura em Grafos

Esta sub-secção foi abordada com mais detalhe (e suporte teórico) na secção análoga da página de ASA. Notem que definitivamente não precisam de saber os algoritmos com todo a minúcia teórica apresentada nessa página: para IAED, de um modo geral, costuma bastar saber como o algoritmo funciona/saber executar o algoritmo sobre um grafo arbitrário.