Algoritmos Gulosos: Resolução de Problemas de Intervalos e Partição de Strings

Removendo Intervalos Sobrepostos (LeetCode 435)

O objetivo deste problema é determinar o número mínimo de intervalos que precisam ser removidos para que os restantes não se sobreponham. A estratégia gulosa consiste em ordenar os intervalos e, sempre que houver uma colisão, optar por manter o intervalo que termina mais cedo, maximizando o espaço para os intervalos subsequentes.

Lógica de Implementação:

  • Ordene a matriz com base no ponto inicial de cada intervalo.
  • Percorra a lista comparando o início do intervalo atual com o fim do anterior.
  • Se houver sobreposição, incrementamos o contador de remoções e atualizamos o limite inferior do fim do intervalo para garantir que estamos "removendo" o que se estende mais à direita.

class Solution {
    public int eraseOverlapIntervals(int[][] blocos) {
        if (blocos.length == 0) return 0;

        // Ordenação por ponto de início
        Arrays.sort(blocos, (a, b) -> Integer.compare(a[0], b[0]));

        int totalRemovidos = 0;
        int fimAnterior = blocos[0][1];

        for (int i = 1; i < blocos.length; i++) {
            // Se o início atual for menor que o fim do anterior, há sobreposição
            if (blocos[i][0] < fimAnterior) {
                totalRemovidos++;
                // Mantemos o intervalo que termina primeiro (estratégia gulosa)
                fimAnterior = Math.min(fimAnterior, blocos[i][1]);
            } else {
                // Não há sobreposição, atualizamos o fim de referência
                fimAnterior = blocos[i][1];
            }
        }
        return totalRemovidos;
    }
}

Parttição de Etiquetas em Strings (LeetCode 763)

Neste desafio, devemos dividir uma string no maior número possível de partes, de modo que cada letra apareça em apenas uma parte. A abordagem gulosa foca em encontrar o limite mais disatnte de cada caractere encontrado até o momento.

Lógica de Implementação:

  • Primeiro, mapeamos a última posição de ocorrência de cada letra (de 'a' a 'z').
  • Percorremos a string mantendo uma variável que rastreia o ponto de fechamento necessário (o maior índice de última ocorrência entre as letras vistas na partição atual).
  • Quando o índice atual alcança esse ponto de fechamento, fechamos a partição e iniciamos uma nova.

class Solution {
    public List<Integer> partitionLabels(String s) {
        int[] ultimaPosicao = new int[26];
        char[] caracteres = s.toCharArray();

        // Mapeia o último índice de cada caractere
        for (int i = 0; i < caracteres.length; i++) {
            ultimaPosicao[caracteres[i] - 'a'] = i;
        }

        List<Integer> particoes = new ArrayList<>();
        int inicioSetor = 0;
        int limiteDireito = 0;

        for (int i = 0; i < caracteres.length; i++) {
            // Atualiza o limite necessário para incluir todas as ocorrências
            limiteDireito = Math.max(limiteDireito, ultimaPosicao[caracteres[i] - 'a']);

            // Se chegamos no limite máximo, cortamos a string
            if (i == limiteDireito) {
                particoes.add(limiteDireito - inicioSetor + 1);
                inicioSetor = i + 1;
            }
        }
        return particoes;
    }
}

Fusão de Intervalos (LeetCode 56)

Dada uma coleção de intervalos, o objetivo é mesclar todos os que se sobrepõem. Diferente do problema de remoção, aqui unificamos as faixas de valores em novos blocos contínuos.

Lógica de Implementação:

  • Ordene os intervalos pelo ponto enicial.
  • Inicie um intervalo temporário com o primeiro elemento.
  • Ao iterar, se o início do próximo intervalo for menor ou igual ao fim do intervalo atual, estendemos o fim do intervalo atual para o máximo entre os dois.
  • Caso contrário, o intervalo atual está completo; adicione-o ao resultado e comece um novo.

class Solution {
    public int[][] merge(int[][] intervalos) {
        if (intervalos.length <= 1) return intervalos;

        // Ordenação por início
        Arrays.sort(intervalos, (i1, i2) -> Integer.compare(i1[0], i2[0]));

        List<int[]> resultado = new ArrayList<>();
        int[] intervaloAtual = intervalos[0];
        resultado.add(intervaloAtual);

        for (int[] proximo : intervalos) {
            int fimAtual = intervaloAtual[1];
            int inicioProximo = proximo[0];
            int fimProximo = proximo[1];

            if (inicioProximo <= fimAtual) {
                // Existe intersecção, expandimos o intervalo atual
                intervaloAtual[1] = Math.max(fimAtual, fimProximo);
            } else {
                // Não há intersecção, avançamos para o próximo bloco
                intervaloAtual = proximo;
                resultado.add(intervaloAtual);
            }
        }

        return resultado.toArray(new int[resultado.size()][]);
    }
}

Tags: GreedyAlgorithms java DataStructures Intervals AlgorithmDesign

Publicado em 9-12 10:18