MAC0338   Análise de Algoritmos

Página preparada por Paulo Feofiloff para a disciplina MAC 338 Análise de Algoritmos, versão 1999.
O endereço original desta página é http://www.ime.usp.br/~pf/mac338/aulas/grafos.htm.

 

Grafos

Esta página introduz os conceitos de grafo, caminho, e território (de um vértice). Ela serve de introdução a vários problemas (caminho mínimo, árvore geradora, ordenação topológica, etc.) que estudaremos a seguir.

 

O que é um grafo?

Um grafo é um objeto formado por dois conjuntos: um conjunto de vértices e um conjunto de arcos.   Se V é o conjunto de vértices e A é o conjunto de arcos de um grafo, podemos dizer que o grafo é o par (V,A)

Cada arco corresponde a um par ordenado de vértices; o primeiro vértice do par é a ponta inicial do arco; o segundo vértice é a ponta final do arco. Um arco com ponta inicial u e ponta final v pode ser indicada por (u,v) ou ainda por uv.  Algumas pessoas gostam de dizer "grafo orientado" ou "grafo dirigido", para enfatizar o caráter orientado dos arcos.

Dizemos que um arco (u,v) sai do vértice u .  Analogamente, um arco (u,v) entra em um vértice v. Dizemos também que um arco (u,v) vai de u a v.

Um grafo é simétrico se para cada arco (u,v) existe um correspondente arco reverso (v,u). É comum, principalmente em livros de teoria das grafos, chamar um grafo simétrico de "não-orientado" ou "não-dirigido" ou simplesmente de "grafo".

Um arco que tem a mesma ponta inicial e final é chamado dito um laço (loop). Dois arcos que têm a mesma forma, digamos (u,v) são chamados de paralelos.

Você pode imaginar que um grafo é uma espécie de mapa rodoviário: os vértices são cidades e as arestas são estradas de mão única.

 

Grafos na natureza

A natureza está cheia de grafos.  Eis alguns exemplos: 

 

Grafos no computador

Para especificar um grafo é preciso dizer quais são seus vértices e quais são suas arestas. É fácil especificar os vértices: podemos dizer que eles são 1, 2,  . . . , n.  Mas como especificar as arestas?

Uma boa estrutura de dados para especificar as arestas é a matriz de adjacência. Trata-se de uma matriz quadrada, digamos A, cujas linhas e colunas são indexadas pelos vértices. Para cada par u,v de vértices,

A[u,v] = 1 se existe aresta de u para v e
A[u,v] = 0 em caso contrário.

Uma outra estrutura de dados usada para especificação as arestas de um grafo é um conjunto de listas de adjacências. Para cada vértice u temos uma lista linear

Adj[u] ,

implementada com ponteiros, que contém todos os vértices v tais que (u,v) é um arco.   (A lista pode ter a seguinte estrutura. Cada nó da lista pode ter um campo vert e um campo prox. Se p é o endereço de um nó então vert[p] é o vértice armazenado no nó e prox[p] é o endereço do próximo nó da lista. Se p é o endereço do último nó da lista então prox[p] = NIL. O endereço do primeiro nó da lista é Adj[u].)

 

Exercícios

  1. Mostre que um grafo com n vértices tem no máximo n2 arestas.

  2. Escreva um algoritmo para verificar se (u,v) é uma aresta, ou seja, para verificar se v é adjacente a u.

  3. Quanto espaço ocupa a representação de um grafo por meio de listas de adjacência?

  4. [IMPORTANTE]  Considere o seguinte algoritmo que calcula o "grau de saída" g[u] de cada vértice u. O algoritmo supõe que os vértices do grafo são 1, 2,  . . . , n.
    EXERCÍCIO (n, Adj)
      para u := 1 até n faça
          g[u] := 0
      para u := 1 até n faça
          para cada v em Adj[u] faça
              g[u] := g[u]+1
      devolva g

    Mostre que esse algoritmo consome O(n+nm) unidades de tempo, onde m é o número de arestas. Melhore esta estimativa, mostrando que o algoritmo consome O(n+m) unidades de tempo.

Grafos no SGB

O SGB representa internamente um grafo através de listas de adjacências.

Vértice (Vertex)

Cada Vértice é representado no SGB através de uma estrutura com dois campos padrões e seis campos de utilidade geral (do tipo util): portanto um vértice ocupa 32 bytes na maioria dos sistemas, não contando a memória necessária para strings suplementares. Os campso padrão são

Se v aponta para um Vertex e v->arcs é NULL, então nào existem arcos saindo de v. Entretanto, se v->arcs não é NULL, ele aponta para uma estrutura do tipo arco Arc representando um arco de saindo de v, e esta estrutura Arc tem um campo next que aponta para o próximo arcos na lista ligada, encabeçada por v->arcs, de todos os arcos saindo de v.

Os campos para uso geral são chamados u, v, w, x, y, z. Macros podem ser usadas para dar a estes campos significado, dependendo da palicação.


  typedef struct vertex_struct {
    struct arc_struct *arcs; /* lista ligada de arcos saindo deste vértice */
    char *name; /* string identificando este vértice simbolicamente */
    util u, v, w, x, y, z; /* campos de uso geral */
  } Vertex;

Arco (Arc)

Cada arco é representado por uma estrutura do tipo Arc. Cada Arc tem três campos padrão e dois campos para uso geral. Portanto, esta estrutura ocupa 20 bytes na maioria dos computadores. Os campos padrao são

Se a aponta par um Arc em uma lista de arcos saindo de v, ele representa um arco de comprimento a->len indo de v até a->tip, e o próximo arco saindo de v na lista é representado por a->next.

Os campos para uso geral são chamdos a e b.


  typedef struct arc_struct {
    struct vertex_struct *tip; /* o arco aponta pra este vértice */
    struct arc_struct *next; /* outro arco saindo do mesmo vértice */
    long len; /* comprimento do arco */
    util a,b; /* campos de uso geral */
  } Arc;
  

Grafo (Graph)

Estamos agora preparados para olharmos em uma estrutura do tipo Graph. Esta estrutura pode ser passada para um algoritmo que trabalha sobre grafos.

Uma estrutura do tipo Graph tem sete campos padrão e seis campos para uso geral. Os campos padrão são

Os campos para uso geral são uu, vv, ww, xx, yy, zz. Como uma conseqüência destas convenções, nós visitamos todos os arcos de um grafo g usando o seguinte trecho de programa:


  Vertex *v;
  Arc *a;

  for (v = g->vertices; v < g->vertices + g->n; v++)
    for (a=v->arcs; a; a= a->netx)
      visite(v,a);


typedef struct graph_struct {
  Vertex *vertices; /* começo do vertor de vértices */
  long n; /* número total de vértices */
  long m; /* número total de arcos */
  char id[ID_FIELD_SIZE]; /* Identificação do grafos */
  char util_types[15]; /* uso dos campos para uso geral */
  Area data; /* o bloco principal de dados */
  Area aux_data; /* blocos auxiliares  */
  util uu,vv,ww,xx,yy,zz; /* campos de uso geral */
} Graph;


Campos para uso geral (UTIL)

As estruturas Vertex, Arc e Graph possuem vários campos para uso geral, que são ou não usados dependendo-se da aplicação. Cada campo de uso geral é do tipo union de nome util que pode armazenar vários tipos de apontadores ou um inteiro longo.

Os sufixos .V, .A, .G e .S no nome de uma variável de uso geral representa um apontador para um Vertex, Arc, Graph ou uma string, respectivamente; O sufixo .I significa que a variável é um inteiro (longo).


typedef union {
  struct vertex_struct *V; /* aponta para um Vertex */
  struct arc_struct *A; /* aponta para um Arc */
  struct graph_struct *G; /* aponta para um Graph */
  char *S; /* aponta para uma  string */
  long I; /* inteiro */
} util;

Campo util_types

O campo util_types deve sempre armazenar uma string de comprimento 14, seguida do usual '/0' (caractere nulo). Os primeiros seis caracteres do util_types especifica o uso dos campos de uso geral u, v, w, x, y e z; os próximos dois caracteres dão o formato do campos de uso geral das estruturas Arc; os últimos seis dão o formato dos campos de uso geral da estrutura Graph. Cada caractere deve ser um I (denotando um inteiro longo), S (denotando um apontador para uma string), V (denotando um apontador para um Vertex, A (denotando um apontador para um Arc, ou Z (denotando um campo que não esta sendo usado). O valor default de util_types é
"ZZZZZZZZZZZZZ"
, quando nenhum campo para uso geral esta sendo usado.

Por exemplo, suponha que um grafo bipartido g usa o campo g->uu.I para guarda o tamanho da primeira das partições. Suponha ainda que g tem uma string em cada campo de uso geral a de cada Arc e usa o campo para uso geral w de cada Vertex para apontar para um Arc. Se g Não uso nenhum dos demais campos de uso geral então o seu util_types deve conter

"ZZAZZZSZIZZZZZ".

Módulo GB_GRAPH do SGB

#include <gb_graph.h>

Este módulo inclui rotinas para alocar e armazenar novos grafos, novos arcos, novas strings e novas struturas de dados de todos os tipos:

Módulo GB_SAVE do SGB

#include <gb_save.h>

Este módulo contém código para converter grafos da sua representação interna para uma representação simbólica e vice-versa:

 

Passeios, caminhos, ciclos e circuitos

Um passeio em um grafo é uma seqüência de vértices em que cada dois vértices consecutivos são ligados por um arco. Mais exatamente, um passeio é uma seqüência 

<v0,a1,v1,a2,...,vk-1,ak,vk> 

onde v0, v1,....,vk são vértices, a1,a2,....,ak são arcos e, para cada i, ai é um arco de vi-1 a vi. O vértice v0 é o início do passeio e o vértice vk é o seu término.

Um caminho é um passeio sem vértices repetidos. Um ciclo é um passeio onde v0 = vk. Finalmente, um circuito é um ciclo sem vértices, com exceção feita a v0 e vk.

 

Exercícios

  1. Problema: Dados vértices r e t de um grafo, decidir se existe um caminho de s a t. Mostre que o algoritmo guloso não resolve o problema.   [O algoritmo guloso pode ser resumido assim: Cada iteração começa com um caminho  (v0,v1,...,vk-1,vk)  tal que v0 = s. Cada iteração consiste no seguinte: se vk = t então pare. Senão, seja vk+1 um vértice adjacente a vk e comece nova iteração com o caminho (v0,v1,...,vk-1,vk,vk+1).]

 

O território (ou domínio) de um vértice

Um vértice v é acessível a partir de um vértice r se existe um caminho de r a v. O território de um vértice r é o conjunto de todos os vértices acessíveis a partir de r. O território de r será denotado por

X(r) .

Um vértice r é central se X(r) é o conjunto de todos os vértices do grafo. Um vértice é sorvedouro de X(r) = {r}.

PROBLEMA:  Dado um vértice r de um grafo, encontrar o território de r.

A solução do problema será dada por um vetor cor indexado pelos véritices: para cada vértice u, teremos cor[u] = NEGRO se e só se u está no território de r.  Um algoritmo guloso muito simples resolve o problema. No início de cada iteração temos duas classes de vértices, a classe dos vértices NEGROs e a classe dos BRANCOs. Todo vértice NEGRO está no território de r. Cada iteração escolhe um arco que vai de um vértice NEGRO a um vértice BRANCO e pinta esse último vértice de NEGRO.

Para administrar o processo, convém introduzir uma terceira cor, CINZA. O algoritmo abaixo supões que os vértices do grafo são 1, 2, . . . , n.

 









TERITÓRIO (n, Adj, r)
  para u := 1 até n faça
    cor[u] := BRANCO
  cor[r] := CINZA
  enquanto existe u tal que cor[u] = CINZA faça
    para cada v em Adj[u] faça
      se cor[v] = BRANCO então
        cor[v] := CINZA
    cor[u] := NEGRO
  devolva cor

 

Conexão

Um grafo é fortemente conexo se, para todo par r, s de seus vértices, existe um caminho de r a s  e  um caminho de sr

Em outras palavras, um grafo é fortemente conexo se e só se X(s) = V para todo s.

 

Exercícios

  1. Escreva um algoritmo que decida se um grafo é ou não fortemente conexo. Que coisa o algoritmo pode devolver para comprovar que o grafo é conexo? Que coisa pode devolver para comprovar que o grafo é desconexo?

 

Grafos simétricos

Um tipo muito comum e muito importante de grafo é o grafo simétrico. Um grafo é simétrico se para cada arco (u,v) existe também o arco (v,u). Direi às vezes que a aresta (v,u) é "gêmea" da aresta (u,v).

Se o grafo é representado no computador por meio de listas de adjacência, digamos Adj, então v está em Adj[u] se e só se u está em Adj[v].   Se o grafo for representado por uma matriz de adjacência, digamos A, então A[u,v] = A[v,u] para todo par u, v.

Propriedade importante de grafos simétricos: existe caminho de r a s se e só se existe caminho de s a r. Conseqüência: um grafo simétrico é conexo se existe um vértice r tal que,  para todo vértice vhá um caminho de r a v.  (Ou seja, um grafo simétrico é conexo se tem um vértice central. E se tiver um vértice central então todos são centrais.)

 

Exercícios

  1. Escreva um algoritmo que decida se um grafo simétrico é ou não conexo. Que coisa o algoritmo pode devolver para comprovar que o grafo é conexo? Que coisa pode devolver para comprovar que o grafo é desconexo?
  2. Escreva um algoritmo que recebe um grafo e decide se ele é simétrico. Qual o consumo de tempo do seu algoritmo?
  3. (Versão mais difícil do exercício acima) Escreva um algoritmo que recebe um grafo e decide em tempo O(n+m) se ele é simétrico, onde n e m são o número de vértices e arcos do grafo, repectivamenete.

 

 

 


Last modified: Tue Mar 18 15:33:46 EST 2003
Paulo Feofiloff
IME-USP