Aula de Rogério Júnior
Imagine o seguinte problema, de enunciado muito simples: Dado um vetor de n inteiros, todos menores que , imprima todos os seus elementos em ordem crescente. A entrada consistirá de duas linhas: a primeira terá um único inteiro: o valor de n, e a segunda terá os n elementos do vetor. Uma primeira ideia, que resolveria o problema seria fazer uma lista dos índices dos números que já imprimimos, depois, percorremos o vetor procurando o menor número que ainda não foi impresso e o imprimimos, repetindo o processo n vezes, até imprimirmos todos os números. Tal ideia é uma adaptação de um algoritmo de ordenação conhecido como Selection Sort. Implementar essa ideia usa apenas comandos que já conhecemos. Para fazermos uma lista eficiente, basta criarmos um vetor de inteiros de nome . No começo, todos os valores nele salvo serão 0. Toda vez que imprimimos um número salvo na posição do vetor, fazemos com que receba 1. Para isso, é bom saber que toda variável global (declarada fora da main), se não receber um valor logo na declaração, é inicializada com o valor 0, assim como um vetor global será inicializado com todos os seus valores iguais a 0. Assim, verificar se um número está na lista é verificar se "(lista[i]==1)". Para identificar o menor número do vetor usaremos as variáveis e _. A primeira começará com um valor muito grande (maior que o valor máximo de um elemento do vetor) e percorreremos todo o vetor, verificando, para cada um de seus elementos, se ele é menor que o número salvo em e se seu índice não está na lista dos já impressos. Se for, faremos receber seu valor e _ receber seu índice, e após percorrermos todos o vetor, imprimiremos o valor salvo em e adicionaremos _ à lista dos já impressos. Segue a implementação da ideia, para n 1000. Ela usará o comando . Ele tem a gramática: "#define a b", e não precisa de ";". Tal comando iria simplesmente procurar no código, a partir da linha em que foi escrito, todas as vezes que escrevi "a" e trocaria por "b". No código usarei, por exemplo, "#define MAXN 1010" e o programa substituirá todos os "MAXN"que aparecerem pelo número 1010, antes de compilar o programa. É uma questão de estética e facilidade de escrita.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <cstdio> // scanf e printf | |
// defino que MAXN é 1010 e INF é 2000000 (pois o valor máximo de um elemento é 1000000) | |
#define MAXN 1010 | |
#define INF 2000000 | |
int n, vetor[MAXN], lista[MAXN], menor, ind_menor; // declaro as variáveis que usarei | |
int main(){ | |
scanf("%d", &n); // leio o valor de n | |
for(int i=1; i<=n; i++) scanf("%d", &vetor[i]); // leio os n números do vetor | |
for(int i=1; i<=n; i++){ // para cada número a ser impresso | |
menor=INF; // faço menor começar como infinito | |
for(int j=1; j<=n; j++){ // percorro o vetor | |
if(lista[j]==0 && vetor[j]<menor){ // procurando um número menor que "menor" que não esteja na lista | |
menor=vetor[j]; // faço "menor" receber seu valor | |
ind_menor=j; // e guardo seu índice em "ind_menor" | |
} | |
} | |
printf("%d ", menor); // imprimo o menr número que achei | |
lista[ind_menor]=1; // e guardo seu índice na lista de impresos | |
} | |
printf("\n"); // imprimo a quebra de linha no fim da saída | |
} |
Outra ideia que também geralmente ocorre quando estamos começando a programar e nos deparamos com esse problema é a de ir trocando os números do vetor de lugar à medida em que encontramos um número no vetor que é maior que o que lhe sucede. Tal ideia é um algoritmo conhecido como Bubble Sort. Ou seja, salvaríamos os números da entrada em um vetor de inteiros de nome , percorreríamos ele por completo e, toda vez que encontrássemos um índice tal que , trocaríamos os valores de e , repetindo esse processo até que todo o vetor estivesse ordenado. Feito isso, imprimiríamos seus valores um a um. Para descobrir quando o vetor está ordenado, declaramos um inteiro , que começará com o valor 0. Toda vez que recomeçamos o processo de procurar "elementos invertidos" no vetor, supomos que ele está ordenado e fazemos receber 1. Se encontramos alguma inversão, além de trocar os elementos, fazemos receber 0, indicando que ainda há desordenações no vetor. Logo, devemos repetir o processo enquanto for 0, pois quando tudo estiver em ordem, ele receberá 1 no começo do loop e, como não haverá inversões, manterá este valor até o final. Segue o código:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <cstdio> // scanf e printf | |
#define MAXN 1100 // defino que MAXN é 1100 | |
int n, a, b, vetor[MAXN]; // declaro as variáveis que vou usar | |
void bubble_sort(){ // declaro a função void buble_sort | |
int ordenado=0; // inicializo "ordenado" como 0, para que o loop comece | |
while(ordenado==0){ // enquanto ordenado for 0 | |
ordenado=1; // suponho que o vetor estáordenado | |
for(int i=1; i<n; i++) // e checo para todas as posições, exceto a última | |
if(vetor[i]>vetor[i+1]){ // se não há inversão entre vetor[i] e vetor[i+1] | |
// se houver, troco os valores de vetor[i] e vetor[i+1] | |
int tmp=vetor[i]; | |
vetor[i]=vetor[i+1]; | |
vetor[i+1]=tmp; | |
ordenado=0; // e salvo que o vetor não está ordenado | |
} | |
} | |
} | |
int main(){ | |
scanf("%d", &n); // leio o valor de n | |
for(int i=1; i<=n; i++) scanf("%d", &vetor[i]); // leio os elementos do vetor | |
bubble_sort(); // chamo a bubble_sort | |
for(int i=1; i<=n; i++) printf("%d ", vetor[i]); // imprimo os elementos do vetor, que agora estarão ordenados | |
printf("\n"); // e imprimo uma quebra de linha no fim da entrada | |
return 0; | |
} |
Os dois algoritmos acima são bons, se o número de elementos a serem ordenados for pequeno. Até agora, vimos apenas como resolver problemas, mas não nos preocupamos com a eficiência dos algoritmos usados, ou seja, quão rápido eles resolvem o problema. A complexidade de um algoritmo é quantas operações ele vai fazer, ou, do que depende, proporcionalmente, essa quantidade de operações. Se o programa tiver uma complexidade muito grande, ele levará Time Limit Exceeded, que significa que o seu programa levou tempo demais para resolver o problema e por isso não foi aceito. Veja, que no primeiro código, do Selection Sort, vamos percorrer o vetor várias vezes. Se o vetor tem n posições, então percorrê-lo todo usa mais ou menos n operações, pois devemos olhar cada casa do vetor. Mais ou menos n significa que este número de operações dependerá unicamente de n, e não mais de nenhuma outra variável, e crescerá proporcionalmente a n. Além disso, o programa não o percorre só uma vez: para cada número que vou imprimir, o programa percorre o vetor inteiro procurando o menor. Assim, vou percorrer o vetor n vezes, ou seja, farei n operações n vezes, logo o programa fará mais ou menos n² operações. Isso significa que a complexidade do problema é quadrática, ou seja, o número de cálculos que ele fará é proporcional ao quadrado de uma variável. Para representar isso, usamos a notação "O(complexidade)", significando que o número de operações é proporcional à complexidade. No, caso, a complexidade do programa é O(n²).
Vamos agora analisar o segundo código, do Bubble Sort. Repare que, novamente, vou percorrer o todo o vetor e isso tem complexidade O(n). Porém vou repetir essa operação várias vezes, até que cada elemento esteja na sua posição certa de ordenação. Como os elementos "andam" uma posição por repetição e a distância máxima entre eles e a suas posições finais é no máximo n posições, o tamanho do vetor, posso ter que repetir o comando de percorrer o vetor cerca n vezes. Novamente, executo um comando de complexidade O(n) (percorrer todo o vetor) n vezes, o que gera uma complexidade de O(n²).
Para a OBI, devemos considerar, por boa prática, que o computador faz cerca de 1 milhão de operações por segundo e que o tempo limite de execução é 1 segundo. Em outras palavras bem mais simples (e até formalmente erradas mas que facilitam a compreensão), nosso código deve ter complexidade de, no máximo, O() . Se o valor de n, for menor que 1000, então nossos algoritmos que rodam em O(n²) irão se encaixar n0 limite. Mas, e se o valor de n for até ? Precisaremos de um algoritmo mais eficiente.
Existe uma ideia muito importante e imprescindível para qualquer um que queira participar de uma competição de informática: a de dividir para conquistar. A ideia é que para resolver um problema, pode ser mais fácil dividí-lo em casos menores, mais fáceis e resolvê-los um a um, e depois juntá-los para resolver o caso maior, na maioria das vezes com recursão. No caso de ordenação, pense: se eu tiver dois vetores ordenados e decidir juntá-los para fazer um vetor maior, também ordenado, não é bem mais simples? Essa é a ideia de um algoritmo conhecido chamado Merge Sort. Veja: se tenho o ordenado e o , também ordenado, e quero juntá-los para formar o , também ordenado, basta que eu percorra os dois vetores, sempre olhando para o começo deles e inserindo em o menor dentre o primeiro elemento ainda não inserido de e de . Segue uma figura que explica como juntar os vetores ordenados {1, 3, 5} e {2, 3, 4} para criarmos um vetor com os 6 elementos ordenados:
Vamos implementar a ideia! Basta usar um for para percorrer o primeiro vetor e, dentro dele, um while, que irá até o último elemento de que é menor que o elemento de que estou olhando, para que, em cada posição do eu antes adicione todos os elementos de que são menores que ele, para só então adicioná-lo a . Feito isso, basta adicionarmos todos os elementos que faltam de . Segue a implementação de um programa que dado o tamanho de um vetor seguido de seus elementos ordenados e o tamanho de outro vetor, também seguido de seus elementos ordenados, imprime o , que é a junção ordenada dos dois vetores.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <cstdio> // scanf e printf | |
#define MAX 100100 // defino que MAX é 100100 | |
int tam1, tam2, vetor1[MAX], vetor2[MAX], vetorzao[MAX]; // declaro as variáveis que vou precisar | |
void merge(){ // declaro a void merge | |
int tam=0; // declaro a int tam, o tamanho atual de vetorzao | |
int j=1; // declaro j, a posição de vetor2 que vou olhar | |
for(int i=1; i<=tam1; i++){ // para cada posição de vetor1 | |
// enquanto houver elemento em vetor2 e seu primeiro não usado for menor que o de vetor1 | |
while(j<=tam2 && vetor2[j]<vetor1[i]){ | |
tam++; // aumento o tamanho de vetorzao | |
vetorzao[tam]=vetor2[j]; // coloco nele o elemento vetor2[j] | |
j++; // e vou para o próximo elemento de vetor2 | |
} | |
//após inserir os elementos de vetor2 que são menores que o vetor1[i] | |
tam++; // aumento o tamanho de vetorzao | |
vetorzao[tam]=vetor1[i]; // e adiciono o elemento vetor1[i] | |
} | |
while(j<=tam2){ // enquanto ainda sobrarem elementos em vetor2 | |
tam++; // aumento o tamanho de vetorzao | |
vetorzao[tam]=vetor2[j]; // coloco nele o elemento vetor2[j] | |
j++; // e vou para o próximo elemento de vetor2 | |
} | |
} | |
int main(){ | |
scanf("%d", &tam1); // leio o tamanho do vetor1 | |
for(int i=1; i<=tam1; i++) scanf("%d", &vetor1[i]); // leio os elementos de vetor1 | |
scanf("%d", &tam2); // leio o tamanho de vetor2 | |
for(int i=1; i<=tam2; i++) scanf("%d", &vetor2[i]); // leio os elementos de vetor2 | |
merge(); // chamo a função merge | |
for(int i=1; i<=tam1+tam2; i++) printf("%d ", vetorzao[i]); // imprimo os valores de vetorzao | |
printf("\n"); // imprimo a quebra de linha no fim da saída | |
return 0; | |
} |
Note que a função apenas percorre dois vetores que juntos têm tamanho n, logo a complexidade dela é O(n).
Agora vamos usar a ideia de dividir para conquistar. Se sempre formos separando o vetor ao meio, ordenando as duas metades e depois juntarmos as duas com uma função semelhante à merge, ficará mais fácil pois eventualmente chegaremos a vetores de uma única posição, que já estarão ordenados! Assim, basta fazermos a função recursiva void merge_sort(int ini, int fim){} recursiva que recebe o começo e o fim do intervalo que irá ordenar (inclusive). No seu começo, ela checa se o começo é igual ao fim (intervalo de um único elemento), se for, ela simplesmente retorna, pois o intervalo já estará ordenado. Se não, ela divide o vetor mais ou menos no meio e chama a si mesma para esses novos dois intervalos com o comando "merge_sort(ini, (ini+fim)/2); merge_sort((ini+fim)/2+1, fim);". Feito isso, o vetor em questão estará dividido ao meio em dois vetores ordenados, e realizaremos os comandos da merge para criarmos um novo vetor que será a junção dos dois intervalos. Feito isso, faremos o intervalo antigo receber o vetor . Note que o caso base da recursão são os intervalos de um único elemento, onde a função retorna sem chamar a recursão, e que a árvore recursiva caminha para este caso base, pois sempre vamos dividindo o intervalo ao meio e, criando intervalos cada vez menores, eles eventualmente terão tamanho 1. Segue o código de um problema que recebe um vetor e imprime seus elementos ordenadamente através da função merge_sort.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <cstdio> // scanf e printf | |
#define MAXN 1100 // defino MAXN como 1100 | |
int n; // declaroo inteiro n | |
int vetor[MAXN], aux[MAXN]; // declaro os vetores que vou usar | |
// declaro a função void merge_sort que recebe como parâmetros os índices de início e fim do intervalo a ser ordenado | |
void merge_sort(int ini, int fim){ | |
if(ini==fim) return; // se for um intervalo de um único elemento, já está ordenado | |
// se não, divido o intervalo eo meio e ordeno as duas metades, com recursão | |
merge_sort(ini, (ini+fim)/2); | |
merge_sort((ini+fim)/2+1, fim); | |
int tam=0; // declaro a variável tam, que é o tamanho do vetor que será criado | |
int j=(ini+fim)/2+1; // declaro a variável j, o índice do segundo vetor que estou olhando | |
for(int i=ini; i<=(ini+fim)/2; i++){ // para cada elemento do primeiro intervalo | |
// enquanto o primeiro elemento não usado do segundo intervalo for menor que o do primeiro | |
while(j<=fim && vetor[j]<vetor[i]){ | |
aux[tam]=vetor[j]; // coloco nele o próximo elemento do segundo intervalo | |
tam++; // aumento o tamanho do vetor aux | |
j++; // passo para o próximo elemento do segundo intervalo | |
} | |
// feito isso | |
aux[tam]=vetor[i]; // insiro nele o próximo elemento do primeiro intervalo | |
tam++; // e aumento o tamanho do vetor | |
} | |
while(j<=fim){ // enquanto ainda houver elemento no segudo intervalo | |
aux[tam]=vetor[j]; // insiro nele o próximo elemento do segundo intervalo | |
j++; // e passo para o p´roximo elemento do segundo intervalo | |
tam++; // aumento o tamanho do vetor aux | |
} | |
// depois coloco os valores do vetor aux no intervalo que queria ordenar | |
for(int i=ini; i<=fim; i++) vetor[i]=aux[i-ini]; // note que aux está indexado de 0 a n-1 | |
} | |
int main(){ | |
scanf("%d", &n); // leio o valor de n, o tamanho do vetor | |
for(int i=1; i<=n; i++) scanf("%d", &vetor[i]); // leio os elementos do vetor | |
merge_sort(1, n); // ordeno o vetor da posição 1 até a posição n | |
for(int i=1; i<=n; i++) printf("%d ", vetor[i]); // imprimo os elementos do vetor, já ordenado | |
printf("\n"); // e imprimo a quebra de linha no fim da entrada | |
return 0; | |
} |
Para fixar ainda mais a ideia, segue uma animação da Wikipédia que simula o Merge Sort no vetor {6, 5, 3, 1 , 8, 7 , 3, 4}:
Vamos analisar, agora, a complexidade da função merge_sort. Observe a seguinte ilustração da chamada do Merge Sort no vetor usado de exemplo acima:
Observe que na linha da árvore de recursão (começando da linha 0) temos que unir vetores ordenados de tamanho . Como já vimos que juntar dois vetores ordenados tem complexidade O(n) (onde n é o tamanho do vetor final), estaremos repetindo, em cada linha, vezes uma operação de complexidade O(), o que gera, em cada linha uma complexidade igual a O() = 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, . Logo, como são linhas, repetiremos vezes uma operação de complexidade O(n), o que gera uma complexidade de (em informática, ao usarmos o termo , nos referimos a logaritmo na base 2, não na base 10). Agora, se n for , a complexidade do programa será , o que passa no tempo do corretor da OBI.
Um pouco de C++
Até agora, no curso, usamos apenas funções da linguagem C. Agora vamos ver um pouco do que o C++ tem a nos oferecer em ordenação. Na biblioteca algorithm temos uma função chamada sort, que utiliza um algoritmo conhecido com Quick Sort com muitas otimizações em sua implementação. Enfim, o importante é saber que ela ordena um vetor qualquer com complexidade média . A função recebe três parâmetros: um ponteiro para o primeiro elemento a ser ordenado, um ponteiro para o primeiro elemento que não será ordenado e a função que o usará para comparação. Em qualquer vetor, seu nome retorna um ponteiro para o elemento da posição 0 e, se somarmos algum inteiro n ao nome, teremos um ponteiro para o elemento da posição n do vetor. No caso, suponha um vetor de nome , e que queremos ordenar os elementos que estão entre as posições 1 e n (de a , inclusive). O ponteiro para o primeiro elemento a ser ordenado é "vetor+1" (que aponta para o elemento ) e o ponteiro para o primeiro elemento que não será ordenado é "vetor+n+1" (que aponta para o elemento ). O terceiro parâmetro da função pode ser omitido e, se o for, ela irá comparar o elementos com os operadores "<" e ">". Logo, para ordenar , da posição 1 à posição n, usamos o comando "sort(vetor+1, vetor+1+n);". É importante ressaltar também que, como é uma biblioteca do C++, após sua declaração, precisamos escrever o comando "using namespace std;", para indicar onde está tal biblioteca. Precisamos escrevê-lo antes de usar as funções da , portanto é bom que o façamos no começo do código, logo após as declarações de bibliotecas e 's.
A função de comparação, quando declarada como parâmetro, deve ser do tipo bool (variável que só guarda 1 bit: true ou false) receber duas variáveis do tipo das que serão ordenadas e retornar true se elas estiverem em ordem (o primeiro parâmetro deve vir antes do segundo na ordenação), e false caso contrário (o primeiro parâmetro deve vir depois do segundo, na ordenação). Suponha, por exemplo, que você tem n empresas identificadas com números de 1 a n, e, no vetor você guarda os fundos de cada uma. Agora você quer um vetor em que a posições de 1 a n guardem as identificações das empresas, ordenadas da mais rica para a mais pobre pelo valor do fundo de cada uma. A primeira coisa a se fazer é criar o vetor com as n empresas. Crie um vetor de nome e use um for para preenchê-lo, da posição 1 à posição n com todos os números de 1 a n ("for(int i=1; i<=n; i++) empr[i]=i;"). Antes da main, crie a função bool compara(int x, int y){}. Ela receberá dois inteiros x e y e retornará true se x deve vir antes de y no vetor ordenado e false, caso contrário. Como as empresas mais ricas devem vir antes, x deverá anteceder y se . Feito tudo isso, para ordenar o vetor , basta executarmos o comando "sort(empr+1, empr+n+1, compara);". A entrada consistirá de duas linha: a primeira terá um inteiro n, o número de empresas, e a segunda n inteiros: o capital de cada empresa ordenadamente, ou seja, nessa linha, o -ésimo inteiro representa o capital da empresa . A saída deve gerar uma única linha com n inteiros: os índices das empresas em ordem decrescente de capital, separados entre si por um espaço em branco. Segue o código do programa:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <cstdio> // scanf e printf | |
#include <algorithm> // sort | |
#define MAXN 100100 // defino o limite de n | |
using namespace std; // algorithm | |
int n, capital[MAXN], empr[MAXN]; // declaro a variáveis que vou usar | |
bool compara(int x, int y){ // declaro a função bool compara, que recebe dois inteiros | |
// se o capital do primeiro for maior que do segundo, estão na ordem certa | |
if(capital[x]>capital[y]) return true; | |
//se o programa chegar aqui, então a função não retornou na linha anterior | |
return false; // então o capital de x não é maior, logo x e y não estão em ordem | |
} | |
int main(){ | |
scanf("%d", &n); // leio o valor de n | |
for(int i=1; i<=n; i++) scanf("%d", &capital[i]); // leio o capital de cada empres | |
for(int i=1; i<=n; i++) empr[i]=i; // faço um vetor com o número de cada empresa | |
sort(empr+1, empr+n+1, compara); // ordeno o vetor pelo capital de cada empresa | |
for(int i=1; i<=n; i++) printf("%d ", empr[i]); // imprimo os valores no vetor | |
printf("\n"); // e imprimo a quebra de linha no fim da saída | |
return 0; | |
} |
Outro detalhe comum em problemas de ordenação é o desempate. Imagine por exemplo um problema que lhe dá n, , o número de alunos que participaram de uma prova e, nas próximas n linhas lhe dá, em cada linha, o nome do aluno, de no máximo 20 caracteres (todos letras minúsculas), um espaço em branco e a sua nota em uma prova, que é um número decimal. A saída que o programa deve gerar terá n linhas, em cada uma o nome de um aluno, ordenados de cima para baixo em ordem decrescente de nota. Ainda mais: em caso de empate de nota, o programa deve imprimir os nomes dos alunos empatados em ordem alfabética.
Vamos definir MAXN como 100100 e MAXL como 30. A primeira coisa a ser feita é declarar um vetor double nota[MAXN], que guardará as notas dos alunos e um vetor de strings char nome[MAXN][MAXL] (uma matriz de char) para guardar os nomes dos alunos. Feito isso, vamos criar um vetor int aluno[MAXN], e colocar nele os números de 1 a n. Agora basta lembrar da strcmp para criarmos nossa função de comparação. A função bool compara(int x, int y) receberá dois inteiros como parâmetros (os números dos alunos) e deve retornar true se o aluno x vem antes do y e false, caso contrário. A primeira coisa a ser verificada é a nota: deve vir antes o aluno de maior nota, o que faremos com os comandos: "if(nota[x]>nota[y]) return true; if(nota[y]>nota[x]) return false;". Se a função continuar, então não retornou em nenhum dos dois if's, logo nota[x] é igual a nota[y]. Então agora devemos comparar os nomes com a função strcmp.Lembre-se que ela retorna valor negativo se estiverem em ordem certa e positivo se estiverem em ordem errada. Basta olhar se estão na ordem certa, com o comando: "if(strcmp(nome[x], nome[y])<0) return true;", pois se a função passar desse comando e não retornar, então os alunos estão em ordem errada e basta darmos o comando "return false;". Agora podemos chamar o sort e ordenar o vetor e, depois, imprimir o nomes de seus elementos, que já estarão na ordem certa. Segue o código:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <cstdio> // scanf e printf | |
#include <algorithm> // sort | |
#include <cstring> // strcmp | |
// defino os limites de n e do tamanho da string | |
#define MAXN 100100 | |
#define MAXL 30 | |
using namespace std; // algorithm | |
int n, aluno[MAXN]; // declaro os inteiros que vou usar | |
double nota[MAXN]; // declaro o vetor de double, nota | |
char nome[MAXN][MAXL]; // declaro o vetor de strings, nome | |
bool compara(int x, int y){ // declaro a bool compara, que recebe dois inteiros | |
if(nota[x]>nota[y]) return true; // se a nota do primeiro for maior estão em ordem | |
if(nota[y]>nota[x]) return false; // se a nota do segundo for maior, não estão em ordem | |
// se o programa chegar nessa linha, então as notas são iguais, então olho para os nomes | |
if(strcmp(nome[x], nome[y])<0) return true; // se o nome do prieiro vier antes, estão em ordem | |
//se o programa chegar nessa linha, então o nome do primeiro não vem antes | |
return false; // então eles não estão em ordem | |
} | |
int main(){ | |
scanf("%d", &n); // leio o valor de n | |
for(int i=1; i<=n; i++) scanf(" %s %lf", nome[i], ¬a[i]); // leios os nomes e notas dos alunos | |
for(int i=1; i<=n; i++) aluno[i]=i; // faço o vetor aluno guardar os números de 1 a n | |
sort(aluno+1, aluno+n+1, compara); // e o ordeno segundo a função compara | |
// depois imprimo os nomes dos alunos salvos em aluno | |
for(int i=1; i<=n; i++) printf("%s\n", nome[ aluno[i] ]); // para cada i, imprimo o nome de aluno[i] | |
return 0; | |
} |
Sempre que você precisar ordenar um vetor, ou parte dele, e a complexidade passar no tempo, use o sort sem medo, ele é simples e eficiente. Isso não significa, entretanto, que você deva esquecer as funções de ordenação que resolveu hoje, pois mesmo que não as use diretamente para ordenar um vetor, as ideias que elas trazem são de extrema importância e ampla aplicação. Problemas de flutuação (troca de elementos consecutivos em uma sequência a fim de que atinjam uma ordem desejada) são geralmente resolvidos com a implementação do Bubble Sort, e a ideia de dividir para conquistar do Merge Sort é muito utilizada em vários problemas.
Um problema conhecido é o do número de inversões de um vetor. Contar o número de inversões em um vetor de tamanho n, indexado de 1 a n, é encontrar a quantidade de pares , com , de modo que o elemento de índice do vetor é maior que o elemento de índice do vetor. Ou seja, para cada elemento, devemos contar quantos elementos do vetor são menores que ele e estão à sua frente, somar tudo e imprimir a resposta. Como nos problemas de ordenação, a resposta em O(n²) é trivial, basta que, para cada elemento, percorramos toda a parte do vetor que está à sua direita em busca de elementos menores que ele ou contemos quantas trocas o Bubble Sort leva para ordenar o vetor. Novamente, esta solução está certa mas é muito ineficiente, e podemos fazer esta conta em . Você consegue ver qual das ideias vistas hoje melhor se aplica ao problema? Com certeza é a de dividir para conquistar. A função recursiva que resolve este problema difere em pouquíssimas linhas do Merge Sort. Tente resolver o problema. Se não conseguir, segue a resposta explicada, mas realmente tente fazê-lo sozinho antes de lê-la!
Para aplicar a ideia de dividir para conquistar, basta dividir um intervalo do vetor ao meio e notar que o número de inversões no intervalo todo é a soma do número de inversões em cada metade do intervalo somado ao número de inversões entre números de metades diferentes. Se ambas as metade estiverem ordenadas, o número de inversões entre elementos de intervalos diferentes fica fácil de ser calculada, bastando que vejamos, para cada índice da metade da direita, o primeiro índice da metade da esquerda que guarda um elemento maior que o elemento , pois estando a metade ordenada, todos os números à direita de serão maiores ou iguais ao elemento e, por consequência, maiores que o elemento .
Agora ficou fácil não? Basta fazermos um Merge Sort que retorna um inteiro: o número de inversões no intervalo a ser ordenado. Em um intervalo de um único elemento não há inversões, e a função retornará 0. Se o intervalo tiver mais que um elemento, declaramos a variável , que guardará o número de inversões no intervalo. Ela começará com o valor retornado pelo Merge Sort das duas metades do intervalo (soma das inversões em cada metade). Agora, basta somar a ela as inversões entre posições de metades diferentes. Para isso, quando percorrermos as metades, para juntá-las em um vetor maior, e encontramos um índice j da segunda metade com um elemento menor que um elemento de índice i da primeira metade, vamos adicionar a o valor de , pois como a primeira metade vai até o índice , esse será o número de elementos à direita do índice (), que serão inversões, somando ainda a inversão com o próprio (por isso o +1). Segue o código para melhor entendimento:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <cstdio> // scanf e printf | |
#define MAXN 100100 // defino o limite de n | |
typedef long long int lli; // defino o tipo lli como long long int | |
int n, vetor[MAXN], aux[MAXN]; // declaro as variáveis e vetores que vamos usar | |
lli merge_sort(int ini, int fim){ // declaro a função merge_sort, que agora retorna um int | |
if(ini==fim) return 0; // se o intervalo tiver um único elemento, ele não tem inversões | |
// caso o contrário, declaro a variável invers, que começa com a soma das inversões das duas metades | |
lli invers=merge_sort(ini, (ini+fim)/2) + merge_sort((ini+fim)/2+1, fim); // observe que chamei a recursão e ordenei as metades | |
int tam=0, j=(ini+fim)/2+1; // declaro tam e j, como feito no código anterior do merge_sort | |
for(int i=ini; i<=(ini+fim)/2; i++){ // para cada posição da metade da esquerda | |
while(j<=fim && vetor[j]<vetor[i]){ // procuro os elementos da metade da direita menores que i | |
// os adiciono ao vetor aux | |
aux[tam]=vetor[j]; | |
tam++; | |
j++; // passo para o próximo elemento | |
invers+=(ini+fim)/2-i+1; // e adicino o número de inversões em metades diferentes com o elemento j | |
} | |
// adiciono o elemento i | |
aux[tam]=vetor[i]; | |
tam++; | |
} | |
// adiciono o resto dos elementosda segunda metade | |
while(j<=fim){ | |
aux[tam]=vetor[j]; | |
tam++; | |
j++; | |
} | |
for(int i=ini; i<=fim; i++) vetor[i]=aux[i-ini]; // e troco os valores do vetor original pelos ordenados | |
return invers; // retorno o número de inversões calculado | |
} | |
int main(){ | |
scanf("%d", &n); // leio o valor de n | |
for(int i=1; i<=n; i++) scanf("%d", &vetor[i]); // leio os valores do vetor | |
printf("%lld\n", merge_sort(1, n)); // imprimo a quantidade de inversões do vetor | |
return 0; | |
} |
Agora que você já sabe tudo isso, tente fazer os seguintes problemas. Alguns serão só de ordenação, outros usarão as demais ideias trabalhadas hoje. Se tiver alguma dúvida na aula ou dificuldade em algum problema, clique aqui para voltar à página inicial do curso e preencher o formulário para enviar sua dúvida.
Problema 1 - Ordenação por Tamanho
Problema 2 - Fila do Recreio
Problema 3 - Spurs Rocks
Problema 4 - Altura - obs* scanf e printf são métodos rápidos de entrada e saída
Problema 5 - O Elfo das Trevas
Problema 6 - Organizador de Vagões
Problema 7- Camisetas
Problema 8 - Olimpíadas (OBI P1 fase 2 - 2009)
Problema 9 - P-Networks
Problema 10 - Balé (OBI P1 fase 2 - 2011 - um dos problemas mais difíceis da história da P1, simples depois dessa aula!) - Solução alternativa com BIT aqui! (recomendo que vejam somente ao fim do curso, pois é realmente complicado! Uma dica para o começo é não tentar entender o BIT, mas apenas aprender as operações que ele faz).
As aulas do Curso Noic de Informática são propriedade do Noic e qualquer reprodução sem autorização prévia é terminantemente proibida. Se você tem interesse em reproduzir algum material do Curso Noic de Informática para poder ministrar aulas, você pode nos contatar por esta seção de contato para que possamos fornecer materiais específicos para reprodução.