graph LR
subgraph s1["Grafo A"]
A((a))
B((b))
C((c))
D((d))
end
subgraph s2["Grafo B"]
direction LR
A1((e))
B1((f))
C1((g))
D1((h))
end
A --- B
B --- C
C --- A
B --- D
A1 --> B1
B1 --> C1
C1 --> A1
C1 --> D1
s1 ~~~ s2
A:::WT
B:::WT
C:::WT
D:::WT
A1:::WT
B1:::WT
C1:::WT
D1:::WT
classDef WT fill:#FFFFFF
style s1 stroke:none,fill:transparent
style s2 stroke:none,fill:transparent
Representações Computacionais
Até aqui descrevemos grafos de forma abstrata. Para armazená-los e manipulá-los em computador, precisamos de representações concretas. Vamos usar os mesmos grafos da seção passada:
Matriz de Adjacência
Dado o grafo com \(n\) vértices, a matriz de adjacência é uma matriz quadrada de ordem \(n\) em que as linhas e colunas são os vértices. O valor na posição \((i, j)\) indica se existe aresta entre o vértice \(i\) e o vértice \(j\). Usamos \(1\) para existência e \(0\) para ausência.
Para os grafos A e B:
| A | B | C | D | |
|---|---|---|---|---|
| A | 0 | 1 | 1 | 0 |
| B | 1 | 0 | 1 | 1 |
| C | 1 | 1 | 0 | 0 |
| D | 0 | 1 | 0 | 0 |
Note que a matriz é simétrica (\(A=A^T\)) para grafos não-direcionados.
| A | B | C | D | |
|---|---|---|---|---|
| A | 0 | 1 | 0 | 0 |
| B | 0 | 0 | 1 | 0 |
| C | 1 | 0 | 0 | 1 |
| D | 0 | 0 | 0 | 0 |
Note que as linhas representam de onde as arestas partem. Já as colunas, representam onde elas incidem.
Implementação
Vamos implementar a matriz de adjacência para um grafo não-direcionado. Fica como tarefa realizar a implementação da versão direcionada.
A função add_edge (adicionar aresta) recebe uma matriz quadrada de ordem V (número de vértices) e os vértices que estão conectados por uma aresta. Como o grafo é não-direcionado, a matriz deve ser simétrica, isto é, mat[i][j] = mat[j][i] = 1.
Uma dica de segurança para a função add_edge é adicionar a seguinte linha:
if (i < 0 || i >= V || j < 0 || j >= V) return;Isso vai garantir que não serão passados valores fora do intervalo permitido.
Para imprimir a matriz, utilizamos um laço duplo percorrendo as linhas e colunas:
Para testar a implementação, considere o grafo A.
Se liga! O comando int mat[V][V] = {0} inicializa a matriz com zero em todas as posições.
Neste modelo, trocamos os rótulos \(a,b,c\) e \(d\) por índices na matriz. Por exemplo, add_edge(mat, 0, 1) corresponde à aresta entre os vértices \(a\) e \(b\).
Modifique a implementação anterior para grafos direcionados. Use o grafo B para testar seu código.
Refinamento
Nota-se que é muito simples implementar uma matriz de adjacências. Nesta subseção, propomos algumas modificações a fim de obter um código mais funcional.
Ao invés de termos um grafo com tamanho fixo definido por uma constante, vamos criar nossa matriz usando o tamanho do conjunto de vértices dinâmicamente.
A função zeros aloca uma matriz bidimensional com lin linhas e col colunas, inicializando todas as posições com zero. O uso de calloc garante tanto a alocação quanto a inicialização. Em caso de falha durante a alocação de alguma linha, toda a memória previamente alocada é corretamente liberada.
int** zeros(size_t lin, size_t col) {
int **matrix = calloc(lin, sizeof(int *));
if (!matrix) return NULL;
for (size_t i = 0; i < lin; i++) {
matrix[i] = calloc(col, sizeof(int));
if (!matrix[i]) {
for (size_t k = 0; k < i; k++)
free(matrix[k]);
free(matrix);
return NULL;
}
}
return matrix;
}
void display_matrix(int** matrix, size_t lin, size_t col) {
for (size_t i = 0; i < lin; i++) {
for (size_t j = 0; j < col; j++)
printf("%d ", matrix[i][j]);
printf("\n");
}
}Na linha 1, estamos alocando memória para um vetor de ponteiros. Como a alocação é feita com calloc, todos os ponteiros são inicializados com o valor zero, isto é, NULL. Como de praxe, na linha seguinte, verificamos se a alocação foi realizada com sucesso.
O laço da linha 5 aloca memória para as linhas da matriz utilizando calloc, garantindo que todas as posições sejam inicializadas com zero. Após cada alocação, verificamos se ela foi bem-sucedida. Em caso de falha, liberamos a memória previamente alocada para as linhas anteriores e para o vetor de ponteiros.
A função auxiliar display_matrix é usada para imprimir uma matriz. Por exemplo, o código abaixo
int** mat = zeros(3, 4);
display_matrix(mat, 3, 4);vai gerar a seguinte saída:
0 0 0 0
0 0 0 0
0 0 0 0
Uma vez que alocamos memória, precisamos liberar o espaço reservado quando finalizarmos o programa.
Primeiramente, liberamos a memória de cada linha com free(mat[i]). Depois liberamos o vetor de ponteiros mat.
Agora podemos trabalhar com a semâtica de grafos ao invés de matrizes. Primeiro, vamos criar um nova estrutura que contém uma matriz de ordem \(|V|\times |V|\).
Usamos size_t para armazenar a ordem do grafo, isto é, o número de vértices. Lembre-se que size_t é usado para contagem de elementos ou tamanho de estruturas por se tratar de um inteiro sem sinal e, portanto, não admite valores negativos.
Sabemos que a matriz de adjacência de um grafo é sempre quadrada, portanto:
Assim, a função simple_graph nada mais faz do que criar uma estrutura do tipo Graph na memória heap e instanciar a matriz quadrada de ordem V inicialmente preenchida com zeros, representando um grafo sem arestas.
Para adicionar uma aresta entre dois vértices v e u, vamos associar a ela um peso w. Note que verificamos se os índices v e u pertencem ao intervalo válido.
Também tomamos cuidado para que não fossem acessadas posições negativas na matriz usando size_t.
Neste modelo, pesos iguais a zero não são permitidos, pois o valor zero é reservado para indicar ausência de aresta.
As funções a seguir calculam o grau e a vizinhança de um vértice v em um grafo não-direcionado representado por matriz de adjacência. A condição mat[v][j] != 0 indica a existência de uma aresta entre os vértices v e j.
size_t degree(Graph* graph, size_t v) {
size_t deg = 0;
for (size_t j = 0; j < graph->V; j++){
if (graph->adj_matrix[v][j] != 0)
deg++;
}
return deg;
}
size_t* neighborhood(Graph* graph, size_t v) {
if (v >= graph->V) return NULL;
size_t k = 0;
size_t size = degree(graph, v);
size_t* neighbor = malloc(size*sizeof(size_t));
if (!neighbor) return NULL;
for (size_t j = 0; j < graph->V; j++){
if (graph->adj_matrix[v][j] != 0)
neighbor[k++] = j;
}
return neighbor;
}A função neighborhood recebe o grafo graph e um vértice v. Ela percorre a linha correspondente a v na matriz de adjacência, preenchendo o conjunto de vértices adjacentes.
As próximas funções imprimem a matriz de adjacências e a vizinhança, respectivamente.
Finalmente, vamos encapsular a função destroy_matrix para obtermos uma consistência semântica.
Exemplo de uso para o grafo A:
int main() {
size_t V = 4;
Graph* g = simple_graph(V);
add_edge(mat, 0, 1, 1); // Liga 'a' e 'b'
add_edge(mat, 0, 2, 1); // Liga 'a' e 'c'
add_edge(mat, 1, 2, 1); // Liga 'b' e 'c'
add_edge(mat, 1, 3, 1); // Liga 'b' e 'd'
printf("Representação por Matriz de Adjacencia:\n");
display_graph(g);
size_t v = 2;
size_t* neighbor = malloc(V * sizeof(size_t));
size_t deg = neighborhood(g, v, neighbor);
printf("\nO vertice v = %zu possui %zu vizinho(s): ", v, deg);
display_neighborhood(neighbor, deg);
destroy_graph(g);
free(neighbor);
return 0;
}Saída:
Representação por Matriz de Adjacencia:
0 1 1 0
1 0 1 1
1 1 0 0
0 1 0 0
O vertice v = 2 possui 2 vizinho(s): N(v) = { 0 1 }
Modifique a implementação anterior para grafos direcionados. Use o grafo B para testar seu código. Lembre-se que agora temos de calcular a vizinhança/grau de entrada e de saída.
A função para imprimir a vizinhança deve mostrar a vizinhança de entrada e a de saída, por exemplo:
O vertice v = 2 possui 3 vizinho(s): N-(v) = { 1 } e N+(v) = { 0 3 }
Se liga! A matriz de adjacência não pode ser usada para implementar multigrafos naturalmente, pois ela não distingue arestas paralelas e, por isso, exige adaptações.
Lista de Adjacência
A lista de adjacência nada mais é que uma lista de vizinhos para cada vértice.
Usando os mesmos exemplos anteriores, para o grafo A:
- \(\mathcal{N}(a) = \{b, c\}\)
- \(\mathcal{N}(b) = \{a, c, d\}\)
- \(\mathcal{N}(c) = \{a, b\}\)
- \(\mathcal{N}(d) = \{b\}\)
Para o grafo direcionado B, temos:
- \(\mathcal{N^-}(e)=\{g\}\text{ e }\mathcal{N^+}(e)=\{f\}\)
- \(\mathcal{N^-}(f)=\{e\}\text{ e }\mathcal{N^+}(f)=\{g\}\)
- \(\mathcal{N^-}(g)=\{f\}\text{ e }\mathcal{N^+}(g)=\{e,h\}\)
- \(\mathcal{N^-}(h)=\{g\}\text{ e }\mathcal{N^+}(h)=\emptyset\)
Implementação
Para implementar essas listas, vamos construir listas ligadas.
void display_graph(Graph* graph) {
for (size_t i = 0; i < graph->V; i++) {
printf("%zu:", i);
for (Node* cur = graph->array[i]; cur; cur = cur->next) {
printf(" %d", cur->dest);
}
printf("\n");
}
}
void display_neighborhood(Node* neighbor, size_t size) {
printf("N(v) = { ");
for (Node* cur = neighbor; cur; cur = cur->next)
printf("%d ", cur->dest);
printf("}\n");
}int main() {
size_t V = 4;
Graph* graph = simple_graph(V);
add_edge(graph, 0, 1);
add_edge(graph, 0, 2);
add_edge(graph, 1, 2);
add_edge(graph, 1, 3);
printf("Representação por Lista de Adjacencia:\n");
display_graph(graph);
size_t v = 2;
size_t deg = degree(graph, v);
Node* neighbor = neighborhood(graph, v);
printf("\nO vertice v = %zu possui %zu vizinho(s): ", v, deg);
display_neighborhood(neighbor, deg);
destroy_graph(graph);
return 0;
}Representação por Lista de Adjacencia:
0: 2 1
1: 3 2 0
2: 1 0
3: 1
O vertice v = 2 possui 2 vizinho(s): N(v) = { 1 0 }
Matriz de Incidência
Dado o grafo com \(n\) vértices e \(m\) arestas, a matriz de incidência é uma matriz \(n\times m\) em que as linhas são os vértices, as colunas são as arestas e o valor na interseção indica se a aresta incide sobre o vértice.
| (A,B) | (A,C) | (B,C) | (B,D) | |
|---|---|---|---|---|
| A | 1 | 1 | 0 | 0 |
| B | 1 | 0 | 1 | 1 |
| C | 0 | 1 | 1 | 0 |
| D | 0 | 0 | 0 | 1 |
Note que a soma de cada coluna é sempre 2.
| (A,B) | (B,C) | (C,A) | (C,D) | |
|---|---|---|---|---|
| A | -1 | 0 | 1 | 0 |
| B | 1 | -1 | 0 | 0 |
| C | 0 | 1 | -1 | -1 |
| D | 0 | 0 | 0 | 1 |
No caso de grafo direcionado, atribuímos \(-1\) se a aresta \(e\) sai de \(v\), \(1\) se a aresta \(e\) entra em \(v\) e \(0\) caso contrário. Logo, a soma das colunas deve ser sempre zero.
Implementação
Considerando a implementação da matriz de adjacência, vamos reutilizar as funções:
zerosdisplay_matrixdestroy_matrixdisplay_neighborhooddestroy_graph
A matriz de incidência não é necessariamente quadrada, portanto:
Também modificamos o nome da matriz para inc_matrix para se referir a incidência. Dessa forma, reaproveitamos as funções add_edge, degree e neighborhood trocando adj_matrix por inc_matrix.
A função para criar um grafo vazio (matriz com zero em todas as posições) é semelhante ao caso da matriz de adjacência. Só temos de garantir que a matriz tenha V linhas e E colunas.
Para imprimir a matriz, temos de tomar cuidado para passar as linhas e colunas corretamente.
Agora, um exemplo de uso usando o grafo A novamente:
int main() {
size_t V = 4; // vertices
size_t E = 4; // arestas
Graph* g = simple_graph(V, E);
add_edge(g, 0, 1, 1);
add_edge(g, 1, 2, 1);
add_edge(g, 2, 3, 1);
add_edge(g, 2, 0, 1);
printf("Representação por Matriz de Incidencia:\n");
display_graph(g);
size_t v = 2;
size_t deg = degree(g, v);
size_t* neighbor = neighborhood(g, v);
printf("\nO vertice v = %zu possui %zu vizinho(s): ", v, deg);
display_neighborhood(neighbor, deg);
destroy_graph(g);
free(neighbor);
return 0;
}Representação por Matriz de Incidencia:
0 1 1 0
1 0 1 0
1 1 0 1
0 0 1 0
O vertice v = 2 possui 3 vizinho(s): N(v) = { 0 1 3 }
Modifique a implementação anterior para grafos direcionados. Use o grafo B para testar seu código. Lembre-se que agora temos de calcular a vizinhança/grau de entrada e de saída.
Nesta seção, estudamos como os grafos podem ser representados na programação. Nós
✅ estudamos a matriz de adjacência, as listas de adjacência e a matriz de incidência;
✅ implementamos os algoritmos para determinar o grau de um vértice e sua vizinhança.