Resolução de Problemas com Busca Binária em C++

Introdução à Busca Binária em Problemas de Programação Competitiva

A busca binária é um algoritmo fundamental amplamente utilizado na programação competitiva devido à sua eficiência (complexidade de tempo logarítmica O(log N)). Este artigo explora diversas aplicações da busca binária em problemas comuns, desde a localização de elementos até a contagem de ocorrências e o processamento de consultas em sequências.

1. Encontrando Elementos Comuns em Duas Listas Ordenadas (P1571 Olhos Vermelhos da Medusa)

O primeiro desafio consiste em identificar e listar elementos de uma sequência que também estão presentes em outra sequência, que pode ser previamente ordenada para otimizar a busca. Para cada elemento da primeira sequência, verificamos sua existência na segunda, mantendo a ordem original da primeira.

Método de Implementação

Para resolver este problema de forma eficiente, a estratégia é ordenar a segunda lista de números. Em seguida, para cada número da primeira lista, realizamos uma busca binária na segunda lista ordenada. Se um número for encontrado, ele é adicionado à saída.

Código C++

#include <iostream>
#include <vector>
#include <algorithm>
#include <climits> // Para INT_MIN, INT_MAX

// Função de busca binária para encontrar um valor em um vetor ordenado.
// Retorna o índice do elemento se encontrado, -1 caso contrário.
int buscarElemento(const std::vector<int>& arrOrdenado, int valor) {
    int esq = 0; // Início do intervalo de busca
    int dir = arrOrdenado.size() - 1; // Fim do intervalo de busca

    while (esq <= dir) {
        int meio = esq + (dir - esq) / 2;
        if (arrOrdenado[meio] == valor) {
            return meio; // Elemento encontrado
        } else if (arrOrdenado[meio] < valor) {
            esq = meio + 1;
        } else {
            dir = meio - 1;
        }
    }
    return -1; // Elemento não encontrado
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);
    std::cout.tie(NULL);

    int n, m;
    std::cin >> n >> m;

    std::vector<int> listaA(n);
    for (int i = 0; i < n; ++i) {
        std::cin >> listaA[i];
    }

    std::vector<int> listaB(m);
    for (int i = 0; i < m; ++i) {
        std::cin >> listaB[i];
    }

    // Ordena a lista B para permitir a busca binária eficiente
    std::sort(listaB.begin(), listaB.end());

    for (int i = 0; i < n; ++i) {
        if (buscarElemento(listaB, listaA[i]) != -1) {
            std::cout << listaA[i] << ' ';
        }
    }
    std::cout << std::endl;

    return 0;
}

2. Localizando a Primeira Ocorrência em um Array Ordenado (P2249 【Fundamentos 13.Ex1】Busca)

Este problema pede para encontrar o índice da primeira ocorrência de um determinado valor em um array ordenado. É uma variação comum da busca binária, onde precisamos garantir que, mesmo que o valor apareça múltiplas vezes, o índice retornado seja o do primeiro.

Análise de Erro Comum: 'Segmentation Fault'

Durante a implementação, um erro frequente em C++ é o "Segmentation fault with invalid memory reference" (erro de segmentação com referência de memória inválida). Isso geralmente ocorre quando o programa tenta acessar uma área de memória à qual não tem permissão, como ao exceder os limites de um array alocado. No contexto de problemas de programação, a causa mais comum é um array declarado com tamanho insuficiente para os dados de entrada, levando a acessos fora dos limites quando o índice excede a capacidade real do array.

Método de Implementação

Utilizamos uma busca binária modificada para localizar a primeira ocorrência. A ideia é, ao encontrar uma correspondência, continuar a busca na metade esquerda para ver se existe uma ocorrência ainda mais à esquerda.

Código C++ Corrigido

#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>

// Função de busca binária para encontrar a primeira ocorrência de um valor
// em um vetor ordenado, retornando seu índice (1-based) ou -1 se não encontrado.
int buscarPrimeiraOcorrencia(const std::vector<int>& arr, int valor) {
    int esq = 0; // Início do intervalo de busca (0-based)
    int dir = arr.size() - 1; // Fim do intervalo de busca (0-based)
    int resultado_idx = -1;

    while (esq <= dir) {
        int meio = esq + (dir - esq) / 2;
        if (arr[meio] == valor) {
            resultado_idx = meio; // Armazena este índice como possível resposta
            dir = meio - 1;       // Tenta encontrar uma ocorrência ainda mais à esquerda
        } else if (arr[meio] < valor) {
            esq = meio + 1;
        } else {
            dir = meio - 1;
        }
    }
    return resultado_idx != -1 ? resultado_idx + 1 : -1; // Retorna 1-based index
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);
    std::cout.tie(NULL);

    int n;
    std::cin >> n; // Tamanho do array

    std::vector<int> dados(n);
    for (int i = 0; i < n; ++i) {
        std::cin >> dados[i];
    }

    int m;
    std::cin >> m; // Número de consultas

    for (int i = 0; i < m; ++i) {
        int consulta_valor;
        std::cin >> consulta_valor;
        std::cout << buscarPrimeiraOcorrencia(dados, consulta_valor) << ' ';
    }
    std::cout << std::endl;

    return 0;
}

3. Minimizar a Diferença Absoluta (P1678 Voluntariado para o Vestibular)

O problema consiste em, para cada aluno, encontrar a universidade cuja pontuação de corte é a mais próxima de sua própria pontuação. O objetivo é somar todas essas diferenças mínimas.

Método de Implementação

Primeiro, as pontuações de corte das universidades são ordenadas. Para cada pontuação de aluno, utilizamos a função std::lower\_bound para encontrar a primeira universidade cuja pontuação de corte é maior ou igual à pontuação do aluno. Em seguida, comparamos a diferença absoluta com esta universidade e, se houver, com a universidade imediatamente anterior na lista ordenada, escolhendo a menor diferença.

Código C++

#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath> // Para std::abs

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);
    std::cout.tie(NULL);

    int num_universidades, num_alunos;
    std::cin >> num_universidades >> num_alunos;

    std::vector<int> cortes_universidades(num_universidades);
    for (int i = 0; i < num_universidades; ++i) {
        std::cin >> cortes_universidades[i];
    }

    std::sort(cortes_universidades.begin(), cortes_universidades.end());

    long long soma_diferencas = 0;
    for (int i = 0; i < num_alunos; ++i) {
        int pontuacao_aluno;
        std::cin >> pontuacao_aluno;

        // Encontra o iterador para o primeiro elemento >= pontuacao_aluno
        auto it = std::lower_bound(cortes_universidades.begin(), cortes_universidades.end(), pontuacao_aluno);

        long long menor_diferenca;

        if (it == cortes_universidades.begin()) {
            // Se o aluno é menor ou igual ao menor corte, a única opção é o primeiro
            menor_diferenca = std::abs(pontuacao_aluno - *it);
        } else if (it == cortes_universidades.end()) {
            // Se o aluno é maior que todos os cortes, a única opção é o último
            menor_diferenca = std::abs(pontuacao_aluno - cortes_universidades.back());
        } else {
            // O aluno está entre dois cortes ou igual a um deles
            long long diff_direita = std::abs(pontuacao_aluno - *it);
            long long diff_esquerda = std::abs(pontuacao_aluno - *(--it)); // Move o iterador para o elemento anterior
            menor_diferenca = std::min(diff_direita, diff_esquerda);
        }
        soma_diferencas += menor_diferenca;
    }

    std::cout << soma_diferencas << "\n";

    return 0;
}

4. Consultando o Índice Original de Elementos Ordenados (P1918 Boliche)

Neste problema, cada "bola" (representando um valor) possui um ID único que indica sua posição original. Precisamos ser capazes de encontrar o ID original de uma bola dado seu valor, após as bolas serem reordenadas por valor.

Método de Implementação

Para manter a associação entre o valor da bola e seu ID original, definimos uma estrutura de dados que armazena ambos. Criamos um vetor dessas estruturas e o ordenamos com base nos valores das bolas. Em seguida, implementamos uma busca binária para encontrar uma bola com um valor específico e retornar seu ID original.

Código C++

#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>

// Estrutura para armazenar o valor da bola e seu ID original
struct Bola {
    int id_original;
    int valor;
};

// Função de comparação para ordenar as bolas pelo valor
bool compararBolas(const Bola& a, const Bola& b) {
    return a.valor < b.valor;
}

// Função de busca binária para encontrar o ID original de uma bola por seu valor
// Retorna o ID original se encontrada, 0 caso contrário (se o problema permite 0 como não encontrado)
int buscarIdBola(const std::vector<Bola>& bolas_ordenadas, int valor_procurado) {
    int esq = 0;
    int dir = bolas_ordenadas.size() - 1;

    while (esq <= dir) {
        int meio = esq + (dir - esq) / 2;
        if (bolas_ordenadas[meio].valor == valor_procurado) {
            return bolas_ordenadas[meio].id_original;
        } else if (bolas_ordenadas[meio].valor < valor_procurado) {
            esq = meio + 1;
        } else {
            dir = meio - 1;
        }
    }
    return 0; // Não encontrado
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);
    std::cout.tie(NULL);

    int n; // Número de bolas
    std::cin >> n;

    std::vector<Bola> bolas(n);
    for (int i = 0; i < n; ++i) {
        std::cin >> bolas[i].valor;
        bolas[i].id_original = i + 1; // IDs são 1-based
    }

    std::sort(bolas.begin(), bolas.end(), compararBolas);

    int m; // Número de consultas
    std::cin >> m;

    for (int i = 0; i < m; ++i) {
        int op;
        std::cin >> op;
        std::cout << buscarIdBola(bolas, op) << "\n";
    }

    return 0;
}

5. Contagem de Pares com Diferença Fixa (P1102 Pares A-B)

O problema é encontrar o número de pares (A, B) em um array tal que A - B = C, onde C é uma constante dada. Isso pode ser reescrito como A = B + C ou B = A - C.

Método de Implementação

A abordagem eficiente envolve ordenar o array primeiro. Para cada elemento A no array, buscmaos quantos elementos B existem tal que B = A - C. As funções std::lower\_bound e std::upper\_bound são ideais para essa tarefa. lower\_bound retorna um iterador para a primeira ocorrência do valor (ou onde ele deveria ser inserido para manter a ordem), e upper\_bound retorna um iterador para o primeiro elemento *maior* que o valor. A diferença entre esses dois iteradores nos dá o número de ocorrências do valor.

Código C++

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);
    std::cout.tie(NULL);

    int n; // Número de elementos
    long long c; // Constante C
    std::cin >> n >> c;

    std::vector<long long> elementos(n);
    for (int i = 0; i < n; ++i) {
        std::cin >> elementos[i];
    }

    std::sort(elementos.begin(), elementos.end());

    long long contador_pares = 0;
    for (int i = 0; i < n; ++i) {
        long long valor_a = elementos[i];
        long long valor_b_alvo = valor_a - c; // Queremos encontrar B tal que B = A - C

        // Encontra o início do intervalo de ocorrências de valor_b_alvo
        auto it_lower = std::lower_bound(elementos.begin(), elementos.end(), valor_b_alvo);
        // Encontra o fim do intervalo de ocorrências de valor_b_alvo
        auto it_upper = std::upper_bound(elementos.begin(), elementos.end(), valor_b_alvo);

        // A diferença entre os iteradores dá o número de ocorrências
        contador_pares += (it_upper - it_lower);
    }

    std::cout << contador_pares << "\n";

    return 0;
}

6. Processamento de Consultas em Sequência com Offset Global (B3799 [NICA #1] Sequência)

Este problema envolve uma sequência de números e dois tipos de consultas: 1. Adicionar um valor k a um offset global, que afeta todos os elementos da sequência. 2. Calcular a soma de todos os elementos a\[i\] (após aplicar o offset global) que são maiores ou iguais a zero.

Método de Implementação

Como o offset é global, não precisamos modificar cada elemento individualmente. Podemos aplicar o offset no momento da consulta tipo 2. 1. **Pré-processamento:** Ordene o array de elementos e calcule os prefix sums (somas acumuladas) para consultas eficientes de soma em intervalos. 2. **Consulta Tipo 1 (Adicionar offset):** Simplesmente atualize uma variável global offset\_acumulado. 3. **Consulta Tipo 2 (Calcular soma):** * Para cada elemento a\[i\], a condição a\[i\] + offset\_acumulado &gt;= 0 precisa ser satisfeita. Isso é equivalente a a\[i\] &gt;= -offset\_acumulado. * Use std::lower\_bound no array ordenado para encontrar o primeiro elemento a\[i\] que satisfaz a\[i\] &gt;= -offset\_acumulado. Isso nos dá um ponto de corte. * Calcule a soma dos elementos do ponto de corte até o final do array usando os prefix sums. * Para os elementos selecionados, adicione o offset\_acumulado a cada um e some. Isso pode ser feito como (soma\_dos\_elementos\_originais\_no\_intervalo) + (numero\_de\_elementos\_no\_intervalo \* offset\_acumulado).

Código C++

#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric> // Para std::accumulate (se necessário, mas prefix sums são manuais aqui)

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);
    std::cout.tie(NULL);

    int n_elementos; // Número de elementos na sequência
    int q_consultas; // Número de consultas
    std::cin >> n_elementos >> q_consultas;

    std::vector<long long> sequencia(n_elementos);
    for (int i = 0; i < n_elementos; ++i) {
        std::cin >> sequencia[i];
    }

    std::sort(sequencia.begin(), sequencia.end());

    std::vector<long long> prefix_sums(n_elementos + 1, 0);
    for (int i = 0; i < n_elementos; ++i) {
        prefix_sums[i + 1] = prefix_sums[i] + sequencia[i];
    }

    long long offset_global = 0;

    while (q_consultas--) {
        int tipo_consulta;
        std::cin >> tipo_consulta;

        if (tipo_consulta == 1) {
            long long k;
            std::cin >> k;
            offset_global += k;
        } else {
            // Consulta tipo 2: Somar elementos a[i] + offset_global >= 0
            // Equivalente a a[i] >= -offset_global

            // Encontra o primeiro elemento que satisfaz a condição
            auto it = std::lower_bound(sequencia.begin(), sequencia.end(), -offset_global);
            
            int indice_inicio_soma = std::distance(sequencia.begin(), it); // 0-based index

            if (indice_inicio_soma == n_elementos) {
                // Todos os elementos são menores que -offset_global, então a soma é 0
                std::cout << 0 << "\n";
            } else {
                long long soma_originais_intervalo = prefix_sums[n_elementos] - prefix_sums[indice_inicio_soma];
                long long num_elementos_no_intervalo = n_elementos - indice_inicio_soma;
                
                long long soma_total = soma_originais_intervalo + (num_elementos_no_intervalo * offset_global);
                std::cout << soma_total << "\n";
            }
        }
    }

    return 0;
}

Tags: C++ busca binária Algoritmos Programação Competitiva estrutura de dados

Publicado em 7-13 10:38