apostila alg 3

40
Algoritmos e Estrutura de Dados III Plano de ensino da disciplina: 30.705-0 Algoritmos e Estrutura de Dados III Departamento: 03 Engenharias e Ciência da Computação Carga horária: 60 horas 30 / 30 horas Créditos: 04 EMENTA: Estrutura de dados dinâmicas: tipos de árvores e suas aplicações, implementação de índices utilizando árvores e sua interação com arquivos de dados, tipos de arquivos, compressão, grafos, estudo de implementações. OBJETIVOS: Capacitar o aluno na avaliação e no projeto de quaisquer estruturas de dados que utilizam árvores. Operações com arquivos e implementação de índices com árvores, correção e eficiência das implementações. RELAÇÃO DOS CONTEÚDOS: - Conceituação de estruturas de dados tipo árvores: tree, tree C, AVL, etc.; - Estrutura de dados tipo árvores binárias, implementação utilizando TAD com modalidade em ponteiros. - Conceituação de estrutura de dados tipos árvore B, B+, aplicações de árvore B e B+. Implementações de Índices - Arquivos x Índices, pesquisa em tabelas: Arquivos sequenciais, sequenciais indexados, indexados e endereço por cálculo (Hashing); - Técnicas de compressão de arquivos. - Conceituação de grafos em computadores, aplicações de grafos, implementação de grafos; BIBLIOGRAFIA BÁSICA (LIVROS TEXTOS): TENEMBAUM, Aaron M. Estrutura de Dados Usando C. São Paulo: Makron Books do Brasil, 1995. PEREIRA, Sílvio Lago. Estruturas de Dados Fundamentais: Conceitos e Aplicações. São Paulo: Ed. Érica, 1996. VELLOSO, Paulo. Estruturas de Dados. Rio de Janeiro: Ed. Campus, 1991. VILLAS, Marcos Vianna & Outros. Estruturas de Dados. Conceitos e Técnicas de implementação. Rio de Janeiro: Ed. Campus, 1993. BIBLIOGRAFIA COMPLEMENTAR (LIVROS REFERENCIADOS): HOLZNER, Steven. Fundamentos de Estruturas de Dados. 3 ed. Rio de Janeiro: Ed. Campus, 1987. HOROWITZ, Ellis. Fundamentos de Estruturas de Dados. 3 ed. Rio de Janeiro: Ed. Campus, 1987. SZWARCFITER, JAIME LUÍZ. Estruturas de Dados e seus Algoritmos. Rio de Janeiro: Ed. LTC, 1994.

Upload: tay-gomes

Post on 02-Jan-2016

128 views

Category:

Documents


1 download

TRANSCRIPT

Page 1: Apostila Alg 3

Algoritmos e Estrutura de Dados III

Plano de ensino da disciplina: 30.705-0 Algoritmos e Estrutura de Dados III

Departamento: 03 Engenharias e Ciência da Computação

Carga horária: 60 horas 30 / 30 horas Créditos: 04

EMENTA:

Estrutura de dados dinâmicas: tipos de árvores e suas aplicações, implementação de índices utilizando árvores e sua interação com arquivos de dados, tipos de arquivos, compressão, grafos, estudo de implementações.

OBJETIVOS:

Capacitar o aluno na avaliação e no projeto de quaisquer estruturas de dados que utilizam árvores. Operações com arquivos e implementação de índices com árvores, correção e eficiência das implementações.

RELAÇÃO DOS CONTEÚDOS:

- Conceituação de estruturas de dados tipo árvores: tree, tree C, AVL, etc.;- Estrutura de dados tipo árvores binárias, implementação utilizando TAD com modalidade em ponteiros.- Conceituação de estrutura de dados tipos árvore B, B+, aplicações de árvore B e B+. Implementações de Índices- Arquivos x Índices, pesquisa em tabelas: Arquivos sequenciais, sequenciais indexados, indexados e endereço por

cálculo (Hashing);- Técnicas de compressão de arquivos.- Conceituação de grafos em computadores, aplicações de grafos, implementação de grafos;

BIBLIOGRAFIA BÁSICA (LIVROS TEXTOS):

TENEMBAUM, Aaron M. Estrutura de Dados Usando C. São Paulo: Makron Books do Brasil, 1995.PEREIRA, Sílvio Lago. Estruturas de Dados Fundamentais: Conceitos e Aplicações. São Paulo: Ed. Érica, 1996.VELLOSO, Paulo. Estruturas de Dados. Rio de Janeiro: Ed. Campus, 1991.VILLAS, Marcos Vianna & Outros. Estruturas de Dados. Conceitos e Técnicas de implementação. Rio de Janeiro:

Ed. Campus, 1993. BIBLIOGRAFIA COMPLEMENTAR (LIVROS REFERENCIADOS):

HOLZNER, Steven. Fundamentos de Estruturas de Dados. 3 ed. Rio de Janeiro: Ed. Campus, 1987.HOROWITZ, Ellis. Fundamentos de Estruturas de Dados. 3 ed. Rio de Janeiro: Ed. Campus, 1987.SZWARCFITER, JAIME LUÍZ. Estruturas de Dados e seus Algoritmos. Rio de Janeiro: Ed. LTC, 1994.

Page 2: Apostila Alg 3

Árvores

Além das filas e pilhas vistas no semestre anterior, que eram estruturas lineares, existem outras formas usadas para representar os dados, que são chamadas genericamente de “não-lineares”. Essa representação permite que sejam feitos outros tipos de relações entre os dados, como por exemplo, relação de hierarquia.

Um dos exemplos mais significativos de estruturas não lineares que representam as relações de hierarquia é a Árvore.

Uma árvore pode ser definida como um conjunto de nós (ou nodos), tais que:• existe um nó denominado raíz da árvore• os demais nós formam subconjuntos distintos S1,S2,...Sm, onde cada subconjunto desses é uma

árvore. Esses subconjuntos recebem a denominação de sub-árvores.

Um exemplo bem conhecido de relação de estruturação em árvore é a estruturação de um livro, que é subdividido em capítulos, onde cada capítulo é subdividido em seções, que por sua vez, possuem tópicos.

Considere agora o seguinte exemplo de árvore genealógica

Analisando essa árvore, podemos concluir:• Essa árvore possui 10 nós distribuídos aleatoriamente. O nodo Pedro é a raiz da árvore, que tem 3

sub-árvores com Maria, André e Marcelo como raízes.

• O número de sub-árvores de um nó determina o grau desse nó. Dessa forma, Pedro e Maria têm grau 3, enquanto André tem grau 2 e Marcelo tem grau zero. Nodos que tem grau zero são denomidados terminais ou folhas.

Pergunta: Qual é o grau do nodo Viviane?

Apostila de Algoritmos e Estrutura de Dados III 2

Árvores

Introdução ÁrvoresBinárias

Árvores

PercursoConceito Balanceamento

Pedro

VivianeMárciaRodrigo Cézar Aide

Cláudio

Maria André Marcelo

Page 3: Apostila Alg 3

• Considerando que cada nó tem um grau máximo (número de sub-árvores das quais esse nó é raiz) e que todos os nós possuem o mesmo grau máximo, definimos esse grau como o grau da árvore.

• Uma árvore é homogênea quando todos os seus nodos possuem as mesmas características de conteúdo, ou seja, o mesmo tipo de informação; caso contrário ela é então chamada de árvore heterogênea.

• Para identificar os nós da estrutura, usamos as denominações da relação de hierarquia existente em uma árvore genealógica. Dessa forma, Pedro é pai de Maria, André e Marcelo que são irmãos entre si. No sentido inverso, Rodrigo, Márcia e Viviane são filhos de Maria e netos de Pedro. Para conhecermos os antepassados de um nodo, basta identificarmos todos os nodos ao longo do caminho entre a raíz e este nodo. Ex: os antepassados de Cláudio são, na ordem: Pedro, Maria e Viviane.

• Um conceito importante no estudo de árvores é o conceito de nível, que representa a distância do nodo até a raiz. Por definição, a raiz da árvore tem nível 0. Na figura anterior, os nodos Maria, André e Marcelo têm nível 1, os nodos Rodrigo, Márcia, Viviane, Cézar e Aide tem nível 2, assim por diante. O nodo de maior nível nos fornece a altura (ou profundidade) da árvore.

Pergunta: No exemplo que estamos usando, a árvore tem altura (???)? Exercício : Tente construir uma árvore com base nas informações abaixo:- O nodo C tem grau 3.- O nodo X é neto de C e filho de B.- O avô de B é A.- O nodo A tem altura 0 e T tem altura 1.- Os antepassados de P são A,T e K, que são também antepassados de H.- T tem grau 2, e um dos seus filhos é o nodo S.- O nodo G tem 2 sub-árvores que são netos de C.- D é irmão de G, e é uma folha.- E e F tem graus 0 e 1 respectivamente e são também netos do nodo C.- O nodo N tem nível 4.

Representações das Árvores É comum a utilização da representação “cima-baixo” devido à sua popularidade, porém, uma árvore pode ser representada também de outras maneiras:

• Representação natural de uma árvore:

Apostila de Algoritmos e Estrutura de Dados III 3

E JG H

CB D

IF

A

K

Page 4: Apostila Alg 3

• Representação por endentação:

ABEFCGDHKIJ

• Representação por conjuntos

• Representação por nível:( A (B) (C) (D) ) ...

Árvores Binárias e variantesDefinição: uma árvore binária se caracteriza pelo fato de todos os seus nodos terem no máximo duas sub-árvores, ou seja é uma árvore de grau 2. As duas sub-árvores de cada nó são denominadas sub-árvore esquerda e sub-árvore direita. Na figura abaixo, temos duas árvores binárias, sendo a da esquerda, incompleta e a da direita completa:

No segundo caso, cada nodo possui dois descendentes, exceto as folhas, que por definição não tem filhos. Geralmente nos referimos aos filhos de um nodo numa árvore binária como sendo da esquerda ou da direita, de acordo com o posicionamento dele.

Conversão de uma árvore de um grau qualquer para uma árvore binária:Supomos que temos um nodo A com 3 descendentes diretos, B C e D, etc. Deve-se então fazer o seguinte:

Apostila de Algoritmos e Estrutura de Dados III 4

A

B C D

E F GH

I JK

A

CB

D F

A

CB

D E F G

Page 5: Apostila Alg 3

- A raiz torna-se a raiz de uma árvore binária.- B torna-se o descendente da esquerda de A - C torna-se o descendente da direita de B.- D torna-se o descendente da direita de C.- E torna-se o descendente da direita de D.

Exercícios: transforme as seguintes árvores em árvores binárias:

Representações alternativas (no computador):

Existem duas maneiras alternativas de representarmos árvores binárias no computador:- em listas lineares e- em listas encadeadas.

a) Árvores binárias em listas lineares:Uma maneira de representar uma árvore binária numa lista linear consiste em declarar um vetor com tamanho máximo igual ao número máximo de nós da árvore binária.

A representação de árvores binárias na forma de listas lineares é especialmente indicada quando as árvores são completas, pois resulta na utilização máxima do espaço alocado.

A representação na forma de uma lista linear não é indicada em duas situações:- quando for necessário efetuar operações de inserção e retirada de nodos, pois potencialmente

exigirá uma grande movimentação dos dados para conseguir o efeito desejado; e - quando a árvore afasta-se mais e mais da condição de completa, ou seja, não é indicada em

situações onde a estrutura de dados é frequentemente alterada.

Árvore binária completa alocada sequencialmente. Árvore binária incompleta alocada sequencialmente

Apostila de Algoritmos e Estrutura de Dados III 5

A

DB C

A

DB C

E F H I J

A

DB C

FE G H I J

Page 6: Apostila Alg 3

Nesse tipo de representação, fica difícil saber qual é o ascendente de um átomo e embora é geralmente adequada para a maioria das aplicações, poderá haver sempre desperdício de espaço.

Na alocação encadeada subdividimos o espaço de um nó em três campos, como mostrado abaixo:

nodo Pos sub-árvore esquerda Pos sub-árvore direita

Veja abaixo, a representação da seguinte árvore:

Outra forma de representar a árvores seria no seguinte formato

Link esquerdo Nodo Link direito

A árvore então, seria apresentada da seguinte forma:

Operadores para árvores binárias:

Donald E. Knuth popularizou três ordens de percurso em árvores binárias: Pré-ordem ou prefixa, simétrica ou infixa e Pós-ordem ou pós-fixa.

Prefixa: - visita a raiz- visita a sub-árvore à esquerda- visita a sub-árvore à direita

Infixa: - visita a sub-árvore à esquerda- visita a raiz- visita a sub-árvore à direita

Posfixa: - visita a sub-árvore à esquerda- visita a sub-árvore à direita- visita a raiz

Considerando a árvore apresentada ao lado, veja a seqüência de nodos visitados de acordo com cada ordem de visitação:Visitação prefixa: A,B,D,E,C,F,G Visitação infixa: D,B,E,A,F,C,GVisitação posfixa: D,E,B,F,G,C,A

Apostila de Algoritmos e Estrutura de Dados III 6

A

BC

E F GD

Page 7: Apostila Alg 3

Exercícios:

1) Dada as seguintes árvores, faça para cada uma delas a visitação dos nodos utilizando a ordem prefixa, infixa e pósfixa

Árvores de Expressões

Uma expressão aritmética pode ser armazenada sob a forma de uma árvore binária, onde as raízes armazenariam as operações a serem efetuadas, e as sub-árvores à esquerda e direita armazenariam os operandos a serem usados.

Por exemplo, a expressão a+b deveria então ser apresentada com a seguinte forma:

Já no caso da expressão a + b * c, a prioridade dos operadores e a ordem de ocorrência, ambos, devem ser respeitados

ou seja, ao se percorrer as árvores na forma infixa e posfixa respectivamente teremos:a,+,b,*,c e a,b,c,*,+,

que é diferente da expressão b * c + a, que seria representada assim:

Apostila de Algoritmos e Estrutura de Dados III 7

A

BC

E F G

IH

D

a) b) A

BC

E G

IH

D F

K LJ

M

J

A

BC

E F

GIH

D

c)

K

ML

BA

+

B C

A

+

*

B C

A

+

*

Page 8: Apostila Alg 3

Após estas análises, a expressão ((A+B)/C)*(D-E), então, seria representada assim:

Como dica para resolver uma árvore completa, procure sempre resolver as expressões do centro para fora. No caso anterior, por exemplo, represente:

A + B / C D - E *

Exercícios:

Transforme as seguintes expressões para posfixa utilizando árvores binárias para fazer a transformação:a) (a+b) * (c-d)b) (a+b*c)*a + (c-d)*3c) (a+b*c)*a – 4*(5-6+1) + (c-d)*3d) (a+b-c) – (3-4)*(5-6+1) + (c-d*3)

Árvore binária de pesquisa

Uma árvore binária, cuja raiz armazena o elemento R, é denominada árvore binária de pesquisa se:♦ todo elemento armazenado na sub-árvore à esquerda é menor do que R,♦ todo elemento armazenado na sub-árvore à direita é maior do que R.

Uma árvore binária de pesquisa, visitada em ordem simétrica/infixa (E,R,D), resulta em uma lista de dados em ordem crescente. Veja os casos abaixo: na árvore Y, todos os elementos da sub-árvore esquerda (a,b,c) são menores que a raiz, e nenhum elemento da sub-árvore direita (e,f,g) é menor que a raiz. Entretanto, ela não é uma árvore binária de pesquisa, pois na sub-árvore da direita, cuja raiz é G, tem uma sub-árvore direita que armazena uma informação menor que a raiz.

X) Ordenada (binária de pesquisa) Y) Não ordenada

A árvore abaixo, é binária de pesquisa?

Apostila de Algoritmos e Estrutura de Dados III 8

*

-/

C+ D E

BA

D

E

C

A

G

FB

D

EB

C D FA

D

GB

C E FA

Page 9: Apostila Alg 3

Formato dos nodos a serem empregados na organização dos dados: Cada nodo precisa armazenar um elemento e referenciar duas sub-árvores:

Trabalho (parte 1):

Implementar as seguintes operações sobre uma A. B. P.:

- Inserção em uma ABP: Fornecido- Busca de 1 elemento na ABP: ?- Remoção de 1 elemento da ABP: ?- Listagem da árvore na forma: Em nível: opcional Prefixa: ? Infixa: Fornecido Posfixa: ?- Rotina para det. o > elemento da ABP: ?- Rotina para det. a altura da ABP: opcional

Obs: - as rotinas ? devem ser desenvolvidas pelo aluno.- As rotinas opcionais valerão meio ponto extra no trabalho- O programa deve ainda apresentar um menu para que seja selecionada a opção desejada.

Bibliografia Recomendada:

TENEMBAUM, Aaron M. e.. Estruturas de Dados Usando C. SP.: Ed. McGraw-Hill, 1995.PEREIRA, Sílvio do Lago. Estruturas de Dados Fundamentais: Conceitos e Aplicações. SP.: Ed. Érica, 1996.

Apostila de Algoritmos e Estrutura de Dados III 9

Page 10: Apostila Alg 3

Código Fornecido (em C++), base para compreensão da manipulação em árvore# include <iostream>

using namespace std;class nodo { private: nodo *esq; char info; nodo *dir; public: void ins (nodo* *T, char informacao); void infixa (nodo* *T);};

nodo *P;

void nodo::ins (nodo* *T, char informacao){ cout << *T ; if (*T == NULL) { *T = new nodo; //pode-se verificar aqui se a (*T) == NULL para inserir (*T)->esq = NULL; (*T)->info = informacao; (*T)->dir = NULL; cout << "ins. no " << (*T) <<" "<< (*T)->esq <<" "<< (*T)->info <<" "<< (*T)->dir << endl; } else{ if (informacao < (*T)->info) { ins (&((*T)->esq), informacao); } else{ ins (&((*T)->dir), informacao); } }}

void nodo::infixa (nodo* *T){ if ((*T) != NULL) { infixa (&(*T)->esq); cout << (*T) << " " << (*T)->esq << " " << (*T)->info << " " << (*T)->dir << endl; infixa (&(*T)->dir); }}

int main (void) { nodo *T; T = NULL; cout << &T << " "; T->ins (&T, 'd'); cout << &T << " "; T->ins (&T, 'c'); cout << &T << " "; T->ins (&T, 'a'); cout << &T << " "; T->ins (&T, 'h'); cout << &T << " "; T->ins (&T, 'e'); cout << &T << " "; T->ins (&T, 'i'); cout << &T << " "; T->ins (&T, 'g'); cout << &T << " "; T->ins (&T, 'f'); cout << endl << "Infixa: " << endl; T->infixa (&T); return(0);}

Apostila de Algoritmos e Estrutura de Dados III 10

Page 11: Apostila Alg 3

a) Inserção em uma A. B. P.:

Inserir um elemento X em uma Árvore Binária de Pesquisa vazia é trivial. Se a árvore não se encontra vazia deve-se fazer o seguinte: Compara-se o elemento a inserir com o elemento raiz da árvore. Se o elemento for menor, o inserimos na sub-árvore esquerda, senão, o inserimos na sub-árvore direita.

Insere (nodo *T , char inform) início se T = NULO então novo(T) T->esq = NULO T->info = inform T->dir = NULO senão se inform < T->info então Insere(T->esq,inform) senão Insere(T->dir,inform) fim_se fim_sefim

Para entendermos o funcionamento do algoritmo de inserção, veja o exemplo de construção de uma árvore binária de pesquisa com o conjunto de números{17,99,13,1,3,100,400}raiz nuloraiz 17

Análise DETALHADA do procedimento de inserção:Considere como exemplo a árvore B abaixo, que foi inserida através das linhas de programa apresentadas ao lado dela. Note que para criar a árvore, os nodos devem ser inseridos sempre por nível, do nível 0 ao nível n ou na ordem prefixa, pois para inserir o “C”, por exemplo, já deve ter sido inserido anteriormente o nodo “E”. Para inserir o “F”, já deve ter sido inserido anteriormente o nodo “H”, e assim por diante

Só para recordar, consideramos um programa que cria e atribui um ponteiro, por exemplo:

char *x;*x='a';

FFF0 “X”

FFF4

FFF4

a

Neste caso: x = FFF4 &x=FFF0 e *x = 'a'

Apostila de Algoritmos e Estrutura de Dados III 11

Representação interna de uma árvore binária ( com ponteiros)

17 17

99

17

9913

17

991313

1

3

100

400

E

H13C

A IF

G

nodo *T; T = NULL;

T->ins (&T, 'e'); T->ins (&T, 'c'); T->ins (&T, 'a'); T->ins (&T, 'h'); T->ins (&T, 'f'); T->ins (&T, 'i'); T->ins (&T, 'g');

Page 12: Apostila Alg 3

Com a análise da estrutura da árvore na memória do computador, após a inserção de todos os nodos, obtém-se uma tabela que é apresentada abaixo.

Para gerar tal tabela, basta incluir a seguinte linha na rotina infixa():cout << (*T) << " " << (*T)->esq << " " << (*T)->info << " " << (*T)->dir << endl;

T

(& de quem aponta para o nodo)

*T (*T)->esq

que informação tenho no cpo “esq”

(*T)->dir

que informação tenho no cpo “dir”

(*T)->info

que informação tenho no cpo “info”

FFF4 09FE 0A08 0A12 E

09FE 0A08 0A1C ----- (NULL) C

0A08 0A1C ----- ----- A

0A01 0A12 0A26 0A30 H

0A12 0A26 ----- 0A3A F

0A29 0A3A ----- ----- G

0A15 0A30 ----- ----- I

b) Pesquisa em uma árvore binária

Se a árvore for nula, nada a fazer, caso contrário, o processo de busca é o mesmo utilizado para inserção.

Void procura (...){ if (*T ==NULL) printf (“Nodo não existe!”); else { if( informacao < (*T)->info) procura ( usando sub-árvore esquerda) // procura para a esquerda else { if (informacao > (*T)->info) procura ( usando sub-árvore direita) // procura para a direita else // encontrou } }}

Apostila de Algoritmos e Estrutura de Dados III 12

09FE0A08

09FF 0A00E

0A010A12

FFF4 &(*T)09FE *T

0A080A1C0A1C

0A09 0A0AC

0A0B-

0A120A26

0A13 0A14H

0A150A30

0A1C0A1C 0A1D 0A1EA

0A1F-

0A26-

0A27 0A28F

0A290A3A

0A30-

0A31 0A32I

0A33-

0A3A-

0A3B 0A3CG

0A3D-

E

H13C

A IF

G

(*T)->esq (*T)->info (*T)->dirEstrutura do Nodo:

1 byte 2 bytes 1 byte

? TFFF4

09FE0A08

09FF 0A00E

0A010A12

FFF4 &(*T)09FE *T

0A080A1C0A1C

0A09 0A0AC

0A0B-

0A120A26

0A13 0A14H

0A150A30

0A1C0A1C 0A1D 0A1EA

0A1F-

0A26-

0A27 0A28F

0A290A3A

0A30-

0A31 0A32I

0A33-

0A3A-

0A3B 0A3CG

0A3D-

E

H13C

A IF

G

(*T)->esq (*T)->info (*T)->dirEstrutura do Nodo:

1 byte 2 bytes 1 byte

? TFFF4

Page 13: Apostila Alg 3

c) Retirada em uma A. B. P.:

Se o nodo a ser retirado de uma árvore for uma folha, basta atualizar o link do seu pai para que não aponte mais para o nó a ser retirado.

No caso de retirada de um nodo que é raiz, deve-se adotar o seguinte procedimento:- se a raiz não tem filho esquerdo, o filho direito passa a ser a raiz, e vice-versa-se a raiz tem ambos os filhos, pode-se optar por buscar a > chave esquerda ou a < chave direita para ser a nova raiz:Exercício: Remover o D:

Análise do procedimento de retiradaConsidere como exemplo a árvore B abaixo (a mesma que foi utilizada para explicar o procedimento de inserção)

São quatro as possibilidades existentes para retirada. Considere que o nodo a ser retirado pode ser:

a) Um nodo sem descendentes

b) Um nodo sem descendentes na direita mas com descendentes na esquerda.

c) Um nodo sem descendentes na esquerda mas com descendentes na direita.

d) Um nodo com 2 filhos.

No programa, isso pode ser representado por 4 testes de seleção (if)if ((*T)-> esq != NULL) && ((*T)-> dir ==NULL){ //somente descendentes na esq. ... (1 linha)}

if ((*T)-> esq == NULL) && ((*T)-> dir !=NULL){ //somente descendentes na dir. ... (1 linha)}

if ((*T)-> esq == NULL) && ((*T)-> dir ==NULL){ //nodo sem descendentes. ... (1 linha)}

if ((*T)-> esq != NULL) && ((*T)-> dir != NULL){ //nodo com 2 filhos ... (algumas linhas)}

No caso da retirada de uma folha (A, G ou I), (item a) o procedimento a ser realizado é muito simples:Simplesmente atribui nulo (NULL) para o ponteiro que está apontando para o nodo a ser retirado e libera-se a memória ocupada anteriormente pelo nodo.

Apostila de Algoritmos e Estrutura de Dados III 13

D

FB

C E GA

E

H13C

A IF

G

Page 14: Apostila Alg 3

No caso da retirada de um nodo com um filho (item b ou c), simplesmente o nodo pai passa a apontar para o neto. Exemplos:

No caso da retirada de um nodo com um ambos os filhos (item d), deve-se criar uma rotina aqui chamada de pega_maior ou Get_max, que deverá pegar o maior elemento da sub-árvore que está sendo excluída:

Apostila de Algoritmos e Estrutura de Dados III 14

E

H13C

A IF

G

E

H13C

IF

G

E

H13C

A IF

G

E

H13A

IF

G

E

H13C

A IF

G

E

H13C

A IG

E

H13C

A IF

G

Neste caso, como o Get_max deve ser aplicado sobre a sub-árvore esquerda do H, o valor a retornar pela função será o G

E

H13C

A IF

G

E

13C

A IF

G

G

E

13C

A IF

G

Page 15: Apostila Alg 3

Se, por exemplo, tivesse um F2 “pendurado” na esquerda do “G”, após a retirada do G ele deveria aparecer “pendurado” na direita do “F”

Abaixo é apresentado o algoritmo para exclusão:

Procedure exclusao(...)início se (*T ==NULL) exit; // elemento não foi encontrado se (informacao == T->info) // elemento encontrado na raiz P=t; se (T->esq == NULL) T=T->dir // A raiz não tem filho esquerdo

senão se (T->dir=NULL)

T=T->esq senão P = PegaMaior(T->esq); T->info = P->info;

fim_se libera(P);

fim_se senão se (informacao < T->info) então // elemento encontrado numa das sub-árvores exclusao(T->esq,informacao) senão exclusao(T->dir,informacao) fim_se fim_sefim

d) Listagem em uma Árvore Binária

Conforme visto anteriormente, podemos percorrer uma árvore passando por todos os nodos dela. Existem 4 formas de atravessamento (listagem) de uma árvore:- Prefixa (?) - Posfixa (?)

Infixa(...);início se (T != nil) infixa(t->esq); escreve(T->.info,','); infixa(t->.dir); fim_sefim

Rotina para pegar o maior elemento de uma árvore ( Pega_maior):O maior elemento de uma árvore binária ordenada encontra-se sempre no nodo que estiver mais à direita possível na árvore.

Apostila de Algoritmos e Estrutura de Dados III 15

E

13C

A IF

G

H

F2

E

13C

A IF

F2

G

Page 16: Apostila Alg 3

Trabalho 1 de Algoritmos e Estrutura de Dados 3

1. Introdução e Objetivos:

Este trabalho tem por objetivo verificar a aplicação de árvores binárias de pesquisa para a resolução de problemas comuns no âmbito da Ciência da Computação.

2. Descrição do trabalho

O trabalho, que poderá ser realizado por duplas ou grupos de até 3 alunos (no máximo) consiste em duas etapas:

a) implementação das rotinas de: - Inserção em uma ABP - Busca de 1 elemento na ABP: - Remoção de 1 elemento da ABP: - Listagem da árvore na forma prefixa, infixa e posfixa - Rotina para det. o maior elemento da ABP:

b) Aplicação prática da Árvore Binária de Pesquisa:

Dicionário de 1 Segundo de Andy.

Andy está com 9 anos e ficando ambicioso. Ele quer criar um programa que extraia palavras de um texto, até mesmo as palavras com hífens e listar todas as palavras encontradas no texto em ordem alfabética. A entrada para o programa seria um texto com no máximo 500 palavras, terminado com um EOF.

O papel do grupo (vocês) é criar um programa que liste todas as distintas palavras que existirem no texto em ordem alfabética, todas convertidas em minúsculas. Letras também são consideradas. Palavras como “maçã”, “Maçã”, “MAÇÔ ou “MaÇã” devem ser consideradas como a mesma palavras e serem gravadas no dicionário como “maçã”. E existe ainda um detalhe: se acontecer no texto a separação devido à uma troca de linha, o programa deverá entender a palavra. Vamos ao exemplo.

Arquivo de entrada (entrada.txt):

E então o padre guarda-va a sua coleção dentrodo GUaRDa-RoUpA, fican-do de guarda para que ninguém a roubasse.

Arquivo de saída (saída.txt)

acoleçãode dentrodo e entãoficandoguardaguarda-roupaguardavaninguémopadrepara queroubassesua

Dica para implementação: deve-se fazer um programa que leia o texto de entrada inserindo cada palavra lida em uma árvore binária de pesquisa, no caso da palavra ainda não se encontrar na árvore. Após o término do arquivo de entrada, deve ser gerado um arquivo de saída com as palavras ordenadas, bastando para isso percorrer a árvore binária de pesquisa de forma infixa.

* O idem b) deste trabalho foi tirado do conjunto de problemas mantidos pela Universidade de Valladolid, sob número 10815.

Apostila de Algoritmos e Estrutura de Dados III 16

Page 17: Apostila Alg 3

Exercícios

1 Construa uma árvore com base nas informações abaixo:- O nodo B tem grau 3.- O nodo K é neto de B e um de seus filhos é F.- O avô de S é A.- O nodo K tem um irmão.- O nodo Pai de T é o nodo U.- O filho de A, que é nodo irmão de B, tem dois filhos, sendo que um deles é o nodo P.- O nodo T tem altura 3, mas não é neto de B, tampouco filho de P.- Os nodos T e C estão no mesmo nível.- O nodo A tem altura 0, e o nodo D tem altura 1.- A árvore tem altura 4,- Os nodos X e Y tem grau 0.

2. Transforme as árvores do exercício 1 para árvore binária.

3. Represente as árvores do exercício 2 em uma estrutura de dados através de 3 vetores: ELOE, NODO e ELOD.

4. Faça o caminhamento prefixo, infixo e pós-fixo para as árvores do exercício 4.

5. Represente a expressão (A*B^C+(2/A)*B-3*C/(2*A)) através de uma árvore, forma infixa.

6. Sejam ABCDEFGHI e DACBFGFIJ as seqüências infixa e prefixa de uma árvore de Busca Binária T, reconstrua graficamente a árvore T.

7. Construa uma ABB com o conjunto de números:{100,23,13,15,66,43,134,87,166,786,214,221,7,1880,200}

8. Faça o balanceamento da árvore 7 e apresente-a graficamente

Apostila de Algoritmos e Estrutura de Dados III 17

Page 18: Apostila Alg 3

Trabalho (parte 2):

As árvores são ótimas para implementar indexação, pois aceleram o processo de pesquisa de informações. Acesso em disco é sempre mais lento do que acesso em memória. Exclusões e inclusões mantendo os dados de um arquivo ordenado, por exemplo, implicariam na movimentação de uma grande quantidade de dados, sendo um processo excessivamente lento para ser utilizado em aplicações práticas.

A utilização de índices tem por objetivos: rapidez no processo de inclusão de novos registros rapidez no processo de pesquisa do arquivo rapidez no processo de atualização do arquivo

A inclusão em um arquivo de dados será sempre efetuada no final do arquivo, sendo então um procedimento fácil de implementar. Para garantir então rapidez de pesquisa, uma estrutura de indexação reduziria em muito os acessos a disco.

O índice seria um mapeamento que associa cada chave da árvore de índice a uma chave no arquivo de dados.

A atualização utilizando índices também é muito eficiente. Se um registro deve ser alterado, podemos encontrá-lo rapidamente. Se um registro deve ser removido basta remover a sua referência no índice (remoção lógica). A remoção definitiva do registro do arquivo pode ser feita em um processo de reorganização posterior, realizado esporadicamente.

Uma base de dados indexada é então composta por duas partes básicas: um arquivo de dados e uma estrutura de índice associada. Segue exemplo de uma estrutura de índice utilizando ABP, onde a chave de índice é o campo nome:

A estrutura do arquivo de dados é apresentada a seguir:

Registro E# Código Nome Telefone Endereço Cidade UF

0 12 Elisa 9999-2222 R. Goiás, 14 Erechim RS

1 4 Caio 520-9091 Av. P. P. de Souza, 124, Ap 301 Erechim RS ... ... ... ... ... ... ... ...

11 26 Gilda 321-3675 Erechim RS

A estrutura da árvore de indexação seria:

struct tiponodo{

struct tiponodo *esq;char nome[10];int pos;struct tiponodo *dir;

};typedef struct tiponodo *NODO;

A Implementar:

Implemente o programa agenda que é a manipulação de um arquivo de dados com a estrutura apresentada acima e índice por nome. O programa deve apresentar as seguintes opções:

• Incluir• Excluir• Consultar dados pelo Nome da pessoa• Listar em ordem Alfabética

Apostila de Algoritmos e Estrutura de Dados III 18

Elisa

T (ponteiro)

Gilda 11Caio 1

Ana Denis Fábio 12 Valdo 10

Bia João

Hélio

14

19

8

9 4

0

Page 19: Apostila Alg 3

Árvores de busca Multidirecionais

Uma ABP não é viável para implementação em disco, pois a busca de cada nodo requer um acesso a disco. Se agruparmos várias chaves em cada nodo, reduzimos o tempo de pesquisa. Esse agrupamento reduz a altura da árvore. Ex:

ABP

A implementação com agrupamento de chaves é chamada Árvore de busca Multidirecional ou Árvore de busca M-vias, e é uma árvore de grau M.

Em uma árvore de ORDEM M, cada nodo tem M ou menos subárvores.--> Um nodo com 2 chaves = 3 subárvores (Ordem 3)--> Um nodo com 3 chaves = 4 subárvores (Ordem 4)--> Um nodo com 4 chaves = 5 subárvores (Ordem 5)

Alguns aspectos das árvores multidirecionais abaixo:- Na árvore (a), os nodos A,D,E,G são completos e B, C, F e H são incompletos.- Um nodo com pelo menos uma subárvore vazia é denominado semifolha: (a): b até h. (b): B até G e I até R.- A árvore (a) e a árvore (b) não são balanceadas, porque diferentemente das ABPs, que podem ter folhas em níveis diferentes, uma Árvore multidirecional só será balanceada se todas as folhas se encontrarem no mesmo nível.

Apostila de Algoritmos e Estrutura de Dados III 19

500

720480180

800

980

150 300 455 499 850798515 995

112

110245

190 987

670

536

420

Ordem:Balanceada(s/n):

Ordem:Balanceada(s/n):

Page 20: Apostila Alg 3

Inserção em uma árvore multidirecional balanceada de ordem 3

Árvores B

Devido ao pouco desperdício, as árvores B (ou variações dela) são utilizadas com muita freqüência em sistemas de arquivos reais. Uma árvore B de ordem M é uma árvore de busca multidirecional balanceada que:- cada nodo tem no máximo M-1 chaves e a raiz tem no mínimo 1 chave - cada nodo não-raiz tem no mínimo (M-1)/2 chaves ( ordem 11 tem no mínimo 5 chaves /nodo; ordem 4: mínimo 1)- todas as folhas devem estar no mesmo nível

* Alguns autores consideram que uma árvore de ordem 4 possa ter 4 chaves e 5 filhos em cada nodo. Outros ainda consideram a ordem como n/2, ou seja, uma árvore de ordem 2 possa ter 4 chaves por nodo e 5 filhos.

-> Como em Tenembaum 95, de forma coerente, consideraremos Ordem como sendo o Grau máximo da árvore (número máximo de filhos por nodo).

Nível Mínimo Máximo

Nodos Chave Nodos Chaves

0

1

2...

1

2

2* (q+1)

1

2*q

2*q*(q+1)

1

m

m2

m-1

(m-1)*m

(m-1)*m2

sendo que q = (m-1)/2

Apostila de Algoritmos e Estrutura de Dados III 20

Ordem:Balanceada(s/n):

Page 21: Apostila Alg 3

Por exemplo, considerando uma árvore de Ordem 4, qual o número mínimo e máximo de nodos que ela pode ter nos níveis 0, 1 e 2 respectivamente? E no total? Mostre graficamente.

Comparando com uma árvore binária qualquer de 3 níveis (0-2), qual é o número máximo de chaves que ela poderia ter?

Inserção em uma árvore B

A seguir, são mostrados alguns passos para inserção em árvores B. Deve-se observar a divisão que ocorre em um nodo quando o número de chaves ultrapassa o número de nodos permitidos. Se a ordem da árvore for par, as n-1 chaves, excluindo a chave do meio devem ser divididas em 2 grupos de tamanhos diferentes: um grupo com tamanho n/2 e outro grupo com tamanho (n-1)/2.O segundo grupo é sempre de tamanho (n-1)/2 independentemente do fato de n ser ímpar ou par, pois quando n é impar, (n-1)/2 será igual a n/2. Como exemplo, considere duas árvores, de ordem 4 e 5 respectivamente.

Veja abaixo, na figura da esquerda, o procedimento de inserção em uma árvore de ordem 5.

Inserção em uma árvore B de ordem 3:

Tente inserir o nodo 10:

Cada nodo pode ter no máximo 3 filhos

Apostila de Algoritmos e Estrutura de Dados III 21

3 6

1 2 4 5 7 8

Page 22: Apostila Alg 3

Compressão de Dados 1. Para que comprimir?Ao contrário do que possa parecer, comprimir não é somente reduzir o tamanho de um arquivo: além da compressão utilizada em arquivos para reduzir o espaço físico utilizado por eles, há também a compressão que é utilizada para melhorar a performance em uma trasmissão de dados.

A redução do espaço físico é mais comumente utilizada em bancos de dados que, incorporando a compressão no projeto de seus registros, permite um significativo ganho em termos de ocupação em disco e velocidade de acesso.Exemplos (bancos de dados): Dbase, Interbase, Paradox, PostgressExemplos (arquivos): Openoffice, Word

Como foi mencionado anteriormente, a utilização mais conhecida da compressão de dados é a redução do espaço ocupado por arquivos. De fato, esta é aplicação mais comum e mais difundida comercialmente. Afinal, dado um dispositivo restrito de armazenamento que é um disquete, e um grande depositório de arquivos que é o disco rígido, é evidente a necessidade de muitas vezes realizar-se a redução do tamanho de arquivos para transportá-los ou simplesmente armazená-los.

A compressão também é utilizada para agilizar a transmissão de dados alterando a taxa de transmissão. Se possuímos um modem que opera a 9600 bps (bits por segundo), é possível que ele transmita como se estivesse a 14400 bps? Sim! Basta que ele permita a compressão de dados transmitidos, ampliando sua capacidade de transferência de informação. Na verdade, o modem continuará transmitindo a uma taxa de transmissão de 9600 bps, mas a taxa de transferência de informação estará ampliada para 14400 bps. A compressão de dados permite, portanto, o aumento na velocidade de transmissão de dados.

2. Tipos de Compressão

1.1. Compressão Lógica

A compressão lógica refere-se ao projeto de representação otimizada de dados. Um exemplo clássico é o projeto de um banco de dados utilizando seqüências de bits para a representação de campos de dados. No lugar de seqüências de caracteres ou inteiros, utiliza-se bits, reduzindo significativamente o espaço de utilização do banco de dados.

Este tipo de compressão é possível de ser efetivada em campos projetados para representar dados constantes, como datas, códigos e quaisquer outros campos formados por números. A característica lógica da compressão encontra-se no fato dos dados já serem comprimidos no momento do armazenamento, não ocorrendo sua transformação de dados estendidos para comprimidos. Ex.: Representação de “01 de Abril de 2003” : 15 bytes

“01 Abr 2002” : 9 bytes“01042002” : 8 bytes

Tem como reduzir ainda mais?????

A razão da compressão é dada por: tamanhodos dadosoriginaistamanhoda cadeiade dados comprimido

2.1. Compressão Física

A compressão física é aquela realizada sobre dados existentes, a partir dos quais é verificada a repetição de caracteres para efetivar a redução do número de elementos de dados. Existem dois tipos de técnicas para sinalizar a ocorrência de caracteres repetidos:

1. um deles indica o caracter (ou conjunto de caracteres) repetido através da substituição por um caracter especial; 2. outras técnicas indicam a freqüência de repetição de caracteres e representam isto através de seqüências de bits.

Apostila de Algoritmos e Estrutura de Dados III 22

Page 23: Apostila Alg 3

3. Compressão Orientada a Caracter As técnicas de compressão orientadas a caracter não são as mais eficientes ou as mais sofisticadas. Pelo contrário, em geral elas são utilizadas num primeiro nível de compressão multinível, onde os demais níveis podem ser técnicas estatísticas de compressão.

Aqui serão analisados 8 técnicas de compressão orientada a caracter, de modo a dar uma noção das possíveis aplicações deste tipo de compressão.

a) Supressão de Caracteres Nulos ou brancos

Nesta técnica, temos como objetivo comprimir apenas os caracteres nulos ou brancos. Para a compressão, utiliza-se a seguinte seleção de caracteres indicadores: Ce N onde Ce é um caracter especial (poderia ser o $, por exemplo) e N é o número (em binário) de caracteres brancos repetidos seqüencialmente.

Então, por exemplo, uma seqüência do tipo: ... vazio. Exatamente o que...

pode ser comprimida como sendo: ... vazio.$8Exatamente o que...

o que demonstra uma redução de 6 bytes no trecho de texto apresentado.

Esta técnica pode ser extendida aos números nulos (zero). Ex.: A seqüência “000053João Silva 890000” ficaria:

“#453João Silva$2789#4”

Outras formas de indicação de compressão podem ser utilizadas, com visto na seção anterior. Como trata-se de uma compressão de brancos, não há a necessidade de explicitação do caracter comprimido.

O algoritmo para a técnica apresentada é muito simples: 1. inicializa-se um contador para o cálculo do número de repetições de brancos;2. lê-se o caracter do arquivo a comprimir;3. verifica-se se o caracter é um branco;4. se for, incrementa-se o contador e verifica-se se o número de caracteres ultrapassou a 255 (limite binário); se

verdadeiro, colocam-se os caracteres indicadores no arquivo comprimido e volta-se ao passo 1, caso contrário, volta-se ao passo 2;

5. se não for, verifica-se se o contador é maior que 2, valendo a pena comprimir; se é, colocam-se os caracteres indicadores no arquivo comprimido, e volta-se ao passo 1, se não é, copiam-se os caracteres lidos no arquivo comprimido e volta-se ao passo 1.

O fluxograma para o algoritmo apresentado é mostrado a seguir:

No caso de haver 350 caracteres brancos seguidos, utilizaria-se $255$95

Apostila de Algoritmos e Estrutura de Dados III 23

Page 24: Apostila Alg 3

b) Comprimento de Fileira

Também chamada de Run-length, esta técnica utiliza sempre uma combinação de 3 bytes para representar uma compactação. o formato é Ce X N onde Ce é o caracter especial, X é o caracter repetido e N é o número (binário) de repetições.

No caso de um caracter ser repetido mais do que 255 vezes, basta repetir a combinação Ce X N, onde n será o número de repetições que ainda faltam para comprimir corretamente a cadeia original.

Funcionamento do algoritmo:1. inicializa-se um contador de caracteres, destinado ao controle de cada caracter, buscando a verificação de existência de

repetição;2. inicializa-se um contador de repetições do caracter procurado;faz-se a leitura do caracter no arquivo a comprimir;3. incrementa-se o contador de caracteres;4. se o contador de caracteres for igual a 1, o caracter é armazenado e volta-se ao passo 3 para verificação de repetição;5. verifica-se se o caracter armazenado é o que procuramos; se for, incrementa-se o contador de repetições;6. verifica-se se o contador de repetição é maior ou igual a 4; se for menor, gravam-se os caracteres lidos no arquivo

comprimido e volta-se ao passo 1;7. realiza-se a gravação dos caracteres indicadores no arquivo comprimido e volta-se ao passo 1.

c) Mapeamento de Bit

Quando é sabida a existência de múltiplas ocorrências não consecutivas de determinado caracter no arquivo a comprimir, utiliza-se a compressão por mapeamento de bit. Esta técnica utiliza-se de um byte no arquivo comprimido para indicar, através dos bits, a ocorrência do caractere repetido.

Desta forma, caso desejarmos comprimir todos os caracteres a de um texto, devemos indicá-lo no mapa de bits. Cada mapa descreve 8 bytes, um por bit do arquivo manipulado. Portanto, para letra encontrada a cada trecho de 8 bytes, será assinalada no mapa de bits. Por exemplo: ... abacate ... será descrito como: ... Mbbcte... onde Mb é o mapa de bits correspondente à compressão do caracter a . Este mapa é composto, para o exemplo, dos bits:

onde o primeiro zero indica a presença de um caracter a na primeira posição, valor um em seguida indica um caracter diferente, e assim por diante, até completar o mapa. Convenciona-se, portanto, que o bit 0 indica a presença do caracter a comprimir e o bit 1 a sua ausência.

Apostila de Algoritmos e Estrutura de Dados III 24

contcar = 0

contrep = 1

buffer = ''

Lê caracter

coloca caracter no buffercontcar = 1?

contcar = contcar +1

caracter = procurado?

contrep = contrep + 1

contrep>= 4 saida = saida + '@' + procurado + chr(contrep)

procurado = caracter

sim

não

sim

não

sim

não

saida = saida + '@' + buffer*contrep

Ao terminar a leitura, joga-se o

que restou no buffer para a saída

buffer = caracter

contcar = contrep = 1

Page 25: Apostila Alg 3

O algoritmo para esta técnica necessita do controle do mapa de bits:1. inicializa-se o mapa de bits colocando todos os bits em zero;2. inicializa-se o contador;3. realiza-se a leitura do caracter no arquivo a comprimir;4. compara-se o caracter lido com o caracter procurado; se forem o mesmo, então vai-se para o passo 6;5. troca-se para 1 o bit da posição atual;6. incrementa-se o contador;7. verifica-se se o contador chegou a 8; se verdadeiro, então grava-se o mapa de bits e os caracteres diferentes do

comprimido, e volta-se para o passo 1; senão, volta-se ao passo 3.

A seguir é apresentado o fluxograma que descreve o algoritmo:

d) Compactação de Meio Byte

Este tipo de compactação é utilizado quando encontramos uma seqüência de bits em comum nos caracteres de determinado arquivo. Um tipo de repetição de bits de grande ocorrência é a que acontece em caracteres numéricos. Por exemplo, se observarmos o conjunto binário da tabela ASCII para os caracteres numéricos, teremos:

ou seja, se isolarmos os primeiros 4 bits de cada byte de caracter numérico, veremos que há uma constância de valores. Estes valores constantes são um exemplo de objetos de compactação de meio byte. A compactação de meio byte propriamente dita consiste na seguinte seqüência de caracteres indicadores:

onde Ce é o caracter especial, N é o número (binário) de caracteres comprimidos e Cn é a metade do caracter comprimido. Na seqüência de caracteres indicadores apresentada são apresentados 5 caracteres porque este é o mínimo para que a compressão seja válida, uma vez que até 4 caracteres ela necessita um número de byte igual ou maior. Observe também que o número de repetições está ocupando apenas meio byte. Isso significa que ele pode indicar apenas até 16 caracteres, ou seja, este formato permite a compactação de uma seqüência de, no máximo, 16 caracteres.

Mas então surge a pergunta: não há como estender este formato para permitir a compactação de uma seqüência maior de caracteres? A resposta é sim, existe a compactação de byte inteiro , onde o número N ocupa 1 byte, ao invés de meio.

Apostila de Algoritmos e Estrutura de Dados III 25

Page 26: Apostila Alg 3

Neste formato estendido, o restante dos caracteres indicadores permanecem os mesmos, sendo alterado apenas a representação do número de caracteres. Com a representação de byte inteiro, a capacidade de compressão aumenta para 256 caracteres, o que permite um ganho expressivo em se tratando de longas seqüências de caracteres.

O algoritmo para implementação da compactação de meio byte é o seguinte:1. inicializa-se um contador de caracteres;2. procede-se a leitura do caracter;3. verifica-se se o caracter possui a seqüência de bits a comprimir;4. caso esteja dentro do grupo a comprimir, incrementa-se o contador e volta-se ao passo 2;5. verifica-se se o contador é maior que 4;6. se não for, gravam-se os caracteres lidos no arquivo comprimido e volta-se ao passo 1, reiniciando o processo;7. colocam-se os caracteres indicadores no arquivo comprimido e volta-se ao passo 1.

O fluxograma para a representação do algoritmo é apresentado a seguir.

e) Codificação Diatômica

Esta técnica de compressão permite a representação de um par de caracteres em apenas um caracter especial. Normalmente utilizam-se tabelas com pares de caracteres e sua freqüência de ocorrência em determinado tipo de arquivo. Obviamente procura-se substituir os caracteres de maior freqüência, associando a cada dupla um caracter especial.

Em texto da língua portuguesa, por exemplo, duplas de ocorrência freqüente são a letra a acentuada com til seguido da letra o (ão) e a letra e com acento agudo seguido de um espaço em branco (é ). A cada uma dessas seqüência deve-se atribuir um caracter especial para nos permitir a compactação através da codificação diatômica. Obviamente, estes são apenas dois exemplos de duplas de caracteres, numa tabela normal para compactação utilizam-se mais de 20 duplas para que seja obtida uma compressão razoável.

Para a implementação da codificação diatômica segue-se o seguinte algoritmo:1. lê-se um par de caracteres;2. verifica-se sua existência na tabela de pares;3. se exitente, coloca-se o caracter especial correspondente no arquivo comprimido e volta-se ao passo 1;4. coloca-se apenas o primeiro caracter no arquivo comprimido;5. desloca-se o segundo caracter para a primeira posição;6. lê-se o próximo caracter e volta-se ao passo 2.

O fluxograma correspondente é o seguinte:

Apostila de Algoritmos e Estrutura de Dados III 26

Page 27: Apostila Alg 3

f) Substituição de Padrões

A substituição de padrões é semelhante à codificação diatômica, pois também ocorre a substituição de um conjunto de caracteres por um caracter especial. PALAVRA => Ce

A utilização mais comum para este tipo de compressão é a de arquivos de programas de linguagens de programação. Uma vez que as linguagens contém diversas palavras que se repetem freqüentemente em programas, utiliza-se esta característica para a sua compressão.

Uma variante da substituição de padrões para permitir a codificação de um maior número de palavras é a utilização de dois caracteres para indicação da ocorrência de determinada palavra: C N

onde C é um caracter escolhido para indicar a compressão e N é o número (binário) da palavra a substituir. Isso permite a codificação de até 256 palavras reservadas, o que anteriormente era limitado ao número de caracteres especiais que poderíamos utilizar.

Por exemplo, as palavras reservadas begin e end da linguagem Pascal poderiam ser, por exemplo, substituídas pelos códigos $1 e $2 . As demais palavras reservadas da linguagem também poderiam ser codificadas desta maneira, permitindo uma compressão considerável de um arquivo de programa.

Como esta técnica assemelha-se muito à da codificação diatômica, deve tomar como base para a programação o algoritmo e o fluxograma apresentados para aquela técnica. Desta forma não serão apresentados formas distintas de programação.

4. Compressão Estatística A idéia da compressão estatística é realizar uma representação otimizada de caracteres ou grupos de caracteres. Caracteres de maior freqüência de utilização são representados por códigos binários pequenos, e os de menor freqüência são representados por códigos proporcionalmente maiores.

Neste tipo de compressão portanto, não necessitamos saber qual caracter vai ser comprimido, mas é necessário, porém, ter o conhecimento da probabilidade de ocorrência de todos os caracteres sujeitos à compressão. Caso não seja possível a tabulação de todos os caracteres sujeitos à compressão, utiliza-se uma técnica adequada para levantamento estatístico dos dados a comprimir, formando tabelas de probabilidades.

Para sabermos como foi concebido este tipo de compressão, veremos na seção seguinte a Teoria da Harmonia, a partir da qual teremos uma noção de como se processa a compressão estatística.

4.1. Teoria da Harmonia

Esta Teoria baseia-se no princípio físico da Entropia. A Entropia é a propriedade de distribuição de energia entre os átomos, tendendo ao equilíbrio. Sempre que um sistema físico possui mais ou menos quantidade de energia que outro sistema físico em contato direto, há troca de energia entre ambos até que atinjam a entropia, ou seja, o equilíbrio da quantidade de energia existente nos sistemas. Ao atingir o estado de equilíbrio, sabe-se que estes sistemas estão utilizando o mínimo de energia possível para sua manutenção, e assim se manterão até que outro sistema interaja sobre eles.

Aplicada à informação, a Teoria da Entropia permite a concepção de uma teoria da Harmonia, ou seja, um ponto de equilíbrio onde a informação pode ser representada por uma quantidade mínima de símbolos. Para chegarmos a esta representação ideal, basta que tenhamos a quantidade de símbolos utilizada e a probabilidade de ocorrência deles. Com base nisso, é possível calcular a quantidade média de bits por intervalo de símbolo:

havendo n símbolos, cada qual com uma probabilidade p . A representação de quantidades em binário é dada pela base 2 do logaritmo. Foi utilizado um valor n log n por sua proporcionalidade entre quantidade de informação e tempo (para mais detalhes, veja bibliografia indicada no capítulo A). Por outro lado, a fórmula apresentada é semelhante à utilizada para verificação da energia de um sistema físico, utilizada na Teoria da Entropia.

Na prática, a fórmula anteriormente apresentada permite-nos verificar se é possível a otimização da quantidade de bits utilizados para representação de determinado conjunto de símbolos. Duas representações podem ser comparadas para a verificação de qual ocupa menos bits em média. Isso nos permite concluir que é possível a criação de um método de compactação construído a partir da probabilidade de ocorrência de símbolos.

Apostila de Algoritmos e Estrutura de Dados III 27

Page 28: Apostila Alg 3

De fato, existem técnicas que utilizam a análise da probabilidade para compactação de dados. Estas são chamadas de estatísticas e serão analisadas nas seções seguintes.

4.2. Codificação Huffman

Esta técnica de compressão permite a representação em binário de caracteres a partir de sua probabilidade de ocorrência. Esta representação é gerada por um sistema de decodificação em árvore binária, o que impede a ambigüidade na análise do código.

A ambigüidade, neste caso, refere-se a uma decodificação que permite a confusão com outros caracteres. Por exemplo, determinado caracter C1 tem o código binário 01 e outro caracter C2 tem o código 0100, isto implica que, ao verificarmos a seqüência binária para C2 poderemos estar interpretando como C1, ao serem lidos apenas os bits 01. Por isso, a codificação Huffman utiliza o projeto em árvore binária para projeto dos bits que representam os caracteres, de forma que permitam uma decodificação única para cada caracter.

A codificação Huffman necessita de que cada caracter tenha um valor de probabilidade de ocorrência. A partir dos caracteres de menor valor, começa a construção da árvore binária.

Suponha que após a análise estatística de um arquivo texto a ser compactado verificou-se que 91% dos dados contidos no arquivo sejam os caracteres IHFBDEGCA. Um configuração possível para a árvore de huffman então seria:

A probabilidade do ramo é a soma das probabilidades das folhas (GC) 13% = 6% + 7% . Os valores binários serão membros do código formado. Para a codificação dos próximos caracteres, basta continuarmos a construção da árvore.

A probabilidade final da árvore é sempre 1,0, uma vez que necessariamente deve-se atingir 100% das ocorrências de caracteres, permitindo uma codificação total. Uma vez terminada a árvore, basta a formalização da codificação, que é feita com a leitura dos valores binários, da raiz para as folhas. Os valores binários lidos serão o código do percurso da raiz até a folha correspondente ao caracter que se deseja o código. A tabela de códigos fica a seguinte:

Tabela dos símbolos e freqüência:

Símbolo Freqüência Código

A 15 111B 6 101C 7 1101D 12 011E 25 10F 4 01001G 6 1100H 1 01000T 15 00

Apostila de Algoritmos e Estrutura de Dados III 28

HFB | DI, 15%

D, 12%

GC | A

A, 15%

91 %

H | F C, 7%

28 %

53 %38 %

13 %HF | B

H, 1% F, 4%

B, 6%

I | HFBD

IHFBD | EGCA

E | GCA

E, 25%

G | C

G, 6%

[Tene 95]

Page 29: Apostila Alg 3

Desta forma, estão codificados os caracteres através da técnica de Huffman. Observe que o caracter de maior freqüência possui o menor valor. Isso é exatamente o objetivo da compressão estatística, uma vez que permite a substituição do caracter de maior ocorrência por apenas um bit. Assim acontece com os demais caracteres, em ordem crescente do número de bits, conforme a prioridade.

Cabe salientar, por fim, que a codificação Huffman manteve a propriedade de permitir a decodificação direta, não permitindo que os bits da codificação de um caracter confundisse com a de outro.

4.3. Codificação Shannon-Fano

A forma desta técnica tem muitas semelhanças com a de Huffman. Necessita-se de uma tabela com a probabilidade de ocorrência de cada caracter, e de um procedimento para a codificação em binário. Por outro lado, o procedimento para a codificação, diferentemente de Huffman, baseia-se na divisão de conjuntos de probabilidades para a obtenção do código binário.

Para a codificação, devemos ter os caracteres com suas probabilidades de ocorrência:

O passo seguinte é ordenar colocando os caracteres de maior probabilidade no topo da tabela, até o menor, na base:

Uma vez feito isso, divide-se a tabela em dois grupos cuja soma de probabilidades seja igual ou semelhante. No caso da tabela acima, serão obtidos dois grupos, um composto pelo caracter C1 e outro pelos demais. O primeiro grupo recebe como primeiro valor de código o binário 0 e o segundo recebe 1:

Como para o primeiro grupo não há ambigüidade em termos apenas um bit, vamos resolver o problema do segundo grupo. Para isso, repetimos o procedimento anterior, dividindo em dois subgrupos de probabilidades equivalentes. O caracter C2 forma o primeiro subgrupo e os demais formam o segundo. Mais uma vez vamos colocar 0 para distinguir o primeiro e 1 para o segundo:

Finalmente, para resolução da última duplicidade, repete-se o processo, inserindo o binário 0 na seqüência do código de C1 e 1 para C3:

Apostila de Algoritmos e Estrutura de Dados III 29

Page 30: Apostila Alg 3

Teoria dos Grafos

A teoria dos grafos estuda problemas computacionais que envolvem objetos conhecidos como grafos Os problemas tornaram-se célebres porque ocorrem em diversas áreas da computação, da engenharia, e em muitas aplicações industriais.

Parte do material abaixo é um curso de algoritmos para grafos baseado no livro Algorithms in C: Graph Algorithms de R. Sedgewick.

Conceitos básicos

Um digrafo (digraph = directed graph) é composto por um conjunto de nodos e um conjunto de arestas, tal que:- vértice (nó, ponto): representa uma entidade, como “uma pessoa”, “uma fruta” ou “um pedaço de terra”.- Arestas ou arcos (linha): é a relação que liga 2 nodos, tal como “irmão” que liga 2 “pessoas”.

Notação: G (n,a): onde G é o nome do grafo, n é o número de nodos e n é o número de arestas.

Exemplo: G(4,7) é um grafo com 4 nodos e 7 arestas:

Uma boa maneira de especificar um digrafo é exibir seu conjunto de arcos. O Conjunto de arcos do digrafo abaixo é:

a-a, a-b, a-d, a-c, c-d, d-f, f-c, f-g. Nota-se que a-b indica um arco do sentido de a até b.

Digrafo simétrico

O grafo é simétrico quando se cada um dos arcos é anti-paralelo a outro arco. Por exemplo, se existir o arco 0-5, deve existir o arco 5-0.

Grafo

São os tipos especiais de digrafos que são não orientado. Para simplificação, utilizaremos a partir de agora a notação grafo para ambos (grafos e digrafos).

Grau de um vértice

O grau de entrada e de saída de um vértice em um grafo é determinado pela quantidade de arestas que incidem sobre ele e a quantidade de arcos que saem dele, respectivamente.

Exemplo: os nodos são cidades e os arcos contém a distância entre duas cidades:

Apostila de Algoritmos e Estrutura de Dados III 30

4

3

2

1 Grau = 3

a

b

d

c

g

f

A

B

D

C

G

F

8

15

3126

3

4

Page 31: Apostila Alg 3

Representações computacionais de grafos

Considerando o grafo abaixo:

As seguintes representações são possíveis:a) Matriz de adjacência:

1 2 3 4 5 6

1

2

3

4

5

6EstruturaUm digrafo será representado por uma struct graph que contém o número de vértices, o número de arcos e a matriz de adjacência do digrafo.

/* A estrutura digraph representa um digrafo. O campo adj é um ponteiro para a matriz de adjacência do digrafo. O campo V contém o número de vértices e o campo A contém o número de arcos do digrafo.*/ struct graph { int V; int A; int **adj; };

/* Um objeto do tipo Graph contém o endereço de um graph.*/

typedef struct graph *Graph;

/* Pode-se utilizar o nome digraph ou graph conforme a preferência*/

#define digraph graph;

#define Digraph Graph;

b) Matriz de incidência: c) Lista de adjacência

a b c d e f g h

1 -1 1 -1 -1 0 0 0 0

2

3

4

5

6

Apostila de Algoritmos e Estrutura de Dados III 31

fd

c1 h

e

2

4

3

6

5

g

ab

A[i,j]=

Page 32: Apostila Alg 3

Exemplo de Aplicação de grafos

Kaliningrad, antigamente Königsberg é uma cidade do oeste da Rússia, junto ao Rio Pregel. A capital de Kaliningrad, Oblast, é o um grande centro industrial e comercial, ligado por um canal com Baltiysk, um porto no Mar Báltico. O matemático Leonardo euler (1707-1786) deu início ao estudo de grafos ao solucionar o problema das pontes de Konisberg, ao provar que não era possível atravessar todas as pontes usando um único caminho.

O problema das "Pontes de Königsberg ".

"O rio Pregel tem duas ilhas. Estas estão unidas por uma ponte. Uma ilha tem uma ponte que a une a ambas as margens; a outra tem duas pontes para cada margem. Podem os cidadãos de Conisberga atravessar todas as sete pontes num só passeio contínuo? " Ele fundou sem saber a base da teoria dos grafos

Se procuramos um caminho contínuo, sem passar duas vezes pela mesma ponte, a resposta é negativa. Vejamos uma justificação simples: vamos substituir o desenho por um correspondente diagrama, ou seja, um modelo do nosso problema; fazendo corresponder a cada massa de terra um ponto e a cada ponte uma linha, ligando de seguida os pontos apropriados.

A chave do problema reside no fato de cada ponto ou é um ponto de partida, de chegada ou de passagem. Se um ponto tem um número ímpar de caminhos terá de ser um ponto de partida ou de

chegada. Para fazermos um caminho contínuo temos de passar por todos os pontos, pelo que os que não são de partida ou de chegada têm de possuir um número par de caminhos, pois se chegamos temos de partir.

No problema colocado temos quatro pontos com um número ímpar de caminhos, quer isto dizer, que temos três pontos que não podem ser de passagem. Logo não é possível efetuar o caminho contínuo proposto.

De um modo geral e intuitivamente, concluímos que:

– os habitantes só podiam efectuar um caminho contínuo se todos os pontos tivessem um número par de caminhos;

– não tendo todos um número par de caminhos, apenas dois podiam ter um número ímpar, um seria o ponto de partida o outro o de chegada.

Grafos EulerianosO estudo de Leunard Euler deu origem à trajetória euleriana:– um caminho completo, que passe por todas as arestas de um grafo, sem retraçar nenhuma aresta. Exemplos

Apostila de Algoritmos e Estrutura de Dados III 32

Resposta:

a b

dc

ae f

b

c d

Page 33: Apostila Alg 3

Grafos HamiltonianoO matemático irlandês Willian Powan Hamilton, criou uma trajetória um pouco diferente da trajetória euleriana. Deve-se aqui passar por todos os vértices do grafo sem repetir nenhum vértice, começando e terminando no mesmo vértice. Um grafo euleriano é apresentado abaixo:

Um clássico problema hamiltoniano é o problema do caixeiro-viajante. Nesse problema, um caixeiro viajante deve visitar várias cidades e retornar ao ponto de partida. Como podem haver vários ciclos Hamiltonianos, o objetivo aí pode ser encontrar o ciclo de menor peso total. Esse é um algoritmo denominado NP Completo. Não existe algoritmo eficiente para isso. Para 9 cidades, por exemplo, seriam necessárias 8! combinações (o que nos daria 40320 caminhos).Veja o exemplo abaixo com 5 cidades:

Exercícios. Indique para os grafos abaixo se existe pelo menos 1 caminho Euleriano e 1 hamiltoniano para cada um:

Algoritmos para grafos + conhecidos e a sua aplicação

a) DijkstraUm dos algoritmos mais conhecidos de grafos é o dijkstra. É o algoritmo mais eficiente quando se deseja calcular o melhor caminho para seguir em um grafo ponderado. O funcionamento é o seguinte. Escolhe-se a origem (um vértice qualquer do grafo que representaria no caso uma cidade) e a partir daí, o algoritmo retorna a menor distância desta origem para cada um de todos os outros vértices existentes. Ou seja, se a cidade origem é o nodo 0, por exemplo, o algoritmo monta uma árvore mínima com o menor caminho possível para cada um dos outros nodos. Vejamos o exemplo a seguir, considerando o mapa abaixo:6 110 1 410 5 293 0 453 5 385 1 295 4 214 3 361 4 324 2 322 3 501 2 5110 5

Apostila de Algoritmos e Estrutura de Dados III 33

a b

dc

d

ae f

b

c d

ag f

b

c d

e

a b

dc

d

dcd

a

b

dc d

b

aE______H______

E______H______

E______H______

E______H______

E______H______

Obs.: A explicação do Dikstra será feita em sala de aula, no quadro-negro, apresentando os três vetores: cst, from e parnt

Page 34: Apostila Alg 3

Aplicação do Dijkstra - Maratona de Programação da SBC – ACM ICPC –2006 página 6

Problema C

Países em GuerraNome do arquivo fonte: paises.c, paises.cpp, paises.java

No ano 2050, após diversas tentativas da ONU de manter a paz no mundo, explode a terceira guerra mundial. Segredos industriais, comerciais e militares obrigaram todos os países a utilizar serviços de espionagem extremamente sofisticados, de forma que em cada cidade do mundo há ao menos um espião de cada país. Esses espiões precisam se comunicar com outros espiões, com informantes e mesmo com as suas centrais durante as suas ações. Infelizmente não existe uma forma segura de um espião se comunicar em um período de guerra, então as mensagens são sempre enviadas em código para que somente o destinatário consiga ler a mensagem e entender o seu significado.

Os espiões utilizam o único serviço que funciona no período de guerra, os correios. Cada cidade possui uma agência postal onde as cartas são enviadas. As cartas podem ser enviadas diretamente ao seu destino ou a outras agências postais, até que a carta chegue à agência postal da cidade de destino, se isso for possível.

Uma agência postal na cidade A pode enviar diretamente uma carta impressa para a agência postal da cidade B se houver um acordo de envio de cartas, que determina o tempo, em horas, que uma carta leva para chegar da cidade A à cidade B (e não necessariamente o contrário). Se não houver um acordo entre as agências A e B, a agência A pode tentar enviar a carta a quantas agências for necessário para que a carta chegue ao seu destino, se isso for possível.

Algumas agências são interligadas por meios eletrônicos de comunicação, como satélites e fibras ópticas. Antes da guerra, essas ligações atingiam todas as agências, fazendo com que uma carta fosse enviada de forma instantânea, mas durante o período de hostilidades cada país passou a controlar a comunicação eletrônica e uma agência somente pode enviar uma carta à outra agência por meio eletrônico (ou seja, instantaneamente) se ela estiver no mesmo país. Duas agências, A e B, estão no mesmo país se houver uma forma de uma carta impressa enviada de uma das agências ser entregue na outra agência.

O serviço de espionagem do seu país conseguiu obter o conteúdo de todos os acordos de envios de mensagens existentes no mundo e deseja descobrir o tempo mínimo para se enviar uma carta entre diversos pares de cidades. Você seria capaz de ajudá-lo?

EntradaA entrada contém vários casos de teste. A primeira linha de cada caso de teste contém dois inteiros separados por um espaço, N (1≤ N≤ 500) e E (0≤ E≤ N2), indicando o número de cidades (numeradas de 1 a N) e de acordos de envio de mensagens, respectivamente. Seguem-se, então, E linhas, cada uma com três inteiros separados por espaços, X, Y e H (1≤ X, Y≤ N, 1≤ H≤ 1000), indicando que existe um acordo para enviar uma carta impressa da cidade X à cidade Y , e que tal carta será entregue em H horas.

Em seguida, haverá uma linha com um inteiro K (0≤ K≤ 100), o número de consultas. Finalmente, virão K linhas, cada uma representando uma consulta e contendo dois inteiros separados por um espaço, O e D (1≤ O,D≤ N). Você deve determinar o tempo mínimo para se enviar uma carta da cidade O à cidade D. O final da entrada é indicado por N = 0.

A entrada deve ser lida da entrada padrão.

SaídaPara cada caso de teste da entrada seu programa deve produzir K linhas na saída. A I-ésima linha deve conter um inteiro M, o tempo mínimo, em horas, para se enviar uma carta na I-ésima consulta. Se não houver meio de comunica¸cão entre as cidades da consulta, você deve imprimir ”Nao e possivel entregar a carta” (sem acentos).

Imprima uma linha em branco após cada caso de teste.

A saída deve ser escrita na saída padrão.

Apostila de Algoritmos e Estrutura de Dados III 34

Page 35: Apostila Alg 3

Exemplo de entrada4 51 2 52 1 103 4 84 3 72 3 651 21 31 44 34 13 31 2 102 3 13 2 131 33 13 20 0

Saída para o exemplo de entrada0660Nao e possivel entregar a carta

10Nao e possivel entregar a carta0

Resoluçãoa) InterpretaçãoComo muitas vezes já foi falado em sala de aula, a resolução de um problema já começa na sua correta interpretação. Pois bem, 90% de quem ler o enunciado fará uma interpretação errada do problema. Existe uma frase no meio do texto que diz o seguinte:

“Duas agências, A e B, estão no mesmo país se houver uma forma de uma carta impressa enviada de uma das agências ser entregue na outra agência.”

Pois bem. Desta forma, independendo da distância, se houver uma ligação de ida e volta entre duas cidades, isso significa que elas estão no mesmo país, e portanto, o custo de tempo de entrega sempre será 0 (zero), pois ela será entregue instantaneamente por meio eletrônico.

b) EntradaFeita a interpretação correta do exercício, o que não é uma tarefa muito fácil passamos para o próximo passo, que é analisar as entradas do problema

“A entrada contém vários casos de teste. A primeira linha de cada caso de teste contém dois inteiros separados por um espaço, N (1≤ N≤ 500) e E (0≤ E≤ N2), indicando o número de cidades (numeradas de 1 a N) e de acordos de envio de mensagens, respectivamente.”

Bem, neste caso temos cidades (N) e acordos (E) que no algoritmo chamaremos de cidades e acordos para facilitar a compreenção, sendo que os seguintes valores são possíveis para cada uma das variáveis:

int cidades ( 1 até 500), acordos ( 0 até 250.000)

Exemplo de entrada4 5

“Seguem-se, então, E linhas, cada uma com três inteiros separados por espaços, X, Y e H (1≤ X, Y≤ N, 1≤ H≤ 1000), indicando que existe um acordo para enviar uma carta impressa da cidade X à cidade Y , e que tal carta será entregue em H horas.”

Bem, então considerando o número de acordos (5 para o exemplo), para cada um deles teremos a origem (X), o destino (Y) e tempo (H).

int origem ( mínimo 1 ), destino (<=500), tempo (entre 1 e 1000)

Apostila de Algoritmos e Estrutura de Dados III 35

Page 36: Apostila Alg 3

Exemplo de entrada4 51 2 52 1 103 4 84 3 72 3 6

“Em seguida, haverá uma linha com um inteiro K (0≤ K≤ 100), o número de consultas. Finalmente, virão K linhas, cada uma representando uma consulta e contendo dois inteiros separados por um espaço, O e D (1≤ O,D≤ N). Você deve determinar o tempo mínimo para se enviar uma carta da cidade O à cidade D.”

int consultas (de 0 a 100) //Neste caso, pode-se utilizar a mesma variável origem e destino, para O e D respectivamente, uma vez que após a entrada anterior, estas variáveis estão disponíveis

Exemplo de entrada4 51 2 52 1 103 4 84 3 72 3 651 21 31 44 34 1

Programa

a) Declaração do GrafoA estrutura digraph representa um digrafo. O campo adj é um ponteiro para a matriz de adjacência do digrafo. O campo Vertices contém o número de Vértices e o campo A contém o número de arcos do digrafo. */

struct digraph { int V; int A; int adj[501][501];};

typedef struct digraph *Digraph;

>> Para facilitar, optou-se em não utilizar as posições 0 da matriz adj, por isso a definição com 501 espaços, ao invés de 500 b) Variáveis declaradas no programa principalint main(void){ Digraph D; D= new (struct digraph); int i,j,cidades,acordos,consultas,Origem,Destino,Tempo; Vertices parnt[501]; // Vetor utilizado no algoritmo de Dijkstra double cst[501]; // Vetor utilizado no algoritmo de Dijkstra // O vetor fr[501] “from” é declarado dentro da função Dijkstra

c) Após ler as cidades e acordos deve-se inicializar a estrutura digrap: cin >> cidades; cin >> acordos; while (cidades){ // Inicialização da estrutura digraph, colocando INFINITO // para todos as posições da matriz de adjacência (adj) for (i=0;i<=cidades;i++) for (j=0;j<=cidades;j++) D->adj[i][j]=INFINITO;

Apostila de Algoritmos e Estrutura de Dados III 36

3

25

6

8

1

47

10 Obs.: conforme explicado anteriormente, as cidades 1 e 2 estão em 1 mesmo país, o mesmo valendo para as cidades 3 e 4.

Saída.: neste caso, convorme o mapa acima, fica fácil definir as saídas:

0 (a cidade 1 e a cidade 2 estão no mesmo país)6 (existem 6 horas de tempo da cidade 2 p/ a 3)6 (existem 6 horas de tempo da cidade 2 p/ a 3) 0 (a cidade 4 e a cidade 3 estão no mesmo país)Nao e possivel entregar a carta

3

20

6

0

1

40

0

Algumas definições utilizadas:- #define maxV 501- #define maxCST 999999- #define INFINITO 999999- #define Vertices int- #define graph digraph- #define Graph Digraph

Page 37: Apostila Alg 3

d) Colocação das distâncias na matriz de adjacência // Colocação de todas as distâncias entre as cidades (tempos) // para todos as posições da matriz de adjacência (adj)

// IMPORTANTE: Se existir rota de ida e de volta entre 2 // agências, deve-se considerar que elas estão no mesmo país, e // a distância entre elas neste caso deve ser = ZERO (*%)

for (i=0; i<acordos; i++){ cin >> Origem; cin >> Destino; cin >> Tempo; if (D->adj[Destino][Origem]!=INFINITO) { D->adj[Origem][Destino]=0; //*% D->adj[Destino][Origem]=0; //*% } else D->adj[Origem][Destino]=Tempo; } D->V=cidades+1; // para vértices irem de 1 até 500, e não de 0 até 499 D->A=acordos;

e) Por fim, as consultas das horas que levaria para chegar a carta de uma cidade à outra, e a impressão de uma linha em branco no final cin >> consultas; for (int i=0; i< consultas; i++){ cin >> Origem; cin >> Destino; Dijkstra (D,Origem,parnt,cst,Destino); }

cout << endl;

f) algoritmo de dijkstra (adaptado no final para mostrar as mensagens que o programa exigevoid Dijkstra (Digraph G, Vertices s, Vertices parnt[], double cst[], Vertices Destino) { Vertices w, w0, fr[maxV]; for (w = 0; w < G->V; w++) { parnt[w] = -1; cst[w] = maxCST; } fr[s] = s; cst[s] = 0.0;

while (1) { double mincst = maxCST; for (w = 0; w < G->V; w++) { if (parnt[w] == -1 && mincst > cst[w]) { mincst = cst[w0=w]; } } if (mincst == maxCST) break; parnt[w0] = fr[w0];

for (w = 0; w < G->V; w++) if (cst[w] > cst[w0] + G->adj[w0][w]) { cst[w] = cst[w0] + G->adj[w0][w]; fr[w] = w0; } }

// Adaptação para este problema em específico if (cst[Destino] < INFINITO) cout << cst[Destino]; else cout << "Nao e possivel entregar a carta" ; cout << endl;}

Exercício: implemente o programa acima e com a entrada real de teste (paises.in), confira quanto tempo irá demorar para executar todos os casos de teste.

Apostila de Algoritmos e Estrutura de Dados III 37

Page 38: Apostila Alg 3

b) Floyd

Igualmente ao dijkstra, esse algoritmo faz uma matriz de custo do grafo. Ou seja, ele verifica a distância entre cada par de vértices. Se existir uma aresta, o valor que ele coloca naquela posição da matriz é o custo da aresta. Se não existir uma aresta entre o par de vértice, ele coloca o valor ∞.

Em seguida, ele verifica se existe um caminho de menor custo entre cada par de vértices, ao passar por um vértice intermediário. Ou seja, suponha um grafo com 5 vértices. De um modo geral, após montar a matriz de distâncias, ele fará 5 iterações:

1ª. Iteração: descobrir se há caminhos que ficam menores ao passar pelo vértice 1 2ª. Iteração: descobrir se há caminhos que ficam menores ao passar pelo vértice 2 3ª. Iteração: descobrir se há caminhos que ficam menores ao passar pelo vértice 3 4ª. Iteração: descobrir se há caminhos que ficam menores ao passar pelo vértice 4 5ª. Iteração: descobrir se há caminhos que ficam menores ao passar pelo

Grafo exemplo para entrada

a) Matriz de adjacência:

1 2 3 4 5 6

1

2

3

4

5

6

void digraph::floyd (void) { // Para caso real substitua o 6 por v (vértices) for (int k=1; k <= 6; k++) { // k <= D->V for (int i=1; i <= 6; i++) { for (int j=1; j <= 6; j++){ if ( (D->adj[j][k]+D->adj[k][i] < D->adj[j][i]) ) { D->adj[j][i]=D->adj[j][k]+D->adj[k][i]; }// j,k + k,i < j,i j,k + k,i < j,i j,k + k,i < j,i j,k + k,i < j,i } // 1,1 + 1,1 < 1,1 1,1 + 1,2 < 1,2 1,1 + 1,3 < 1,3 1,1 + 1,4 < 1,4 ... } // 2,1 + 1,1 < 2,1-> 2,1 + 1,2 < 2,2-> 2,1 + 1,3 < 2,3-> 2,1 + 1,4 < 2,4 ... } // 3,1 + 1,1 < 3,1 3,1 + 1,2 < 3,2 3,1 + 1,3 < 3,3 3,1 + 1,4 < 3,4 ...} // 4,1 + 1,1 < 4,1 4,1 + 1,2 < 4,2 4,1 + 1,3 < 4,3 4,1 + 1,4 < 4,4 ... // 5,1 + 1,1 < 5,1 5,1 + 1,2 < 5,2 5,1 + 1,3 < 5,3 5,1 + 1,4 < 5,4 ... // 6,1 + 1,1 < 6,1 6,1 + 1,2 < 6,2 6,1 + 1,3 < 6,3 6,1 + 1,4 < 6,4 ...

// 1,2 + 2,1 < 1,1 1,2 + 2,2 < 1,2 1,2 + 2,3 < 1,3 1,2 + 2,4 < 1,4 ... // 2,2 + 2,1 < 2,1-> 2,2 + 2,2 < 2,2-> 2,2 + 2,3 < 2,3-> 2,2 + 2,4 < 2,4 ... // 3,2 + 2,1 < 3,1 3,2 + 2,2 < 3,2 3,2 + 2,3 < 3,3 3,2 + 2,4 < 3,4 ... // 4,2 + 2,1 < 4,1 4,2 + 2,2 < 4,2 4,2 + 2,3 < 4,3 4,2 + 2,4 < 4,4 ... // 5,2 + 2,1 < 5,1 5,2 + 2,2 < 5,2 5,2 + 2,3 < 5,3 5,2 + 2,4 < 5,4 ... // 6,2 + 2,1 < 6,1 6,2 + 2,2 < 6,2 6,2 + 2,3 < 6,3 6,2 + 2,4 < 6,4 ...

// 1,3 + 3,1 < 1,1 1,3 + 3,2 < 1,2 1,3 + 3,3 < 1,3 1,3 + 3,4 < 1,4 ... // 2,3 + 3,1 < 2,1-> 2,3 + 3,2 < 2,2-> 2,3 + 3,3 < 2,3-> 2,3 + 3,4 < 2,4 ... // 3,3 + 3,1 < 3,1 3,3 + 3,2 < 3,2 3,3 + 3,3 < 3,3 3,3 + 3,4 < 3,4 ... // 4,3 + 3,1 < 4,1 4,3 + 3,2 < 4,2 4,3 + 3,3 < 4,3 4,3 + 3,4 < 4,4 ... // 5,3 + 3,1 < 5,1 5,3 + 3,2 < 5,2 5,3 + 3,3 < 5,3 5,3 + 3,4 < 5,4 ... // 6,3 + 3,1 < 6,1 6,3 + 3,2 < 6,2 6,3 + 3,3 < 6,3 6,3 + 3,4 < 6,4 ...

Apostila de Algoritmos e Estrutura de Dados III 38

2

3

1

4

6

5

34

3 8 20

6

3

2

2

Page 39: Apostila Alg 3

// 1,4 + 4,1 < 1,1 1,4 + 4,2 < 1,2 1,4 + 4,3 < 1,3 1,4 + 4,4 < 1,4 ... // 2,4 + 4,1 < 2,1-> 2,4 + 4,2 < 2,2-> 2,4 + 4,3 < 2,3-> 2,4 + 4,4 < 2,4 ... // 3,4 + 4,1 < 3,1 3,4 + 4,2 < 3,2 3,4 + 4,3 < 3,3 3,4 + 4,4 < 3,4 ... // 4,4 + 4,1 < 4,1 4,4 + 4,2 < 4,2 4,4 + 4,3 < 4,3 4,4 + 4,4 < 4,4 ... // 5,4 + 4,1 < 5,1 5,4 + 4,2 < 5,2 5,4 + 4,3 < 5,3 5,4 + 4,4 < 5,4 ... // 6,4 + 4,1 < 6,1 6,4 + 4,2 < 6,2 6,3 + 4,3 < 6,3 6,4 + 4,4 < 6,4 ...

// 1,5 + 5,1 < 1,1 1,5 + 5,2 < 1,2 1,5 + 5,3 < 1,3 1,5 + 5,4 < 1,4 ... // 2,5 + 5,1 < 2,1-> 2,5 + 5,2 < 2,2-> 2,5 + 5,3 < 2,3-> 2,5 + 5,4 < 2,4 ... // 3,5 + 5,1 < 3,1 3,5 + 5,2 < 3,2 3,5 + 5,3 < 3,3 3,5 + 5,4 < 3,4 ... // 4,5 + 5,1 < 4,1 4,5 + 5,2 < 4,2 4,5 + 5,3 < 4,3 4,5 + 5,4 < 4,4 ... // 5,5 + 5,1 < 5,1 5,5 + 5,2 < 5,2 5,5 + 5,3 < 5,3 5,5 + 5,4 < 5,4 ... // 6,5 + 5,1 < 6,1 6,5 + 5,2 < 6,2 6,5 + 5,3 < 6,3 6,5 + 5,4 < 6,4 ...

// 1,6 + 6,1 < 1,1 1,6 + 6,2 < 1,2 1,6 + 6,3 < 1,3 1,6 + 6,4 < 1,4 ... // 2,6 + 6,1 < 2,1-> 2,6 + 6,2 < 2,2-> 2,6 + 6,3 < 2,3-> 2,6 + 6,4 < 2,4 ... // 3,6 + 6,1 < 3,1 3,6 + 6,2 < 3,2 3,6 + 6,3 < 3,3 3,6 + 6,4 < 3,4 ... // 4,6 + 6,1 < 4,1 4,6 + 6,2 < 4,2 4,6 + 6,3 < 4,3 4,6 + 6,4 < 4,4 ... // 5,6 + 6,1 < 5,1 5,6 + 6,2 < 5,2 5,6 + 6,3 < 5,3 5,6 + 6,4 < 5,4 ... // 6,6 + 6,1 < 6,1 6,6 + 6,2 < 6,2 6,6 + 6,3 < 6,3 6,6 + 6,4 < 6,4 ...

Exercício: com base na resolução do problema paises.cpp, implemente a resolução de duas outras maneiras:a) modifique o fonte original que utiliza o dijkstra utilizando classes ao invés de structs (baseie-se no fonte abaixo)b) implemente a mesma resolução utilizando agora o floyd e compare o tempo de execução dos 2.

// Aplicação do algoritmo de Floyd para encontrar todos os melhores caminhos#include <iostream> #include <iomanip>#define INFINITO 999999#define Vertex int #define graph digraphusing namespace std;/* A estrutura digraph representa um digrafo. adj é a matriz de adjacência do digrafo. O campo Vertices contém o núm. de Vértices e o campo A contém o núm. de arcos do digrafo */class digraph { public: int V; int A; int adj[501][501]; void floyd (void); };

digraph *D; /* Um objeto do tipo Digraph contém o endereço de um digraph. */

void digraph::floyd (void) { // Para caso real substitua o 6 por v (vértices)...

int main(void){ D= new (class digraph); int i,j,cidades,acordos,consultas,Origem,Destino,Tempo; cin >> cidades; ...

c) Bellman-Ford

O Algoritmo de Bellman-Ford, igualmente ao Dijkstra, é um algoritmo de busca de caminhos mínimos em um digrafo ponderado, ou seja, cujas arestas têm peso, inclusive negativo. O Algoritmo de Dijkstra resolve o mesmo problema em um tempo muito menor, porém exige que todas as arestas tenham pesos positivos. Portanto, o algoritmo de Bellman-Ford é normalmente usado apenas quando existem arestas de peso negativo.

Apostila de Algoritmos e Estrutura de Dados III 39

Page 40: Apostila Alg 3

// Aplicação do algoritmo de Bellman-Ford no problema dos países em guerra#include <iostream>#include <cstdio>using namespace std;#define INFINITY 99999999 typedef struct { int source; int dest; int weight;} Edge;

void BellmanFord(Edge edges[], int edgecount, int nodecount, int source, int dest){ int i,j; int distance[501]; for (i = 0; i < nodecount; i++) { if (i == source) distance[i] = 0; else distance[i] = INFINITY; } for (i = 0; i < nodecount; i++) { for (j = 0; j < edgecount; j++) { if (distance[edges[j].dest] > distance[edges[j].source] + edges[j].weight){ distance[edges[j].dest] = distance[edges[j].source] + edges[j].weight; } } } if (distance[dest] < 100000) cout << distance[dest]; else cout << "Nao e possivel entregar a carta"; cout << endl;}

int main (void){ Edge Arestas[25000]; int cidades, acordos,consultas, org , dest;

while (1){ cin >> cidades; cin >> acordos; if (cidades==0) break; for (int i=0; i < acordos; i++){ cin >> Arestas[i].source; cin >> Arestas[i].dest; cin >> Arestas[i].weight; }

for (int i=0; i < acordos; i++){ for (int j=i; j < acordos; j++){ if ((Arestas[j].source==Arestas[i].dest)&&(Arestas[j].dest==Arestas[i].source)) Arestas[i].weight=Arestas[j].weight=0; } }

cin >> consultas; for (int i=0; i < consultas; i++){ cin >> org; cin >> dest; BellmanFord(Arestas,acordos,cidades+1,org,dest); } cout << endl; } return(0);}

Exercício: Implemente o Algoritmo de Bellman-Ford, e compare a performance com o Floyd e Dijkstra comparando o tempo necessário para rodar a entrada padrão paises.in.

Apostila de Algoritmos e Estrutura de Dados III 40