Um grafo é ponderado quando suas arestas possuem um peso. O que significa isso? Bom… Vamos supor que eu queira ir de um lugar pra outro, mas o mais importante pra mim não seja a distância entre eles mas o pedágio que vou ter que pagar para pegar cada aresta (estrada). Nesse caso, o peso de cada aresta seria o custo que eu tenho pra passar pela estrada. O problema então seria calcular o caminho onde eu pago menos (o caminho que tem a menor soma de preços) e não o menor caminho no grafo “não-ponderado” (onde consideramos aresta=1 e nada=0).
Neste grafo, por exemplo, o menor caminho de 0 a 3 não é a aresta 0–3, mas sim a aresta 0–2 e depois a aresta 2–3.
Para representar um grafo ponderado usando a matriz de adjacência, onde antes marcávamos “1”, marcamos o peso que temos de ir de um vértice para o outro e onde marcávamos “0” marcamos ∞ (infinito).
0
1
2
3
4
5
0
∞
∞
3
5
∞
∞
1
∞
∞
2
∞
∞
∞
2
3
2
∞
1
∞
∞
3
5
∞
1
∞
∞
∞
4
∞
∞
∞
∞
∞
7
5
∞
∞
∞
∞
7
∞
Na verdade, só fazemos isso do infinito porque neste caso iríamos querer o menor caminho e o 0 poderia atrapalhar, porque poderíamos ter um caminho sem pedágio, por exemplo, mas isso sempre depende do caso.
Usando listas de adjacência, podemos representar as ligações de cada vértice com dois vetores (um para dizer a qual ele se liga e outro o peso desta ligação) ou com um vetor de structs como:
No último artigo, conhecemos a representação chamada “grafo” da seguinte maneira:
Como todos sabemos, seria bem difícil trabalhar uma árvore assim na programação! Por isso, existem várias maneiras de representar um grafo. Nesta série só vou usar as duas mais populares:
Matriz de Adjacência
Lista de Adjacência
Poderíamos falar também sobre a Matriz de Incidência, mas eu nunca precisei utilizá-la, então prefiro só entrar nessas duas mesmo.
Cada vértice é um número
Para representar um grafo, cada vértice sempre vai ser um número. No caso de você querer representar amizade entre duas pessoas, como no exemplo do Orkut no último artigo, você cria um vetor chamado nome[] que contém o nome de cada número…
Eu
João
Maria
José
Pedro
Matriz de Adjacência
A matriz de adjacência é uma matriz de N x N (onde N é o número de vértices do grafo). Ela inicialmente é preenchida toda com 0 e quando há uma relação entre o vértice do x (número da coluna) com o do y (número da linha), matriz[x][y] é marcado um 1.
Vamos escrever este grafo aqui usando uma matriz de adjacência:
Matriz Inicial
1
2
3
4
5
1
2
3
4
5
Relações do nosso grafo
Já que o grafo não é orientado, a relação 1–2 significa 2–1 também.
1–2 (2–1)
1–3 (3–1)
2–3 (3–2)
2–4 (4–2)
4–5 (5–4)
Essas são as cinco arestas do nosso grafo. Vamos representá-la na matriz de adjacência:
1
2
3
4
5
1
1
1
2
1
1
1
3
1
1
4
1
1
5
1
Simetria
Uma das características da matriz de adjacência quando o grafo não é orientado é a simetria encontrada na “diagonal”. É interessante que se lermos uma coluna de índice v ou uma linha de índice v vamos encontrar a mesma coisa.
Problemas da OBI
Alguns desses programas são complicados, mas isto não entra em questão. Apenas dê uma olhada no recebimento da entrada deles. Todos estão em C. O que eles têm em comum é a utilização de uma matriz de adjacência para guardar o grafo (geralmente nomeada g):
* – Grafo orientado
+ – Grafo ponderado (veremos no próximo artigo)
X – Não queira ver esse problema. Nunca vi solução mais feia. Já estou providenciando uma implementação mais decente… ;)
Descobrir o grau de cada vértice
Eu não disse pra vocês que era fácil conseguir emprego no Orkut? Hehehe… Vamos pensar como podemos descobrir o grau (relembrando… o número de arestas que cada vértice tem OU o número de amigos que cada pessoa tem) na matriz de adjacências. Não basta contar quantos 1s tem na sua linha correspondente? Então façamos isto.
O custo é Θ(n2) até no melhor caso… Será que não há uma maneira mais simples de fazer isso? Imagina um negócio do tamanho do Orkut. Milhões de pessoas… Não seria bem mais fácil se ao invés de termos que passar por todos os vértices, só passarmos pelos amigos? Não poderíamos colocar somente seus amigos num vetor? É por isto que utilizamos a lista de adjacência.
Lista de Adjacência
A lista de adjacência consiste em criar um vetor para cada vértice. Este vetor contém cada vértice que o vértice “conhece” (tem uma aresta para). Geralmente é representada com uma matriz, porque cada vértice vai precisar de um vetor diferente, não é? Já que eu não vou ser duas vezes “amigo” de ninguém, então podemos fazer uma matriz de NxN.
1
2
3
4
5
1
2
3
4
5
A lista consiste em escrever para cada número de linha (= vértice) seus amigos, da seguinte maneira:
2, 3
1, 3, 4
1, 2
2, 5
4
Portanto, na programação seria representado da seguinte maneira:
1
2
3
4
5
1
2
3
2
1
3
4
3
1
2
4
2
5
5
4
O método da lista de adjacências tem várias vantagens: dependendo de como você implementa você não precisa inicializar a lista (zerar), as buscas são bem mais rápidas (você só passa pelos vértices que são “amigos” do atual) e geralmente você já tem o grau do vértice na ponta da língua (eu, pelo menos, sempre uso um vetor cont que contém o número de amigos de cada vértice para ficar mais fácil inserir o próximo elemento na lista – lista[cont[v]++]=w).
Como implementar
Vamos trabalhar com uma entrada de vários x, y, indicando relação entre x-y (y-x) até que x=0 e y=0. O grafo não é orientado.
Isso depende totalmente do problema. Em alguns casos, o mais barato é usar as duas representações juntas. As vantagens da lista de adjacências eu já escrevi [aqui][15]. A única vantagem da matriz de adjacências é você em tempo constante (não é nem linear) saber se um vértice é amigo de outro. Afinal, basta testar matriz[v][w].
Até maio do ano passado, eu não tinha aprendido isso direito e sempre usava a matriz de adjacências. Por isso muitos dos meus problemas estão resolvidos de forma pouco eficiente… e isso pode ser crucial numa prova. Por isso, saiba usar as duas formas!
Este artigo pode ter ficado meio confuso… Acho que acabei me perdendo no meio… :S Deixe um comentário se quiser discutir. ;)
Lógica da Programação
Estava me perguntando hoje: “O que é a lógica da programação?”
E não consegui obter nenhuma resposta
com exatidão.
Afinal, o que é esse negócio
que “todo programador tem que saber”
e que até cursos existem
para o profissional aprender?
Desculpem… Não pude resistir… Hehehe :D Escrevi algo parecido e coube direitinho, aí dei uma modificada pra ficar em forma de “poesia”. Mas vamos ao artigo…
Talvez minhas idéias sejam diferentes das de quem já fez curso e aprendeu formalmente esse conceito, mas hoje estava pensando e cheguei a conclusão de que a lógica da programação é a lógica da vida, nua e crua. É simplesmente a lógica matemática/filosófica que já conhecemos há tempo e que aprendemos com experiências na vida. Então pra que estudá-la como sendo algo novo?
Um exemplo de lógica da programação, no meu ponto de vista, poderia ser o seguinte:
Eu não gosto de quem mexe nas minhas coisas. E O meu irmão mexeu nas minhas coisas. Disso, deduzimos que eu não gosto do meu irmão. Não importa se é verdadeiro, é lógico. É assim que o computador pensa, não é mesmo? A lógica portanto, para mim, é uma relação entre duas ou mais coisas.
Lógica Matemática
Aí lembrei-me que da lógica matemática, algo que não aprendi formalmente, mas, bom… O Professor Vavá me mencionou no meio desse ano, num dos nossos encontros matemáticos. Ele, decepcionado, mencionou que estava ensinando “lógica” para uma turma de sexta série e passou uma prova com a seguinte questão: As duas afirmações a seguir são corretas: ‘Todo professor de matemática é bonito.’ e ‘O Fabiano é professor de matemática.’ Sabendo disso, o que podemos deduzir? E aí tinham opções como: O Fabiano é feio., O Fabiano é bonito, entre várias outras coisas. Bom… Como é possível alguém errar uma questão dessas?* E, é claro, um monte de gente errou!
Condições Compostas
Cheguei a conclusão de que isto é a lógica da programação. Que é a mesma coisa que a lógica matemática. Aí… Putz… Lembrei-me da semana passada, quando o Leandro Matos (nada contra ele, até tá certo postar artigos assim porque tem gente que consegue ter dificuldade numa coisa dessas!) postou em seu blog um artigo sobre condições compostas – conjunções. Eu, olhando um título como esses, pensei: “Puxa, que esplêndido! Do que será este artigo se trata?” e, antes até de tentar interpretar o título, inocentemente abri uma página para “aprender” sobre o uso do e em condicionais na programação. Realmente decepcionante…
Quer dizer… Poxa! Como é que é possível alguém não entender uma frase como:
n>4 e n<6
… só vai ser verdadeira se 4>nEn<6? Pô, não consigo entender. Já não tá tudo escrito? Não tem nem o que interpretar: basta ler! Então, cheguei a conclusão de que o errado é o que muitas pessoas aprendem como programação. Muitas pessoas não sabem programar. Desculpa se você é um dos caras que tem dificuldade com uma coisa como “condições compostas” (nome bonito, hein?), mas sinceramente, você não vai conseguir programar nada dessa maneira. Você não consegue transpôr um conceito básico da sua vida e das suas experiências pessoais para a programação. Cheguei a conclusão de que as pessoas não entendem o que significa o “verdadeiro” e o “falso” de um condicional, não entendem um ! (“não” no C e linguagens “derivadas”), não entendem que basta você “ler”/“interpretar” a programação. Pô… Olha só a tabela que o Leandro postou no site dele! Hehehe…
É por isso que a gente chama C, PHP e outras siglas de “linguagens”, porque elas não passam de uma maneira diferente de comunicar uma coisa, geralmente muito parecida com o nosso inglês… Aliás, muito parecida com tudo que a gente vê. Quem não entende é porque não tá acostumado com o que a gente vê todos os dias ou faz tanto parte da rotina que nem nota e nem aprende.
Nerds
Então, afinal, quem é um nerd? O programador? Ora… O programador é um ser pensante, ele conhece A Lógica, porque ele tem experiências de vida. Quem não programa direito (não conhece “A Lógica”) é que é nerd, que não tem muitas experiências na vida e não consegue absorver uma mensagem e utilizá-la em outro local. E aí eu fui pensar bem e tentei lembrar-me do que eu considero “nerd”. Sempre me vem imagens de um cara que passa a noite num quarto escuro estourando sua placa aceleradora num jogo viciante e com um fone de ouvido. Nunca me vem a mente um laboratório arrumado, claro, com um programador escrevendo linhas de código.
O exemplo do “Tio Marcos”
E aí, pra completar esse laço gigantesco, lembrei-me do meu tio Marcos. É um cara bem power, o presidente do Conselho Nacional de Psicologia. A gente sempre comenta que o cara sabe “de tudo”. Tipo assim, eu comento sobre software livre, ele responde citando o Linux e com Mozilla. Se um marinheiro vai falar com ele sobre nós de marinheiro, ele sabe um monte sobre isso também! Meu irmão fala com ele sobre música erudita e ele sabe identificar os momentos históricos e qualidades de vários compositores. E a gente sempre chega a conclusão de que ele sabe tudo isso porque ele cria relações entre as coisas, relações adquiridas através de experiências pessoais. Ele já morou na Europa duas vezes (na França e na Espanha), fala essas duas línguas (francês e espanhol), além do português do inglês e vive viajando pelo mundo. Fala com gente de todos os estilos, freqüenta uns locais bem alternativos e dessa maneira é um cara que realmente sabe de “lógica”. Ele tem uma facilidade imensa para relacionar dois conteúdos e tenho certeza que ele não teria dificuldade nenhuma para programar.
Conclusão
E cheguei a conclusão portanto de que lógica é a capacidade que a pessoa tem de relacionar dois eventos. E a lógica é um dos valores mais importantes do homem. Não serve só para a programação, mas para a matemática, para a filosofia e para qualquer coisa que você vá fazer. A lógica não passa do seu raciocínio e do seu pensamento. Aliás, acho que a “inteligência” é uma função que recebe a “lógica” como argumento… ;) Para adquirir lógica, não adianta você ler um monte de coisa; saia do computador e vá conhecer pessoas e adquirir experiências! :D
Concorda? Não concorda? Comente!
* Importante observar que o Professor Vavá nunca elogiaria o seu “Professor Rival”… Na verdade, ele colocou um aviso bem grande na questão: “Deduções lógicas podem às vezes estar erradas, como acontece, por exemplo, nesse caso.” :)
No primeiro artigo desta série, expliquei o que é um algoritmo e até citei exemplos do cotidiano, como acordar ou falar com outra pessoa. Talvez você nem tenha se dado conta, mas usando listas numeradas eu representei os algoritmos ali presentes, inclusive destacando a entrada e a saída de cada situação-problema. Porém, não é sempre assim que representamos algoritmos.
Não existe uma regra padrão para a representação de algoritmos. Cada pessoa escreve de forma diferente, mas o importante é ser legível e convencionado. Por exemplo, o livro Algoritmos: Teoria e Prática* traz nas páginas 14 e 15 convenções do pseudocódigo que utiliza no livro inteiro. Já eu, quando vou passar o mesmo algoritmo, utilizaria outro tipo de código, você pode utilizar outro, e por aí vai. Mas todos têm que ter o mesmo foco: legibilidade e fácil implementação para qualquer linguagem.
* A partir deste artigo, sempre que eu falar “Cormen”, “CLRS”, “Introduction to Algorithms” ou “Algoritmos: Teoria e Prática” estarei me referindo a um livro que é referência essencial nessa área. A versão que tenho (de onde tiro o número das páginas) é a tradução da segunda edição americana publicada pela Elsevier em 2002.
Os pseudocódigos costumam parecer um código em linguagem Pascal traduzido para a sua língua. :) Usam quase sempre estruturas que estamos acostumados a usar na programação, como se, enquanto, para, arrays, etc. Eles existem para que o algoritmo seja de fácil leitura para qualquer programador, que programe em qualquer linguagem “normal”. Veja o pseudocódigo do Insertion Sort, um algoritmo de ordenação de vetores bastante simples:
(Não se preocupe em entender o que ele faz, AINDA, pois veremos isso mais adiante)
Se você programa em qualquer linguagem, você não terá dificuldade em traduzir esse pseudocódigo para ela. O pseudocódigo é sempre uma maneira bem simples de escrever o código. Veja por exemplo, o mesmo código em C:
Você deve ter percebido que ao invés de usar três linhas com uma declaração, um condicional e um incremento, eu juntei todos num só for. Mas por isso o algoritmo é bem simples e sempre parte do princípio de que a sua linguagem é simples. Veja só a implementação do código em Pascal e compare-a com a do pseudocódigo:
for j:=2to comprimento,dobegin
elemento := vetor[j];
i := j-1;while i>0 && vetor[i]> elemento,dobegin
vetor[i+1]:= vetor[i];
i := i-1;end;
vetor[i]:= elemento;end;
Linha por linha ela é exatamente igual! A única diferença é que o pseudocódigo é traduzido… Geralmente os pseudocódigos são parecidos sempre com essa base e suas implementações não são muito diferentes. E vai ser sempre dessa maneira que eu vou representar os algoritmos (usando pseudocódigos e alguns traduzindo para C para mostrar implementações). No entanto, qualquer dúvida sobre essa representação, fique a vontade para perguntar através dos comentários.
Um algoritmo é um procedimento computacional definido que recebe um ou mais valores (entrada) e produz um ou mais valores (saída). O algoritmo é aquela fórmula matemática, aquele pedaço de código, que fica ali no meio da entrada e da saída para transformar o primeiro no segundo.
Vamos supôr por exemplo que temos a função:
f(x)=3x2
A sua entrada é o x e a sua saída é o y (ou f(x), o valor que a função retorna).
O algoritmo aqui seria o seginte:
Entrada: Receber o valor X.
Elevar X ao quadrado e guardar o número resultante como Z.
Dividir Z por 3 e guardar o número resultante como Y.
Saída: Imprimir o valor Y.
O algoritmo, portanto, é a lógica do nosso problema matemático, ou, informático. É a seqüência de passos que eu faço na minha cabeça (ou, quando é complexo, no papel) antes de escrever, em C, a função f:
intf(int x){int z, y;
z =pow(x,2);
y = z/3;return y;}
Se formos pensar, veremos que tudo o que fazemos é um algoritmo, é um procedimento que recebe uma entrada e envia uma saída. Não só no computador, mas na vida. Quando eu falo com alguém, eu espero sua entrada (o que a pessoa fala pra mim), então penso e transformo essa entrada numa saída (a resposta que vou dar pra pessoa). E assim é com várias outras coisas. Podemos dizer também que acordar é um algoritmo, por exemplo:
Entrada: Meu cérebro disse que eu estou acordado!
Percebi que acordei, mas estou com sono. Espero um pouco.
Saída: Abrir os olhos.
Saída: Se espreguiçar.
Saída: Tirar a coberta.
Saída: Sentar na cama.
Saída: Sair da cama.
Podem existir vários algoritmos diferentes para resolver o mesmo problema. No caso de Acordar, cada um acorda de forma diferente, por exemplo. Foi até um exemplo meio estranho esse aí, mas outro algoritmo poderia dar outra saída, como por exemplo simplesmente abrir os olhos e cair da cama. Ou no caso acima da função matemática, poderíamos ter um algoritmo que fizesse a mesma coisa de maneira diferente também.
O algoritmo que usamos depende principalmente do tempo que ele demora pra ser executado e a memória que ele gasta no computador. Chamamos isso de custo. Quando começarmos a ver os algoritmos de ordenação de vetores (arrays), veremos que cada algoritmo faz uma coisa diferente, mas todos servem para o mesmo propósito: ordenar o vetor. Para uma entrada pequena, um pode ser mais rápido… Para uma maior, outro. Portanto, o algoritmo que queremos usar (o tempo que ele vai demorar pra ser executado e a memória que ele vai gastar no computador) depende principalmente do tamanho da entrada (que chamamos de n e no exemplo da função seria lá em cima seria a variável x).
Na maioria dos casos (e vai ser sempre assim aqui nos meus artigos), a entrada será o teclado (por exemplo, o usuário digita o X para a função) e a saída será a tela (por exemplo, o programa imprime o resultado da função, o Y, para a tela). Essas são a entrada e saída padrão (standardinput output do C), que é usada nas olimpíadas e na maioria dos problemas que resolvemos no computador.
Em resumo, portanto, um algoritmo é a lógica de um programa computacional. Nos próximos artigos, isso deverá ser mais esclarecido e começaremos a ver algoritmos “de verdade” ;)
Qualquer dúvida, sugestão ou notificação de erro; poste um comentário ou me envie um e-mail (não só nesse, mas também nos próximos artigos). Espero que gostem.