Algoritmo de Classificação Rápida (Quick Sort)
O Quick Sort é um algoritmo de classificação eficiente baseado no paradigma de dividir para conquistar. Ele seleciona um elemento como pivô e particiona o array em dois sub-arrays, um com elementos menores que o pivô e outro com elementos maiores. Em seguida, aplica recursivamente o mesmo processo aos sub-arrays.
void ordenarRapido(int arr[], int inicio, int fim) {
if (inicio >= fim) return;
// Escolhe o pivô como o elemento do meio e inicializa os ponteiros
int ptrEsquerda = inicio - 1, ptrDireita = fim + 1;
int pivot = arr[inicio + (fim - inicio) / 2]; // Evita overflow para (inicio + fim) grande
while (ptrEsquerda < ptrDireita) {
// Encontra um elemento à esquerda maior ou igual ao pivô
do ptrEsquerda++; while (arr[ptrEsquerda] < pivot);
// Encontra um elemento à direita menor ou igual ao pivô
do ptrDireita--; while (arr[ptrDireita] > pivot);
// Se os ponteiros não se cruzaram, troca os elementos
if (ptrEsquerda < ptrDireita) {
std::swap(arr[ptrEsquerda], arr[ptrDireita]);
}
}
// As chamadas recursivas particionam o array em torno do ponteiro da direita.
// O elemento no índice 'ptrDireita' agora está em sua posição final correta.
ordenarRapido(arr, inicio, ptrDireita);
ordenarRapido(arr, ptrDireita + 1, fim);
}
Algoritmo de Classificação por Fusão (Merge Sort)
O Merge Sort também é um algoritmo de dividir para conquistar. Ele divide repetidamente o array em duas metades até que cada metade contenha apenas um elemento (que é considerado classificado). Em seguida, mescla essas metades de forma ordenada até que todo o array esteja classificado.
// É comum usar um array auxiliar global ou passado como parâmetro para a fusão
// int arrayAuxiliar[TAMANHO_MAXIMO]; // Exemplo de declaração global para array auxiliar
void ordenarPorFusao(int arr[], int inicio, int fim) {
if (inicio >= fim) return;
int meio = inicio + (fim - inicio) / 2;
ordenarPorFusao(arr, inicio, meio); // Classifica a primeira metade
ordenarPorFusao(arr, meio + 1, fim); // Classifica a segunda metade
// Mescla as duas metades classificadas
// Assumindo que 'arrayAuxiliar' é um array acessível (e.g., global)
static int arrayAuxiliar[100005]; // Exemplo de declaração estática para fins de demonstração
int k = 0; // Índice para o array auxiliar
int idxEsquerda = inicio; // Ponteiro para a primeira metade
int idxDireita = meio + 1; // Ponteiro para a segunda metade
// Copia elementos para o array auxiliar em ordem classificada
while (idxEsquerda <= meio && idxDireita <= fim) {
if (arr[idxEsquerda] < arr[idxDireita]) {
arrayAuxiliar[k++] = arr[idxEsquerda++];
} else {
arrayAuxiliar[k++] = arr[idxDireita++];
}
}
// Copia os elementos restantes da primeira metade (se houver)
while (idxEsquerda <= meio) {
arrayAuxiliar[k++] = arr[idxEsquerda++];
}
// Copia os elementos restantes da segunda metade (se houver)
while (idxDireita <= fim) {
arrayAuxiliar[k++] = arr[idxDireita++];
}
// Copia os elementos classificados do array auxiliar de volta para o array original
for (int i = inicio, j = 0; i <= fim; i++, j++) {
arr[i] = arrayAuxiliar[j];
}
}
Busca Binária para Inteiros
A busca binária é um algoritmo eficiente para encontrar um elemento em um array classificado ou para determinar uma posição que satisfaça uma condição. Existem duas variações comuns, dependendo de como o intervalo é dividido e qual limite é ajustado.
Modelo 1: Busca pelo primeiro elemento que satisfaz a condição (intervalo [baixo, meio] ou [meio + 1, cima])
Este modelo é usado quando a função verifica(meio) é verdadeira para o resultado e todos os valores menores. A busca converge para o menor valor que satisfaz a condição.
// bool verifica(int valor) { /* ... */ } // Função de exemplo para verificar a condição
int buscaBinariaPrimeiro(int baixo, int cima) {
while (baixo < cima) {
int meio = baixo + (cima - baixo) / 2; // (baixo + cima) >> 1
if (verifica(meio)) {
cima = meio; // 'meio' pode ser o resultado, tenta na metade inferior
} else {
baixo = meio + 1; // 'meio' não satisfaz, resultado está na metade superior
}
}
return baixo; // Retorna o primeiro valor que satisfaz a condição
}
Modelo 2: Busca pelo último elemento que satisfaz a condição (intervalo [baixo, meio - 1] ou [meio, cima])
Este modelo é usado quando a função verifica(meio) é verdadeira para o resultado e todos os valores maiores. A busca converge para o maior valor que satisfaz a condição.
int buscaBinariaUltimo(int baixo, int cima) {
while (baixo < cima) {
int meio = baixo + (cima - baixo + 1) / 2; // Para evitar loop infinito quando baixo = meio
if (verifica(meio)) {
baixo = meio; // 'meio' pode ser o resultado, tenta na metade superior
} else {
cima = meio - 1; // 'meio' não satisfaz, resultado está na metade inferior
}
}
return baixo; // Retorna o último valor que satisfaz a condição
}
Busca Binária para Ponto Flutuante
A busca binária para números de ponto flutuante é similar à inteira, mas em vez de contar iterações, ela converge até que a diferença entre os limites superior e inferior esteja dentro de uma precisão aceitável (epsilon).
// bool verifica(double valor) { /* ... */ } // Função de exemplo para verificar a condição
double buscaBinariaPontoFlutuante(double limiteInferior, double limiteSuperior) {
const double precisao = 1e-7; // Ajuste 'precisao' conforme a necessidade do problema
while (limiteSuperior - limiteInferior > precisao) {
double meio = (limiteInferior + limiteSuperior) / 2;
if (verifica(meio)) {
limiteSuperior = meio; // 'meio' pode ser o resultado, busca na metade inferior
} else {
limiteInferior = meio; // 'meio' não satisfaz, busca na metade superior
}
}
return limiteInferior; // Retorna o valor aproximado
}
Aritmética de Alta Precisão: Soma
Para somar números inteiros muito grandes que excedem os limites dos tipos de dados padrão (como long long), podemos representá-los como vetores de dígitos (geralmente armazenados na ordem inversa para facilitar as operações).
Pré-condição: resultado = num1 + num2, onde num1 >= 0 e num2 >= 0.
std::vector<int> somaGrandesNumeros(std::vector<int> &num1, std::vector<int> &num2) {
// Garante que num1 seja o número com mais dígitos ou de tamanho igual
if (num1.size() < num2.size()) {
return somaGrandesNumeros(num2, num1);
}
std::vector<int> resultado;
int carry = 0; // Armazena o "vai um" (carry)
for (int i = 0; i < num1.size(); i++) {
carry += num1[i]; // Adiciona o dígito de num1
if (i < num2.size()) {
carry += num2[i]; // Adiciona o dígito de num2, se existir
}
resultado.push_back(carry % 10); // Adiciona o dígito da soma
carry /= 10; // Atualiza o carry
}
if (carry) { // Se ainda houver um carry no final
resultado.push_back(carry);
}
return resultado;
}
Aritmética de Alta Precisão: Subtração
A subtração de números grandes é implementada de forma similar à adição, mas lidando com "empréstimos" (borrows) entre os dígitos.
Pré-condição: diferenca = minuendo - subtraendo, onde minuendo >= subtraendo, minuendo >= 0 e subtraendo >= 0.
std::vector<int> subtraiGrandesNumeros(std::vector<int> &minuendo, std::vector<int> &subtraendo) {
std::vector<int> diferenca;
int emprestimo = 0; // Armazena o "pega emprestado" (borrow)
for (int i = 0; i < minuendo.size(); i++) {
int digitoAtual = minuendo[i] - emprestimo; // Subtrai o empréstimo anterior
if (i < subtraendo.size()) {
digitoAtual -= subtraendo[i]; // Subtrai o dígito do subtraendo, se existir
}
diferenca.push_back((digitoAtual + 10) % 10); // Adiciona o dígito resultante (garante positivo)
if (digitoAtual < 0) { // Se o resultado temporário for negativo, houve um empréstimo
emprestimo = 1;
} else {
emprestimo = 0;
}
}
// Remove zeros à esquerda (exceto se o resultado for apenas '0')
while (diferenca.size() > 1 && diferenca.back() == 0) {
diferenca.pop_back();
}
return diferenca;
}
Aritmética de Alta Precisão: Multiplicação por Inteiro Único
A multiplicação de um número grande por um inteiro de baixa precisão é realizada dígito a dígito, similar à multiplicação manual.
Pré-condição: produto = fatorGrande * fatorPequeno, onde fatorGrande >= 0 e fatorPequeno > 0.
std::vector<int> multiplicaGrandePorInt(std::vector<int> &fatorGrande, int fatorPequeno) {
std::vector<int> produto;
if (fatorPequeno == 0) return {0}; // Caso especial: multiplicação por zero
int carry = 0; // Armazena o "vai um"
for (int i = 0; i < fatorGrande.size() || carry; i++) {
if (i < fatorGrande.size()) {
carry += fatorGrande[i] * fatorPequeno; // Multiplica dígito por fatorPequeno e adiciona carry
}
produto.push_back(carry % 10); // Adiciona o dígito resultante
carry /= 10; // Atualiza o carry
}
// Remove zeros à esquerda, se houver
while (produto.size() > 1 && produto.back() == 0) {
produto.pop_back();
}
return produto;
}
Aritmética de Alta Precisão: Divisão por Inteiro Único
A divisão de um número grande por um inteiro de baixa precisão simula o processo de "divisão longa" que aprendemos na escola.
Pré-condição: dividendo / divisor = quociente ... resto, onde dividendo >= 0 e divisor > 0.
std::vector<int> divideGrandePorInt(std::vector<int> ÷ndo, int divisor, int &resto) {
std::vector<int> quociente;
resto = 0; // Inicializa o resto
// Itera os dígitos do dividendo da esquerda para a direita (do mais significativo ao menos significativo)
for (int i = dividendo.size() - 1; i >= 0; i--) {
resto = resto * 10 + dividendo[i]; // Incorpora o próximo dígito ao resto atual
quociente.push_back(resto / divisor); // Calcula o dígito do quociente
resto %= divisor; // Atualiza o resto
}
std::reverse(quociente.begin(), quociente.end()); // Os dígitos foram adicionados em ordem inversa, agora os inverte
// Remove zeros à esquerda (exceto se o quociente for apenas '0')
while (quociente.size() > 1 && quociente.back() == 0) {
quociente.pop_back();
}
return quociente;
}
Soma de Prefixos (Prefix Sum) em Uma Dimensão
A soma de prefixos é uma técnica para calcular eficientemente a soma de elementos em sub-arrays. Um array de soma de prefixos S armazena a soma cumulativa dos elementos de um array original A.
S[i] = A[0] + A[1] + ... + A[i](considerando indexação base 0)- A soma dos elementos de
A[inicio]aA[fim](enclusive) éS[fim] - S[inicio - 1]. Seinicio = 0, a soma éS[fim].
Soma de Prefixos (Prefix Sum) em Duas Dimensões
Similarmente, para matrrizes, uma matriz de soma de prefixos S armazena a soma de todos os elementos em uma sub-matriz que vai de (0,0) até (i,j).
S[i, j]representa a soma de todos os elementos na região retangular superior-esquerda até a célula(i, j).- A soma de uma sub-matriz com canto superior-esquerdo
(x1, y1)e canto inferior-direito(x2, y2)é calculada por:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1](ondeS[qualquer_indice_negativo]é considerado 0).
Array de Diferenças (Difference Array) em Uma Dimansão
O array de diferenças D é usado para realizar eficientes atualizações de intervalo em um array A. Para adicionar um valor c a todos os elementos no intervalo [l, r] (inclusive), podemos atualizar apenas dois pontos no array de diferenças:
D[l] += cD[r + 1] -= c
Após todas as atualizações, o array original A pode ser reconstruído a partir de D usando uma soma de prefixos em D.
Array de Diferenças (Difference Array) em Duas Dimensões
Para aplicar um valor c a todos os elementos de uma sub-matriz com canto superior-esquerdo (x1, y1) e canto inferior-direito (x2, y2):
D[x1, y1] += cD[x2 + 1, y1] -= cD[x1, y2 + 1] -= cD[x2 + 1, y2 + 1] += c
A matriz original pode ser reconstruída aplicando a soma de prefixos bidimensional ao array de diferenças D.
Operações Bit a Bit
Operações de bit são fundamentais para manipular bits individuais dentro de um número inteiro.
- Para encontrar o k-ésimo bit de um número
n(contando do bit 0 à direita):(n >> k) & 1 - Para obter o valor do bit menos significativo (o último '1') de um número
n:lowbit(n) = n & (-n). Esta operação é útil em estruturas de dados como Fenwick Trees (BIT).
Algoritmo de Dois Ponteiros
O algoritmo de dois ponteiros é uma técnica comum para resolver problemas que envolvem arrays ou listas, onde dois ponteiros iteram sobre a estrutura, geralmente mantendo alguma propriedade do intervalo entre eles ou comparando elementos.
void exemploDoisPonteiros(int arr[], int n) {
for (int ptrDireita = 0, ptrEsquerda = 0; ptrDireita < n; ptrDireita++) {
// Aumenta ptrEsquerda enquanto a condição é satisfeita (ou não)
// Por exemplo, para manter uma propriedade no intervalo [ptrEsquerda, ptrDireita]
while (ptrEsquerda < ptrDireita && !condicaoValida(arr, ptrEsquerda, ptrDireita)) {
ptrEsquerda++;
}
// Lógica específica do problema com o intervalo [ptrEsquerda, ptrDireita]
// Ex: calcular soma, comprimento máximo, etc.
}
}
Categorias comuns de problemas:
- Manter um intervalo ou janela em um único array ou sequência.
- Mesclar ou encontrar relações entre dois arrays ordenados (ex: fase de mesclagem do Merge Sort).
Discretização
A discretização é uma técnica para mapear valores de um grande domínio (por exemplo, coordenadas muito grandes) para um domínio menor e contíguo (como índices 0, 1, 2, ...). Isso é útil quando apenas a ordem relativa dos valores importa, e não seus valores absolutos.
std::vector<int> discretizar(const std::vector<int> &data) {
std::vector<int> valoresUnicos = data; // Copia os dados para processamento
std::sort(valoresUnicos.begin(), valoresUnicos.end()); // Ordena todos os valores
// Remove elementos duplicados, mantendo apenas valores únicos
valoresUnicos.erase(std::unique(valoresUnicos.begin(), valoresUnicos.end()), valoresUnicos.end());
return valoresUnicos;
}
// Função para encontrar o índice discretizado de um valor
// Retorna o índice baseado em 0 (ou 1, se necessário, ajustando o retorno)
int encontrarIndiceDiscretizado(int valor, const std::vector<int> &valoresUnicos) {
// std::lower_bound retorna um iterador para o primeiro elemento >= valor
auto it = std::lower_bound(valoresUnicos.begin(), valoresUnicos.end(), valor);
return std::distance(valoresUnicos.begin(), it); // Retorna o índice baseado em 0
}
Mesclagem de Intervalos
A mesclagem de intervalos é um problema comum em que uma lista de intervalos sobrepostos ou adjacentes é combinada em um conjunto mínimo de intervalos não sobrepostos.
#include <vector>
#include <algorithm> // Para std::sort, std::max
#include <utility> // Para std::pair
// Define um par de inteiros para representar um intervalo [inicio, fim]
using Intervalo = std::pair<int, int>;
std::vector<Intervalo> mesclarIntervalos(std::vector<Intervalo> &intervalos) {
std::vector<Intervalo> intervalosMesclados;
if (intervalos.empty()) {
return intervalosMesclados;
}
// Primeiro, ordena os intervalos pelo seu ponto de início
std::sort(intervalos.begin(), intervalos.end());
// Inicializa o intervalo atual com o primeiro intervalo da lista ordenada
int inicioAtual = intervalos[0].first;
int fimAtual = intervalos[0].second;
for (size_t i = 1; i < intervalos.size(); ++i) {
Intervalo proximoIntervalo = intervalos[i];
// Se o próximo intervalo se sobrepõe ou é adjacente ao intervalo atual
if (proximoIntervalo.first <= fimAtual) {
// Estende o fim do intervalo atual
fimAtual = std::max(fimAtual, proximoIntervalo.second);
} else {
// Não há sobreposição, adiciona o intervalo atual aos mesclados
intervalosMesclados.push_back({inicioAtual, fimAtual});
// Começa um novo intervalo
inicioAtual = proximoIntervalo.first;
fimAtual = proximoIntervalo.second;
}
}
// Adiciona o último intervalo que estava sendo processado
intervalosMesclados.push_back({inicioAtual, fimAtual});
return intervalosMesclados;
}