Amontoados Relaxados e Filas com Prioridade



Documentos relacionados
Fila de Prioridade. Siang Wun Song - Universidade de São Paulo - IME/USP. MAC Estruturas de Dados

CAPÍTULO 2. Grafos e Redes

Solução de problemas por meio de busca (com Python) Luis Martí DEE/PUC-Rio

Barómetro Regional da Qualidade Avaliação da Satisfação dos Utentes dos Serviços de Saúde

INF 1010 Estruturas de Dados Avançadas

7 - Análise de redes Pesquisa Operacional CAPÍTULO 7 ANÁLISE DE REDES. 4 c. Figura Exemplo de um grafo linear.

Árvores de Suporte de Custo Mínimo

DAS5102 Fundamentos da Estrutura da Informação

Técnicas de Computação Paralela Capítulo III Design de Algoritmos Paralelos

Jogos vs. Problemas de Procura

Modelos, em escala reduzida, de pontes e barragens. Simuladores de voo (ou de condução), com os quais se treinam pilotos (ou condutores).

FILAS DE PRIORIDADE e HEAPS

AULA 6 Esquemas Elétricos Básicos das Subestações Elétricas

Documento SGS. PLANO DE TRANSIÇÃO da SGS ICS ISO 9001:2008. PTD v Pág 1 de 6

Filas com prioridade - Introdução (1)

Notas sobre a Fórmula de Taylor e o estudo de extremos

Resolução de Problemas

Processo de Bolonha. Regime de transição na FCTUC

POC 13 - NORMAS DE CONSOLIDAÇÃO DE CONTAS

B2S SISTEMAS DE INFORMAÇÃO, LDA. RUA ARTILHARIA UM, Nº 67 3º FRT LISBOA TEL: FAX: B2S@B2S.

Resolução de sistemas lineares

Faculdade de Engenharia Optimização. Prof. Doutor Engº Jorge Nhambiu

PARLAMENTO EUROPEU. Comissão dos Assuntos Jurídicos PE v01-00

FEUP RELATÓRIO DE CONTAS BALANÇO

CT-234. Análise de Algoritmos e Complexidade Estrutural. Carlos Alberto Alonso Sanches

1 Introdução. 2 Exemplo de aplicação

Facturação Guia do Utilizador

Manual do Gestor da Informação do Sistema

TÉCNICAS DE PROGRAMAÇÃO

Catálogo Nacional de Compras Públicas. Manual de Fornecedores

Introdução ao estudo de equações diferenciais

Tabelas vista de estrutura

Índice. Como aceder ao serviço de Certificação PME? Como efectuar uma operação de renovação da certificação?

Truques e Dicas. = 7 30 Para multiplicar fracções basta multiplicar os numeradores e os denominadores: 2 30 = 12 5

Busca em Memória. Secundária

Relatório Trabalho Prático 2 : Colônia de Formigas para Otimização e Agrupamento

Especificação Operacional.

Introdução à Programação B Licenciatura em Engenharia Informática. Enunciado do trabalho prático. Quem quer ser milionário? 20 de Dezembro de 2007

Jornal Oficial da União Europeia L 141/5

Utilização do SOLVER do EXCEL

Diretrizes para determinação de intervalos de comprovação para equipamentos de medição.

SIMULADO DO TESTE DE RESOLUÇÃO DE PROBLEMAS

NCRF 19 Contratos de construção

Serviço de Clientes. Gestix Enterprise. Gestix.com

Arquimedes e Controle de Obra

Transcrição Automática de Música

Resolução da Assembleia da República n.º 64/98 Convenção n.º 162 da Organização Internacional do Trabalho, sobre a segurança na utilização do amianto.

Inteligência Artificial. Metodologias de Busca

[ \ x Recordemos o caso mais simples de um VLVWHPD de duas HTXDo}HVOLQHDUHV nas duas LQFyJQLWDV [ e \.

Base de dados I. Uma base de dados é um simples repositório de informação relacionado com um determinado assunto ou finalidade

Curriculum DeGóis Guia de preenchimento do Curriculum Vitae (Informação mínima necessária)

PROGRAMAÇÃO DE UM MICROPROCESSADOR

Exercícios Teóricos Resolvidos

Impostos sobre os veículos automóveis ligeiros de passageiros *

Capítulo 2. VARIÁVEIS DO TIPO INTEIRO

1 Introdução simulação numérica termoacumulação

Classificação da imagem (ou reconhecimento de padrões): objectivos Métodos de reconhecimento de padrões

Afinal o que são e como se calculam os quartis? Universidade do Algarve Departamento de Matemática

Inteligência Artificial Prof. Marcos Quinet Pólo Universitário de Rio das Ostras PURO Universidade Federal Fluminense UFF

TRABALHO LABORATORIAL NO ENSINO DAS CIÊNCIAS: UM ESTUDO SOBRE AS PRÁTICAS DE FUTUROS PROFESSORES DE BIOLOGIA E GEOLOGIA

Observações. Referência Título / Campo de Aplicação Emissor Data de adoção

Área de Intervenção IV: Qualidade de vida do idoso

O mecanismo de alocação da CPU para execução de processos constitui a base dos sistemas operacionais multiprogramados.

Manual do GesFiliais

Conceito de Tensão. Índice

Árvores Binárias de Busca

Memória cache. Prof. Francisco Adelton

3 Qualidade de Software

Processos. Estados principais de um Processo: Contexto de um Processo. Nível de um Processo.

Figure 2 - Nós folhas de uma árvore binária representando caracteres ASCII

Formulários FOLHA DE ROSTO

Tarefa Orientada 18 Procedimentos armazenados

INSTITUTO TECNOLÓGICO

NORMA BRASILEIRA DE CONTABILIDADE NBC TSC 4410, DE 30 DE AGOSTO DE 2013

Visão Artificial Para a Indústria. Manual do Utilizador

8. Perguntas e Respostas

o(a) engenheiro(a) Projeto é a essência da engenharia 07/02/ v8 dá vazão

Faculdade Sagrada Família

ARQUITECTURA DE COMPUTADORES CAPÍTULO II AULA X

Classificação e Pesquisa de Dados

Como foi visto no tópico anterior, existem duas formas básicas para representar uma função lógica qualquer:

Modelo Cascata ou Clássico

Trabalho de Implementação Jogo Reversi

natureza do projeto e da aplicação métodos e ferramentas a serem usados controles e produtos que precisam ser entregues

a 1 x a n x n = b,

2006/2007 Análise e Síntese de Algoritmos 2

REQUISITOS MÍNIMOS FUNCIONAIS QUANTO A CONFIGURAÇÕES DE BARRAS PARA SUBESTAÇÕES DA REDE BÁSICA DO SISTEMA INTERLIGADO BRASILEIRO.

Banco de Dados I Módulo V: Indexação em Banco de Dados. (Aulas 1, 2 e 3) Clodis Boscarioli

3.1 Definições Uma classe é a descrição de um tipo de objeto.

BREVE HISTÓRIA DA LINGUAGEM FORTRAN

Jornal Oficial da União Europeia

fx-82ms fx-83ms fx-85ms fx-270ms fx-300ms fx-350ms

DISCUSSÕES UE/EUA RELATIVAS AO ACORDO SOBRE EQUIVALÊNCIA VETERINÁRIA

Survey de Satisfação de Clientes 2009

IBM SmartCloud para Social Business. Manual do Utilizador do IBM SmartCloud Engage e IBM SmartCloud Connections

6. Programação Inteira

Cálculo da distância mínima a um. obstáculo para produção de eco

NORMA CONTABILISTICA E DE RELATO FINANCEIRO 1 ESTRUTURA E CONTEÚDO DAS DEMONSTRAÇÕES FINANCEIRAS

Transcrição:

Cátia Vaz Amontoados Relaxados e Filas com Prioridade 9 de Setembro de 2009 ISEL, Instituto Politécnico de Lisboa

Conteúdo 1 Introdução........................................................... 1 1.1 Estado da Arte..................................................... 3 2 Amontoados Relaxados............................................... 7 2.1 Árvores Binomiais.................................................. 7 2.2 Amontoados Relaxados.............................................. 9 2.2.1 Rank Relaxed Heaps............................................ 10 2.2.2 Run Relaxed Heaps............................................. 26 2.3 Detalhes de Implementação.......................................... 38 3 Filas com Prioridade................................................. 41 3.1 Tipo de Dados Abstracto............................................ 41 3.2 Detalhes de Implementação.......................................... 43 4 Avaliação Experimental.............................................. 45 4.1 Sequências de Números Aleatórios..................................... 45 4.2 Algoritmo de Dijkstra............................................... 49 5 Conclusão............................................................ 53 A Implementação em Java.............................................. 55 A.1 Rank Relaxed Heaps................................................. 55 A.2 Run Relaxed Heaps.................................................. 77 A.3 Priority Queue..................................................... 107 Referências.............................................................. 117

1 Introdução O problema de gerir uma fila de elementos com prioridade surge frequentemente no desenvolvimento de algoritmos para resolver problemas em engenharia informática e, na maior parte dos casos, tem um impacto significativo na eficiência da solução implementada. Dado um conjunto de elementos x cuja prioridade é definida por uma chave k(x), uma fila com prioridade é um tipo de dados abstracto que suporta as seguintes operações: insert(x), que insere o elemento x na fila; removemin(), que retorna e remove o elemento de menor chave na fila; minimum(), que retorna o elemento de menor chave na fila sem o remover. Neste caso o elemento com menor chave terá maior prioridade. Alternativamente, pode-se definir fila com prioridade como um tipo de dados abstracto com as operações remove- Max, que retorna e remove o elemento de maior chave na fila, e maximum, que retorna o elemento de maior chave na fila mas sem o remover. Contudo a escolha entre as duas definições está directamente relacionada com a ordem total imposta aos elementos. Logo, sem perda de generalidade, neste trabalho adopta-se a primeira definição, i.e., o elemento com a menor chave tem maior prioridade. Na solução de alguns problemas é necessário que o tipo de dados abstracto fila com prioridade suporte a operação: decreasekey(x, v), que atribui a chave v < k(x) ao elemento x, i.e., k(x) = v. Embora estas sejam as operações básicas, este tipo de dados abstracto pode incluir outras operações como a operação remove, que remove um dado elemento da fila, ou a operação meld, que funde duas filas com prioridade. Caso a operação remove seja suportada, a operação decreasekey pode ser definida através da operação remove seguida da operação insert. Contudo esta não é a prática usual por razões de eficiência. Em engenharia informática as filas com prioridade são utilizadas na solução de vários problemas, e.g., na determinação de caminhos mais curtos em grafos [1], na determinação de árvores abrangentes de menor custo [2] e no escalonamento de processos e tarefas em

1 Introdução 2 sistemas operativos [3]. Muitos destes problemas surgem no contexto de sistemas de alta performance e em tempo real, onde a eficiência da implementação das filas com prioridade é determinante. Em geral a implementação varia bastante com o problema em estudo. Por exemplo, no escalonamento de processos e tarefas em sistemas operativos, as chaves tomam valor num conjunto de valores finito. Neste caso, dado que a dimensão do conjunto domínio é apenas de algumas dezenas, a implementação mais simples passa por associar uma lista de elementos a cada chave e todas as operações podem ser implementadas com tempo O(1). Porém, quer para o problema de determinar os caminhos mais curtos, quer para o problema de determinar a árvore abrangente de menor custo, esta abordagem não é em geral viável dada a dimensão do domínio e a natureza das chaves. Neste caso a implementação mais simples será utilizar uma lista em que os elementos são guardados por ordem arbitrária ou uma lista em que os elementos são ordenados pelo valor da chave. Contudo estas abordagens implicam que a operação removemin, no primeiro caso, e a operação insert, no segundo caso, corram em tempo Ω(n) com n o número de elementos na fila. Embora outras implementações sejam possíveis, nas últimas três décadas foram publicados vários resultados que apontam os amontoados como a estrutura de dados mais adequada para a implementação de filas com prioridade. De um modo geral um amontoado é uma árvore em que cada nó tem uma chave associada e todos os descendentes tem chaves de valor superior. Nos trabalhos publicados foram apresentadas diversas estruturas de dados e vários resultados teóricos a respeito dos tempos de execução das operações descritas. Contudo nem todas as estruturas de dados são viáveis para implementar na prática, conduzindo muitas vezes a piores resultados que estruturas de dados teoricamente menos eficientes. O objectivo deste estudo é analisar a viabilidade prática das estruturas de dados avançadas para implementar amontoados e, consequentemente, filas com prioridade. A motivação para este estudo prende-se com a utilidade prática destas estruturas de dados no desenvolvimento de inúmeros algoritmos e com o interesse pedagógico em ensiná-las em disciplinas avançados de engenharia informática. As contribuições deste estudo são: a implementação das várias estruturas de dados discutidas e a sua avaliação experimental; a implementação do tipo de dados abstracto fila com prioridades permitindo a utilização das várias estruturas de dados; a discussão do interesse pedagógico e viabilidade prática da inclusão destas implementações em bibliotecas de uso geral.

1.1 Estado da Arte 3 1.1 Estado da Arte A concretização mais simples de amontoados foi introduzida por Williams [4], conhecidos por amontoados binários. Uma fila com prioridade implementada sobre amontoados binários permite consultar o mínimo em tempo O(1) no pior caso e extrair o mínimo, inserir e remover elementos em tempo O(log n) no pior caso. Os amontoados binários, ainda que teoricamente não constituam a estrutura de dados mais eficiente, são os mais utilizados nas implementações de filas com prioridade disponíveis nas diversas bibliotecas. Este facto deve-se à simplicidade de implementação e a ser possível implementar amontoados binários sobre vectores recorrendo apenas à indexação, conduzindo a uma menor utilização de memória. Dado o tempo de execução das operações e os baixos requisitos de memória, os amontoados binários são em geral apropriados para aplicações que tenham de manipular grandes quantidades de dados. Nos últimos anos foram propostas novas concretizações de amontoados com vista a melhorar o tempo de execução das várias operações e que contribuíram para implementações eficientes de vários algoritmos, como por exemplo os algoritmos de Dijkstra [1] e de Prim [2]. Na sua grande maioria as estruturas de dados propostas suportam a operação meld, i.e., amontoados fundíveis [5]. O principal objectivo consiste em (1) reduzir o tempo de execução de todas as operações para O(1) no pior caso, excepto as operações removemin e remove que devem ter tempo de execução O(log n) no pior caso. A razão deste objectivo prende-se com o facto que na maioria dos casos o número de inserções e de diminuições do valor das chaves é superior ao número de remoções. Um exemplo é o algoritmo de Dijkstra [1] em que o número de actualizações é proporcional ao número de arcos m no grafo e o número de extracções é proporcional ao número de vértices n. Como o número de arcos é regra geral maior que o número de vértices, uma estrutura de dados que satisfaça (1) conduz a um tempo de execução do algoritmo O(n log n + m) no pior caso. Os amontoados binomiais foram propostos por Vuillemin [6] e suportam todas as operações em tempo O(log n) no pior caso, incluindo a operação meld que nos amontoados binários leva tempo O(n). Com base nos amontoados binomiais foram propostas várias estruturas de dados que exploram versões relaxadas. Fredman and Tarjan [7, 8] propuseram os amontoados de Fibonacci, uma relaxação dos amontoados binomiais em que se permite que as árvores binomiais não sejam completas. Os amontoados de Fibonacci suportam as operação de remoção e de extracção do mínimo em tempo O(log n) amortizado e todas as outras operações, em particular a actualização de chaves, em tempo O(1) amortizado [9]. Desta forma, os amontoados de Fibonacci permitiram melhorar a comple-

1.1 Estado da Arte 4 xidade assimptótica de vários algoritmos, em particular dos algoritmos de Dijkstra [1] e de Prim [2] para um tempo de execução O(n log n + m) no pior caso. Contudo, dada a complexidade da estrutura de dados e os requisitos de memória, na prática os amontoados de Fibonacci não provaram ser eficientes, conseguindo-se regra geral melhores resultados com os amontoados binários. Os amontoados emparelhados (pairing heaps) [10] foram introduzidos como uma alternativa aos amontoados de Fibonacci. Esta estrutura de dados garante para todas as operações tempo O(log n) amortizado e, conjecturava-se, tempo O(1) amortizado para a operação decreasekey. No entanto em trabalhos posteriores provou-se que o tempo amortizado para a operação decreasekey é O(2 2 log log n ) [11], verificando-se ainda Ω(log log n) [12]. Recentemente, Elmasry [13] introduziu uma variante de amontoados emparelhados com um tempo O(log log n) amortizado para a operação decreasekey. A vantagem em relação aos amontoados de Fibonacci reside no facto de serem mais competitivos na prática. Driscoll et al. [14] propuseram duas estruturas de dados, rank relaxed heaps e run relaxed heaps, ambas com base nos amontoados binomiais. Tal como os amontoados de Fibonacci, a técnica consiste em relaxar os amontoados binomiais. Porém neste caso as árvores binomiais são completas e a relaxação verifica-se ao nível da propriedade dos amontoados, i.e., um número limitado de nós pode ter uma chave menor que o seu pai. Os rank relaxed heaps têm a mesma complexidade assimptótica amortizada que os amontoados de Fibonacci. Os run relaxed heaps suportam as operações remove, removemin e meld em tempo O(log n) no pior caso e todas as outras operações em tempo O(1) no pior caso. Brodal [15] obteve ainda uma estrutura de dados que suporta as operações de remoção em tempo O(log n) no pior caso e todas as outras operações em tempo O(1) no pior caso. No entanto é uma estrutura de dados bastante complexa quando comparada com os amontoados relaxados propostos por Driscoll et al.. Com vista a melhorar os tempos de execução das operações dos amontoados relaxados, nomeadamente o número de comparações, Elmasry et al. apresentou recentemente uma nova estrutura, two-tier relaxed heaps, composta por dois run relaxed heaps [16]. Na última década, o foco tem sido a melhoria das estruturas de dados de modo a conseguir-se na prática implementações competitivas, continuando a garantir os limites assimptóticos teóricos para o tempo de execução das operações [17, 18, 19, 20]. Os principais objectivos são reduzir o número instruções para cada operação, em particular o número de comparações [21], e reduzir os requisitos de memória.

1.1 Estado da Arte 5 Ainda que se tenham verificado todos estes esforços para obter implementações eficientes de filas com prioridade, a maior parte das bibliotecas disponíveis não implementam nenhuma destas estruturas mais avançadas. No que respeita à implementação do tipo de dados abstracto fila com prioridades, a maior parte das implementações não suporta a operação decreasekey directamente e são baseadas em amontoados binários. É claro que é possível implementar um fila de prioridades sobre o tipo de dados abstracto mapa ordenado disponibilizado em quase todas as bibliotecas e que, regra geral, é implementado sobre árvores binárias de pesquisa balanceadas ou sobre amontoados binários. Em ambos os casos o custo das operações insert, removemin e decreasekey é O(log n), não garantindo os limites assimptóticos das estruturas de dados mais avançadas para as operações insert e decreasekey.

2 Amontoados Relaxados Os amontoados relaxados foram propostos por Driscoll et al. [14] como uma alternativa aos amontoados de Fibonacci [8]. Quer os amontoados relaxados quer os amontoados de Fibonacci são baseados em amontoados binomiais e, com vista a melhorar o tempo de execução das operações, introduzem relaxações ao nível das árvores binomiais. A principal diferença entre ambos reside precisamente na forma como são feitas as relaxações. Enquanto que os amontoados de Fibonacci introduzem relaxações ao nível da estrutura das árvores binomiais, os amontoados relaxados consideram árvores binomiais completas e introduzem relaxações ao nível da ordem dos nós. Esta última abordagem, para além de permitir implementações mais eficientes na prática, permite concretizar as operações remove, removemin e meld em tempo O(log n) no pior caso e todas as outras operações em tempo O(1) no pior caso também, em particular a operação decreasekey. Driscoll et al. propuseram duas estruturas de dados, rank relaxed heaps e run relaxed heaps. Ambas as estruturas de dados garantem os tempos anteriores para a execução das operações, contudo no primeiro caso os tempos são amortizados tal como acontece com os amontoados de Fibonacci. No que se segue apresentam-se e discutem-se em detalhe ambas as concretizações de amontoados relaxados. Antes é no entanto necessário introduzir árvores binomiais e alguma notação. 2.1 Árvores Binomiais Antes de definir árvore binomial é necessário rever alguns conceitos relativos a árvores em geral. Uma árvore é um grafo ligado, não dirigido e acíclico, i.e., um par (V, E) com V o conjunto de vértices ou nós e E V V o conjunto de arcos tal que existe um e um só

2.1 Árvores Binomiais 8 x 1 y 30 10 5 8 15 22 25 15 11 13 27 22 21 16 36 Figura 2.1. À esquerda, construção de uma árvore binomial Br+1 a partir de duas árvores binomiais Br. À direita, exemplo de uma árvore binomial B 4, i.e., com grau 4. caminho (constituído por vértices distintos) entre cada par de vértices. Uma árvore com raiz é uma árvore em que um vértice, designado por raiz, é distinguido dos outros. Considere-se uma árvore com raiz. Dado x um nó da árvore, designa-se por ascendente de x qualquer nó y que se encontre no caminho entre o nó x e a raiz r. Neste caso x é também um descendente de y. Dado um nó y e um nó x descendente de y e adjacente a y, x diz-se filho de y e y pai de x. Note-se que a raiz é o único nó sem pai. Dado um nó x, a sub-árvore de raiz x é a árvore induzida pelos descendentes de x, com raiz x. Um nó designa-se por folha ou terminal se não tiver descendentes, caso contrário designase por nó interno ou não terminal. Qualquer nó não terminal distinto da raiz pode também designar-se por nó intermédio. O grau ou rank de um nó é igual ao seu número de filhos e uma árvore diz-se n-ária se tiver pelo menos um nó de grau n. Dada uma árvore com raiz, define-se o nível para cada nó e a altura da árvore. Um nó x diz-se estar no nível n se o caminho entre a raiz r e o nó x tiver n arcos. Uma árvore tem altura h se o máximo dos níveis das suas folhas for h. Dada uma ordem total para os nós, uma árvore com raiz diz-se ordenada se os filhos de cada nó estiverem ordenados. Neste caso, se x tiver grau n, os seus filhos são identificados por primeiro filho, segundo filho,..., n-ésimo filho. Neste documento o n-ésimo filho é também designado por último filho do nó x. As árvores binomiais definem-se recursivamente como se segue: a árvore binomial B 0 consiste num único nó, a sua raiz; a árvore binomial +1 é composta por duas árvores binomiais em que o nó raiz de uma é filho do nó raiz da outra. Na Figura 2.1, à esquerda, ilustra-se esta construção. Por construção, uma árvore binomial tem 2 r nós, altura r e a sua raiz tem grau r. Estes factos podem ser facilmente provados por indução em r.

2.2 Amontoados Relaxados 9 No contexto deste documento as árvores binomiais são ordenadas: os filhos de cada nó são ordenados por ordem crescente do grau. Uma árvore binomial é também designada por árvore binomial de grau r. A designação destas árvores advém do facto de existirem ( ) r l nós com nível l. 2.2 Amontoados Relaxados Um amontoado é uma estrutura de dados baseada em árvores com raiz que satisfaz a seguinte propriedade: a chave de um nó (mais precisamente, a chave do elemento associado a um nó) é maior ou igual à chave do seu nó pai. Esta propriedade pressupõe uma ordem total para as chaves e, quando é satisfeita, diz-se que a árvore é ordenada em amontoado. Esta estrutura de dados ganha mais relevância com o facto de permitir implementações eficientes de filas com prioridade. Nesta secção discute-se em detalhe as duas versões de amontoados relaxados propostos por Driscoll et al. [14], rank relaxed heaps e run relaxed heaps, ambas com base nos amontoados binomiais. Os rank relaxed heaps suportam as operações com a mesma complexidade assimptótica amortizada que os amontoados de Fibonacci, enquanto que os run relaxed heaps suportam as operações remove, removemin e meld em tempo O(log n) no pior caso e todas as outras operações em tempo O(1) no pior caso. Para além dos amontoados relaxados serem a primeira concretização de amontoados a garantir estes limites assimptóticos no pior caso, constituem também a base para o desenvolvimento de estruturas mais eficientes como os two-tier relaxed heaps propostos recentemente por Elmasry et al. [16], compostos por dois run relaxed heaps. Os amontoados relaxados são constituídos por uma colecção de árvores binomiais ordenadas em amontoado. Porém, para garantir o tempo O(1) para algumas operações como o decreasekey, os amontoados relaxados permitem a existência de nós que não satisfazem a propriedade de ordenação de amontoado, designados por nós maus. É importante notar que o número de nós maus não pode ser arbitrário tendo em conta que se pretende que a operação removemin ocorra em tempo O(log n). Portanto, uma vez que esta operação implica determinar o nó com menor chave e um nó mau pode ter a menor a chave, o número de nós maus não poderá exceder O(log(n)). Quer os rank relaxed heaps quer os run relaxed heaps não admitem mais do que log(n) nós activos, nos quais se incluem os possíveis nós maus. Para garantir esta propriedade é necessário um conjunto de transformações para gerir eficientemente a estrutura de dados.

2.2 Amontoados Relaxados 10 0 1 7 7 2 10 5 10 9 0 12 13 12 11 14 Figura 2.2. Exemplo de um amontoado relaxado com chaves inteiras. Este amontoado tem 4 árvores binomiais e 2 nós activos, os nós com chave 5 e 0. Note-se que as árvores binomiais são ordenadas por grau, i.e., os filhos de cada nó estão ordenados por grau, e estão ordenadas em amontoado à excepção dos nós activos. Embora os rank relaxed heaps sejam menos flexíveis quanto à ocorrência de nós maus, as transformações são idênticas. Deste modo começar-se-á por explicar as transformações e a concretização das operações para os rank relaxed heaps. De seguida discute-se a adaptação das transformações para os run relaxed heaps. No final discutem-se alguns detalhes importantes para a implementação eficiente dos amontoados relaxados. 2.2.1 Rank Relaxed Heaps Um rank relaxed heap é constituído por um conjunto de árvores binomiais, onde existem nós que são distinguidos por activos, e que satisfazem as seguintes propriedades: 1. existe no máximo uma árvore binomial no amontoado cuja raiz tenha um dado grau; 2. cada árvore binomial é ordenada em amontoado, embora possa existir no máximo um nó activo com grau r para cada grau r em toda a colecção; 3. cada nó activo no amontoado tem de ser o filho de maior grau de algum nó; 4. cada nó de uma árvore binomial no amontoado tem os filhos ordenados por grau. Na Figura 2.2.1 mostra-se um exemplo de um rank relaxed heap em que os nós com chave 5 e 0 são activos e, neste caso, maus. Note-se que estes nós violam a propriedade de ordenação em amontoado uma vez que são menores que os seus pais. Como referido acima, os amontoados relaxados são baseados em amontoados binomiais e, de facto, têm a mesma estrutura que os amontoados binomiais: um conjunto de árvores binomiais tal que cada árvore é ordenada em amontoado e existe no máximo uma árvore binomial de cada grau. A principal diferença reside na existência de nós maus, i.e., nós que não respeitam a propriedade de ordem do amontoado. Da primeira propriedade de rank relaxed heap tem-se que existe no máximo uma árvore binomial de grau r na colecção para cada grau r e, dado o número n de elementos no

2.2 Amontoados Relaxados 11 amontoado, podemos determinar quais as árvores na colecção pela análise da representação binária de n. Uma árvore binomial de grau r,, tem 2 r nós. Portanto, considerando a representação binária de n, o número de árvores binomiais será igual ao número de bits a 1. Note-se que a representação binária de n tem log(n) + 1 bits e n = log(n) i=0 b i 2 i, onde b i é o i-ésimo bit na representação binária de n. Logo, a árvore binomial B i surge no amontoado se e só se b i = 1. Por exemplo, se considerarmos um amontoado com 6 nós, em binário 110, existem duas árvores binomiais B 1 e B 2 com 2 e 4 nós respectivamente. Esta análise implica ainda que num amontoado com n elementos existem no máximo log(n) +1 árvores binomiais. A segunda propriedade permite inferir que existem no máximo log(n) nós activos. Da análise acima da representação binária do número n tem-se que B log(n) é a maior árvore possível no amontoado. Uma vez que todos os nós têm grau inferior ao seu pai e que o nó raiz da árvore B log(n) tem o maior grau, log(n), tem-se que todos os nós têm no máximo grau log(n). Por outro lado, pela segunda propriedade, tem-se que existe no máximo um nó activo por cada grau. Logo, existem no máximo log(n) nós activos. As terceira e quarta propriedades são determinantes para a implementação das operações sobre o amontoado com os tempos de execução mencionados acima. Em particular permitem localizar os nós activos e realizar várias transformações no amontoado de forma eficiente. Estas propriedades serão analisadas em maior detalhe na concretização das operações. Antes de definir as operações, é necessário introduzir alguns detalhes a respeito da representação dos rank relaxed heaps. Com vista à simplificação da descrição das operações, assuma-se que existe um nó auxiliar head cuja chave terá sempre o menor valor possível, i.e., key[head] < key[a] qualquer que seja o nó a, e que tem como filhos todos os nós raiz das árvores binomiais no amontoado, ordenados por grau. Desta forma nenhum destes nós poderá vir a ser activo, todos os nós no amontoado têm nó pai e, importante em algumas situações que se seguem, todos os nós activos têm nó avô definido. Em relação aos nós, cada nó a tem os seguintes atributos: 1. key[a], a chave do elemento associado ao nó a; 2. parent[a], o nó pai do nó a; 3. rank[a], o grau do nó a; 4. child[a][r], o filho do nó a com grau r, em que 0 r < rank[a]. É ainda necessário guardar o nó activo active[r] para cada grau r, caso exista, e uma referência min para o nó com a chave mínima. Nota-se que estas são definições abstractas dos atributos e que não pressupõem a utilização de uma estrutura de dados particular. No

2.2 Amontoados Relaxados 12 entanto é indispensável que os atributos sejam acessíveis em tempo O(1) e a implementação terá de garantir isso. Na Secção 2.3 discutir-se-ão os detalhes a ter em conta para uma implementação eficiente das estruturas de dados. Um rank relaxed heap é inicializado da seguinte forma: makerankrelaxedheap(n) 1 n é a capacidade deste amontoado 2 min nil 3 key[head] 4 parent[head] nil 5 rank[head] 0 6 for r 0 to log(n) 7 do child[head][r] nil 8 active[r] nil Operação decreasekey Os rank relaxed heaps permitem executar a operação decreasekey em tempo O(1) amortizado. Esta operação será a mais difícil de concretizar uma vez que, ao diminuir-se a chave de um elemento do amontoado, pode criar-se um novo nó mau e portanto activo. Este facto implica que uma única operação de diminuição de chave pode fazer com que as propriedades 2 e 3 se deixem de verificar. A dificuldade é então restaurar estas duas propriedades garantindo um tempo de execução O(1) amortizado. Considere-se um nó a ao qual se diminui a chave key[a]. É importante salientar que caso a seja raiz de uma árvore binomial do amontoado, a nunca se tornará num nó mau, note-se que key[head] =, e portanto nunca se tornará activo. Seja r o grau e p o nó pai do nó a, i.e., rank[a] = r e parent[a] = p. A diminuição do valor da chave key[a] conduz a quatro casos possíveis: 1. key[a] key[p]; 2. key[a] < key[p], a é o último filho e não existe outro nó activo com grau r; 3. key[a] < key[p], a é o último filho e existe outro nó activo com grau r; 4. key[a] < key[p] e a não é o último filho. Os dois primeiros casos resolvem-se sem dificuldade. No primeiro caso o nó a é bom, logo se for activo pode passar a inactivo. Neste caso o nó a não poderia ser mau antes da operação, caso contrário continuaria a ser mau uma vez que a chave diminui. Contudo poderia ainda

2.2 Amontoados Relaxados 13 assim estar marcado como activo devido a operações anteriores, e.g., a chave key[p] pode ter diminuído de valor numa operação anterior. No segundo caso o nó a é mau e portanto terá de ser marcado como activo, o que pode ser feito uma vez que é último filho e não existe outro nó activo com grau r. Portanto, em ambos os casos todas as propriedades de rank relaxed heap são verificadas. O terceiro e quarto casos levantam mais dificuldades, uma vez que é necessário realizar transformações nas árvores para garantir as propriedades. No terceiro caso o nó a não pode ser marcado activo uma vez que existe outro nó activo com grau r e, portanto, a propriedade 2 não se verificaria. No caso quatro o nó a não pode ser marcado activo porque não é o último filho de p, caso contrário a propriedade 3 não se verificaria. Deste modo, uma vez que nos terceiro e quarto casos o nó a é mau e não pode ser marcado activo, é necessário introduzir um conjunto de transformações para reduzir o número de nós activos e garantir que todos os nós activos são últimos filhos. Dado um nó a e um novo valor v para a chave key[a], a operação decreasekey definese da seguinte forma: decreasekey(a, v) 1 if key[a] < v 2 then error a nova chave é maior do que a chave actual 3 else key[a] v 4 update(a) A última instrução, update(a), verifica se a deve ficar activo e realiza as transformações necessárias no amontoado, de forma a restaurar as propriedades de rank relaxed heap. Se key[a] key[p] e a estiver marcado como activo, i.e., active[r] = a, então a deverá ser marcado inactivo, i.e., active[r] = nil. Se key[a] < key[p], a for o último filho de p e não existir nenhum nó activo com grau r, i.e., active[r] = nil, então a deverá ser marcado como activo, i.e., active[r] = a. Se key[a] < key[p] e já se verificar active[r] = a, então não é necessário fazer nada. Nos outros casos em que key[a] < key[p] é necessário realizar uma ou mais das seguintes transformações: 1. pairtransform, aplica-se quando a é o filho de maior grau, i.e., o último filho, e existe um nó activo com grau r diferente de a; 2. goodsiblingtransform, aplica-se quando a não é último filho e o irmão direito s, i.e., s = child[p][r + 1], não é activo, i.e., active[r + 1] s; 3. activesiblingtransform, aplica-se quando a não é último filho e o irmão direito é activo, i.e., active[r + 1] = s.

2.2 Amontoados Relaxados 14 Note-se que a só será marcado activo após a execução das transformações, quando não existir outro nó activo com grau r. O procedimento update define-se da seguinte forma: update(a) 1 r rank[a] 2 p parent[a] 3 if key[a] key[p] 4 then if active[r] = a 5 then active[r] nil 6 else key[a] < key[p] 7 if r = rank[p] 1 a é o último filho de p 8 then if active[r] = nil 9 then active[r] = a 10 else if active[r] a 11 then pairtransform(a) 12 else a não é o último filho de p 13 s child[p][r + 1] s é o irmão direito de a 14 if active[r + 1] = s se s é activo 15 then activesiblingtransform(a) 16 else goodsiblingtransform(a) No caso da transformação pairtransform tem-se que a é um nó mau e que existe um nó activo b com grau r, em que ambos são últimos filhos. O objectivo desta transformação é reduzir o número de nós activos em pelo menos um nó por forma a resolver o conflito entre o nó a e o nó activo com grau r. Na Figura 2.3 ilustra-se a execução da transformação pairtransform. Sejam pa o nó pai de a, pb o nó pai de b, ga o nó avô de a, i.e., nó pai de pa, e gb o nó pai de b, i.e., nó pai de pb. Em primeiro lugar os nós a e b são removidos do amontoado, constituindo duas árvores binomiais de grau r com raízes a e b, e são combinadas para formar uma árvore binomial de grau r +1. Sem perda de generalidade assuma-se que key[a] > key[b]. A árvore resultante terá grau r + 1, raiz b e o último filho b será o nó a. Em segundo lugar, uma vez que os nós pa e pb tinham grau r + 1 antes de perderem o último filho (agora têm grau r), a árvore com raiz b irá substituir um deles. Assuma-se sem perda de generalidade que key[pa] < key[pb]. Então, caso pb seja um nó activo, pb é marcado inactivo e, enquanto filho do nó gb, é substituído pelo nó b. Finalmente o nó pb é colocado como o filho de grau r do nó pa. Importa referir que estas operações

2.2 Amontoados Relaxados 15 a) 4 ga pa 5 7 12 14 20 11 1 24 a 1 30 10 15 5 22 25 27 8 15 gb 3 11 13 22 21 pb 2 10 b 36 ga b) 4 5 7 12 pa 14 20 13 1 21 pb 1 30 10 15 5 22 25 27 8 15 gb 3 11 10 22 36 b 2 11 a 24 Figura 2.3. Aplicação da transformação pairtransform ao nó a cuja chave foi diminuída para 11. O nó b é um nó activo com o mesmo grau que o nó a. A numeração dos cortes e uniões ilustra uma ordem possível das operações. não afectam os restantes nós do amontoado e que ou a deixa de ser mau ou b deixa de ser activo. Portanto, tendo em conta que a deveria de ser marcado activo, o número de nós activos foi reduzido em pelo menos um. Este facto deve-se a que ao combinar os nós a e b garante-se que um deles não é mau. A transformação pairtransform executa ainda update(b) uma vez que b pode ser um nó mau em relação a gb. Em particular, dado que pb não era necessariamente o último filho do nó gb, b pode não ser o último filho e, por outro lado, pode existir outro nó activo com grau r + 1. Logo, podem ser necessárias outras transformações. A Figura 2.4 ilustra o caso geral da transformação pairtransform e a Figura 2.5 exemplifica o caso em que o resultado de combinar os nós a e b produz um nó activo. Adiante ver-se-á como resolver este caso. A transformação pairtransform define-se da seguinte forma:

2.2 Amontoados Relaxados 16 pairtransform(a) 1 r rank[a] 2 b active[r] 3 active[r] nil 4 pa parent[a] 5 pb parent[b] 6 ga parent[pa] 7 gb parent[pb] 8 c combine(a, b) 9 if key[pa] key[pb] 10 then child[pa][r] pb tornar pb o último filho de pa 11 parent[pb] pa 12 child[gb][r + 1] c substituir pb enquanto filho de gb por c 13 parent[c] gb 14 rank[pb] rank[pb] 1 15 if active[r + 1] = pb 16 then if key[c] < key[gb] 17 then active[r + 1] = c 18 else active[r + 1] = nil 19 else update(c) 20 else child[pb][r] pa tornar pa o último filho de pb 21 parent[pa] pb 22 child[ga][r + 1] c substituir pa enquanto filho de ga por c 23 parent[c] ga 24 rank[pa] rank[pa] 1 25 if active[r + 1] = pa 26 then if key[c] < key[ga] 27 then active[r + 1] = c 28 else active[r + 1] = nil 29 else update(c) Na definição da transformação pairtransform, linha 8, é invocada a operação combine. Dados dois nós a e b com o mesmo grau r, esta operação retorna uma árvore com grau r + 1 que resulta de combinar a sub-árvore com raiz a e a sub-árvore com raiz b.

2.2 Amontoados Relaxados 17 ga pa gb pb k(pa)<k(pb) ga pa gb c a b B pb r B d r Figura 2.4. Caso geral da transformação pairtransform. Os nós c e d são a ou b mediante a comparação da chaves key[a] e key[b]. ga pa gb c) 4 a 1 5 2 12 1 30 10 3 5 pb 8 14 20 13 15 2 22 0 b 15 11 13 21 27 22 36 14 24 ga d) 4 pa 1 3 1 gb 5 5 12 pb 22 20 13 30 10 15 0 b = c 2 22 2 a 8 15 11 13 21 14 22 36 14 24 Figura 2.5. Aplicação da transformação pairtransform ao amontoado b) da Figura 2.3 após uma operação decreasekey. Primeiro a chave do nó b passou de 25 para 0, constituindo um nó activo, e numa segunda operação a chave do nó a passou de 7 para 2. Uma vez que a e b têm o mesmo grau, é necessário aplicar a transformação pairtransform ao nó a conduzindo ao amontoado d). Neste caso ga = head e o nó c, que resulta de combinar a e b, fica activo e requer outra transformação. Note-se que c não é o último filho. Se key[a] key[b], então a árvore retornada terá como raiz o nó a e o nó b passa a ser o último filho de a. Se key[a] > key[b] verifica-se o oposto. Porém esta operação pode fazer com que a propriedade 3 não se verifique. Assuma-se, sem perda de generalidade, que key[a] key[b]. Se antes da operação combine o último filho do nó a for activo, i.e., active[r 1] = child[a][r 1], após a operação esse nó continuará activo mas já não será o último filho uma vez que o grau do nó a passou a ser r + 1 e o último filho é o nó b. Nestas

2.2 Amontoados Relaxados 18 c c xc d xd d xd xa Figura 2.6. Aplicação da operação clean ao nó xc. situações é necessário aplicar a operação clean que permuta o filho activo de a com grau r 1 com o filho r 1 de b. Note-se que se o filho de a com grau r 1 for activo, o filho de b com grau r 1 não pode ser activo pela propriedade 2. Portanto a operação clean produz sempre o efeito desejado, restaurando a propriedade 3. A Figura 2.6 ilustra esta operação no caso geral. A operação combine define-se da seguinte forma: combine(a, b) 1 a e b são dois nós com o mesmo grau r 2 if key[a] key[b] 3 then child[a][r] b 4 parent[b] a 5 rank[a] rank[a] + 1 6 c a 7 else child[b][r] a 8 parent[a] b 9 rank[b] rank[b] + 1 10 c b 11 clean(child[c][r 1]) 12 return c Considere-se agora a transformação activesiblingtransform invocada na operação update, linha 15. Esta transformação aplica-se quando o nó a é mau, não é o último filho e o seu irmão direito é activo. Recorde-se que p = parent[a] é o pai de a, r = rank[a] é o grau de a e s = child[p][r + 1] é o irmão direito de a. Pela propriedade 3 tem-se que s é o último filho de p e, portanto, p tem grau r + 2 uma vez que a tem grau r e s tem grau r + 1. Seja g = parent[p] o nó pai de p, avô de a. A Figura 2.7 ilustra a sequência de operações da transformação activesiblingtransform. Começa-se por remover o nó mau a e nó activo s do amontoado. De seguida remove-se o nó p do amontoado que agora

2.2 Amontoados Relaxados 19 g k(a) > k(s) s g p a s +1 a p +1 g k(a) < k(s) a p s +1 Figura 2.7. Caso geral da transformação activesiblingtransform. tem grau r uma vez que perdeu os dois filhos de maior grau. Em terceiro lugar aplicam-se duas vezes a operação combine. Primeiro aos nós p e a com grau r dando origem a uma árvore de grau r + 1 com raiz a. Neste caso a raiz é a porque a era um nó mau, i.e., key[a] < key[p]. A árvore de grau r + 1 é combinada de seguida com a sub-árvore com raiz s dando origem a uma árvore com grau r + 2. Finalmente a raiz c da nova árvore de grau r + 2 passa a ser o filho de grau r + 2 de g. Tal como na transformação pairtransform é necessário verificar se o nó c é mau, i.e., se key[c] < key[g]. E da mesma forma que na transformação pairtransform, basta invocar update(c) no final. É importante notar que esta transformação também reduz o número de nós activos em pelo menos um. No início existiam dois nós activos, o nó mau a e o nó activo s, e no final apenas c poderá vir a ser activo. Todos os outros nós do amontoado permanecem inalterados. A transformação activesiblingtransform define-se da seguinte forma:

2.2 Amontoados Relaxados 20 activesiblingtransform(a) 1 r rank[a] 2 p parent[a] 3 g parent[p] 4 s child[p][r + 1] 5 active[r + 1] nil s era activo, i.e., active[r + 1] = s 6 rank[p] rank[p] 2 p perde os dois filhos de maior grau 7 a combine(a, p) como a era mau, i.e., key[a] < key[p], a raiz continua a ser a 8 c combine(a, s) c é raiz de uma árvore de grau r + 2 9 child[g][r + 2] c 10 parent[c] g 11 if active[r + 2] = p depois de combinar p com a, p já não pode ser activo 12 then active[r + 2] = nil 13 update(c) A operação update depende ainda da transformação goodsiblingtransform. Esta transformação aplica-se quando o nó a é mau, não é o último filho e o irmão direito não é activo. Como nos casos anteriores, p = parent[a] é o pai de a, r = rank[a] é o grau de a, s = child[p][r + 1] é o irmão direito de a e g = parent[p] o pai de p. Ao contrário do que acontece na transformação activesiblingtransform, aqui s pode não ser o último filho. A transformação começa por distinguir dois casos. A Figura 2.8 ilustra a transformação goodsiblingtransform no caso geral. Se o último filho c de s não for activo, basta aplicar a operação clean ao nó a e este permuta com o nó c, passando a ser um último filho de s. Caso exista outro nó activo com grau r, é ainda necessário aplicar a transformação pairtransform ao nó a. Na prática aplica-se a operação update ao nó a após a operação clean. Este é o caso do amontoado d) da Figura 2.5, em que é necessário aplicar a transformação goodsiblingtransformation. No segundo caso o nó c é activo e portanto basta aplicar a transformação pairtransform ao nó a, garantindo que s continua a ser filho de p. Note-se que neste caso o pai de s e avô de c é o nó p, logo à partida poderia haver problemas com a aplicação da operação pairtransform. Porém s não é activo e portanto é um nó bom, i.e., key[p] < key[s], sendo que no resultado da transformação pairtransform o nó s continua a ser filho do nó p. Apenas é necessário garantir que s também continua a ser filho de p quando key[p] = key[s], o que se verifica na definição da transformação pairtransform, linha 9. É importante referir que, embora

2.2 Amontoados Relaxados 21 a) g p a s... g p c s... c a b) g p... COMBINE s... a s a c c g p Figura 2.8. Caso geral a), quando o nó c não é activo, e caso geral b), quando o nó c é activo, da transformação goodsiblingtransform. no segundo caso a transformação pairtransform garante que o número de nós activos é reduzido em pelo menos um, no primeiro caso pode não ocorrer redução do número de nós activos. No entanto, se a operação update não reduzir o número de nós activos no primeiro caso, é porque não existe outro nó activo com grau r e as propriedades de rank relaxed heap são válidas. A transformação goodsiblingtransform define-se da seguinte forma: goodsiblingtransform(a) 1 r rank[a] 2 p parent[a] 3 g parent[p] 4 s child[p][r + 1] irmão direito de a 5 c child[s][r] último filho de s 6 if active[r] = c se c for activo 7 then pairtransform(a) 8 else clean(a) 9 update(a) Embora não seja óbvio, a operação decreasekey decorre em tempo O(1) amortizado. Cada uma das transformações descritas opera em tempo O(1) no pior caso, excluindo a possível execução no final de outra transformação. Pela propriedade 2 de rank relaxed heap

2.2 Amontoados Relaxados 22 existem no máximo log(n) nós activos. Portanto, tendo em conta que cada transformação reduz o número de nós activos em pelo menos um e que pode executar outra transformação, a operação decreasekey decorrerá em tempo O(log n) no pior caso. Porém, considere-se m operações decreasekey. Cada uma destas operações pode introduzir no máximo um nó activo ao diminuir a chave do mesmo. Por outro lado cada transformação reduz um nó activo quando é executada. Logo, independentemente do número de transformações que é executado em cada operação decreasekey, em m operações decreasekey apenas podem ser executadas m transformações no máximo admitindo que no início não existiam nós activos. Portanto, m operações decreasekey decorrem em tempo O(m) no pior caso, i.e., a operação decreasekey decorre em tempo O(1) amortizado. Operação removemin A operação removemin encontra e retorna o nó com a menor chave no amontoado. O nó com menor chave ou é uma raiz das árvores binomiais no amontoado, i.e., um dos filhos do nó head, ou é um dos nós activos. Como discutido anteriormente, o número de árvores binomiais no amontoado é no máximo log(n) +1 e o número de nós activos no amontoado é no máximo log(n). Logo o nó com menor chave pode ser encontrado em tempo O(log(n)), bastando percorrer os filhos do nó head e os nós activos. Aqui pressupõe-se que é possível enumerar os nós activos de forma eficiente, ver-se-á adiante como fazer. Após determinar o nó com a menor chave, denotado por nó min, este será removido do amontoado. A Figura 2.9 ilustra a eliminação do nó min no caso deste se encontrar na raiz de uma árvore binomial no amontoado, i.e., quando parent[min] = head. Primeiro retira-se o nó raiz y no amontoado com menor grau e adicionam-se todos os seus filhos como filhos do nó head, i.e., os filhos de y passam a ser raízes do amontoado. Note-se que esta operação não produz árvores binomiais com o mesmo grau no amontoado, basta observar que todos os nós filhos de y têm grau distinto e inferior ao grau de y e y é a raiz com menor grau. Logo, as propriedades de rank relaxed heap permanecem válidas. Caso o nó min seja o próprio y, a operação removemin está concluída. Caso min y é necessário recombinar o nó y com os filhos do nó min e substituir o nó min no amontoado pela árvore resultante da recombinação. Na recolocação dos filhos de y e na operação de recombinação de y com os filhos do nó min dever-se-á desmarcar os nós que estejam activos e que deixam de o ser. É ainda necessário verificar se a substituição do nó min dá origem a um novo nó activo. Uma vez que o número de filhos de um nó é no máximo log(n) e que a operação combine decorre em tempo O(1) no pior caso, tem-se que estas operações

y... min... 2.2 Amontoados Relaxados 23 y... y x... 0 r 0 x k B k y... y... 0 r COMBINE y x0... x k Figura 2.9. Operação removemin quando o nó min é a raiz de uma das árvores binomiais no amontoado. Quando o nó min é um dos nós activos a operação é idêntica. decorrem em tempo O(log n) no pior caso. Repare-se que a operação removemin não aumenta o número de nós activos e, portanto, não afecta o tempo amortizado da operação decreasekey. A operação removemin define-se da seguinte forma:

2.2 Amontoados Relaxados 24 removemin() 1 if rank[head] = 0 se o amontoado for vazio 2 then return nil 3 min head 4 y nil 5 for r = 0 to log(n) procurar o nó min e o filho y do nó head com menor grau 6 do x child[head][r] filho de grau r do nó head 7 if x nil key[min] > key[x] 8 then min x 9 if y = nil verificar se y já foi encontrado 10 then y x 11 x active[r] nó activo de grau r 12 if x nil key[min] > key[x] 13 then min x 14 for r = 0 to rank[y] adicionar todos os filhos de y ao nó head 15 do child[head][r] child[y][r] 16 parent[child[y][r]] head 17 if active[rank[y] 1] = child[y][rank[y] 1] se o último filho de y for activo 18 then active[rank[y] 1] = nil 19 rank[y] 0 20 p parent[min] 21 if active[rank[min]] = min se min for um nó activo 22 then active[rank] nil 23 if min y 24 then for r = 0 to rank[min] recombinar y com todos os filhos do nó min 25 do y combine(y, child[min][r]) 26 child[p][rank[min]] y 27 parent[y] p 28 if key[y] < key[p] 29 then active[rank[y]] = y isto só ocorre se o nó min era activo 30 else active[rank[y]] = nil 31 return min

2.2 Amontoados Relaxados 25 Outras Operações Para além das operações já descritas, os rank relaxed heaps também suportam as operações minimum e insert com tempo O(1) no pior caso e as operações remove e meld em tempo O(log(n)) no pior caso. Nesta Secção ver-se-á como concretizar as operações minimum, insert e meld. A operação remove é em tudo idêntica à operação removemin, apenas o nó é dado como argumento em vez de ser o nó min. A operação minimum pode ser facilmente definida com tempo O(1) no pior caso alterando a definição da operação decreasekey por forma a manter o nó min actualizado. Nesse caso a operação deletemin não precisa de procurar o nó com a menor chave no início, mas terá de actualizar o nó com a menor chave depois de eliminar o nó min. Reparese que estas alterações não alteram os tempos das operações decreasekey e deletemin. No primeiro caso basta comparar o nó a actualizar com o nó min actual e no segundo caso basta fazer a procura no final em vez de no início. Da mesma forma a operação insert terá de actualizar o nó min aquando da inserção de um novo nó. Com estas alterações o nó min será sempre o nó com a menor chave e a operação minimum só tem de retornar esse nó, o que é feito em tempo O(1) no pior caso. Definir a operação insert com tempo O(log(n)) no pior caso é relativamente simples. Cria-se um novo nó x e, enquanto existir uma raiz y no amontoado, i.e., y é filho do nó head, com o mesmo grau que x, substitui-se y pelo resultado de combine(x, y). Uma vez que o número de filhos do nó head é no máximo log(n) + 1 e que a operação combine decorre em tempo O(1) no pior caso, tem-se que esta versão da operação insert decorre em tempo O(log(n)) no pior caso. Veja-se agora como concretizar a operação insert com tempo O(1) no pior caso. A ideia é também recombinar as raízes, contudo será feito de forma gradual. O nó head passa a poder ter filhos com o mesmo grau, de facto poderá ter no máximo dois com o mesmo grau r para cada r. Cada par de raízes com o mesmo grau é mantido numa lista de pares e, sempre que for inserido um novo nó, a operação insert retira um par de raízes da lista de pares e aplica a operação combine reduzindo o número de pares, i.e., o número de raízes com o mesmo grau, em uma unidade. Note-se que desta operação pode surgir um novo par, i.e., pode já existir outra raiz com o mesmo grau. Portanto, o número de pares de raízes com o mesmo grau pode manter-se. Todavia, dado que o número de raízes é finito, sucessivas inserções levarão à diminuição do número de pares que será sempre inferior a log(n) +1. Uma vez que a operação combine decorre em tempo O(1) no pior caso, a operação insert também decorre em tempo O(1) no pior caso. Existe apenas mais um detalhe a ter em conta, a procura do nó com menor chave

2.2 Amontoados Relaxados 26 terá de ser modificada. A forma mais simples é, na operação removemin onde se procura entre as raízes e os nós activos o nó com menor chave, resolver todos os conflitos na lista de pares. Repare-se que isto não altera o tempo de execução da operação removemin pois, embora o número de raízes possa ser agora 2 log(n) + 2, este continua a ser O(log(n)). A operação meld, i.e., a fusão de dois amontoados, pode ser definida com tempo O(log(n)) de forma idêntica à primeira versão da operação insert discutida antes. Portanto, o processo consiste em juntar todas as raízes por ordem crescente de grau e aplicar a operação combine tantas vezes quanto as necessárias. Assumindo-se que os amontoados têm n e n elementos, a operação combine tem ser aplicada log(n) + log(n ) + 1 vezes no pior caso. Logo, a operação meld decorre em tempo O(log(n)) no pior caso. 2.2.2 Run Relaxed Heaps Tal como nos rank relaxed heaps, a ideia é permitir a ocorrência de nós maus no amontoado. Contudo, ainda que continuem a existir no máximo log(n) nós activos, existe maior flexibilidade quanto à sua ocorrência. De facto no caso dos run relaxed heaps os nós activos podem ocorrer livremente no amontoado e não apenas em determinados locais. Esta diferença permitirá definir a operação decreasekey com tempo O(1) no pior caso. Portanto, um run relaxed heap é constituído por um conjunto de árvores binomiais, onde existem nós que são distinguidos por activos, e que satisfazem as seguintes propriedades: 1. existe no máximo uma árvore binomial no amontoado cuja raiz tenha um dado grau; 2. cada árvore binomial é ordenada em amontoado, embora possam existir no máximo log(n) nós activos em toda a colecção; 3. cada nó de uma árvore binomial no amontoado tem os filhos ordenados por grau. Uma vez que os nós activos podem ocorrer livremente no amontoado, podem existir sequências de dois ou mais irmãos activos. Estas sequências são designadas na literatura por runs, daí a designação run relaxed heap. Quando um nó activo não faz parte de uma sequência de nós activos, diz-se que é um nó singular. A principal dificuldade nestes amontoados é gerir os nós activos de forma a garantir os tempos desejados para as operações. Para tal, considere-se a estrutura de dados auxiliar run-singleton: 1. para cada grau r existe uma lista singlelist[r] que contém todos os nós activos singulares com grau r; 2. a lista runlist guarda o último nó, i.e., o nó de maior grau, de cada sequência de nós activos;

2.2 Amontoados Relaxados 27 3. a lista pairlist guarda os graus r para os quais a lista singlelist[r] tem mais de dois elementos, i.e., singlelist[r] 2. Dado que existem no máximo log(n) nós activos, tem-se que esta estrutura requer no máximo O(log(n)) espaço. Contudo, isto implica que uma implementação eficiente utilize, por exemplo, listas duplamente ligadas. Logo, para garantir todas as operações com tempo O(1) é necessário guardar informação a respeito da posição na lista em que cada nó ocorre. Na prática cada nó deverá manter uma referência para a posição na lista, caso ocorra em alguma das listas singlelist[r] ou pairlist. Note-se que um nó não pode ocorrer em simultâneo em ambas as listas. Da mesma forma convém guardar para cada grau r, caso existam mais de dois nós activos com grau r, a referência da sua posição na lista pairlist. Estas referências para os nós das listas são importantes para que se possam eliminar elementos em tempo O(1). Tendo em conta estas observações é então possível concretizar as seguintes operações em tempo constante: 1. addsingleton(x), adiciona o nó x com grau r à lista singlelist[r] e, caso se verifique singlelist[r] 2, adiciona r à lista pairlist; 2. removesingleton(x), remove o nó x com grau r da lista singlelist[r] e, caso se verifique singlelist[r] < 2, remove r da lista pairlist; 3. addrun(x), adiciona o nó x à lista runlist; 4. removerun(x), remove o nó x da lista runlist. Na representação dos run relaxed heaps, cada nó terá os atributos vistos no caso dos rank relaxed heaps e um quinto atributo booleano isactive. Este atributo permite marcar cada nó como activo ou inactivo e determinar se um dado nó é activo em tempo constante. Repara-se que a estrutura run-singleton não guarda todos os nós activos, apenas os singulares e último nó de cada sequência de nós activos, pelo que não permite decidir em tempo constante se um dado nó é activo ou não. A inicialização de um run relaxed heap é em tudo igual à inicialização de um rank relaxed heap, acrescendo apenas a instrução isactive[x] false. Antes de se discutirem as operações sobre o amontoado é útil definir as operações auxiliares setactive e setinactive. Dado um nó a, estas operações actualizam o estado de a e a estrutura de dados run-singleton. Sejam r = rank[a] o grau de a, p = parent[a] o nó pai de a, s = child[p][r + 1] o irmão direito de a e t = child[p][r 1] o irmão esquerdo de a. Note-se que s e t podem não existir, e.g., o nó a pode ser o último filho de p e nesse caso não existe o nó s. A operação setactive começa por marcar o nó a activo e actualiza a estrutura de dados run-singleton da seguinte forma: