ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO
Departamento de Engenharia de Computação e Sistemas Digitais
PCS
3111
-
L
ABORATÓRIO DE
P
ROGRAMAÇÃO
O
RIENTADA A
O
BJETOS PARA A
E
NGENHARIA
E
LÉTRICA
E
XERCÍCIOI
NTEGRADOR–
P
ARTE2
–
2014
Resumo
A segunda parte do exercício integrador tem como objetivo usar o conceito de grafo em um software orientado a objetos. Deve-se criar uma nova funcionalidade no sistema para permitir traçar a rota entre um espaço e outro, considerando uma configuração de espaços obtida em um arquivo texto.
Objetivos
Usar o conceito de grafos em um software orientado a objetos. Acessar um arquivo de configuração.
Usar os demais containers da STL.
1 Introdução
Usando o projeto que foi implementado na aula anterior (Aula 10), deve-se acrescentar uma funcionalidade para traçar uma rota entre dois espaços. Tal rota poderia ser útil, por exemplo, para que um entregador de um novo equipamento pudesse colocá-lo de modo conveniente no Espaço desejado. Para criar tal rota, será usado um grafo para representar a relação entre Espaços em um Local.
1.1
Fundamentação teórica
A seguir será apresentada uma breve explicação sobre grafos e busca em largura. Sugere-se a consulta dos livros do Cormen et al. [1] e do Johnsonbaugh [2] para maiores informações sobre grafos.
1.1.1 Grafos
Grafos são abstrações usadas para modelar problemas de várias áreas como, por exemplo, Engenharia, Computação, Biologia, Economia e Sociologia. Um grafo é definido como um par (V, E), onde V representa um conjunto de vértices (ou nós) e E é um conjunto de arestas (ou arcos). Cada aresta representa um par (v1, v2), com v1 e v2 V.
Existem diversas maneiras de se representar um grafo computacionalmente. De uma forma geral, um grafo pode ser representado como uma matriz de adjacência ou uma lista de adjacência1. Especificamente sobre uma lista de adjacência (que será usada neste exercício), para representar um grafo usa-se um arranjo em que cada posição representa uma lista de vértices adjacentes. Por exemplo, considere um grafo não orientado G = (V, E) definido como:
1
V = {1, 2, 3, 4}
E = {(1, 2), (1, 3), (2, 3), (2, 4), (3, 4)}
Esse grafo é representado graficamente na Figura 1a e através de uma lista de adjacência na Figura 1b. A lista de adjacências usa um vetor de tamanho |V| em que cada posição é uma lista ligada.
(a) (b)
Figura 1: Um exemplo de um grafo (a) e a representação dele como uma lista de adjacência (b).
1.2
Busca em largura
Existem dois algoritmos básicos para varrer um grafo: a busca em largura e a busca em profundidade. Na busca em largura, primeiro visita-se todos os vértices conectados a um vértice antes de visitar outros vértices. Por exemplo, no caso do grafo apresentado na Figura 1a, a busca em largura a partir do vértice 1 visitaria todos os vértices ligados a 1, ou seja, os vértices 2 e 3 e só então analisaria os vértices conectados ao vértice 2 e ao vértice 3 (no caso o vértice 4).
Um algoritmo para busca em largura (BFS) é apresentado a seguir, baseado no definido em [1]. BFS (G, s)
for each vértice u ϵ G.V – {s} u.cor = BRANCO u.dist = ∞ u.pred = NIL s.cor = CINZA s.dist = 0 s.pred = NIL Q = Ø Enqueue(Q, s) while Q != Ø u = Dequeue(Q)
for each v ϵ G.Adjacencia[u] if v.cor == BRANCO v.cor = CINZA v.dist = u.dist + 1 v.pred = u Enqueue(Q,v) u.cor = PRETO
Esse algoritmo usa a ideia de cor para saber se um vértice já foi visitado ou não (os brancos não foram visitados, enquanto que os cinza estão na fila para serem analizados e os pretos já foram analisados). Para se saber a rota entre um vértice e a origem é possível usar o atributo predecessor (pred).
1.3
Implementação de grafos em C++
A seguir será apresentado como implementar um grafo em C++, considerando o projeto da disciplina.
1 3 2 4 1 2 3 4 2 2 1 1 3 / 3 2 3 / 4 / 4 /
1.3.1 Lista de adjacência em C++
Uma forma simples de representar uma lista de adjacência é fazer um vetor de listas, com um tamanho igual ao número de vértices. Com isso, em cada posição há uma lista. Usando a biblioteca STL do C++, pode-se usar o container std::list como implementação de uma lista ligada:
// Um array de listas de Vértices
std::list<Vertice*> listaDeAdjacencia[NUMERO_DE_VERTICES];
// Um vector de listas de Vértices
std::vector<std::list<Vertice*>> listaDeAdjacencia;
Uma dificuldade dessa implementação é que é preciso realizar um mapeamento manual entre uma posição do vetor e um Vértice; ou seja, é preciso saber que um Vértice va está na posição 0 do vetor e que
um Vértice vb está na posição 1 do vetor, por exemplo. Uma forma simples de evitar isso é usar um mapa,
disponível na STL em std::map. Mapas armazenam elementos na forma de pares chave e valor. A partir de uma chave, acessa-se o valor associado a ela. Em C++, a chave e o valor podem ser de qualquer tipo. No nosso exemplo, a chave seria um vértice e o valor seria uma lista de vértices. Isso poderia ser declarado como:
std::map<Vertice*, std::list<Vertice*>> listaDeAdjacencia;
Usando essa implementação, pode-se conectar um Vértice a outro em um grafo não orientado da seguinte forma:
Vértice *v1 = ...; Vértice *v2 = ...;
listaDeAdjacencia[v1].push_back(v2); listaDeAdjacencia[v2].push_back(v1); 1.3.2 Local como um grafo
Um Local do sistema de automação residencial possui diversos Espaços, conforme apresentado no diagrama de classes do Apêndice A. Na implementação, um vector é usado para armazenar uma lista de Espaços. Porém, essa solução não permite representar as relações entre vários Espaços.
Por exemplo, considere a planta de uma casa apresentada na Figura 2. A cozinha, a sala, a suíte, etc. são espaços e as portas são representadas por um retângulo preto. Dessa forma, existe uma porta entre a cozinha e a sala e entre a sala de jantar e a cozinha, por exemplo. Essa relação entre os Espaços pode ser representada como um grafo: cada Espaço é um vértice e cada porta é uma aresta, ligando dois Espaços. O grafo representando a planta da Figura 2 é apresentado na Figura 3.
Figura 2: Planta de uma casa.
Sala de Jantar Cozinha Sala Quarto1 Quarto2 Suíte Banheiro Banheiro da Suíte Corredor
Figura 3: Grafo representando a ligação entre Espaços da Figura 2.
1.3.3 Busca em largura em C++
Em uma solução OO, um vértice é um objeto. Na implementação do procedimento BFS, seria necessário criar novos atributos para implementar a cor, a distância e o predecessor. Porém, isso pode não ser adequado já que geram-se atributos que não fazem sentido para o tipo em questão. Por exemplo, esses atributos não fazem sentido para a classe Espaço do nosso sistema. Uma forma de resolver este problema é usar uma ou mais estruturas adicionais para representar as informações necessárias para executar o procedimento.
Ao analisar quais informações são realmente necessárias, é possível simplificar a estrutura necessária para executar o algoritmo. A cor, por exemplo, pode ser facilmente retirada ao usar o predecessor (se o predecessor for NIL significa que o vértice ainda não foi visitado). Se a distância também não for relevante (como não será no nosso projeto), só será necessária uma estrutura para guardar o predecessor de um determinado Espaço.
Para implementar uma Fila em C++ pode-se usar o container std::queue. Por fim, para imprimir um caminho é possível usar o predecessor, indo de trás para frente (do destino à origem).
2. Atividades
2.1
Entrega 0 (já feito em casa)
Espaço
o Altere a implementação para usar um vector ao invés de um arranjo de Equipamentos (Equipamento[]). Sala de Jantar Sala Suíte Banheiro da Suíte Quarto1 Quarto2 Banheiro Corredor Cozinha
2.2 Entrega 1
Mapao Crie uma classe Mapa que permite representar as relações entre os Espaços como um Grafo. Essa classe deve ter a seguinte definição:
class Mapa { public: Mapa();
void adicionarEspaco(Espaco* e);
void conectar(Espaco* e1, Espaco* e2);
void imprimirCaminho(Espaco* origem, Espaco* destino); private:
std::vector<Espaco*> espacos;
std::map<Espaco*,std::list<Espaco*>> listaDeAdjacencia; };
O método adicionarEspaço deve adicionar o Espaço e ao Mapa.
O método conectar deve criar uma aresta entre o Espaco e1 e o Espaco e2 (e vice-versa).
O método imprimirCaminho deve usar o algoritmo de busca em largura para imprimir o caminho entre a origem e o destino.
Nota1: Agora, há dois “espacos”: Local::espacos e Mapa::espacos. Em condições normais, os dois atributos devem ser iguais.
Nota2: conectar(e1,e2) deve criar duas arestas: e1->e2 E e2->e1.
Nota3: Possivelmente, a solução ficaria mais simples se trabalhasse com índices em vetor “espacos” em vez de ponteiros para Espaco.
Nota4: É importante ter como testar se o programa está correto. Faça funcionar a seguinte main.cpp, usando casa.txt: ... try { LeitorDeConfiguracao leitor; Local *local; string arquivo;
cout << "Informe o arquivo de configuracao ou aperte ENTER para usar o padrao" << endl; getline(cin, arquivo);
cout << endl;
if (arquivo == "") local = leitor.carregar(); else local = leitor.carregar(arquivo);
Mapa *mapa=new Mapa;
Painel painelCentral(local); // Aula10
Espaco *e0=local->getEspaco(0); cout << e0->getNome() << endl; Espaco *e1=local->getEspaco(1); cout << e1->getNome() << endl; Espaco *e2=local->getEspaco(2); cout << e2->getNome() << endl; Espaco *e3=local->getEspaco(3);
cout << endl; mapa->adicionarEspaco( e0 ); mapa->adicionarEspaco( e1 ); mapa->adicionarEspaco( e2 ); mapa->adicionarEspaco( e3 ); mapa->conectar( e0, e2 ); mapa->conectar( e1, e2 ); mapa->conectar( e2, e3 ); mapa->imprimirCaminho(e0,e1); mapa->imprimirCaminho(e0,e3); delete local; delete mapa;
} catch (ErroDeArquivo erro) { ... Saída: Sala SalaDeJantar Corredor Quarto1 Sala->Corredor->SalaDeJantar Sala->Corredor->Quarto1
2.3 Entrega 2
Crie uma classe LeitorDoMapa que lê um arquivo de configuração. Caso ao ler o arquivo se encontre um Espaco inválido, deve-se jogar um ErroDeArquivo.
o Essa classe deve possuir a seguinte definição: class LeitorDoMapa {
public:
LeitorDoMapa(Local* local); Mapa* carregar();
Mapa* carregar(std::string arquivo); private:
Espaco* encontrar(std::string nome); static const std::string ARQUIVO_PADRAO; Local *local;
};
O método carregar deve criar um mapa considerando o local e o arquivo de configuração (o método sem parâmetros usa o ARQUIVO_PADRAO).
O método encontrar é um método auxiliar para encontrar um Espaco a partir de seu nome. Onde? No Local::espacos?
Use como ARQUIVO_PADRAO o arquivo “mapa.txt”. o O formato do arquivo de configuração deve ser o seguinte:
<Número de Espacos> <Nome do Espaco A> <Nome do Espaco B> ...
<Número de arestas>
<Nome do Espaco A> <Nome do Espaco B> <Nome do Espaco C> <Nome do Espaco A> ...
O arquivo “mapa.txt” apresenta um exemplo desse arquivo considerando o local descrito em “casa.txt”.
Caso um Espaço do arquivo de configuração não esteja no Local, jogue um ErroDeArquivo.
É importante ter como testar se o programa está correto. Para isso, primeiro, declare temporariamente todos os atributos do Mapa.h como públicos:
//private:
std::vector<Espaco*> espacos;
Faça funcionar a seguinte main.cpp, usando casa.txt e mapa.txt:
try {
LeitorDeConfiguracao leitor; Local *local;
string arquivo;
cout << "Arquivo de configuracao (ENTER para o padrao): "; getline(cin, arquivo);
cout << endl;
if (arquivo == "") local = leitor.carregar(); else local = leitor.carregar(arquivo);
LeitorDoMapa leitorDoMapa(local);
cout << "Arquivo com o mapa (ENTER o padrao): "; getline(cin, arquivo);
Mapa *mapa;
if (arquivo == "") mapa = leitorDoMapa.carregar(); else mapa = leitorDoMapa.carregar(arquivo);
cout << endl;
// Hae: Para poder testar,
// deixe Mapa::espacos e Mapa::listaDeAdjacencia // temporariamente como public.
for (int i=0; i<mapa->espacos.size(); i++) { Espaco *e=mapa->espacos[i];
cout << e->getNome() << " [conectado a:] "; list<Espaco*> l=mapa->listaDeAdjacencia[e]; for (auto p=l.begin(); p!=l.end(); p++) cout << (*p)->getNome() << " "; cout << endl; } cout << endl; delete local; delete mapa;
} catch (ErroDeArquivo erro) {
A saída deve ser:
Sala [conectado a:] Corredor Cozinha
SalaDeJantar [conectado a:] Corredor Cozinha
Corredor [conectado a:] Sala SalaDeJantar Quarto1 Quarto2 Banheiro Suite Quarto1 [conectado a:] Corredor
Quarto2 [conectado a:] Corredor Banheiro [conectado a:] Corredor
Suite [conectado a:] Corredor BanheiroSuite BanheiroSuite [conectado a:] Suite
2.4 Entrega 3
Altere o main para carregar o Mapa usando a classe LeitorDoMapa. Pergunte ao usuário se ele quer usar o arquivo padrão ou informar o arquivo.
Altere a classe Painel para permitir traçar uma rota entre dois Espaços. o Crie uma opção 5: “Traçar Rota”.
o Altere o construtor do Painel para receber também um Mapa.
o Para fazer com que o usuário selecione um Espaço, use o método selecionaEspaco. Você deverá fazer duas chamadas para esse método (para obter a origem e o destino). Aqui, deve voltar a colocar os atributos do Mapa.h como privados:
private:
std::vector<Espaco*> espacos;
std::map<Espaco*, std::list<Espaco*>> listaDeAdjacencia;
3 Dicas
Use os arquivos .h entregues (Mapa.h e LeitorDoMapa.h).
Use a classe LeitorDeConfiguração como base para criar o LeitorDoMapa.
Para imprimir um caminho é possível usar o predecessor. Porém, se terá um caminho de trás para frente (do destino até a origem). Uma solução elegante é usar uma pilha (std::stack) para imprimir o caminho na ordem certa.
4 Bibliografia
[1] Cormen, T.; Leiserson, C.E.; Rivest, R.L.; Stein, C. Introduction to Algorithms. The MIT Press, 3rd ed. 2009. Capítulos 22 e Apêndice B.
Apêndice A – Diagrama de classes do projeto
ItemControlável(nome) ~ItemControlável() getNome() controlar() exibir() nome ItemControlável carregar() carregar(arquivo) ARQUIVO_PADRÃO LeitorDeConfiguração Local(nome) ~Local()adicionarEspaco(nome, limiteInferior, limiteSuperior) controlar() exibir() getEspaco(numero) getQuantidadeDeEspacos() Local Painel(local) mostrar() menuEconomiaDeAgua() menuMaquinaDeLavarRoupa(maquina) Painel
Espaco(nome, limiteInferior, limiteSuperior) ~Espaco()
controlar() exibir()
getEquipamento(numero) getQuantidadeDeEquipamentos()
adicionarArCondicionado(nome, temperatura, velocidade) adicionarLampada(nome) adicionarVentilador(nome, velocidade) adicionarChuveiro(nome) adicionarMáquinaDeLavarRoupa(nome) quantidadeDeEquipamentos Espaco Sensor() ~Sensor() geId() alarmeAcionado() ler() exibir() id contador Sensor SensorDePresenca() alarmeAcionado() ler() exibir() presencaDetectada SensorDePresenca SensorDeTemperatura(limiteInferior, limiteSuperior) alarmeAcionado() ler() exibir() getTemperatura() getLimiteInferior() getLimiteSuperior() temperatura limiteInferior limiteSuperior SensorDeTemperatura sensorDeTemperatura 1 possui sensorDePresenca 1 possui espacos * Equipamento(nome) ~Equipamento() ligar() desligar() estaLigado() ligado Equipamento ErroDeArquivo ErroDeCadastro ErroDeControle EquipamentoDeVentilacao(
nome, velocidade, sensor) controlar() ligar() getVelocidade() getVelocidadeMaxima() aumentarVelocidade() diminuirVelocidade() velocidade velocidadeMaxima EquipamentoDeVentilacao Lampada(nome) controlar() exibir() Lampada
ArCondicionado(nome, temperatura, velocidade, sensor) exibir()
setTemperatura(temperatura) temperatura
ArCondicionado
Ventilador(nome, velocidade, sensor) exibir() Ventilador sensor 1 monitora 1 local controla Chuveiro MáquinaDeLavarRoupa sensor 1 monitora equipamentos *