MergeSort

Divisão e conquista é um método amplamente conhecido e utilizado na programação competitiva em diversas áreas: grafos, estruturas de dados, análises de sequências, etc. A ideia é que, para resolver um problema, pode ser mais fácil dividi-lo em casos menores, mais fáceis e resolvê-los, um a um, para depois juntá-los, de modo a resolver o caso maior. Como a ideia gira em torno da divisão do problema em subcasos, este método geralmente é implementado com o auxílio de funções recursivas.

Uma das aplicações mais simples e conhecidas de Divisão e Conquista é algoritmo do Merge Sort, um tipo de ordenação com complexidade O(n log n). Suponha que tenhamos que ordenar um vetor. Já conhecemos alguns algoritmos quadráticos que resolvem o problema. Entretanto, o problema fica muito mais fácil se dividimos o vetor em duas metades, as ordenamos, e só então tivéssemos que uni-las de forma a gerar o vetor final ordenado.

Note que o problema fica muito mais fácil: basta que adicionemos um a um os elementos dos dois vetores, escolhendo sempre o menor. Isso fica fácil pois, como os vetores já estão ordenados, basta escolher o menor entre os primeiros elementos de cada vetor e depois retirá-lo. A seguinte imagem dá uma boa ilustração de com unir dois vetores ordenados em um vetor maior, também ordenado:

MERGE-SORT

MERGE-SORT

A pergunta é: com ordenar magicamente as metades do meu vetor? Basta que o Merge Sort seja uma função recursiva, para que eu chame a própria função em cada metade do vetor que quero ordenar! Se você ainda não entendeu bem o que a função deve fazer, observe esta animação da Wikipédia:

MERGESORTGIF

MERGESORTGIF

O algoritmo é simples e, para facilitar ainda mais a ordenação, usaremos o vector. No momento, você deve saber muito pouco sobre ponteiros, mas por enquanto basta a seguinte informação: se em uma função, ao invés de receber como parâmetro uma variável x, receber o seu endereço endereço &x, a função irá trabalhar diretamente com a variável original, ou seja, as mudanças que a função fizer na variável irão alterar a variável original que foi passada como parâmetro. Deste modo, se quero implementar a função merge_sort que de fato altera e ordena o vector v, ela não receberá um simples vector como parâmetro, mas o endereço do vector que quero ordenar, ou seja, a declaro da forma void merge_sort(vector &v){}. Vale ressaltar que, uma vez declarada assim, não precisamos mais nos preocupar com o &, pois passamos normalmente os parâmetros sem ele na hora da chamada da função, e ela já entenderá que estou trabalhando com o endereço da variável.

Vamos agora pensar na implementação da função merge_sort, que deve ordenar o vector v. Se v tiver um único elemento, ele já está ordenado, logo a função não precisa fazer nada, e retornamos. Caso contrário, precisamos usar da recursão para implementar a divisão e conquista. O primeiro passo é criar dois vetores auxiliares u1 e u2. No primeiro, colocaremos a primeira metade de v, e no segundo, a segunda. Mais especificamente, se v tem s elementos, u1 ficará com os de índice entre 0 e s/2-1, quanto que u2 ficará com os de índice entre s/2 e s-1. O próximo passo é ordenar u1 e u2, e faço isso chamando o merge_sort para cada um deles, com os comados merge_sort(u1) e merge_sort(u2). Agora, devo uni-los em v. Vamos declarar as variáveis ini1 e ini2, que irão guardar as primeiras posições de u1 e u2, respectivamente, que ainda não foram utilizados, começando, portando, com o valor 0.

Agora, vamos percorrer v, do começo ao fim, colocando em cada posição o menor número de u1 ou u2 que ainda não foi utilizado. Para isso, basta olharmos, para cada posição i de v que iremos preencher, quem é o menor: u1[ini1] ou u2[ini2], pois eles são os menores elementos ainda não utilizados de cada metade. Se for o menor, é simples, basta que o coloquemos na posição i de v (v[i]=u1[ini1];) indicando que o novo menor número não utilizado de u1 é o próximo, ou seja, ini1+1. Logo, basta aumentar o valor de ini1 em uma unidade (ini1++). Agimos de maneira análoga se u2[ini2] for o menor.

Note que a implementação acima pode trazer problemas quando já percorri, por exemplo, todo o vetor u1 e tento acessar sua próxima posição, pois ela não existe e acesso memória inválida. Resolver este problema é simples, basta que eu nunca percorra todo o vetor u1. Para isso, basta adicionar nele, antes de começar a uni-lo com u2 um número muito grande que certamente ficará por último e será maior que os s números colocados em v. O chamarremos de INF, e seu valor depende do tamanho dos números que podem estar em v (ele deve ser maior que qualquer um deles). Faremos o mesmo com u2.

Após percorrer todas as posições de v, ele estará completamente ordenado, e posso terminar a função. Segue seu código para melhor entendimento:

            #include <vector> // para o uso de vector
            // neste código, irei definir INF como 1 bilhão
            #define INF 1000000000
            using namespace std;
            // função merge_sort que ordena um vector v
            void merge_sort(vector<int> &v){
                // se o tamanho de v for 1, retorno a função
                if(v.size()==1) return;
                // se não
                // declaro os vetore u1 e u2
                vector<int> u1, u2;
                // e faço cada um receber uma metade de v
                for(int i=0;i<v.size()/2;i++) u1.push_back(v[i]);
                for(int i=v.size()/2;i<v.size();i++) u2.push_back(v[i]);
                // ordeno u1 e u2
                merge_sort(u1);
                merge_sort(u2);
                // e adiciono INF ao final de cada um deles
                u1.push_back(INF);
                u2.push_back(INF);
                // declaro ini1 e ini2 com valore inicial zero
                int ini1=0, ini2=0;
                // percorro cada posição de v
                for(int i=0;i<v.size();i++){
                    // se o menor não usado de u1 for menor o mesmo em u2
                    if(u1[ini1]<u2[ini2]){
                        // então o coloco em v
                        v[i]=u1[ini1];
                        // e incremento o valor de ini1
                        ini1++;
                    }
                    // caso contrário, faço o análogo com u2 e ini2
                    else{
                        v[i]=u2[ini2];
                        ini2++;
                    }
                }
                // por fim, retorno a função merge_sort
                return;
            }

Agora, façamos uma rápida análise da complexidade deste algoritmo. Para isso, observe antes a seguinte imagem, que representa as chamadas recursivas da função merge_sort em um determinado vetor:

MERGESORT_TREE

MERGESORT_TREE

Suponha agora a mesma árvore para um vetor qualquer de \(n\) posições. Observe que, na linha \(i\) da árvore de recursão (começando da linha \(0\)) temos que unir \(2^i\) vetores ordenados de tamanho \(\frac{n}{2^i}\). Como já vimos que juntar dois vetores ordenados tem complexidade \(O(n)\) (onde \(n\) é o tamanho do vetor final, pois usa um único for para preencher todo o vetor), estaremos repetindo, em cada linha, \(2^i\) vezes uma operação de complexidade \(O\left(\frac{n}{2^i}\right)\), o que gera, em cada linha uma complexidade igual a \(O\left(2^i\cdot\frac{n}{2^i}\right)=O(n)\). Mas quantas linhas a função chamará? Repare que dividiremos o vetor ao meio até que só reste \(1\) elemento em cada intervalo, logo, sendo \(m\) a quantidade de linhas, \(\frac{n}{2^m}=1\Leftrightarrow n=2^m\Leftrightarrow\log_2n=\log_2 2^m=m\Leftrightarrow m=\log_2n\). Logo, como são \(\log_2n\) linhas, repetiremos\(\log_2n\) vezes uma operação de complexidade \(O(n)\), o que gera uma complexidade de \(O(n\log n)\) (em informática, ao usarmos o termo \(\log\), nos referimos a logaritmo na base 2, não na base 10).

Agora, tente implementar esta e outras aplicações do método da Divisão e conquista. Lembre-se, novamente, que para usar um ponteiro como um parâmetro em uma função, só precisamos usar o & na hora de declarar a função, e usamos o nome normal da variável (sem o &) tanto no corpo da função como na hora de chamá-la no código.

Back to top