• Nenhum resultado encontrado

Representações cache eficientes para índices baseados em Wavelet trees

N/A
N/A
Protected

Academic year: 2021

Share "Representações cache eficientes para índices baseados em Wavelet trees"

Copied!
79
0
0

Texto

(1)

REPRESENTAÇÕES CACHE EFICIENTES PARA ÍNDICES

BASEADOS EM WAVELET TREES

Dissertação de Mestrado

Universidade Federal de Pernambuco [email protected] www.cin.ufpe.br/~posgraduacao

RECIFE 2016

(2)

ISRAEL BATISTA FREITAS DA SILVA

REPRESENTAÇÕES CACHE EFICIENTES PARA ÍNDICES

BASEADOS EM WAVELET TREES

Trabalho apresentado ao Programa de Pós-graduação em Ciência da Computação do Centro de Informática da Univer-sidade Federal de Pernambuco como requisito parcial para obtenção do grau de Mestre em Ciência da Computação.

Orientador: Paulo Gustavo Soares da Fonseca

RECIFE 2016

(3)

S586r Silva, Israel Batista Freitas da.

Representações cache eficientes para índices baseados em Wavelet trees / Israel Batista Freitas da Silva – 2016.

78f.: fig., tab.

Orientador: Paulo Gustavo Soares da Fonseca.

Dissertação (Mestrado) – Universidade Federal de Pernambuco. CIn. Ciência da Computação, Recife, 2016.

Inclui referências e apêndices.

1. Algorítmos computacionais. 2. Entropia. 3. Indexação de textos. I. Fonseca, Paulo Gustavo Soares da. (Orientador). II. Titulo.

(4)

Israel Batista Freitas da Silva

Representações Cache Eficientes para Índices Baseados em Wavelet Trees

Dissertação de Mestrado apresentada ao Programa de Pós-Graduação em Ciência da Computação da Universidade Federal de Pernambuco, como requisito parcial para a obtenção do título de Mestre em Ciência da Computação

Aprovado em: 12/12/2016.

BANCA EXAMINADORA

__________________________________________ Profa. Dra. Katia Silva Guimarães

Centro de Informática / UFPE

__________________________________________ Prof. Dr. Marco Cesar Goldbarg

Departamento de Informática e Matemática Aplicada / UFRN

__________________________________________ Prof. Dr. Paulo Gustavo Soares da Fonseca

Centro de Informática / UFPE

(5)

Primeiramente, gostaria de agradecer a Deus por ter me acompanhado nesta longa caminhada até aqui, me ajudando a superar todos os desafios e obstáculos desta jornada, além de suprir todas as minhas necessidades e por não ter me permitido desistir. A Ele toda honra e toda glória.

Em segundo lugar, agradeço a minha família, em especial minha mãe Josineide, que conviveu comigo por longos 25 anos me suportando e me apoiando em todos os momentos. Agradeço também ao meu orientador Paulo Gustavo pela companhia durante todo o curso, pelas conversas, pelas histórias divertidas, pelo PhDComics e, principalmente, por não desistir de uma pessoa que tem uma dissertação para fazer mas não gosta de escrever. Reconheço a ajuda e amizade de todos os meus amigos, assim como o apoio do meu grande amigo Wellington que me emprestou a máquina na qual pude realizar todos os meus experimentos.

Agradeço ao Centro de Informática por todo apoio, tanto na graduação quanto no mes-trado, possibilitando que meus sonhos fossem realizados, oferencendo uma boa infra-estrutura e professores de qualidade ao longo da minha vida acadêmica, e pelo projeto Maratona de Programação do qual pude participar, aprender e desenvolver minhas habilidades, durante alguns anos.

Gostaria também de agradecer à equipe que desenvolveu o RiSE, permitindo que eu pudesse focar meus esforços muito mais no conteúdo da dissertação, do que na forma, e a Bojian Xu que me deu apoio para compreender um dos artigos referenciados.

Este trabalho foi desenvolvido no âmbito do Projeto Algoritmos e estruturas de dados cache-oblivious e aplicações à Biologia Computacional (MCTI/CNPq/Universal 449842/2014-2, FACEPE APQ-0587-1.03/14).

(6)

Deep in the human unconscious is a pervasive need for a logical universe that makes sense. But the real universe is always one step beyond logic.

(7)

Hoje em dia, há um exponencial crescimento do volume de informação no mundo. Esta explosão cria uma demanda por técnicas mais eficientes de indexação e consulta de dados, uma vez que, para serem úteis, eles precisarão ser manipuláveis. Casamento de padrões se refere à busca de um texto menor (padrão) em um texto muito maior (texto), reportando a quantidade de ocorrências e/ou as localizações das ocorrências. Para tal, pode-se construir uma estrutura chamada índice que pré-processará o texto e permitirá que consultas sejam feitas eficientemente. A eficiência prática de um índice, além da sua eficiência teórica, pode definir o quão utilizado ele será, e isto está diretamente ligado a como ele se comporta nas arquiteturas dos computadores atuais. O principal objetivo deste estudo é analisar o uso da estrutura Wavelet Tree como índice avaliando o impacto da reorganização interna dos seus dados quanto à localidade espacial e, assim propor formas de organização que reduzam efetivamente a quantidade de cache misses ocorridos na execução de operações neste índice.

Através de análises empíricas com dados simulados e dados textuais obtidos de dois repositórios públicos, avaliou-se alguns aspectos de cinco tipos de organizações para os dados da estrutura com o objetivo de compará-las quanto ao tempo de execução e quantidade de cache misses ocorridos. Adicionalmente, uma análise teórica da complexidade da quantidade de cache misses ocorridos para operação de consulta de um padrão é descrita para uma das organizações propostas.

Dois experimentos realizados sugerem comportamentos assintóticos para duas das organizações analisadas. Um terceiro experimento executado mostra que, para quatro das cinco organizações apresentadas, houve uma sistemática redução na quantidade de cache misses ocorridos para a cache de menor nível. Entretanto a redução de cache misses para cache de menor nível não se refletiu integralmente numa diferença no tempo de execução das operações, tendo sido esta menos significativa, nem na quantidade de cache misses ocorridos na cache de maior nível, onde houveram variações positivas e negativas.

Os resultados obtidos permitem concluir que a escolha de uma representação adequada pode acarretar numa melhora significativa de utilização da cache.

Diferentemente do modelo teórico, o custo de acesso à memória responde apenas por uma fração do tempo de computação das operações sobre as Wavelet Trees, pelo que a diminuição no número de cache misses não se traduziu integralmente no tempo de execução. No entanto, este fator pode ser crítico em situações mais extremas de utilização de memória.

Palavras-chave: Algoritmos. Análise de Algoritmos. Casamento de Padrões. Entropia. Estrutura de Dados. Indexação de Textos. Índices de Texto Completo. Wavelet Tree

(8)

Abstract

Today, there is an exponential growth in the volume of information in the world. This increase creates the demand for more efficient indexing and querying techniques, since, to be useful, that data needs to be manageable. Pattern matching means searching for a string (pattern) in a much bigger string (text), reporting the number of occurrences and/or its locations. To do that, we need to build a data structure known as index. This structure will preprocess the text to allow for efficient queries. The adoption of an index depends heavily on its efficiency, and this is directly related to how well it performs on current machine architectures.

The main objective of this work is to analyze the Wavelet Tree data structure as an index, assessing the impact of its internal organization with respect to spatial locality, and propose ways to organize its data as to reduce the amount of cache misses incurred by its operations.

We performed an empirical analysis using both real and simulated textual data to compare the running time and cache behavior of Wavelet Trees using five different proposals of internal data layout. A theoretical analysis about the cache complexity of a query operation is also presented for the most efficient layout.

Two experiments suggest good asymptotic behavior for two of the analyzed layouts. A third experiment shows that for four of the five layouts, there was a systematic reduction in the number of cache misses for the lowest level cache. Despite this, this reduction was not reflected in the runtime, neither in the performance for the highest level cache.

The results obtained allow us to conclude that the choice of a suitable layout can lead to a significant improvement in cache usage. Unlike the theoretical model, however, the cost of memory access only accounts for a fraction of the operations’ computation time on the Wavelet Trees, so the decrease in the number of cache misses did not translate fully into gains in the execution time. However, this factor can still be critical in more extreme memory utilization situations.

Keywords: Algorithms. Analysis of Algorithms. Pattern Matching. Entropy. Data Structures. Text Indexing. Full Text Indexes. Wavelet Tree

(9)

1.1 Árvore de Sufixos para a cadeia abaaba$ . . . 17

1.2 Hierarquia de Memória . . . 20

2.1 Rotações da cadeia abaaba$. . . 22

2.2 Rotações da cadeia abaaba$ ordenadas lexicograficamente . . . 23

2.3 Vetor de Sufixos da cadeia abaaba$ . . . 23

2.4 Primeira e última colunas da BW M. . . 24

2.5 BW M com índices nos símbolos da primeira e da última colunas. . . 24

2.6 BW M parcialmente preenchida . . . 24

2.7 BW M com índices em todos os caracteres da matriz. . . 25

2.8 Busca dos sufixos a e ba . . . 26

2.9 Busca dos sufixos aba e aaba . . . 27

2.10 Representação de uma W T para a cadeia mississipi. . . 28

2.11 Consultando caractere na posição 4 de uma W T . . . 31

2.12 W T balanceada para a cadeia wavelet_tree. . . 33

2.13 Árvores de Huffman . . . 33

2.14 Etapas da primeira fase do algoritmo de Hu-Tucker . . . 35

2.15 Etapas da segunda fase do algoritmo de Hu-Tucker . . . 36

2.16 Árvores de Hu-Tucker . . . 36

2.17 Modelo Cache Ideal . . . 38

3.1 Árvore balanceada para cadeia brown_fox_jumps_over_dog . . . 40

3.2 Árvore apresentando divisões da vEBDemaine . . . 41

3.3 Organização dos nós na memória principal . . . 41

3.4 Árvore com subárvores destacadas . . . 43

3.5 Organização dos vetores binários compondo o vetor binário B . . . 45

4.1 Desempenho (suavizado) na cache L1 para Experimento I . . . 48

4.2 Desempenho (suavizado) na cache LL para Experimento I . . . 49

4.3 Desempenho (suavizado) na cache L1 para Experimento II . . . 50

4.4 Desempenho (suavizado) na cache LL para Experimento II . . . 51

B.1 Árvore apresentando divisões feitas pela organização Busca em Largura. . . 69

B.2 Árvore apresentando divisões feitas pela organização Busca em Profundidade. . 69

B.3 Árvore apresentando divisões feitas pela vEBProkop. . . 70

B.4 Árvore apresentando divisões feitas pela vEBDemaine. . . 70

C.1 Arquivos textuais obtidos de repositórios públicos. . . 71

D.1 Desempenho na cache L1 para Experimento I . . . 73

D.2 Desempenho na cache LL para Experimento I . . . 74

D.3 Desempenho na cache L1 para Experimento II . . . 75

(10)

Lista de Tabelas

1.1 Comparação entre os sufixos e o Vetor de Sufixos da cadeia abaaba$. . . 18

2.1 Frequência dos símbolos na cadeia AAACCGGTTT. . . 34

4.1 Desempenho no tempo médio para Experimento III . . . 53

4.2 p-valores dos desempenhos para o tempo médio no Experimento III . . . 53

4.3 Desempenho na cache L1 para Experimento III . . . 54

4.4 p-valores dos desempenhos para cache L1 no Experimento III . . . 54

4.5 Desempenho na cache LL para Experimento III . . . 55

4.6 p-valores dos desempenhos para cache LL no Experimento III . . . 55

A.1 Frequência dos símbolos na cadeia abracadabra. . . 67

A.2 Frequência dos símbolos com contexto de tamanho 1. . . 68

(11)

α Alfabeto . . . 22

σ Comprimento do alfabeto α . . . 19

B Vetor Binário único. . . 45

BST Árvore Binária de Busca (do Inglês, Binary Search Tree - BST) . . . 48

BW M Matriz de Burrows-Wheeler. . . 22

BW T Transformada de Burrows-Wheeler. . . 22

Z Capacidade total de memória da cache. . . 37

L Capacidade de blocos de memória em uma linha da cache. . . 37

|| Operador de concatenação entre duas sequências. . . 40

H0 Entropia de ordem 0. . . 19

Hk Entropia de ordem k. . . 19

L Função que recebe uma árvore como parâmetro e retorna seus nós em alguma ordem total. . . 40

P Padrão. Cadeia de símbolos que será buscada em um texto. . . 14

SA Vetor de Sufixos . . . 17

T Texto . . . 14

T Árvore . . . 34

H (T) Altura da árvore T. . . 40

vEB Organização van Emde Boas. . . 40

vEBProkop Organização van Emde Boas proposta por Prokop. . . 40

vEBDemaine Organização van Emde Boas proposta por Demaine. . . 40

(12)

Lista de Algoritmos

2.1 Busca de padrões com FM-Index . . . 26

2.2 Construção de uma W T . . . 29

2.3 Construção de uma W T (versão 2) . . . 30

2.4 Consultar o caractere de uma posição da W T . . . 31

2.5 Rank generalizado . . . 32

2.6 Construção de uma árvore de Huffman . . . 34

3.1 Estrutura de Dados da Árvore . . . 42

3.2 Acessar descendente mais à esquerda levels níveis abaixo. . . 42

3.3 Acessar descendente mais à direita levels níveis abaixo. . . 42

(13)

1 Introdução 14

1.1 Pattern Matching . . . 14

1.2 Casamento Exato de Padrões Online x Offline . . . 15

1.2.1 Online . . . 15 1.2.2 Offline . . . 16 1.2.3 Índices Clássicos . . . 16 1.2.3.1 Árvore de Sufixos . . . 16 1.2.3.2 Vetores de Sufixos . . . 17 1.3 Aplicações à Bioinformática . . . 18 1.4 Complexidade de Espaço . . . 19

1.5 Índices Comprimidos e Sucintos . . . 19

1.6 Arquitetura dos Computadores . . . 20

1.7 Objetivos . . . 21 1.8 Visão Geral . . . 22 2 Estado da Arte 23 2.1 Transformada de Burrows-Wheeler . . . 23 2.1.1 A Transformação . . . 23 2.1.2 A Reversão . . . 24 2.1.3 Construção . . . 26 2.1.4 FM Index . . . 26 2.2 Wavelet Tree . . . 28 2.2.1 Definição . . . 29 2.2.2 Construção . . . 29 2.2.3 Operações . . . 31 2.2.4 Partição do Alfabeto . . . 33 2.2.4.1 Balanceado . . . 34 2.2.4.2 Huffman . . . 34 2.2.4.3 Hu-Tucker . . . 34

2.2.5 Codificação dos Vetores Binários . . . 36

2.3 Cache . . . 37

2.3.1 Modelo Cache Ideal . . . 39

3 Desenvolvimento 41 3.1 Construção da BW T . . . 41

3.2 Alfabeto . . . 41

3.3 Codificação dos Vetores Binários . . . 42

3.4 Organização da Árvore . . . 42

3.4.1 Linearidade das Funções de Organização dos Nós . . . 46

3.5 Organização dos Vetores Binários . . . 47

(14)

4 Resultados 49

4.1 Ambiente Experimental . . . 49

4.1.1 Conjunto de Ferramentas Valgrind . . . 49

4.1.2 Ambiente Computacional . . . 49

4.2 Experimento I - Análise do Padrão de Acesso aos nós da W T . . . 50

4.3 Experimento II - Vetores Binários . . . 51

4.4 Experimento III - Busca de Padrões em Dados Textuais Reais . . . 52

5 Conclusão 58 Referências 60 Apêndice 68 A Entropia 69 B Diferentes Organizações dos Nós da Árvore 71 C Dados Textuais Reais 73 C.1 Pré-Processamento . . . 73

(15)

1

Introdução

Hoje em dia, há um exponencial crescimento do volume de informação no mundo, de uma forma geral, assim como o aumento na quantidade de informação em contextos mais espe-cíficos, como: redes sociais (MISLOVE et al., 2008); wikis colaborativas, como a Wikipedia (SPINELLIS; LOURIDAS, 2008; ORTEGA, 2009; DAS; MAGDON-ISMAIL, 2010); e-mails corporativos (COLES et al., 2006); biologia computacional, com as novas técnicas de sequencia-mento que permitem que genomas maiores possam ser totalmente sequenciados (GUT, 2013). Foi estimado que a informação global, hoje, dobraria de tamanho a cada 11 horas, segundo um estudo da IBM (COLES et al., 2006). Outro estudo mais recente afirma que o universo digital dobraria de tamanho a cada dois anos e, em 2020, terá dez vezes o tamanho que tinha em 2013 (TURNER et al., 2014). Esta explosão de dados aumenta a preocupação com relação, tanto ao armazenamento, quanto ao desempenho do processamento dessas informações.

Apesar dos avanços no processamento das máquinas atuais, sua evolução fica muito atrás do crescimento do volume de dados. Esse problema aumenta o interesse em técnicas que permitam o processamento de uma grande quantidade de dados de forma mais eficiente. Entre essas técnicas de processamento, pode-se citar a Indexação, que é uma abordagem na qual cria-se uma estrutura chamada índice para permitir que consultas sobre o conjunto de dados original sejam feitas eficientemente através do índice.

1.1 Pattern Matching

Pattern Matching, ou Casamento de Padrões, é, basicamente, o problema de se procurar um padrão de dados, em um conjunto maior. Um exemplo simples é a busca de um termo em um dicionário, ou em uma enciclopéda. Este problema básico da computação é fundamental em algumas áreas, como casamento de sequências de DNA, detecção de intrusão em redes de computadores, consultas em bancos de dados, detecção de plágio, entre outros (SAIKRISHNA; RASOOL; KHARE, 2012).

Mais formalmente, seja P uma cadeia de caracteres representando um padrão p0p1..pm−1,

e T o texto t0t1..tn−1. Uma representação alternativa, que também será utilizada ao longo deste

trabalho, para o padrão, é P[0..m − 1] e, para o texto, T [0..n − 1]. String matching é o problema de encontrar todas as ocorrências do padrão P em T , ou seja, encontrar todas as posições j em T em que há um match, isto é tj+i=pi, para i = 0,1,...,m − 1.

O casamento de padrões pode ser exato ou aproximado. No casamento de padrões aproxi-mado, é permitido que algumas posições do padrão não sejam iguais a posições correspondentes no texto e, ainda assim, seja considerado um match. Uma ocorrência aproximada na posição j permitindo até k erros, onde o valor de k é definido dependendo da aplicação e contexto, é dada por

(16)

1.2. CASAMENTO EXATO DE PADRÕES ONLINE X OFFLINE 15 onde a função θ representa a distância entre a sequência P e a subsequência consecutiva de T da posição j até j + q − 1, e 0 < q ≤ n − j. As funções de distância entre cadeias mais comuns são distância de Hamming (HAMMING, 1950), dada por

dH(A,B) = m

i=1 g(A[i],B[i]) 1.1 g(a,b) =(0 se a = b 1 caso contrário,  1.2 utilizada para se calcular casamento aproximado com até k mismatches, e distância de Levenshtein (LEVENSHTEIN, 1966) dada por

dL(A = a0. . .an−1,B = b0. . .bm−1) =                |A| se|B| = 0 |B| se|A| = 0 min      dL(A[1..n − 1],B) + 1 dL(A,B[1..m − 1]) + 1 dL(A[1..n − 1],B[1..m − 1]) + g(a0,b0) ,  1.3 utilizada para se calcular casamento aproximado com até k diferenças, onde |S| representa o comprimento da cadeia S, e a função auxiliar g definida na Equação 1.2, adiciona custo 1 se os caracteres forem diferentes, e 0 caso contrário. Como pode-se ver, a função dL calcula a distância

de edição analisando, primeiramente, os caracteres do início das cadeias. As duas primeiras situações ocorrem quando alguma cadeia é vazia, e então o custo será remover ou inserir todos os caracteres da outra cadeia. Os três casos seguintes referem-se à remoção de caracteres de A, inserção de caracteres em B, e substituição de caracteres entre A e B, respectivamente. Note que a substituição de um caractere c pelo mesmo caractere c tem custo 0.

O casamento exato é equivalente ao casamento aproximado com até 0 erros.

1.2 Casamento Exato de Padrões Online x Offline

Os algoritmos de casamento exato de padrões podem ser divididos em duas categorias: (1) Online, quando o algoritmo não conhece o texto a priori, e (2) Offline, quando o algoritmo conhece o texto a priori. A diferença é que algoritmos offline pré-processam o texto para otimizar as buscas, ao contrário dos algoritmos online, que não realizam nenhum tipo de pré-processamento no texto.

1.2.1 Online

Assim como descrito em MICHAILIDIS; MARGARITIS (2001), os algoritmos online para casamento de padrão consistem de duas fases: (1) Pré-processamento do padrão P e construção de uma estrutura X, e (2) Processamento do texto T utilizando a estrutura X para encontrar as ocorrências do padrão. Autores classificam estes algoritmos em quatro categorias:  Clássica: Baseia-se na comparação de caracteres entre o padrão e o texto, mas a maioria dos algoritmos utilizam idéias e heurísticas para evitar um número quadrático de comparações. Como exemplos, temos os algoritmos Knuth-Morris-Pratt (KNUTH; MORRIS; PRATT, 1977), e Boyer-Moore (BOYER; MOORE, 1977).

(17)

 Autômato: Utiliza um autômato como estrutura para realizar a busca do padrão no texto. Como exemplos, temos o algoritmo Aho-Corasick (AHO; CORASICK, 1975) que cria uma máquina de estados finita do padrão a ser procurado e após calcular uma informação adicional chamada de função de falha, realiza a busca dos padrões com custo linear, e o algoritmo Reverse Factor (LECROQ, 1992) que cria um autômato de sufixos do padrão revertido. O Aho-Corasick permite que vários padrões sejam buscados ao mesmo tempo mantendo o custo linear do algoritmo.

 Manipulação de Bits: Baseia-se intensivamente no uso de manipulações de bits para realizar as comparações entre o padrão e o texto. Como muitas das operações realizadas podem ser executadas em paralelo, algoritmos desta categoria podem tirar um maior proveito de uma arquitetura que permita a realização de operações parale-lamente. Como exemplo, temos o algoritmo Shift-Or (BAEZA-YATES; GONNET, 1992).

 Hashing: Baseia-se na comparação dos valores hashing das cadeias de caracteres na tentativa de obter um custo constante ao realizar a comparação entre elas. Tentativa, pois, como o número de cadeias distintas possíveis, é, na prática, bem maior que o limite dos valores para o hashing, logo, mais de uma cadeia distinta corresponderá ao mesmo valor hash. Então, quando o valor hash é igual, é necessário conferir se as cadeias são, realmente, iguais. Como exemplo, temos o algoritmo Rabin-Karp (KARP; RABIN, 1987).

1.2.2 Offline

O desempenho de busca dos algoritmos online depende fortemente do tamanho do texto, uma vez que, dado que o texto é desconhecido, será necessário, no mínimo, percorrer todo o texto, adicionando complexidade O(n) ao custo da busca, onde n representa o tamanho do texto. Os algoritmos offline pré-processam o texto, de forma que o custo de percorrer o texto é adicionado apenas na fase de pré-processamento. Se houver um número grande de buscas por padrões, o maior custo na fase de pré-processamento é compensado pelo baixo custo da operação de busca.

1.2.3 Índices Clássicos

Os índices completos são estruturas que permitem que qualquer subcadeia consecutiva do texto original seja consultada, e/ou recuperada. Há outros tipos de índices, como os dicionários, mas estes não serão abordados neste trabalho. Os índices completos clássicos mais conhecidos são Árvore de Sufixos e Vetor de Sufixos.

1.2.3.1 Árvore de Sufixos

Árvore de Sufixos é uma estrutura de dados representada por uma árvore compacta que contém todos os sufixos de um texto T de comprimento n. Apesar de existirem n + 1 sufixos distintos, o espaço ocupado por esta estrutura é O(n). Por exemplo, para a cadeia abaaba$ a Árvore de Sufixos está demonstrada na Figura 1.1, onde cada folha apresenta o índice do sufixo que a mesma representa. O sufixo T [i..] é computado concatenando-se os rótulos das arestas no caminho único da raiz até a folha de índice i. Esta estrutura foi introduzida como uma forma linear, tanto em espaço quanto em tempo, para resolver alguns problemas de casamento

(18)

1.2. CASAMENTO EXATO DE PADRÕES ONLINE X OFFLINE 17 6 5 2 3 0 4 1 $ a $ aba$ ba $ aba$ ba $ aba$

Figura 1.1: Árvore de Sufixos para a cadeia abaaba$ Fonte: Elaborada pelo autor.

de padrões (WEINER, 1973). Posteriormente, outros algoritmos mais simples foram propostos apresentando um tempo linear para alfabetos de tamanho constante (MCCREIGHT, 1976; UKKONEN, 1995), e, em 1997, o primeiro algoritmo de tempo linear para qualquer alfabeto foi publicado (FARACH, 1997).

Seja CST uma árvore compacta de sufixos da cadeia T de comprimento n. Então, para todo sufixo de T , existe um caminho em CST que representa este sufixo. Se todo sufixo está representado na árvore, então qualquer subsequência de T estará representada por algum caminho na CST , pois toda subsequêcia T [i.. j] é prefixo do sufixo T [i..n − 1]. O custo de procurar um padrão P de comprimento m nesta estrutura é O(m).

Há outras aplicações para esta estrutura, como contar a quantidade de ocorrências de um padrão, encontrar o menor sufixo lexicográfico do texto, encontrar palíndromos, encontrar subsequências comuns entre duas ou mais cadeias, encontrar a menor cadeia que não ocorre como subsequência de T , ordenar lexicograficamente todos os sufixos de T , entre outras (APOS-TOLICO, 1985).

Árvores de Sufixos têm sido utilizadas para realizar consultas em grandes quantidades de dados, como dados genômicos. Porém, com o aumento do volume de dados, os algoritmos inicialmente eficientes, podem apresentar um desempenho insuficiente, principalmente na fase de construção do índice. Quando o volume de dados não cabe na memória principal, esta rotina terá que lidar com leituras e escritas em disco, que é uma operação bem mais lenta do que acessar informações na memória principal. Algoritmos para construção da Árvore de Sufixos levando em consideração este cenário vêm sendo desenvolvidos ao longo dos anos (TATA; HANKINS; PATEL, 2004; PHOOPHAKDEE; ZAKI, 2007; BARSKY et al., 2008, 2009).

1.2.3.2 Vetores de Sufixos

Vetores de Sufixos surgiram como uma alternativa para as Árvores de Sufixos com um uso de memória, na prática, entre três e cinco vezes menor (MANBER; MYERS, 1990). Esta nova estrutura é representada por uma sequência ordenada SA dos sufixos de um texto, onde SA[i] representa o i-ésimo sufixo do texto em ordem lexicográfica. Em sua versão original, as operações realizadas pelo Vetor de Sufixos apresentam um desempenho teórico pior que

(19)

as mesmas operações na Árvore de Sufixos, mas alguns trabalhos posteriores conseguiram alcançar um custo linear para a fase de construção da estrutura (KäRKKäINEN; SANDERS; BURKHARDT, 2006; UKKONEN, 1995). Por outro lado, o custo de buscar um padrão é O(|P| + log2(|T |)), em contraste com o O(|P|) da Árvore de Sufixos. Na prática, os Vetores de Sufixos tem um desempenho competitivo com as Árvores de Sufixos.

Buscar um padrão em um Vetor de Sufixos é equivalente a buscar uma cadeia de caracteres em uma lista de cadeias, de tal forma que as cadeias da lista estão ordenadas lexicograficamente, e elas apresentam um forte relacionamento uma com a outra, como pode ser visto no exemplo da Tabela 1.1, na qual os sufixos da cadeia abaaba$ estão relatados, primeiro, do maior para o menor comprimento e, na última coluna, em ordem lexicográfica. Isto pode ser feito utilizando-se uma busca binária para encontrar o primeiro sufixo da lista que possui o padrão como prefixo, e outra busca binária para encontrar o último sufixo da lista com esta mesma propriedade. Todos os sufixos entre eles são sufixos que possuem o padrão procurado como prefixo.

Sufixos Vetor de Sufixos 0 abaaba$ $ 1 baaba$ a$ 2 aaba$ aaba$ 3 aba$ aba$ 4 ba$ abaaba$ 5 a$ ba$ 6 $ baaba$

Tabela 1.1: Comparação entre os sufixos e o Vetor de Sufixos da cadeia abaaba$.

1.3 Aplicações à Bioinformática

Sequenciamento da nova geração (do Inglês, Next-Generation Sequencing - NGS) é um termo utilizado para se referir ao conjunto de tecnologias que são capazes de sequenciar um grande volume de dados, e que não são baseadas na tradicional tecnologia Sanger (SANGER; COULSON, 1975). Métodos NGS são capazes de sequenciar milhões de fragmentos de DNA ao mesmo tempo, permitindo que o conjunto total de dados possa chegar a bilhões de pares de base. Os métodos anteriores, baseados em Sanger, alcançam entre 500 e mil pares de base. Utilizar uma vez um método NGS pode gerar 600 GB de dados, o que é bom, pois permite que o nível de cobertura de análises realizadas sejam bem maior do que anteriormente, mas cria demandas relacionadas ao armazenamento e processamento deste grande volume de informação.

Resequenciamento é uma importante atividade em Bioinformática que consiste na uti-lização de um genoma já conhecido (genoma de referência) como base para o mapeamento de fragmentos de um novo genoma sequenciado, para descobrir se houve mutações, inserções, e/ou deleções. Este processo não é, necessariamente, utilizado em todo o genoma, podendo ser aplicado apenas em uma região de interesse, dependendo do contexto e aplicação. Este processo de mapeamento de uma sequência do genoma em uma sequência de referência é chamado de mapping, ou read mapping, (onde read representa um fragmento do genoma) e é equivalente ao casamento de padrões. Uma das principais aplicações é a identificação de mutações associadas a doenças congênitas.

(20)

1.4. COMPLEXIDADE DE ESPAÇO 19 Há várias ferramentas que podem realizar o mapeamento de reads. Entre elas, temos BLAST (ALTSCHUL et al., 1990), SOAP (LI et al., 2008), SeqMap (JIANG; WONG, 2008), MAQ (LI; RUAN; DURBIN, 2008), MUMmer (KURTZ et al., 2004), OASIS (ROBINSON; LEE; MARX, 2012), Bowtie (LANGMEAD et al., 2009), BWA (LI; DURBIN, 2009), SOAP2 (LI et al., 2009), Stampy (LUNTER; GOODSON, 2010). As quatro primeiras ferramentas se baseiam em uma abordagem seed-and-extend, utilizando hash para encontrar casamentos exatos de trechos do padrão procurado e expandindo essas "sementes" em alinhamentos completos do padrão. As próximas duas ferramentas se baseiam em Árvores de Sufixos e as três seguintes se baseiam no FM-Index (FERRAGINA; MANZINI, 2000), que é um índice criado a partir da transformação de Burrows-Wheeler (BURROWS; WHEELER, 1994), que será detalhada na Seção 2.1. A última ferramenta (Stampy) utiliza uma abordagem híbrida de seed-and-extend com hash e a transformação de Burrows-Wheeler. Análises comparativas mais detalhadas entre ferramentas podem ser encontradas em LI; HOMER (2010); SCHBATH et al. (2012).

1.4 Complexidade de Espaço

O uso de índices para realização de buscas rápidas podem ser desfavorecidos pelo tamanho destas estruturas. Índices para o genoma humano completo, que possui cerca de três bilhões de pares de base, podem alcançar um tamanho de 2.7 GB utilizando-se a ferramenta Bowtie (LANGMEAD et al., 2009). Isso torna índices que aplicam técnicas para reduzir o espaço ocupado mais atrativos.

Os genomas de outros seres vivos também têm um tamanho considerável, como milho (2.4 GB (HABERER et al., 2005)), trigo (17 GB (BRENCHLEY et al., 2012)), pimenta (3.8 GB (KIM et al., 2014)), nicotina (3 GB (BOMBARELY et al., 2012)). O maior genoma conhecido hoje é da planta Paris japonica com 150 GB (PELLICER; FAY; LEITCH, 2010).

1.5 Índices Comprimidos e Sucintos

As versões iniciais das Árvores de Sufixos utilizavam muita memória, apesar de serem assintoticamente lineares. O algoritmo de McCreight (MCCREIGHT, 1976) surgiu como uma melhoria, entre outras coisas, na quantidade de espaço utilizado pela estrutura, em relação ao algoritmo de Weiner (WEINER, 1973), ocupando cerca de 28n bytes no pior caso, onde n representa o tamanho do texto. Posteriormente, Manbers e Myers (MANBER; MYERS, 1990) reduziram este limite para 22.4n, e, assim, este limite foi sendo reduzido ao longo do

tempo, motivando um trabalho que focou especificamente na complexidade de espaço na prática (KURTZ, 1999), até alcançar algo próximo ao limite teórico, com as versões comprimidas desta estrutura (SADAKANE, 2007).

Um índice sucinto é um índice que apresenta uma complexidade de espaço "próxima" ao limite inferior teórico sem comprometer a eficiência das operações que podem ser realizadas na estrutura, ao contrário de outros índices comprimidos. Outra característica deste tipo de estrutura é que ela permite que as operações sejam feitas diretamente nos dados comprimidos, ou seja, sem a necessidade de realizar qualquer descompressão. Este conceito de sucinto foi introduzido em JACOBSON (1988), e, desde então, tem sido bastante difundido. Grossi e Vitter (GROSSI; VITTER, 2000) reduziram o espaço ocupado pelo Vetor de Sufixos de O(nlog2(n)) para O(nlog2(σ )) bits utilizando uma estrutura de dados auxiliar chamada de Wavelet Tree (W T ), onde n representa o tamanho do texto e σ representa o tamanho do alfabeto do texto. Sadakane (SADAKANE, 2007) fez a mesma redução de complexidade para Árvores de Sufixos.

(21)

Assim como para a Árvore de Sufixos, melhorias foram feitas para o Vetor de Sufixos de forma a alcançar um baixo consumo de espaço na prática, alcançando algo próximo ao limite inferior teórico (SADAKANE, 2007; RUSSO; NAVARRO; OLIVEIRA, 2008).

O limite inferior teórico para compressão de uma cadeia pode ser calculado através da entropia. A entropia de ordem 0 (H0) de um texto estabelece um limite inferior de espaço que um

compressor pode alcançar se comprimir os caracteres independentemente, ou seja, considerando apenas a frequência dos mesmos no texto. A entropia de ordem k (Hk) de um texto estabele um

limite inferior de espaço que pode-se alcançar se, ao comprimir um caractere, considerando o contexto representado pelos últimos k caracteres. O Apêndice A aborda esse assunto e apresenta alguns exemplos.

1.6 Arquitetura dos Computadores

A grande maioria dos algoritmos não leva em consideração algo que é muito impor-tante e afeta diretamente a eficiência de um programa, que é a arquitetura dos computadores modernos. Atualmente, os computadores apresentam uma hierarquia de memória com vários níveis conforme ilustrado na Figura 1.2, onde a memória que está no topo apresenta um custo de acesso muito baixo, e a que está na base da hierarquia apresenta um custo de acesso alto. Em contrapartida, a memória que é muito rápida apresenta uma pequena capacidade e um alto custo, ao passo que a memória que é mais lenta apresenta uma grande capacidade e baixo custo.

CPU Regis-tradores Cache Cache Nível 1 (L1) Cache Nível 2 (L2) Memória Física Memória de Acesso Aleatório (RAM)

Memória de Estado Sólido (SSD) Memória FLASH não-Volátil

Memória Virtual Memória baseada em Arquivo Custo por megabyte Desempenho (V elocidade) Processador SD-RAM, DDR-SDRAM SSD, Memória FLASH Disco Rígido Mecânico

Figura 1.2: Hierarquia de Memória

Fonte: Elaborada pelo autor baseado em KIOUSELOGLOU (2015).

Na típica arquitetura atual, a cache representa o segundo nível de memória, onde os registradores representam o primeiro nível. Os dados que são manipulados pelo processador devem estar na cache. Uma vez que um dado que deve ser processado não está na cache (cache

(22)

1.7. OBJETIVOS 21 miss), ele precisa ser trazido para cache de algum outro nível mais baixo (outra cache, a memória principal, o disco rígido, etc.). Uma vez que estes outros níveis apresentam um tempo de acesso maior, a quantidade de cache miss na execução de um programa pode causar um grande impacto em sua eficiência na prática. A diferença entre o tempo de acesso da cache e da memória principal variava entre 2 e 400 vezes, para valores simbólicos do ano 2006 (HENNESSY; PATTERSON, 2011). Na tentativa de reduzir a quantidade de vezes que um cache miss ocorre, pode-se tirar vantagem do Princípio da Localidade utilizado pela cache, que se baseia em:

 Princípio da Localidade Espacial: É esperado que, ao acessar um determinado dado, o processador irá acessar dados vizinhos ao dado acessado, em um futuro próximo.  Princípio da Localidade Temporal: É esperado que, ao acessar um determinado dado,

o processador irá acessá-lo novamente em um futuro próximo.

Algoritmos que utilizam explicitamente os parâmetros da arquitetura do computador, são chamados de cache-aware. Uma preocupação importante com relação a estes algoritmos, são os ajustes a serem realizados nos valores dos parâmetros para descobrir qual a configuração mais eficiente, uma vez que é possível que os valores exatos para os parâmetros não resultem na configuração mais eficiente do algoritmo (BURCSI; KOVÁCS, 2007).

Há algoritmos que não utilizam informação sobre os parâmetros da arquitetura do computador, mas, ainda assim podem apresentar desempenho comparável ao dos algoritmos cache-aware. Estes algoritmos são chamados de cache-oblivious (FRIGO et al., 1999), e conseguem utilizar eficientemente tanto as diferentes caches, quanto a hierarquia multinível da memória.

Atualmente, alguns algoritmos e estruturas de dados possuem uma versão cache-oblivious, como multiplicação de matrizes (FRIGO et al., 1999), Transformada Rápida de Fourier (FRIGO et al., 1999), busca em largura (BRODAL et al., 2004), árvore B (BENDER; FARACH-COLTON; KUSZMAUL, 2006), "versão" concorrente da árvore B (BENDER et al., 2005), árvore R (ARGE; BERG; HAVERKORT, 2007), ordenação (BRODAL; FAGERBERG; VINTHER, 2008), pro-cessamento de consultas em banco de dados relacional (HE; LUO, 2007, 2008), um índice para casamento aproximado de padrões (HON et al., 2011), entre outros.

1.7 Objetivos

A redução do espaço utilizado por uma estrutura de dados tem relação com melhorias no desempenho de suas operações (FERRAGINA; MANZINI, 2000). Neste trabalho abordaremos uma estrutura de dados sucinta para indexação de textos avaliando algumas modificações na sua organização dos dados de forma a obter uma redução na quantidade de cache misses ocorridos para realização de operações de casamento de padrão.

Modificações na organização dos dados da W T já foram propostas anteriormente em NAVARRO (2014) onde não se avaliou o impacto das modificações no desempenho das operações da estrutura, e em CLAUDE; NAVARRO; ORDÓÑEZ (2015) onde o foco foi apenas dados com alfabetos grandes, o que se refletiu nos experimentos que avaliaram apenas dados com mais de um milhão de símbolos diferentes. As modificações aqui propostas serão avaliadas em dados textuais onde a quantidade de símbolos varia de 16 a pouco mais de 3 mil.

(23)

1.8 Visão Geral

A Wavelet Tree será construída sobre uma permutação do texto original de forma que seja possível realizar eficientes consultas no mesmo. A transformação que será aplicada ao texto para obter esta permutação será explicada no início do Capítulo 2. Além disto, nesse capítulo descreveremos a estrutura de dados W T e abordaremos tópicos relevantes relacionados à cache do computador, e um modelo para avaliar teoricamente o uso da cache.

No Capítulo 3 descreveremos as modificações que serão feitas na estrutura de dados de forma a torná-la mais eficiente do ponto de vista da cache, e no Capítulo 4 apresentaremos a fer-ramenta utilizada para avaliar na prática a quantidade de cache misses ocorridos, os experimentos e os resultados obtidos sobre a estrutura modificada.

A conclusão dos resultados obtidos assim como as possibilidades de trabalhos futuros serão apresentadas no Capítulo 5.

(24)

23 23 23

2

Estado da Arte

2.1 Transformada de Burrows-Wheeler

A Transformada de Burrows-Wheeler (do Inglês, Burrows-Wheeler Transform - BW T ) é uma transformação da sequência de caracteres em uma das suas permutações de forma que há uma maior redundância de caracteres consecutivos (BURROWS; WHEELER, 1994), permitindo que técnicas de compressão, como, por exemplo, Run-Length Encoding (ROBINSON; CHERRY, 1967), ou a transformação Move-to-Front (BENTLEY et al., 1986), sejam aplicadas sobre

esta nova permutação, alcançando taxas de compressão superiores às do que se aplicadas na sequência original. Ou seja, a BW T , por si só, não é uma técnica de compressão, e sim uma forma reversível de reorganizar o texto para se beneficiar da redundância local dos caracteres do texto transformado.

Atualmente, vários autores têm utilizado a BW T em trabalhos de análise e/ou processa-mento do genoma (MEDINA et al., 2016; KIMURA; KOIKE, 2015; NOGUEIRA; TOMAS; ROMA, 2015; BAIER; BELLER; OHLEBUSCH, 2015; KIM; LANGMEAD; SALZBERG, 2015; HOLT; MCMILLAN, 2014; COX et al., 2012; HURGOBIN, 2016), e como base para construção de outras estruturas de dados, como um Vetor de Sufixos comprimido (LIPPERT; MOBARRY; WALENZ, 2005).

2.1.1 A Transformação

Seja T uma cadeia sobre um alfabeto α e $ um caractere especial lexicograficamente menor que todos os outros caracteres que marca o final da cadeia. Seja, inicialmente, a matriz obtida a partir de todas as permutações cíclicas de T . A Figura 2.1 contém um exemplo para T = abaaba$. a b a a b a $ $ a b a a b a a $ a b a a b b a $ a b a a a b a $ a b a a a b a $ a b b a a b a $ a

Figura 2.1: Rotações da cadeia abaaba$.

Considere, em seguida, a matriz obtida pela ordenação lexicográfica dessas permutações de T . A Figura 2.2 contém a matriz resultante para a cadeia do exemplo acima.

(25)

$ a b a a b a a $ a b a a b a a b a $ a b a b a $ a b a a b a a b a $ b a $ a b a a b a a b a $ a

Figura 2.2: Rotações da cadeia abaaba$ ordenadas lexicograficamente. Em negrito, os últimos caracteres das cadeias que formarão a BW T do texto.

Esta matriz ordenada é chamada de Matriz de Wheeler (do Inglês, Burrows-Wheeler Matrix - BW M) do texto T . A BW T do texto são os últimos caracteres da BW M, de cima para baixo. A BW T também pode ser construída através do Vetor de Sufixos, pois há uma forte relação entre a BW M e o Vetor de Sufixos (vide Seção 1.2.3.2), como pode-se ver no exemplo da Figura 2.3, onde SA representa o Vetor de Sufixos construído para a cadeia T .

Vetor de Sufixos Matriz de Burrows-Wheeler i SA[i] T [SA[i]] 0 6 $ $ a b a a b a 1 5 a$ a $ a b a a b 2 2 aaba$ a a b a $ a b 3 3 aba$ a b a $ a b a 4 0 abaaba$ a b a a b a $ 5 4 ba$ b a $ a b a a 6 1 baaba$ b a a b a $ a

Figura 2.3: Vetor de Sufixos da cadeia abaaba$, ao lado das rotações da mesma cadeia ordenadas lexicograficamente.

Logo, a BW T pode ser definida em função do Vetor de Sufixos da seguinte forma BW T [i] =(T[SA[i] − 1] se SA[i] > 0

$ caso contrário.  2.1

2.1.2 A Reversão

A partir da BW T , temos a última coluna da BW M. Para se obter a primeira coluna, precisamos apenas ordenar os símbolos presentes na BW T , obtendo a Figura 2.4, onde ainda não se conhece os símbolos nas outras colunas.

A BW M possui uma propriedade importante PropBWM que estabelece que na primeira e na última colunas, os símbolos iguais s aparecem na mesma ordem (FERRAGINA; MANZINI, 2000). Adicionando um índice para os símbolos da primeira e da última colunas apenas de forma a identificá-los, para efeito de ilustração, temos a Figura 2.5.

Como cada linha da BW M é uma rotação da cadeia original T , então os símbolos da primeira coluna são adjacentes aos símbolos da última coluna. Da Figura 2.5, podemos concluir que o símbolo que precede $ é o a0, o que precede a0é o b0, o que precede o a2é o a1, e assim

(26)

2.1. TRANSFORMADA DE BURROWS-WHEELER 25 $ x x x x x a a x x x x x b a x x x x x b a x x x x x a a x x x x x $ b x x x x x a b x x x x x a

Figura 2.4: Primeira e última colunas da BWM.

$ x x x x x a0 a0 x x x x x b0 a1 x x x x x b1 a2 x x x x x a1 a3 x x x x x $ b0 x x x x x a2 b1 x x x x x a3

Figura 2.5: BWM com índices nos símbolos da primeira e da última colunas.

sucessivamente. A partir destas precedências, podemos computar todos os outros elementos da matriz.

Por exemplo, começando do símbolo $, podemos ver que os quatro símbolos que o precedem são, a0, b0, a2, a1, nesta ordem. A computação destes quatro precedentes, assim

como a matriz começando a ser preenchida utilizando os símbolos precedentes, pode ser vista na Figura 2.6. $ x x x x x a0 a0 x x x x x b0 a1 x x x x x b1 a2 x x x x x a1 a3 x x x x x $ b0 x x x x x a2 b1 x x x x x a3 $ x x a1 a2 b0 a0 a0 x x x a1 a2 b0 a1 x x x x x b1 a2 x x x x x a1 a3 x x x x x $ b0 x x x x a1 a2 b1 x x x x x a3

Figura 2.6: Na esquerda, caminho na BWM para computação de quatro antecedentes do símbolo $. Na direita, os antecedentes computados sendo utilizados para preencher a

matriz.

Prosseguindo no cálculo da precedência até que o símbolo $ seja visitado novamente, conseguimos computar a primeira linha da matriz. Como o símbolo especial $ é o menor lexicograficamente e foi adicionado para marcar o final da cadeia, a primeira linha da BW M após o $ representa a cadeia original T . A matriz completa com os índices em cada símbolo, exceto $, pode ser vista na Figura 2.7.

(27)

$ a3 b1 a1 a2 b0 a0 a0 $ a3 b1 a1 a2 b0 a1 a2 b0 a0 $ a3 b1 a2 b0 a0 $ a3 b1 a1 a3 b1 a1 a2 b0 a0 $ b0 a0 $ a3 b1 a1 a2 b1 a1 a2 b0 a0 $ a3

Figura 2.7: BWM com índices em todos os caracteres da matriz.

2.1.3 Construção

A BW T pode ser construída em tempo linear, uma vez que existem algoritmos lineares para construção do Vetor de Sufixos (KäRKKäINEN; SANDERS; BURKHARDT, 2006; UK-KONEN, 1995), e um total de O(n) operações são necessárias para criar a BW T do Vetor de Sufixos, onde n representa o tamanho da cadeia.

A reversão da transformação também pode ser feita em tempo linear. No processo de reversão, não é necessário preencher a matriz toda. É preciso, apenas, preencher a primeira linha, que informa a cadeia original, logo após o caractere especial $. Para computar a primeira coluna, podemos utilizar Counting sort (CORMEN et al., 2009) que apresenta uma complexidade O(n) de tempo. Para calcular os índices, pode-se utilizar um contador para cada caractere do alfabeto, de forma que este cálculo possa ser realizado em O(n).

2.1.4 FM Index

Em 2000, a introdução do índice completo (i.e. que representa todas as subcadeias do texto) FM-Index (FERRAGINA; MANZINI, 2000) foi um grande marco, apresentando uma estrutura competitiva com os Vetores de Sufixos (GROSSI; VITTER, 2000). Esta estrutura de dados também respondeu uma pergunta que permanecia aberta na época mostrando que um índice completo não precisa de espaço linear no tamanho original do texto para suportar operações eficientes de busca de padrões arbitrários, concluindo que uma sobrecarga de espaço não é uma necessidade para o uso de um índice completo.

Esta estrutura tira vantagem da compressibilidade dos dados de entrada reduzindo o espaço ocupado sem prejudicar significativamente o desempenho das operações. Ela é ótima em relação ao espaço ocupado, utilizando O(nHk(T ) + o(1)) bits para um texto T de tamanho

n, onde Hkrepresenta a entropia de ordem k. Sua estrutura utiliza a BW T de forma a organizar

um tipo de Vetor de Sufixos comprimido. A busca de padrões utiliza a propriedade PropBWM, realizando a busca do padrão do final para o início da forma explicitada no Algoritmo 2.1, reportando a quantidade de ocorrências do padrão.

Nesse algoritmo, ao percorrer o padrão procurado P do final para o começo, c representa o caractere atual do padrão, e Cnt[s] representa, para um símbolo s, a quantidade de símbolos presentes no texto T que são menores lexicograficamente que s. As variáveis sp (posição inicial, do Inglês starting point) e ep (posição final, do Inglês ending point) representam o intervalo [sp,ep] da BW M no qual, o sufixo já percorrido do padrão é prefixo de BW M[ j], para todo j onde sp ≤ j ≤ ep. A função occurrences(BW T (T ),c,k) informa a quantidade de caracteres iguais a c no prefixo da BW T (T ) de 0 a k. O custo do Algoritmo 2.1 é O(|P|) multiplicado pelo custo da função occurrences, que pode ser implementada com custo O(1) em tempo utilizando-se compressão independente por bucket, como pode ser visto no trabalho seminal.

(28)

2.1. TRANSFORMADA DE BURROWS-WHEELER 27 Algoritmo 2.1 Busca de padrões com FM-Index

1: procedure FM_INDEX_SEARCH(P[0,m-1], BW T (T )) .Padrão P a ser buscado

2: c ← P[m − 1]

3: i ← m − 1 4: sp ← Cnt[c]

5: ep ← Cnt[c + 1] − 1

6: while sp ≤ ep and i ≥ 1 do .Enquanto houver padrões e caracteres em P

7: c ← P[i − 1] 8: sp ← Cnt[c] + occurrences(BW T (T ),c,sp − 1) 9: ep ← Cnt[c] + occurrences(BW T (T ),c,ep) − 1 10: i ← i − 1 11: end while 12: if ep < sp then

13: return 0 .Padrão P não encontrado

14: else .sp ≤ ep

15: return ep − sp + 1 .Encontradas (ep − sp + 1) ocorrências

16: end if 17: end procedure

Tomemos, por exemplo, a matriz da Figura 2.7 e o padrão P = aaba. Seguindo o Algoritmo 2.1, ao procurarmos o último símbolo do padrão, teremos o intervalo definido por sp = 1 e ep = 4, como destacado na Figura 2.8. Para acharmos o intervalo de linhas da BW M que possuem prefixo igual ao sufixo ba de P, precisamos encontrar quais os símbolos b que estão no final das linhas correspondentes ao intervalo [sp,ep]. As linhas que começam com estes símbolos b definirão o novo intervalo [sp,ep]. Como pode ser visto na Figura 2.8, os símbolos b da última coluna que estão no intervalo [1,4] são b0 e b1. Os índices dos símbolos b sempre

serão consecutivos, dado a propriedade PropBWM. O novo intervalo também pode ser visto na Figura 2.8. $ a3 b1 a1 a2 b0 a0 a0 $ a3 b1 a1 a2 b0 a1 a2 b0 a0 $ a3 b1 a2 b0 a0 $ a3 b1 a1 a3 b1 a1 a2 b0 a0 $ b0 a0 $ a3 b1 a1 a2 b1 a1 a2 b0 a0 $ a3 $ a3 b1 a1 a2 b0 a0 a0 $ a3 b1 a1 a2 b0 a1 a2 b0 a0 $ a3 b1 a2 b0 a0 $ a3 b1 a1 a3 b1 a1 a2 b0 a0 $ b0 a0 $ a3 b1 a1 a2 b1 a1 a2 b0 a0 $ a3

Figura 2.8: Na esquerda, intervalo para busca do sufixo a. Na direita, intervalo para busca do sufixo ba.

Agora iremos buscar o sufixo aba procurando o símbolo a no intervalo definido por [5,6]. Os símbolos a que aparecem na última coluna neste intervalo são a2e a3, o que nos dá o novo intervalo [3,4] demonstrado na Figura 2.9. Continuando o processo, iremos computar quais símbolos a aparecem no intervalo [3,4], o que resultará apenas no símbolo a1. Como todo

o padrão foi buscado, o intervalo de ocorrências do padrão P é definido apenas pela linha do símbolo a1([2,2]), resultando em apenas uma ocorrência do padrão P no texto.

(29)

$ a3 b1 a1 a2 b0 a0 a0 $ a3 b1 a1 a2 b0 a1 a2 b0 a0 $ a3 b1 a2 b0 a0 $ a3 b1 a1 a3 b1 a1 a2 b0 a0 $ b0 a0 $ a3 b1 a1 a2 b1 a1 a2 b0 a0 $ a3 $ a3 b1 a1 a2 b0 a0 a0 $ a3 b1 a1 a2 b0 a1 a2 b0 a0 $ a3 b1 a2 b0 a0 $ a3 b1 a1 a3 b1 a1 a2 b0 a0 $ b0 a0 $ a3 b1 a1 a2 b1 a1 a2 b0 a0 $ a3

Figura 2.9: Na esquerda, intervalo para busca do sufixo aba. Na direita, intervalo para busca do sufixo aaba.

utilizada para se encontrar a primeira posição da primeira coluna na qual o símbolo c aparece, e a função occurrences computa o deslocamento (offset) dos símbolos c que definem o intervalo. Perceba que neste cálculo, só utilizamos informações da primeira e da última colunas da BW M. As outras colunas foram exibidas nas Figuras 2.8 e 2.9 apenas para critério de demonstração.

FERRAGINA; MANZINI (2000) também propõem duas abordagens para determinar a posição de cada uma das ocorrências, mas apenas a primeira abordagem será descrita aqui. A idéia é armazenar, apenas para algumas posições da BW T , as posições originais dos caracteres no texto. Quando for necessário calcular a posição de um caractere c0, a rotina irá iterar de c0,

computando seus precedentes para encontrar c00tal que c00tem sua posição original armazenada.

A posição de c0será a posição de c00 mais a quantidade de iterações utilizadas para se chegar de

c0até c00.

Supondo que armazenemos a informação das posições apenas para os símbolos a3e a2,

para calcular a posição do símbolo a0, teríamos

position(a0) =1 + position(b0)

=1 + 1 + position(a2) =1 + 1 + 3

=5

,

pois o símbolo precedente ao a0é b0, e o precedente ao b0é a2.

Utilizando esta abordagem, há um compromisso entre o espaço utilizado para esta estrutura auxiliar, que é fortemente influenciado pela quantidade de elementos armazenados, e o tempo de processamento da função position.

2.2 Wavelet Tree

A Wavelet Tree (W T ), introduzida por Grossi, Gupta e Vitter (GROSSI; GUPTA; VIT-TER, 2003), é uma estrutura de dados em forma de árvore na qual cada nó codifica uma subsequência da cadeia original utilizando apenas um subconjunto de símbolos. A forma como a codificação é feita permite que a complexidade de espaço seja competitiva com outras estruturas de dados.

Apesar de ter sido desenvolvida com foco em indexação de textos, hoje essa estrutura possui variadas aplicações: indexação de imagens, compressão de permutações, compressão de grafos, busca espacial (geográfica), representação de relações binárias, representação de sequên-cias numéricas, indexação de XML, entre outras (FERRAGINA; GIANCARLO; MANZINI,

(30)

2.2. WAVELET TREE 29 2009; MAKRIS, 2012; NAVARRO, 2014).

2.2.1 Definição

A W T de uma cadeia é obtida dividindo-se recursivamente o alfabeto e o texto em duas partes. Para cada divisão, utiliza-se um vetor binário (bit vector) para indicar se o caractere naquela posição está representado na primeira, ou na segunda parte do alfabeto dividido. O critério para divisão do alfabeto pode variar, assim como a forma em que os vetores binários são representados.

Na Figura 2.10, pode-se ver a representação de uma W T para a cadeia mississipi. A imagem serve apenas para ilustrar, mas a implementação manteria apenas as cadeias binárias de cada nó e o alfabeto α da cadeia, que neste caso é α = {i,m,p,s}. Dado o alfabeto e as cadeias binárias, é possível recuperar o texto completo.

mississippi 00110110110 miiii 10000 sssspp 111100 {i,m} {i} {m} {p,s} {p} {s}

Figura 2.10: Representação de uma WT para a cadeia mississipi.

Na Figura 2.10, a cadeia binária no nó raiz indica, para cada caractere da cadeia original, onde o símbolo está, sendo que 0 indica o filho à esquerda, e 1 indica o filho à direita. Na raiz, o alfabeto α é composto por todos os caracteres que aparecem na cadeia original, neste caso α = {i, m, p, s}. No filho à esquerda, o alfabeto se torna a primeira metade do alfabeto original (α0= {i, m}) e a cadeia corrente se torna a subsequência da cadeia original composta apenas por

caracteres de α0. O mesmo raciocínio se aplica ao filho à direita. Os filhos do nó raiz não têm

filhos porque o alfabeto teria tamanho 1, e não é necessário criar uma estrutura para representar um conjunto de caracteres iguais.

2.2.2 Construção

O Algoritmo 2.2 constrói uma W T da cadeia T recebida como parâmetro. A condição imposta sobre o retorno do método partition na linha 5 é necessária para que as variáveis α0e

α1tenham tamanho maior que 1. A forma como o método partition selecionará um subconjunto

pode ser implementado de várias formas, como será discutido na Seção 2.2.4. O laço da linha 11, além de definir os bits de node, decompõe a cadeia T em T0 e T1 que serão utilizadas nas

chamadas recursivas (linhas 21 e 22). A função append recebe uma cadeia e um caractere e retorna uma nova cadeia que é a concatenação dos parâmetros recebidos. O atributo le f t se refere ao filho da esquerda de node, e right se refere ao filho da direita. O alfabeto é passado como parâmetro apenas para facilitar, mas ele poderia ser omitido e computado em cada chamada de acordo com os caracteres de T .

O particionamento da cadeia T em T0e T1, como descrito no Algoritmo 2.2, cria uma

cópia de T em cada nível da W T , considerando-se uma árvore balanceada onde σ representa o comprimento do alfabeto α, consumindo O(nlog22(σ )) bits, uma vez que a árvore possui

(31)

Algoritmo 2.2 Construção de uma WT

1: procedure WT_CONSTRUCTION(T [0,n − 1],α) .Cadeia T de comprimento n

2: if |α| = 1 then 3: return NULL

4: else .Há, pelo menos, dois caracteres no alfabeto α

5: α0←partition(α) .Seleciona um subconjunto de α tal que 0 < |α0| < |α|

6: α1← α − α0 . α0e α1são disjuntos, e a união deles forma α

7:

8: node ← new_node(n)

9: T0←00 .Inicializa T0com uma cadeia vazia

10: T1←00 .Inicializa T1com uma cadeia vazia

11: for i ← 0 to (n − 1) do 12: if T[i] ∈ α0then 13: node.set_bit(i,0) 14: T0←append(T0,T [i]) 15: else 16: node.set_bit(i,1) 17: T1←append(T1,T [i]) 18: end if 19: end for 20: 21: node.le f t ← WT_CONSTRUCTION(T0, α0) 22: node.right ← WT_CONSTRUCTION(T1, α1) 23: return node 24: end if 25: end procedure

O(log2(σ ))níveis, e são necessários O(nlog2(σ ))bits para representar o texto de comprimento n de uma forma não comprimida. Uma observação importante quanto a isto é que, após o vetor binário, T0e T1terem sido computados, T torna-se sem utilidade para o algoritmo. Uma vez

que esta estrutura de dados esteja construída, poderemos recuperar qualquer porção do texto utilizando apenas o alfabeto α e os vetores binários dos nós da árvore. Assim sendo, para evitar esse consumo de memória excessivo, podemos desalocar a memória utilizada pela variável T após a computação do vetor binário, de T0 e de T1. Entretanto, para evitar a alocação e

desalocação de memória em cada chamada ao Algoritmo 2.2, iremos utilizar o espaço alocado de T para T0e T1. Isto será feito através da reorganização dos caracteres de T de forma que

a mesma represente a concatenação de T0e T1e assim, cada chamada ao algoritmo utilizará,

como parâmetro, um intervalo representando a porção do texto que está sendo processado. O Algoritmo 2.3 representa o Algoritmo 2.2 após feitas estas alterações. A variável sp (do Inglês, starting point) indica o início da cadeia, e a variável ep (do Inglês, ending point) indica o fim da cadeia. Os valores iniciais para sp e ep são, respectivamente, 0 e n − 1. A função stable_inplace_sort realiza a reorganização dos caracteres de T .

A função de ordenação stable_inplace_sort deve ser estável e é altamente desejável que ele seja in place para não aumentar o espaço necessário para execução do algoritmo. Estável significa que ele precisa manter a ordem relativa dos caracteres de T que pertencem ao mesmo subalfabeto (α0ou α1). In place significa que ele só pode utilizar uma quantidade constante

(32)

2.2. WAVELET TREE 31 Algoritmo 2.3 Construção de uma WT (versão 2)

1: procedure WT_CONSTRUCTION(T [0,n − 1],sp,ep,α) 2: if |α| = 1 then

3: return NULL

4: else .Há, pelo menos, dois caracteres no alfabeto α

5: α0←partition(α)

6: α1← α − α0

7:

8: size0←stable_inplace_sort(T ,sp,ep,α0)

9: size1← (ep − sp + 1) − size0 10: 11: node ← new_node(n) 12: for i ← sp to ep do 13: if T[i] ∈ α0then 14: node.set_bit(i,0) 15: else 16: node.set_bit(i,1) 17: end if 18: end for 19:

20: node.le f t ← WT_CONSTRUCTION(T ,sp,sp + size0−1,α0)

21: node.right ← WT_CONSTRUCTION(T ,sp + size0,ep,α1)

22: return node 23: end if

24: end procedure

alguns deles serão listados a seguir. O algoritmo STL pode ser utilizado como ponto de partida, uma vez que é a implementação utilizada em uma biblioteca amplamente conhecida.

Block Sort Utilizando a idéia de dividir para conquistar, divide a entrada em blocos, os reorga-niza, e posteriormente, une os blocos resultando na entrada ordenada (KIM; KUTZNER, 2008). Uma implementação pública está disponível em MCFADDEN (2016).

Huang Utiliza a idéia de dividir a entrada em blocos (HUANG; LANGSTON, 1992) de maneira similar à opção anterior. Uma implementação está disponível em ASTRELIN (2016). STL Implementação da função __inplace_stable_partition utilizado pelo método

stable_partition da biblioteca STL (Standard Template Library) de C++ (SGI, 2000), que também utiliza uma abordagem dividir para conquistar, particionando a entrada ao meio em cada chamada recursiva.

2.2.3 Operações

Esta estrutura suporta duas operações básicas que são utilizadas em outras operações, que são rank e select. A função rank recebe três parâmetros que são um nó da W T , uma posição p e um valor binário b (0 ou 1), e retorna a quantidade de posições entre 0 e p (inclusive) em que o vetor binário do nó apresenta valor igual ao valor b informado. A função select é o inverso da função rank, recebendo também três parâmetros que são um nó da W T , uma quantidade q e um

(33)

valor binário b, e retornando a menor posição pos tal que, entre 0 e pos, há a quantidade q de bits iguais ao valor b informado como parâmetro. Matematicamente falando, temos

rank(node, p,b) = |{k ∈ [0..p] : node.get_bit(k) = b}|, 2.2 select(node,q,b) = min{k ∈ [0..∞] : rank(node,k,b) = q}. 2.3 O Algoritmo 2.4 consulta qual caractere está em uma determinada posição do texto T original dado que a W T foi construída sobre T . A ideia é, ao visitar cada nó, descobrir se o caractere está no filho à esquerda ou no filho à direita do nó atual. Isto pode ser feito com o auxílio da função rank. Nas linhas 10 e 13, o rank é calculado, pois a posição do caractere procurado será uma posição relativa no nó filho (à esquerda ou à direita). A função partition deve ser a mesma utilizada no algoritmo de construção (Algoritmos 2.2 e 2.3). Ainda no Algoritmo 2.4, chamadas recursivas são feitas até que o alfabeto α contenha apenas um caractere, que será o caractere procurado. A Figura 2.11 mostra um exemplo em que a posição do caractere muda ao visitar um nó descendente. Ao consultar a posição 4 do nó raiz, temos que a posição contém um 0, significando que o filho à esquerda será consultado. A posição relativa que será consultada no filho à esquerda é equivalente à posição atual considerando apenas os zeros, ou seja, a posição 4 do nó raiz é equivalente à posição 2 do filho à esquerda. De maneira similar, temos que a posição 4 do nó raiz é equivalente à posição 2 do filho à direita.

Algoritmo 2.4 Consultar o caractere de uma posição da WT

1: procedure WT_QUERY_POSITION(pos,node,α) .Consulta posição pos de node

2: if |α| = 1 then

3: return α[0] .Retorna o único caractere de α

4: else 5: α0←partition(α) 6: α1← α − α0 7: 8: bit ← node.get_bit(pos) 9: if bit = 0 then

10: r ← rank(node, pos,0) .Computa rank0

11: return WT_QUERY_POSITION(r,node.le f t,α0)

12: else

13: r ← rank(node, pos,1) .Computa rank1

14: return WT_QUERY_POSITION(r,node.right,α1)

15: end if 16: end if 17: end procedure

Pode-se calcular a quantidade de vezes que um símbolo aparece em um prefixo da cadeia sobre a qual a W T foi construída em tempo logarítmico. A definição é similar à da Equação 2.2, mas nessa a cadeia é binária. Para se calcular para uma cadeia de alfabeto com mais de dois símbolos, a W T decompõe a cadeia original em O(σ) cadeias binárias. Cada uma destas cadeias binárias representa um subconjunto do alfabeto de acordo com parâmetros relativos à construção da árvore. Ao realizar a decomposição de uma cadeia qualquer em cadeias binárias e permitir a composição de funções rank e/ou select, a W T pode ser vista como uma generalização destas funções. O Algoritmo 2.5 computa o rank de um símbolo qualquer para um determinado prefixo da cadeia original. O valor do parâmetro node deve ser, inicialmente, a

(34)

2.2. WAVELET TREE 33 mississippi 00110110110 miiii 10000 sssspp 111100 {i,m} {i} {m} {p,s} {p} {s} Posição 4 Posição 2

Figura 2.11: Consultando caractere na posição 4 de uma WT.

Algoritmo 2.5 Computa a quantidade de ocorrências do símbolo value até a posição pos inclusive

1: procedure WT_OCCURRENCES(node, pos,value) 2: if node = NULL or pos < 0 then

3: return 0 4: else

5: while node <> NULL and pos ≥ 0 do

6: if value < node.separator then .value está no ramo esquerdo

7: pos ← rank(node, pos,0) − 1 .rank0

8: node ← node.le f t

9: else .value está no ramo direito

10: pos ← rank(node, pos,1) − 1 .rank1

11: node ← node.right 12: end if 13: end while 14: 15: return pos + 1 16: end if 17: end procedure

raiz da estrutura. Na linha 2 algumas verificações básicas são feitas em relação à raiz da árvore e ao prefixo. Repetidamente, se visita um nó da árvore computando a quantidade de elementos do prefixo que estão no ramo à esquerda, ou à direita, dependendo do ramo no qual o símbolo procurado está. Assim, o tamanho do prefixo também pode ser modificado ao longo do processo que só se encerra ao chegar em um nó folha (nó NULL) que representa o símbolo procurado. A ideia da mudança do tamanho do prefixo pode ser vista na Figura 2.11.

As duas principais características que definem uma W T são (a) a maneira em que o alfabeto é particionado, e (b) a implementação utilizada para representar os vetores binários de cada nó.

2.2.4 Partição do Alfabeto

A maneira utilizada para dividir o alfabeto define a forma da árvore. As três principais maneiras de particionar são tratadas a seguir.

(35)

2.2.4.1 Balanceado

Utilizando a forma Balanceada (Figura 2.12), temos que a altura da árvore será Θ(log2σ ), onde σ representa o tamanho do alfabeto α, uma vez que o alfabeto α é sempre dividido ao meio. O alfabeto α deve conter apenas os símbolos que aparecem no texto.

wavelet_tree 101000111000 aeleree 0010100 aeeee 01111 lr 01 wvt_t 01010 vtt 100 w_ 01 {a,e,l,r} {a,e} {a} {e} {l,r} {l} {r} {t,v,w,_} {t,v} {t} {v} {w,_} {w} {_}

Figura 2.12: WT balanceada para a cadeia wavelet_tree.

Uma vantagem no uso desta opção é a previsibilidade de onde os símbolos estarão na árvore, uma vez que, como o alfabeto é sempre particionado ao meio, é fácil identificar onde estará a folha que representa um determinado símbolo s baseado apenas no tamanho do alfabeto (σ) e na posição do símbolo s no alfabeto.

2.2.4.2 Huffman

Utilizando a forma Huffman (Figura 2.13), o alfabeto será particionado de acordo com uma árvore de Huffman (HUFFMAN, 1952), que é construída baseada na frequência dos símbolos na cadeia, ou baseada em frequências conhecidas a priori, de forma que o espaço necessário para representação da cadeia original seja nH0(n). Utilizando a forma Huffman, a

altura da árvore pode chegar a Θ(σ).

Para se construir uma árvore de forma que a cadeia original possa ser representada em nH0(n) bits, a idéia é atribuir menores representações binárias para símbolos mais frequentes e,

similarmente, atribuir maiores representações binárias para símbolos menos frequentes. Quanto mais frequente um símbolo for, menor será a quantidade de bits que atribuiremos a ele e, desta forma, maior será a economia de bits.

O Algoritmo 2.6 constrói uma árvore de Huffman a partir do texto T . S é um conjunto de nós, cada um dos quais contendo, inicialmente, um símbolo do alfabeto com sua respectiva frequência na cadeia original. Enquanto houver mais de um nó em S, os dois nós de menor frequência em S são escolhidos para serem substituídos por um novo nó new_node que, na estrutura final, será o pai dos dois nós recém removidos de S. O último nó restante em S será a raiz da árvore de Huffman. Se S for implementado como uma fila de prioridades, a complexidade total do Algoritmo 2.6 se torna O(σ log2(σ )).

2.2.4.3 Hu-Tucker

Utilizando a forma Hu-Tucker, o alfabeto será particionado de acordo com uma árvore binária similar à de Huffman que busca representar a cadeia em nH0(n), mas que mantém a

Referências

Documentos relacionados

Seguindo a orientação da citação acima, as mudanças que identificamos nas configurações observadas se aproximam do que a autora identifica como redimensionamento da

In this work, TiO2 nanoparticles were dispersed and stabilized in water using a novel type of dispersant based on tailor-made amphiphilic block copolymers of

da quem praticasse tais assaltos às igrejas e mosteiros ou outros bens da Igreja, 29 medida que foi igualmente ineficaz, como decorre das deliberações tomadas por D. João I, quan-

Our contributions are: a set of guidelines that provide meaning to the different modelling elements of SysML used during the design of systems; the individual formal semantics for

 Ambulância da marca Ford (viatura nº8), de matrícula XJ-23-45, dotada com sirene, luz rotativa e equipamento de comunicação (Emissor/Receptor com adaptador);.  Ambulância da

A assistência da equipe de enfermagem para a pessoa portadora de Diabetes Mellitus deve ser desenvolvida para um processo de educação em saúde que contribua para que a

servidores, software, equipamento de rede, etc, clientes da IaaS essencialmente alugam estes recursos como um serviço terceirizado completo...