LICENCIATURA EM ENGENHARIA INFORMÁTICA E COMPUTAÇÃO Algoritmos e Estruturas de Dados II, 2002/2003 EXAME TIPO COM CONSULTA DURAÇÃO: 2 horas 1. [2.0] Mostre o resultado final de inserir numa tabela de dispersão de tamanho N=13, inicialmente vazia, os valores 16, 4 e 29, por esta ordem, usando a função de dispersão H(X) = X mod 13 e dispersão aberta com teste quadrático para resolução de colisões. 2. [2.0] Mostre o estado de um heap binário (na representação em árvore) inicialmente vazio após a realização de cada uma das seguintes operações: insert(10), insert(5), insert(12), insert(3), deleteMin(). 3. [2.0] Mostre o resultado final de inserir numa árvore k-d inicialmente vazia os pontos (3,5), (8,2), (4, 3), (2, 5) e (1, 7), por esta ordem. 4. [2.0] Suponha que se pretende contar o número de palavras diferentes existentes numa lista desordenada de palavras. É possível resolver este problema em tempo linear no tamanho da lista? Com que estrutura de dados? Justifique. 5. [2.0] Suponha que se pretende guardar um conjunto de valores numa estrutura de dados suportando eficientemente as seguintes operações: i) verificar se um valor pertence ao conjunto; ii) inserir um novo valor no conjunto; iii) eliminar um valor do conjunto; iv) obter o valor mínimo ou o valor máximo existente no conjunto; v) obter uma lista ordenada dos valores existentes no conjunto entre um valor mínimo e um valor máximo especificados; vi) reunir dois conjuntos num só (sem valores repetidos). Que estrutura de dados estudada na disciplina seria mais apropriada para este efeito? Com essa estrutura de dados, qual seria a complexidade temporal, no pior caso e no caso médio, de cada uma das operações acima indicadas? Justifique. 6. [2.0] Assinale os componentes fortemente conexos do grafo indicado na figura seguinte. i a c e d f g b k h j 7. [2.0] Dado um grafo dirigido com custos não negativos associados às arestas, pretende-se encontrar um caminho de custo mínimo de um vértice s para um vértice t, passando obrigatoriamente, uma e uma só vez, por vértices intermédios x1, ..., xk, pela ordem indicada (podendo passar mais do que uma vez por outros vértices). Explique de que forma este problema pode ser resolvido eficientemente com base em algoritmos estudados na disciplina e indique, justificando, em quanto tempo pode ser resolvido o problema, em notação O(). A figura seguinte exemplifica um caminho de custo mínimo deste tipo. x2 1 s a 3 Caminho: t 3 s-a-x1-a-x2-a-t 1 2 5 5 1 2 x1 1 Pagina 1 de 6 8. [2.5] Um grafo G=(V,E) diz-se bipartido se é possível partir o conjunto V de vértices em dois subconjuntos V1 e V2 disjuntos de tal forma que quaisquer dois vértices adjacentes em G pertencem a subconjuntos diferentes. Por exemplo, o grafo G1 da figura seguinte é bipartido, enquanto que o grafo G2 não é bipartido (na realidade, o grafo G2 é tripartido). G1 G2 V1 V2 1 2 3 4 V1 1 V2 2 5 V3 3 6 4 O problema de verificar se um grafo não dirigido é bipartido e, em caso afirmativo, encontrar uma possível partição dos vértices, pode ser resolvido eficientemente com base no algoritmo de visita em profundidade, de que se recorda a seguir uma implementação em pseudo-Java (na realidade, C#). class Graph { // ... public void dfs() { foreach (Vertex v in V) // V - vértices de G v.visited = false; foreach (Vertex v in V) if (! v.visited) dfs(v); } private void dfs(Vertex v) { v.visited = true; foreach (Vertex w in v.adj) // adj - vértices adjacentes if(! w.visited) dfs(w); } } Escreva em Java ou pseudo-Java um método isBipartite(), baseado na pesquisa em profundidade, que devolve true no caso do grafo ser bipartido e, adicionalmente, preenche a propriedade v.partition (de tipo int) de cada vértice v com o número do subconjunto (V1 ou V2) em que o vértice é colocado. 9. [2.5] Esboce em Java ou pseudo-Java um algoritmo baseado em retrocesso (backtracking) para verificar se dois grafos G1 e G2 são isomorfos. Dois grafos dizem-se isomorfos se é possível estabelecer uma correspondência entre os vértices dos dois grafos, de forma a que, para cada aresta (u,v) num dos grafos, existe uma aresta (u*,v*) no outro grafo, em que u* é o vértice correspondente a u e v* é o vértice correspondente a v. Na figura seguinte, apenas os grafos G1 e G2 são isomorfos. G2 G1 a c b d x z Pagina 2 de 6 G3 y y x w z w Resolução: 1. H(16)=3 H(4)=4 H(29)=3 como a posição 3 está ocupada, tenta em 3+12=4, que também está ocupada, e depois em 3+22=7 Índice 0 1 2 3 4 5 6 7 8 9 10 11 12 Conteúdo 16 4 29 Nota: Basta apresentar a tabela, mas, em caso de erro, a explicação inicial pode ajudar. 2. Nota prévia: como se fala em deleteMin, supõe-se que é um heap que guarda o mínimo à cabeça. insert(10) insert(5) 10 10 5 insert(3) percolateUp 5 10 3 12 insert(12) percolateUp 5 10 10 12 12 percolateDown deleteMin() 3 5 5 10 5 5 12 10 12 10 Nota: não é necessário mostrar os estados intermédios (indicados a tracejado), mas ajudam a perceber erros. Pagina 3 de 6 3. (3,5) (8,2) (2,5) (4,3) (1,7) 4. Sim, é possível resolver este problema em tempo linear no tamanho da lista, em termos médios, utilizando uma tabela de dispersão, da seguinte forma: Começa-se com uma tabela de dispersão (T) inicialmente vazia e com um contador de palavras diferentes (K) inicialmente a 0. Percorrem-se sequencialmente os elementos da lista (L) e, para cada elemento, verifica-se se existe na tabela de dispersão. Em caso afirmativo, passa-se ao elemento seguinte. Em caso negativo, incrementa-se o contador de palavras diferentes e insere-se o elemento na tabela de dispersão. Sendo N o tamanho da lista e K o número de palavras diferentes, há que somar os seguintes tempos: O(N) - percorrer a lista (caso médio e pior caso) N * O(1) = O(N) - verificar se elemento existe na tabela de dispersão (caso médio) K * O(1) = O(K) - inserir elemento na tabela de dispersão (caso médio) Uma vez que KN, o tempo total é O(N), em média. 5. Uma boa solução de compromisso seria uma árvore binária equilibrada (por exemplo, árvore AVL), conforme mostra a tabela seguinte. Se bem que uma tabela de dispersão seja um pouco melhor (só apenas no caso médio), em algumas operações, é muito pior nas operações que se baseiam na ordenação (iv e v). Operação Tempo com árvore AVL, no Tempo com tabela de pior caso e caso médio dispersão, no caso médio i) verificar se um valor pertence O(log N) O(1) ao conjunto ii) inserir um novo valor no O(log N) O(1) conjunto iii) eliminar um valor do O(log N) O(1) conjunto iv) obter o valor mínimo ou o O(log N) O(N) (caso médio e pior caso) valor máximo existente no conjunto v) obter uma lista ordenada dos O(R+log N), em que R é o O(N + R log R) (caso médio e valores existentes no conjunto tamanho do resultado. A parcela pior caso), em que R é o entre um valor mínimo e um log N refere-se ao acesso a tamanho do resultado. O tempo valor máximo especificados elementos que não fazem parte O(N) refere-se à selecção dos do resultado (no máximo 1 valores no intervalo pretendido, elemento de cada nível pela e o tempo O(R log R) refere-se à esquerda e pela direita). ordenação dos valores. vi) reunir dois conjuntos num só O[N2log(N1+N2-Ncomuns)], O(N2), supondo que se inserem (sem valores repetidos) supondo que se inserem os os elementos do 2º conjunto no elementos do 2º conjunto no 1º 1º Nota: não é necessário indicar os tempos da tabela de dispersão. Pagina 4 de 6 6. O grafo tem os 4 componentes fortemente conexos indicados por rectângulos de cantos arredondados na figura. i a c d e f g k h b j 7. O problema pode ser resolvido por aplicação repetida do algoritmo de Dijkstra: determinar o caminho de custo mínimo (ou mais curto) de s para x1, considerando eliminados do grafo (do ponto de vista lógico) os vértices x2, ..., xk; para i=1, ..., k-1, determinar o caminho de custo mínimo de xi para xi+1, considerando eliminados do grafo (do ponto de vista lógico) os vértices x1, xi-1,..., xi+1, xk; determinar o caminho de custo mínimo de xk para t, considerando eliminados do grafo (do ponto de vista lógico) os vértices x1, ..., xk-1. Para determinar o caminho mais curto (ou de custo mínimo) entre dois vértices, usa-se o algoritmo de Dijkstra, o qual determina todos os caminhos mais curtos entre um dado vértice e todos os restantes. Uma vez que o tempo de execução de cada aplicação do algoritmo de Dijkstra é de ordem O(|E| log |V|), e o algoritmo é aplicado k+1 vezes, o tempo total de resolução do problema é de ordem O((k+1)|E| log |V|). 8. class Graph { /** Verifica se o grafo é bipartido. */ public bool isBipartite() { foreach (Vertex v in V) // V - vértices de G v.partition = 0; // ainda não atribuido foreach (Vertex v in V) if (! v.partition) if ( ! dfs2(v, 1) ) return false; return true; } /** * Efectua visita e define partição de cada vértice. * @param v - vértice a visitar, bem como adjacentes * @param partition - partição (1 ou 2) em que deve colocar * este vértice **/ private bool dfs2(Vertex v, int partition) { v.partition = partition; partition = (partition==1)? 2 : 1; // para adjacentes foreach (Vertex w in v.adj) // adj - vértices adjacentes if(! w.partition) dfs(w, partition); else if (w.partition != partition) return false; return true; } } Pagina 5 de 6 9. Nota: neste esboço de resolução apenas é detalhada a estratégia de backtracking. static bool isomorfos(Graph G1, Graph G2) { if (G1.V.size()! = G2.V.size()) return false; foreach(Vertex v in G1) v.image = null; foreach(Vertex v in G2) v.image = null; return isomorfos_rec(G1,G2); } // Tenta definir correspondente para 1 vértice de G1 // e chamar-se recursivamente para os restantes. static bool ismorfos_rec(Graph G1, Graph G2) { v1 = select1(Vertex v1 in G1 such that v1.image==null); if (v1 == null) return true; foreach(Vertex v2 in G2 such that v2.image == null && nonnull(image(v1.adj)) == nonnull(v2.adj) && nonnull(image(v1.invadj)) == nonnull(v2.invadj)) // v.adj - conjunto de vértices adjacentes de v // v.invadj - conjunto de vértices de que v é adjacente // image(...) - aplicado a conjunto de vértices, dá o // conjunto das suas imagens // nonnull(...) - retira nulls's de conjunto { v1.image = v2; v2.image = v1; if (isomorfos_rec(G1, G2)) return true; v1.image = null; v2.image = null; } return false; } Pagina 6 de 6