Árvore Splay: Estrutura de Dados Balanceada

A árvore Splay é uma estrutura de dados balanceada que suporta operações eficientes em conjuntos e sequências. Diferente de outras árvores binárias de busca, ela mantém a propriedade de que o percurso in-order em uma sequência mantida correspnode à própria sequência. A complexidade amortida das operações como inserção, remoção e busca é O(log n), garantida por análise de potencial.

O princípio fundamental das árvores Splay é mover o nó acessado para a raiz após cada operação, usando rotações. Isso otimiza o acesso frequente a determinados nós, adaptando-se a padrões de acesso.

Rotações

As rotações em árvores Splay são semelhantes às de árvores Treap, mas incluem rotações simples e duplas. Rotações simples são rotações à esquerda ou à direita, enquanto rotações duplas combinam múltiplas rotações para balancear a árvore.

Rotação Simples

Uma rotação simples altera a estrutura local da árvore para mover um nó para cima. O código a seguir implementa uma rotação genérica usando operações bit a bit para simplificar a lógica.

const int MAX_NOS = 100005;

struct No {
    int filhos[2], pai;
    int valor, contagem, tamanho;
#define esq (arvore[pos].filhos[0])
#define dir (arvore[pos].filhos[1])
} arvore[MAX_NOS];
int totalNos, raiz;

int criarNo(int val) {
    int id = ++totalNos;
    arvore[id].valor = val;
    arvore[id].contagem = arvore[id].tamanho = 1;
    return id;
}

int obterPosicao(int pos) {
    return pos == arvore[arvore[pos].pai].filhos[1];
}

void atualizarTamanho(int pos) {
    arvore[pos].tamanho = arvore[esq].tamanho + arvore[dir].tamanho + arvore[pos].contagem;
}

void conectar(int pos, int novoPai, int direcao) {
    arvore[pos].pai = novoPai;
    arvore[novoPai].filhos[direcao] = pos;
}

void rotacaoSimples(int pos) {
    int pai = arvore[pos].pai;
    int avo = arvore[pai].pai;
    int direcao = obterPosicao(pos);
    conectar(pos, avo, obterPosicao(pai));
    conectar(arvore[pos].filhos[direcao ^ 1], pai, direcao);
    conectar(pai, pos, direcao ^ 1);
    atualizarTamanho(pai);
    atualizarTamanho(pos);
}

Rotações Duplas

Rotações duplas são aplicadas quando o nó alvo e seu pai estão no mesmo lado (ajuste isomórfico) ou em lados opostos (ajuste anisomórfico) em relação ao avô. O objetivo é balancear a árvore mais eficientemente.

Operação Splay

A operação Splay move um nó para uma posição alvo (geralmente a raiz) usando rotações duplas repetidamente. O código a seguir implementa essa operação, garantindo que o nó seja acessado frequentemente permaneça próximo à raiz.

void splay(int pos, int alvo) {
    if (!alvo) raiz = pos;
    while (arvore[pos].pai != alvo) {
        if (arvore[arvore[pos].pai].pai != alvo) {
            if (obterPosicao(pos) ^ obterPosicao(arvore[pos].pai))
                rotacaoSimples(pos);
            else
                rotacaoSimples(arvore[pos].pai);
        }
        rotacaoSimples(pos);
    }
}

Operações de Árvore de Busca Binária

As operações padrão de inserção, busca de predecessor/sucessor e remoção são semelhantes às de outras árvores balanceadas, mas seguidas de uma operação Splay no nó acessado.

Inserção

A inserção adiciona um novo nó e o move para a raiz usando Splay.

void inserir(int &pos, int val, int pai) {
    if (!pos) {
        pos = criarNo(val);
        arvore[pos].pai = pai;
        splay(pos, 0);
        return;
    }
    if (val < arvore[pos].valor)
        inserir(esq, val, pos);
    else if (val > arvore[pos].valor)
        inserir(dir, val, pos);
    else
        arvore[pos].contagem++;
    atualizarTamanho(pos);
}

Busca de Predecessor e Sucessor

Para encontrar o predecsesor ou sucessor de um valor, percorremos a árvore e aplicamos Splay no nó resultante para otimizar futuros acessos.

int obterPredecessor(int pos, int val) {
    if (!pos) return -1;
    if (arvore[pos].valor >= val) return obterPredecessor(esq, val);
    int candidato = obterPredecessor(dir, val);
    if (candidato == -1 || arvore[pos].valor > arvore[candidato].valor) {
        if (pos == raiz) splay(pos, 0);
        return pos;
    } else {
        if (candidato == raiz) splay(candidato, 0);
        return candidato;
    }
}

int obterSucessor(int pos, int val) {
    if (!pos) return -1;
    if (arvore[pos].valor <= val) return obterSucessor(dir, val);
    int candidato = obterSucessor(esq, val);
    if (candidato == -1 || arvore[pos].valor < arvore[candidato].valor) {
        if (pos == raiz) splay(pos, 0);
        return pos;
    } else {
        if (candidato == raiz) splay(candidato, 0);
        return candidato;
    }
}

Remoção

Para remover um nó, primeiro movemos ele para a raiz com Splay. Se a contagem for maior que 1, decrementamos; caso contrário, removemos o nó e mesclamos as subárvores.

Manutenção de Informações

Árvores Splay podem manter informações adicionais, como marcações de atraso para operações em intervalos. Um exemplo clássico é a inversão de intervalos, onde nós são rotulados para indicar inversões pendentes, semelhante a técnicas usadas em árvores de segmentos.

Para implementar operações como inversão de intervalos, utilizamos uma marcação de atraso que é propagada durante as rotações e acessos, garantindo que as operações sejam aplicadas de forma eficiente.

Tags: SplayTree BalancedTree TreeRotation SplayOperation BinarySearchTree

Publicado em 6-12 18:10 por Thomas